Repository: Microsoft/VFSForGit Branch: master Commit: 7fa6733dc6e0 Files: 665 Total size: 4.2 MB Directory structure: gitextract_91dbp7i6/ ├── .azure-pipelines/ │ └── release.yml ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── build.yaml │ ├── functional-tests.yaml │ ├── release-winget.yaml │ └── scripts/ │ └── validate_release.ps1 ├── .gitignore ├── .vsconfig ├── AuthoringTests.md ├── CONTRIBUTING.md ├── Directory.Build.props ├── Directory.Build.targets ├── Directory.Packages.props ├── Directory.Solution.props ├── GVFS/ │ ├── FastFetch/ │ │ ├── CheckoutPrefetcher.cs │ │ ├── CheckoutStage.cs │ │ ├── FastFetch.csproj │ │ ├── FastFetchLibGit2Repo.cs │ │ ├── FastFetchVerb.cs │ │ ├── GitEnlistment.cs │ │ ├── Index.cs │ │ ├── IndexLock.cs │ │ ├── NativeMethods.cs │ │ ├── Program.cs │ │ └── WorkingTree.cs │ ├── GVFS/ │ │ ├── CommandLine/ │ │ │ ├── CacheServerVerb.cs │ │ │ ├── CacheVerb.cs │ │ │ ├── CloneVerb.cs │ │ │ ├── ConfigVerb.cs │ │ │ ├── DehydrateVerb.cs │ │ │ ├── DiagnoseVerb.cs │ │ │ ├── GVFSVerb.cs │ │ │ ├── HealthVerb.cs │ │ │ ├── LogVerb.cs │ │ │ ├── MountVerb.cs │ │ │ ├── PrefetchVerb.cs │ │ │ ├── RepairVerb.cs │ │ │ ├── ServiceVerb.cs │ │ │ ├── SparseVerb.cs │ │ │ ├── StatusVerb.cs │ │ │ ├── UnmountVerb.cs │ │ │ └── UpgradeVerb.cs │ │ ├── GVFS.csproj │ │ ├── InternalsVisibleTo.cs │ │ ├── Program.cs │ │ └── RepairJobs/ │ │ ├── BackgroundOperationDatabaseRepairJob.cs │ │ ├── BlobSizeDatabaseRepairJob.cs │ │ ├── GitConfigRepairJob.cs │ │ ├── GitHeadRepairJob.cs │ │ ├── GitIndexRepairJob.cs │ │ ├── RepairJob.cs │ │ ├── RepoMetadataDatabaseRepairJob.cs │ │ └── VFSForGitDatabaseRepairJob.cs │ ├── GVFS.Common/ │ │ ├── AzDevOpsOrgFromNuGetFeed.cs │ │ ├── ConcurrentHashSet.cs │ │ ├── ConsoleHelper.cs │ │ ├── Database/ │ │ │ ├── GVFSDatabase.cs │ │ │ ├── GVFSDatabaseException.cs │ │ │ ├── IDbCommandExtensions.cs │ │ │ ├── IDbConnectionFactory.cs │ │ │ ├── IGVFSConnectionPool.cs │ │ │ ├── IPlaceholderCollection.cs │ │ │ ├── IPlaceholderData.cs │ │ │ ├── ISparseCollection.cs │ │ │ ├── PlaceholderTable.cs │ │ │ ├── SparseTable.cs │ │ │ └── SqliteDatabase.cs │ │ ├── DiskLayoutUpgrades/ │ │ │ ├── DiskLayoutUpgrade.cs │ │ │ ├── DiskLayoutUpgrade_SqlitePlaceholders.cs │ │ │ └── DiskLayoutVersion.cs │ │ ├── Enlistment.cs │ │ ├── EpochConverter.cs │ │ ├── FileBasedCollection.cs │ │ ├── FileBasedCollectionException.cs │ │ ├── FileBasedDictionary.cs │ │ ├── FileBasedLock.cs │ │ ├── FileSystem/ │ │ │ ├── DirectoryItemInfo.cs │ │ │ ├── FileProperties.cs │ │ │ ├── FlushToDiskFileStream.cs │ │ │ ├── HooksInstaller.cs │ │ │ ├── IKernelDriver.cs │ │ │ ├── IPlatformFileSystem.cs │ │ │ └── PhysicalFileSystem.cs │ │ ├── GVFS.Common.csproj │ │ ├── GVFSConstants.cs │ │ ├── GVFSContext.cs │ │ ├── GVFSEnlistment.Shared.cs │ │ ├── GVFSEnlistment.cs │ │ ├── GVFSLock.Shared.cs │ │ ├── GVFSLock.cs │ │ ├── GVFSPlatform.cs │ │ ├── Git/ │ │ │ ├── DiffTreeResult.cs │ │ │ ├── EndianHelper.cs │ │ │ ├── GVFSGitObjects.cs │ │ │ ├── GitAuthentication.cs │ │ │ ├── GitConfigHelper.cs │ │ │ ├── GitConfigSetting.cs │ │ │ ├── GitCoreGVFSFlags.cs │ │ │ ├── GitIndexGenerator.cs │ │ │ ├── GitObjectContentType.cs │ │ │ ├── GitObjects.cs │ │ │ ├── GitOid.cs │ │ │ ├── GitPathConverter.cs │ │ │ ├── GitProcess.cs │ │ │ ├── GitRefs.cs │ │ │ ├── GitRepo.cs │ │ │ ├── GitSsl.cs │ │ │ ├── GitVersion.cs │ │ │ ├── HashingStream.cs │ │ │ ├── ICredentialStore.cs │ │ │ ├── IGitInstallation.cs │ │ │ ├── LibGit2Exception.cs │ │ │ ├── LibGit2Repo.cs │ │ │ ├── LibGit2RepoInvoker.cs │ │ │ ├── NoOpStream.cs │ │ │ ├── RefLogEntry.cs │ │ │ ├── RequiredGitConfig.cs │ │ │ ├── Sha1Id.cs │ │ │ └── SideChannelStream.cs │ │ ├── GitCommandLineParser.cs │ │ ├── GitStatusCache.cs │ │ ├── GitStatusCacheConfig.cs │ │ ├── HealthCalculator/ │ │ │ ├── EnlistmentHealthCalculator.cs │ │ │ ├── EnlistmentHealthData.cs │ │ │ ├── EnlistmentHydrationSummary.cs │ │ │ ├── EnlistmentPathData.cs │ │ │ └── HydrationStatusCircuitBreaker.cs │ │ ├── HeartbeatThread.cs │ │ ├── Http/ │ │ │ ├── CacheServerInfo.cs │ │ │ ├── CacheServerResolver.cs │ │ │ ├── ConfigHttpRequestor.cs │ │ │ ├── GitEndPointResponseData.cs │ │ │ ├── GitObjectsHttpException.cs │ │ │ ├── GitObjectsHttpRequestor.cs │ │ │ └── HttpRequestor.cs │ │ ├── IDiskLayoutUpgradeData.cs │ │ ├── IHeartBeatMetadataProvider.cs │ │ ├── IProcessRunner.cs │ │ ├── InternalVerbParameters.cs │ │ ├── InternalsVisibleTo.cs │ │ ├── InvalidRepoException.cs │ │ ├── LegacyPlaceholderListDatabase.cs │ │ ├── LocalCacheResolver.cs │ │ ├── LocalGVFSConfig.cs │ │ ├── Maintenance/ │ │ │ ├── GitMaintenanceQueue.cs │ │ │ ├── GitMaintenanceScheduler.cs │ │ │ ├── GitMaintenanceStep.cs │ │ │ ├── GitProcessChecker.cs │ │ │ ├── LooseObjectsStep.cs │ │ │ ├── PackfileMaintenanceStep.cs │ │ │ ├── PostFetchStep.cs │ │ │ └── PrefetchStep.cs │ │ ├── MissingTreeTracker.cs │ │ ├── ModifiedPathsDatabase.cs │ │ ├── NamedPipes/ │ │ │ ├── AllowAllLocksNamedPipeServer.cs │ │ │ ├── BrokenPipeException.cs │ │ │ ├── HydrationStatusNamedPipeMessages.cs │ │ │ ├── LockNamedPipeMessages.cs │ │ │ ├── NamedPipeClient.cs │ │ │ ├── NamedPipeMessages.cs │ │ │ ├── NamedPipeServer.cs │ │ │ ├── NamedPipeStreamReader.cs │ │ │ ├── NamedPipeStreamWriter.cs │ │ │ ├── PipeNameLengthException.cs │ │ │ └── UnstageNamedPipeMessages.cs │ │ ├── NativeMethods.Shared.cs │ │ ├── NativeMethods.cs │ │ ├── NetworkStreams/ │ │ │ ├── BatchedLooseObjectDeserializer.cs │ │ │ ├── PrefetchPacksDeserializer.cs │ │ │ └── RestrictedStream.cs │ │ ├── OrgInfoApiClient.cs │ │ ├── Paths.Shared.cs │ │ ├── Prefetch/ │ │ │ ├── BlobPrefetcher.cs │ │ │ ├── Git/ │ │ │ │ ├── DiffHelper.cs │ │ │ │ ├── PathWithMode.cs │ │ │ │ └── PrefetchGitObjects.cs │ │ │ └── Pipeline/ │ │ │ ├── BatchObjectDownloadStage.cs │ │ │ ├── Data/ │ │ │ │ ├── BlobDownloadRequest.cs │ │ │ │ ├── IndexPackRequest.cs │ │ │ │ └── TreeSearchRequest.cs │ │ │ ├── FindBlobsStage.cs │ │ │ ├── HydrateFilesStage.cs │ │ │ ├── IndexPackStage.cs │ │ │ └── PrefetchPipelineStage.cs │ │ ├── ProcessHelper.cs │ │ ├── ProcessResult.cs │ │ ├── ProcessRunnerImpl.cs │ │ ├── RepoMetadata.cs │ │ ├── RetryBackoff.cs │ │ ├── RetryCircuitBreaker.cs │ │ ├── RetryConfig.cs │ │ ├── RetryWrapper.cs │ │ ├── RetryableException.cs │ │ ├── ReturnCode.cs │ │ ├── SHA1Util.cs │ │ ├── ServerGVFSConfig.cs │ │ ├── StreamUtil.cs │ │ ├── Tracing/ │ │ │ ├── DiagnosticConsoleEventListener.cs │ │ │ ├── EventLevel.cs │ │ │ ├── EventListener.cs │ │ │ ├── EventMetadata.cs │ │ │ ├── EventOpcode.cs │ │ │ ├── IEventListenerEventSink.cs │ │ │ ├── IQueuedPipeStringWriterEventSink.cs │ │ │ ├── ITracer.cs │ │ │ ├── JsonTracer.cs │ │ │ ├── Keywords.cs │ │ │ ├── LogFileEventListener.cs │ │ │ ├── NullTracer.cs │ │ │ ├── PrettyConsoleEventListener.cs │ │ │ ├── QueuedPipeStringWriter.cs │ │ │ ├── TelemetryDaemonEventListener.cs │ │ │ ├── TraceEventMessage.cs │ │ │ └── TracingConstants.cs │ │ ├── VersionResponse.cs │ │ ├── WorktreeCommandParser.cs │ │ └── X509Certificates/ │ │ ├── CertificateVerifier.cs │ │ └── SystemCertificateStore.cs │ ├── GVFS.FunctionalTests/ │ │ ├── AssemblyAttributes.cs │ │ ├── Categories.cs │ │ ├── FileSystemRunners/ │ │ │ ├── BashRunner.cs │ │ │ ├── CmdRunner.cs │ │ │ ├── FileSystemRunner.cs │ │ │ ├── PowerShellRunner.cs │ │ │ ├── ShellRunner.cs │ │ │ └── SystemIORunner.cs │ │ ├── GVFS.FunctionalTests.csproj │ │ ├── GVFSTestConfig.cs │ │ ├── GlobalSetup.cs │ │ ├── Program.cs │ │ ├── Settings.cs │ │ ├── Should/ │ │ │ └── FileSystemShouldExtensions.cs │ │ ├── Tests/ │ │ │ ├── DiskLayoutVersionTests.cs │ │ │ ├── EnlistmentPerFixture/ │ │ │ │ ├── BasicFileSystemTests.cs │ │ │ │ ├── CacheServerTests.cs │ │ │ │ ├── CloneTests.cs │ │ │ │ ├── DehydrateTests.cs │ │ │ │ ├── DiagnoseTests.cs │ │ │ │ ├── GVFSLockTests.cs │ │ │ │ ├── GVFSUpgradeReminderTests.cs │ │ │ │ ├── GitBlockCommandsTests.cs │ │ │ │ ├── GitCorruptObjectTests.cs │ │ │ │ ├── GitFilesTests.cs │ │ │ │ ├── GitMoveRenameTests.cs │ │ │ │ ├── GitReadAndGitLockTests.cs │ │ │ │ ├── HealthTests.cs │ │ │ │ ├── MountTests.cs │ │ │ │ ├── MoveRenameFileTests.cs │ │ │ │ ├── MoveRenameFileTests_2.cs │ │ │ │ ├── MoveRenameFolderTests.cs │ │ │ │ ├── MultithreadedReadWriteTests.cs │ │ │ │ ├── ParallelHydrationTests.cs │ │ │ │ ├── PrefetchVerbTests.cs │ │ │ │ ├── PrefetchVerbWithoutSharedCacheTests.cs │ │ │ │ ├── SparseTests.cs │ │ │ │ ├── StatusVerbTests.cs │ │ │ │ ├── SymbolicLinkTests.cs │ │ │ │ ├── TestsWithEnlistmentPerFixture.cs │ │ │ │ ├── UnmountTests.cs │ │ │ │ ├── UpdatePlaceholderTests.cs │ │ │ │ ├── WorkingDirectoryTests.cs │ │ │ │ └── WorktreeTests.cs │ │ │ ├── EnlistmentPerTestCase/ │ │ │ │ ├── DiskLayoutUpgradeTests.cs │ │ │ │ ├── LooseObjectStepTests.cs │ │ │ │ ├── ModifiedPathsTests.cs │ │ │ │ ├── PersistedWorkingDirectoryTests.cs │ │ │ │ ├── RepairTests.cs │ │ │ │ └── TestsWithEnlistmentPerTestCase.cs │ │ │ ├── FastFetchTests.cs │ │ │ ├── GVFSVerbTests.cs │ │ │ ├── GitCommands/ │ │ │ │ ├── AddStageTests.cs │ │ │ │ ├── CheckoutTests.cs │ │ │ │ ├── CherryPickConflictTests.cs │ │ │ │ ├── CorruptionReproTests.cs │ │ │ │ ├── CreatePlaceholderTests.cs │ │ │ │ ├── DeleteEmptyFolderTests.cs │ │ │ │ ├── EnumerationMergeTest.cs │ │ │ │ ├── GitCommandsTests.cs │ │ │ │ ├── GitRepoTests.cs │ │ │ │ ├── HashObjectTests.cs │ │ │ │ ├── MergeConflictTests.cs │ │ │ │ ├── RebaseConflictTests.cs │ │ │ │ ├── RebaseTests.cs │ │ │ │ ├── ResetHardTests.cs │ │ │ │ ├── ResetMixedTests.cs │ │ │ │ ├── ResetSoftTests.cs │ │ │ │ ├── RmTests.cs │ │ │ │ ├── StatusTests.cs │ │ │ │ ├── UpdateIndexTests.cs │ │ │ │ └── UpdateRefTests.cs │ │ │ ├── MultiEnlistmentTests/ │ │ │ │ ├── ConfigVerbTests.cs │ │ │ │ ├── ServiceVerbTests.cs │ │ │ │ ├── SharedCacheTests.cs │ │ │ │ └── TestsWithMultiEnlistment.cs │ │ │ ├── PrintTestCaseStats.cs │ │ │ └── TestResultsHelper.cs │ │ ├── Tools/ │ │ │ ├── ControlGitRepo.cs │ │ │ ├── FileSystemHelpers.cs │ │ │ ├── GVFSFunctionalTestEnlistment.cs │ │ │ ├── GVFSHelpers.cs │ │ │ ├── GVFSProcess.cs │ │ │ ├── GVFSServiceProcess.cs │ │ │ ├── GitHelpers.cs │ │ │ ├── GitProcess.cs │ │ │ ├── NativeMethods.cs │ │ │ ├── ProcessHelper.cs │ │ │ ├── ProcessResult.cs │ │ │ ├── ProjFSFilterInstaller.cs │ │ │ ├── RepositoryHelpers.cs │ │ │ └── TestConstants.cs │ │ └── Windows/ │ │ ├── TestData/ │ │ │ └── BackgroundGitUpdates/ │ │ │ ├── PersistentDictionary.edb │ │ │ ├── PersistentDictionary.jfm │ │ │ ├── epc.chk │ │ │ ├── epcres00001.jrs │ │ │ └── epcres00002.jrs │ │ ├── Tests/ │ │ │ ├── JunctionAndSubstTests.cs │ │ │ ├── ServiceTests.cs │ │ │ ├── SharedCacheUpgradeTests.cs │ │ │ ├── WindowsDiskLayoutUpgradeTests.cs │ │ │ ├── WindowsFileSystemTests.cs │ │ │ ├── WindowsFolderUsnUpdate.cs │ │ │ ├── WindowsTombstoneTests.cs │ │ │ └── WindowsUpdatePlaceholderTests.cs │ │ └── Tools/ │ │ └── RegistryHelper.cs │ ├── GVFS.FunctionalTests.LockHolder/ │ │ ├── AcquireGVFSLock.cs │ │ ├── GVFS.FunctionalTests.LockHolder.csproj │ │ └── Program.cs │ ├── GVFS.Hooks/ │ │ ├── GVFS.Hooks.csproj │ │ ├── HooksPlatform/ │ │ │ └── GVFSHooksPlatform.cs │ │ ├── KnownGitCommands.cs │ │ ├── Program.Unstage.cs │ │ ├── Program.Worktree.cs │ │ ├── Program.cs │ │ └── UnstageCommandParser.cs │ ├── GVFS.Installers/ │ │ ├── GVFS.Installers.csproj │ │ ├── Setup.iss │ │ ├── info.bat │ │ └── install.bat │ ├── GVFS.MSBuild/ │ │ ├── CompileTemplatedFile.cs │ │ ├── GVFS.MSBuild.csproj │ │ ├── GVFS.targets │ │ ├── GVFS.tasks │ │ ├── GenerateGVFSConstants.cs │ │ ├── GenerateGVFSVersionHeader.cs │ │ └── GenerateWindowsAppManifest.cs │ ├── GVFS.Mount/ │ │ ├── GVFS.Mount.csproj │ │ ├── InProcessMount.cs │ │ ├── InProcessMountVerb.cs │ │ ├── MountAbortedException.cs │ │ └── Program.cs │ ├── GVFS.NativeHooks.Common/ │ │ ├── common.h │ │ └── common.windows.cpp │ ├── GVFS.NativeTests/ │ │ ├── FileUtils.cpp │ │ ├── FileUtils.h │ │ ├── GVFS.NativeTests.vcxproj │ │ ├── GVFS.NativeTests.vcxproj.filters │ │ ├── ReadMe.txt │ │ ├── include/ │ │ │ ├── NtFunctions.h │ │ │ ├── SafeHandle.h │ │ │ ├── SafeOverlapped.h │ │ │ ├── Should.h │ │ │ ├── TestException.h │ │ │ ├── TestHelpers.h │ │ │ ├── TestVerifiers.h │ │ │ ├── prjlib_internal.h │ │ │ ├── prjlibp.h │ │ │ ├── stdafx.h │ │ │ └── targetver.h │ │ ├── interface/ │ │ │ ├── NtQueryDirectoryFileTests.h │ │ │ ├── PlaceholderUtils.h │ │ │ ├── ProjFS_BugRegressionTest.h │ │ │ ├── ProjFS_DeleteFileTest.h │ │ │ ├── ProjFS_DeleteFolderTest.h │ │ │ ├── ProjFS_DirEnumTest.h │ │ │ ├── ProjFS_FileAttributeTest.h │ │ │ ├── ProjFS_FileEATest.h │ │ │ ├── ProjFS_FileOperationTest.h │ │ │ ├── ProjFS_MoveFileTest.h │ │ │ ├── ProjFS_MoveFolderTest.h │ │ │ ├── ProjFS_MultiThreadsTest.h │ │ │ ├── ProjFS_SetLinkTest.h │ │ │ ├── ReadAndWriteTests.h │ │ │ └── TrailingSlashTests.h │ │ ├── packages.config │ │ └── source/ │ │ ├── NtFunctions.cpp │ │ ├── NtQueryDirectoryFileTests.cpp │ │ ├── PlaceholderUtils.cpp │ │ ├── ProjFS_BugRegressionTest.cpp │ │ ├── ProjFS_DeleteFileTest.cpp │ │ ├── ProjFS_DeleteFolderTest.cpp │ │ ├── ProjFS_DirEnumTest.cpp │ │ ├── ProjFS_FileAttributeTest.cpp │ │ ├── ProjFS_FileEATest.cpp │ │ ├── ProjFS_FileOperationTest.cpp │ │ ├── ProjFS_MoveFileTest.cpp │ │ ├── ProjFS_MoveFolderTest.cpp │ │ ├── ProjFS_MultiThreadTest.cpp │ │ ├── ProjFS_SetLinkTest.cpp │ │ ├── ReadAndWriteTests.cpp │ │ ├── TrailingSlashTests.cpp │ │ ├── dllmain.cpp │ │ └── stdafx.cpp │ ├── GVFS.Payload/ │ │ ├── GVFS.Payload.csproj │ │ └── layout.bat │ ├── GVFS.PerfProfiling/ │ │ ├── GVFS.PerfProfiling.csproj │ │ ├── ProfilingEnvironment.cs │ │ └── Program.cs │ ├── GVFS.Platform.Windows/ │ │ ├── ActiveEnumeration.cs │ │ ├── CurrentUser.cs │ │ ├── DiskLayoutUpgrades/ │ │ │ ├── DiskLayout14to15Upgrade_ModifiedPaths.cs │ │ │ ├── DiskLayout15to16Upgrade_GitStatusCache.cs │ │ │ ├── DiskLayout16to17Upgrade_FolderPlaceholderValues.cs │ │ │ ├── DiskLayout17to18Upgrade_TombstoneFolderPlaceholders.cs │ │ │ ├── DiskLayout18to19Upgrade_SqlitePlacholders.cs │ │ │ └── WindowsDiskLayoutUpgradeData.cs │ │ ├── GVFS.Platform.Windows.csproj │ │ ├── HResultExtensions.cs │ │ ├── PatternMatcher.cs │ │ ├── PlatformLoader.Windows.cs │ │ ├── ProjFSFilter.cs │ │ ├── Readme.md │ │ ├── WindowsFileBasedLock.cs │ │ ├── WindowsFileSystem.Shared.cs │ │ ├── WindowsFileSystem.cs │ │ ├── WindowsFileSystemVirtualizer.cs │ │ ├── WindowsGitHooksInstaller.cs │ │ ├── WindowsGitInstallation.cs │ │ ├── WindowsPhysicalDiskInfo.cs │ │ ├── WindowsPlatform.Shared.cs │ │ └── WindowsPlatform.cs │ ├── GVFS.PostIndexChangedHook/ │ │ ├── GVFS.PostIndexChangedHook.vcxproj │ │ ├── GVFS.PostIndexChangedHook.vcxproj.filters │ │ ├── Version.rc │ │ ├── main.cpp │ │ ├── resource.h │ │ ├── stdafx.cpp │ │ ├── stdafx.h │ │ └── targetver.h │ ├── GVFS.ReadObjectHook/ │ │ ├── GVFS.ReadObjectHook.vcxproj │ │ ├── GVFS.ReadObjectHook.vcxproj.filters │ │ ├── Version.rc │ │ ├── main.cpp │ │ ├── packet.cpp │ │ ├── packet.h │ │ ├── resource.h │ │ ├── stdafx.cpp │ │ ├── stdafx.h │ │ └── targetver.h │ ├── GVFS.Service/ │ │ ├── Configuration.cs │ │ ├── GVFS.Service.csproj │ │ ├── GVFSMountProcess.cs │ │ ├── GVFSService.Windows.cs │ │ ├── Handlers/ │ │ │ ├── EnableAndAttachProjFSHandler.cs │ │ │ ├── GetActiveRepoListHandler.cs │ │ │ ├── INotificationHandler.cs │ │ │ ├── MessageHandler.cs │ │ │ ├── NotificationHandler.cs │ │ │ ├── RegisterRepoHandler.cs │ │ │ ├── RequestHandler.Windows.cs │ │ │ ├── RequestHandler.cs │ │ │ └── UnregisterRepoHandler.cs │ │ ├── IRepoMounter.cs │ │ ├── IRepoRegistry.cs │ │ ├── Program.cs │ │ ├── RepoRegistration.cs │ │ └── RepoRegistry.cs │ ├── GVFS.Tests/ │ │ ├── DataSources.cs │ │ ├── GVFS.Tests.csproj │ │ ├── NUnitRunner.cs │ │ └── Should/ │ │ ├── EnumerableShouldExtensions.cs │ │ ├── StringExtensions.cs │ │ ├── StringShouldExtensions.cs │ │ └── ValueShouldExtensions.cs │ ├── GVFS.UnitTests/ │ │ ├── Category/ │ │ │ └── CategoryConstants.cs │ │ ├── CommandLine/ │ │ │ ├── CacheVerbTests.cs │ │ │ └── HooksInstallerTests.cs │ │ ├── Common/ │ │ │ ├── AzDevOpsOrgFromNuGetFeedTests.cs │ │ │ ├── BackgroundTaskQueueTests.cs │ │ │ ├── CacheServerResolverTests.cs │ │ │ ├── Database/ │ │ │ │ ├── GVFSDatabaseTests.cs │ │ │ │ ├── PlaceholderTableTests.cs │ │ │ │ ├── SparseTableTests.cs │ │ │ │ └── TableTests.cs │ │ │ ├── EnlistmentHydrationSummaryTests.cs │ │ │ ├── EpochConverterTests.cs │ │ │ ├── FileBasedDictionaryTests.cs │ │ │ ├── GVFSEnlistmentHealthTests.cs │ │ │ ├── GVFSEnlistmentTests.cs │ │ │ ├── GVFSLockTests.cs │ │ │ ├── Git/ │ │ │ │ ├── GitSslTests.cs │ │ │ │ └── Sha1IdTests.cs │ │ │ ├── GitCommandLineParserTests.cs │ │ │ ├── GitConfigHelperTests.cs │ │ │ ├── GitObjectsTests.cs │ │ │ ├── GitPathConverterTests.cs │ │ │ ├── GitStatusCacheTests.cs │ │ │ ├── GitVersionTests.cs │ │ │ ├── HydrationStatusCircuitBreakerTests.cs │ │ │ ├── HydrationStatusErrorPathTests.cs │ │ │ ├── JsonTracerTests.cs │ │ │ ├── LegacyPlaceholderDatabaseTests.cs │ │ │ ├── LibGit2RepoInvokerTests.cs │ │ │ ├── LibGit2RepoSafeDirectoryTests.cs │ │ │ ├── MissingTreeTrackerTests.cs │ │ │ ├── ModifiedPathsDatabaseTests.cs │ │ │ ├── NamedPipeStreamReaderWriterTests.cs │ │ │ ├── NamedPipeTests.cs │ │ │ ├── OrgInfoApiClientTests.cs │ │ │ ├── PathsTests.cs │ │ │ ├── PhysicalFileSystemDeleteTests.cs │ │ │ ├── RefLogEntryTests.cs │ │ │ ├── RetryBackoffTests.cs │ │ │ ├── RetryConfigTests.cs │ │ │ ├── RetryWrapperTests.cs │ │ │ ├── SHA1UtilTests.cs │ │ │ ├── WorktreeCommandParserTests.cs │ │ │ ├── WorktreeEnlistmentTests.cs │ │ │ ├── WorktreeInfoTests.cs │ │ │ └── WorktreeNestedPathTests.cs │ │ ├── Data/ │ │ │ ├── backward.txt │ │ │ ├── caseChange.txt │ │ │ ├── forward.txt │ │ │ └── index_v4 │ │ ├── GVFS.UnitTests.csproj │ │ ├── Git/ │ │ │ ├── GVFSGitObjectsTests.cs │ │ │ ├── GitAuthenticationTests.cs │ │ │ ├── GitObjectsTests.cs │ │ │ └── GitProcessTests.cs │ │ ├── Hooks/ │ │ │ ├── PostIndexChangedHookTests.cs │ │ │ └── UnstageTests.cs │ │ ├── Maintenance/ │ │ │ ├── GitMaintenanceQueueTests.cs │ │ │ ├── GitMaintenanceStepTests.cs │ │ │ ├── LooseObjectStepTests.cs │ │ │ ├── PackfileMaintenanceStepTests.cs │ │ │ └── PostFetchStepTests.cs │ │ ├── Mock/ │ │ │ ├── Common/ │ │ │ │ ├── MockFileBasedLock.cs │ │ │ │ ├── MockGVFSEnlistment.cs │ │ │ │ ├── MockGitStatusCache.cs │ │ │ │ ├── MockLocalGVFSConfig.cs │ │ │ │ ├── MockLocalGVFSConfigBuilder.cs │ │ │ │ ├── MockPhysicalGitObjects.cs │ │ │ │ ├── MockPlatform.cs │ │ │ │ ├── MockTracer.cs │ │ │ │ └── Tracing/ │ │ │ │ └── MockListener.cs │ │ │ ├── FileSystem/ │ │ │ │ ├── ConfigurableFileSystem.cs │ │ │ │ ├── MockDirectory.cs │ │ │ │ ├── MockFile.cs │ │ │ │ ├── MockFileSystem.cs │ │ │ │ ├── MockFileSystemCallbacks.cs │ │ │ │ ├── MockFileSystemWithCallbacks.cs │ │ │ │ └── MockPlatformFileSystem.cs │ │ │ ├── Git/ │ │ │ │ ├── MockBatchHttpGitObjects.cs │ │ │ │ ├── MockGVFSGitObjects.cs │ │ │ │ ├── MockGitInstallation.cs │ │ │ │ ├── MockGitProcess.cs │ │ │ │ ├── MockGitRepo.cs │ │ │ │ ├── MockHttpGitObjects.cs │ │ │ │ └── MockLibGit2Repo.cs │ │ │ ├── MockCacheServerInfo.cs │ │ │ ├── MockTextWriter.cs │ │ │ ├── ReusableMemoryStream.cs │ │ │ └── Virtualization/ │ │ │ ├── Background/ │ │ │ │ └── MockBackgroundTaskManager.cs │ │ │ ├── BlobSize/ │ │ │ │ └── MockBlobSizesDatabase.cs │ │ │ ├── FileSystem/ │ │ │ │ └── MockFileSystemVirtualizer.cs │ │ │ └── Projection/ │ │ │ └── MockGitIndexProjection.cs │ │ ├── Prefetch/ │ │ │ ├── BatchObjectDownloadStageTests.cs │ │ │ ├── BlobPrefetcherTests.cs │ │ │ ├── DiffHelperTests.cs │ │ │ ├── DiffTreeResultTests.cs │ │ │ ├── PrefetchPacksDeserializerTests.cs │ │ │ └── PrefetchTracingTests.cs │ │ ├── Program.cs │ │ ├── Readme.md │ │ ├── Service/ │ │ │ └── RepoRegistryTests.cs │ │ ├── Setup.cs │ │ ├── Tracing/ │ │ │ ├── EventListenerTests.cs │ │ │ ├── QueuedPipeStringWriterTests.cs │ │ │ └── TelemetryDaemonEventListenerTests.cs │ │ ├── Virtual/ │ │ │ ├── CommonRepoSetup.cs │ │ │ ├── FileSystemVirtualizerTester.cs │ │ │ └── TestsWithCommonRepo.cs │ │ ├── Virtualization/ │ │ │ ├── FileSystemCallbacksTests.cs │ │ │ └── Projection/ │ │ │ ├── GitIndexEntryTests.cs │ │ │ ├── LazyUTF8StringTests.cs │ │ │ ├── ObjectPoolTests.cs │ │ │ └── SortedFolderEntriesTests.cs │ │ └── Windows/ │ │ ├── CommandLine/ │ │ │ └── SparseVerbTests.cs │ │ ├── Mock/ │ │ │ ├── MockVirtualizationInstance.cs │ │ │ ├── MockWriteBuffer.cs │ │ │ └── WindowsFileSystemVirtualizerTester.cs │ │ ├── Platform/ │ │ │ └── ProjFSFilterTests.cs │ │ ├── Virtualization/ │ │ │ ├── ActiveEnumerationTests.cs │ │ │ ├── PatternMatcherTests.cs │ │ │ └── WindowsFileSystemVirtualizerTests.cs │ │ └── WindowsFileBasedLockTests.cs │ ├── GVFS.VirtualFileSystemHook/ │ │ ├── GVFS.VirtualFileSystemHook.vcxproj │ │ ├── GVFS.VirtualFileSystemHook.vcxproj.filters │ │ ├── Version.rc │ │ ├── main.cpp │ │ ├── resource.h │ │ ├── stdafx.cpp │ │ ├── stdafx.h │ │ └── targetver.h │ ├── GVFS.Virtualization/ │ │ ├── Background/ │ │ │ ├── BackgroundFileSystemTaskRunner.cs │ │ │ ├── FileSystemTask.cs │ │ │ ├── FileSystemTaskQueue.cs │ │ │ └── FileSystemTaskResult.cs │ │ ├── BlobSize/ │ │ │ ├── BlobSizes.cs │ │ │ └── BlobSizesException.cs │ │ ├── FileSystem/ │ │ │ ├── FSResult.cs │ │ │ ├── FileSystemResult.cs │ │ │ ├── FileSystemVirtualizer.cs │ │ │ ├── UpdateFailureReason.cs │ │ │ └── UpdatePlaceholderType.cs │ │ ├── FileSystemCallbacks.cs │ │ ├── GVFS.Virtualization.csproj │ │ ├── InternalsVisibleTo.cs │ │ └── Projection/ │ │ ├── GitIndexProjection.FileData.cs │ │ ├── GitIndexProjection.FileTypeAndMode.cs │ │ ├── GitIndexProjection.FolderData.cs │ │ ├── GitIndexProjection.FolderEntryData.cs │ │ ├── GitIndexProjection.GitIndexEntry.cs │ │ ├── GitIndexProjection.GitIndexParser.cs │ │ ├── GitIndexProjection.LazyUTF8String.cs │ │ ├── GitIndexProjection.ObjectPool.cs │ │ ├── GitIndexProjection.PoolAllocationMultipliers.cs │ │ ├── GitIndexProjection.SortedFolderEntries.cs │ │ ├── GitIndexProjection.SparseFolder.cs │ │ ├── GitIndexProjection.cs │ │ ├── IProfilerOnlyIndexProjection.cs │ │ ├── ProjectedFileInfo.cs │ │ ├── Readme.md │ │ └── SizesUnavailableException.cs │ └── GitHooksLoader/ │ ├── GitHooksLoader.cpp │ ├── GitHooksLoader.vcxproj │ ├── GitHooksLoader.vcxproj.filters │ ├── Version.rc │ ├── resource.h │ ├── stdafx.cpp │ ├── stdafx.h │ └── targetver.h ├── GVFS.sln ├── GvFlt_EULA.md ├── License.md ├── Protocol.md ├── Readme.md ├── SECURITY.md ├── ThirdPartyNotices.txt ├── Version.props ├── docs/ │ ├── faq.md │ ├── getting-started.md │ ├── index.md │ └── troubleshooting.md ├── global.json ├── nuget.config └── scripts/ ├── Build.bat ├── CreateBuildArtifacts.bat ├── InitializeEnvironment.bat ├── RunFunctionalTests-Dev.ps1 ├── RunFunctionalTests.bat ├── RunUnitTests.bat ├── StopAllServices.bat └── StopService.bat ================================================ FILE CONTENTS ================================================ ================================================ FILE: .azure-pipelines/release.yml ================================================ # NOTE: this pipeline definition is not currently used to build releases of VFS for Git. # This is still done in the GVFS-Release-RealSign "classic" pipeline. name: $(date:yy)$(DayOfYear)$(rev:.r) variables: signType: test teamName: GVFS configuration: Release signPool: VSEng-MicroBuildVS2019 GVFSMajorAndMinorVersion: 1.0 GVFSRevision: $(Build.BuildNumber) jobs: - job: build displayName: Windows Build and Sign pool: name: $(signPool) steps: - task: ms-vseng.MicroBuildTasks.30666190-6959-11e5-9f96-f56098202fef.MicroBuildSigningPlugin@2 displayName: Install signing plugin inputs: signType: '$(SignType)' - task: UseDotNet@2 displayName: Install .NET SDK inputs: packageType: sdk version: 8.0.413 - task: CmdLine@2 displayName: Build VFS for Git inputs: script: $(Build.Repository.LocalPath)\scripts\Build.bat $(configuration) $(GVFSMajorAndMinorVersion).$(GVFSRevision) detailed - task: CmdLine@2 displayName: Run unit tests inputs: script: $(Build.Repository.LocalPath)\scripts\RunUnitTests.bat $(configuration) - task: CmdLine@2 displayName: Create build artifacts inputs: script: $(Build.Repository.LocalPath)\scripts\CreateBuildArtifacts.bat $(configuration) $(Build.ArtifactStagingDirectory) - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: Installer' inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)\NuGetPackages ArtifactName: Installer - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: FastFetch' inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)\FastFetch ArtifactName: FastFetch - task: PublishSymbols@1 displayName: Enable Source Server condition: eq(succeeded(), eq(variables['signType'], 'real')) inputs: SearchPattern: '**\*.pdb' SymbolsFolder: $(Build.ArtifactStagingDirectory)\Symbols - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: Symbols' inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)\Symbols ArtifactName: Symbols - task: ms-vscs-artifact.build-tasks.artifactSymbolTask-1.artifactSymbolTask@0 displayName: Publish to Symbols on Symweb condition: eq(succeeded(), eq(variables['signType'], 'real')) inputs: symbolServiceURI: https://microsoft.artifacts.visualstudio.com/DefaultCollection sourcePath: $(Build.ArtifactStagingDirectory)/Symbols expirationInDays: 2065 usePat: false - task: NuGetCommand@2 displayName: Push GVFS.Installers package condition: eq(succeeded(), eq(variables['signType'], 'real')) inputs: command: push packagesToPush: $(Build.ArtifactStagingDirectory)\NuGetPackages\GVFS.Installers.*.nupkg nuGetFeedType: external publishFeedCredentials: '1essharedassets GVFS [PUBLISH]' - task: ms-vseng.MicroBuildTasks.521a94ea-9e68-468a-8167-6dcf361ea776.MicroBuildCleanup@1 displayName: Send MicroBuild Telemetry condition: always() ================================================ FILE: .editorconfig ================================================ # EditorConfig: https://EditorConfig.org # top-most EditorConfig file root = true [*.cs] trim_trailing_whitespace = true ================================================ FILE: .gitattributes ================================================ ############################################################################### # Do not normalize any line endings. ############################################################################### * -text *.cs diff=csharp ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file # especially # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot#enabling-dependabot-version-updates-for-actions version: 2 updates: - package-ecosystem: "github-actions" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" ================================================ FILE: .github/workflows/build.yaml ================================================ name: VFS for Git run-name: ${{ inputs.run_name || 'VFS for Git' }} on: pull_request: branches: [ master, releases/shipped ] push: branches: [ master, releases/shipped ] workflow_dispatch: inputs: git_version: description: 'Microsoft Git version tag to include in the build (leave empty for default)' required: false type: string run_name: description: 'Optional display name for this run (used for cross-repo automation)' required: false type: string permissions: contents: read actions: read env: GIT_VERSION: ${{ github.event.inputs.git_version || 'v2.53.0.vfs.0.6' }} jobs: validate: runs-on: windows-2025 name: Validation outputs: skip: ${{ steps.check.outputs.result }} steps: - name: Look for prior successful runs id: check if: github.event.inputs.git_version == '' uses: actions/github-script@v8 with: github-token: ${{secrets.GITHUB_TOKEN}} result-encoding: string script: | /* * It would be nice if GitHub Actions offered a convenient way to avoid running * successful workflow runs _again_ for the respective commit (or for a tree-same one): * We would expect the same outcome in those cases, right? * * Let's check for such a scenario: Look for previous runs that have been successful * and that correspond to the same commit, or at least a tree-same one. If there is * one, skip running the build and tests _again_. * * There are challenges, though: We need to require those _jobs_ to succeed before PRs * can be merged. You can mark workflow _jobs_ as required on GitHub, but not * _workflows_. So if those jobs are now simply skipped, the requirement isn't met and * the PR cannot be merged. We can't just skip the job. Instead, we need to run the job * _but skip every single step_ so that the job can "succeed". */ try { // Figure out workflow ID, commit and tree const { data: run } = await github.rest.actions.getWorkflowRun({ owner: context.repo.owner, repo: context.repo.repo, run_id: context.runId, }); const workflow_id = run.workflow_id; const head_sha = run.head_sha; const tree_id = run.head_commit.tree_id; // See whether there is a successful run for that commit or tree const { data: runs } = await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, per_page: 500, workflow_id, }); // first look at commit-same runs, then at finished ones, then at in-progress ones const rank = (a) => (a.status === 'in_progress' ? 0 : (head_sha === a.head_sha ? 2 : 1)) const demoteInProgressToEnd = (a, b) => (rank(b) - rank(a)) for (const run of runs.workflow_runs.sort(demoteInProgressToEnd)) { if (head_sha !== run.head_sha && tree_id !== run.head_commit?.tree_id) continue if (context.runId === run.id) continue // do not wait for the current run to finish ;-) if (run.event === 'workflow_dispatch') continue // skip runs that were started manually: they can override the Git version if (run.status === 'in_progress') { // poll until the run is done const pollIntervalInSeconds = 30 let seconds = 0 for (;;) { console.log(`Found existing, in-progress run at ${run.html_url}; Waiting for it to finish (waited ${seconds} seconds so far)...`) await new Promise((resolve) => { setTimeout(resolve, pollIntervalInSeconds * 1000) }) seconds += pollIntervalInSeconds const { data: polledRun } = await github.rest.actions.getWorkflowRun({ owner: context.repo.owner, repo: context.repo.repo, run_id: run.id }) if (polledRun.status !== 'in_progress') break } } if (run.status === 'completed' && run.conclusion === 'success') { core.notice(`Skipping: There already is a successful run: ${run.html_url}`) return run.html_url } } return '' } catch (e) { core.error(e) core.warning(e) } - name: Checkout source if: steps.check.outputs.result == '' uses: actions/checkout@v6 - name: Validate Microsoft Git version if: steps.check.outputs.result == '' shell: pwsh env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | & "$env:GITHUB_WORKSPACE\.github\workflows\scripts\validate_release.ps1" ` -Repository microsoft/git ` -Tag $env:GIT_VERSION && ` Write-Host ::notice title=Validation::Using microsoft/git version $env:GIT_VERSION - name: Download microsoft/git installers if: steps.check.outputs.result == '' shell: cmd env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release download %GIT_VERSION% --repo microsoft/git --pattern "Git*.exe" --dir MicrosoftGit - name: Create Git install script if: steps.check.outputs.result == '' shell: cmd run: | >MicrosoftGit\install.bat ( echo @ECHO OFF echo SETLOCAL echo. echo IF "%%PROCESSOR_ARCHITECTURE%%"=="AMD64" ^( echo SET GIT_ARCH=64-bit echo ^) ELSE IF "%%PROCESSOR_ARCHITECTURE%%"=="ARM64" ^( echo SET GIT_ARCH=arm64 echo ^) ELSE ^( echo ECHO Unknown architecture: %%PROCESSOR_ARCHITECTURE%% echo exit 1 echo ^) echo. echo FOR /F "tokens=* USEBACKQ" %%%%F IN ^( `where /R %%~dp0 Git*-%%GIT_ARCH%%.exe` ^) DO SET GIT_INSTALLER=%%%%F echo. echo SET LOGDIR=%%~dp0\logs echo IF EXIST %%LOGDIR%% ^( rmdir /S /Q %%LOGDIR%% ^) echo mkdir %%LOGDIR%% echo. echo ECHO Installing Git ^(%%GIT_ARCH%%^)... echo %%GIT_INSTALLER%% /LOG="%%LOGDIR%%\git.log" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /ALLOWDOWNGRADE=1 ) - name: Upload microsoft/git installers if: steps.check.outputs.result == '' uses: actions/upload-artifact@v7 with: name: MicrosoftGit path: MicrosoftGit build: runs-on: windows-2025 name: Build and Unit Test needs: validate strategy: matrix: configuration: [ Debug, Release ] fail-fast: false steps: - name: Skip this job if there is a previous successful run if: needs.validate.outputs.skip != '' id: skip uses: actions/github-script@v8 with: script: | core.info(`Skipping: There already is a successful run: ${{ needs.validate.outputs.skip }}`) return true - name: Checkout source if: steps.skip.outputs.result != 'true' uses: actions/checkout@v6 with: path: src - name: Install .NET SDK if: steps.skip.outputs.result != 'true' uses: actions/setup-dotnet@v5 with: dotnet-version: 8.0.413 - name: Add MSBuild to PATH if: steps.skip.outputs.result != 'true' uses: microsoft/setup-msbuild@v3.0.0 - name: Build VFS for Git if: steps.skip.outputs.result != 'true' shell: cmd run: src\scripts\Build.bat ${{ matrix.configuration }} - name: Run unit tests if: steps.skip.outputs.result != 'true' shell: cmd run: src\scripts\RunUnitTests.bat ${{ matrix.configuration }} - name: Create build artifacts if: steps.skip.outputs.result != 'true' shell: cmd run: src\scripts\CreateBuildArtifacts.bat ${{ matrix.configuration }} artifacts - name: Upload functional tests drop if: steps.skip.outputs.result != 'true' uses: actions/upload-artifact@v7 with: name: FunctionalTests_${{ matrix.configuration }} path: artifacts\GVFS.FunctionalTests - name: Upload FastFetch drop if: steps.skip.outputs.result != 'true' uses: actions/upload-artifact@v7 with: name: FastFetch_${{ matrix.configuration }} path: artifacts\FastFetch - name: Upload GVFS installer if: steps.skip.outputs.result != 'true' uses: actions/upload-artifact@v7 with: name: GVFS_${{ matrix.configuration }} path: artifacts\GVFS.Installers functional_tests: name: Functional Tests needs: [validate, build] uses: ./.github/workflows/functional-tests.yaml with: skip: ${{ needs.validate.outputs.skip }} result: runs-on: ubuntu-latest name: Build, Unit and Functional Tests Successful needs: [functional_tests] steps: - name: Success! # for easier identification of successful runs in the Checks Required for Pull Requests run: echo "Workflow run is successful!" ================================================ FILE: .github/workflows/functional-tests.yaml ================================================ name: Functional Tests on: workflow_call: inputs: vfs_repository: description: 'Repository to download the VFSForGit artifacts from (defaults to the calling repository)' required: false type: string default: '' vfs_run_id: description: 'Workflow run ID to download FT executables and GVFS installer from (defaults to the calling run)' required: false type: string default: '' git_repository: description: 'Repository to download the Git installer artifact from (defaults to the calling repository)' required: false type: string default: '' git_run_id: description: 'Workflow run ID to download the Git installer artifact from (defaults to the calling run)' required: false type: string default: '' git_artifact_name: description: 'Name of the artifact containing the Git installer (must include an install.bat script)' required: false type: string default: 'MicrosoftGit' skip: description: 'URL of a previous successful run; if non-empty, all steps are skipped (job still succeeds for required checks)' required: false type: string default: '' output_prefix: description: 'Prefix for uploaded artifact names (e.g. "VFSForGit" to namespace artifacts in cross-repo runs)' required: false type: string default: '' secrets: vfs_token: description: 'Token for downloading VFSForGit artifacts (required for cross-repository downloads; defaults to GITHUB_TOKEN)' required: false git_token: description: 'Token for downloading the Git installer artifact (required for cross-repository downloads; defaults to GITHUB_TOKEN)' required: false permissions: contents: read actions: read jobs: functional_test: runs-on: ${{ matrix.architecture == 'arm64' && 'windows-11-arm' || 'windows-2025' }} name: Test env: ARTIFACT_PREFIX: ${{ inputs.output_prefix && format('{0}_', inputs.output_prefix) || '' }} FT_MATRIX_NAME: ${{ format('{0}_{1}-{2}', matrix.configuration, matrix.architecture, matrix.nr) }} strategy: matrix: configuration: [ Debug, Release ] architecture: [ x86_64, arm64 ] nr: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 10 parallel jobs to speed up the tests fail-fast: false # most failures are flaky tests, no need to stop the other jobs from succeeding steps: - name: Skip this job if there is a previous successful run if: inputs.skip != '' id: skip uses: actions/github-script@v8 with: script: | core.info(`Skipping: There already is a successful run: ${{ inputs.skip }}`) return true - name: Download Git installer if: steps.skip.outputs.result != 'true' uses: actions/download-artifact@v8 with: name: ${{ inputs.git_artifact_name }} path: git repository: ${{ inputs.git_repository || github.repository }} run-id: ${{ inputs.git_run_id || github.run_id }} github-token: ${{ secrets.git_token || github.token }} - name: Download GVFS installer if: steps.skip.outputs.result != 'true' uses: actions/download-artifact@v8 with: name: GVFS_${{ matrix.configuration }} path: gvfs repository: ${{ inputs.vfs_repository || github.repository }} run-id: ${{ inputs.vfs_run_id || github.run_id }} github-token: ${{ secrets.vfs_token || github.token }} - name: Download functional tests drop if: steps.skip.outputs.result != 'true' uses: actions/download-artifact@v8 with: name: FunctionalTests_${{ matrix.configuration }} path: ft repository: ${{ inputs.vfs_repository || github.repository }} run-id: ${{ inputs.vfs_run_id || github.run_id }} github-token: ${{ secrets.vfs_token || github.token }} - name: ProjFS details (pre-install) if: steps.skip.outputs.result != 'true' shell: cmd continue-on-error: true run: gvfs\info.bat - name: Install Git if: steps.skip.outputs.result != 'true' shell: cmd run: git\install.bat - name: Install VFS for Git if: steps.skip.outputs.result != 'true' shell: cmd run: gvfs\install.bat - name: ProjFS details (post-install) if: steps.skip.outputs.result != 'true' shell: cmd continue-on-error: true run: gvfs\info.bat - name: Upload installation logs if: always() && steps.skip.outputs.result != 'true' uses: actions/upload-artifact@v7 continue-on-error: true with: name: ${{ env.ARTIFACT_PREFIX }}InstallationLogs_${{ env.FT_MATRIX_NAME }} path: | git\logs gvfs\logs - name: Run functional tests if: steps.skip.outputs.result != 'true' shell: cmd run: | SET PATH=C:\Program Files\VFS for Git;%PATH% SET GIT_TRACE2_PERF=C:\temp\git-trace2.log ft\GVFS.FunctionalTests.exe /result:TestResult.xml --ci --slice=${{ matrix.nr }},10 - name: Upload functional test results if: always() && steps.skip.outputs.result != 'true' uses: actions/upload-artifact@v7 with: name: ${{ env.ARTIFACT_PREFIX }}FunctionalTests_Results_${{ env.FT_MATRIX_NAME }} path: TestResult.xml - name: Upload Git trace2 output if: always() && steps.skip.outputs.result != 'true' uses: actions/upload-artifact@v7 with: name: ${{ env.ARTIFACT_PREFIX }}GitTrace2_${{ env.FT_MATRIX_NAME }} path: C:\temp\git-trace2.log - name: ProjFS details (post-test) if: always() && steps.skip.outputs.result != 'true' shell: cmd continue-on-error: true run: gvfs\info.bat ================================================ FILE: .github/workflows/release-winget.yaml ================================================ name: "release-winget" on: release: types: [released] jobs: release: runs-on: windows-latest steps: - name: Publish manifest with winget-create run: | # Get correct release asset $github = Get-Content '${{ github.event_path }}' | ConvertFrom-Json $asset = $github.release.assets | Where-Object -Property name -match 'SetupGVFS[\d\.]*.exe' # Remove 'v' from the version $version = $github.release.tag_name -replace ".v","" # Download and run wingetcreate Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe .\wingetcreate.exe update Microsoft.VFSforGit -u $asset.browser_download_url -v $version -o manifests -t "${{ secrets.WINGET_TOKEN }}" -s shell: powershell ================================================ FILE: .github/workflows/scripts/validate_release.ps1 ================================================ param( [Parameter(Mandatory=$true)] [string]$Tag, [Parameter(Mandatory=$true)] [string]$Repository ) function Write-GitHubActionsCommand { param( [Parameter(Mandatory=$true)] [string]$Command, [Parameter(Mandatory=$true)] [string]$Message, [Parameter(Mandatory=$true)] [string]$Title ) Write-Host "::$Command title=$Title::$Message" } function Write-GitHubActionsWarning { param( [Parameter(Mandatory=$true)] [string]$Message, [Parameter(Mandatory=$false)] [string]$Title = "Warning" ) if ($env:GITHUB_ACTIONS -eq "true") { Write-GitHubActionsCommand -Command "warning" -Message $Message -Title $Title } else { Write-Host "! Warning: $Message" -ForegroundColor Yellow } } function Write-GitHubActionsError { param( [Parameter(Mandatory=$true)] [string]$Message, [Parameter(Mandatory=$false)] [string]$Title = "Error" ) if ($env:GITHUB_ACTIONS -eq "true") { Write-GitHubActionsCommand -Command "error" -Message $Message -Title $Title } else { Write-Host "x Error: $Message" -ForegroundColor Red } } if ([string]::IsNullOrWhiteSpace($Tag)) { Write-GitHubActionsError -Message "Tag parameter is required" exit 1 } if ([string]::IsNullOrWhiteSpace($Repository)) { Write-GitHubActionsError -Message "Repository parameter is required" exit 1 } Write-Host "Validating $Repository release '$Tag'..." # Prepare headers for GitHub API $headers = @{ 'Accept' = 'application/vnd.github.v3+json' 'User-Agent' = 'VFSForGit-Build' } if ($env:GITHUB_TOKEN) { $headers['Authorization'] = "Bearer $env:GITHUB_TOKEN" } # Check if the tag exists in microsoft/git repository try { $releaseResponse = Invoke-RestMethod ` -Uri "https://api.github.com/repos/$Repository/releases/tags/$Tag" ` -Headers $headers Write-Host "✓ Tag '$Tag' found in $Repository" -ForegroundColor Green Write-Host " Release : $($releaseResponse.name)" Write-Host " Published : $($releaseResponse.published_at.ToString('u'))" # Check if this a pre-release if ($releaseResponse.prerelease -eq $true) { Write-GitHubActionsWarning ` -Message "Using a pre-released version of $Repository" ` -Title "Pre-release $Repository version" } # Get the latest release for comparison try { $latestResponse = Invoke-RestMethod ` -Uri "https://api.github.com/repos/$Repository/releases/latest" ` -Headers $headers $latestTag = $latestResponse.tag_name # Check if this is the latest release if ($Tag -eq $latestTag) { Write-Host "✓ Using the latest release" -ForegroundColor Green exit 0 } # Not the latest! $warningTitle = "Outdated $Repository release" $warningMsg = "Not using latest release of $Repository (latest: $latestTag)" Write-GitHubActionsWarning -Message $warningMsg -Title $warningTitle } catch { Write-GitHubActionsWarning -Message "Could not check latest release info for ${Repository}: $($_.Exception.Message)" } } catch { if ($_.Exception.Response.StatusCode -eq 404) { Write-GitHubActionsError -Message "Tag '$Tag' does not exist in $Repository" exit 1 } else { Write-GitHubActionsError -Message "Error validating release '$Tag': $($_.Exception.Message)" exit 1 } } ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # VS 2017 user-specific files launchSettings.json # User-specific files *.suo *.user *.userosscache *.sln.docstates # Mac xcuserdata .DS_Store # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ build/ bld/ [Bb]in/ [Oo]bj/ # Visual Studio 2015 cache/options directory .vs/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opensdf *.sdf *.cachefile *.VC.opendb *.VC.db # Visual Studio profiler *.psess *.vsp *.vspx # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml ## TODO: Comment the next line if you want to checkin your ## web deploy settings but do note that will include unencrypted ## passwords #*.pubxml *.publishproj # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # Windows Azure Build Output csx/ *.build.csdef # Windows Store app package directory AppPackages/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ [Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.publishsettings node_modules/ orleans.codegen.cs # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # LightSwitch generated files GeneratedArtifacts/ _Pvt_Extensions/ ModelManifest.xml *.dll *.cab *.cer # VS Code private directory .vscode/ # ProjFS Kext Unit Test coverage results ProjFS.Mac/CoverageResult.txt ================================================ FILE: .vsconfig ================================================ { "version": "1.0", "components": [ "Microsoft.Component.MSBuild", "Microsoft.Net.Component.4.7.1.TargetingPack", "Microsoft.Net.Component.4.7.1.SDK", "Microsoft.Net.Core.Component.SDK.8.0", "Microsoft.VisualStudio.Component.VC.v143.x86.x64", "Microsoft.VisualStudio.Component.Windows11SDK.26100", "Microsoft.VisualStudio.Workload.NativeDesktop", "Microsoft.VisualStudio.Workload.ManagedDesktop", "Microsoft.VisualStudio.Workload.NetCoreTools" ] } ================================================ FILE: AuthoringTests.md ================================================ # Authoring Tests ## Functional Tests #### Runnable functional test projects - `GVFS.FunctionalTests` - `GVFS.FunctionalTests.Windows` `GVFS.FunctionalTests` is a .NET Core project and contains all cross-platform functional tests. `GVFS.FunctionalTests.Windows`, contains functional tests that require Windows. Additionally, `GVFS.FunctionalTests.Windows` includes all the `GVFS.FunctionalTests` allowing it to run both cross-platform and Windows-specific functional tests. #### Other functional test projects *GVFS.NativeTests* `GVFS.NativeTests` contains tests written in C++ that use the Windows API directly. These tests are called from the managed tests (see above) using PInvoke. *GVFS.FunctionalTests.LockHolder* The `LockHolder` is a small program that allows the functional tests to request and release the `GVFSLock`. `LockHolder` is useful for simulating different timing/race conditions. ## Running the Functional Tests The functional tests are built on NUnit 3, which is available as a set of NuGet packages. ### Windows 1. Build VFS for Git: **Option 1:** Open GVFS.sln in Visual Studio and build everything. **Option 2:** Run `Scripts\BuildGVFSForWindows.bat` from the command line 2. Run the VFS4G installer that was built in step 2. This will ensure that ProjFS is properly installed/enabled on your machine, and that VFS4G will be able to find the correct version of the pre/post-command hooks. The installer will be placed in `BuildOutput\GVFS.Installer.Windows\bin\x64\` 3. Run the tests **with elevation**. Elevation is required because the functional tests create and delete a test service. **Option 1:** Run the `GVFS.FunctionalTests.Windows` project from inside Visual Studio launched as Administrator. **Option 2:** Run `Scripts\RunFunctionalTests.bat` from CMD launched as Administrator. #### Selecting Which Tests are Run By default, the functional tests run a subset of tests as a quick smoke test for developers. There are three mutually exclusive arguments that can be passed to the functional tests to change this behavior: - `--full-suite`: Run all configurations of all functional tests - `--extra-only`: Run only those tests marked as "ExtraCoverage" (i.e. the tests that are not run by default) - `--windows-only`: Run only the tests marked as being Windows specific **NOTE** `Scripts\RunFunctionalTests.bat` already uses some of these arguments. If you run the tests using `RunFunctionalTests.bat` consider locally modifying the script rather than passing these flags as arguments to the script. ### Mac 1. Build VFS for Git: `Scripts/Mac/BuildGVFSForMac.sh` 2. Run the tests: `Scripts/Mac/RunFunctionalTests.sh ` If you need the VS for Mac debugger attached for a functional test run: 1. Make sure you've built your latest changes 2. Run `./ProjFS.Mac/Scripts/LoadPrjFSKext.sh` 3. Open GVFS.sln in VS for Mac 4. Run->Run With->Custom Configuration... 5. Select "Start external program" and specify the published functional test binary (e.g. `/Users//Repos/VFSForGit/Publish/GVFS.FunctionalTests`) 6. Specify any desired arguments (e.g. [a specific test](#Running-Specific-Tests) ) 7. Run Action -> "Debug - .Net Core Debugger" 8. Click "Debug" ### Customizing the Functional Test Settings The functional tests take a set of parameters that indicate what paths and URLs to work with. If you want to customize those settings, they can be found in [`GVFS.FunctionalTests\Settings.cs`](/GVFS/GVFS.FunctionalTests/Settings.cs). ## Running Specific Tests Specific tests can be run by adding the `--test=` command line argument to the functional test project/scripts. Note that the test name must include the class and namespace and that `Debug` or `Release` must be specified when running the functional test scripts. *Example* Windows (Script): `Scripts\RunFunctionalTests.bat Debug --test=GVFS.FunctionalTests.Tests.EnlistmentPerFixture.GitFilesTests.CreateFileTest` Windows (Visual Studio): 1. Set `GVFS.FunctionalTests.Windows` as StartUp project 2. Project Properties->Debug->Start options->Command line arguments (all on a single line): `--test=GVFS.FunctionalTests.Tests.EnlistmentPerFixture.GitFilesTests.CreateFileTest` Mac: `Scripts/Mac/RunFunctionalTests.sh Debug --test=GVFS.FunctionalTests.Tests.EnlistmentPerFixture.GitFilesTests.CreateFileTest` ## How to Write a Functional Test Each piece of functionality that we add to VFS for Git should have corresponding functional tests that clone a repo, mount the filesystem, and use existing tools and filesystem APIs to interact with the virtual repo. Since these are functional tests that can potentially modify the state of files on disk, you need to be careful to make sure each test can run in a clean environment. There are three base classes that you can derive from when writing your tests. It's also important to put your new class into the same namespace as the base class, because NUnit treats namespaces like test suites, and we have logic that keys off that for deciding when to create enlistments. 1. `TestsWithLongRunningEnlistment` Before any test in this namespace is executed, we create a single enlistment and mount VFS for Git. We then run all tests in this namespace that derive from this base class. Only put tests in here that are purely readonly and will leave the repo in a good state for future tests. 2. `TestsWithEnlistmentPerFixture` For any test fixture (a fixture is the same as a class in NUnit) that derives from this class, we create an enlistment and mount VFS for Git before running any of the tests in the fixture, and then we unmount and delete the enlistment after all tests are done (but before any other fixture runs). If you need to write a sequence of tests that manipulate the same repo, this is the right base class. 3. `TestsWithEnlistmentPerTestCase` Derive from this class if you need a new enlistment created for each test case. This is the most reliable, but also most expensive option. ## Updating the Remote Test Branch By default, the functional tests clone `master`, check out the branch "FunctionalTests/YYYYMMDD" (with the day the FunctionalTests branch was created), and then remove all remote tracking information. This is done to guarantee that remote changes to tip cannot break functional tests. If you need to update the functional tests to use a new FunctionalTests branch, you'll need to create a new "FunctionalTests/YYYYMMDD" branch and update the `Commitish` setting in `Settings.cs` to have this new branch name. Once you have verified your scenarios locally you can push the new FunctionalTests branch and then your changes. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to VFS for Git Thank you for taking the time to contribute! ## Guidelines * [Code of Conduct](#code-of-conduct) * [Design Reviews](#design-reviews) * [Platform Specific Code](#platform-specific-code) * [Tracing and Logging](#tracing-and-logging) * [Error Handling](#error-handling) * [Background Threads](#background-threads) * [Coding Conventions](#coding-conventions) * [Testing](#testing) ## Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Design Reviews Architectural changes and new features should start with a design review. It's easier and wastes less time to incorporate feedback at this stage. The design review process is as follows: 1. Create a pull request that contains a design document in Markdown (.md) format for the proposed change. Assign the `design-doc` label to the pull request. 2. Use the pull request for design feedback and for iterating on the design. 3. Once the design is approved, create a new issue with a description that includes the final design document. Include a link to the pull request that was used for discussion. 4. Close (without merging!) the pull request used for the design discussion. ## Platform Specific Code - *Prefer cross-platform code to platform-specific code* Cross-platform code is more easily reused. Reusing code reduces the amount of code that must be written, tested, and maintained. - *Platform specific code, and only platform specific code, should go in `GVFSPlatform`* When platform specific code is required, it should be placed in `GVFSPlatform` or one of the platforms it contains (e.g. `IKernelDriver`) ## Tracing and Logging - *The "Error" logging level is reserved for non-retryable errors that result in I/O failures or the VFS4G process shutting down* The expectation from our customers is that when VFS4G logs an "Error" level message in its log file either: * VFS4G had to shut down unexpectedly * VFS4G encountered an issue severe enough that user-initiated I/O would fail. - *Log full exception stacks* Full exception stacks (i.e. `Exception.ToString`) provide more details than the exception message alone (`Exception.Message`). Full exception stacks make root-causing issues easier. - *Do not display full exception stacks to users* Exception call stacks are not usually actionable for the user. Users frequently (sometimes incorrectly) assume that VFS4G has crashed when shown a full stack. The full stack *should* be included in VFS4G logs, but *should not* be displayed as part of the error message provided to the user. - *Include relevant details when logging exceptions* Sometimes an exception call stack alone is not enough to root cause failures in VFS4G. When catching (or throwing) exceptions, log relevant details that will help diagnose the issue. As a general rule, the closer an exception is caught to where it's thrown, the more relevant details there will be to log. Example: ``` catch (Exception e) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", "Mount"); metadata.Add(nameof(enlistmentHookPath), enlistmentHookPath); metadata.Add(nameof(installedHookPath), installedHookPath); metadata.Add("Exception", e.ToString()); context.Tracer.RelatedError(metadata, $"Failed to compare {hook.Name} version"); } ``` ## Error Handling - *Fail fast: An error or exception that risks data loss or corruption should shut down VFS4G immediately* Preventing data loss and repository corruption is critical. If an error or exception occurs that could lead to data loss, it's better to shut down VFS4G than keep the repository mounted and risk corruption. - *Do not catch exceptions that are indicative of a programmer error (e.g. `ArgumentNullException`)* Any exceptions that result from programmer error (e.g. `ArgumentNullException`) should be discovered as early in the development process as possible. Avoid `catch` statements that would hide these errors (e.g. `catch(Exception)`). The only exception to this rule is for [unhandled exceptions in background threads](#bgexceptions) - *Do not use exceptions for normal control flow* Prefer writing code that does not throw exceptions. The `TryXXX` pattern, for example, avoids the performance costs that come with using exceptions. Additionally, VFS4G typically needs to know exactly where errors occur and handle the errors there. The `TryXXX` pattern helps ensure errors are handled in that fashion. Example: Handle errors where they occur (good): ``` bool TryDoWorkOnDisk(string fileContents, out string error) { if (!TryCreateReadConfig()) { error = "Failed to read config file"; return false; } if (!TryCreateTempFile(fileContents)) { error = "Failed to create temp file"; return false; } if (!TryRenameTempFile()) { error = "Failed to rename temp file"; if (!TryDeleteTempFile()) { error += ", and failed to cleanup temp file"; } return false; } error = null; return true; } ``` Example: Handle errors in `catch` without knowing where they came from (bad): ``` bool TryDoWorkOnDisk(string fileContents, out string error) { try { CreateReadConfig(); CreateTempFile(fileContents); RenameTempFile(); } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { error = "Something went wrong doing work on disk"; try { if (TempFileExists()) { DeleteTempFile(); } } catch (Exception e) when (e is IOException || e is UnauthorizedAccessException) { error += ", and failed to cleanup temp file"; } return false; } error = null; return true; } ``` - *Provide the user with user-actionable messages whenever possible* Don't tell a user what went wrong. Help the user fix the problem. Example: > `"You can only specify --hydrate if the repository is mounted. Run 'gvfs mount' and try again."` ## Background Threads - *Avoid using the thread pool (and avoid using async)* `HttpRequestor.SendRequest` makes a [blocking call](https://github.com/Microsoft/VFSForGit/blob/4baa37df6bde2c9a9e1917fc7ce5debd653777c0/GVFS/GVFS.Common/Http/HttpRequestor.cs#L135) to `HttpClient.SendAsync`. That blocking call consumes a thread from the managed thread pool. Until that design changes, the rest of VFS4G must avoid using the thread pool unless absolutely necessary. If the thread pool is required, any long running tasks should be moved to a separate thread managed by VFS4G itself (see [GitMaintenanceQueue](https://github.com/Microsoft/VFSForGit/blob/4baa37df6bde2c9a9e1917fc7ce5debd653777c0/GVFS/GVFS.Common/Maintenance/GitMaintenanceQueue.cs#L19) for an example). Long-running or blocking work scheduled on the managed thread pool can prevent the normal operation of VFS4G. For example, it could prevent downloading file sizes, loose objects, or file contents in a timely fashion. - *Catch all exceptions on long-running tasks and background threads* Wrap all code that runs in the background thread in a top-level `try/catch(Exception)`. Any exceptions caught by this handler should be logged, and then VFS4G should be forced to terminate with `Environment.Exit`. It's not safe to allow VFS4G to continue to run after an unhandled exception stops a background thread or long-running task. Testing has shown that `Environment.Exit` consistently terminates the VFS4G mount process regardless of how background threads are started (e.g. native thread, `new Thread()`, `Task.Factory.StartNew()`). An example of this pattern can be seen in [`BackgroundFileSystemTaskRunner.ProcessBackgroundTasks`](https://github.com/Microsoft/VFSForGit/blob/4baa37df6bde2c9a9e1917fc7ce5debd653777c0/GVFS/GVFS.Virtualization/Background/BackgroundFileSystemTaskRunner.cs#L233). ## Coding Conventions - *Most C# coding style guidelines are covered by StyleCop* Fix any StyleCop issues reported in changed code. When adding new projects to VFS4G, be sure that StyleCop is analyzing them as part of the build. - *Prefer explicit types to interfaces and implicitly typed variables* Avoid the use of `var` (C#), `dynamic` (C#), and `auto` (C++). Prefer concrete/explicit types to interfaces (e.g. prefer `List` to `IList`). The VFS4G codebase uses this approach because: * Interfaces can hide the performance characteristics of their underlying type. For example, an `IDictionary` could be a `SortedList` or a `Dictionary` (or several other data types). * Interfaces can hide the thread safety (or lack thereof) of their underlying type. For example, an `IDictionary` could be a `Dictionary` or a `ConcurrentDictionary`. * Explicit types make these performance and thread safety characteristics explicit when reviewing code. * VFS4G is not a public API and its components are always shipped together. Develoepers are free to make API changes to VFS4G's public methods. - *Method names start with a verb (e.g. "GetProjectedFolderEntryData" rather than "ProjectedFolderEntryData")* Starting with a verb in the name improves readability and helps ensure consistency with the rest of the VFS4G codebase. - *Aim to write self-commenting code. When necessary, comments should give background needed to understand the code.* Helpful (good) comment: ``` // Order the folders in descending order so that we walk the tree from bottom up. // Traversing the folders in this order: // 1. Ensures child folders are deleted before their parents // 2. Ensures that folders that have been deleted by git // (but are still in the projection) are found before their // parent folder is re-expanded (only applies on platforms // where EnumerationExpandsDirectories is true) foreach (PlaceholderListDatabase.PlaceholderData folderPlaceholder in placeholderFoldersListCopy.OrderByDescending(x => x.Path)) ``` Obvious (bad) comment: ``` // Check if enumeration expands directories on the current platform if (GVFSPlatform.Instance.KernelDriver.EnumerationExpandsDirectories) ``` - *Add new interfaces when it makes sense for the product, not simply for unit testing* When a class needs to be mocked (or have a subset of its behavior mocked), prefer using virtual methods instead of adding a new interface. VFS4G uses interfaces when multiple implementations of the interface exist in the product code. - *Check for `null` using the equality (`==`) and inequality (`!=`) operators rather than `is`* A corollary to this guideline is that equality/inequality operators that break `null` checks should not be added (see [this post](https://stackoverflow.com/questions/40676426/what-is-the-difference-between-x-is-null-and-x-null) for an example). - *Use `nameof(...)` rather than hardcoded strings* Using `nameof` ensures that when methods/variables are renamed the logging of those method/variable names will also be updated. However, hard coded strings are still appropriate when they are used for generating reports and changing the strings would break the reports. ### C/C++ - *Do not use C-style casts. Use C++-style casts.* C++ style casts (e.g. `static_cast`) more clearly express the intent of the programmer, allow for better validation by the compiler, and are easier to search for in the codebase. - *Declare static functions at the top of source files* This ensures that the functions can be called from anywhere inside the file. - *Do not use namespace `using` statements in header files* `using` statements inside header files are picked up by all source files that include the headers and can cause unexpected errors if there are name collisions. - *Prefer `using` to full namespaces in source files* Example: ``` // Inside MyFavSourceFile.cpp using std::string; // Do not use `std::` namespace with `string` static string s_myString; ``` - *Use a meaningful prefix for "public" free functions, and use the same prefix for all functions in a given header file* Example: ``` // Functions declared in VirtualizationRoots.h have "VirtualizationRoot_" prefix bool VirtualizationRoot_IsOnline(VirtualizationRootHandle rootHandle); // Static helper function in VirtualizationRoots.cpp has no prefix static VirtualizationRootHandle FindOrDetectRootAtVnode(vnode_t vnode, const FsidInode& vnodeFsidInode); ``` ## Testing - *Add new unit and functional tests when making changes* Comprehensive tests are essential for maintaining the health and quality of the product. For more details on writing tests see [Authoring Tests](https://github.com/Microsoft/VFSForGit/blob/master/AuthoringTests.md). - *Functional tests are black-box tests and should not build against any VFS4G product code* Keeping the code separate helps ensure that bugs in the product code do not compromise the integrity of the functional tests. ### C# Unit Tests - *Add `ExceptionExpected` to unit tests that are expected to have exceptions* Example: ``` [TestCase] [Category(CategoryConstants.ExceptionExpected)] public void ParseFromLsTreeLine_NullRepoRoot() ``` Unit tests should be tagged with `ExceptionExpected` when either the test code or the product code will throw an exception. `ExceptionExpected` tests are not executed when the debugger is attached, and this prevents developers from having to keep continuing the tests each time exceptions are caught by the debugger. - *Use a `mock` prefix for absolute file system paths and URLs* The unit tests should not touch the real file system nor should they reach out to any real URLs. Using `mock:\\` and `mock://` ensures that any product code that was not properly mocked will not interact with the real file system or attempt to contact a real URL. ================================================ FILE: Directory.Build.props ================================================ $(MSBuildThisFileDirectory) $(RepoPath) $(RepoPath)..\out\ $(RepoOutPath)$(MSBuildProjectName)\ $(RepoPath)..\packages\ true win-x64 x64 $(ProjectOutPath)bin\ $(ProjectOutPath)obj\ x64 $(ProjectOutPath)bin\$(Platform)\$(Configuration)\ $(ProjectOutPath)intermediate\$(Platform)\$(Configuration)\ $(IntDir)include\ 10.0.16299.0 ================================================ FILE: Directory.Build.targets ================================================ $(GVFSVersion) false ================================================ FILE: Directory.Packages.props ================================================ true ================================================ FILE: Directory.Solution.props ================================================ ================================================ FILE: GVFS/FastFetch/CheckoutPrefetcher.cs ================================================ using GVFS.Common; using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.Prefetch; using GVFS.Common.Prefetch.Git; using GVFS.Common.Prefetch.Pipeline; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace FastFetch { public class CheckoutPrefetcher : BlobPrefetcher { private readonly int checkoutThreadCount; private readonly bool allowIndexMetadataUpdateFromWorkingTree; private readonly bool forceCheckout; public CheckoutPrefetcher( ITracer tracer, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor, int chunkSize, int searchThreadCount, int downloadThreadCount, int indexThreadCount, int checkoutThreadCount, bool allowIndexMetadataUpdateFromWorkingTree, bool forceCheckout) : base( tracer, enlistment, objectRequestor, chunkSize, searchThreadCount, downloadThreadCount, indexThreadCount) { this.checkoutThreadCount = checkoutThreadCount; this.allowIndexMetadataUpdateFromWorkingTree = allowIndexMetadataUpdateFromWorkingTree; this.forceCheckout = forceCheckout; } /// A specific branch to filter for, or null for all branches returned from info/refs public override void Prefetch(string branchOrCommit, bool isBranch) { if (string.IsNullOrWhiteSpace(branchOrCommit)) { throw new FetchException("Must specify branch or commit to fetch"); } GitRefs refs = null; string commitToFetch; if (isBranch) { refs = this.ObjectRequestor.QueryInfoRefs(branchOrCommit); if (refs == null) { throw new FetchException("Could not query info/refs from: {0}", this.Enlistment.RepoUrl); } else if (refs.Count == 0) { throw new FetchException("Could not find branch {0} in info/refs from: {1}", branchOrCommit, this.Enlistment.RepoUrl); } commitToFetch = refs.GetTipCommitId(branchOrCommit); } else { commitToFetch = branchOrCommit; } using (new IndexLock(this.Enlistment.EnlistmentRoot, this.Tracer)) { this.DownloadMissingCommit(commitToFetch, this.GitObjects); // Configure pipeline // Checkout uses DiffHelper when running checkout.Start(), which we use instead of LsTreeHelper // Checkout diff output => FindBlobs => BatchDownload => IndexPack => Checkout available blobs CheckoutStage checkout = new CheckoutStage(this.checkoutThreadCount, this.FolderList, commitToFetch, this.Tracer, this.Enlistment, this.forceCheckout); FindBlobsStage blobFinder = new FindBlobsStage(this.SearchThreadCount, checkout.RequiredBlobs, checkout.AvailableBlobShas, this.Tracer, this.Enlistment); BatchObjectDownloadStage downloader = new BatchObjectDownloadStage(this.DownloadThreadCount, this.ChunkSize, blobFinder.MissingBlobs, checkout.AvailableBlobShas, this.Tracer, this.Enlistment, this.ObjectRequestor, this.GitObjects); IndexPackStage packIndexer = new IndexPackStage(this.IndexThreadCount, downloader.AvailablePacks, checkout.AvailableBlobShas, this.Tracer, this.GitObjects); // Start pipeline downloader.Start(); blobFinder.Start(); checkout.Start(); blobFinder.WaitForCompletion(); this.HasFailures |= blobFinder.HasFailures; // Delay indexing. It interferes with FindMissingBlobs, and doesn't help Bootstrapping. packIndexer.Start(); downloader.WaitForCompletion(); this.HasFailures |= downloader.HasFailures; packIndexer.WaitForCompletion(); this.HasFailures |= packIndexer.HasFailures; // Since pack indexer is the last to finish before checkout finishes, it should propagate completion. // This prevents availableObjects from completing before packIndexer can push its objects through this link. checkout.AvailableBlobShas.CompleteAdding(); checkout.WaitForCompletion(); this.HasFailures |= checkout.HasFailures; if (!this.SkipConfigUpdate && !this.HasFailures) { bool shouldSignIndex = !this.GetIsIndexSigningOff(); // Update the index - note that this will take some time EventMetadata updateIndexMetadata = new EventMetadata(); updateIndexMetadata.Add("IndexSigningIsOff", shouldSignIndex); using (ITracer activity = this.Tracer.StartActivity("UpdateIndex", EventLevel.Informational, Keywords.Telemetry, updateIndexMetadata)) { Index sourceIndex = this.GetSourceIndex(); GitIndexGenerator indexGen = new GitIndexGenerator(this.Tracer, this.Enlistment, shouldSignIndex); indexGen.CreateFromRef(commitToFetch, indexVersion: 2, isFinal: false); this.HasFailures |= indexGen.HasFailures; if (!indexGen.HasFailures) { Index newIndex = new Index( this.Enlistment.EnlistmentRoot, this.Tracer, indexGen.TemporaryIndexFilePath, readOnly: false); // Update from disk only if the caller says it is ok via command line // or if we updated the whole tree and know that all files are up to date bool allowIndexMetadataUpdateFromWorkingTree = this.allowIndexMetadataUpdateFromWorkingTree || checkout.UpdatedWholeTree; newIndex.UpdateFileSizesAndTimes(checkout.AddedOrEditedLocalFiles, allowIndexMetadataUpdateFromWorkingTree, shouldSignIndex, sourceIndex); // All the slow stuff is over, so we will now move the final index into .git\index, shortly followed by // updating the ref files and releasing index.lock. string indexPath = Path.Combine(this.Enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName); this.Tracer.RelatedEvent(EventLevel.Informational, "MoveUpdatedIndexToFinalLocation", new EventMetadata() { { "UpdatedIndex", indexGen.TemporaryIndexFilePath }, { "Index", indexPath } }); File.Delete(indexPath); File.Move(indexGen.TemporaryIndexFilePath, indexPath); newIndex.WriteFastFetchIndexVersionMarker(); } } if (!this.HasFailures) { this.UpdateRefs(branchOrCommit, isBranch, refs); if (isBranch) { // Update the refspec before setting the upstream or git will complain the remote branch doesn't exist this.HasFailures |= !this.UpdateRefSpec(this.Tracer, this.Enlistment, branchOrCommit, refs); using (ITracer activity = this.Tracer.StartActivity("SetUpstream", EventLevel.Informational)) { string remoteBranch = refs.GetBranchRefPairs().Single().Key; GitProcess git = new GitProcess(this.Enlistment); GitProcess.Result result = git.SetUpstream(branchOrCommit, remoteBranch); if (result.ExitCodeIsFailure) { activity.RelatedError("Could not set upstream for {0} to {1}: {2}", branchOrCommit, remoteBranch, result.Errors); this.HasFailures = true; } } } } } } } /// /// * Updates local branch (N/A for checkout to detached HEAD) /// * Updates HEAD /// * Calls base to update shallow file and remote branch. /// protected override void UpdateRefs(string branchOrCommit, bool isBranch, GitRefs refs) { if (isBranch) { KeyValuePair remoteRef = refs.GetBranchRefPairs().Single(); string remoteBranch = remoteRef.Key; string fullLocalBranchName = branchOrCommit.StartsWith(RefsHeadsGitPath) ? branchOrCommit : (RefsHeadsGitPath + branchOrCommit); this.HasFailures |= !this.UpdateRef(this.Tracer, fullLocalBranchName, remoteRef.Value); this.HasFailures |= !this.UpdateRef(this.Tracer, "HEAD", fullLocalBranchName); } else { this.HasFailures |= !this.UpdateRef(this.Tracer, "HEAD", branchOrCommit); } base.UpdateRefs(branchOrCommit, isBranch, refs); } private Index GetSourceIndex() { string indexPath = Path.Combine(this.Enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName); if (File.Exists(indexPath)) { Index output = new Index(this.Enlistment.EnlistmentRoot, this.Tracer, indexPath, readOnly: true); output.Parse(); return output; } return null; } private bool GetIsIndexSigningOff() { // The first bit of core.gvfs is set if index signing is turned off. const uint CoreGvfsUnsignedIndexFlag = 1; GitProcess git = new GitProcess(this.Enlistment); GitProcess.ConfigResult configCoreGvfs = git.GetFromConfig("core.gvfs"); string coreGvfs; string error; if (!configCoreGvfs.TryParseAsString(out coreGvfs, out error)) { return false; } uint valueCoreGvfs; // No errors getting the configuration and it is either "true" or numeric with the right bit set. return !string.IsNullOrEmpty(coreGvfs) && (coreGvfs.Equals("true", StringComparison.OrdinalIgnoreCase) || (uint.TryParse(coreGvfs, out valueCoreGvfs) && ((valueCoreGvfs & CoreGvfsUnsignedIndexFlag) == CoreGvfsUnsignedIndexFlag))); } } } ================================================ FILE: GVFS/FastFetch/CheckoutStage.cs ================================================ using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Prefetch.Git; using GVFS.Common.Prefetch.Pipeline; using GVFS.Common.Tracing; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; namespace FastFetch { public class CheckoutStage : PrefetchPipelineStage { private const string AreaPath = nameof(CheckoutStage); private const int NumOperationsPerStatus = 10000; private ITracer tracer; private Enlistment enlistment; private PhysicalFileSystem fileSystem; private string targetCommitSha; private bool forceCheckout; private DiffHelper diff; private int directoryOpCount = 0; private int fileDeleteCount = 0; private int fileWriteCount = 0; private long bytesWritten = 0; private long shasReceived = 0; // Checkout requires synchronization between the delete/directory/add stages, so control the parallelization private int maxParallel; public CheckoutStage(int maxParallel, IEnumerable folderList, string targetCommitSha, ITracer tracer, Enlistment enlistment, bool forceCheckout) : base(maxParallel: 1) { this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata: null); this.enlistment = enlistment; this.fileSystem = new PhysicalFileSystem(); this.diff = new DiffHelper(tracer, enlistment, new string[0], folderList, includeSymLinks: true); this.targetCommitSha = targetCommitSha; this.forceCheckout = forceCheckout; this.AvailableBlobShas = new BlockingCollection(); // Keep track of how parallel we're expected to be later during DoWork // Note that '1' is passed to the base object, forcing DoWork to be single threaded // This allows us to control the synchronization between stages by doing the parallization ourselves this.maxParallel = maxParallel; } public BlockingCollection RequiredBlobs { get { return this.diff.RequiredBlobs; } } public BlockingCollection AvailableBlobShas { get; } public bool UpdatedWholeTree { get { return this.diff.UpdatedWholeTree; } } public BlockingCollection AddedOrEditedLocalFiles { get; } = new BlockingCollection(); protected override void DoBeforeWork() { if (this.forceCheckout) { // Force search the entire tree by treating the repo as if it were brand new. this.diff.PerformDiff(sourceTreeSha: null, targetTreeSha: this.targetCommitSha); } else { // Let the diff find the sourceTreeSha on its own. this.diff.PerformDiff(this.targetCommitSha); } this.HasFailures = this.diff.HasFailures; } protected override void DoWork() { // Do the delete operations first as they can't have dependencies on other work using (ITracer activity = this.tracer.StartActivity( nameof(this.HandleAllFileDeleteOperations), EventLevel.Informational, Keywords.Telemetry, metadata: null)) { Parallel.For(0, this.maxParallel, (i) => { this.HandleAllFileDeleteOperations(); }); EventMetadata metadata = new EventMetadata(); metadata.Add("FilesDeleted", this.fileDeleteCount); activity.Stop(metadata); } // Do directory operations after deletes in case a file delete must be done first using (ITracer activity = this.tracer.StartActivity( nameof(this.HandleAllDirectoryOperations), EventLevel.Informational, Keywords.Telemetry, metadata: null)) { Parallel.For(0, this.maxParallel, (i) => { this.HandleAllDirectoryOperations(); }); EventMetadata metadata = new EventMetadata(); metadata.Add("DirectoryOperationsCompleted", this.directoryOpCount); activity.Stop(metadata); } // Do add operations last, after all deletes and directories have been created using (ITracer activity = this.tracer.StartActivity( nameof(this.HandleAllFileAddOperations), EventLevel.Informational, Keywords.Telemetry, metadata: null)) { Parallel.For(0, this.maxParallel, (i) => { this.HandleAllFileAddOperations(); }); EventMetadata metadata = new EventMetadata(); metadata.Add("FilesWritten", this.fileWriteCount); activity.Stop(metadata); } } protected override void DoAfterWork() { // If for some reason a blob doesn't become available, // checkout might complete with file writes still left undone. if (this.diff.FileAddOperations.Count > 0) { this.HasFailures = true; EventMetadata errorMetadata = new EventMetadata(); if (this.diff.FileAddOperations.Count < 10) { errorMetadata.Add("RemainingShas", string.Join(",", this.diff.FileAddOperations.Keys)); } else { errorMetadata.Add("RemainingShaCount", this.diff.FileAddOperations.Count); } this.tracer.RelatedError(errorMetadata, "Not all file writes were completed"); } this.AddedOrEditedLocalFiles.CompleteAdding(); EventMetadata metadata = new EventMetadata(); metadata.Add("DirectoryOperations", this.directoryOpCount); metadata.Add("FileDeletes", this.fileDeleteCount); metadata.Add("FileWrites", this.fileWriteCount); metadata.Add("BytesWritten", this.bytesWritten); metadata.Add("ShasReceived", this.shasReceived); this.tracer.Stop(metadata); } private void HandleAllDirectoryOperations() { DiffTreeResult treeOp; while (this.diff.DirectoryOperations.TryDequeue(out treeOp)) { string absoluteTargetPath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, treeOp.TargetPath); if (this.HasFailures) { return; } switch (treeOp.Operation) { case DiffTreeResult.Operations.Modify: case DiffTreeResult.Operations.Add: try { Directory.CreateDirectory(absoluteTargetPath); } catch (Exception ex) { EventMetadata metadata = new EventMetadata(); metadata.Add("Operation", "CreateDirectory"); metadata.Add(nameof(treeOp.TargetPath), absoluteTargetPath); this.tracer.RelatedError(metadata, ex.Message); this.HasFailures = true; } break; case DiffTreeResult.Operations.Delete: try { if (Directory.Exists(absoluteTargetPath)) { this.fileSystem.DeleteDirectory(absoluteTargetPath); } } catch (Exception ex) { // We are deleting directories and subdirectories in parallel if (Directory.Exists(absoluteTargetPath)) { EventMetadata metadata = new EventMetadata(); metadata.Add("Operation", "DeleteDirectory"); metadata.Add(nameof(treeOp.TargetPath), absoluteTargetPath); this.tracer.RelatedError(metadata, ex.Message); this.HasFailures = true; } } break; default: this.tracer.RelatedError("Ignoring unexpected Tree Operation {0}: {1}", absoluteTargetPath, treeOp.Operation); continue; } if (Interlocked.Increment(ref this.directoryOpCount) % NumOperationsPerStatus == 0) { EventMetadata metadata = new EventMetadata(); metadata.Add("DirectoryOperationsQueued", this.diff.DirectoryOperations.Count); metadata.Add("DirectoryOperationsCompleted", this.directoryOpCount); this.tracer.RelatedEvent(EventLevel.Informational, "CheckoutStatus", metadata); } } } private void HandleAllFileDeleteOperations() { string path; while (this.diff.FileDeleteOperations.TryDequeue(out path)) { if (this.HasFailures) { return; } try { if (File.Exists(path)) { File.Delete(path); } Interlocked.Increment(ref this.fileDeleteCount); } catch (Exception ex) { EventMetadata metadata = new EventMetadata(); metadata.Add("Operation", "DeleteFile"); metadata.Add("Path", path); this.tracer.RelatedError(metadata, ex.Message); this.HasFailures = true; } } } private void HandleAllFileAddOperations() { using (FastFetchLibGit2Repo repo = new FastFetchLibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot)) { string availableBlob; while (this.AvailableBlobShas.TryTake(out availableBlob, Timeout.Infinite)) { if (this.HasFailures) { return; } Interlocked.Increment(ref this.shasReceived); HashSet paths; if (this.diff.FileAddOperations.TryRemove(availableBlob, out paths)) { try { long written; if (!repo.TryCopyBlobToFile(availableBlob, paths, out written)) { // TryCopyBlobTo emits an error event. this.HasFailures = true; } Interlocked.Add(ref this.bytesWritten, written); foreach (PathWithMode modeAndPath in paths) { this.AddedOrEditedLocalFiles.Add(modeAndPath.Path); if (Interlocked.Increment(ref this.fileWriteCount) % NumOperationsPerStatus == 0) { EventMetadata metadata = new EventMetadata(); metadata.Add("AvailableBlobsQueued", this.AvailableBlobShas.Count); metadata.Add("NumberBlobsNeeded", this.diff.FileAddOperations.Count); this.tracer.RelatedEvent(EventLevel.Informational, "CheckoutStatus", metadata); } } } catch (Exception ex) { EventMetadata errorData = new EventMetadata(); errorData.Add("Operation", "WriteFile"); this.tracer.RelatedError(errorData, ex.ToString()); this.HasFailures = true; } } } } } } } ================================================ FILE: GVFS/FastFetch/FastFetch.csproj ================================================ Exe net471 x64 true Microsoft400 false ================================================ FILE: GVFS/FastFetch/FastFetchLibGit2Repo.cs ================================================ using GVFS.Common.Git; using GVFS.Common.Prefetch.Git; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; namespace FastFetch { public class FastFetchLibGit2Repo : LibGit2Repo { public FastFetchLibGit2Repo(ITracer tracer, string repoPath) : base(tracer, repoPath) { } public virtual bool TryCopyBlobToFile(string sha, IEnumerable destinations, out long bytesWritten) { IntPtr objHandle; if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.ResultCode.Success) { bytesWritten = 0; EventMetadata metadata = new EventMetadata(); metadata.Add("ObjectSha", sha); this.Tracer.RelatedError(metadata, "Couldn't find object"); return false; } try { // Avoid marshalling raw content by using byte* and native writes unsafe { switch (Native.Object.GetType(objHandle)) { case Native.ObjectTypes.Blob: byte* originalData = Native.Blob.GetRawContent(objHandle); long originalSize = Native.Blob.GetRawSize(objHandle); foreach (PathWithMode destination in destinations) { NativeMethods.WriteFile(this.Tracer, originalData, originalSize, destination.Path, destination.Mode); } bytesWritten = originalSize * destinations.Count(); break; default: throw new NotSupportedException("Copying object types other than blobs is not supported."); } } } finally { Native.Object.Free(objHandle); } return true; } } } ================================================ FILE: GVFS/FastFetch/FastFetchVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.Prefetch; using GVFS.Common.Tracing; using System; namespace FastFetch { [Verb("fastfetch", HelpText = "Fast-fetch a branch")] public class FastFetchVerb { // Testing has shown that more than 16 download threads does not improve // performance even with 56 core machines with 40G NICs. More threads does // create more load on the servers as they have to handle extra connections. private const int MaxDefaultDownloadThreads = 16; private const int ExitFailure = 1; private const int ExitSuccess = 0; [Option( 'c', "commit", Required = false, HelpText = "Commit to fetch")] public string Commit { get; set; } [Option( 'b', "branch", Required = false, HelpText = "Branch to fetch")] public string Branch { get; set; } [Option( "cache-server-url", Required = false, Default = "", HelpText = "Defines the url of the cache server")] public string CacheServerUrl { get; set; } [Option( "chunk-size", Required = false, Default = 4000, HelpText = "Sets the number of objects to be downloaded in a single pack")] public int ChunkSize { get; set; } [Option( "checkout", Required = false, Default = false, HelpText = "Checkout the target commit into the working directory after fetching")] public bool Checkout { get; set; } [Option( "force-checkout", Required = false, Default = false, HelpText = "Force FastFetch to checkout content as if the current repo had just been initialized." + "This allows you to include more folders from the repo that were not originally checked out." + "Can only be used with the --checkout option.")] public bool ForceCheckout { get; set; } [Option( "search-thread-count", Required = false, Default = 0, HelpText = "Sets the number of threads to use for finding missing blobs. (0 for number of logical cores)")] public int SearchThreadCount { get; set; } [Option( "download-thread-count", Required = false, Default = 0, HelpText = "Sets the number of threads to use for downloading. (0 for number of logical cores)")] public int DownloadThreadCount { get; set; } [Option( "index-thread-count", Required = false, Default = 0, HelpText = "Sets the number of threads to use for indexing. (0 for number of logical cores)")] public int IndexThreadCount { get; set; } [Option( "checkout-thread-count", Required = false, Default = 0, HelpText = "Sets the number of threads to use for checkout. (0 for number of logical cores)")] public int CheckoutThreadCount { get; set; } [Option( 'r', "max-retries", Required = false, Default = 10, HelpText = "Sets the maximum number of attempts for downloading a pack")] public int MaxAttempts { get; set; } [Option( "git-path", Default = "", Required = false, HelpText = "Sets the path and filename for git.exe if it isn't expected to be on %PATH%.")] public string GitBinPath { get; set; } [Option( "folders", Required = false, Default = "", HelpText = "A semicolon-delimited list of folders to fetch")] public string FolderList { get; set; } [Option( "folders-list", Required = false, Default = "", HelpText = "A file containing line-delimited list of folders to fetch")] public string FolderListFile { get; set; } [Option( "Allow-index-metadata-update-from-working-tree", Required = false, Default = false, HelpText = "When specified, index metadata (file times and sizes) is updated from disk if not already in the index. " + "This flag should only be used when the working tree is known to be in a good state. " + "Do not use this flag if the working tree is not 100% known to be good as it would cause 'git status' to misreport.")] public bool AllowIndexMetadataUpdateFromWorkingTree { get; set; } [Option( "verbose", Required = false, Default = false, HelpText = "Show all outputs on the console in addition to writing them to a log file")] public bool Verbose { get; set; } [Option( "parent-activity-id", Required = false, Default = "", HelpText = "The GUID of the caller - used for telemetry purposes.")] public string ParentActivityId { get; set; } public void Execute() { Environment.ExitCode = this.ExecuteWithExitCode(); } private int ExecuteWithExitCode() { // CmdParser doesn't strip quotes, and Path.Combine will throw this.GitBinPath = this.GitBinPath.Replace("\"", string.Empty); if (!GVFSPlatform.Instance.GitInstallation.GitExists(this.GitBinPath)) { Console.WriteLine( "Could not find git.exe {0}", !string.IsNullOrWhiteSpace(this.GitBinPath) ? "at " + this.GitBinPath : "on %PATH%"); return ExitFailure; } if (this.Commit != null && this.Branch != null) { Console.WriteLine("Cannot specify both a commit sha and a branch name."); return ExitFailure; } if (this.ForceCheckout && !this.Checkout) { Console.WriteLine("Cannot use --force-checkout option without --checkout option."); return ExitFailure; } this.SearchThreadCount = this.SearchThreadCount > 0 ? this.SearchThreadCount : Environment.ProcessorCount; this.DownloadThreadCount = this.DownloadThreadCount > 0 ? this.DownloadThreadCount : Math.Min(Environment.ProcessorCount, MaxDefaultDownloadThreads); this.IndexThreadCount = this.IndexThreadCount > 0 ? this.IndexThreadCount : Environment.ProcessorCount; this.CheckoutThreadCount = this.CheckoutThreadCount > 0 ? this.CheckoutThreadCount : Environment.ProcessorCount; this.GitBinPath = !string.IsNullOrWhiteSpace(this.GitBinPath) ? this.GitBinPath : GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); GitEnlistment enlistment = GitEnlistment.CreateFromCurrentDirectory(this.GitBinPath); if (enlistment == null) { Console.WriteLine("Must be run within a git repo"); return ExitFailure; } string commitish = this.Commit ?? this.Branch; if (string.IsNullOrWhiteSpace(commitish)) { GitProcess.Result result = new GitProcess(enlistment).GetCurrentBranchName(); if (result.ExitCodeIsFailure || string.IsNullOrWhiteSpace(result.Output)) { Console.WriteLine("Could not retrieve current branch name: " + result.Errors); return ExitFailure; } commitish = result.Output.Trim(); } Guid parentActivityId = Guid.Empty; if (!string.IsNullOrWhiteSpace(this.ParentActivityId) && !Guid.TryParse(this.ParentActivityId, out parentActivityId)) { Console.WriteLine("The ParentActivityId provided (" + this.ParentActivityId + ") is not a valid GUID."); } using (JsonTracer tracer = new JsonTracer("Microsoft.Git.FastFetch", parentActivityId, "FastFetch", enlistmentId: null, mountId: null, disableTelemetry: true)) { if (this.Verbose) { tracer.AddDiagnosticConsoleEventListener(EventLevel.Informational, Keywords.Any); } else { tracer.AddPrettyConsoleEventListener(EventLevel.Error, Keywords.Any); } string fastfetchLogFile = Enlistment.GetNewLogFileName(enlistment.FastFetchLogRoot, "fastfetch"); tracer.AddLogFileEventListener(fastfetchLogFile, EventLevel.Informational, Keywords.Any); CacheServerInfo cacheServer = new CacheServerInfo(this.GetRemoteUrl(enlistment), null); tracer.WriteStartEvent( enlistment.EnlistmentRoot, enlistment.RepoUrl, cacheServer.Url, new EventMetadata { { "TargetCommitish", commitish }, { "Checkout", this.Checkout }, }); string error; if (!enlistment.Authentication.TryInitialize(tracer, enlistment, out error)) { tracer.RelatedError(error); Console.WriteLine(error); return ExitFailure; } RetryConfig retryConfig = new RetryConfig(this.MaxAttempts, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); BlobPrefetcher prefetcher = this.GetFolderPrefetcher(tracer, enlistment, cacheServer, retryConfig); if (!BlobPrefetcher.TryLoadFolderList(enlistment, this.FolderList, this.FolderListFile, prefetcher.FolderList, readListFromStdIn: false, error: out error)) { tracer.RelatedError(error); Console.WriteLine(error); return ExitFailure; } bool isSuccess; try { Func doPrefetch = () => { try { bool isBranch = this.Commit == null; prefetcher.Prefetch(commitish, isBranch); return !prefetcher.HasFailures; } catch (BlobPrefetcher.FetchException e) { tracer.RelatedError(e.Message); return false; } }; if (this.Verbose) { isSuccess = doPrefetch(); } else { isSuccess = ConsoleHelper.ShowStatusWhileRunning( doPrefetch, "Fetching", output: Console.Out, showSpinner: !Console.IsOutputRedirected, gvfsLogEnlistmentRoot: null); Console.WriteLine(); Console.WriteLine("See the full log at " + fastfetchLogFile); } isSuccess &= !prefetcher.HasFailures; } catch (AggregateException e) { isSuccess = false; foreach (Exception ex in e.Flatten().InnerExceptions) { tracer.RelatedError(ex.ToString()); } } catch (Exception e) { isSuccess = false; tracer.RelatedError(e.ToString()); } EventMetadata stopMetadata = new EventMetadata(); stopMetadata.Add("Success", isSuccess); tracer.Stop(stopMetadata); return isSuccess ? ExitSuccess : ExitFailure; } } private string GetRemoteUrl(Enlistment enlistment) { if (!string.IsNullOrWhiteSpace(this.CacheServerUrl)) { return this.CacheServerUrl; } string configuredUrl = CacheServerResolver.GetUrlFromConfig(enlistment); if (!string.IsNullOrWhiteSpace(configuredUrl)) { return configuredUrl; } return enlistment.RepoUrl; } private BlobPrefetcher GetFolderPrefetcher(ITracer tracer, Enlistment enlistment, CacheServerInfo cacheServer, RetryConfig retryConfig) { GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, cacheServer, retryConfig); if (this.Checkout) { return new CheckoutPrefetcher( tracer, enlistment, objectRequestor, this.ChunkSize, this.SearchThreadCount, this.DownloadThreadCount, this.IndexThreadCount, this.CheckoutThreadCount, this.AllowIndexMetadataUpdateFromWorkingTree, this.ForceCheckout); } else { return new BlobPrefetcher( tracer, enlistment, objectRequestor, this.ChunkSize, this.SearchThreadCount, this.DownloadThreadCount, this.IndexThreadCount); } } } } ================================================ FILE: GVFS/FastFetch/GitEnlistment.cs ================================================ using GVFS.Common; using System; using System.IO; namespace FastFetch { public class GitEnlistment : Enlistment { private GitEnlistment(string repoRoot, string gitBinPath) : base( repoRoot, repoRoot, repoRoot, null, gitBinPath, flushFileBuffersForPacks: false, authentication: null) { this.GitObjectsRoot = Path.Combine(repoRoot, GVFSConstants.DotGit.Objects.Root); this.LocalObjectsRoot = this.GitObjectsRoot; this.GitPackRoot = Path.Combine(this.GitObjectsRoot, GVFSConstants.DotGit.Objects.Pack.Name); } public override string GitObjectsRoot { get; protected set; } public override string LocalObjectsRoot { get; protected set; } public override string GitPackRoot { get; protected set; } public string FastFetchLogRoot { get { return Path.Combine(this.EnlistmentRoot, GVFSConstants.DotGit.Root, ".fastfetch"); } } public static GitEnlistment CreateFromCurrentDirectory(string gitBinPath) { string root = Paths.GetGitEnlistmentRoot(Environment.CurrentDirectory); if (root != null) { return new GitEnlistment(root, gitBinPath); } return null; } } } ================================================ FILE: GVFS/FastFetch/Index.cs ================================================ using GVFS.Common; using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.MemoryMappedFiles; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; namespace FastFetch { public class Index { // This versioning number lets us track compatibility with previous // versions of FastFetch regarding the index. This should be bumped // when the index older versions of fastfetch created may not be compatible private const int CurrentFastFetchIndexVersion = 1; // Constants used for parsing an index entry private const ushort ExtendedBit = 0x4000; private const ushort SkipWorktreeBit = 0x4000; private const int BaseEntryLength = 62; // Buffer used to get path from index entry private const int MaxPathBufferSize = 4096; // Index default names private const string UpdatedIndexName = "index.updated"; private static readonly byte[] MagicSignature = new byte[] { (byte)'D', (byte)'I', (byte)'R', (byte)'C' }; // Location of the version marker file private readonly string versionMarkerFile; private readonly bool readOnly; private readonly string indexPath; private readonly ITracer tracer; private readonly string repoRoot; private Dictionary indexEntryOffsets; private uint entryCount; /// /// Creates a new Index object to parse the specified index file /// public Index( string repoRoot, ITracer tracer, string indexFullPath, bool readOnly) { this.tracer = tracer; this.repoRoot = repoRoot; this.indexPath = indexFullPath; this.readOnly = readOnly; this.versionMarkerFile = Path.Combine(this.repoRoot, GVFSConstants.DotGit.Root, ".fastfetch", "VersionMarker"); } public uint IndexVersion { get; private set; } /// /// Updates entries in the current index with file sizes and times /// Algorithm: /// 1) If there was an index in place when this object was constructed, then: /// a) Copy all valid entries (below) from the previous index to the new index /// b) Conditionally (below) get times/sizes from the working tree for files not updated from the previous index /// /// 2) If there was no index in place, conditionally populate all entries from disk /// /// Conditions: /// - Working tree is only searched if allowUpdateFromWorkingTree is specified /// - A valid entry is an entry that exist and has a non-zero creation time (ctime) /// /// A collection of added or edited files /// Set to true if the working tree is known good and can be used during the update. /// An optional index to source entry values from public void UpdateFileSizesAndTimes(BlockingCollection addedOrEditedLocalFiles, bool allowUpdateFromWorkingTree, bool shouldSignIndex, Index sourceIndex = null) { if (this.readOnly) { throw new InvalidOperationException("Cannot update a readonly index."); } using (ITracer activity = this.tracer.StartActivity("UpdateFileSizesAndTimes", EventLevel.Informational, Keywords.Telemetry, null)) { this.Parse(); bool anyEntriesUpdated = false; using (MemoryMappedFile mmf = this.GetMemoryMappedFile()) using (MemoryMappedViewAccessor indexView = mmf.CreateViewAccessor()) { // Only populate from the previous index if we believe it's good to populate from // For now, a current FastFetch version marker is the only criteria if (sourceIndex != null) { if (this.IsFastFetchVersionMarkerCurrent()) { using (this.tracer.StartActivity("UpdateFileInformationFromPreviousIndex", EventLevel.Informational, Keywords.Telemetry, null)) { anyEntriesUpdated |= this.UpdateFileInformationForAllEntries(indexView, sourceIndex, allowUpdateFromWorkingTree); } if (addedOrEditedLocalFiles != null) { // always update these files from disk or the index won't have good information // for them and they'll show as modified even those not actually modified. anyEntriesUpdated |= this.UpdateFileInformationFromDiskForFiles(indexView, addedOrEditedLocalFiles); } } } else if (allowUpdateFromWorkingTree) { // If we didn't update from a previous index, update from the working tree if allowed. anyEntriesUpdated |= this.UpdateFileInformationFromWorkingTree(indexView); } indexView.Flush(); } if (shouldSignIndex) { this.SignIndex(); } } } public void Parse() { using (ITracer activity = this.tracer.StartActivity("ParseIndex", EventLevel.Informational, Keywords.Telemetry, new EventMetadata() { { "Index", this.indexPath } })) { using (Stream indexStream = new FileStream(this.indexPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { this.ParseIndex(indexStream); } } } private static string FromDotnetFullPathToGitRelativePath(string path, string repoRoot) { return path.Substring(repoRoot.Length).TrimStart(Path.DirectorySeparatorChar).Replace(Path.DirectorySeparatorChar, GVFSConstants.GitPathSeparator); } private static string FromGitRelativePathToDotnetFullPath(string path, string repoRoot) { return Path.Combine(repoRoot, path.Replace(GVFSConstants.GitPathSeparator, Path.DirectorySeparatorChar)); } private MemoryMappedFile GetMemoryMappedFile() { return MemoryMappedFile.CreateFromFile(this.indexPath, FileMode.Open); } private bool UpdateFileInformationFromWorkingTree(MemoryMappedViewAccessor indexView) { long updatedEntries = 0; using (ITracer activity = this.tracer.StartActivity("UpdateFileInformationFromWorkingTree", EventLevel.Informational, Keywords.Telemetry, null)) { WorkingTree.ForAllFiles( this.repoRoot, (path, files) => { foreach (FileInfo file in files) { string gitPath = FromDotnetFullPathToGitRelativePath(file.FullName, this.repoRoot); long offset; if (this.indexEntryOffsets.TryGetValue(gitPath, out offset)) { if (NativeMethods.TryStatFileAndUpdateIndex(this.tracer, gitPath, indexView, offset)) { Interlocked.Increment(ref updatedEntries); } } } }); } return updatedEntries > 0; } private bool UpdateFileInformationFromDiskForFiles(MemoryMappedViewAccessor indexView, BlockingCollection addedOrEditedLocalFiles) { long updatedEntriesFromDisk = 0; using (ITracer activity = this.tracer.StartActivity("UpdateDownloadedFiles", EventLevel.Informational, Keywords.Telemetry, null)) { Parallel.ForEach( addedOrEditedLocalFiles, (localPath) => { string gitPath = localPath.Replace(Path.DirectorySeparatorChar, GVFSConstants.GitPathSeparator); long offset; if (this.indexEntryOffsets.TryGetValue(gitPath, out offset)) { if (NativeMethods.TryStatFileAndUpdateIndex(this.tracer, gitPath, indexView, offset)) { Interlocked.Increment(ref updatedEntriesFromDisk); } else { this.tracer.RelatedError($"{nameof(this.UpdateFileInformationFromDiskForFiles)}: Failed to update file information from disk for file {0}", gitPath); } } }); } this.tracer.RelatedEvent(EventLevel.Informational, "UpdateIndexFileInformation", new EventMetadata() { { "UpdatedFromDisk", updatedEntriesFromDisk } }, Keywords.Telemetry); return updatedEntriesFromDisk > 0; } private bool UpdateFileInformationForAllEntries(MemoryMappedViewAccessor indexView, Index otherIndex, bool shouldAlsoTryPopulateFromDisk) { long updatedEntriesFromOtherIndex = 0; long updatedEntriesFromDisk = 0; using (MemoryMappedFile mmf = otherIndex.GetMemoryMappedFile()) using (MemoryMappedViewAccessor otherIndexView = mmf.CreateViewAccessor()) { Parallel.ForEach( this.indexEntryOffsets, entry => { string currentIndexFilename = entry.Key; long currentIndexOffset = entry.Value; if (!IndexEntry.HasInitializedCTimeEntry(indexView, currentIndexOffset)) { long otherIndexOffset; if (otherIndex.indexEntryOffsets.TryGetValue(currentIndexFilename, out otherIndexOffset)) { if (IndexEntry.HasInitializedCTimeEntry(otherIndexView, otherIndexOffset)) { IndexEntry currentIndexEntry = new IndexEntry(indexView, currentIndexOffset); IndexEntry otherIndexEntry = new IndexEntry(otherIndexView, otherIndexOffset); currentIndexEntry.CtimeSeconds = otherIndexEntry.CtimeSeconds; currentIndexEntry.CtimeNanosecondFraction = otherIndexEntry.CtimeNanosecondFraction; currentIndexEntry.MtimeSeconds = otherIndexEntry.MtimeSeconds; currentIndexEntry.MtimeNanosecondFraction = otherIndexEntry.MtimeNanosecondFraction; currentIndexEntry.Dev = otherIndexEntry.Dev; currentIndexEntry.Ino = otherIndexEntry.Ino; currentIndexEntry.Uid = otherIndexEntry.Uid; currentIndexEntry.Gid = otherIndexEntry.Gid; currentIndexEntry.Size = otherIndexEntry.Size; Interlocked.Increment(ref updatedEntriesFromOtherIndex); } } else if (shouldAlsoTryPopulateFromDisk) { string localPath = FromGitRelativePathToDotnetFullPath(currentIndexFilename, this.repoRoot); if (NativeMethods.TryStatFileAndUpdateIndex(this.tracer, localPath, indexView, entry.Value)) { Interlocked.Increment(ref updatedEntriesFromDisk); } } } }); } this.tracer.RelatedEvent( EventLevel.Informational, "UpdateIndexFileInformation", new EventMetadata() { { "UpdatedFromOtherIndex", updatedEntriesFromOtherIndex }, { "UpdatedFromDisk", updatedEntriesFromDisk } }, Keywords.Telemetry); return (updatedEntriesFromOtherIndex > 0) || (updatedEntriesFromDisk > 0); } private void SignIndex() { using (ITracer activity = this.tracer.StartActivity("SignIndex", EventLevel.Informational, Keywords.Telemetry, metadata: null)) { using (FileStream fs = File.Open(this.indexPath, FileMode.Open, FileAccess.ReadWrite)) { // Truncate the old hash off. The Index class is expected to preserve any existing hash. fs.SetLength(fs.Length - 20); using (HashingStream hashStream = new HashingStream(fs)) { fs.Position = 0; hashStream.CopyTo(Stream.Null); byte[] hash = hashStream.Hash; // The fs pointer is now where the old hash used to be. Perfect. :) fs.Write(hash, 0, hash.Length); } } } } public void WriteFastFetchIndexVersionMarker() { if (File.Exists(this.versionMarkerFile)) { File.SetAttributes(this.versionMarkerFile, FileAttributes.Normal); } Directory.CreateDirectory(Path.GetDirectoryName(this.versionMarkerFile)); File.WriteAllText(this.versionMarkerFile, CurrentFastFetchIndexVersion.ToString(), Encoding.ASCII); File.SetAttributes(this.versionMarkerFile, FileAttributes.ReadOnly); this.tracer.RelatedEvent(EventLevel.Informational, "MarkerWritten", new EventMetadata() { { "Version", CurrentFastFetchIndexVersion } }); } private bool IsFastFetchVersionMarkerCurrent() { if (File.Exists(this.versionMarkerFile)) { int version; string marker = File.ReadAllText(this.versionMarkerFile, Encoding.ASCII); bool isMarkerCurrent = int.TryParse(marker, out version) && (version == CurrentFastFetchIndexVersion); this.tracer.RelatedEvent(EventLevel.Informational, "PreviousMarker", new EventMetadata() { { "Content", marker }, { "IsCurrent", isMarkerCurrent } }, Keywords.Telemetry); return isMarkerCurrent; } this.tracer.RelatedEvent(EventLevel.Informational, "NoPreviousMarkerFound", null, Keywords.Telemetry); return false; } private void ParseIndex(Stream indexStream) { byte[] buffer = new byte[40]; indexStream.Position = 0; byte[] signature = new byte[4]; indexStream.Read(signature, 0, 4); if (!Enumerable.SequenceEqual(MagicSignature, signature)) { throw new InvalidDataException("Incorrect magic signature for index: " + string.Join(string.Empty, signature.Select(c => (char)c))); } this.IndexVersion = this.ReadUInt32(buffer, indexStream); if (this.IndexVersion < 2 || this.IndexVersion > 4) { throw new InvalidDataException("Unsupported index version: " + this.IndexVersion); } this.entryCount = this.ReadUInt32(buffer, indexStream); this.tracer.RelatedEvent(EventLevel.Informational, "IndexData", new EventMetadata() { { "Index", this.indexPath }, { "Version", this.IndexVersion }, { "entryCount", this.entryCount } }, Keywords.Telemetry); this.indexEntryOffsets = new Dictionary((int)this.entryCount, GVFSPlatform.Instance.Constants.PathComparer); int previousPathLength = 0; byte[] pathBuffer = new byte[MaxPathBufferSize]; for (int i = 0; i < this.entryCount; i++) { // See https://github.com/git/git/blob/867b1c1bf68363bcfd17667d6d4b9031fa6a1300/Documentation/technical/index-format.txt#L38 long entryOffset = indexStream.Position; int entryLength = BaseEntryLength; // Skip the next 60 bytes. // 40 bytes encapsulated by IndexEntry but not needed now. // 20 bytes of sha indexStream.Position += 60; ushort flags = this.ReadUInt16(buffer, indexStream); bool isExtended = (flags & ExtendedBit) == ExtendedBit; ushort pathLength = (ushort)(flags & 0xFFF); entryLength += pathLength; bool skipWorktree = false; if (isExtended && (this.IndexVersion > 2)) { ushort extendedFlags = this.ReadUInt16(buffer, indexStream); skipWorktree = (extendedFlags & SkipWorktreeBit) == SkipWorktreeBit; entryLength += 2; } if (this.IndexVersion == 4) { int replaceLength = this.ReadReplaceLength(indexStream); int replaceIndex = previousPathLength - replaceLength; indexStream.Read(pathBuffer, replaceIndex, pathLength - replaceIndex + 1); previousPathLength = pathLength; } else { // Simple paths but 1 - 8 nul bytes as necessary to pad the entry to a multiple of eight bytes int numNulBytes = 8 - (entryLength % 8); indexStream.Read(pathBuffer, 0, pathLength + numNulBytes); } if (!skipWorktree) { // Examine only the things we're not skipping... // Potential Future Perf Optimization: Perform this work on multiple threads. If we take the first byte and % by number of threads, // we can ensure that all entries for a given folder end up in the same dictionary string path = Encoding.UTF8.GetString(pathBuffer, 0, pathLength); this.indexEntryOffsets[path] = entryOffset; } } } /// /// Get the length of the replacement string. For definition of data, see: /// https://github.com/git/git/blob/867b1c1bf68363bcfd17667d6d4b9031fa6a1300/Documentation/technical/index-format.txt#L38 /// /// stream to read bytes from /// private int ReadReplaceLength(Stream stream) { int headerByte = stream.ReadByte(); int offset = headerByte & 0x7f; // Terminate the loop when the high bit is no longer set. for (int i = 0; (headerByte & 0x80) != 0; i++) { headerByte = stream.ReadByte(); if (headerByte < 0) { throw new EndOfStreamException("Index file has been truncated."); } offset += 1; offset = (offset << 7) + (headerByte & 0x7f); } return offset; } private uint ReadUInt32(byte[] buffer, Stream stream) { buffer[3] = (byte)stream.ReadByte(); buffer[2] = (byte)stream.ReadByte(); buffer[1] = (byte)stream.ReadByte(); buffer[0] = (byte)stream.ReadByte(); return BitConverter.ToUInt32(buffer, 0); } private ushort ReadUInt16(byte[] buffer, Stream stream) { buffer[1] = (byte)stream.ReadByte(); buffer[0] = (byte)stream.ReadByte(); // (ushort)BitConverter.ToInt16 avoids the running the duplicated checks in ToUInt16 return (ushort)BitConverter.ToInt16(buffer, 0); } /// /// Private helper class to read/write specific values from a Git Index entry based on offset in a view. /// internal class IndexEntry { private const long UnixEpochMilliseconds = 116444736000000000; private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private MemoryMappedViewAccessor indexView; public IndexEntry(MemoryMappedViewAccessor indexView, long offset) { this.indexView = indexView; this.Offset = offset; } // EntryOffsets is the offset from the start of a index entry where specific data exists // For more information about the layout of git index entries, see: // https://github.com/git/git/blob/867b1c1bf68363bcfd17667d6d4b9031fa6a1300/Documentation/technical/index-format.txt#L38 private enum EntryOffsets { ctimeSeconds = 0, ctimeNanoseconds = 4, mtimeSeconds = 8, mtimeNanoseconds = 12, dev = 16, ino = 20, uid = 28, gid = 32, filesize = 36, flags = 80, extendedFlags = 82, } public long Offset { get; set; } public uint CtimeSeconds { get { return this.ReadUInt32(EntryOffsets.ctimeSeconds); } set { this.WriteUInt32(EntryOffsets.ctimeSeconds, value); } } public uint CtimeNanosecondFraction { get { return this.ReadUInt32(EntryOffsets.ctimeNanoseconds); } set { this.WriteUInt32(EntryOffsets.ctimeNanoseconds, value); } } public DateTime Ctime { get { return this.ToDotnetTime(this.CtimeSeconds, this.CtimeNanosecondFraction); } set { IndexEntryTime time = this.ToGitTime(value); this.CtimeSeconds = time.Seconds; this.CtimeNanosecondFraction = time.NanosecondFraction; } } public uint MtimeSeconds { get { return this.ReadUInt32(EntryOffsets.mtimeSeconds); } set { this.WriteUInt32(EntryOffsets.mtimeSeconds, value); } } public uint MtimeNanosecondFraction { get { return this.ReadUInt32(EntryOffsets.mtimeNanoseconds); } set { this.WriteUInt32(EntryOffsets.mtimeNanoseconds, value); } } public DateTime Mtime { get { return this.ToDotnetTime(this.MtimeSeconds, this.MtimeNanosecondFraction); } set { IndexEntryTime times = this.ToGitTime(value); this.MtimeSeconds = times.Seconds; this.MtimeNanosecondFraction = times.NanosecondFraction; } } public uint Size { get { return this.ReadUInt32(EntryOffsets.filesize); } set { this.WriteUInt32(EntryOffsets.filesize, value); } } public uint Dev { get { return this.ReadUInt32(EntryOffsets.dev); } set { this.WriteUInt32(EntryOffsets.dev, value); } } public uint Ino { get { return this.ReadUInt32(EntryOffsets.ino); } set { this.WriteUInt32(EntryOffsets.ino, value); } } public uint Uid { get { return this.ReadUInt32(EntryOffsets.uid); } set { this.WriteUInt32(EntryOffsets.uid, value); } } public uint Gid { get { return this.ReadUInt32(EntryOffsets.gid); } set { this.WriteUInt32(EntryOffsets.gid, value); } } public ushort Flags { get { return this.ReadUInt16(EntryOffsets.flags); } set { this.WriteUInt16(EntryOffsets.flags, value); } } public bool IsExtended { get { return (this.Flags & Index.ExtendedBit) == Index.ExtendedBit; } } public static bool HasInitializedCTimeEntry(MemoryMappedViewAccessor indexView, long offset) { return EndianHelper.Swap(indexView.ReadUInt32(offset + (long)EntryOffsets.ctimeSeconds)) != 0; } private uint ReadUInt32(EntryOffsets fromOffset) { return EndianHelper.Swap(this.indexView.ReadUInt32(this.Offset + (long)fromOffset)); } private void WriteUInt32(EntryOffsets fromOffset, uint data) { this.indexView.Write(this.Offset + (long)fromOffset, EndianHelper.Swap(data)); } private ushort ReadUInt16(EntryOffsets fromOffset) { return EndianHelper.Swap(this.indexView.ReadUInt16(this.Offset + (long)fromOffset)); } private void WriteUInt16(EntryOffsets fromOffset, ushort data) { this.indexView.Write(this.Offset + (long)fromOffset, EndianHelper.Swap(data)); } private IndexEntryTime ToGitTime(DateTime datetime) { if (datetime > UnixEpoch) { // Using the same FileTime -> Unix time conversion that Git uses. long unixEpochRelativeNanoseconds = datetime.ToFileTime() - IndexEntry.UnixEpochMilliseconds; uint wholeSeconds = (uint)(unixEpochRelativeNanoseconds / (long)10000000); uint nanosecondFraction = (uint)((unixEpochRelativeNanoseconds % 10000000) * 100); return new IndexEntryTime() { Seconds = wholeSeconds, NanosecondFraction = nanosecondFraction }; } else { return new IndexEntryTime() { Seconds = 0, NanosecondFraction = 0 }; } } private DateTime ToDotnetTime(uint seconds, uint nanosecondFraction) { DateTime time = UnixEpoch.AddSeconds(seconds).AddMilliseconds(nanosecondFraction / 1000000); return time; } private class IndexEntryTime { public uint Seconds { get; set; } public uint NanosecondFraction { get; set; } } } } } ================================================ FILE: GVFS/FastFetch/IndexLock.cs ================================================ using GVFS.Common; using GVFS.Common.Git; using GVFS.Common.Prefetch; using GVFS.Common.Tracing; using System; using System.IO; namespace FastFetch { /// /// A mechanism for holding the 'index.lock' on a repository for the time it takes to update the index /// and working tree. It attempts to create the file in the constructor and throws if that fails. /// It closes and deletes index.lock on dispose. /// /// /// /// This class should not have to exist. If FastFetch was in compliance with the git way of doing /// business, then would work like this: /// /// /// /// It would open index.lock like this does - with CreateNew, before it started messing with the working tree. /// /// /// It would have just one class responsible for writing the new index into index.lock (now it has two, /// and ). And this combined class would write in the /// file size and timestamp information from the appropriate sources as it goes. /// /// /// It would then reread index.lock (without closing it) and calculate the hash. /// /// /// It would then delete the old index file, close index.lock, and move it to index. /// /// /// /// This is all in contrast to how it works now, where it has separate operations for updating /// the working tree, creating an index with no size/timestamp information, and then rewriting /// it with that information. /// /// /// This class is just a bodge job to make it so that we can leave the code pretty much as-is (and reduce /// the risk of breaking things) and still get the protection we need against simultaneous git commands /// being run. /// /// public class IndexLock : IDisposable { private string lockFilePath; private FileStream lockFileStream; public IndexLock(string repositoryRoot, ITracer tracer) { this.lockFilePath = Path.Combine(repositoryRoot, GVFSConstants.DotGit.IndexLock); try { this.lockFileStream = File.Open(lockFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None); } catch (Exception ex) { tracer.RelatedError("Unable to create: {0}: {1}", lockFilePath, ex.Message); throw new BlobPrefetcher.FetchException("Could not acquire index.lock."); } } /// > public void Dispose() { if (this.lockFilePath == null) { return; } if (this.lockFileStream == null) { throw new ObjectDisposedException(nameof(IndexLock)); } this.lockFileStream.Dispose(); this.lockFileStream = null; File.Delete(this.lockFilePath); this.lockFilePath = null; } } } ================================================ FILE: GVFS/FastFetch/NativeMethods.cs ================================================ using GVFS.Common.Tracing; using Microsoft.Win32.SafeHandles; using System; using System.ComponentModel; using System.IO; using System.IO.MemoryMappedFiles; using System.Runtime.InteropServices; namespace FastFetch { internal static class NativeMethods { private const int AccessDeniedWin32Error = 5; public static unsafe void WriteFile(ITracer tracer, byte* originalData, long originalSize, string destination, ushort mode) { try { using (SafeFileHandle fileHandle = OpenForWrite(tracer, destination)) { if (fileHandle.IsInvalid) { throw new Win32Exception(Marshal.GetLastWin32Error()); } byte* data = originalData; long size = originalSize; uint written = 0; while (size > 0) { uint toWrite = size < uint.MaxValue ? (uint)size : uint.MaxValue; if (!WriteFile(fileHandle, data, toWrite, out written, IntPtr.Zero)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } size -= written; data = data + written; } } } catch (Exception e) { EventMetadata metadata = new EventMetadata(); metadata.Add("destination", destination); metadata.Add("exception", e.ToString()); tracer.RelatedError(metadata, "Error writing file."); throw; } } public static bool TryStatFileAndUpdateIndex(ITracer tracer, string path, MemoryMappedViewAccessor indexView, long offset) { try { FileInfo file = new FileInfo(path); if (file.Exists) { Index.IndexEntry indexEntry = new Index.IndexEntry(indexView, offset); indexEntry.Mtime = file.LastWriteTimeUtc; indexEntry.Ctime = file.CreationTimeUtc; indexEntry.Size = (uint)file.Length; return true; } } catch (System.Security.SecurityException) { // Skip these. } catch (System.UnauthorizedAccessException) { // Skip these. } return false; } private static SafeFileHandle OpenForWrite(ITracer tracer, string fileName) { SafeFileHandle handle = CreateFile(fileName, FileAccess.Write, FileShare.None, IntPtr.Zero, FileMode.Create, FileAttributes.Normal, IntPtr.Zero); if (handle.IsInvalid) { // If we get a access denied, try reverting the acls to defaults inherited by parent if (Marshal.GetLastWin32Error() == AccessDeniedWin32Error) { tracer.RelatedEvent( EventLevel.Warning, "FailedOpenForWrite", new EventMetadata { { TracingConstants.MessageKey.WarningMessage, "Received access denied. Attempting to delete." }, { "FileName", fileName } }); File.SetAttributes(fileName, FileAttributes.Normal); File.Delete(fileName); handle = CreateFile(fileName, FileAccess.Write, FileShare.None, IntPtr.Zero, FileMode.Create, FileAttributes.Normal, IntPtr.Zero); } } return handle; } [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern SafeFileHandle CreateFile( [MarshalAs(UnmanagedType.LPTStr)] string filename, [MarshalAs(UnmanagedType.U4)] FileAccess access, [MarshalAs(UnmanagedType.U4)] FileShare share, IntPtr securityAttributes, // optional SECURITY_ATTRIBUTES struct or IntPtr.Zero [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes, IntPtr templateFile); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static unsafe extern bool WriteFile( SafeFileHandle file, byte* buffer, uint numberOfBytesToWrite, out uint numberOfBytesWritten, IntPtr overlapped); } } ================================================ FILE: GVFS/FastFetch/Program.cs ================================================ using CommandLine; using GVFS.PlatformLoader; namespace FastFetch { public class Program { public static void Main(string[] args) { GVFSPlatformLoader.Initialize(); Parser.Default.ParseArguments(args) .WithParsed(fastFetch => fastFetch.Execute()); } } } ================================================ FILE: GVFS/FastFetch/WorkingTree.cs ================================================ using System; using System.IO; using System.Linq; using System.Threading.Tasks; using GVFS.Common; namespace FastFetch { public static class WorkingTree { /// /// Enumerates all files in the working tree in a asynchronous parallel manner. /// Files found are sent to callback in chunks. /// Make no assumptions about ordering or how big chunks will be /// /// /// public static void ForAllFiles(string repoRoot, Action asyncParallelCallback) { ForAllDirectories(new DirectoryInfo(repoRoot), asyncParallelCallback); } public static void ForAllDirectories(DirectoryInfo dir, Action asyncParallelCallback) { asyncParallelCallback(dir.FullName, dir.GetFiles()); Parallel.ForEach( dir.EnumerateDirectories().Where(subdir => (!subdir.Name.Equals(GVFSConstants.DotGit.Root, GVFSPlatform.Instance.Constants.PathComparison) && !subdir.Attributes.HasFlag(FileAttributes.ReparsePoint))), subdir => { ForAllDirectories(subdir, asyncParallelCallback); }); } } } ================================================ FILE: GVFS/GVFS/CommandLine/CacheServerVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.Http; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.Linq; namespace GVFS.CommandLine { [Verb(CacheVerbName, HelpText = "Manages the cache server configuration for an existing repo.")] public class CacheServerVerb : GVFSVerb.ForExistingEnlistment { private const string CacheVerbName = "cache-server"; [Option( "set", Default = null, Required = false, HelpText = "Sets the cache server to the supplied name or url")] public string CacheToSet { get; set; } [Option("get", Required = false, HelpText = "Outputs the current cache server information. This is the default.")] public bool OutputCurrentInfo { get; set; } [Option( "list", Required = false, HelpText = "List available cache servers for the remote repo")] public bool ListCacheServers { get; set; } protected override string VerbName { get { return CacheVerbName; } } protected override void Execute(GVFSEnlistment enlistment) { this.BlockEmptyCacheServerUrl(this.CacheToSet); RetryConfig retryConfig = new RetryConfig(RetryConfig.DefaultMaxRetries, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); using (ITracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "CacheVerb")) { CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); ServerGVFSConfig serverGVFSConfig = null; string error = null; // Handle the three operation types: list, set, and get (default) if (this.ListCacheServers) { // For listing, require config endpoint to succeed (no fallback) if (!this.TryAuthenticateAndQueryGVFSConfig( tracer, enlistment, retryConfig, out serverGVFSConfig, out error)) { this.ReportErrorAndExit(tracer, "Unable to query /gvfs/config" + Environment.NewLine + error); } List cacheServers = serverGVFSConfig.CacheServers.ToList(); if (cacheServers != null && cacheServers.Any()) { this.Output.WriteLine(); this.Output.WriteLine("Available cache servers for: " + enlistment.RepoUrl); foreach (CacheServerInfo cacheServerInfo in cacheServers) { this.Output.WriteLine(cacheServerInfo); } } else { this.Output.WriteLine("There are no available cache servers for: " + enlistment.RepoUrl); } } else if (this.CacheToSet != null) { // Setting a new cache server CacheServerInfo cacheServer = cacheServerResolver.ParseUrlOrFriendlyName(this.CacheToSet); // For set operation, allow fallback if config endpoint fails but cache server URL is valid if (!this.TryAuthenticateAndQueryGVFSConfig( tracer, enlistment, retryConfig, out serverGVFSConfig, out error, fallbackCacheServer: cacheServer)) { this.ReportErrorAndExit(tracer, "Authentication failed: " + error); } cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); if (!cacheServerResolver.TrySaveUrlToLocalConfig(cacheServer, out error)) { this.ReportErrorAndExit("Failed to save cache to config: " + error); } this.Output.WriteLine("You must remount GVFS for this to take effect."); } else { // Default operation: get current cache server info CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); // For get operation, allow fallback if config endpoint fails but cache server URL is valid if (!this.TryAuthenticateAndQueryGVFSConfig( tracer, enlistment, retryConfig, out serverGVFSConfig, out error, fallbackCacheServer: cacheServer)) { this.ReportErrorAndExit(tracer, "Authentication failed: " + error); } CacheServerInfo resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverGVFSConfig); this.Output.WriteLine("Using cache server: " + resolvedCacheServer); } } } } } ================================================ FILE: GVFS/GVFS/CommandLine/CacheVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using System; using System.Globalization; using System.IO; namespace GVFS.CommandLine { [Verb(CacheVerb.CacheVerbName, HelpText = "Display information about the GVFS shared object cache")] public class CacheVerb : GVFSVerb.ForExistingEnlistment { private const string CacheVerbName = "cache"; public CacheVerb() { } protected override string VerbName { get { return CacheVerbName; } } protected override void Execute(GVFSEnlistment enlistment) { using (ITracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "CacheVerb")) { string localCacheRoot; string gitObjectsRoot; this.GetLocalCachePaths(tracer, enlistment, out localCacheRoot, out gitObjectsRoot); if (string.IsNullOrWhiteSpace(gitObjectsRoot)) { this.ReportErrorAndExit("Could not determine git objects root. Is this a GVFS enlistment with a shared cache?"); } this.Output.WriteLine("Repo URL: " + enlistment.RepoUrl); this.Output.WriteLine("Cache root: " + (localCacheRoot ?? "(unknown)")); this.Output.WriteLine("Git objects: " + gitObjectsRoot); string packRoot = Path.Combine(gitObjectsRoot, GVFSConstants.DotGit.Objects.Pack.Name); if (!Directory.Exists(packRoot)) { this.Output.WriteLine(); this.Output.WriteLine("Pack directory not found: " + packRoot); tracer.RelatedError("Pack directory not found: " + packRoot); return; } int prefetchPackCount; long prefetchPackSize; int otherPackCount; long otherPackSize; long latestPrefetchTimestamp; this.GetPackSummary(packRoot, out prefetchPackCount, out prefetchPackSize, out otherPackCount, out otherPackSize, out latestPrefetchTimestamp); int looseObjectCount = this.CountLooseObjects(gitObjectsRoot); long totalSize = prefetchPackSize + otherPackSize; this.Output.WriteLine(); this.Output.WriteLine("Total pack size: " + this.FormatSizeForUserDisplay(totalSize)); this.Output.WriteLine("Prefetch packs: " + prefetchPackCount + " (" + this.FormatSizeForUserDisplay(prefetchPackSize) + ")"); this.Output.WriteLine("Other packs: " + otherPackCount + " (" + this.FormatSizeForUserDisplay(otherPackSize) + ")"); if (latestPrefetchTimestamp > 0) { try { DateTimeOffset latestTime = DateTimeOffset.FromUnixTimeSeconds(latestPrefetchTimestamp).ToLocalTime(); this.Output.WriteLine("Latest prefetch: " + latestTime.ToString("yyyy-MM-dd HH:mm:ss zzz")); } catch (ArgumentOutOfRangeException) { tracer.RelatedWarning("Prefetch timestamp out of range: " + latestPrefetchTimestamp); } } this.Output.WriteLine("Loose objects: " + looseObjectCount.ToString("N0")); EventMetadata metadata = new EventMetadata(); metadata.Add("repoUrl", enlistment.RepoUrl); metadata.Add("localCacheRoot", localCacheRoot); metadata.Add("gitObjectsRoot", gitObjectsRoot); metadata.Add("prefetchPackCount", prefetchPackCount); metadata.Add("prefetchPackSize", prefetchPackSize); metadata.Add("otherPackCount", otherPackCount); metadata.Add("otherPackSize", otherPackSize); metadata.Add("latestPrefetchTimestamp", latestPrefetchTimestamp); metadata.Add("looseObjectCount", looseObjectCount); tracer.RelatedEvent(EventLevel.Informational, "CacheInfo", metadata, Keywords.Telemetry); } } internal void GetPackSummary( string packRoot, out int prefetchPackCount, out long prefetchPackSize, out int otherPackCount, out long otherPackSize, out long latestPrefetchTimestamp) { prefetchPackCount = 0; prefetchPackSize = 0; otherPackCount = 0; otherPackSize = 0; latestPrefetchTimestamp = 0; string[] packFiles = Directory.GetFiles(packRoot, "*.pack"); foreach (string packFile in packFiles) { long length; try { length = new FileInfo(packFile).Length; } catch (IOException) { continue; } string fileName = Path.GetFileName(packFile); if (fileName.StartsWith(GVFSConstants.PrefetchPackPrefix, StringComparison.OrdinalIgnoreCase)) { prefetchPackCount++; prefetchPackSize += length; long? timestamp = this.TryGetPrefetchTimestamp(packFile); if (timestamp.HasValue && timestamp.Value > latestPrefetchTimestamp) { latestPrefetchTimestamp = timestamp.Value; } } else { otherPackCount++; otherPackSize += length; } } } internal int CountLooseObjects(string gitObjectsRoot) { int looseObjectCount = 0; for (int i = 0; i < 256; i++) { string hexDir = Path.Combine(gitObjectsRoot, i.ToString("x2")); if (Directory.Exists(hexDir)) { try { looseObjectCount += Directory.GetFiles(hexDir).Length; } catch (IOException) { } } } return looseObjectCount; } private long? TryGetPrefetchTimestamp(string packPath) { string filename = Path.GetFileName(packPath); string[] parts = filename.Split('-'); if (parts.Length > 1 && long.TryParse(parts[1], out long timestamp)) { return timestamp; } return null; } internal string FormatSizeForUserDisplay(long bytes) { if (bytes >= 1L << 30) { return string.Format(CultureInfo.CurrentCulture, "{0:F1} GB", bytes / (double)(1L << 30)); } if (bytes >= 1L << 20) { return string.Format(CultureInfo.CurrentCulture, "{0:F1} MB", bytes / (double)(1L << 20)); } if (bytes >= 1L << 10) { return string.Format(CultureInfo.CurrentCulture, "{0:F1} KB", bytes / (double)(1L << 10)); } return bytes + " bytes"; } private void GetLocalCachePaths(ITracer tracer, GVFSEnlistment enlistment, out string localCacheRoot, out string gitObjectsRoot) { localCacheRoot = null; gitObjectsRoot = null; try { string error; if (RepoMetadata.TryInitialize(tracer, Path.Combine(enlistment.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot), out error)) { if (!RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error)) { tracer.RelatedWarning("Failed to read local cache root: " + error); } if (!RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error)) { tracer.RelatedWarning("Failed to read git objects root: " + error); } } else { this.ReportErrorAndExit("Failed to read repo metadata: " + error); } } catch (Exception e) { this.ReportErrorAndExit("Failed to read repo metadata: " + e.Message); } finally { RepoMetadata.Shutdown(); } } } } ================================================ FILE: GVFS/GVFS/CommandLine/CloneVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; namespace GVFS.CommandLine { [Verb(CloneVerb.CloneVerbName, HelpText = "Clone a git repo and mount it as a GVFS virtual repo")] public class CloneVerb : GVFSVerb { private const string CloneVerbName = "clone"; [Value( 0, Required = true, MetaName = "Repository URL", HelpText = "The url of the repo")] public string RepositoryURL { get; set; } [Value( 1, Required = false, Default = "", MetaName = "Enlistment Root Path", HelpText = "Full or relative path to the GVFS enlistment root")] public override string EnlistmentRootPathParameter { get; set; } [Option( "cache-server-url", Required = false, Default = null, HelpText = "The url or friendly name of the cache server")] public string CacheServerUrl { get; set; } [Option( 'b', "branch", Required = false, HelpText = "Branch to checkout after clone")] public string Branch { get; set; } [Option( "single-branch", Required = false, Default = false, HelpText = "Use this option to only download metadata for the branch that will be checked out")] public bool SingleBranch { get; set; } [Option( "no-mount", Required = false, Default = false, HelpText = "Use this option to only clone, but not mount the repo")] public bool NoMount { get; set; } [Option( "no-prefetch", Required = false, Default = false, HelpText = "Use this option to not prefetch commits after clone")] public bool NoPrefetch { get; set; } [Option( "local-cache-path", Required = false, HelpText = "Use this option to override the path for the local GVFS cache.")] public string LocalCacheRoot { get; set; } protected override string VerbName { get { return CloneVerbName; } } public override void Execute() { int exitCode = 0; this.ValidatePathParameter(this.EnlistmentRootPathParameter); this.ValidatePathParameter(this.LocalCacheRoot); string fullEnlistmentRootPathParameter; string normalizedEnlistmentRootPath = this.GetCloneRoot(out fullEnlistmentRootPathParameter); if (!string.IsNullOrWhiteSpace(this.LocalCacheRoot)) { string fullLocalCacheRootPath = Path.GetFullPath(this.LocalCacheRoot); string errorMessage; string normalizedLocalCacheRootPath; if (!GVFSPlatform.Instance.FileSystem.TryGetNormalizedPath(fullLocalCacheRootPath, out normalizedLocalCacheRootPath, out errorMessage)) { this.ReportErrorAndExit($"Failed to determine normalized path for '--local-cache-path' path {fullLocalCacheRootPath}: {errorMessage}"); } if (normalizedLocalCacheRootPath.StartsWith( Path.Combine(normalizedEnlistmentRootPath, GVFSConstants.WorkingDirectoryRootName), GVFSPlatform.Instance.Constants.PathComparison)) { this.ReportErrorAndExit("'--local-cache-path' cannot be inside the src folder"); } } this.CheckKernelDriverSupported(normalizedEnlistmentRootPath); this.CheckNotInsideExistingRepo(normalizedEnlistmentRootPath); this.BlockEmptyCacheServerUrl(this.CacheServerUrl); try { GVFSEnlistment enlistment; Result cloneResult = new Result(false); CacheServerInfo cacheServer = null; ServerGVFSConfig serverGVFSConfig = null; bool trustPackIndexes; using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "GVFSClone")) { cloneResult = this.TryCreateEnlistment(fullEnlistmentRootPathParameter, normalizedEnlistmentRootPath, out enlistment); if (cloneResult.Success) { // Create the enlistment root explicitly with CreateDirectoryAccessibleByAuthUsers before calling // AddLogFileEventListener to ensure that elevated and non-elevated users have access to the root. string createDirectoryError; if (!GVFSPlatform.Instance.FileSystem.TryCreateDirectoryAccessibleByAuthUsers(enlistment.EnlistmentRoot, out createDirectoryError)) { this.ReportErrorAndExit($"Failed to create '{enlistment.EnlistmentRoot}': {createDirectoryError}"); } tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.Clone), EventLevel.Informational, Keywords.Any); tracer.WriteStartEvent( enlistment.EnlistmentRoot, enlistment.RepoUrl, this.CacheServerUrl, new EventMetadata { { "Branch", this.Branch }, { "LocalCacheRoot", this.LocalCacheRoot }, { "SingleBranch", this.SingleBranch }, { "NoMount", this.NoMount }, { "NoPrefetch", this.NoPrefetch }, { "Unattended", this.Unattended }, { "IsElevated", GVFSPlatform.Instance.IsElevated() }, { "NamedPipeName", enlistment.NamedPipeName }, { "ProcessID", Process.GetCurrentProcess().Id }, { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, { nameof(fullEnlistmentRootPathParameter), fullEnlistmentRootPathParameter }, }); CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); cacheServer = cacheServerResolver.ParseUrlOrFriendlyName(this.CacheServerUrl); string resolvedLocalCacheRoot; if (string.IsNullOrWhiteSpace(this.LocalCacheRoot)) { string localCacheRootError; if (!LocalCacheResolver.TryGetDefaultLocalCacheRoot(enlistment, out resolvedLocalCacheRoot, out localCacheRootError)) { this.ReportErrorAndExit( tracer, $"Failed to determine the default location for the local GVFS cache: `{localCacheRootError}`"); } } else { resolvedLocalCacheRoot = Path.GetFullPath(this.LocalCacheRoot); } this.Output.WriteLine("Clone parameters:"); this.Output.WriteLine(" Repo URL: " + enlistment.RepoUrl); this.Output.WriteLine(" Branch: " + (string.IsNullOrWhiteSpace(this.Branch) ? "Default" : this.Branch)); this.Output.WriteLine(" Cache Server: " + cacheServer); this.Output.WriteLine(" Local Cache: " + resolvedLocalCacheRoot); this.Output.WriteLine(" Destination: " + enlistment.EnlistmentRoot); RetryConfig retryConfig = this.GetRetryConfig(tracer, enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); string authErrorMessage; if (!this.TryAuthenticateAndQueryGVFSConfig( tracer, enlistment, retryConfig, out serverGVFSConfig, out authErrorMessage, fallbackCacheServer: cacheServer)) { this.ReportErrorAndExit(tracer, "Cannot clone because authentication failed: " + authErrorMessage); } cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); this.ValidateClientVersions(tracer, enlistment, serverGVFSConfig, showWarnings: true); this.ShowStatusWhileRunning( () => { cloneResult = this.TryClone(tracer, enlistment, cacheServer, retryConfig, serverGVFSConfig, resolvedLocalCacheRoot); return cloneResult.Success; }, "Cloning", normalizedEnlistmentRootPath); } if (!cloneResult.Success) { tracer.RelatedError(cloneResult.ErrorMessage); } using (var repo = new LibGit2RepoInvoker(tracer, enlistment.WorkingDirectoryBackingRoot)) { trustPackIndexes = repo.GetConfigBoolOrDefault(GVFSConstants.GitConfig.TrustPackIndexes, GVFSConstants.GitConfig.TrustPackIndexesDefault); } } if (cloneResult.Success) { if (!this.NoPrefetch) { /* If pack indexes are not trusted, the prefetch can take a long time. * We will run the prefetch command in the background. */ if (trustPackIndexes) { ReturnCode result = this.Execute( enlistment, verb => { verb.Commits = true; verb.SkipVersionCheck = true; verb.ResolvedCacheServer = cacheServer; verb.ServerGVFSConfig = serverGVFSConfig; }); if (result != ReturnCode.Success) { this.Output.WriteLine("\r\nError during prefetch @ {0}", fullEnlistmentRootPathParameter); exitCode = (int)result; } } else { try { string gvfsExecutable = Assembly.GetExecutingAssembly().Location; Process.Start(new ProcessStartInfo( fileName: gvfsExecutable, arguments: "prefetch --commits") { UseShellExecute = true, WindowStyle = ProcessWindowStyle.Minimized, WorkingDirectory = enlistment.EnlistmentRoot }); this.Output.WriteLine("\r\nPrefetch of commit graph has been started as a background process. Git operations involving history may be slower until prefetch has completed.\r\n"); } catch (Win32Exception ex) { this.Output.WriteLine("\r\nError starting prefetch: " + ex.Message); this.Output.WriteLine("Run 'gvfs prefetch --commits' from within your enlistment to prefetch the commit graph."); } } } if (this.NoMount) { this.Output.WriteLine("\r\nIn order to mount, first cd to within your enlistment, then call: "); this.Output.WriteLine("gvfs mount"); } else { this.Execute( enlistment, verb => { verb.SkipMountedCheck = true; verb.SkipVersionCheck = true; verb.ResolvedCacheServer = cacheServer; verb.DownloadedGVFSConfig = serverGVFSConfig; }); } } else { this.Output.WriteLine("\r\nCannot clone @ {0}", fullEnlistmentRootPathParameter); this.Output.WriteLine("Error: {0}", cloneResult.ErrorMessage); exitCode = (int)ReturnCode.GenericError; } } catch (AggregateException e) { this.Output.WriteLine("Cannot clone @ {0}:", fullEnlistmentRootPathParameter); foreach (Exception ex in e.Flatten().InnerExceptions) { this.Output.WriteLine("Exception: {0}", ex.ToString()); } exitCode = (int)ReturnCode.GenericError; } catch (VerbAbortedException) { throw; } catch (Exception e) { this.ReportErrorAndExit("Cannot clone @ {0}: {1}", fullEnlistmentRootPathParameter, e.ToString()); } Environment.Exit(exitCode); } private static bool IsForceCheckoutErrorCloneFailure(string checkoutError) { if (string.IsNullOrWhiteSpace(checkoutError) || checkoutError.Contains("Already on")) { return false; } return true; } private Result TryCreateEnlistment( string fullEnlistmentRootPathParameter, string normalizedEnlistementRootPath, out GVFSEnlistment enlistment) { enlistment = null; // Check that EnlistmentRootPath is empty before creating a tracer and LogFileEventListener as // LogFileEventListener will create a file in EnlistmentRootPath if (Directory.Exists(normalizedEnlistementRootPath) && Directory.EnumerateFileSystemEntries(normalizedEnlistementRootPath).Any()) { if (fullEnlistmentRootPathParameter.Equals(normalizedEnlistementRootPath, GVFSPlatform.Instance.Constants.PathComparison)) { return new Result($"Clone directory '{fullEnlistmentRootPathParameter}' exists and is not empty"); } return new Result($"Clone directory '{fullEnlistmentRootPathParameter}' ['{normalizedEnlistementRootPath}'] exists and is not empty"); } string gitBinPath = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); if (string.IsNullOrWhiteSpace(gitBinPath)) { return new Result(GVFSConstants.GitIsNotInstalledError); } this.CheckGVFSHooksVersion(tracer: null, hooksVersion: out _); try { enlistment = new GVFSEnlistment( normalizedEnlistementRootPath, this.RepositoryURL, gitBinPath, authentication: null); } catch (InvalidRepoException e) { return new Result($"Error when creating a new GVFS enlistment at '{normalizedEnlistementRootPath}'. {e.Message}"); } return new Result(true); } private Result TryClone( JsonTracer tracer, GVFSEnlistment enlistment, CacheServerInfo cacheServer, RetryConfig retryConfig, ServerGVFSConfig serverGVFSConfig, string resolvedLocalCacheRoot) { Result pipeResult; using (NamedPipeServer pipeServer = this.StartNamedPipe(tracer, enlistment, out pipeResult)) { if (!pipeResult.Success) { return pipeResult; } using (GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, cacheServer, retryConfig)) { GitRefs refs = objectRequestor.QueryInfoRefs(this.SingleBranch ? this.Branch : null); if (refs == null) { return new Result("Could not query info/refs from: " + Uri.EscapeUriString(enlistment.RepoUrl)); } if (this.Branch == null) { this.Branch = refs.GetDefaultBranch(); EventMetadata metadata = new EventMetadata(); metadata.Add("Branch", this.Branch); tracer.RelatedEvent(EventLevel.Informational, "CloneDefaultRemoteBranch", metadata); } else { if (!refs.HasBranch(this.Branch)) { EventMetadata metadata = new EventMetadata(); metadata.Add("Branch", this.Branch); tracer.RelatedEvent(EventLevel.Warning, "CloneBranchDoesNotExist", metadata); string errorMessage = string.Format("Remote branch {0} not found in upstream origin", this.Branch); return new Result(errorMessage); } } if (!enlistment.TryCreateEnlistmentSubFolders()) { string error = "Could not create enlistment directories"; tracer.RelatedError(error); return new Result(error); } if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(enlistment.EnlistmentRoot, out string fsError)) { string error = $"FileSystem unsupported: {fsError}"; tracer.RelatedError(error); return new Result(error); } string localCacheError; if (!this.TryDetermineLocalCacheAndInitializePaths(tracer, enlistment, serverGVFSConfig, cacheServer, resolvedLocalCacheRoot, out localCacheError)) { tracer.RelatedError(localCacheError); return new Result(localCacheError); } // There's no need to use CreateDirectoryAccessibleByAuthUsers as these directories will inherit // the ACLs used to create LocalCacheRoot Directory.CreateDirectory(enlistment.GitObjectsRoot); Directory.CreateDirectory(enlistment.GitPackRoot); Directory.CreateDirectory(enlistment.BlobSizesRoot); return this.CreateClone(tracer, enlistment, objectRequestor, refs, this.Branch); } } } private NamedPipeServer StartNamedPipe(ITracer tracer, GVFSEnlistment enlistment, out Result errorResult) { try { errorResult = new Result(true); return AllowAllLocksNamedPipeServer.Create(tracer, enlistment); } catch (PipeNameLengthException) { errorResult = new Result("Failed to clone. Path exceeds the maximum number of allowed characters"); return null; } } private string GetCloneRoot(out string fullEnlistmentRootPathParameter) { fullEnlistmentRootPathParameter = null; try { string repoName = this.RepositoryURL.Substring(this.RepositoryURL.LastIndexOf('/') + 1); fullEnlistmentRootPathParameter = string.IsNullOrWhiteSpace(this.EnlistmentRootPathParameter) ? Path.Combine(Environment.CurrentDirectory, repoName) : this.EnlistmentRootPathParameter; fullEnlistmentRootPathParameter = Path.GetFullPath(fullEnlistmentRootPathParameter); string errorMessage; string enlistmentRootPath; if (!GVFSPlatform.Instance.FileSystem.TryGetNormalizedPath(fullEnlistmentRootPathParameter, out enlistmentRootPath, out errorMessage)) { this.ReportErrorAndExit("Unable to determine normalized path of clone root: " + errorMessage); return null; } return enlistmentRootPath; } catch (IOException e) { this.ReportErrorAndExit("Unable to determine clone root: " + e.ToString()); return null; } } private void CheckKernelDriverSupported(string normalizedEnlistmentRootPath) { string warning; string error; if (!GVFSPlatform.Instance.KernelDriver.IsSupported(normalizedEnlistmentRootPath, out warning, out error)) { this.ReportErrorAndExit($"Error: {error}"); } else if (!string.IsNullOrEmpty(warning)) { this.Output.WriteLine(); this.Output.WriteLine($"WARNING: {warning}"); } } private void CheckNotInsideExistingRepo(string normalizedEnlistmentRootPath) { string errorMessage; string existingEnlistmentRoot; if (GVFSPlatform.Instance.TryGetGVFSEnlistmentRoot(normalizedEnlistmentRootPath, out existingEnlistmentRoot, out errorMessage)) { this.ReportErrorAndExit("Error: You can't clone inside an existing GVFS repo ({0})", existingEnlistmentRoot); } if (this.IsExistingPipeListening(normalizedEnlistmentRootPath)) { this.ReportErrorAndExit($"Error: There is currently a GVFS.Mount process running for '{normalizedEnlistmentRootPath}'. This process must be stopped before cloning."); } } private bool TryDetermineLocalCacheAndInitializePaths( ITracer tracer, GVFSEnlistment enlistment, ServerGVFSConfig serverGVFSConfig, CacheServerInfo currentCacheServer, string localCacheRoot, out string errorMessage) { errorMessage = null; LocalCacheResolver localCacheResolver = new LocalCacheResolver(enlistment); string error; string localCacheKey; if (!localCacheResolver.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers( tracer, serverGVFSConfig, currentCacheServer, localCacheRoot, localCacheKey: out localCacheKey, errorMessage: out error)) { errorMessage = "Error determining local cache key: " + error; return false; } EventMetadata metadata = new EventMetadata(); metadata.Add("localCacheRoot", localCacheRoot); metadata.Add("localCacheKey", localCacheKey); metadata.Add(TracingConstants.MessageKey.InfoMessage, "Initializing cache paths"); tracer.RelatedEvent(EventLevel.Informational, "CloneVerb_TryDetermineLocalCacheAndInitializePaths", metadata); enlistment.InitializeCachePathsFromKey(localCacheRoot, localCacheKey); return true; } private Result CreateClone( ITracer tracer, GVFSEnlistment enlistment, GitObjectsHttpRequestor objectRequestor, GitRefs refs, string branch) { Result initRepoResult = this.TryInitRepo(tracer, refs, enlistment); if (!initRepoResult.Success) { return initRepoResult; } PhysicalFileSystem fileSystem = new PhysicalFileSystem(); string errorMessage; if (!this.TryCreateAlternatesFile(fileSystem, enlistment, out errorMessage)) { return new Result("Error configuring alternate: " + errorMessage); } GitRepo gitRepo = new GitRepo(tracer, enlistment, fileSystem); GVFSContext context = new GVFSContext(tracer, fileSystem, gitRepo, enlistment); GVFSGitObjects gitObjects = new GVFSGitObjects(context, objectRequestor); if (!this.TryDownloadCommit( refs.GetTipCommitId(branch), enlistment, objectRequestor, gitObjects, gitRepo, out errorMessage)) { return new Result(errorMessage); } if (!GVFSVerb.TrySetRequiredGitConfigSettings(enlistment) || !GVFSVerb.TrySetOptionalGitConfigSettings(enlistment)) { return new Result("Unable to configure git repo"); } CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); if (!cacheServerResolver.TrySaveUrlToLocalConfig(objectRequestor.CacheServer, out errorMessage)) { return new Result("Unable to configure cache server: " + errorMessage); } GitProcess git = new GitProcess(enlistment); string originBranchName = "origin/" + branch; GitProcess.Result createBranchResult = git.CreateBranchWithUpstream(branch, originBranchName); if (createBranchResult.ExitCodeIsFailure) { return new Result("Unable to create branch '" + originBranchName + "': " + createBranchResult.Errors + "\r\n" + createBranchResult.Output); } File.WriteAllText( Path.Combine(enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Head), "ref: refs/heads/" + branch); if (!this.TryDownloadRootGitAttributes(enlistment, gitObjects, gitRepo, out errorMessage)) { return new Result(errorMessage); } this.CreateGitScript(enlistment); string installHooksError; if (!HooksInstaller.InstallHooks(context, out installHooksError)) { tracer.RelatedError(installHooksError); return new Result(installHooksError); } GitProcess.Result forceCheckoutResult = git.ForceCheckout(branch); if (forceCheckoutResult.ExitCodeIsFailure && forceCheckoutResult.Errors.IndexOf("unable to read tree") > 0) { // It is possible to have the above TryDownloadCommit() fail because we // already have the commit and root tree we intend to check out, but // don't have a tree further down the working directory. If we fail // checkout here, its' because we don't have these trees and the // read-object hook is not available yet. Force downloading the commit // again and retry the checkout. if (!this.TryDownloadCommit( refs.GetTipCommitId(branch), enlistment, objectRequestor, gitObjects, gitRepo, out errorMessage, checkLocalObjectCache: false)) { return new Result(errorMessage); } forceCheckoutResult = git.ForceCheckout(branch); } if (forceCheckoutResult.ExitCodeIsFailure) { string[] errorLines = forceCheckoutResult.Errors.Split('\n'); StringBuilder checkoutErrors = new StringBuilder(); foreach (string gitError in errorLines) { if (IsForceCheckoutErrorCloneFailure(gitError)) { checkoutErrors.AppendLine(gitError); } } if (checkoutErrors.Length > 0) { string error = "Could not complete checkout of branch: " + branch + ", " + checkoutErrors.ToString(); tracer.RelatedError(error); return new Result(error); } } if (!RepoMetadata.TryInitialize(tracer, enlistment.DotGVFSRoot, out errorMessage)) { tracer.RelatedError(errorMessage); return new Result(errorMessage); } try { RepoMetadata.Instance.SaveCloneMetadata(tracer, enlistment); this.LogEnlistmentInfoAndSetConfigValues(tracer, git, enlistment); } catch (Exception e) { tracer.RelatedError(e.ToString()); return new Result(e.Message); } finally { RepoMetadata.Shutdown(); } // Prepare the working directory folder for GVFS last to ensure that gvfs mount will fail if gvfs clone has failed Exception exception; string prepFileSystemError; if (!GVFSPlatform.Instance.KernelDriver.TryPrepareFolderForCallbacks(enlistment.WorkingDirectoryBackingRoot, out prepFileSystemError, out exception)) { EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(prepFileSystemError), prepFileSystemError); if (exception != null) { metadata.Add("Exception", exception.ToString()); } tracer.RelatedError(metadata, $"{nameof(this.CreateClone)}: TryPrepareFolderForCallbacks failed"); return new Result(prepFileSystemError); } return new Result(true); } // TODO(#1364): Don't call this method on POSIX platforms (or have it no-op on them) private void CreateGitScript(GVFSEnlistment enlistment) { FileInfo gitCmd = new FileInfo(Path.Combine(enlistment.EnlistmentRoot, "git.cmd")); using (FileStream fs = gitCmd.Create()) using (StreamWriter writer = new StreamWriter(fs)) { writer.Write( @" @echo OFF echo . echo ^ echo This repo was cloned using GVFS, and the git repo is in the 'src' directory echo Switching you to the 'src' directory and rerunning your git command echo  @echo ON cd src git %* "); } gitCmd.Attributes = FileAttributes.Hidden; } private Result TryInitRepo(ITracer tracer, GitRefs refs, Enlistment enlistmentToInit) { string repoPath = enlistmentToInit.WorkingDirectoryBackingRoot; GitProcess.Result initResult = GitProcess.Init(enlistmentToInit); if (initResult.ExitCodeIsFailure) { string error = string.Format("Could not init repo at to {0}: {1}", repoPath, initResult.Errors); tracer.RelatedError(error); return new Result(error); } try { GVFSPlatform.Instance.FileSystem.EnsureDirectoryIsOwnedByCurrentUser(enlistmentToInit.DotGitRoot); } catch (IOException e) { string error = string.Format("Could not ensure .git directory is owned by current user: {0}", e.Message); tracer.RelatedError(error); return new Result(error); } GitProcess.Result remoteAddResult = new GitProcess(enlistmentToInit).RemoteAdd("origin", enlistmentToInit.RepoUrl); if (remoteAddResult.ExitCodeIsFailure) { string error = string.Format("Could not add remote to {0}: {1}", repoPath, remoteAddResult.Errors); tracer.RelatedError(error); return new Result(error); } File.WriteAllText( Path.Combine(repoPath, GVFSConstants.DotGit.PackedRefs), refs.ToPackedRefs()); return new Result(true); } private class Result { public Result(bool success) { this.Success = success; this.ErrorMessage = string.Empty; } public Result(string errorMessage) { this.Success = false; this.ErrorMessage = errorMessage; } public bool Success { get; } public string ErrorMessage { get; } } } } ================================================ FILE: GVFS/GVFS/CommandLine/ConfigVerb.cs ================================================ using CommandLine; using GVFS.Common; using System; using System.Collections.Generic; namespace GVFS.CommandLine { [Verb(ConfigVerbName, HelpText = "Get and set GVFS options.")] public class ConfigVerb : GVFSVerb.ForNoEnlistment { private const string ConfigVerbName = "config"; private LocalGVFSConfig localConfig; [Option( 'l', "list", Required = false, HelpText = "Show all settings")] public bool List { get; set; } [Option( 'd', "delete", Required = false, HelpText = "Name of setting to delete")] public string KeyToDelete { get; set; } [Value( 0, Required = false, MetaName = "Setting name", HelpText = "Name of setting that is to be set or read")] public string Key { get; set; } [Value( 1, Required = false, MetaName = "Setting value", HelpText = "Value of setting to be set")] public string Value { get; set; } protected override string VerbName { get { return ConfigVerbName; } } public override void Execute() { if (!GVFSPlatform.Instance.UnderConstruction.SupportsGVFSConfig) { this.ReportErrorAndExit("`gvfs config` is not yet implemented on this operating system."); } this.localConfig = new LocalGVFSConfig(); string error = null; if (this.IsMutuallyExclusiveOptionsSet(out error)) { this.ReportErrorAndExit(error); } if (this.List) { Dictionary allSettings; if (!this.localConfig.TryGetAllConfig(out allSettings, out error)) { this.ReportErrorAndExit(error); } const string ConfigOutputFormat = "{0}={1}"; foreach (KeyValuePair setting in allSettings) { Console.WriteLine(ConfigOutputFormat, setting.Key, setting.Value); } } else if (!string.IsNullOrEmpty(this.KeyToDelete)) { if (!GVFSPlatform.Instance.IsElevated()) { this.ReportErrorAndExit("`gvfs config` must be run from an elevated command prompt when deleting settings."); } if (!this.localConfig.TryRemoveConfig(this.KeyToDelete, out error)) { this.ReportErrorAndExit(error); } } else if (!string.IsNullOrEmpty(this.Key)) { bool valueSpecified = !string.IsNullOrEmpty(this.Value); if (valueSpecified) { if (!GVFSPlatform.Instance.IsElevated()) { this.ReportErrorAndExit("`gvfs config` must be run from an elevated command prompt when configuring settings."); } if (!this.localConfig.TrySetConfig(this.Key, this.Value, out error)) { this.ReportErrorAndExit(error); } } else { string valueRead = null; if (!this.localConfig.TryGetConfig(this.Key, out valueRead, out error) || string.IsNullOrEmpty(valueRead)) { this.ReportErrorAndExit(error); } else { Console.WriteLine(valueRead); } } } else { this.ReportErrorAndExit("You must specify an option. Run `gvfs config --help` for details."); } } private bool IsMutuallyExclusiveOptionsSet(out string consoleMessage) { bool deleteSpecified = !string.IsNullOrEmpty(this.KeyToDelete); bool setOrReadSpecified = !string.IsNullOrEmpty(this.Key); bool listSpecified = this.List; if (deleteSpecified && listSpecified) { consoleMessage = "You cannot delete and list settings at the same time."; return true; } if (setOrReadSpecified && listSpecified) { consoleMessage = "You cannot list all and view (or update) individual settings at the same time."; return true; } if (setOrReadSpecified && deleteSpecified) { consoleMessage = "You cannot delete a setting and view (or update) individual settings at the same time."; return true; } consoleMessage = null; return false; } } } ================================================ FILE: GVFS/GVFS/CommandLine/DehydrateVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.Database; using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.Maintenance; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using GVFS.DiskLayoutUpgrades; using GVFS.Virtualization.Projection; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace GVFS.CommandLine { [Verb(DehydrateVerb.DehydrateVerbName, HelpText = "EXPERIMENTAL FEATURE - Fully dehydrate a GVFS repo")] public class DehydrateVerb : GVFSVerb.ForExistingEnlistment { private const string DehydrateVerbName = "dehydrate"; private const string FolderListSeparator = ";"; private PhysicalFileSystem fileSystem = new PhysicalFileSystem(); [Option( "confirm", Default = false, Required = false, HelpText = "Pass in this flag to actually do the dehydrate")] public bool Confirmed { get; set; } [Option( "no-status", Default = false, Required = false, HelpText = "Do not require a clean git status when dehydrating. To prevent data loss, this option cannot be combined with --folders option.")] public bool NoStatus { get; set; } [Option( "folders", Default = "", Required = false, HelpText = "A semicolon (" + FolderListSeparator + ") delimited list of folders to dehydrate. " + "Each folder must be relative to the repository root. " + "When omitted (without --full), all root-level folders are dehydrated.")] public string Folders { get; set; } [Option( "full", Default = false, Required = false, HelpText = "Perform a full dehydration that unmounts, backs up the entire src folder, and re-creates the virtualization root from scratch. " + "Without this flag, the default behavior dehydrates individual folders which is faster and does not require a full unmount.")] public bool Full { get; set; } public string RunningVerbName { get; set; } = DehydrateVerbName; public string ActionName { get; set; } = DehydrateVerbName; /// /// True if another verb (e.g. 'gvfs sparse') has already validated that status is clean /// public bool StatusChecked { get; set; } protected override string VerbName { get { return DehydrateVerb.DehydrateVerbName; } } protected override void Execute(GVFSEnlistment enlistment) { using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "Dehydrate")) { tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.Dehydrate), EventLevel.Informational, Keywords.Any); tracer.WriteStartEvent( enlistment.EnlistmentRoot, enlistment.RepoUrl, CacheServerResolver.GetUrlFromConfig(enlistment), new EventMetadata { { "Confirmed", this.Confirmed }, { "NoStatus", this.NoStatus }, { "Full", this.Full }, { "NamedPipeName", enlistment.NamedPipeName }, { "Folders", this.Folders }, { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, }); // This is only intended to be run by functional tests if (this.MaintenanceJob != null) { this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig: null, serverGVFSConfig: null, cacheServer: null); PhysicalFileSystem fileSystem = new PhysicalFileSystem(); using (GitRepo gitRepo = new GitRepo(tracer, enlistment, fileSystem)) using (GVFSContext context = new GVFSContext(tracer, fileSystem, gitRepo, enlistment)) { switch (this.MaintenanceJob) { case "LooseObjects": (new LooseObjectsStep(context, forceRun: true)).Execute(); return; case "PackfileMaintenance": (new PackfileMaintenanceStep( context, forceRun: true, batchSize: this.PackfileMaintenanceBatchSize ?? PackfileMaintenanceStep.DefaultBatchSize)).Execute(); return; case "PostFetch": (new PostFetchStep(context, new System.Collections.Generic.List(), requireObjectCacheLock: false)).Execute(); return; default: this.ReportErrorAndExit($"Unknown maintenance job requested: {this.MaintenanceJob}"); break; } } } bool fullDehydrate = this.Full; bool hasFoldersList = !string.IsNullOrEmpty(this.Folders); if (fullDehydrate && hasFoldersList) { this.ReportErrorAndExit("Cannot combine --full with --folders."); } if (!this.Confirmed && fullDehydrate) { this.Output.WriteLine( $@"WARNING: THIS IS AN EXPERIMENTAL FEATURE Dehydrate --full will back up your src folder, and then create a new, empty src folder with a fresh virtualization of the repo. All of your downloaded objects, branches, and siblings of the src folder will be preserved. Your modified working directory files will be moved to the backup, and your new working directory will not have any of your uncommitted changes. Before you dehydrate, make sure you have committed any working directory changes you want to keep. If you choose not to, you can still find your uncommitted changes in the backup folder, but it will be harder to find them because 'git status' will not work in the backup. To actually execute the dehydrate, run 'gvfs dehydrate --confirm --full' from {enlistment.EnlistmentRoot}. "); return; } else if (!this.Confirmed) { string folderDescription = hasFoldersList ? "the folders specified" : "all root-level folders"; string confirmCommand = hasFoldersList ? $"'gvfs dehydrate --confirm --folders '" : $"'gvfs dehydrate --confirm'"; this.Output.WriteLine( $@"WARNING: THIS IS AN EXPERIMENTAL FEATURE All of your downloaded objects, branches, and siblings of the src folder will be preserved. This will remove {folderDescription} and any working directory files and folders even if ignored by git similar to 'git clean -xdf '. Before you dehydrate, you will have to commit any working directory changes you want to keep and have a clean 'git status', or run with --no-status to undo any uncommitted changes. To actually execute the dehydrate, run {confirmCommand} from a parent of the folders list. "); return; } if (fullDehydrate && Environment.CurrentDirectory.StartsWith(enlistment.WorkingDirectoryBackingRoot)) { /* If running from /src, the dehydrate would fail because of the handle we are holding on it. */ this.Output.WriteLine($"Dehydrate --full must be run from {enlistment.EnlistmentRoot}"); return; } bool cleanStatus = this.StatusChecked || this.CheckGitStatus(tracer, enlistment, fullDehydrate); string backupRoot = Path.GetFullPath(Path.Combine(enlistment.EnlistmentRoot, "dehydrate_backup", DateTime.Now.ToString("yyyyMMdd_HHmmss"))); this.Output.WriteLine(); if (fullDehydrate) { this.WriteMessage(tracer, $"Starting {this.RunningVerbName}. All of your existing files will be backed up in " + backupRoot); } else { this.WriteMessage(tracer, $"Starting {this.RunningVerbName}. Selected folders will be backed up in " + backupRoot); } this.WriteMessage(tracer, $"WARNING: If you abort the {this.RunningVerbName} after this point, the repo may become corrupt"); this.Output.WriteLine(); if (fullDehydrate) { this.Unmount(tracer); string error; if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer, enlistment.EnlistmentRoot, out error)) { this.ReportErrorAndExit(tracer, error); } RetryConfig retryConfig; if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) { this.ReportErrorAndExit(tracer, "Failed to determine GVFS timeout and max retries: " + error); } string errorMessage; if (!this.TryAuthenticate(tracer, enlistment, out errorMessage)) { this.ReportErrorAndExit(tracer, errorMessage); } // Local cache and objects paths are required for TryDownloadGitObjects this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig: null, cacheServer: null); this.RunFullDehydrate(tracer, enlistment, backupRoot, retryConfig); } else { string[] folders; if (hasFoldersList) { folders = this.Folders.Split(new[] { FolderListSeparator }, StringSplitOptions.RemoveEmptyEntries); } else { folders = this.GetRootLevelFolders(enlistment); } if (folders.Length > 0) { if (cleanStatus) { this.DehydrateFolders(tracer, enlistment, folders, backupRoot); } else { this.ReportErrorAndExit($"Cannot {this.ActionName}: must have a clean git status."); } } else { this.ReportErrorAndExit($"No folders to {this.ActionName}."); } } } } private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, string[] folders, string backupRoot) { if (!this.TryBackupNonSrcFiles(tracer, enlistment, backupRoot)) { return; } List foldersToDehydrate = new List(); List folderErrors = new List(); if (!this.ShowStatusWhileRunning( () => { if (!ModifiedPathsDatabase.TryLoadOrCreate( tracer, Path.Combine(GetBackupDatabasesPath(backupRoot), GVFSConstants.DotGVFS.Databases.ModifiedPaths), this.fileSystem, out ModifiedPathsDatabase modifiedPaths, out string error)) { this.WriteMessage(tracer, $"Unable to open modified paths database: {error}"); return false; } using (modifiedPaths) { string ioError; foreach (string folder in folders) { string normalizedPath = GVFSDatabase.NormalizePath(folder); if (!this.IsFolderValid(normalizedPath)) { this.WriteMessage(tracer, $"Cannot {this.ActionName} folder '{folder}': invalid folder path."); } else { // Need to check if parent folder is in the modified paths because // dehydration will not do any good with a parent folder there if (modifiedPaths.ContainsParentFolder(folder, out string parentFolder)) { this.WriteMessage(tracer, $"Cannot {this.ActionName} folder '{folder}': Must {this.ActionName} parent folder '{parentFolder}'."); } else { string fullPath = Path.Combine(enlistment.WorkingDirectoryBackingRoot, folder); foldersToDehydrate.Add(folder); } } } } return true; }, "Cleaning up folders")) { this.ReportErrorAndExit(tracer, $"{this.ActionName} for folders failed."); } if (foldersToDehydrate.Count > 0) { string backupSrc = GetBackupSrcPath(backupRoot); this.SendDehydrateMessage(tracer, enlistment, folderErrors, foldersToDehydrate, backupSrc); } if (folderErrors.Count > 0) { foreach (string folderError in folderErrors) { this.ErrorOutput.WriteLine(folderError); } this.ReportErrorAndExit(tracer, ReturnCode.DehydrateFolderFailures, $"Failed to dehydrate {folderErrors.Count} folder(s)."); } } private static string GetBackupSrcPath(string backupRoot) { return Path.Combine(backupRoot, "src"); } private string[] GetRootLevelFolders(GVFSEnlistment enlistment) { HashSet rootFolders = new HashSet(GVFSPlatform.Instance.Constants.PathComparer); GitProcess git = new GitProcess(enlistment); GitProcess.Result result = git.LsTree( GVFSConstants.DotGit.HeadName, line => { // ls-tree output format: " \t" int tabIndex = line.IndexOf('\t'); if (tabIndex >= 0) { string path = line.Substring(tabIndex + 1); int separatorIndex = path.IndexOf('/'); string rootFolder = separatorIndex >= 0 ? path.Substring(0, separatorIndex) : path; if (!rootFolder.Equals(GVFSConstants.DotGit.Root, StringComparison.OrdinalIgnoreCase)) { rootFolders.Add(rootFolder); } } }, recursive: false, showDirectories: true); if (result.ExitCodeIsFailure) { this.ReportErrorAndExit($"Failed to enumerate root-level folders from HEAD: {result.Errors}"); } return rootFolders.ToArray(); } private bool IsFolderValid(string folderPath) { if (folderPath == GVFSConstants.DotGit.Root || folderPath.StartsWith(GVFSConstants.DotGit.Root + Path.DirectorySeparatorChar) || folderPath.StartsWith(".." + Path.DirectorySeparatorChar) || folderPath.Contains(Path.DirectorySeparatorChar + ".." + Path.DirectorySeparatorChar) || Path.GetInvalidPathChars().Any(invalidChar => folderPath.Contains(invalidChar))) { return false; } return true; } private void SendDehydrateMessage( ITracer tracer, GVFSEnlistment enlistment, List folderErrors, List folders, string backupFolder) { NamedPipeMessages.DehydrateFolders.Response response = null; try { using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) { if (!pipeClient.Connect()) { this.Output.WriteLine("Mounting..."); this.Mount(tracer, skipVersionCheck: false); if (!pipeClient.Connect()) { this.ReportErrorAndExit("Unable to connect to GVFS. Try running 'gvfs mount'"); } } NamedPipeMessages.DehydrateFolders.Request request = new NamedPipeMessages.DehydrateFolders.Request( folders: string.Join(";", folders), backupFolderPath: backupFolder); pipeClient.SendRequest(request.CreateMessage()); response = NamedPipeMessages.DehydrateFolders.Response.FromMessage(NamedPipeMessages.Message.FromString(pipeClient.ReadRawResponse())); } } catch (BrokenPipeException e) { this.ReportErrorAndExit("Unable to communicate with GVFS: " + e.ToString()); } if (response != null) { foreach (string folder in response.SuccessfulFolders) { this.WriteMessage(tracer, $"{folder} folder {this.ActionName} successful."); } foreach (string folder in response.FailedFolders) { this.WriteMessage(tracer, $"{folder} folder failed to {this.ActionName}. You may need to reset the working directory by deleting {folder}, running `git reset --hard`, and retry the {this.ActionName}."); folderErrors.Add(folder); } } } private void RunFullDehydrate(JsonTracer tracer, GVFSEnlistment enlistment, string backupRoot, RetryConfig retryConfig) { if (this.TryBackupFiles(tracer, enlistment, backupRoot)) { if (this.TryDownloadGitObjects(tracer, enlistment, retryConfig) && this.TryRecreateIndex(tracer, enlistment)) { // Converting the src folder to partial must be the final step before mount this.PrepareSrcFolder(tracer, enlistment); // We can skip the version check if git status was run because git status requires // that the repo already be mounted (meaning we don't need to perform another version check again) this.Mount( tracer, skipVersionCheck: !this.NoStatus); this.Output.WriteLine(); this.WriteMessage(tracer, "The repo was successfully dehydrated and remounted"); } } else { this.Output.WriteLine(); this.WriteMessage(tracer, "ERROR: Backup failed. We will attempt to mount, but you may need to reclone if that fails"); // We can skip the version check if git status was run because git status requires // that the repo already be mounted (meaning we don't need to perform another version check again) this.Mount( tracer, skipVersionCheck: !this.NoStatus); this.WriteMessage(tracer, "Dehydrate failed, but remounting succeeded"); } } private void Mount(ITracer tracer, bool skipVersionCheck) { if (!this.ShowStatusWhileRunning( () => { return this.ExecuteGVFSVerb( tracer, verb => { verb.SkipInstallHooks = true; verb.SkipVersionCheck = skipVersionCheck; verb.SkipMountedCheck = true; }) == ReturnCode.Success; }, "Mounting")) { this.ReportErrorAndExit(tracer, "Failed to mount."); } } private bool CheckGitStatus(ITracer tracer, GVFSEnlistment enlistment, bool fullDehydrate) { if (this.NoStatus) { return true; } this.WriteMessage(tracer, $"Running git status before {this.ActionName} to make sure you don't have any pending changes."); if (fullDehydrate) { this.WriteMessage(tracer, $"If this takes too long, you can abort and run {this.RunningVerbName} with --no-status to skip this safety check."); } this.Output.WriteLine(); bool isMounted = false; GitProcess.Result statusResult = null; if (!this.ShowStatusWhileRunning( () => { if (this.ExecuteGVFSVerb(tracer) != ReturnCode.Success) { return false; } isMounted = true; GitProcess git = new GitProcess(enlistment); statusResult = git.Status(allowObjectDownloads: false, useStatusCache: false, showUntracked: true); if (statusResult.ExitCodeIsFailure) { return false; } if (!statusResult.Output.Contains("nothing to commit, working tree clean")) { return false; } return true; }, "Running git status", suppressGvfsLogMessage: true)) { this.Output.WriteLine(); if (!isMounted) { this.WriteMessage(tracer, "Failed to run git status because the repo is not mounted"); if (fullDehydrate) { this.WriteMessage(tracer, "Either mount first, or run with --no-status"); } } else if (statusResult.ExitCodeIsFailure) { this.WriteMessage(tracer, "Failed to run git status: " + statusResult.Errors); } else { this.WriteMessage(tracer, statusResult.Output); this.WriteMessage(tracer, "git status reported that you have dirty files"); if (fullDehydrate) { this.WriteMessage(tracer, $"Either commit your changes or run {this.RunningVerbName} with --no-status"); } else { this.WriteMessage(tracer, "Either commit your changes or reset and clean your working directory."); } } this.ReportErrorAndExit(tracer, $"Aborted {this.ActionName}"); return false; } else { return true; } } private void PrepareSrcFolder(ITracer tracer, GVFSEnlistment enlistment) { Exception exception; string error; if (!GVFSPlatform.Instance.KernelDriver.TryPrepareFolderForCallbacks(enlistment.WorkingDirectoryBackingRoot, out error, out exception)) { EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(error), error); if (exception != null) { metadata.Add("Exception", exception.ToString()); } tracer.RelatedError(metadata, $"{nameof(this.PrepareSrcFolder)}: TryPrepareFolderForCallbacks failed"); this.ReportErrorAndExit(tracer, "Failed to recreate the virtualization root: " + error); } } private bool TryBackupNonSrcFiles(ITracer tracer, GVFSEnlistment enlistment, string backupRoot) { string backupSrc = GetBackupSrcPath(backupRoot); string backupGit = Path.Combine(backupRoot, ".git"); string backupGvfs = Path.Combine(backupRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); string backupDatabases = GetBackupDatabasesPath(backupGvfs); string errorMessage = string.Empty; if (!this.ShowStatusWhileRunning( () => { string ioError; if (!this.TryIO(tracer, () => Directory.CreateDirectory(backupRoot), "Create backup directory", out ioError) || !this.TryIO(tracer, () => Directory.CreateDirectory(backupGit), "Create backup .git directory", out ioError) || !this.TryIO(tracer, () => Directory.CreateDirectory(backupGvfs), "Create backup .gvfs directory", out ioError) || !this.TryIO(tracer, () => Directory.CreateDirectory(backupDatabases), "Create backup .gvfs databases directory", out ioError)) { errorMessage = "Failed to create backup folders at " + backupRoot + ": " + ioError; return false; } // ... backup the .gvfs hydration-related data structures... string databasesFolder = Path.Combine(enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.Name); if (!this.TryCopyFilesInFolder(tracer, databasesFolder, backupDatabases, searchPattern: "*", filenamesToSkip: "RepoMetadata.dat")) { return false; } // ... backup everything related to the .git\index... if (!this.TryIO( tracer, () => File.Copy( Path.Combine(enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName), Path.Combine(backupGit, GVFSConstants.DotGit.IndexName)), "Backup the git index", out errorMessage) || !this.TryIO( tracer, () => File.Copy( Path.Combine(enlistment.DotGVFSRoot, GitIndexProjection.ProjectionIndexBackupName), Path.Combine(backupGvfs, GitIndexProjection.ProjectionIndexBackupName)), "Backup GVFS_projection", out errorMessage)) { return false; } // ... backup all .git\*.lock files if (!this.TryCopyFilesInFolder(tracer, enlistment.DotGitRoot, backupGit, searchPattern: "*.lock")) { errorMessage = "Failed to backup .git lock files."; return false; } return true; }, "Backing up your files")) { this.Output.WriteLine(); this.WriteMessage(tracer, "ERROR: " + errorMessage); return false; } return true; } private static string GetBackupDatabasesPath(string backupGvfs) { return Path.Combine(backupGvfs, GVFSConstants.DotGVFS.Databases.Name); } private bool TryBackupFiles(ITracer tracer, GVFSEnlistment enlistment, string backupRoot) { string backupSrc = GetBackupSrcPath(backupRoot); string backupGit = Path.Combine(backupRoot, ".git"); string backupGvfs = Path.Combine(backupRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); string backupDatabases = GetBackupDatabasesPath(backupRoot); string errorMessage = string.Empty; if (!this.ShowStatusWhileRunning( () => { string ioError; if (!this.TryIO(tracer, () => Directory.CreateDirectory(backupRoot), "Create backup directory", out ioError) || !this.TryIO(tracer, () => Directory.CreateDirectory(backupGit), "Create backup .git directory", out ioError) || !this.TryIO(tracer, () => Directory.CreateDirectory(backupGvfs), "Create backup .gvfs directory", out ioError) || !this.TryIO(tracer, () => Directory.CreateDirectory(backupDatabases), "Create backup .gvfs databases directory", out ioError)) { errorMessage = "Failed to create backup folders at " + backupRoot + ": " + ioError; return false; } // Move the current src folder to the backup location... if (!this.TryIO(tracer, () => Directory.Move(enlistment.WorkingDirectoryBackingRoot, backupSrc), "Move the src folder", out ioError)) { errorMessage = "Failed to move the src folder: " + ioError + Environment.NewLine; errorMessage += "Make sure you have no open handles or running processes in the src folder"; return false; } // ... but move the .git folder back to the new src folder so we can preserve objects, refs, logs... if (!this.TryIO(tracer, () => Directory.CreateDirectory(enlistment.WorkingDirectoryBackingRoot), "Create new src folder", out errorMessage) || !this.TryIO(tracer, () => Directory.Move(Path.Combine(backupSrc, ".git"), enlistment.DotGitRoot), "Keep existing .git folder", out errorMessage)) { return false; } // ... backup the .gvfs hydration-related data structures... string databasesFolder = Path.Combine(enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.Name); if (!this.TryBackupFilesInFolder(tracer, databasesFolder, backupDatabases, searchPattern: "*", filenamesToSkip: "RepoMetadata.dat")) { return false; } // ... backup everything related to the .git\index... if (!this.TryIO( tracer, () => File.Move( Path.Combine(enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName), Path.Combine(backupGit, GVFSConstants.DotGit.IndexName)), "Backup the git index", out errorMessage) || !this.TryIO( tracer, () => File.Move( Path.Combine(enlistment.DotGVFSRoot, GitIndexProjection.ProjectionIndexBackupName), Path.Combine(backupGvfs, GitIndexProjection.ProjectionIndexBackupName)), "Backup GVFS_projection", out errorMessage)) { return false; } // ... backup all .git\*.lock files if (!this.TryBackupFilesInFolder(tracer, enlistment.DotGitRoot, backupGit, searchPattern: "*.lock")) { return false; } return true; }, "Backing up your files")) { this.Output.WriteLine(); this.WriteMessage(tracer, "ERROR: " + errorMessage); return false; } return true; } private bool TryBackupFilesInFolder(ITracer tracer, string folderPath, string backupPath, string searchPattern, params string[] filenamesToSkip) { string errorMessage; foreach (string file in Directory.GetFiles(folderPath, searchPattern)) { string fileName = Path.GetFileName(file); if (!filenamesToSkip.Any(x => x.Equals(fileName, GVFSPlatform.Instance.Constants.PathComparison))) { if (!this.TryIO( tracer, () => File.Move(file, file.Replace(folderPath, backupPath)), $"Backing up {Path.GetFileName(file)}", out errorMessage)) { return false; } } } return true; } private bool TryCopyFilesInFolder(ITracer tracer, string folderPath, string backupPath, string searchPattern, params string[] filenamesToSkip) { string errorMessage; foreach (string file in Directory.GetFiles(folderPath, searchPattern)) { string fileName = Path.GetFileName(file); if (!filenamesToSkip.Any(x => x.Equals(fileName, GVFSPlatform.Instance.Constants.PathComparison))) { if (!this.TryIO( tracer, () => File.Copy(file, file.Replace(folderPath, backupPath)), $"Backing up {Path.GetFileName(file)}", out errorMessage)) { return false; } } } return true; } private bool TryDownloadGitObjects(ITracer tracer, GVFSEnlistment enlistment, RetryConfig retryConfig) { string errorMessage = null; if (!this.ShowStatusWhileRunning( () => { CacheServerInfo cacheServer = new CacheServerInfo(enlistment.RepoUrl, null); using (GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, cacheServer, retryConfig)) { PhysicalFileSystem fileSystem = new PhysicalFileSystem(); GitRepo gitRepo = new GitRepo(tracer, enlistment, fileSystem); GVFSGitObjects gitObjects = new GVFSGitObjects(new GVFSContext(tracer, fileSystem, gitRepo, enlistment), objectRequestor); GitProcess.Result revParseResult = enlistment.CreateGitProcess().RevParse("HEAD"); if (revParseResult.ExitCodeIsFailure) { errorMessage = "Unable to determine HEAD commit id: " + revParseResult.Errors; return false; } string headCommit = revParseResult.Output.TrimEnd('\n'); if (!this.TryDownloadCommit(headCommit, enlistment, objectRequestor, gitObjects, gitRepo, out errorMessage) || !this.TryDownloadRootGitAttributes(enlistment, gitObjects, gitRepo, out errorMessage)) { return false; } } return true; }, "Downloading git objects", suppressGvfsLogMessage: true)) { this.WriteMessage(tracer, errorMessage); return false; } return true; } private bool TryRecreateIndex(ITracer tracer, GVFSEnlistment enlistment) { string errorMessage = null; if (!this.ShowStatusWhileRunning( () => { // Create a new index based on the new minimal modified paths using (NamedPipeServer pipeServer = AllowAllLocksNamedPipeServer.Create(tracer, enlistment)) { GitProcess git = new GitProcess(enlistment); GitProcess.Result checkoutResult = git.ForceCheckout("HEAD"); errorMessage = checkoutResult.Errors; return checkoutResult.ExitCodeIsSuccess; } }, "Recreating git index", suppressGvfsLogMessage: true)) { this.WriteMessage(tracer, "Failed to recreate index: " + errorMessage); return false; } return true; } private void WriteMessage(ITracer tracer, string message) { this.Output.WriteLine(message); tracer.RelatedEvent( EventLevel.Informational, "Dehydrate", new EventMetadata { { TracingConstants.MessageKey.InfoMessage, message } }); } private bool TryIO(ITracer tracer, Action action, string description, out string error) { try { action(); tracer.RelatedEvent( EventLevel.Informational, "TryIO", new EventMetadata { { "Description", description } }); error = null; return true; } catch (Exception e) { error = e.Message; tracer.RelatedError( new EventMetadata { { "Description", description }, { "Error", error } }, "TryIO: Caught exception performing action"); } return false; } } } ================================================ FILE: GVFS/GVFS/CommandLine/DiagnoseVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; namespace GVFS.CommandLine { [Verb(DiagnoseVerb.DiagnoseVerbName, HelpText = "Diagnose issues with a GVFS repo")] public class DiagnoseVerb : GVFSVerb.ForExistingEnlistment { private const string DiagnoseVerbName = "diagnose"; private const string DeprecatedUpgradeLogsDirectory = "Logs"; private TextWriter diagnosticLogFileWriter; private PhysicalFileSystem fileSystem; public DiagnoseVerb() : base(false) { this.fileSystem = new PhysicalFileSystem(); } protected override string VerbName { get { return DiagnoseVerbName; } } protected override void Execute(GVFSEnlistment enlistment) { string diagnosticsRoot = Path.Combine(enlistment.DotGVFSRoot, "diagnostics"); if (!Directory.Exists(diagnosticsRoot)) { Directory.CreateDirectory(diagnosticsRoot); } string archiveFolderPath = Path.Combine(diagnosticsRoot, "gvfs_" + DateTime.Now.ToString("yyyyMMdd_HHmmss")); Directory.CreateDirectory(archiveFolderPath); using (FileStream diagnosticLogFile = new FileStream(Path.Combine(archiveFolderPath, "diagnostics.log"), FileMode.CreateNew)) using (this.diagnosticLogFileWriter = new StreamWriter(diagnosticLogFile)) { this.WriteMessage("Collecting diagnostic info into temp folder " + archiveFolderPath); this.WriteMessage(string.Empty); this.WriteMessage("gvfs version " + ProcessHelper.GetCurrentProcessVersion()); GitVersion gitVersion = null; string error = null; if (!string.IsNullOrEmpty(enlistment.GitBinPath) && GitProcess.TryGetVersion(enlistment.GitBinPath, out gitVersion, out error)) { this.WriteMessage("git version " + gitVersion.ToString()); } else { this.WriteMessage("Could not determine git version. " + error); } this.WriteMessage(enlistment.GitBinPath); this.WriteMessage(string.Empty); this.WriteMessage("Enlistment root: " + enlistment.EnlistmentRoot); this.WriteMessage("Cache Server: " + CacheServerResolver.GetCacheServerFromConfig(enlistment)); string localCacheRoot; string gitObjectsRoot; this.GetLocalCachePaths(enlistment, out localCacheRoot, out gitObjectsRoot); string actualLocalCacheRoot = !string.IsNullOrWhiteSpace(localCacheRoot) ? localCacheRoot : gitObjectsRoot; this.WriteMessage("Local Cache: " + actualLocalCacheRoot); this.WriteMessage(string.Empty); this.PrintDiskSpaceInfo(actualLocalCacheRoot, this.EnlistmentRootPathParameter); this.RecordVersionInformation(); this.ShowStatusWhileRunning( () => this.RunAndRecordGVFSVerb(archiveFolderPath, "gvfs_status.txt") != ReturnCode.Success || this.RunAndRecordGVFSVerb(archiveFolderPath, "gvfs_unmount.txt", verb => verb.SkipLock = true) == ReturnCode.Success, "Unmounting", suppressGvfsLogMessage: true); this.ShowStatusWhileRunning( () => { // .gvfs this.CopyAllFiles(enlistment.EnlistmentRoot, archiveFolderPath, GVFSPlatform.Instance.Constants.DotGVFSRoot, copySubFolders: false); // driver if (this.FlushKernelDriverLogs()) { string kernelLogsFolderPath = GVFSPlatform.Instance.KernelDriver.LogsFolderPath; // This copy sometimes fails because the OS has an exclusive lock on the etl files. The error is not actionable // for the user so we don't write the error message to stdout, just to our own log file. this.CopyAllFiles(Path.GetDirectoryName(kernelLogsFolderPath), archiveFolderPath, Path.GetFileName(kernelLogsFolderPath), copySubFolders: false, hideErrorsFromStdout: true); } // .git this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, GVFSConstants.DotGit.Root, copySubFolders: false); this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, GVFSConstants.DotGit.Hooks.Root, copySubFolders: false); this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, GVFSConstants.DotGit.Info.Root, copySubFolders: false); this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, GVFSConstants.DotGit.Logs.Root, copySubFolders: true); this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, GVFSConstants.DotGit.Refs.Root, copySubFolders: true); this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, GVFSConstants.DotGit.Objects.Info.Root, copySubFolders: false); this.LogDirectoryEnumeration(enlistment.WorkingDirectoryRoot, Path.Combine(archiveFolderPath, GVFSConstants.DotGit.Objects.Root), GVFSConstants.DotGit.Objects.Pack.Root, "packs-local.txt"); this.LogLooseObjectCount(enlistment.WorkingDirectoryRoot, Path.Combine(archiveFolderPath, GVFSConstants.DotGit.Objects.Root), GVFSConstants.DotGit.Objects.Root, "objects-local.txt"); // databases this.CopyAllFiles(enlistment.DotGVFSRoot, Path.Combine(archiveFolderPath, GVFSPlatform.Instance.Constants.DotGVFSRoot), GVFSConstants.DotGVFS.Databases.Name, copySubFolders: false); // local cache this.CopyLocalCacheData(archiveFolderPath, localCacheRoot, gitObjectsRoot); // corrupt objects this.CopyAllFiles(enlistment.DotGVFSRoot, Path.Combine(archiveFolderPath, GVFSPlatform.Instance.Constants.DotGVFSRoot), GVFSConstants.DotGVFS.CorruptObjectsName, copySubFolders: false); // service this.CopyAllFiles( GVFSPlatform.Instance.GetCommonAppDataRootForGVFS(), archiveFolderPath, this.ServiceName, copySubFolders: true); this.CopyAllFiles( GVFSPlatform.Instance.GetSecureDataRootForGVFS(), archiveFolderPath, this.ServiceName, copySubFolders: true); if (GVFSPlatform.Instance.UnderConstruction.SupportsGVFSConfig) { this.CopyFile(GVFSPlatform.Instance.GetSecureDataRootForGVFS(), archiveFolderPath, LocalGVFSConfig.FileName); } if (!GVFSPlatform.Instance.TryCopyPanicLogs(archiveFolderPath, out string errorMessage)) { this.WriteMessage(errorMessage); } return true; }, "Copying logs"); this.ShowStatusWhileRunning( () => this.RunAndRecordGVFSVerb(archiveFolderPath, "gvfs_mount.txt") == ReturnCode.Success, "Mounting", suppressGvfsLogMessage: true); this.CopyAllFiles(enlistment.DotGVFSRoot, Path.Combine(archiveFolderPath, GVFSPlatform.Instance.Constants.DotGVFSRoot), "logs", copySubFolders: false); } string zipFilePath = archiveFolderPath + ".zip"; this.ShowStatusWhileRunning( () => { ZipFile.CreateFromDirectory(archiveFolderPath, zipFilePath); this.fileSystem.DeleteDirectory(archiveFolderPath); return true; }, "Creating zip file", suppressGvfsLogMessage: true); this.Output.WriteLine(); this.Output.WriteLine("Diagnostics complete. All of the gathered info, as well as all of the output above, is captured in"); this.Output.WriteLine(zipFilePath); } private void WriteMessage(string message, bool skipStdout = false) { message = message.TrimEnd('\r', '\n'); if (!skipStdout) { this.Output.WriteLine(message); } this.diagnosticLogFileWriter.WriteLine(message); } private void RecordVersionInformation() { string information = GVFSPlatform.Instance.GetOSVersionInformation(); this.diagnosticLogFileWriter.WriteLine(information); } private void CopyFile( string sourceRoot, string targetRoot, string fileName) { string sourceFile = Path.Combine(sourceRoot, fileName); string targetFile = Path.Combine(targetRoot, fileName); try { if (!File.Exists(sourceFile)) { return; } File.Copy(sourceFile, targetFile); } catch (Exception e) { this.WriteMessage( string.Format( "Failed to copy file {0} in {1} with exception {2}", fileName, sourceRoot, e)); } } private void CopyAllFiles( string sourceRoot, string targetRoot, string folderName, bool copySubFolders, bool hideErrorsFromStdout = false, string targetFolderName = null) { string sourceFolder = Path.Combine(sourceRoot, folderName); string targetFolder = Path.Combine(targetRoot, targetFolderName ?? folderName); try { if (!Directory.Exists(sourceFolder)) { return; } this.RecursiveFileCopyImpl(sourceFolder, targetFolder, copySubFolders, hideErrorsFromStdout); } catch (Exception e) { this.WriteMessage( string.Format( "Failed to copy folder {0} in {1} with exception {2}. copySubFolders: {3}", folderName, sourceRoot, e, copySubFolders), hideErrorsFromStdout); } } private void GetLocalCachePaths(GVFSEnlistment enlistment, out string localCacheRoot, out string gitObjectsRoot) { localCacheRoot = null; gitObjectsRoot = null; try { using (ITracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "DiagnoseVerb")) { string error; if (RepoMetadata.TryInitialize(tracer, Path.Combine(enlistment.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot), out error)) { RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error); RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error); } else { this.WriteMessage("Failed to determine local cache path and git objects root, RepoMetadata error: " + error); } } } catch (Exception e) { this.WriteMessage(string.Format("Failed to determine local cache path and git objects root, Exception: {0}", e)); } finally { RepoMetadata.Shutdown(); } } private void CopyLocalCacheData(string archiveFolderPath, string localCacheRoot, string gitObjectsRoot) { try { string localCacheArchivePath = Path.Combine(archiveFolderPath, GVFSConstants.DefaultGVFSCacheFolderName); Directory.CreateDirectory(localCacheArchivePath); if (!string.IsNullOrWhiteSpace(localCacheRoot)) { // Copy all mapping.dat files in the local cache folder (i.e. mapping.dat, mapping.dat.tmp, mapping.dat.lock) foreach (string filePath in Directory.EnumerateFiles(localCacheRoot, "mapping.dat*")) { string fileName = Path.GetFileName(filePath); try { File.Copy(filePath, Path.Combine(localCacheArchivePath, fileName)); } catch (Exception e) { this.WriteMessage(string.Format( "Failed to copy '{0}' from {1} to {2} with exception {3}", fileName, localCacheRoot, archiveFolderPath, e)); } } } if (!string.IsNullOrWhiteSpace(gitObjectsRoot)) { this.LogDirectoryEnumeration(gitObjectsRoot, localCacheArchivePath, GVFSConstants.DotGit.Objects.Pack.Name, "packs-cached.txt"); this.LogLooseObjectCount(gitObjectsRoot, localCacheArchivePath, string.Empty, "objects-cached.txt"); // Store all commit-graph files this.CopyAllFiles(gitObjectsRoot, localCacheArchivePath, GVFSConstants.DotGit.Objects.Info.Root, copySubFolders: true); } } catch (Exception e) { this.WriteMessage(string.Format("Failed to copy local cache data with exception: {0}", e)); } } private void LogDirectoryEnumeration(string sourceRoot, string targetRoot, string folderName, string logfile) { try { if (!Directory.Exists(targetRoot)) { Directory.CreateDirectory(targetRoot); } string folder = Path.Combine(sourceRoot, folderName); string targetLog = Path.Combine(targetRoot, logfile); List lines = new List(); if (Directory.Exists(folder)) { DirectoryInfo packDirectory = new DirectoryInfo(folder); lines.Add($"Contents of {folder}:"); foreach (FileInfo file in packDirectory.EnumerateFiles()) { lines.Add($"{file.Name, -70} {file.Length, 16}"); } } File.WriteAllLines(targetLog, lines.ToArray()); } catch (Exception e) { this.WriteMessage(string.Format( "Failed to log file sizes for {0} in {1} with exception {2}. logfile: {3}", folderName, sourceRoot, e, logfile)); } } private void LogLooseObjectCount(string sourceRoot, string targetRoot, string folderName, string logfile) { try { if (!Directory.Exists(targetRoot)) { Directory.CreateDirectory(targetRoot); } string objectFolder = Path.Combine(sourceRoot, folderName); string targetLog = Path.Combine(targetRoot, logfile); List lines = new List(); if (Directory.Exists(objectFolder)) { DirectoryInfo objectDirectory = new DirectoryInfo(objectFolder); int countLoose = 0; int countFolders = 0; lines.Add($"Object directory stats for {objectFolder}:"); foreach (DirectoryInfo directory in objectDirectory.EnumerateDirectories()) { if (GitObjects.IsLooseObjectsDirectory(directory.Name)) { countFolders++; int numObjects = directory.EnumerateFiles().Count(); lines.Add($"{directory.Name} : {numObjects, 7} objects"); countLoose += numObjects; } } lines.Add($"Total: {countLoose} loose objects"); } File.WriteAllLines(targetLog, lines.ToArray()); } catch (Exception e) { this.WriteMessage(string.Format( "Failed to log loose object count for {0} in {1} with exception {2}. logfile: {3}", folderName, sourceRoot, e, logfile)); } } private void RecursiveFileCopyImpl(string sourcePath, string targetPath, bool copySubFolders, bool hideErrorsFromStdout) { if (!Directory.Exists(targetPath)) { Directory.CreateDirectory(targetPath); } foreach (string filePath in Directory.EnumerateFiles(sourcePath)) { string fileName = Path.GetFileName(filePath); try { string sourceFilePath = Path.Combine(sourcePath, fileName); if (!GVFSPlatform.Instance.FileSystem.IsSocket(sourceFilePath) && !GVFSPlatform.Instance.FileSystem.IsExecutable(sourceFilePath)) { File.Copy( Path.Combine(sourcePath, fileName), Path.Combine(targetPath, fileName)); } } catch (Exception e) { this.WriteMessage( string.Format( "Failed to copy '{0}' in {1} with exception {2}", fileName, sourcePath, e), hideErrorsFromStdout); } } if (copySubFolders) { DirectoryInfo dir = new DirectoryInfo(sourcePath); foreach (DirectoryInfo subdir in dir.GetDirectories()) { string targetFolderPath = Path.Combine(targetPath, subdir.Name); try { this.RecursiveFileCopyImpl(subdir.FullName, targetFolderPath, copySubFolders, hideErrorsFromStdout); } catch (Exception e) { this.WriteMessage( string.Format( "Failed to copy subfolder '{0}' to '{1}' with exception {2}", subdir.FullName, targetFolderPath, e), hideErrorsFromStdout); } } } } private ReturnCode RunAndRecordGVFSVerb(string archiveFolderPath, string outputFileName, Action configureVerb = null) where TVerb : GVFSVerb, new() { try { using (FileStream file = new FileStream(Path.Combine(archiveFolderPath, outputFileName), FileMode.CreateNew)) using (StreamWriter writer = new StreamWriter(file)) { return this.Execute( this.EnlistmentRootPathParameter, verb => { if (configureVerb != null) { configureVerb(verb); } verb.Output = writer; }); } } catch (Exception e) { this.WriteMessage(string.Format( "Verb {0} failed with exception {1}", typeof(TVerb), e)); return ReturnCode.GenericError; } } private bool FlushKernelDriverLogs() { string errors; bool flushSuccess = GVFSPlatform.Instance.KernelDriver.TryFlushLogs(out errors); this.diagnosticLogFileWriter.WriteLine(errors); return flushSuccess; } private void PrintDiskSpaceInfo(string localCacheRoot, string enlistmentRootParameter) { try { string enlistmentNormalizedPathRoot; string localCacheNormalizedPathRoot; string enlistmentErrorMessage; string localCacheErrorMessage; bool enlistmentSuccess = GVFSPlatform.Instance.TryGetNormalizedPathRoot(enlistmentRootParameter, out enlistmentNormalizedPathRoot, out enlistmentErrorMessage); bool localCacheSuccess = GVFSPlatform.Instance.TryGetNormalizedPathRoot(localCacheRoot, out localCacheNormalizedPathRoot, out localCacheErrorMessage); if (!enlistmentSuccess || !localCacheSuccess) { this.WriteMessage("Failed to acquire disk space information:"); if (!string.IsNullOrEmpty(enlistmentErrorMessage)) { this.WriteMessage(enlistmentErrorMessage); } if (!string.IsNullOrEmpty(localCacheErrorMessage)) { this.WriteMessage(localCacheErrorMessage); } this.WriteMessage(string.Empty); return; } DriveInfo enlistmentDrive = new DriveInfo(enlistmentNormalizedPathRoot); string enlistmentDriveDiskSpace = this.FormatByteCount(enlistmentDrive.AvailableFreeSpace); if (string.Equals(enlistmentNormalizedPathRoot, localCacheNormalizedPathRoot, GVFSPlatform.Instance.Constants.PathComparison)) { this.WriteMessage("Available space on " + enlistmentDrive.Name + " drive(enlistment and local cache): " + enlistmentDriveDiskSpace); } else { this.WriteMessage("Available space on " + enlistmentDrive.Name + " drive(enlistment): " + enlistmentDriveDiskSpace); DriveInfo cacheDrive = new DriveInfo(localCacheRoot); string cacheDriveDiskSpace = this.FormatByteCount(cacheDrive.AvailableFreeSpace); this.WriteMessage("Available space on " + cacheDrive.Name + " drive(local cache): " + cacheDriveDiskSpace); } this.WriteMessage(string.Empty); } catch (Exception e) { this.WriteMessage("Failed to acquire disk space information, exception: " + e.ToString()); this.WriteMessage(string.Empty); } } private string FormatByteCount(double byteCount) { const int Divisor = 1024; const string ByteCountFormat = "0.00"; string[] unitStrings = { " B", " KB", " MB", " GB", " TB" }; int unitIndex = 0; while (byteCount >= Divisor && unitIndex < unitStrings.Length - 1) { unitIndex++; byteCount = byteCount / Divisor; } return byteCount.ToString(ByteCountFormat) + unitStrings[unitIndex]; } } } ================================================ FILE: GVFS/GVFS/CommandLine/GVFSVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security; using System.Text; namespace GVFS.CommandLine { public abstract class GVFSVerb { protected const string StartServiceInstructions = "Run 'sc start GVFS.Service' from an elevated command prompt to ensure it is running."; private readonly bool validateOriginURL; public GVFSVerb(bool validateOrigin = true) { this.Output = Console.Out; // Currently stderr is only being used for machine readable output for failures in sparse --prune this.ErrorOutput = Console.Error; this.ReturnCode = ReturnCode.Success; this.validateOriginURL = validateOrigin; this.ServiceName = GVFSConstants.Service.ServiceName; this.StartedByService = false; this.Unattended = GVFSEnlistment.IsUnattended(tracer: null); this.InitializeDefaultParameterValues(); } public abstract string EnlistmentRootPathParameter { get; set; } [Option( GVFSConstants.VerbParameters.InternalUseOnly, Required = false, HelpText = "This parameter is reserved for internal use.")] public string InternalParameters { set { if (!string.IsNullOrEmpty(value)) { try { InternalVerbParameters mountInternal = InternalVerbParameters.FromJson(value); if (!string.IsNullOrEmpty(mountInternal.ServiceName)) { this.ServiceName = mountInternal.ServiceName; } if (!string.IsNullOrEmpty(mountInternal.MaintenanceJob)) { this.MaintenanceJob = mountInternal.MaintenanceJob; } if (!string.IsNullOrEmpty(mountInternal.PackfileMaintenanceBatchSize)) { this.PackfileMaintenanceBatchSize = mountInternal.PackfileMaintenanceBatchSize; } this.StartedByService = mountInternal.StartedByService; } catch (JsonReaderException e) { this.ReportErrorAndExit("Failed to parse InternalParameters: {0}.\n {1}", value, e); } } } } public string ServiceName { get; set; } public string MaintenanceJob { get; set; } public string PackfileMaintenanceBatchSize { get; set; } public bool StartedByService { get; set; } public bool Unattended { get; private set; } public string ServicePipeName { get { return GVFSPlatform.Instance.GetGVFSServiceNamedPipeName(this.ServiceName); } } public TextWriter Output { get; set; } public TextWriter ErrorOutput { get; set; } public ReturnCode ReturnCode { get; private set; } protected abstract string VerbName { get; } public static bool TrySetRequiredGitConfigSettings(Enlistment enlistment) { Dictionary requiredSettings = RequiredGitConfig.GetRequiredSettings(enlistment); if (!TrySetConfig(enlistment, requiredSettings, isRequired: true)) { return false; } return true; } public static bool TrySetOptionalGitConfigSettings(Enlistment enlistment) { // These settings are optional, because they impact performance but not functionality of GVFS. // These settings should only be set by the clone or repair verbs, so that they do not // overwrite the values set by the user in their local config. Dictionary optionalSettings = new Dictionary { { "status.aheadbehind", "false" }, }; if (!TrySetConfig(enlistment, optionalSettings, isRequired: false)) { return false; } return true; } public abstract void Execute(); public virtual void InitializeDefaultParameterValues() { } protected ReturnCode Execute( string enlistmentRootPath, Action configureVerb = null) where TVerb : GVFSVerb, new() { TVerb verb = new TVerb(); verb.EnlistmentRootPathParameter = enlistmentRootPath; verb.ServiceName = this.ServiceName; verb.Unattended = this.Unattended; if (configureVerb != null) { configureVerb(verb); } try { verb.Execute(); } catch (VerbAbortedException) { } return verb.ReturnCode; } protected ReturnCode Execute( GVFSEnlistment enlistment, Action configureVerb = null) where TVerb : GVFSVerb.ForExistingEnlistment, new() { TVerb verb = new TVerb(); verb.EnlistmentRootPathParameter = enlistment.EnlistmentRoot; verb.ServiceName = this.ServiceName; verb.Unattended = this.Unattended; if (configureVerb != null) { configureVerb(verb); } try { verb.Execute(enlistment.Authentication); } catch (VerbAbortedException) { } return verb.ReturnCode; } protected bool ShowStatusWhileRunning( Func action, string message, string gvfsLogEnlistmentRoot) { return ConsoleHelper.ShowStatusWhileRunning( action, message, this.Output, showSpinner: !this.Unattended && this.Output == Console.Out && !GVFSPlatform.Instance.IsConsoleOutputRedirectedToFile(), gvfsLogEnlistmentRoot: gvfsLogEnlistmentRoot, initialDelayMs: 0); } protected bool ShowStatusWhileRunning( Func action, string message, bool suppressGvfsLogMessage = false) { string gvfsLogEnlistmentRoot = null; if (!suppressGvfsLogMessage) { string errorMessage; GVFSPlatform.Instance.TryGetGVFSEnlistmentRoot(this.EnlistmentRootPathParameter, out gvfsLogEnlistmentRoot, out errorMessage); } return this.ShowStatusWhileRunning(action, message, gvfsLogEnlistmentRoot); } protected bool TryAuthenticate(ITracer tracer, GVFSEnlistment enlistment, out string authErrorMessage) { return this.TryAuthenticateAndQueryGVFSConfig(tracer, enlistment, null, out _, out authErrorMessage); } /// /// Combines authentication and GVFS config query into a single operation, /// eliminating a redundant HTTP round-trip. If /// is null, a default RetryConfig is used. /// If the config query fails but a valid /// URL is available, auth succeeds but /// will be null (caller should handle this gracefully). /// protected bool TryAuthenticateAndQueryGVFSConfig( ITracer tracer, GVFSEnlistment enlistment, RetryConfig retryConfig, out ServerGVFSConfig serverGVFSConfig, out string errorMessage, CacheServerInfo fallbackCacheServer = null) { ServerGVFSConfig config = null; string error = null; bool result = this.ShowStatusWhileRunning( () => enlistment.Authentication.TryInitializeAndQueryGVFSConfig( tracer, enlistment, retryConfig ?? new RetryConfig(), out config, out error), "Authenticating", enlistment.EnlistmentRoot); if (!result && fallbackCacheServer != null && !string.IsNullOrWhiteSpace(fallbackCacheServer.Url)) { // Auth/config query failed, but we have a fallback cache server. // Allow auth to succeed so mount/clone can proceed; config will be null. tracer.RelatedWarning("Config query failed but continuing with fallback cache server: " + error); serverGVFSConfig = null; errorMessage = null; return true; } serverGVFSConfig = config; errorMessage = error; return result; } protected void ReportErrorAndExit(ITracer tracer, ReturnCode exitCode, string error, params object[] args) { if (!string.IsNullOrEmpty(error)) { if (args == null || args.Length == 0) { this.Output.WriteLine(error); if (tracer != null && exitCode != ReturnCode.Success) { tracer.RelatedError(error); } } else { this.Output.WriteLine(error, args); if (tracer != null && exitCode != ReturnCode.Success) { tracer.RelatedError(error, args); } } } this.ReturnCode = exitCode; throw new VerbAbortedException(this); } protected void ReportErrorAndExit(string error, params object[] args) { this.ReportErrorAndExit(tracer: null, exitCode: ReturnCode.GenericError, error: error, args: args); } protected void ReportErrorAndExit(ITracer tracer, string error, params object[] args) { this.ReportErrorAndExit(tracer, ReturnCode.GenericError, error, args); } protected RetryConfig GetRetryConfig(ITracer tracer, GVFSEnlistment enlistment, TimeSpan? timeoutOverride = null) { RetryConfig retryConfig; string error; if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) { this.ReportErrorAndExit(tracer, "Failed to determine GVFS timeout and max retries: " + error); } if (timeoutOverride.HasValue) { retryConfig.Timeout = timeoutOverride.Value; } return retryConfig; } // QueryGVFSConfig for callers that require config to succeed (no fallback) protected ServerGVFSConfig QueryGVFSConfig(ITracer tracer, GVFSEnlistment enlistment, RetryConfig retryConfig) { ServerGVFSConfig serverGVFSConfig = null; string errorMessage = null; if (!this.ShowStatusWhileRunning( () => { using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig)) { const bool LogErrors = true; return configRequestor.TryQueryGVFSConfig(LogErrors, out serverGVFSConfig, out _, out errorMessage); } }, "Querying remote for config", suppressGvfsLogMessage: true)) { this.ReportErrorAndExit(tracer, "Unable to query /gvfs/config" + Environment.NewLine + errorMessage); } return serverGVFSConfig; } protected bool IsExistingPipeListening(string enlistmentRoot) { using (NamedPipeClient pipeClient = new NamedPipeClient(GVFSPlatform.Instance.GetNamedPipeName(enlistmentRoot))) { if (pipeClient.Connect(500)) { return true; } } return false; } protected void ValidateClientVersions(ITracer tracer, GVFSEnlistment enlistment, ServerGVFSConfig gvfsConfig, bool showWarnings) { this.CheckGitVersion(tracer, enlistment, out string gitVersion); enlistment.SetGitVersion(gitVersion); this.CheckGVFSHooksVersion(tracer, out string hooksVersion); enlistment.SetGVFSHooksVersion(hooksVersion); this.CheckFileSystemSupportsRequiredFeatures(tracer, enlistment); string errorMessage = null; bool errorIsFatal = false; if (!this.TryValidateGVFSVersion(enlistment, tracer, gvfsConfig, out errorMessage, out errorIsFatal)) { if (errorIsFatal) { this.ReportErrorAndExit(tracer, errorMessage); } else if (showWarnings) { this.Output.WriteLine(); this.Output.WriteLine(errorMessage); this.Output.WriteLine(); } } } protected bool TryCreateAlternatesFile(PhysicalFileSystem fileSystem, GVFSEnlistment enlistment, out string errorMessage) { try { string alternatesFilePath = this.GetAlternatesPath(enlistment); string tempFilePath = alternatesFilePath + ".tmp"; fileSystem.WriteAllText(tempFilePath, enlistment.GitObjectsRoot); fileSystem.MoveAndOverwriteFile(tempFilePath, alternatesFilePath); } catch (SecurityException e) { errorMessage = e.Message; return false; } catch (IOException e) { errorMessage = e.Message; return false; } errorMessage = null; return true; } protected void CheckGVFSHooksVersion(ITracer tracer, out string hooksVersion) { string error; if (!GVFSPlatform.Instance.TryGetGVFSHooksVersion(out hooksVersion, out error)) { this.ReportErrorAndExit(tracer, error); } string gvfsVersion = ProcessHelper.GetCurrentProcessVersion(); if (hooksVersion != gvfsVersion) { this.ReportErrorAndExit(tracer, "GVFS.Hooks version ({0}) does not match GVFS version ({1}).", hooksVersion, gvfsVersion); } } protected void BlockEmptyCacheServerUrl(string userInput) { if (userInput == null) { return; } if (string.IsNullOrWhiteSpace(userInput)) { this.ReportErrorAndExit( @"You must specify a value for the cache server. You can specify a URL, a name of a configured cache server, or the special names None or Default."); } } protected CacheServerInfo ResolveCacheServer( ITracer tracer, CacheServerInfo cacheServer, CacheServerResolver cacheServerResolver, ServerGVFSConfig serverGVFSConfig) { CacheServerInfo resolvedCacheServer = cacheServer; if (cacheServer.Url == null) { string cacheServerName = cacheServer.Name; string error = null; if (!cacheServerResolver.TryResolveUrlFromRemote( cacheServerName, serverGVFSConfig, out resolvedCacheServer, out error)) { this.ReportErrorAndExit(tracer, error); } } else if (cacheServer.Name.Equals(CacheServerInfo.ReservedNames.UserDefined)) { resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverGVFSConfig); } this.Output.WriteLine("Using cache server: " + resolvedCacheServer); return resolvedCacheServer; } protected void ValidatePathParameter(string path) { if (!string.IsNullOrWhiteSpace(path)) { try { Path.GetFullPath(path); } catch (Exception e) { this.ReportErrorAndExit("Invalid path: '{0}' ({1})", path, e.Message); } } } protected bool TryDownloadCommit( string commitId, GVFSEnlistment enlistment, GitObjectsHttpRequestor objectRequestor, GVFSGitObjects gitObjects, GitRepo repo, out string error, bool checkLocalObjectCache = true) { if (!checkLocalObjectCache || !repo.CommitAndRootTreeExists(commitId, out _)) { if (!gitObjects.TryDownloadCommit(commitId)) { error = "Could not download commit " + commitId + " from: " + Uri.EscapeUriString(objectRequestor.CacheServer.ObjectsEndpointUrl); return false; } } error = null; return true; } protected bool TryDownloadRootGitAttributes(GVFSEnlistment enlistment, GVFSGitObjects gitObjects, GitRepo repo, out string error) { List rootEntries = new List(); GitProcess git = new GitProcess(enlistment); GitProcess.Result result = git.LsTree( GVFSConstants.DotGit.HeadName, line => rootEntries.Add(DiffTreeResult.ParseFromLsTreeLine(line)), recursive: false); if (result.ExitCodeIsFailure) { error = "Error returned from ls-tree to find " + GVFSConstants.SpecialGitFiles.GitAttributes + " file: " + result.Errors; return false; } DiffTreeResult gitAttributes = rootEntries.FirstOrDefault(entry => entry.TargetPath.Equals(GVFSConstants.SpecialGitFiles.GitAttributes)); if (gitAttributes == null) { error = "This branch does not contain a " + GVFSConstants.SpecialGitFiles.GitAttributes + " file in the root folder. This file is required by GVFS clone"; return false; } if (!repo.ObjectExists(gitAttributes.TargetSha)) { if (gitObjects.TryDownloadAndSaveObject(gitAttributes.TargetSha, GVFSGitObjects.RequestSource.GVFSVerb) != GitObjects.DownloadAndSaveObjectResult.Success) { error = "Could not download " + GVFSConstants.SpecialGitFiles.GitAttributes + " file"; return false; } } error = null; return true; } /// /// Request that PrjFlt be enabled and attached to the volume of the enlistment root /// /// Enlistment root. If string.Empty, PrjFlt will be enabled but not attached to any volumes /// Error meesage (in the case of failure) /// True is successful and false otherwise protected bool TryEnableAndAttachPrjFltThroughService(string enlistmentRoot, out string errorMessage) { errorMessage = string.Empty; NamedPipeMessages.EnableAndAttachProjFSRequest request = new NamedPipeMessages.EnableAndAttachProjFSRequest(); request.EnlistmentRoot = enlistmentRoot; using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) { if (!client.Connect()) { errorMessage = "GVFS.Service is not responding. " + GVFSVerb.StartServiceInstructions; return false; } try { client.SendRequest(request.ToMessage()); NamedPipeMessages.Message response = client.ReadResponse(); if (response.Header == NamedPipeMessages.EnableAndAttachProjFSRequest.Response.Header) { NamedPipeMessages.EnableAndAttachProjFSRequest.Response message = NamedPipeMessages.EnableAndAttachProjFSRequest.Response.FromMessage(response); if (!string.IsNullOrEmpty(message.ErrorMessage)) { errorMessage = message.ErrorMessage; return false; } if (message.State != NamedPipeMessages.CompletionState.Success) { errorMessage = $"Failed to attach ProjFS to volume."; return false; } else { return true; } } else { errorMessage = string.Format("GVFS.Service responded with unexpected message: {0}", response); return false; } } catch (BrokenPipeException e) { errorMessage = "Unable to communicate with GVFS.Service: " + e.ToString(); return false; } } } protected void LogEnlistmentInfoAndSetConfigValues(ITracer tracer, GitProcess git, GVFSEnlistment enlistment) { string mountId = CreateMountId(); EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId); metadata.Add(nameof(mountId), mountId); metadata.Add("Enlistment", enlistment); metadata.Add("PhysicalDiskInfo", GVFSPlatform.Instance.GetPhysicalDiskInfo(enlistment.WorkingDirectoryRoot, sizeStatsOnly: false)); tracer.RelatedEvent(EventLevel.Informational, "EnlistmentInfo", metadata, Keywords.Telemetry); GitProcess.Result configResult = git.SetInLocalConfig(GVFSConstants.GitConfig.EnlistmentId, RepoMetadata.Instance.EnlistmentId, replaceAll: true); if (configResult.ExitCodeIsFailure) { string error = "Could not update config with enlistment id, error: " + configResult.Errors; tracer.RelatedWarning(error); } configResult = git.SetInLocalConfig(GVFSConstants.GitConfig.MountId, mountId, replaceAll: true); if (configResult.ExitCodeIsFailure) { string error = "Could not update config with mount id, error: " + configResult.Errors; tracer.RelatedWarning(error); } } private static string CreateMountId() { return Guid.NewGuid().ToString("N"); } private static bool TrySetConfig(Enlistment enlistment, Dictionary configSettings, bool isRequired) { GitProcess git = new GitProcess(enlistment); Dictionary existingConfigSettings; // If the settings are required, then only check local config settings, because we don't want to depend on // global settings that can then change independent of this repo. if (!git.TryGetAllConfig(localOnly: isRequired, configSettings: out existingConfigSettings)) { return false; } foreach (KeyValuePair setting in configSettings) { GitConfigSetting existingSetting; if (setting.Value != null) { if (!existingConfigSettings.TryGetValue(setting.Key, out existingSetting) || (isRequired && !existingSetting.HasValue(setting.Value))) { GitProcess.Result setConfigResult = git.SetInLocalConfig(setting.Key, setting.Value); if (setConfigResult.ExitCodeIsFailure) { return false; } } } else { if (existingConfigSettings.TryGetValue(setting.Key, out existingSetting)) { git.DeleteFromLocalConfig(setting.Key); } } } return true; } private string GetAlternatesPath(GVFSEnlistment enlistment) { // Use DotGitRoot (shared .git dir for worktrees) since // objects/info/alternates lives in the shared git directory. return Path.Combine(enlistment.DotGitRoot, GVFSConstants.DotGit.Objects.Info.AlternatesRelativePath); } private void CheckFileSystemSupportsRequiredFeatures(ITracer tracer, Enlistment enlistment) { try { string warning; string error; if (!GVFSPlatform.Instance.KernelDriver.IsSupported(enlistment.EnlistmentRoot, out warning, out error)) { this.ReportErrorAndExit(tracer, $"Error: {error}"); } } catch (VerbAbortedException) { // ReportErrorAndExit throws VerbAbortedException. Catch and re-throw here so that GVFS does not report that // it failed to determine if file system supports required features throw; } catch (Exception e) { if (tracer != null) { EventMetadata metadata = new EventMetadata(); metadata.Add("Exception", e.ToString()); tracer.RelatedError(metadata, "Failed to determine if file system supports features required by GVFS"); } this.ReportErrorAndExit(tracer, "Error: Failed to determine if file system supports features required by GVFS."); } } private void CheckGitVersion(ITracer tracer, GVFSEnlistment enlistment, out string version) { GitVersion gitVersion = null; if (string.IsNullOrEmpty(enlistment.GitBinPath) || !GitProcess.TryGetVersion(enlistment.GitBinPath, out gitVersion, out string _)) { this.ReportErrorAndExit(tracer, "Error: Unable to retrieve the Git version"); } version = gitVersion.ToString(); if (gitVersion.Platform != GVFSConstants.SupportedGitVersion.Platform) { this.ReportErrorAndExit(tracer, "Error: Invalid version of Git {0}. Must use vfs version.", version); } if (gitVersion.IsLessThan(GVFSConstants.SupportedGitVersion)) { this.ReportErrorAndExit( tracer, "Error: Installed Git version {0} is less than the minimum supported version of {1}.", gitVersion, GVFSConstants.SupportedGitVersion); } /* We require that the revision (Z) of the Git version string (2.X.Y.vfs.Z.W) * is an exact match. We will use this to signal that a microsoft/git version introduces * a breaking change that requires a VFS for Git upgrade. * Using the revision part allows us to modify the other version items arbitrarily, * including taking version numbers 2.X.Y from upstream and updating .W if we have any * hotfixes to microsoft/git. */ else if (gitVersion.Revision != GVFSConstants.SupportedGitVersion.Revision) { this.ReportErrorAndExit( tracer, "Error: Installed Git version {0} has revision number {1} instead of {2}." + " This Git version is too new, so either downgrade Git or upgrade VFS for Git." + " The minimum supported version of Git is {3}.", gitVersion, gitVersion.Revision, GVFSConstants.SupportedGitVersion.Revision, GVFSConstants.SupportedGitVersion); } } private bool TryValidateGVFSVersion(GVFSEnlistment enlistment, ITracer tracer, ServerGVFSConfig config, out string errorMessage, out bool errorIsFatal) { errorMessage = null; errorIsFatal = false; using (ITracer activity = tracer.StartActivity("ValidateGVFSVersion", EventLevel.Informational)) { if (ProcessHelper.IsDevelopmentVersion()) { /* Development Version will start with 0 and include a "+{commitID}" suffix * so it won't ever be valid, but it needs to be able to run so we can test it. */ return true; } string recordedVersion = ProcessHelper.GetCurrentProcessVersion(); // Work around the default behavior in .NET SDK 8 where the revision ID // is appended after a '+' character, which cannot be parsed by `System.Version`. int plus = recordedVersion.IndexOf('+'); Version currentVersion = new Version(plus < 0 ? recordedVersion : recordedVersion.Substring(0, plus)); IEnumerable allowedGvfsClientVersions = config != null ? config.AllowedGVFSClientVersions : null; if (allowedGvfsClientVersions == null || !allowedGvfsClientVersions.Any()) { errorMessage = "WARNING: Unable to validate your GVFS version" + Environment.NewLine; if (config == null) { errorMessage += "Could not query valid GVFS versions from: " + Uri.EscapeUriString(enlistment.RepoUrl); } else { errorMessage += "Server not configured to provide supported GVFS versions"; } EventMetadata metadata = new EventMetadata(); tracer.RelatedError(metadata, errorMessage, Keywords.Network); return false; } foreach (ServerGVFSConfig.VersionRange versionRange in config.AllowedGVFSClientVersions) { if (currentVersion >= versionRange.Min && (versionRange.Max == null || currentVersion <= versionRange.Max)) { activity.RelatedEvent( EventLevel.Informational, "GVFSVersionValidated", new EventMetadata { { "SupportedVersionRange", versionRange }, }); enlistment.SetGVFSVersion(currentVersion.ToString()); return true; } } activity.RelatedError("GVFS version {0} is not supported", currentVersion); } errorMessage = "ERROR: Your GVFS version is no longer supported. Install the latest and try again."; errorIsFatal = true; return false; } public abstract class ForExistingEnlistment : GVFSVerb { public ForExistingEnlistment(bool validateOrigin = true) : base(validateOrigin) { } [Value( 0, Required = false, Default = "", MetaName = "Enlistment Root Path", HelpText = "Full or relative path to the GVFS enlistment root")] public override string EnlistmentRootPathParameter { get; set; } public sealed override void Execute() { this.Execute(authentication: null); } public void Execute(GitAuthentication authentication) { this.ValidatePathParameter(this.EnlistmentRootPathParameter); this.PreCreateEnlistment(); GVFSEnlistment enlistment = this.CreateEnlistment(this.EnlistmentRootPathParameter, authentication); this.Execute(enlistment); } protected virtual void PreCreateEnlistment() { } protected abstract void Execute(GVFSEnlistment enlistment); protected void InitializeLocalCacheAndObjectsPaths( ITracer tracer, GVFSEnlistment enlistment, RetryConfig retryConfig, ServerGVFSConfig serverGVFSConfig, CacheServerInfo cacheServer) { string error; if (!RepoMetadata.TryInitialize(tracer, Path.Combine(enlistment.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot), out error)) { this.ReportErrorAndExit(tracer, "Failed to initialize repo metadata: " + error); } this.InitializeCachePathsFromRepoMetadata(tracer, enlistment); // Note: Repos cloned with a version of GVFS that predates the local cache will not have a local cache configured if (!string.IsNullOrWhiteSpace(enlistment.LocalCacheRoot)) { this.EnsureLocalCacheIsHealthy(tracer, enlistment, retryConfig, serverGVFSConfig, cacheServer); } RepoMetadata.Shutdown(); } protected ReturnCode ExecuteGVFSVerb(ITracer tracer, Action configureVerb = null, TextWriter outputWriter = null) where TVerb : GVFSVerb, new() { try { ReturnCode returnCode; StringBuilder commandOutput = new StringBuilder(); using (StringWriter writer = new StringWriter(commandOutput)) { returnCode = this.Execute( this.EnlistmentRootPathParameter, verb => { verb.Output = outputWriter ?? writer; configureVerb?.Invoke(verb); }); } EventMetadata metadata = new EventMetadata(); if (outputWriter == null) { metadata.Add("Output", commandOutput.ToString()); } else { // If a parent verb is redirecting the output of its child, include a reminder // that the child verb's activity was recorded in its own log file metadata.Add("Output", $"Check {new TVerb().VerbName} logs for output"); } metadata.Add("ReturnCode", returnCode); tracer.RelatedEvent(EventLevel.Informational, typeof(TVerb).Name, metadata); return returnCode; } catch (Exception e) { tracer.RelatedError( new EventMetadata { { "Verb", typeof(TVerb).Name }, { "Exception", e.ToString() } }, "ExecuteGVFSVerb: Caught exception"); return ReturnCode.GenericError; } } protected void Unmount(ITracer tracer) { if (!this.ShowStatusWhileRunning( () => { return this.ExecuteGVFSVerb(tracer) != ReturnCode.Success || this.ExecuteGVFSVerb(tracer) == ReturnCode.Success; }, "Unmounting", suppressGvfsLogMessage: true)) { this.ReportErrorAndExit(tracer, "Unable to unmount."); } } private void InitializeCachePathsFromRepoMetadata( ITracer tracer, GVFSEnlistment enlistment) { string error; string gitObjectsRoot; if (!RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error)) { this.ReportErrorAndExit(tracer, "Failed to determine git objects root from repo metadata: " + error); } if (string.IsNullOrWhiteSpace(gitObjectsRoot)) { this.ReportErrorAndExit(tracer, "Invalid git objects root (empty or whitespace)"); } string localCacheRoot; if (!RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error)) { this.ReportErrorAndExit(tracer, "Failed to determine local cache path from repo metadata: " + error); } // Note: localCacheRoot is allowed to be empty, this can occur when upgrading from disk layout version 11 to 12 string blobSizesRoot; if (!RepoMetadata.Instance.TryGetBlobSizesRoot(out blobSizesRoot, out error)) { this.ReportErrorAndExit(tracer, "Failed to determine blob sizes root from repo metadata: " + error); } if (string.IsNullOrWhiteSpace(blobSizesRoot)) { this.ReportErrorAndExit(tracer, "Invalid blob sizes root (empty or whitespace)"); } enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot); } private void EnsureLocalCacheIsHealthy( ITracer tracer, GVFSEnlistment enlistment, RetryConfig retryConfig, ServerGVFSConfig serverGVFSConfig, CacheServerInfo cacheServer) { if (!Directory.Exists(enlistment.LocalCacheRoot)) { try { tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Local cache root: {enlistment.LocalCacheRoot} missing, recreating it"); Directory.CreateDirectory(enlistment.LocalCacheRoot); } catch (Exception e) { EventMetadata metadata = new EventMetadata(); metadata.Add("Exception", e.ToString()); metadata.Add("enlistment.LocalCacheRoot", enlistment.LocalCacheRoot); tracer.RelatedError(metadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create local cache root"); this.ReportErrorAndExit(tracer, "Failed to create local cache: " + enlistment.LocalCacheRoot); } } // Validate that the GitObjectsRoot directory is on disk, and that the GVFS repo is configured to use it. // If the directory is missing (and cannot be found in the mapping file) a new key for the repo will be added // to the mapping file and used for BOTH the GitObjectsRoot and BlobSizesRoot PhysicalFileSystem fileSystem = new PhysicalFileSystem(); if (Directory.Exists(enlistment.GitObjectsRoot)) { bool gitObjectsRootInAlternates = false; string alternatesFilePath = this.GetAlternatesPath(enlistment); if (File.Exists(alternatesFilePath)) { try { using (Stream stream = fileSystem.OpenFileStream( alternatesFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, callFlushFileBuffers: false)) { using (StreamReader reader = new StreamReader(stream)) { while (!reader.EndOfStream) { string alternatesLine = reader.ReadLine(); if (string.Equals(alternatesLine, enlistment.GitObjectsRoot, GVFSPlatform.Instance.Constants.PathComparison)) { gitObjectsRootInAlternates = true; } } } } } catch (Exception e) { EventMetadata exceptionMetadata = new EventMetadata(); exceptionMetadata.Add("Exception", e.ToString()); tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to validate alternates file"); this.ReportErrorAndExit(tracer, $"Failed to validate that alternates file includes git objects root: {e.Message}"); } } else { tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Alternates file not found"); } if (!gitObjectsRootInAlternates) { tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({enlistment.GitObjectsRoot}) missing from alternates files, recreating alternates"); string error; if (!this.TryCreateAlternatesFile(fileSystem, enlistment, out error)) { this.ReportErrorAndExit(tracer, $"Failed to update alternates file to include git objects root: {error}"); } } } else { tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({enlistment.GitObjectsRoot}) missing, determining new root"); if (cacheServer == null) { cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); } string error; if (serverGVFSConfig == null) { if (retryConfig == null) { if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) { this.ReportErrorAndExit(tracer, "Failed to determine GVFS timeout and max retries: " + error); } } serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); } string localCacheKey; LocalCacheResolver localCacheResolver = new LocalCacheResolver(enlistment); if (!localCacheResolver.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers( tracer, serverGVFSConfig, cacheServer, enlistment.LocalCacheRoot, localCacheKey: out localCacheKey, errorMessage: out error)) { this.ReportErrorAndExit(tracer, $"Previous git objects root ({enlistment.GitObjectsRoot}) not found, and failed to determine new local cache key: {error}"); } EventMetadata metadata = new EventMetadata(); metadata.Add("localCacheRoot", enlistment.LocalCacheRoot); metadata.Add("localCacheKey", localCacheKey); metadata.Add(TracingConstants.MessageKey.InfoMessage, "Initializing and persisting updated paths"); tracer.RelatedEvent(EventLevel.Informational, "GVFSVerb_EnsureLocalCacheIsHealthy_InitializePathsFromKey", metadata); enlistment.InitializeCachePathsFromKey(enlistment.LocalCacheRoot, localCacheKey); tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating GitObjectsRoot ({enlistment.GitObjectsRoot}), GitPackRoot ({enlistment.GitPackRoot}), and BlobSizesRoot ({enlistment.BlobSizesRoot})"); try { Directory.CreateDirectory(enlistment.GitObjectsRoot); Directory.CreateDirectory(enlistment.GitPackRoot); } catch (Exception e) { EventMetadata exceptionMetadata = new EventMetadata(); exceptionMetadata.Add("Exception", e.ToString()); exceptionMetadata.Add("enlistment.LocalCacheRoot", enlistment.LocalCacheRoot); exceptionMetadata.Add("enlistment.GitObjectsRoot", enlistment.GitObjectsRoot); exceptionMetadata.Add("enlistment.GitPackRoot", enlistment.GitPackRoot); exceptionMetadata.Add("enlistment.BlobSizesRoot", enlistment.BlobSizesRoot); tracer.RelatedError(exceptionMetadata, $"{nameof(this.InitializeLocalCacheAndObjectsPaths)}: Exception while trying to create objects, pack, and sizes folders"); this.ReportErrorAndExit(tracer, "Failed to create objects, pack, and sizes folders"); } tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating new alternates file"); if (!this.TryCreateAlternatesFile(fileSystem, enlistment, out error)) { this.ReportErrorAndExit(tracer, $"Failed to update alterates file with new objects path: {error}"); } tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving git objects root ({enlistment.GitObjectsRoot}) in repo metadata"); RepoMetadata.Instance.SetGitObjectsRoot(enlistment.GitObjectsRoot); tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving blob sizes root ({enlistment.BlobSizesRoot}) in repo metadata"); RepoMetadata.Instance.SetBlobSizesRoot(enlistment.BlobSizesRoot); } // Validate that the BlobSizesRoot folder is on disk. // Note that if a user performed an action that resulted in the entire .gvfscache being deleted, the code above // for validating GitObjectsRoot will have already taken care of generating a new key and setting a new enlistment.BlobSizesRoot path if (!Directory.Exists(enlistment.BlobSizesRoot)) { tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: BlobSizesRoot ({enlistment.BlobSizesRoot}) not found, re-creating"); try { Directory.CreateDirectory(enlistment.BlobSizesRoot); } catch (Exception e) { EventMetadata exceptionMetadata = new EventMetadata(); exceptionMetadata.Add("Exception", e.ToString()); exceptionMetadata.Add("enlistment.BlobSizesRoot", enlistment.BlobSizesRoot); tracer.RelatedError(exceptionMetadata, $"{nameof(this.InitializeLocalCacheAndObjectsPaths)}: Exception while trying to create blob sizes folder"); this.ReportErrorAndExit(tracer, "Failed to create blob sizes folder"); } } } private GVFSEnlistment CreateEnlistment(string enlistmentRootPath, GitAuthentication authentication) { string gitBinPath = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); if (string.IsNullOrWhiteSpace(gitBinPath)) { this.ReportErrorAndExit("Error: " + GVFSConstants.GitIsNotInstalledError); } GVFSEnlistment enlistment = null; try { enlistment = GVFSEnlistment.CreateFromDirectory( enlistmentRootPath, gitBinPath, authentication, createWithoutRepoURL: !this.validateOriginURL); } catch (InvalidRepoException e) { this.ReportErrorAndExit( "Error: '{0}' is not a valid GVFS enlistment. {1}", enlistmentRootPath, e.Message); } return enlistment; } } public abstract class ForNoEnlistment : GVFSVerb { public ForNoEnlistment(bool validateOrigin = true) : base(validateOrigin) { } public override string EnlistmentRootPathParameter { get { throw new InvalidOperationException(); } set { throw new InvalidOperationException(); } } } public class VerbAbortedException : Exception { public VerbAbortedException(GVFSVerb verb) { this.Verb = verb; } public GVFSVerb Verb { get; } } } } ================================================ FILE: GVFS/GVFS/CommandLine/HealthVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; namespace GVFS.CommandLine { [Verb(HealthVerb.HealthVerbName, HelpText = "EXPERIMENTAL FEATURE - Measure the health of the repository")] public class HealthVerb : GVFSVerb.ForExistingEnlistment { private const string HealthVerbName = "health"; private const decimal MaximumHealthyHydration = 0.5m; [Option( 'n', Required = false, HelpText = "Only display the most hydrated directories in the output")] public int DirectoryDisplayCount { get; set; } = 5; [Option( 'd', "directory", Required = false, HelpText = "Get the health of a specific directory (default is the current working directory")] public string Directory { get; set; } [Option( 's', "status", Required = false, HelpText = "Display only the hydration % of the repository, similar to 'git status' in a repository with sparse-checkout")] public bool StatusOnly { get; set; } protected override string VerbName => HealthVerbName; internal PhysicalFileSystem FileSystem { get; set; } = new PhysicalFileSystem(); protected override void Execute(GVFSEnlistment enlistment) { using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, HealthVerbName)) { tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.Health), EventLevel.Informational, Keywords.Any); if (this.StatusOnly) { this.OutputHydrationPercent(enlistment, tracer); return; } // Now default to the current working directory when running the verb without a specified path if (string.IsNullOrEmpty(this.Directory) || this.Directory.Equals(".")) { if (Environment.CurrentDirectory.StartsWith(enlistment.WorkingDirectoryRoot, GVFSPlatform.Instance.Constants.PathComparison)) { this.Directory = Environment.CurrentDirectory.Substring(enlistment.WorkingDirectoryRoot.Length); } else { // If the path is not under the source root, set the directory to empty this.Directory = string.Empty; } } this.Output.WriteLine("\nGathering repository data..."); this.Directory = this.Directory.Replace(GVFSPlatform.GVFSPlatformConstants.PathSeparator, GVFSConstants.GitPathSeparator); EnlistmentPathData pathData = new EnlistmentPathData(); pathData.LoadPlaceholdersFromDatabase(enlistment); pathData.LoadModifiedPaths(enlistment, tracer); pathData.LoadPathsFromGitIndex(enlistment); pathData.NormalizeAllPaths(); EnlistmentHealthCalculator enlistmentHealthCalculator = new EnlistmentHealthCalculator(pathData); EnlistmentHealthData enlistmentHealthData = enlistmentHealthCalculator.CalculateStatistics(this.Directory); this.PrintOutput(enlistmentHealthData); } } private void OutputHydrationPercent(GVFSEnlistment enlistment, ITracer tracer) { // Try cached summary from mount process first (fast path) string cachedMessage = this.TryGetCachedHydrationMessage(enlistment); if (cachedMessage != null) { this.Output.WriteLine(cachedMessage); return; } // Fall back to in-proc computation with index-based folder count Func folderCountProvider = () => GVFS.Virtualization.Projection.GitIndexProjection.CountIndexFolders(tracer, enlistment.GitIndexPath); EnlistmentHydrationSummary summary = EnlistmentHydrationSummary.CreateSummary( enlistment, this.FileSystem, tracer, folderCountProvider); this.Output.WriteLine(summary.ToMessage()); } /// /// Try to get the cached hydration summary from the mount process via named pipe. /// Returns null if unavailable (GVFS not mounted, no cached value, parse error, timeout). /// private string TryGetCachedHydrationMessage(GVFSEnlistment enlistment) { const int ConnectTimeoutMs = 500; const int TotalTimeoutMs = 1000; try { Task task = Task.Run(() => { using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) { if (!pipeClient.Connect(timeoutMilliseconds: ConnectTimeoutMs)) { return null; } pipeClient.SendRequest(new NamedPipeMessages.Message(NamedPipeMessages.HydrationStatus.Request, null)); NamedPipeMessages.Message response = pipeClient.ReadResponse(); if (response.Header != NamedPipeMessages.HydrationStatus.SuccessResult || !NamedPipeMessages.HydrationStatus.Response.TryParse(response.Body, out NamedPipeMessages.HydrationStatus.Response status)) { return null; } return status.ToDisplayMessage(); } }); if (task.Wait(TotalTimeoutMs) && task.Status == TaskStatus.RanToCompletion) { return task.Result; } return null; } catch (Exception) { return null; } } private void PrintOutput(EnlistmentHealthData enlistmentHealthData) { string trackedFilesCountFormatted = enlistmentHealthData.GitTrackedItemsCount.ToString("N0"); string placeholderCountFormatted = enlistmentHealthData.PlaceholderCount.ToString("N0"); string modifiedPathsCountFormatted = enlistmentHealthData.ModifiedPathsCount.ToString("N0"); // Calculate spacing for the numbers of total files int longest = Math.Max(trackedFilesCountFormatted.Length, placeholderCountFormatted.Length); longest = Math.Max(longest, modifiedPathsCountFormatted.Length); // Sort the dictionary to find the most hydrated directories by health score List topLevelDirectoriesByHydration = enlistmentHealthData.DirectoryHydrationLevels.Take(this.DirectoryDisplayCount).ToList(); this.Output.WriteLine("\nHealth of directory: " + enlistmentHealthData.TargetDirectory); this.Output.WriteLine("Total files in HEAD commit: " + trackedFilesCountFormatted.PadLeft(longest) + " | 100%"); this.Output.WriteLine("Files managed by VFS for Git (fast): " + placeholderCountFormatted.PadLeft(longest) + " | " + this.FormatPercent(enlistmentHealthData.PlaceholderPercentage)); this.Output.WriteLine("Files managed by Git: " + modifiedPathsCountFormatted.PadLeft(longest) + " | " + this.FormatPercent(enlistmentHealthData.ModifiedPathsPercentage)); this.Output.WriteLine("\nTotal hydration percentage: " + this.FormatPercent(enlistmentHealthData.PlaceholderPercentage + enlistmentHealthData.ModifiedPathsPercentage).PadLeft(longest + 7)); this.Output.WriteLine("\nMost hydrated top level directories:"); int maxCountLength = 0; int maxTotalLength = 0; foreach (EnlistmentHealthCalculator.SubDirectoryInfo directoryInfo in topLevelDirectoriesByHydration) { maxCountLength = Math.Max(maxCountLength, directoryInfo.HydratedFileCount.ToString("N0").Length); maxTotalLength = Math.Max(maxTotalLength, directoryInfo.TotalFileCount.ToString("N0").Length); } foreach (EnlistmentHealthCalculator.SubDirectoryInfo directoryInfo in topLevelDirectoriesByHydration) { this.Output.WriteLine(" " + directoryInfo.HydratedFileCount.ToString("N0").PadLeft(maxCountLength) + " / " + directoryInfo.TotalFileCount.ToString("N0").PadRight(maxTotalLength) + " | " + directoryInfo.Name); } bool healthyRepo = (enlistmentHealthData.PlaceholderPercentage + enlistmentHealthData.ModifiedPathsPercentage) < MaximumHealthyHydration; this.Output.WriteLine("\nRepository status: " + (healthyRepo ? "OK" : "Highly Hydrated")); } /// /// Takes a fractional decimal and formats it as a percent taking exactly 4 characters with no decimals /// /// Fractional decimal to format to a percent /// A 4 character string formatting the percent correctly private string FormatPercent(decimal percent) { return percent.ToString("P0").PadLeft(4); } } } ================================================ FILE: GVFS/GVFS/CommandLine/LogVerb.cs ================================================ using CommandLine; using GVFS.Common; using System.Collections.Generic; using System.IO; using System.Linq; namespace GVFS.CommandLine { [Verb(LogVerb.LogVerbName, HelpText = "Show the most recent GVFS log files")] public class LogVerb : GVFSVerb { private const string LogVerbName = "log"; private static readonly int LogNameConsoleOutputFormatWidth = GetMaxLogNameLength(); [Value( 0, Required = false, Default = "", MetaName = "Enlistment Root Path", HelpText = "Full or relative path to the GVFS enlistment root")] public override string EnlistmentRootPathParameter { get; set; } [Option( "type", Default = null, HelpText = "The type of log file to display on the console")] public string LogType { get; set; } protected override string VerbName { get { return LogVerbName; } } public override void Execute() { this.ValidatePathParameter(this.EnlistmentRootPathParameter); this.Output.WriteLine("Most recent log files:"); string errorMessage; string enlistmentRoot; if (!GVFSPlatform.Instance.TryGetGVFSEnlistmentRoot(this.EnlistmentRootPathParameter, out enlistmentRoot, out errorMessage)) { this.ReportErrorAndExit( "Error: '{0}' is not a valid GVFS enlistment", this.EnlistmentRootPathParameter); } string gvfsLogsRoot = Path.Combine( enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot, GVFSConstants.DotGVFS.LogName); if (this.LogType == null) { this.DisplayMostRecent(gvfsLogsRoot, GVFSConstants.LogFileTypes.Clone); // By using MountPrefix ("mount") DisplayMostRecent will display either mount_verb, mount_upgrade, or mount_process, whichever is more recent this.DisplayMostRecent(gvfsLogsRoot, GVFSConstants.LogFileTypes.MountPrefix); this.DisplayMostRecent(gvfsLogsRoot, GVFSConstants.LogFileTypes.Prefetch); this.DisplayMostRecent(gvfsLogsRoot, GVFSConstants.LogFileTypes.Dehydrate); this.DisplayMostRecent(gvfsLogsRoot, GVFSConstants.LogFileTypes.Repair); this.DisplayMostRecent(gvfsLogsRoot, GVFSConstants.LogFileTypes.Sparse); string serviceLogsRoot = GVFSPlatform.Instance.GetLogsDirectoryForGVFSComponent(GVFSConstants.Service.ServiceName); this.DisplayMostRecent(serviceLogsRoot, GVFSConstants.LogFileTypes.Service); } else { string logFile = FindNewestFileInFolder(gvfsLogsRoot, this.LogType); if (logFile == null) { this.ReportErrorAndExit("No log file found"); } else { foreach (string line in File.ReadAllLines(logFile)) { this.Output.WriteLine(line); } } } } private static string FindNewestFileInFolder(string folderName, string logFileType) { string logFilePattern = GetLogFilePatternForType(logFileType); DirectoryInfo logDirectory = new DirectoryInfo(folderName); if (!logDirectory.Exists) { return null; } FileInfo[] files = logDirectory.GetFiles(logFilePattern ?? "*"); if (files.Length == 0) { return null; } return files .OrderByDescending(fileInfo => fileInfo.CreationTime) .First() .FullName; } private static string GetLogFilePatternForType(string logFileType) { return "gvfs_" + logFileType + "_*.log"; } private static int GetMaxLogNameLength() { List lognames = new List { GVFSConstants.LogFileTypes.Clone, GVFSConstants.LogFileTypes.MountPrefix, GVFSConstants.LogFileTypes.Prefetch, GVFSConstants.LogFileTypes.Dehydrate, GVFSConstants.LogFileTypes.Repair, GVFSConstants.LogFileTypes.Sparse, GVFSConstants.LogFileTypes.Service, GVFSConstants.LogFileTypes.UpgradePrefix, }; return lognames.Max(s => s.Length) + 1; } private void DisplayMostRecent(string logFolder, string logFileType) { string logFile = FindNewestFileInFolder(logFolder, logFileType); this.Output.WriteLine( $" {{0, -{LogNameConsoleOutputFormatWidth}}}: {{1}}", logFileType, logFile == null ? "None" : logFile); } } } ================================================ FILE: GVFS/GVFS/CommandLine/MountVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.Http; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using GVFS.DiskLayoutUpgrades; using System; using System.IO; using System.Threading; namespace GVFS.CommandLine { [Verb(MountVerb.MountVerbName, HelpText = "Mount a GVFS virtual repo")] public class MountVerb : GVFSVerb.ForExistingEnlistment { private const string MountVerbName = "mount"; [Option( 'v', GVFSConstants.VerbParameters.Mount.Verbosity, Default = GVFSConstants.VerbParameters.Mount.DefaultVerbosity, Required = false, HelpText = "Sets the verbosity of console logging. Accepts: Verbose, Informational, Warning, Error")] public string Verbosity { get; set; } [Option( 'k', GVFSConstants.VerbParameters.Mount.Keywords, Default = GVFSConstants.VerbParameters.Mount.DefaultKeywords, Required = false, HelpText = "A CSV list of logging filter keywords. Accepts: Any, Network")] public string KeywordsCsv { get; set; } public bool SkipMountedCheck { get; set; } public bool SkipVersionCheck { get; set; } public bool SkipInstallHooks { get; set; } public CacheServerInfo ResolvedCacheServer { get; set; } public ServerGVFSConfig DownloadedGVFSConfig { get; set; } protected override string VerbName { get { return MountVerbName; } } public override void InitializeDefaultParameterValues() { this.Verbosity = GVFSConstants.VerbParameters.Mount.DefaultVerbosity; this.KeywordsCsv = GVFSConstants.VerbParameters.Mount.DefaultKeywords; } protected override void PreCreateEnlistment() { string errorMessage; string enlistmentRoot; // Always check if the given path is a worktree first, before // falling back to the standard .gvfs/ walk-up. A worktree dir // may be under the enlistment tree, so TryGetGVFSEnlistmentRoot // can succeed by walking up — but we still need worktree-specific handling. string pathToCheck = string.IsNullOrEmpty(this.EnlistmentRootPathParameter) ? Environment.CurrentDirectory : this.EnlistmentRootPathParameter; string worktreeError; GVFSEnlistment.WorktreeInfo wtInfo = GVFSEnlistment.TryGetWorktreeInfo(pathToCheck, out worktreeError); if (worktreeError != null) { this.ReportErrorAndExit("Error: failed to check worktree status for '{0}': {1}", pathToCheck, worktreeError); } if (wtInfo?.SharedGitDir != null) { // This is a worktree mount request. Find the primary enlistment root. enlistmentRoot = wtInfo.GetEnlistmentRoot(); if (enlistmentRoot == null) { this.ReportErrorAndExit("Error: could not determine enlistment root for worktree '{0}'", pathToCheck); } // Check the worktree-specific pipe, not the primary if (!this.SkipMountedCheck) { string worktreePipeName = GVFSPlatform.Instance.GetNamedPipeName(enlistmentRoot) + wtInfo.PipeSuffix; using (NamedPipeClient pipeClient = new NamedPipeClient(worktreePipeName)) { if (pipeClient.Connect(500)) { this.ReportErrorAndExit(tracer: null, exitCode: ReturnCode.Success, error: $"The worktree at '{wtInfo.WorktreePath}' is already mounted."); } } } } else if (!GVFSPlatform.Instance.TryGetGVFSEnlistmentRoot(this.EnlistmentRootPathParameter, out enlistmentRoot, out errorMessage)) { this.ReportErrorAndExit("Error: '{0}' is not a valid GVFS enlistment", this.EnlistmentRootPathParameter); } else { // Primary enlistment — check primary pipe as before if (!this.SkipMountedCheck) { if (this.IsExistingPipeListening(enlistmentRoot)) { this.ReportErrorAndExit(tracer: null, exitCode: ReturnCode.Success, error: $"The repo at '{enlistmentRoot}' is already mounted."); } } } if (!DiskLayoutUpgrade.TryRunAllUpgrades(enlistmentRoot)) { this.ReportErrorAndExit("Failed to upgrade repo disk layout. " + ConsoleHelper.GetGVFSLogMessage(enlistmentRoot)); } string error; if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistmentRoot, error: out error)) { this.ReportErrorAndExit("Error: " + error); } } protected override void Execute(GVFSEnlistment enlistment) { string errorMessage = null; string mountExecutableLocation = null; using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "ExecuteMount")) { // Validate these before handing them to the background process // which cannot tell the user when they are bad this.ValidateEnumArgs(); CacheServerInfo cacheServerFromConfig = CacheServerResolver.GetCacheServerFromConfig(enlistment); tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.MountVerb), EventLevel.Verbose, Keywords.Any); tracer.WriteStartEvent( enlistment.EnlistmentRoot, enlistment.RepoUrl, cacheServerFromConfig.Url, new EventMetadata { { "Unattended", this.Unattended }, { "IsElevated", GVFSPlatform.Instance.IsElevated() }, { "NamedPipeName", enlistment.NamedPipeName }, { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, }); if (!GVFSPlatform.Instance.KernelDriver.IsReady(tracer, enlistment.EnlistmentRoot, this.Output, out errorMessage)) { tracer.RelatedEvent( EventLevel.Informational, $"{nameof(MountVerb)}_{nameof(this.Execute)}_EnablingKernelDriverViaService", new EventMetadata { { "KernelDriver.IsReady_Error", errorMessage }, { TracingConstants.MessageKey.InfoMessage, "Service will retry" } }); if (!this.ShowStatusWhileRunning( () => { return this.TryEnableAndAttachPrjFltThroughService(enlistment.EnlistmentRoot, out errorMessage); }, $"Attaching ProjFS to volume")) { this.ReportErrorAndExit(tracer, ReturnCode.FilterError, errorMessage); } } // Verify mount executable exists before launching mountExecutableLocation = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), GVFSPlatform.Instance.Constants.MountExecutableName); if (!File.Exists(mountExecutableLocation)) { this.ReportErrorAndExit(tracer, $"Could not find {GVFSPlatform.Instance.Constants.MountExecutableName}. You may need to reinstall GVFS."); } if (!this.ShowStatusWhileRunning( () => { return this.TryMount(tracer, enlistment, mountExecutableLocation, out errorMessage); }, "Mounting")) { this.ReportErrorAndExit(tracer, errorMessage); } if (!this.Unattended) { tracer.RelatedInfo($"{nameof(this.Execute)}: Registering for automount"); if (this.ShowStatusWhileRunning( () => { return this.RegisterMount(enlistment, out errorMessage); }, "Registering for automount")) { tracer.RelatedInfo($"{nameof(this.Execute)}: Registered for automount"); } else { this.Output.WriteLine(" WARNING: " + errorMessage); tracer.RelatedInfo($"{nameof(this.Execute)}: Failed to register for automount"); } } } } private bool TryMount(ITracer tracer, GVFSEnlistment enlistment, string mountExecutableLocation, out string errorMessage) { const string ParamPrefix = "--"; // For worktrees, pass the worktree path so GVFS.Mount.exe creates the right enlistment string mountPath = enlistment.IsWorktree ? enlistment.WorkingDirectoryRoot : enlistment.EnlistmentRoot; tracer.RelatedInfo($"{nameof(this.TryMount)}: Launching background process('{mountExecutableLocation}') for {mountPath}"); GVFSPlatform.Instance.StartBackgroundVFS4GProcess( tracer, mountExecutableLocation, new[] { mountPath, ParamPrefix + GVFSConstants.VerbParameters.Mount.Verbosity, this.Verbosity, ParamPrefix + GVFSConstants.VerbParameters.Mount.Keywords, this.KeywordsCsv, ParamPrefix + GVFSConstants.VerbParameters.Mount.StartedByService, this.StartedByService.ToString(), ParamPrefix + GVFSConstants.VerbParameters.Mount.StartedByVerb, true.ToString() }); tracer.RelatedInfo($"{nameof(this.TryMount)}: Waiting for repo to be mounted"); return GVFSEnlistment.WaitUntilMounted(tracer, enlistment.NamedPipeName, enlistment.EnlistmentRoot, this.Unattended, out errorMessage); } private bool RegisterMount(GVFSEnlistment enlistment, out string errorMessage) { errorMessage = string.Empty; NamedPipeMessages.RegisterRepoRequest request = new NamedPipeMessages.RegisterRepoRequest(); // Worktree mounts register with their worktree path so they can be // listed and unregistered independently of the primary enlistment. request.EnlistmentRoot = enlistment.IsWorktree ? enlistment.WorkingDirectoryRoot : enlistment.EnlistmentRoot; request.OwnerSID = GVFSPlatform.Instance.GetCurrentUser(); using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) { if (!client.Connect()) { errorMessage = "Unable to register repo because GVFS.Service is not responding."; return false; } try { client.SendRequest(request.ToMessage()); NamedPipeMessages.Message response = client.ReadResponse(); if (response.Header == NamedPipeMessages.RegisterRepoRequest.Response.Header) { NamedPipeMessages.RegisterRepoRequest.Response message = NamedPipeMessages.RegisterRepoRequest.Response.FromMessage(response); if (!string.IsNullOrEmpty(message.ErrorMessage)) { errorMessage = message.ErrorMessage; return false; } if (message.State != NamedPipeMessages.CompletionState.Success) { errorMessage = "Unable to register repo. " + errorMessage; return false; } else { return true; } } else { errorMessage = string.Format("GVFS.Service responded with unexpected message: {0}", response); return false; } } catch (BrokenPipeException e) { errorMessage = "Unable to communicate with GVFS.Service: " + e.ToString(); return false; } } } private void ValidateEnumArgs() { if (!Enum.TryParse(this.KeywordsCsv, out Keywords _)) { this.ReportErrorAndExit("Error: Invalid logging filter keywords: " + this.KeywordsCsv); } if (!Enum.TryParse(this.Verbosity, out EventLevel _)) { this.ReportErrorAndExit("Error: Invalid logging verbosity: " + this.Verbosity); } } } } ================================================ FILE: GVFS/GVFS/CommandLine/PrefetchVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.Maintenance; using GVFS.Common.Prefetch; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; namespace GVFS.CommandLine { [Verb(PrefetchVerb.PrefetchVerbName, HelpText = "Prefetch remote objects for the current head")] public class PrefetchVerb : GVFSVerb.ForExistingEnlistment { private const string PrefetchVerbName = "prefetch"; private const int LockWaitTimeMs = 100; private const int WaitingOnLockLogThreshold = 50; private const int IoFailureRetryDelayMS = 50; private const string PrefetchCommitsAndTreesLock = "prefetch-commits-trees.lock"; private const int ChunkSize = 4000; private static readonly int SearchThreadCount = Environment.ProcessorCount; private static readonly int DownloadThreadCount = Environment.ProcessorCount; private static readonly int IndexThreadCount = Environment.ProcessorCount; [Option( "files", Required = false, Default = "", HelpText = "A semicolon-delimited list of files to fetch. Simple prefix wildcards, e.g. *.txt, are supported.")] public string Files { get; set; } [Option( "folders", Required = false, Default = "", HelpText = "A semicolon-delimited list of folders to fetch. Wildcards are not supported.")] public string Folders { get; set; } [Option( "folders-list", Required = false, Default = "", HelpText = "A file containing line-delimited list of folders to fetch. Wildcards are not supported.")] public string FoldersListFile { get; set; } [Option( "stdin-files-list", Required = false, Default = false, HelpText = "Specify this flag to load file list from stdin. Same format as when loading from file.")] public bool FilesFromStdIn { get; set; } [Option( "stdin-folders-list", Required = false, Default = false, HelpText = "Specify this flag to load folder list from stdin. Same format as when loading from file.")] public bool FoldersFromStdIn { get; set; } [Option( "files-list", Required = false, Default = "", HelpText = "A file containing line-delimited list of files to fetch. Wildcards are supported.")] public string FilesListFile { get; set; } [Option( "hydrate", Required = false, Default = false, HelpText = "Specify this flag to also hydrate files in the working directory.")] public bool HydrateFiles { get; set; } [Option( 'c', "commits", Required = false, Default = false, HelpText = "Fetch the latest set of commit and tree packs. This option cannot be used with any of the file- or folder-related options.")] public bool Commits { get; set; } [Option( "verbose", Required = false, Default = false, HelpText = "Show all outputs on the console in addition to writing them to a log file.")] public bool Verbose { get; set; } public bool SkipVersionCheck { get; set; } public CacheServerInfo ResolvedCacheServer { get; set; } public ServerGVFSConfig ServerGVFSConfig { get; set; } protected override string VerbName { get { return PrefetchVerbName; } } protected override void Execute(GVFSEnlistment enlistment) { using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "Prefetch")) { if (this.Verbose) { tracer.AddDiagnosticConsoleEventListener(EventLevel.Informational, Keywords.Any); } var cacheServerFromConfig = CacheServerResolver.GetCacheServerFromConfig(enlistment); tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.Prefetch), EventLevel.Informational, Keywords.Any); tracer.WriteStartEvent( enlistment.EnlistmentRoot, enlistment.RepoUrl, cacheServerFromConfig.Url); try { EventMetadata metadata = new EventMetadata(); metadata.Add("Commits", this.Commits); metadata.Add("Files", this.Files); metadata.Add("Folders", this.Folders); metadata.Add("FileListFile", this.FilesListFile); metadata.Add("FoldersListFile", this.FoldersListFile); metadata.Add("FilesFromStdIn", this.FilesFromStdIn); metadata.Add("FoldersFromStdIn", this.FoldersFromStdIn); metadata.Add("HydrateFiles", this.HydrateFiles); tracer.RelatedEvent(EventLevel.Informational, "PerformPrefetch", metadata); if (this.Commits) { if (!string.IsNullOrWhiteSpace(this.Files) || !string.IsNullOrWhiteSpace(this.Folders) || !string.IsNullOrWhiteSpace(this.FoldersListFile) || !string.IsNullOrWhiteSpace(this.FilesListFile) || this.FilesFromStdIn || this.FoldersFromStdIn) { this.ReportErrorAndExit(tracer, "You cannot prefetch commits and blobs at the same time."); } if (this.HydrateFiles) { this.ReportErrorAndExit(tracer, "You can only specify --hydrate with --files or --folders"); } GitObjectsHttpRequestor objectRequestor; CacheServerInfo resolvedCacheServer; this.InitializeServerConnection( tracer, enlistment, cacheServerFromConfig, out objectRequestor, out resolvedCacheServer); this.PrefetchCommits(tracer, enlistment, objectRequestor, resolvedCacheServer); } else { string headCommitId; List filesList; List foldersList; FileBasedDictionary lastPrefetchArgs; this.LoadBlobPrefetchArgs(tracer, enlistment, out headCommitId, out filesList, out foldersList, out lastPrefetchArgs); if (BlobPrefetcher.IsNoopPrefetch(tracer, lastPrefetchArgs, headCommitId, filesList, foldersList, this.HydrateFiles)) { Console.WriteLine("All requested files are already available. Nothing new to prefetch."); } else { GitObjectsHttpRequestor objectRequestor; CacheServerInfo resolvedCacheServer; this.InitializeServerConnection( tracer, enlistment, cacheServerFromConfig, out objectRequestor, out resolvedCacheServer); this.PrefetchBlobs(tracer, enlistment, headCommitId, filesList, foldersList, lastPrefetchArgs, objectRequestor, resolvedCacheServer); } } } catch (VerbAbortedException) { throw; } catch (AggregateException aggregateException) { this.Output.WriteLine( "Cannot prefetch {0}. " + ConsoleHelper.GetGVFSLogMessage(enlistment.EnlistmentRoot), enlistment.EnlistmentRoot); foreach (Exception innerException in aggregateException.Flatten().InnerExceptions) { tracer.RelatedError( new EventMetadata { { "Verb", typeof(PrefetchVerb).Name }, { "Exception", innerException.ToString() } }, $"Unhandled {innerException.GetType().Name}: {innerException.Message}"); } Environment.ExitCode = (int)ReturnCode.GenericError; } catch (Exception e) { this.Output.WriteLine( "Cannot prefetch {0}. " + ConsoleHelper.GetGVFSLogMessage(enlistment.EnlistmentRoot), enlistment.EnlistmentRoot); tracer.RelatedError( new EventMetadata { { "Verb", typeof(PrefetchVerb).Name }, { "Exception", e.ToString() } }, $"Unhandled {e.GetType().Name}: {e.Message}"); Environment.ExitCode = (int)ReturnCode.GenericError; } } } private void InitializeServerConnection( ITracer tracer, GVFSEnlistment enlistment, CacheServerInfo cacheServerFromConfig, out GitObjectsHttpRequestor objectRequestor, out CacheServerInfo resolvedCacheServer) { RetryConfig retryConfig = this.GetRetryConfig(tracer, enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); // These this.* arguments are set if this is a follow-on operation from clone or mount. resolvedCacheServer = this.ResolvedCacheServer; ServerGVFSConfig serverGVFSConfig = this.ServerGVFSConfig; // If ResolvedCacheServer is set, then we have already tried querying the server config and checking versions. if (resolvedCacheServer == null) { if (serverGVFSConfig == null) { string authErrorMessage; if (!this.TryAuthenticateAndQueryGVFSConfig( tracer, enlistment, retryConfig, out serverGVFSConfig, out authErrorMessage, fallbackCacheServer: cacheServerFromConfig)) { this.ReportErrorAndExit(tracer, "Unable to prefetch because authentication failed: " + authErrorMessage); } } CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerFromConfig.Url, serverGVFSConfig); if (!this.SkipVersionCheck) { this.ValidateClientVersions(tracer, enlistment, serverGVFSConfig, showWarnings: false); } this.Output.WriteLine("Configured cache server: " + resolvedCacheServer); } this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig, resolvedCacheServer); objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, resolvedCacheServer, retryConfig); } private void PrefetchCommits(ITracer tracer, GVFSEnlistment enlistment, GitObjectsHttpRequestor objectRequestor, CacheServerInfo cacheServer) { bool success; string error = string.Empty; PhysicalFileSystem fileSystem = new PhysicalFileSystem(); GitRepo repo = new GitRepo(tracer, enlistment, fileSystem); GVFSContext context = new GVFSContext(tracer, fileSystem, repo, enlistment); GitObjects gitObjects = new GVFSGitObjects(context, objectRequestor); if (this.Verbose) { success = new PrefetchStep(context, gitObjects, requireCacheLock: false).TryPrefetchCommitsAndTrees(out error); } else { success = this.ShowStatusWhileRunning( () => new PrefetchStep(context, gitObjects, requireCacheLock: false).TryPrefetchCommitsAndTrees(out error), "Fetching commits and trees " + this.GetCacheServerDisplay(cacheServer, enlistment.RepoUrl)); } if (!success) { this.ReportErrorAndExit(tracer, "Prefetching commits and trees failed: " + error); } } private void LoadBlobPrefetchArgs( ITracer tracer, GVFSEnlistment enlistment, out string headCommitId, out List filesList, out List foldersList, out FileBasedDictionary lastPrefetchArgs) { string error; if (!FileBasedDictionary.TryCreate( tracer, Path.Combine(enlistment.DotGVFSRoot, "LastBlobPrefetch.dat"), new PhysicalFileSystem(), out lastPrefetchArgs, out error)) { tracer.RelatedWarning("Unable to load last prefetch args: " + error); } filesList = new List(); foldersList = new List(); if (!BlobPrefetcher.TryLoadFileList(enlistment, this.Files, this.FilesListFile, filesList, readListFromStdIn: this.FilesFromStdIn, error: out error)) { this.ReportErrorAndExit(tracer, error); } if (!BlobPrefetcher.TryLoadFolderList(enlistment, this.Folders, this.FoldersListFile, foldersList, readListFromStdIn: this.FoldersFromStdIn, error: out error)) { this.ReportErrorAndExit(tracer, error); } GitProcess gitProcess = new GitProcess(enlistment); GitProcess.Result result = gitProcess.RevParse(GVFSConstants.DotGit.HeadName); if (result.ExitCodeIsFailure) { this.ReportErrorAndExit(tracer, result.Errors); } headCommitId = result.Output.Trim(); } private void PrefetchBlobs( ITracer tracer, GVFSEnlistment enlistment, string headCommitId, List filesList, List foldersList, FileBasedDictionary lastPrefetchArgs, GitObjectsHttpRequestor objectRequestor, CacheServerInfo cacheServer) { BlobPrefetcher blobPrefetcher = new BlobPrefetcher( tracer, enlistment, objectRequestor, filesList, foldersList, lastPrefetchArgs, ChunkSize, SearchThreadCount, DownloadThreadCount, IndexThreadCount); if (blobPrefetcher.FolderList.Count == 0 && blobPrefetcher.FileList.Count == 0) { this.ReportErrorAndExit(tracer, "Did you mean to fetch all blobs? If so, specify `--files '*'` to confirm."); } if (this.HydrateFiles) { if (!this.CheckIsMounted(verbose: true)) { this.ReportErrorAndExit("You can only specify --hydrate if the repo is mounted. Run 'gvfs mount' and try again."); } } int matchedBlobCount = 0; int downloadedBlobCount = 0; int hydratedFileCount = 0; Func doPrefetch = () => { try { blobPrefetcher.PrefetchWithStats( headCommitId, isBranch: false, hydrateFilesAfterDownload: this.HydrateFiles, matchedBlobCount: out matchedBlobCount, downloadedBlobCount: out downloadedBlobCount, hydratedFileCount: out hydratedFileCount); return !blobPrefetcher.HasFailures; } catch (BlobPrefetcher.FetchException e) { tracer.RelatedError(e.Message); return false; } }; if (this.Verbose) { doPrefetch(); } else { string message = this.HydrateFiles ? "Fetching blobs and hydrating files " : "Fetching blobs "; this.ShowStatusWhileRunning(doPrefetch, message + this.GetCacheServerDisplay(cacheServer, enlistment.RepoUrl)); } if (blobPrefetcher.HasFailures) { Environment.ExitCode = 1; } else { Console.WriteLine(); Console.WriteLine("Stats:"); Console.WriteLine(" Matched blobs: " + matchedBlobCount); Console.WriteLine(" Already cached: " + (matchedBlobCount - downloadedBlobCount)); Console.WriteLine(" Downloaded: " + downloadedBlobCount); if (this.HydrateFiles) { Console.WriteLine(" Hydrated files: " + hydratedFileCount); } } } private bool CheckIsMounted(bool verbose) { Func checkMount = () => this.Execute( this.EnlistmentRootPathParameter, verb => verb.Output = new StreamWriter(new MemoryStream())) == ReturnCode.Success; if (verbose) { return ConsoleHelper.ShowStatusWhileRunning( checkMount, "Checking that GVFS is mounted", this.Output, showSpinner: true, gvfsLogEnlistmentRoot: null); } else { return checkMount(); } } private string GetCacheServerDisplay(CacheServerInfo cacheServer, string repoUrl) { if (!cacheServer.IsNone(repoUrl)) { return "from cache server"; } return "from origin (no cache server)"; } } } ================================================ FILE: GVFS/GVFS/CommandLine/RepairVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using GVFS.DiskLayoutUpgrades; using GVFS.RepairJobs; using System.Collections.Generic; using System.IO; namespace GVFS.CommandLine { [Verb(RepairVerb.RepairVerbName, HelpText = "EXPERIMENTAL FEATURE - Repair issues that prevent a GVFS repo from mounting")] public class RepairVerb : GVFSVerb { private const string RepairVerbName = "repair"; [Value( 1, Required = false, Default = "", MetaName = "Enlistment Root Path", HelpText = "Full or relative path to the GVFS enlistment root")] public override string EnlistmentRootPathParameter { get; set; } [Option( "confirm", Default = false, Required = false, HelpText = "Pass in this flag to actually do repair(s). Without it, only validation will be done.")] public bool Confirmed { get; set; } protected override string VerbName { get { return RepairVerb.RepairVerbName; } } public override void Execute() { this.ValidatePathParameter(this.EnlistmentRootPathParameter); this.CheckGVFSHooksVersion(tracer: null, hooksVersion: out _); if (!Directory.Exists(this.EnlistmentRootPathParameter)) { this.ReportErrorAndExit($"Path '{this.EnlistmentRootPathParameter}' does not exist"); } string errorMessage; string enlistmentRoot; if (!GVFSPlatform.Instance.TryGetGVFSEnlistmentRoot(this.EnlistmentRootPathParameter, out enlistmentRoot, out errorMessage)) { this.ReportErrorAndExit("'gvfs repair' must be run within a GVFS enlistment"); } GVFSEnlistment enlistment = null; try { enlistment = GVFSEnlistment.CreateFromDirectory( this.EnlistmentRootPathParameter, GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath(), authentication: null, createWithoutRepoURL: true); } catch (InvalidRepoException e) { this.ReportErrorAndExit($"Failed to initialize enlistment, error: {e.Message}"); } if (!this.Confirmed) { this.Output.WriteLine( @"WARNING: THIS IS AN EXPERIMENTAL FEATURE This command detects and repairs issues that prevent a GVFS repo from mounting. A few such checks are currently implemented, and some of them can be repaired. More repairs and more checks are coming soon. Without --confirm, it will non-invasively check if repairs are necessary. To actually execute any necessary repair(s), run 'gvfs repair --confirm' "); } string error; if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistment.EnlistmentRoot, error: out error)) { this.ReportErrorAndExit(error); } if (!ConsoleHelper.ShowStatusWhileRunning( () => { // Don't use 'gvfs status' here. The repo may be corrupt such that 'gvfs status' cannot run normally, // causing repair to continue when it shouldn't. using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) { if (!pipeClient.Connect()) { return true; } } return false; }, "Checking that GVFS is not mounted", this.Output, showSpinner: true, gvfsLogEnlistmentRoot: null)) { this.ReportErrorAndExit("You can only run 'gvfs repair' if GVFS is not mounted. Run 'gvfs unmount' and try again."); } this.Output.WriteLine(); using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "RepairVerb", enlistment.GetEnlistmentId(), mountId: null)) { tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.Repair), EventLevel.Verbose, Keywords.Any); tracer.WriteStartEvent( enlistment.EnlistmentRoot, enlistment.RepoUrl, "N/A", new EventMetadata { { "Confirmed", this.Confirmed }, { "IsElevated", GVFSPlatform.Instance.IsElevated() }, { "NamedPipename", enlistment.NamedPipeName }, { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, }); List jobs = new List(); // Repair databases jobs.Add(new BackgroundOperationDatabaseRepairJob(tracer, this.Output, enlistment)); jobs.Add(new RepoMetadataDatabaseRepairJob(tracer, this.Output, enlistment)); jobs.Add(new VFSForGitDatabaseRepairJob(tracer, this.Output, enlistment)); jobs.Add(new BlobSizeDatabaseRepairJob(tracer, this.Output, enlistment)); // Repair .git folder files jobs.Add(new GitHeadRepairJob(tracer, this.Output, enlistment)); jobs.Add(new GitIndexRepairJob(tracer, this.Output, enlistment)); jobs.Add(new GitConfigRepairJob(tracer, this.Output, enlistment)); Dictionary> healthy = new Dictionary>(); Dictionary> cantFix = new Dictionary>(); Dictionary> fixable = new Dictionary>(); foreach (RepairJob job in jobs) { List messages = new List(); switch (job.HasIssue(messages)) { case RepairJob.IssueType.None: healthy[job] = messages; break; case RepairJob.IssueType.CantFix: cantFix[job] = messages; break; case RepairJob.IssueType.Fixable: fixable[job] = messages; break; } } foreach (RepairJob job in healthy.Keys) { this.WriteMessage(tracer, string.Format("{0, -30}: Healthy", job.Name)); this.WriteMessages(tracer, healthy[job]); } if (healthy.Count > 0) { this.Output.WriteLine(); } foreach (RepairJob job in cantFix.Keys) { this.WriteMessage(tracer, job.Name); this.WriteMessages(tracer, cantFix[job]); this.Indent(); this.WriteMessage(tracer, "'gvfs repair' does not currently support fixing this problem"); this.Output.WriteLine(); } foreach (RepairJob job in fixable.Keys) { this.WriteMessage(tracer, job.Name); this.WriteMessages(tracer, fixable[job]); this.Indent(); if (this.Confirmed) { List repairMessages = new List(); switch (job.TryFixIssues(repairMessages)) { case RepairJob.FixResult.Success: this.WriteMessage(tracer, "Repair succeeded"); break; case RepairJob.FixResult.ManualStepsRequired: this.WriteMessage(tracer, "Repair succeeded, but requires some manual steps before remounting."); break; case RepairJob.FixResult.Failure: this.WriteMessage(tracer, "Repair failed. " + ConsoleHelper.GetGVFSLogMessage(enlistment.EnlistmentRoot)); break; } this.WriteMessages(tracer, repairMessages); } else { this.WriteMessage(tracer, "Run 'gvfs repair --confirm' to attempt a repair"); } this.Output.WriteLine(); } } } private void WriteMessage(ITracer tracer, string message) { tracer.RelatedEvent(EventLevel.Informational, "RepairInfo", new EventMetadata { { TracingConstants.MessageKey.InfoMessage, message } }); this.Output.WriteLine(message); } private void WriteMessages(ITracer tracer, List messages) { foreach (string message in messages) { this.Indent(); this.WriteMessage(tracer, message); } } private void Indent() { this.Output.Write(" "); } } } ================================================ FILE: GVFS/GVFS/CommandLine/ServiceVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.NamedPipes; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace GVFS.CommandLine { [Verb(ServiceVerbName, HelpText = "Runs commands for the GVFS service.")] public class ServiceVerb : GVFSVerb.ForNoEnlistment { private const string ServiceVerbName = "service"; [Option( "mount-all", Default = false, Required = false, HelpText = "Mounts all repos")] public bool MountAll { get; set; } [Option( "unmount-all", Default = false, Required = false, HelpText = "Unmounts all repos")] public bool UnmountAll { get; set; } [Option( "list-mounted", Default = false, Required = false, HelpText = "Prints a list of all mounted repos")] public bool List { get; set; } protected override string VerbName { get { return ServiceVerbName; } } public override void Execute() { int optionCount = new[] { this.MountAll, this.UnmountAll, this.List }.Count(flag => flag); if (optionCount == 0) { this.ReportErrorAndExit($"Error: You must specify an argument. Run 'gvfs {ServiceVerbName} --help' for details."); } else if (optionCount > 1) { this.ReportErrorAndExit($"Error: You cannot specify multiple arguments. Run 'gvfs {ServiceVerbName} --help' for details."); } string errorMessage; List repoList; if (!this.TryGetRepoList(out repoList, out errorMessage)) { this.ReportErrorAndExit("Error getting repo list: " + errorMessage); } if (this.List) { foreach (string repoRoot in repoList) { if (this.IsRepoMounted(repoRoot)) { this.Output.WriteLine(repoRoot); } } } else if (this.MountAll) { // Always ask the service to ensure that PrjFlt is enabled. This will ensure that the GVFS installer properly waits for // GVFS.Service to finish enabling PrjFlt's AutoLogger string error; if (!this.TryEnableAndAttachPrjFltThroughService(string.Empty, out error)) { this.ReportErrorAndExit(tracer: null, exitCode: ReturnCode.FilterError, error: $"Failed to enable PrjFlt: {error}"); } List failedRepoRoots = new List(); foreach (string repoRoot in repoList) { if (!this.IsRepoMounted(repoRoot)) { this.Output.WriteLine("\r\nMounting repo at " + repoRoot); ReturnCode result = this.Execute(repoRoot); if (result != ReturnCode.Success) { failedRepoRoots.Add(repoRoot); } } } if (failedRepoRoots.Count() > 0) { string errorString = $"The following repos failed to mount:{Environment.NewLine}{string.Join("\r\n", failedRepoRoots.ToArray())}"; Console.Error.WriteLine(errorString); this.ReportErrorAndExit(Environment.NewLine + errorString); } } else if (this.UnmountAll) { List failedRepoRoots = new List(); foreach (string repoRoot in repoList) { if (this.IsRepoMounted(repoRoot)) { this.Output.WriteLine("\r\nUnmounting repo at " + repoRoot); ReturnCode result = this.Execute( repoRoot, verb => { verb.SkipUnregister = true; verb.SkipLock = true; }); if (result != ReturnCode.Success) { failedRepoRoots.Add(repoRoot); } } } if (failedRepoRoots.Count() > 0) { string errorString = $"The following repos failed to unmount:{Environment.NewLine}{string.Join(Environment.NewLine, failedRepoRoots.ToArray())}"; Console.Error.WriteLine(errorString); this.ReportErrorAndExit(Environment.NewLine + errorString); } } } private bool TryGetRepoList(out List repoList, out string errorMessage) { repoList = null; errorMessage = string.Empty; NamedPipeMessages.GetActiveRepoListRequest request = new NamedPipeMessages.GetActiveRepoListRequest(); using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) { if (!client.Connect()) { errorMessage = "GVFS.Service is not responding."; return false; } try { client.SendRequest(request.ToMessage()); NamedPipeMessages.Message response = client.ReadResponse(); if (response.Header == NamedPipeMessages.GetActiveRepoListRequest.Response.Header) { NamedPipeMessages.GetActiveRepoListRequest.Response message = NamedPipeMessages.GetActiveRepoListRequest.Response.FromMessage(response); if (!string.IsNullOrEmpty(message.ErrorMessage)) { errorMessage = message.ErrorMessage; } else { if (message.State != NamedPipeMessages.CompletionState.Success) { errorMessage = "Unable to retrieve repo list."; } else { repoList = message.RepoList; return true; } } } else { errorMessage = string.Format("GVFS.Service responded with unexpected message: {0}", response); } } catch (BrokenPipeException e) { errorMessage = "Unable to communicate with GVFS.Service: " + e.ToString(); } return false; } } private bool IsRepoMounted(string repoRoot) { // Hide the output of status StringWriter statusOutput = new StringWriter(); ReturnCode result = this.Execute( repoRoot, verb => { verb.Output = statusOutput; }); if (result == ReturnCode.Success) { return true; } return false; } } } ================================================ FILE: GVFS/GVFS/CommandLine/SparseVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.Database; using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; namespace GVFS.CommandLine { [Verb( SparseVerb.SparseVerbName, HelpText = @"EXPERIMENTAL: List, add, or remove from the list of folders that are included in VFS for Git's projection. Folders need to be relative to the repos root directory.") ] public class SparseVerb : GVFSVerb.ForExistingEnlistment { private const string SparseVerbName = "sparse"; private const string FolderListSeparator = ";"; private const char StatusPathSeparatorToken = '\0'; private const char StatusRenameToken = 'R'; private const string PruneOptionName = "prune"; private enum SetDirectoryTimeResult { Success, Failure, DirectoryDoesNotExist } [Option( 's', "set", Required = false, Default = "", HelpText = "A semicolon-delimited list of repo root relative folders to use as the sparse set for determining what to project. Wildcards are not supported.")] public string Set { get; set; } [Option( 'f', "file", Required = false, Default = "", HelpText = "Path to a file that will has repo root relative folders to use as the sparse set. One folder per line. Wildcards are not supported.")] public string File { get; set; } [Option( 'a', "add", Required = false, Default = "", HelpText = "A semicolon-delimited list of repo root relative folders to include in the sparse set for determining what to project. Wildcards are not supported.")] public string Add { get; set; } [Option( 'r', "remove", Required = false, Default = "", HelpText = "A semicolon-delimited list of repo root relative folders to remove from the sparse set for determining what to project. Wildcards are not supported.")] public string Remove { get; set; } [Option( 'l', "list", Required = false, Default = false, HelpText = "List of folders in the sparse set for determining what to project.")] public bool List { get; set; } [Option( 'p', PruneOptionName, Required = false, Default = false, HelpText = "Remove any folders that are not in the list of sparse folders.")] public bool Prune { get; set; } [Option( 'd', "disable", Required = false, Default = false, HelpText = "Disable the sparse feature. This will remove all folders in the sparse list and start projecting all folders.")] public bool Disable { get; set; } protected override string VerbName => SparseVerbName; internal static string GetNextGitPath(ref int index, string statusOutput) { int endOfPathIndex = statusOutput.IndexOf(StatusPathSeparatorToken, index); string gitPath = statusOutput.Substring(index, endOfPathIndex - index); index = endOfPathIndex + 1; return gitPath; } internal static bool PathCoveredBySparseFolders(string gitPath, HashSet sparseFolders) { string filePath = gitPath.Replace(GVFSConstants.GitPathSeparator, Path.DirectorySeparatorChar); if (sparseFolders.Any(x => filePath.StartsWith(x + Path.DirectorySeparatorChar, GVFSPlatform.Instance.Constants.PathComparison))) { // Path is covered by a recursive entry return true; } int pathSeparatorIndex = filePath.LastIndexOf(Path.DirectorySeparatorChar); if (pathSeparatorIndex < 0) { // Path is in the root, and root entries are always in the sparse set return true; } // Get the parent path (including the path separator) string parentPath = filePath.Substring(startIndex: 0, length: pathSeparatorIndex + 1); if (sparseFolders.Any(x => x.StartsWith(parentPath, GVFSPlatform.Instance.Constants.PathComparison))) { // Path is a child of a non-recursive entry // // Example: // - Sparse set: A\B\C // - filePath: A\B\d.txt // // This file is in the sparse set because its parent ("A\B\") is an ancestor of a recursive // entry ("A\B\C\") in the sparse set return true; } return false; } protected override void Execute(GVFSEnlistment enlistment) { if (this.List || ( !this.Prune && !this.Disable && string.IsNullOrEmpty(this.Add) && string.IsNullOrEmpty(this.Remove) && string.IsNullOrEmpty(this.Set) && string.IsNullOrEmpty(this.File))) { this.ListSparseFolders(enlistment.EnlistmentRoot); return; } this.CheckOptions(); using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, SparseVerbName)) { tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.Sparse), EventLevel.Informational, Keywords.Any); EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(this.Set), this.Set); metadata.Add(nameof(this.File), this.File); metadata.Add(nameof(this.Add), this.Add); metadata.Add(nameof(this.Remove), this.Remove); metadata.Add(nameof(this.Prune), this.Prune); metadata.Add(nameof(this.Disable), this.Disable); tracer.RelatedInfo(metadata, $"Running sparse"); HashSet directories; bool needToChangeProjection = false; using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistment.EnlistmentRoot, new SqliteDatabase())) { SparseTable sparseTable = new SparseTable(database); directories = sparseTable.GetAll(); List foldersToRemove = new List(); List foldersToAdd = new List(); if (this.Disable) { if (directories.Count > 0) { this.WriteMessage(tracer, "Removing all folders from sparse list. When the sparse list is empty, all folders are projected."); needToChangeProjection = true; foldersToRemove.AddRange(directories); directories.Clear(); } else { return; } } else if (!string.IsNullOrEmpty(this.Set) || !string.IsNullOrEmpty(this.File)) { IEnumerable folders = null; if (!string.IsNullOrEmpty(this.Set)) { folders = this.ParseFolderList(this.Set); } else if (!string.IsNullOrEmpty(this.File)) { PhysicalFileSystem fileSystem = new PhysicalFileSystem(); folders = this.ParseFolderList(fileSystem.ReadAllText(this.File), folderSeparator: Environment.NewLine); } else { this.WriteMessage(tracer, "Invalid options specified."); throw new InvalidOperationException(); } foreach (string folder in folders) { if (!directories.Contains(folder)) { needToChangeProjection = true; foldersToAdd.Add(folder); } else { // Remove from directories so that the only directories left in the directories collection // will be the ones that will need to be removed from sparse set directories.Remove(folder); } } if (directories.Count > 0) { needToChangeProjection = true; foldersToRemove.AddRange(directories); directories.Clear(); } // Need to add folders that will be in the projection back into directories for the status check foreach (string folder in folders) { directories.Add(folder); } } else { // Process adds and removes foreach (string folder in this.ParseFolderList(this.Remove)) { if (directories.Contains(folder)) { needToChangeProjection = true; directories.Remove(folder); foldersToRemove.Add(folder); } } foreach (string folder in this.ParseFolderList(this.Add)) { if (!directories.Contains(folder)) { needToChangeProjection = true; directories.Add(folder); foldersToAdd.Add(folder); } } } if (needToChangeProjection || this.Prune) { if (directories.Count > 0) { // Make sure there is a clean git status before allowing sparse set to change this.CheckGitStatus(tracer, enlistment, directories); } this.UpdateSparseFolders(tracer, sparseTable, foldersToRemove, foldersToAdd); } if (needToChangeProjection) { // Force a projection update to get the current inclusion set this.ForceProjectionChange(tracer, enlistment); tracer.RelatedInfo("Projection updated after adding or removing folders."); } else { this.WriteMessage(tracer, "No folders to update in sparse set."); } List foldersPruned; if (this.Prune && directories.Count > 0) { foldersPruned = this.PruneFoldersOutsideSparse(tracer, enlistment, sparseTable); } else { foldersPruned = new List(); } if (needToChangeProjection || this.Prune) { // Update the last write times of the parents of folders being added/removed // so that File Explorer will refresh them UpdateParentFolderLastWriteTimes(tracer, enlistment.WorkingDirectoryBackingRoot, foldersToRemove, foldersToAdd, foldersPruned); } } } } private static void UpdateParentFolderLastWriteTimes( ITracer tracer, string rootPath, IEnumerable foldersRemoved, IEnumerable foldersAdded, IEnumerable foldersPruned) { Stopwatch updateTime = Stopwatch.StartNew(); HashSet foldersToUpdate = new HashSet(GVFSPlatform.Instance.Constants.PathComparer); AddNonRootParentPathsToSet(foldersToUpdate, foldersRemoved); AddNonRootParentPathsToSet(foldersToUpdate, foldersAdded); AddNonRootParentPathsToSet(foldersToUpdate, foldersPruned); DateTime refreshTime = DateTime.Now; int foldersUpdated = 0; int foldersNotFound = 0; int folderErrors = 0; // Always refresh the root SetFolderLastWriteTime( tracer, rootPath, refreshTime, ref foldersUpdated, ref folderErrors, ref foldersNotFound); string folderPathPrefix = $"{rootPath}{Path.DirectorySeparatorChar}"; foreach (string path in foldersToUpdate) { SetFolderLastWriteTime( tracer, folderPathPrefix + path, refreshTime, ref foldersUpdated, ref folderErrors, ref foldersNotFound); } updateTime.Stop(); EventMetadata metadata = new EventMetadata(); metadata.Add("foldersToRefresh", foldersToUpdate.Count + 1); // +1 for the root metadata.Add(nameof(foldersUpdated), foldersUpdated); metadata.Add(nameof(folderErrors), folderErrors); metadata.Add(nameof(foldersNotFound), foldersNotFound); metadata.Add(nameof(updateTime.ElapsedMilliseconds), updateTime.ElapsedMilliseconds); metadata.Add(TracingConstants.MessageKey.InfoMessage, "Updated folder last write times"); tracer.RelatedEvent(EventLevel.Informational, $"{nameof(UpdateParentFolderLastWriteTimes)}_Summary", metadata); } private static void AddNonRootParentPathsToSet(HashSet set, IEnumerable folderPaths) { foreach (string folderPath in folderPaths) { int lastSeparatorIndex = folderPath.LastIndexOf(Path.DirectorySeparatorChar); string parentPath = folderPath; while (lastSeparatorIndex > 0) { parentPath = parentPath.Substring(0, lastSeparatorIndex); set.Add(parentPath); lastSeparatorIndex = parentPath.LastIndexOf(Path.DirectorySeparatorChar); } } } private static void SetFolderLastWriteTime( ITracer tracer, string path, DateTime time, ref int successCount, ref int failureCount, ref int directoryNotFoundCount) { SetDirectoryTimeResult result = SetFolderLastWriteTime(tracer, path, time); switch (result) { case SetDirectoryTimeResult.Success: ++successCount; break; case SetDirectoryTimeResult.Failure: ++failureCount; break; case SetDirectoryTimeResult.DirectoryDoesNotExist: ++directoryNotFoundCount; break; } } private static SetDirectoryTimeResult SetFolderLastWriteTime(ITracer tracer, string folderPath, DateTime time) { try { GVFSPlatform.Instance.FileSystem.SetDirectoryLastWriteTime(folderPath, time, out bool directoryExists); if (directoryExists) { return SetDirectoryTimeResult.Success; } return SetDirectoryTimeResult.DirectoryDoesNotExist; } catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is Win32Exception) { EventMetadata metadata = new EventMetadata(); metadata.Add("Exception", e.ToString()); metadata.Add(nameof(folderPath), folderPath); metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(SetFolderLastWriteTime)}: Failed to update folder write time"); tracer.RelatedEvent(EventLevel.Informational, $"{nameof(SetFolderLastWriteTime)}_FailedWriteTimeUpdate", metadata); } return SetDirectoryTimeResult.Failure; } private List PruneFoldersOutsideSparse(ITracer tracer, Enlistment enlistment, SparseTable sparseTable) { List directoriesToDehydrate = new List(); if (!this.ShowStatusWhileRunning( () => { directoriesToDehydrate = this.GetDirectoriesOutsideSparse(enlistment.WorkingDirectoryBackingRoot, sparseTable); return true; }, $"Finding folders to {PruneOptionName}")) { this.ReportErrorAndExit(tracer, $"Failed to {PruneOptionName}."); } this.WriteMessage(tracer, $"Found {directoriesToDehydrate.Count} folders to {PruneOptionName}."); if (directoriesToDehydrate.Count > 0) { ReturnCode verbReturnCode = this.ExecuteGVFSVerb( tracer, verb => { verb.RunningVerbName = this.VerbName; verb.ActionName = PruneOptionName; verb.Confirmed = true; verb.StatusChecked = true; verb.Folders = string.Join(FolderListSeparator, directoriesToDehydrate); }, this.Output); if (verbReturnCode != ReturnCode.Success) { this.ReportErrorAndExit(tracer, verbReturnCode, $"Failed to {PruneOptionName}. Exit Code: {verbReturnCode}"); } } return directoriesToDehydrate; } private List GetDirectoriesOutsideSparse(string rootPath, SparseTable sparseTable) { HashSet sparseFolders = sparseTable.GetAll(); PhysicalFileSystem fileSystem = new PhysicalFileSystem(); Queue foldersToEnumerate = new Queue(); foldersToEnumerate.Enqueue(rootPath); List foldersOutsideSparse = new List(); while (foldersToEnumerate.Count > 0) { string folderToEnumerate = foldersToEnumerate.Dequeue(); foreach (string directory in fileSystem.EnumerateDirectories(folderToEnumerate)) { string enlistmentRootRelativeFolderPath = GVFSDatabase.NormalizePath(directory.Substring(rootPath.Length)); if (!enlistmentRootRelativeFolderPath.Equals(GVFSConstants.DotGit.Root, GVFSPlatform.Instance.Constants.PathComparison)) { if (sparseFolders.Any(x => x.StartsWith(enlistmentRootRelativeFolderPath + Path.DirectorySeparatorChar, GVFSPlatform.Instance.Constants.PathComparison))) { foldersToEnumerate.Enqueue(directory); } else if (!sparseFolders.Contains(enlistmentRootRelativeFolderPath)) { foldersOutsideSparse.Add(enlistmentRootRelativeFolderPath); } } } } return foldersOutsideSparse; } private void UpdateSparseFolders(ITracer tracer, SparseTable sparseTable, List foldersToRemove, List foldersToAdd) { if (!this.ShowStatusWhileRunning( () => { foreach (string directoryPath in foldersToRemove) { tracer.RelatedInfo($"Removing '{directoryPath}' from sparse folders."); sparseTable.Remove(directoryPath); } foreach (string directoryPath in foldersToAdd) { tracer.RelatedInfo($"Adding '{directoryPath}' to sparse folders."); sparseTable.Add(directoryPath); } return true; }, "Updating sparse folder set", suppressGvfsLogMessage: true)) { this.ReportErrorAndExit(tracer, "Failed to update sparse folder set."); } } private void CheckOptions() { if (this.Disable && ( this.Prune || !string.IsNullOrEmpty(this.Set) || !string.IsNullOrEmpty(this.Add) || !string.IsNullOrEmpty(this.Remove) || !string.IsNullOrEmpty(this.File))) { this.ReportErrorAndExit("--disable not valid with other options."); } if (!string.IsNullOrEmpty(this.Set) && ( !string.IsNullOrEmpty(this.Add) || !string.IsNullOrEmpty(this.Remove) || !string.IsNullOrEmpty(this.File))) { this.ReportErrorAndExit("--set not valid with other options."); } if (!string.IsNullOrEmpty(this.File) && ( !string.IsNullOrEmpty(this.Add) || !string.IsNullOrEmpty(this.Remove) || !string.IsNullOrEmpty(this.Set))) { this.ReportErrorAndExit("--file not valid with other options."); } } private void ListSparseFolders(string enlistmentRoot) { using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistmentRoot, new SqliteDatabase())) { SparseTable sparseTable = new SparseTable(database); HashSet directories = sparseTable.GetAll(); if (directories.Count == 0) { this.Output.WriteLine("No folders in sparse list. When the sparse list is empty, all folders are projected."); } else { foreach (string directory in directories) { this.Output.WriteLine(directory); } } } } private IEnumerable ParseFolderList(string folders, string folderSeparator = FolderListSeparator) { if (string.IsNullOrEmpty(folders)) { return new string[0]; } else { return folders.Split(new[] { folderSeparator }, StringSplitOptions.RemoveEmptyEntries) .Select(x => GVFSDatabase.NormalizePath(x)); } } private void ForceProjectionChange(ITracer tracer, GVFSEnlistment enlistment) { string errorMessage = null; if (!this.ShowStatusWhileRunning( () => { NamedPipeMessages.PostIndexChanged.Response response = null; try { using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) { if (!pipeClient.Connect()) { this.ReportErrorAndExit("Unable to connect to GVFS. Try running 'gvfs mount'"); } NamedPipeMessages.PostIndexChanged.Request request = new NamedPipeMessages.PostIndexChanged.Request(updatedWorkingDirectory: true, updatedSkipWorktreeBits: false); pipeClient.SendRequest(request.CreateMessage()); response = new NamedPipeMessages.PostIndexChanged.Response(NamedPipeMessages.Message.FromString(pipeClient.ReadRawResponse()).Header); return response.Result == NamedPipeMessages.PostIndexChanged.SuccessResult; } } catch (BrokenPipeException e) { this.ReportErrorAndExit("Unable to communicate with GVFS: " + e.ToString()); return false; } }, "Forcing a projection change", suppressGvfsLogMessage: true)) { this.WriteMessage(tracer, "Failed to change projection: " + errorMessage); } } private void CheckGitStatus(ITracer tracer, GVFSEnlistment enlistment, HashSet sparseFolders) { GitProcess.Result statusResult = null; HashSet dirtyPathsNotInSparseSet = null; if (!this.ShowStatusWhileRunning( () => { GitProcess git = new GitProcess(enlistment); statusResult = git.StatusPorcelain(); if (statusResult.ExitCodeIsFailure) { return false; } dirtyPathsNotInSparseSet = this.GetPathsNotCoveredBySparseFolders(statusResult.Output, sparseFolders); return dirtyPathsNotInSparseSet.Count == 0; }, "Running git status", suppressGvfsLogMessage: true)) { this.Output.WriteLine(); if (statusResult.ExitCodeIsFailure) { this.WriteMessage(tracer, "Failed to run git status: " + statusResult.Errors); } else { StringBuilder dirtyFilesMessage = new StringBuilder(); dirtyFilesMessage.AppendLine("git status reported that you have dirty files:"); dirtyFilesMessage.AppendLine(); foreach (string path in dirtyPathsNotInSparseSet) { dirtyFilesMessage.AppendLine($" {path}"); } dirtyFilesMessage.AppendLine(); dirtyFilesMessage.Append("Either commit your changes or reset and clean"); this.WriteMessage(tracer, dirtyFilesMessage.ToString()); } this.Output.WriteLine(); this.ReportErrorAndExit(tracer, "Sparse was aborted."); } } private HashSet GetPathsNotCoveredBySparseFolders(string statusOutput, HashSet sparseFolders) { HashSet uncoveredPaths = new HashSet(); int index = 0; while (index < statusOutput.Length - 1) { bool isRename = statusOutput[index] == StatusRenameToken || statusOutput[index + 1] == StatusRenameToken; index = index + 3; string gitPath = GetNextGitPath(ref index, statusOutput); if (!PathCoveredBySparseFolders(gitPath, sparseFolders)) { uncoveredPaths.Add(gitPath); } if (isRename) { gitPath = GetNextGitPath(ref index, statusOutput); if (!PathCoveredBySparseFolders(gitPath, sparseFolders)) { uncoveredPaths.Add(gitPath); } } } return uncoveredPaths; } private void WriteMessage(ITracer tracer, string message) { this.Output.WriteLine(message); tracer.RelatedEvent( EventLevel.Informational, SparseVerbName, new EventMetadata { { TracingConstants.MessageKey.InfoMessage, message } }); } } } ================================================ FILE: GVFS/GVFS/CommandLine/StatusVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.NamedPipes; namespace GVFS.CommandLine { [Verb(StatusVerb.StatusVerbName, HelpText = "Get the status of the GVFS virtual repo")] public class StatusVerb : GVFSVerb.ForExistingEnlistment { private const string StatusVerbName = "status"; protected override string VerbName { get { return StatusVerbName; } } protected override void Execute(GVFSEnlistment enlistment) { using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) { if (!pipeClient.Connect()) { this.ReportErrorAndExit("Unable to connect to GVFS. Try running 'gvfs mount'"); } try { pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); NamedPipeMessages.GetStatus.Response getStatusResponse = NamedPipeMessages.GetStatus.Response.FromJson(pipeClient.ReadRawResponse()); this.Output.WriteLine("Enlistment root: " + getStatusResponse.EnlistmentRoot); this.Output.WriteLine("Repo URL: " + getStatusResponse.RepoUrl); this.Output.WriteLine("Cache Server: " + getStatusResponse.CacheServer); this.Output.WriteLine("Local Cache: " + getStatusResponse.LocalCacheRoot); this.Output.WriteLine("Mount status: " + getStatusResponse.MountStatus); this.Output.WriteLine("GVFS Lock: " + getStatusResponse.LockStatus); this.Output.WriteLine("Background operations: " + getStatusResponse.BackgroundOperationCount); this.Output.WriteLine("Disk layout version: " + getStatusResponse.DiskLayoutVersion); } catch (BrokenPipeException e) { this.ReportErrorAndExit("Unable to communicate with GVFS: " + e.ToString()); } } } } } ================================================ FILE: GVFS/GVFS/CommandLine/UnmountVerb.cs ================================================ using CommandLine; using GVFS.Common; using GVFS.Common.NamedPipes; using System.Diagnostics; namespace GVFS.CommandLine { [Verb(UnmountVerb.UnmountVerbName, HelpText = "Unmount a GVFS virtual repo")] public class UnmountVerb : GVFSVerb { private const string UnmountVerbName = "unmount"; [Value( 0, Required = false, Default = "", MetaName = "Enlistment Root Path", HelpText = "Full or relative path to the GVFS enlistment root")] public override string EnlistmentRootPathParameter { get; set; } [Option( GVFSConstants.VerbParameters.Unmount.SkipLock, Default = false, Required = false, HelpText = "Force unmount even if the lock is not available.")] public bool SkipLock { get; set; } public bool SkipUnregister { get; set; } protected override string VerbName { get { return UnmountVerbName; } } public override void Execute() { this.ValidatePathParameter(this.EnlistmentRootPathParameter); string errorMessage = null; string root; string pipeName; // Check for worktree first — a worktree path will walk up // to find the primary .gvfs/ but needs its own pipe name. string pathToCheck = string.IsNullOrEmpty(this.EnlistmentRootPathParameter) ? System.Environment.CurrentDirectory : this.EnlistmentRootPathParameter; string registrationPath; string worktreeError; GVFSEnlistment.WorktreeInfo wtInfo = GVFSEnlistment.TryGetWorktreeInfo(pathToCheck, out worktreeError); if (worktreeError != null) { this.ReportErrorAndExit("Error: failed to check worktree status for '{0}': {1}", pathToCheck, worktreeError); } if (wtInfo?.SharedGitDir != null) { root = wtInfo.GetEnlistmentRoot(); if (root == null) { this.ReportErrorAndExit("Error: could not determine enlistment root for worktree '{0}'", pathToCheck); } pipeName = GVFSPlatform.Instance.GetNamedPipeName(root) + wtInfo.PipeSuffix; // Worktree mounts register with their worktree path, // so unregister with the same path — not the primary root. registrationPath = wtInfo.WorktreePath; } else if (!GVFSPlatform.Instance.TryGetGVFSEnlistmentRoot(this.EnlistmentRootPathParameter, out root, out errorMessage)) { this.ReportErrorAndExit( "Error: '{0}' is not a valid GVFS enlistment", this.EnlistmentRootPathParameter); return; } else { pipeName = GVFSPlatform.Instance.GetNamedPipeName(root); registrationPath = root; } if (!this.SkipLock) { this.AcquireLock(pipeName, root); } if (!this.ShowStatusWhileRunning( () => { return this.Unmount(pipeName, out errorMessage); }, "Unmounting")) { this.ReportErrorAndExit(errorMessage); } if (!this.Unattended && !this.SkipUnregister) { if (!this.ShowStatusWhileRunning( () => { return this.UnregisterRepo(registrationPath, out errorMessage); }, "Unregistering automount")) { this.Output.WriteLine(" WARNING: " + errorMessage); } } } private bool Unmount(string pipeName, out string errorMessage) { errorMessage = string.Empty; string rawGetStatusResponse = string.Empty; try { using (NamedPipeClient pipeClient = new NamedPipeClient(pipeName)) { if (!pipeClient.Connect()) { errorMessage = "Unable to connect to GVFS.Mount"; return false; } pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); rawGetStatusResponse = pipeClient.ReadRawResponse(); NamedPipeMessages.GetStatus.Response getStatusResponse = NamedPipeMessages.GetStatus.Response.FromJson(rawGetStatusResponse); switch (getStatusResponse.MountStatus) { case NamedPipeMessages.GetStatus.Mounting: errorMessage = "Still mounting, please try again later"; return false; case NamedPipeMessages.GetStatus.Unmounting: errorMessage = "Already unmounting, please wait"; return false; case NamedPipeMessages.GetStatus.Ready: break; case NamedPipeMessages.GetStatus.MountFailed: break; default: errorMessage = "Unrecognized response to GetStatus: " + rawGetStatusResponse; return false; } pipeClient.SendRequest(NamedPipeMessages.Unmount.Request); string unmountResponse = pipeClient.ReadRawResponse(); switch (unmountResponse) { case NamedPipeMessages.Unmount.Acknowledged: string finalResponse = pipeClient.ReadRawResponse(); if (finalResponse == NamedPipeMessages.Unmount.Completed) { errorMessage = string.Empty; return true; } else { errorMessage = "Unrecognized final response to unmount: " + finalResponse; return false; } case NamedPipeMessages.Unmount.NotMounted: errorMessage = "Unable to unmount, repo was not mounted"; return false; case NamedPipeMessages.Unmount.MountFailed: errorMessage = "Unable to unmount, previous mount attempt failed"; return false; default: errorMessage = "Unrecognized response to unmount: " + unmountResponse; return false; } } } catch (BrokenPipeException e) { errorMessage = "Unable to communicate with GVFS: " + e.ToString(); return false; } } private bool UnregisterRepo(string rootPath, out string errorMessage) { errorMessage = string.Empty; NamedPipeMessages.UnregisterRepoRequest request = new NamedPipeMessages.UnregisterRepoRequest(); request.EnlistmentRoot = rootPath; using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) { if (!client.Connect()) { errorMessage = "Unable to unregister repo because GVFS.Service is not responding. " + GVFSVerb.StartServiceInstructions; return false; } try { client.SendRequest(request.ToMessage()); NamedPipeMessages.Message response = client.ReadResponse(); if (response.Header == NamedPipeMessages.UnregisterRepoRequest.Response.Header) { NamedPipeMessages.UnregisterRepoRequest.Response message = NamedPipeMessages.UnregisterRepoRequest.Response.FromMessage(response); if (message.State != NamedPipeMessages.CompletionState.Success) { errorMessage = message.ErrorMessage; return false; } else { errorMessage = string.Empty; return true; } } else { errorMessage = string.Format("GVFS.Service responded with unexpected message: {0}", response); return false; } } catch (BrokenPipeException e) { errorMessage = "Unable to communicate with GVFS.Service: " + e.ToString(); return false; } } } private void AcquireLock(string pipeName, string enlistmentRoot) { using (NamedPipeClient pipeClient = new NamedPipeClient(pipeName)) { try { if (!pipeClient.Connect()) { this.ReportErrorAndExit("Unable to connect to GVFS while acquiring lock to unmount. Try 'gvfs status' to verify if the repo is mounted."); return; } Process currentProcess = Process.GetCurrentProcess(); string result = null; if (!GVFSLock.TryAcquireGVFSLockForProcess( this.Unattended, pipeClient, "gvfs unmount", currentProcess.Id, GVFSPlatform.Instance.IsElevated(), isConsoleOutputRedirectedToFile: GVFSPlatform.Instance.IsConsoleOutputRedirectedToFile(), checkAvailabilityOnly: false, gvfsEnlistmentRoot: enlistmentRoot, gitCommandSessionId: string.Empty, result: out result)) { this.ReportErrorAndExit("Unable to acquire the lock prior to unmount. " + result); } } catch (BrokenPipeException) { this.ReportErrorAndExit("Unable to acquire the lock prior to unmount. Try 'gvfs status' to verify if the repo is mounted."); } } } } } ================================================ FILE: GVFS/GVFS/CommandLine/UpgradeVerb.cs ================================================ using CommandLine; using System; namespace GVFS.CommandLine { [Verb(UpgradeVerbName, HelpText = "Checks for new GVFS release, downloads and installs it when available.")] public class UpgradeVerb : GVFSVerb.ForNoEnlistment { private const string UpgradeVerbName = "upgrade"; public UpgradeVerb() { this.Output = Console.Out; } [Option( "confirm", Default = false, Required = false, HelpText = "Pass in this flag to actually install the newest release")] public bool Confirmed { get; set; } [Option( "dry-run", Default = false, Required = false, HelpText = "Display progress and errors, but don't install GVFS")] public bool DryRun { get; set; } [Option( "no-verify", Default = false, Required = false, HelpText = "Do not verify NuGet packages after downloading them. Some platforms do not support NuGet verification.")] public bool NoVerify { get; set; } protected override string VerbName { get { return UpgradeVerbName; } } public override void Execute() { Console.Error.WriteLine("'gvfs upgrade' is no longer supported. Visit https://github.com/microsoft/vfsforgit for the latest install/upgrade instructions."); } } } ================================================ FILE: GVFS/GVFS/GVFS.csproj ================================================ Exe net471 false Content PreserveNewest Build;DebugSymbolsProjectOutputGroup PreserveNewest ================================================ FILE: GVFS/GVFS/InternalsVisibleTo.cs ================================================ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("GVFS.UnitTests")] ================================================ FILE: GVFS/GVFS/Program.cs ================================================ using CommandLine; using GVFS.CommandLine; using GVFS.Common; using GVFS.PlatformLoader; using System; using System.IO; using System.Linq; namespace GVFS { public class Program { public static void Main(string[] args) { GVFSPlatformLoader.Initialize(); if (!GVFSPlatform.Instance.KernelDriver.RegisterForOfflineIO()) { Console.WriteLine("Unable to register with the kernel for offline I/O. Ensure that VFS for Git installed successfully and try again"); Environment.Exit((int)ReturnCode.UnableToRegisterForOfflineIO); } Type[] verbTypes = new Type[] { typeof(CacheServerVerb), typeof(CacheVerb), typeof(CloneVerb), typeof(ConfigVerb), typeof(DehydrateVerb), typeof(DiagnoseVerb), typeof(LogVerb), typeof(SparseVerb), typeof(MountVerb), typeof(PrefetchVerb), typeof(RepairVerb), typeof(ServiceVerb), typeof(HealthVerb), typeof(StatusVerb), typeof(UnmountVerb), typeof(UpgradeVerb), }; int consoleWidth = 80; // Running in a headless environment can result in a Console with a // WindowWidth of 0, which causes issues with CommandLineParser try { if (Console.WindowWidth > 0) { consoleWidth = Console.WindowWidth; } } catch (IOException) { } try { new Parser( settings => { settings.CaseSensitive = false; settings.EnableDashDash = true; settings.IgnoreUnknownArguments = false; settings.HelpWriter = Console.Error; settings.MaximumDisplayWidth = consoleWidth; }) .ParseArguments(args, verbTypes) .WithNotParsed( errors => { if (errors.Any(error => error is TokenError)) { Environment.Exit((int)ReturnCode.ParsingError); } }) .WithParsed( clone => { // We handle the clone verb differently, because clone cares if the enlistment path // was not specified vs if it was specified to be the current directory clone.Execute(); Environment.Exit((int)ReturnCode.Success); }) .WithParsed( verb => { verb.Execute(); Environment.Exit((int)ReturnCode.Success); }) .WithParsed( verb => { // For all other verbs, they don't care if the enlistment root is explicitly // specified or implied to be the current directory if (string.IsNullOrEmpty(verb.EnlistmentRootPathParameter)) { verb.EnlistmentRootPathParameter = Environment.CurrentDirectory; } verb.Execute(); Environment.Exit((int)ReturnCode.Success); }); } catch (GVFSVerb.VerbAbortedException e) { // Calling Environment.Exit() is required, to force all background threads to exit as well Environment.Exit((int)e.Verb.ReturnCode); } finally { if (!GVFSPlatform.Instance.KernelDriver.UnregisterForOfflineIO()) { Console.WriteLine("Unable to unregister with the kernel for offline I/O."); } } } } } ================================================ FILE: GVFS/GVFS/RepairJobs/BackgroundOperationDatabaseRepairJob.cs ================================================ using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using GVFS.Virtualization.Background; using System.Collections.Generic; using System.IO; namespace GVFS.RepairJobs { public class BackgroundOperationDatabaseRepairJob : RepairJob { private readonly string dataPath; public BackgroundOperationDatabaseRepairJob(ITracer tracer, TextWriter output, GVFSEnlistment enlistment) : base(tracer, output, enlistment) { this.dataPath = Path.Combine(this.Enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.BackgroundFileSystemTasks); } public override string Name { get { return "Background Operation Database"; } } public override IssueType HasIssue(List messages) { string error; FileSystemTaskQueue instance; if (!FileSystemTaskQueue.TryCreate( this.Tracer, this.dataPath, new PhysicalFileSystem(), out instance, out error)) { messages.Add("Failed to read background operations: " + error); return IssueType.CantFix; } return IssueType.None; } public override FixResult TryFixIssues(List messages) { return FixResult.Failure; } } } ================================================ FILE: GVFS/GVFS/RepairJobs/BlobSizeDatabaseRepairJob.cs ================================================ using GVFS.Common; using GVFS.Common.Database; using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using GVFS.Virtualization.BlobSize; using System.Collections.Generic; using System.IO; namespace GVFS.RepairJobs { public class BlobSizeDatabaseRepairJob : RepairJob { private string blobSizeRoot; public BlobSizeDatabaseRepairJob(ITracer tracer, TextWriter output, GVFSEnlistment enlistment) : base(tracer, output, enlistment) { } public override string Name { get { return "Blob Size Database"; } } public override IssueType HasIssue(List messages) { string error; try { if (!RepoMetadata.TryInitialize(this.Tracer, this.Enlistment.DotGVFSRoot, out error)) { messages.Add("Could not open repo metadata: " + error); return IssueType.CantFix; } if (!RepoMetadata.Instance.TryGetBlobSizesRoot(out this.blobSizeRoot, out error)) { messages.Add("Could not find blob sizes root in repo metadata: " + error); return IssueType.CantFix; } } finally { RepoMetadata.Shutdown(); } string blobsizesDatabasePath = Path.Combine(this.blobSizeRoot, BlobSizes.DatabaseName); if (SqliteDatabase.HasIssue(blobsizesDatabasePath, new PhysicalFileSystem(), out error)) { messages.Add("Could not load blob size database: " + error); return IssueType.Fixable; } return IssueType.None; } public override FixResult TryFixIssues(List messages) { if (string.IsNullOrWhiteSpace(this.blobSizeRoot)) { return FixResult.Failure; } return this.TryDeleteFolder(this.blobSizeRoot) ? FixResult.Success : FixResult.Failure; } } } ================================================ FILE: GVFS/GVFS/RepairJobs/GitConfigRepairJob.cs ================================================ using GVFS.CommandLine; using GVFS.Common; using GVFS.Common.Git; using GVFS.Common.Tracing; using System.Collections.Generic; using System.IO; namespace GVFS.RepairJobs { public class GitConfigRepairJob : RepairJob { public GitConfigRepairJob(ITracer tracer, TextWriter output, GVFSEnlistment enlistment) : base(tracer, output, enlistment) { } public override string Name { get { return GVFSConstants.DotGit.Config; } } public override IssueType HasIssue(List messages) { GitProcess git = new GitProcess(this.Enlistment); GitProcess.ConfigResult originResult = git.GetOriginUrl(); string error; string originUrl; if (!originResult.TryParseAsString(out originUrl, out error)) { if (error.Contains("--local")) { // example error: '--local can only be used inside a git repository' // Corrupting the git config does not cause git to not recognize the current folder as "not a git repository". // This is a symptom of deeper issues such as missing HEAD file or refs folders. messages.Add("An issue was found that may be a side-effect of other issues. Fix them with 'gvfs repair --confirm' then 'gvfs repair' again."); return IssueType.CantFix; } messages.Add("Could not read origin url: " + error); return IssueType.Fixable; } if (originUrl == null) { messages.Add("Remote 'origin' is not configured for this repo. You can fix this by running 'git remote add origin '"); return IssueType.CantFix; } // We've validated the repo URL, so now make sure we can authenticate try { GVFSEnlistment enlistment = GVFSEnlistment.CreateFromDirectory( this.Enlistment.EnlistmentRoot, this.Enlistment.GitBinPath, authentication: null); string authError; if (!enlistment.Authentication.TryInitialize(this.Tracer, enlistment, out authError)) { messages.Add("Authentication failed. Run 'gvfs log' for more info."); messages.Add($"{GVFSConstants.DotGit.Config} is valid and remote 'origin' is set, but may have a typo:"); messages.Add(originUrl.Trim()); return IssueType.CantFix; } } catch (InvalidRepoException) { messages.Add("An issue was found that may be a side-effect of other issues. Fix them with 'gvfs repair --confirm' then 'gvfs repair' again."); return IssueType.CantFix; } return IssueType.None; } public override FixResult TryFixIssues(List messages) { string configPath = Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Config); string configBackupPath; if (!this.TryRenameToBackupFile(configPath, out configBackupPath, messages)) { return FixResult.Failure; } File.WriteAllText(configPath, string.Empty); this.Tracer.RelatedInfo("Created empty file: " + configPath); if (!GVFSVerb.TrySetRequiredGitConfigSettings(this.Enlistment) || !GVFSVerb.TrySetOptionalGitConfigSettings(this.Enlistment)) { messages.Add($"Unable to create default {GVFSConstants.DotGit.Config}."); this.RestoreFromBackupFile(configBackupPath, configPath, messages); return FixResult.Failure; } // Don't output the validation output unless it turns out we couldn't fix the problem List validationMessages = new List(); // HasIssue should return CantFix because we can't set the repo url ourselves, // but getting Fixable means that we still failed if (this.HasIssue(validationMessages) == IssueType.Fixable) { messages.Add($"Reinitializing the {GVFSConstants.DotGit.Config} did not fix the issue. Check the errors below for more details:"); messages.AddRange(validationMessages); this.RestoreFromBackupFile(configBackupPath, configPath, messages); return FixResult.Failure; } if (!this.TryDeleteFile(configBackupPath)) { messages.Add($"Failed to delete {GVFSConstants.DotGit.Config} backup file: " + configBackupPath); } messages.Add($"Reinitialized {GVFSConstants.DotGit.Config}. You will need to manually add the origin remote by running"); messages.Add("git remote add origin "); messages.Add("If you previously configured a custom cache server, you will need to configure it again."); return FixResult.ManualStepsRequired; } } } ================================================ FILE: GVFS/GVFS/RepairJobs/GitHeadRepairJob.cs ================================================ using GVFS.Common; using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace GVFS.RepairJobs { public class GitHeadRepairJob : RepairJob { public GitHeadRepairJob(ITracer tracer, TextWriter output, GVFSEnlistment enlistment) : base(tracer, output, enlistment) { } public override string Name { get { return GVFSConstants.DotGit.Head; } } public override IssueType HasIssue(List messages) { if (TryParseHead(this.Enlistment, messages)) { return IssueType.None; } if (!this.CanBeRepaired(messages)) { return IssueType.CantFix; } return IssueType.Fixable; } /// /// Fixes the HEAD using the reflog to find the last SHA. /// We detach HEAD as a side-effect of repair. /// public override FixResult TryFixIssues(List messages) { string error; RefLogEntry refLog; if (!TryReadLastRefLogEntry(this.Enlistment, GVFSConstants.DotGit.HeadName, out refLog, out error)) { this.Tracer.RelatedError(error); messages.Add(error); return FixResult.Failure; } try { string refPath = Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Head); File.WriteAllText(refPath, refLog.TargetSha); } catch (IOException ex) { EventMetadata metadata = new EventMetadata(); this.Tracer.RelatedError(metadata, "Failed to write HEAD: " + ex.ToString()); return FixResult.Failure; } this.Tracer.RelatedEvent( EventLevel.Informational, "MovedHead", new EventMetadata { { "DestinationCommit", refLog.TargetSha } }); messages.Add("As a result of the repair, 'git status' will now complain that HEAD is detached"); messages.Add("You can fix this by creating a branch using 'git checkout -b '"); return FixResult.Success; } /// /// 'git ref-log' doesn't work if the repo is corrupted, so parsing reflogs seems like the only solution. /// /// A full symbolic ref name. eg. HEAD, refs/remotes/origin/HEAD, refs/heads/master private static bool TryReadLastRefLogEntry(Enlistment enlistment, string fullSymbolicRef, out RefLogEntry refLog, out string error) { string refLogPath = Path.Combine(enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Logs.Root, fullSymbolicRef); if (!File.Exists(refLogPath)) { refLog = null; error = "Could not find reflog for ref '" + fullSymbolicRef + "'"; return false; } try { string refLogContents = File.ReadLines(refLogPath).Last(); if (!RefLogEntry.TryParse(refLogContents, out refLog)) { error = "Last ref log entry for " + fullSymbolicRef + " is unparsable."; return false; } } catch (IOException ex) { refLog = null; error = "IOException while reading reflog '" + refLogPath + "': " + ex.Message; return false; } error = null; return true; } private static bool TryParseHead(Enlistment enlistment, List messages) { string refPath = Path.Combine(enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Head); if (!File.Exists(refPath)) { messages.Add("Could not find ref file for '" + GVFSConstants.DotGit.Head + "'"); return false; } string refContents; try { refContents = File.ReadAllText(refPath).Trim(); } catch (IOException ex) { messages.Add($"IOException while reading {GVFSConstants.DotGit.Head}: " + ex.Message); return false; } const string MinimallyValidRef = "ref: refs/"; if (refContents.StartsWith(MinimallyValidRef, StringComparison.OrdinalIgnoreCase) || SHA1Util.IsValidShaFormat(refContents)) { return true; } messages.Add("Invalid contents found in '" + GVFSConstants.DotGit.Head + "': " + refContents); return false; } private bool CanBeRepaired(List messages) { Func createErrorMessage = operation => string.Format("Can't repair HEAD while a {0} operation is in progress", operation); string rebasePath = Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.RebaseApply); if (Directory.Exists(rebasePath)) { messages.Add(createErrorMessage("rebase")); return false; } string mergeHeadPath = Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.MergeHead); if (File.Exists(mergeHeadPath)) { messages.Add(createErrorMessage("merge")); return false; } string bisectStartPath = Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.BisectStart); if (File.Exists(bisectStartPath)) { messages.Add(createErrorMessage("bisect")); return false; } string cherrypickHeadPath = Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.CherryPickHead); if (File.Exists(cherrypickHeadPath)) { messages.Add(createErrorMessage("cherry-pick")); return false; } string revertHeadPath = Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.RevertHead); if (File.Exists(revertHeadPath)) { messages.Add(createErrorMessage("revert")); return false; } return true; } } } ================================================ FILE: GVFS/GVFS/RepairJobs/GitIndexRepairJob.cs ================================================ using GVFS.Common; using GVFS.Common.Git; using GVFS.Common.Tracing; using System.Collections.Generic; using System.IO; namespace GVFS.RepairJobs { public class GitIndexRepairJob : RepairJob { private readonly string indexPath; public GitIndexRepairJob(ITracer tracer, TextWriter output, GVFSEnlistment enlistment) : base(tracer, output, enlistment) { this.indexPath = Path.Combine(this.Enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName); } public override string Name { get { return GVFSConstants.DotGit.Index; } } public override IssueType HasIssue(List messages) { if (!File.Exists(this.indexPath)) { messages.Add($"{GVFSConstants.DotGit.Index} not found"); return IssueType.Fixable; } else { return this.TryParseIndex(this.indexPath, messages); } } public override FixResult TryFixIssues(List messages) { string indexBackupPath = null; if (File.Exists(this.indexPath)) { if (!this.TryRenameToBackupFile(this.indexPath, out indexBackupPath, messages)) { return FixResult.Failure; } } GitIndexGenerator indexGen = new GitIndexGenerator(this.Tracer, this.Enlistment, shouldHashIndex: false); indexGen.CreateFromRef(GVFSConstants.DotGit.HeadName, indexVersion: 4, isFinal: true); if (indexGen.HasFailures || this.TryParseIndex(this.indexPath, messages) != IssueType.None) { if (indexBackupPath != null) { this.RestoreFromBackupFile(indexBackupPath, this.indexPath, messages); } return FixResult.Failure; } if (indexBackupPath != null) { if (!this.TryDeleteFile(indexBackupPath)) { messages.Add($"Warning: Could not delete backed up {GVFSConstants.DotGit.Index} at: " + indexBackupPath); } } return FixResult.Success; } } } ================================================ FILE: GVFS/GVFS/RepairJobs/RepairJob.cs ================================================ using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using GVFS.Virtualization.Projection; using System; using System.Collections.Generic; using System.IO; namespace GVFS.RepairJobs { public abstract class RepairJob { private const string BackupExtension = ".bak"; private PhysicalFileSystem fileSystem; public RepairJob(ITracer tracer, TextWriter output, GVFSEnlistment enlistment) { this.Tracer = tracer; this.Output = output; this.Enlistment = enlistment; this.fileSystem = new PhysicalFileSystem(); } public enum IssueType { None, Fixable, CantFix } public enum FixResult { Success, Failure, ManualStepsRequired } public abstract string Name { get; } protected ITracer Tracer { get; } protected TextWriter Output { get; } protected GVFSEnlistment Enlistment { get; } public abstract IssueType HasIssue(List messages); public abstract FixResult TryFixIssues(List messages); protected bool TryRenameToBackupFile(string filePath, out string backupPath, List messages) { backupPath = filePath + BackupExtension; try { File.Move(filePath, backupPath); this.Tracer.RelatedEvent(EventLevel.Informational, "FileMoved", new EventMetadata { { "SourcePath", filePath }, { "DestinationPath", backupPath } }); } catch (Exception e) { messages.Add("Failed to back up " + filePath + " to " + backupPath); this.Tracer.RelatedError("Exception while moving " + filePath + " to " + backupPath + ": " + e.ToString()); return false; } return true; } protected void RestoreFromBackupFile(string backupPath, string originalPath, List messages) { try { File.Delete(originalPath); File.Move(backupPath, originalPath); this.Tracer.RelatedEvent(EventLevel.Informational, "FileMoved", new EventMetadata { { "SourcePath", backupPath }, { "DestinationPath", originalPath } }); } catch (Exception e) { messages.Add("Could not restore " + originalPath + " from " + backupPath); this.Tracer.RelatedError("Exception while restoring " + originalPath + " from " + backupPath + ": " + e.ToString()); } } protected bool TryDeleteFile(string filePath) { try { File.Delete(filePath); this.Tracer.RelatedEvent(EventLevel.Informational, "FileDeleted", new EventMetadata { { "SourcePath", filePath } }); } catch (Exception e) { this.Tracer.RelatedError("Exception while deleting file " + filePath + ": " + e.ToString()); return false; } return true; } protected bool TryDeleteFolder(string filePath) { try { this.fileSystem.DeleteDirectory(filePath); this.Tracer.RelatedEvent(EventLevel.Informational, "FolderDeleted", new EventMetadata { { "SourcePath", filePath } }); } catch (Exception e) { this.Tracer.RelatedError("Exception while deleting folder " + filePath + ": " + e.ToString()); return false; } return true; } protected IssueType TryParseIndex(string path, List messages) { GVFSContext context = new GVFSContext(this.Tracer, null, null, this.Enlistment); using (GitIndexProjection index = new GitIndexProjection( context, gitObjects: null, blobSizes: null, repoMetadata: null, fileSystemVirtualizer: null, placeholderDatabase: null, sparseCollection: null, modifiedPaths: null)) { try { index.BuildProjectionFromPath(this.Tracer, path); } catch (Exception ex) { messages.Add("Failed to parse index at " + path); this.Tracer.RelatedInfo(ex.ToString()); return IssueType.Fixable; } } return IssueType.None; } } } ================================================ FILE: GVFS/GVFS/RepairJobs/RepoMetadataDatabaseRepairJob.cs ================================================ using GVFS.Common; using GVFS.Common.Tracing; using System.Collections.Generic; using System.IO; namespace GVFS.RepairJobs { public class RepoMetadataDatabaseRepairJob : RepairJob { public RepoMetadataDatabaseRepairJob(ITracer tracer, TextWriter output, GVFSEnlistment enlistment) : base(tracer, output, enlistment) { } public override string Name { get { return "Repo Metadata Database"; } } public override IssueType HasIssue(List messages) { string error; try { if (!RepoMetadata.TryInitialize(this.Tracer, this.Enlistment.DotGVFSRoot, out error)) { messages.Add("Could not open repo metadata: " + error); return IssueType.CantFix; } } finally { RepoMetadata.Shutdown(); } return IssueType.None; } public override FixResult TryFixIssues(List messages) { return FixResult.Failure; } } } ================================================ FILE: GVFS/GVFS/RepairJobs/VFSForGitDatabaseRepairJob.cs ================================================ using GVFS.Common; using GVFS.Common.Database; using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using System.Collections.Generic; using System.IO; namespace GVFS.RepairJobs { public class VFSForGitDatabaseRepairJob : RepairJob { public VFSForGitDatabaseRepairJob(ITracer tracer, TextWriter output, GVFSEnlistment enlistment) : base(tracer, output, enlistment) { } public override string Name { get { return "Placeholder Database"; } } public override IssueType HasIssue(List messages) { string error; string databasePath = Path.Combine(this.Enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.VFSForGit); if (SqliteDatabase.HasIssue(databasePath, new PhysicalFileSystem(), out error)) { messages.Add($"Could not load {this.Name}: {error}"); return IssueType.CantFix; } return IssueType.None; } public override FixResult TryFixIssues(List messages) { return FixResult.Failure; } } } ================================================ FILE: GVFS/GVFS.Common/AzDevOpsOrgFromNuGetFeed.cs ================================================ using System.Text.RegularExpressions; namespace GVFS.Common { public class AzDevOpsOrgFromNuGetFeed { /// /// Given a URL for a NuGet feed hosted on Azure DevOps, /// return the organization that hosts the feed. /// public static bool TryParseOrg(string packageFeedUrl, out string orgName) { // We expect a URL of the form https://pkgs.dev.azure.com/{org} // and want to convert it to a URL of the form https://{org}.visualstudio.com Regex packageUrlRegex = new Regex( @"^https://pkgs.dev.azure.com/(?.+?)/", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); Match urlMatch = packageUrlRegex.Match(packageFeedUrl); if (!urlMatch.Success) { orgName = null; return false; } orgName = urlMatch.Groups["org"].Value; return true; } /// /// Given a URL for a NuGet feed hosted on Azure DevOps, /// return a URL that Git Credential Manager can use to /// query for a credential that is valid for use with the /// NuGet feed. /// public static bool TryCreateCredentialQueryUrl(string packageFeedUrl, out string azureDevOpsUrl, out string error) { if (!TryParseOrg(packageFeedUrl, out string org)) { azureDevOpsUrl = null; error = $"Input URL {packageFeedUrl} did not match expected format for an Azure DevOps Package Feed URL"; return false; } azureDevOpsUrl = $"https://{org}.visualstudio.com"; error = null; return true; } } } ================================================ FILE: GVFS/GVFS.Common/ConcurrentHashSet.cs ================================================ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; namespace GVFS.Common { public class ConcurrentHashSet : IEnumerable { private ConcurrentDictionary dictionary; public ConcurrentHashSet() { this.dictionary = new ConcurrentDictionary(); } public ConcurrentHashSet(IEqualityComparer comparer) { this.dictionary = new ConcurrentDictionary(comparer); } public int Count { get { return this.dictionary.Count; } } public bool Add(T entry) { return this.dictionary.TryAdd(entry, true); } public bool Contains(T item) { return this.dictionary.ContainsKey(item); } public void Clear() { this.dictionary.Clear(); } public IEnumerator GetEnumerator() { return this.dictionary.Keys.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } public bool TryRemove(T key) { bool value; return this.dictionary.TryRemove(key, out value); } } } ================================================ FILE: GVFS/GVFS.Common/ConsoleHelper.cs ================================================ using System; using System.IO; using System.Runtime.InteropServices; using System.Threading; namespace GVFS.Common { public static class ConsoleHelper { public enum ActionResult { Success, CompletedWithErrors, Failure, } public static bool ShowStatusWhileRunning( Func action, string message, TextWriter output, bool showSpinner, string gvfsLogEnlistmentRoot, int initialDelayMs = 0) { Func actionResultAction = () => { return action() ? ActionResult.Success : ActionResult.Failure; }; ActionResult result = ShowStatusWhileRunning( actionResultAction, message, output, showSpinner, gvfsLogEnlistmentRoot, initialDelayMs: initialDelayMs); return result == ActionResult.Success; } public static ActionResult ShowStatusWhileRunning( Func action, string message, TextWriter output, bool showSpinner, string gvfsLogEnlistmentRoot, int initialDelayMs) { ActionResult result = ActionResult.Failure; bool initialMessageWritten = false; try { if (!showSpinner) { output.Write(message + "..."); initialMessageWritten = true; result = action(); } else { ManualResetEvent actionIsDone = new ManualResetEvent(false); bool isComplete = false; Thread spinnerThread = new Thread( () => { int retries = 0; char[] waiting = { '\u2014', '\\', '|', '/' }; while (!isComplete) { if (retries == 0) { actionIsDone.WaitOne(initialDelayMs); } else { output.Write("\r{0}...{1}", message, waiting[(retries / 2) % waiting.Length]); initialMessageWritten = true; actionIsDone.WaitOne(100); } retries++; } if (initialMessageWritten) { // Clear out any trailing waiting character output.Write("\r{0}...", message); } }); spinnerThread.Start(); try { result = action(); } finally { isComplete = true; actionIsDone.Set(); spinnerThread.Join(); } } } finally { switch (result) { case ActionResult.Success: if (initialMessageWritten) { output.WriteLine("Succeeded"); } break; case ActionResult.CompletedWithErrors: if (!initialMessageWritten) { output.Write("\r{0}...", message); } output.WriteLine("Completed with errors."); break; case ActionResult.Failure: if (!initialMessageWritten) { output.Write("\r{0}...", message); } output.WriteLine("Failed" + (gvfsLogEnlistmentRoot == null ? string.Empty : ". " + GetGVFSLogMessage(gvfsLogEnlistmentRoot))); break; } } return result; } public static string GetGVFSLogMessage(string enlistmentRoot) { return "Run 'gvfs log " + enlistmentRoot + "' for more info."; } } } ================================================ FILE: GVFS/GVFS.Common/Database/GVFSDatabase.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using System; using System.Collections.Concurrent; using System.Data; using System.IO; namespace GVFS.Common.Database { /// /// Handles setting up the database for storing data used by GVFS and /// managing the connections to the database /// public class GVFSDatabase : IGVFSConnectionPool, IDisposable { private const int InitialPooledConnections = 5; private const int MillisecondsWaitingToGetConnection = 50; private bool disposed = false; private string databasePath; private IDbConnectionFactory connectionFactory; private BlockingCollection connectionPool; public GVFSDatabase(PhysicalFileSystem fileSystem, string enlistmentRoot, IDbConnectionFactory connectionFactory, int initialPooledConnections = InitialPooledConnections) { this.connectionPool = new BlockingCollection(); this.databasePath = Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.VFSForGit); this.connectionFactory = connectionFactory; string folderPath = Path.GetDirectoryName(this.databasePath); fileSystem.CreateDirectory(folderPath); try { for (int i = 0; i < initialPooledConnections; i++) { this.connectionPool.Add(this.connectionFactory.OpenNewConnection(this.databasePath)); } this.Initialize(); } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(GVFSDatabase)} constructor threw exception setting up connection pool and initializing", ex); } } public static string NormalizePath(string path) { return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar).Trim().Trim(Path.DirectorySeparatorChar); } public void Dispose() { if (this.disposed) { return; } this.disposed = true; this.connectionPool.CompleteAdding(); while (this.connectionPool.TryTake(out IDbConnection connection)) { connection.Dispose(); } this.connectionPool.Dispose(); this.connectionPool = null; } IDbConnection IGVFSConnectionPool.GetConnection() { if (this.disposed) { throw new ObjectDisposedException(nameof(GVFSDatabase)); } IDbConnection connection; if (!this.connectionPool.TryTake(out connection, millisecondsTimeout: MillisecondsWaitingToGetConnection)) { connection = this.connectionFactory.OpenNewConnection(this.databasePath); } return new GVFSConnection(this, connection); } private void ReturnToPool(IDbConnection connection) { if (this.disposed) { connection.Dispose(); return; } try { this.connectionPool.TryAdd(connection); } catch (Exception ex) when (ex is InvalidOperationException || ex is ObjectDisposedException) { connection.Dispose(); } } private void Initialize() { IGVFSConnectionPool connectionPool = this; using (IDbConnection connection = connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { command.CommandText = "PRAGMA journal_mode=WAL;"; command.ExecuteNonQuery(); command.CommandText = "PRAGMA cache_size=-40000;"; command.ExecuteNonQuery(); command.CommandText = "PRAGMA synchronous=NORMAL;"; command.ExecuteNonQuery(); command.CommandText = "PRAGMA user_version;"; object userVersion = command.ExecuteScalar(); if (userVersion == null || Convert.ToInt64(userVersion) < 1) { command.CommandText = "PRAGMA user_version=1;"; command.ExecuteNonQuery(); } PlaceholderTable.CreateTable(connection, GVFSPlatform.Instance.Constants.CaseSensitiveFileSystem); SparseTable.CreateTable(connection, GVFSPlatform.Instance.Constants.CaseSensitiveFileSystem); } } /// /// This class is used to wrap a IDbConnection and return it to the connection pool when disposed /// private class GVFSConnection : IDbConnection { private IDbConnection connection; private GVFSDatabase database; public GVFSConnection(GVFSDatabase database, IDbConnection connection) { this.database = database; this.connection = connection; } public string ConnectionString { get => this.connection.ConnectionString; set => this.connection.ConnectionString = value; } public int ConnectionTimeout => this.connection.ConnectionTimeout; public string Database => this.connection.Database; public ConnectionState State => this.connection.State; public IDbTransaction BeginTransaction() { return this.connection.BeginTransaction(); } public IDbTransaction BeginTransaction(IsolationLevel il) { return this.connection.BeginTransaction(il); } public void ChangeDatabase(string databaseName) { this.connection.ChangeDatabase(databaseName); } public void Close() { this.connection.Close(); } public IDbCommand CreateCommand() { return this.connection.CreateCommand(); } public void Dispose() { this.database.ReturnToPool(this.connection); } public void Open() { this.connection.Open(); } } } } ================================================ FILE: GVFS/GVFS.Common/Database/GVFSDatabaseException.cs ================================================ using System; namespace GVFS.Common.Database { public class GVFSDatabaseException : Exception { public GVFSDatabaseException(string message, Exception innerException) : base(message, innerException) { } } } ================================================ FILE: GVFS/GVFS.Common/Database/IDbCommandExtensions.cs ================================================ using System.Data; namespace GVFS.Common.Database { /// /// Extension methods for the IDbCommand interface /// public static class IDbCommandExtensions { public static IDbDataParameter AddParameter(this IDbCommand command, string name, DbType dbType, object value) { IDbDataParameter parameter = command.CreateParameter(); parameter.ParameterName = name; parameter.DbType = dbType; parameter.Value = value; command.Parameters.Add(parameter); return parameter; } } } ================================================ FILE: GVFS/GVFS.Common/Database/IDbConnectionFactory.cs ================================================ using System.Data; namespace GVFS.Common.Database { /// /// Interface used to open a new connection to a database /// public interface IDbConnectionFactory { IDbConnection OpenNewConnection(string databasePath); } } ================================================ FILE: GVFS/GVFS.Common/Database/IGVFSConnectionPool.cs ================================================ using System.Data; namespace GVFS.Common.Database { /// /// Interface for getting a pooled database connection /// public interface IGVFSConnectionPool { IDbConnection GetConnection(); } } ================================================ FILE: GVFS/GVFS.Common/Database/IPlaceholderCollection.cs ================================================ using System.Collections.Generic; namespace GVFS.Common.Database { /// /// Interface for interacting with placeholders /// public interface IPlaceholderCollection { int GetCount(); void GetAllEntries(out List filePlaceholders, out List folderPlaceholders); int GetFilePlaceholdersCount(); int GetFolderPlaceholdersCount(); HashSet GetAllFilePaths(); void AddPartialFolder(string path, string sha); void AddExpandedFolder(string path); void AddPossibleTombstoneFolder(string path); void AddFile(string path, string sha); void Remove(string path); List RemoveAllEntriesForFolder(string path); void AddPlaceholderData(IPlaceholderData data); } } ================================================ FILE: GVFS/GVFS.Common/Database/IPlaceholderData.cs ================================================ namespace GVFS.Common.Database { /// /// Interface for holding placeholder information /// public interface IPlaceholderData { string Path { get; } string Sha { get; set; } bool IsFolder { get; } bool IsExpandedFolder { get; } bool IsPossibleTombstoneFolder { get; } } } ================================================ FILE: GVFS/GVFS.Common/Database/ISparseCollection.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace GVFS.Common.Database { public interface ISparseCollection { HashSet GetAll(); void Add(string directory); void Remove(string directory); } } ================================================ FILE: GVFS/GVFS.Common/Database/PlaceholderTable.cs ================================================ using System; using System.Collections.Generic; using System.Data; using System.IO; namespace GVFS.Common.Database { /// /// This class is for interacting with the Placeholder table in the SQLite database /// public class PlaceholderTable : IPlaceholderCollection { private IGVFSConnectionPool connectionPool; private object writerLock = new object(); public PlaceholderTable(IGVFSConnectionPool connectionPool) { this.connectionPool = connectionPool; } public static void CreateTable(IDbConnection connection, bool caseSensitiveFileSystem) { using (IDbCommand command = connection.CreateCommand()) { string collateConstraint = caseSensitiveFileSystem ? string.Empty : " COLLATE NOCASE"; command.CommandText = $"CREATE TABLE IF NOT EXISTS [Placeholder] (path TEXT PRIMARY KEY{collateConstraint}, pathType TINYINT NOT NULL, sha char(40) ) WITHOUT ROWID;"; command.ExecuteNonQuery(); } } public int GetCount() { try { using (IDbConnection connection = this.connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { command.CommandText = "SELECT count(path) FROM Placeholder;"; return Convert.ToInt32(command.ExecuteScalar()); } } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(PlaceholderTable)}.{nameof(this.GetCount)} Exception", ex); } } public void GetAllEntries(out List filePlaceholders, out List folderPlaceholders) { try { List tempFilePlaceholders = new List(); List tempFolderPlaceholders = new List(); using (IDbConnection connection = this.connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { command.CommandText = "SELECT path, pathType, sha FROM Placeholder;"; ReadPlaceholders(command, data => { if (data.PathType == PlaceholderData.PlaceholderType.File) { tempFilePlaceholders.Add(data); } else { tempFolderPlaceholders.Add(data); } }); } filePlaceholders = tempFilePlaceholders; folderPlaceholders = tempFolderPlaceholders; } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(PlaceholderTable)}.{nameof(this.GetAllEntries)} Exception", ex); } } public HashSet GetAllFilePaths() { try { using (IDbConnection connection = this.connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { HashSet fileEntries = new HashSet(); command.CommandText = $"SELECT path FROM Placeholder WHERE pathType = {(int)PlaceholderData.PlaceholderType.File};"; using (IDataReader reader = command.ExecuteReader()) { while (reader.Read()) { fileEntries.Add(reader.GetString(0)); } } return fileEntries; } } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(PlaceholderTable)}.{nameof(this.GetAllFilePaths)} Exception", ex); } } public void AddPlaceholderData(IPlaceholderData data) { if (data.IsFolder) { if (data.IsExpandedFolder) { this.AddExpandedFolder(data.Path); } else if (data.IsPossibleTombstoneFolder) { this.AddPossibleTombstoneFolder(data.Path); } else { this.AddPartialFolder(data.Path, data.Sha); } } else { this.AddFile(data.Path, data.Sha); } } public void AddFile(string path, string sha) { if (sha == null || sha.Length != 40) { throw new GVFSDatabaseException($"Invalid SHA '{sha ?? "null"}' for file {path}", innerException: null); } this.Insert(new PlaceholderData() { Path = path, PathType = PlaceholderData.PlaceholderType.File, Sha = sha }); } public void AddPartialFolder(string path, string sha) { this.Insert(new PlaceholderData() { Path = path, PathType = PlaceholderData.PlaceholderType.PartialFolder, Sha = sha }); } public void AddExpandedFolder(string path) { this.Insert(new PlaceholderData() { Path = path, PathType = PlaceholderData.PlaceholderType.ExpandedFolder }); } public void AddPossibleTombstoneFolder(string path) { this.Insert(new PlaceholderData() { Path = path, PathType = PlaceholderData.PlaceholderType.PossibleTombstoneFolder }); } public List RemoveAllEntriesForFolder(string path) { const string fromWhereClause = "FROM Placeholder WHERE path = @path OR path LIKE @pathWithDirectorySeparator;"; // Normalize the path to match what will be in the database path = GVFSDatabase.NormalizePath(path); try { using (IDbConnection connection = this.connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { List removedPlaceholders = new List(); command.CommandText = $"SELECT path, pathType, sha {fromWhereClause}"; command.AddParameter("@path", DbType.String, $"{path}"); command.AddParameter("@pathWithDirectorySeparator", DbType.String, $"{path + Path.DirectorySeparatorChar}%"); ReadPlaceholders(command, data => removedPlaceholders.Add(data)); command.CommandText = $"DELETE {fromWhereClause}"; lock (this.writerLock) { command.ExecuteNonQuery(); } return removedPlaceholders; } } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(PlaceholderTable)}.{nameof(this.RemoveAllEntriesForFolder)}({path}) Exception", ex); } } public void Remove(string path) { try { using (IDbConnection connection = this.connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { command.CommandText = "DELETE FROM Placeholder WHERE path = @path;"; command.AddParameter("@path", DbType.String, path); lock (this.writerLock) { command.ExecuteNonQuery(); } } } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(PlaceholderTable)}.{nameof(this.Remove)}({path}) Exception", ex); } } public int GetFilePlaceholdersCount() { try { using (IDbConnection connection = this.connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { command.CommandText = $"SELECT count(path) FROM Placeholder WHERE pathType = {(int)PlaceholderData.PlaceholderType.File};"; return Convert.ToInt32(command.ExecuteScalar()); } } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(PlaceholderTable)}.{nameof(this.GetCount)} Exception", ex); } } public int GetFolderPlaceholdersCount() { try { using (IDbConnection connection = this.connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { command.CommandText = $"SELECT count(path) FROM Placeholder WHERE pathType = {(int)PlaceholderData.PlaceholderType.PartialFolder};"; return Convert.ToInt32(command.ExecuteScalar()); } } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(PlaceholderTable)}.{nameof(this.GetCount)} Exception", ex); } } private static void ReadPlaceholders(IDbCommand command, Action dataHandler) { using (IDataReader reader = command.ExecuteReader()) { while (reader.Read()) { PlaceholderData data = new PlaceholderData(); data.Path = reader.GetString(0); data.PathType = (PlaceholderData.PlaceholderType)reader.GetByte(1); if (!reader.IsDBNull(2)) { data.Sha = reader.GetString(2); } dataHandler(data); } } } private void Insert(PlaceholderData placeholder) { try { using (IDbConnection connection = this.connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { command.CommandText = "INSERT OR REPLACE INTO Placeholder (path, pathType, sha) VALUES (@path, @pathType, @sha);"; command.AddParameter("@path", DbType.String, placeholder.Path); command.AddParameter("@pathType", DbType.Int32, (int)placeholder.PathType); if (placeholder.Sha == null) { command.AddParameter("@sha", DbType.String, DBNull.Value); } else { command.AddParameter("@sha", DbType.String, placeholder.Sha); } lock (this.writerLock) { command.ExecuteNonQuery(); } } } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(PlaceholderTable)}.{nameof(this.Insert)}({placeholder.Path}, {placeholder.PathType}, {placeholder.Sha}) Exception", ex); } } public class PlaceholderData : IPlaceholderData { public enum PlaceholderType { File = 0, PartialFolder = 1, ExpandedFolder = 2, PossibleTombstoneFolder = 3, } public string Path { get; set; } public PlaceholderType PathType { get; set; } public string Sha { get; set; } public bool IsFolder => this.PathType != PlaceholderType.File; public bool IsExpandedFolder => this.PathType == PlaceholderType.ExpandedFolder; public bool IsPossibleTombstoneFolder => this.PathType == PlaceholderType.PossibleTombstoneFolder; } } } ================================================ FILE: GVFS/GVFS.Common/Database/SparseTable.cs ================================================ using System; using System.Collections.Generic; using System.Data; using System.IO; namespace GVFS.Common.Database { public class SparseTable : ISparseCollection { private IGVFSConnectionPool connectionPool; private object writerLock = new object(); public SparseTable(IGVFSConnectionPool connectionPool) { this.connectionPool = connectionPool; } public static void CreateTable(IDbConnection connection, bool caseSensitiveFileSystem) { using (IDbCommand command = connection.CreateCommand()) { string collateConstraint = caseSensitiveFileSystem ? string.Empty : " COLLATE NOCASE"; command.CommandText = $"CREATE TABLE IF NOT EXISTS [Sparse] (path TEXT PRIMARY KEY{collateConstraint}) WITHOUT ROWID;"; command.ExecuteNonQuery(); } } public void Add(string directoryPath) { try { using (IDbConnection connection = this.connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { command.CommandText = "INSERT OR REPLACE INTO Sparse (path) VALUES (@path);"; command.AddParameter("@path", DbType.String, GVFSDatabase.NormalizePath(directoryPath)); lock (this.writerLock) { command.ExecuteNonQuery(); } } } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(SparseTable)}.{nameof(this.Add)}({directoryPath}) Exception: {ex.ToString()}", ex); } } public HashSet GetAll() { try { using (IDbConnection connection = this.connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { HashSet directories = new HashSet(GVFSPlatform.Instance.Constants.PathComparer); command.CommandText = $"SELECT path FROM Sparse;"; using (IDataReader reader = command.ExecuteReader()) { while (reader.Read()) { directories.Add(reader.GetString(0)); } } return directories; } } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(SparseTable)}.{nameof(this.GetAll)} Exception: {ex.ToString()}", ex); } } public void Remove(string directoryPath) { try { using (IDbConnection connection = this.connectionPool.GetConnection()) using (IDbCommand command = connection.CreateCommand()) { command.CommandText = "DELETE FROM Sparse WHERE path = @path;"; command.AddParameter("@path", DbType.String, GVFSDatabase.NormalizePath(directoryPath)); lock (this.writerLock) { command.ExecuteNonQuery(); } } } catch (Exception ex) { throw new GVFSDatabaseException($"{nameof(SparseTable)}.{nameof(this.Remove)}({directoryPath}) Exception: {ex.ToString()}", ex); } } } } ================================================ FILE: GVFS/GVFS.Common/Database/SqliteDatabase.cs ================================================ using GVFS.Common.FileSystem; using Microsoft.Data.Sqlite; using System; using System.Collections.Generic; using System.Data; namespace GVFS.Common.Database { /// /// Handles creating connections to SQLite database and checking for issues with the database /// public class SqliteDatabase : IDbConnectionFactory { public static bool HasIssue(string databasePath, PhysicalFileSystem filesystem, out string issue) { issue = null; if (filesystem.FileExists(databasePath)) { List integrityCheckResults = new List(); try { string sqliteConnectionString = CreateConnectionString(databasePath); using (SqliteConnection integrityConnection = new SqliteConnection(sqliteConnectionString)) { integrityConnection.Open(); using (SqliteCommand pragmaCommand = integrityConnection.CreateCommand()) { pragmaCommand.CommandText = "PRAGMA integrity_check;"; using (SqliteDataReader reader = pragmaCommand.ExecuteReader()) { while (reader.Read()) { integrityCheckResults.Add(reader.GetString(0)); } } } } } catch (Exception e) { issue = $"Exception while trying to access {databasePath}: {e.Message}"; return true; } // If pragma integrity_check finds no errors, a single row with the value 'ok' is returned // http://www.sqlite.org/pragma.html#pragma_integrity_check if (integrityCheckResults.Count != 1 || integrityCheckResults[0] != "ok") { issue = string.Join(",", integrityCheckResults); return true; } } return false; } public static string CreateConnectionString(string databasePath) { // Share-Cache mode allows multiple connections from the same process to share the same data cache // http://www.sqlite.org/sharedcache.html return $"data source={databasePath};Cache=Shared"; } public IDbConnection OpenNewConnection(string databasePath) { SqliteConnection connection = new SqliteConnection(CreateConnectionString(databasePath)); connection.Open(); return connection; } } } ================================================ FILE: GVFS/GVFS.Common/DiskLayoutUpgrades/DiskLayoutUpgrade.cs ================================================ using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; namespace GVFS.DiskLayoutUpgrades { public abstract class DiskLayoutUpgrade { private static Dictionary majorVersionUpgrades; private static Dictionary> minorVersionUpgrades; protected abstract int SourceMajorVersion { get; } protected abstract int SourceMinorVersion { get; } protected abstract bool IsMajorUpgrade { get; } public static bool TryRunAllUpgrades(string enlistmentRoot) { majorVersionUpgrades = new Dictionary(); minorVersionUpgrades = new Dictionary>(); foreach (DiskLayoutUpgrade upgrade in GVFSPlatform.Instance.DiskLayoutUpgrade.Upgrades) { RegisterUpgrade(upgrade); } using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "DiskLayoutUpgrade")) { try { DiskLayoutUpgrade upgrade = null; while (TryFindUpgrade(tracer, enlistmentRoot, out upgrade)) { if (upgrade == null) { return true; } if (!upgrade.TryUpgrade(tracer, enlistmentRoot)) { return false; } if (!CheckLayoutVersionWasIncremented(tracer, enlistmentRoot, upgrade)) { return false; } } return false; } catch (Exception e) { StartLogFile(enlistmentRoot, tracer); tracer.RelatedError(e.ToString()); return false; } finally { RepoMetadata.Shutdown(); } } } public static bool TryCheckDiskLayoutVersion(ITracer tracer, string enlistmentRoot, out string error) { error = string.Empty; int majorVersion; int minorVersion; try { if (TryGetDiskLayoutVersion(tracer, enlistmentRoot, out majorVersion, out minorVersion, out error)) { if (majorVersion < GVFSPlatform.Instance.DiskLayoutUpgrade.Version.MinimumSupportedMajorVersion) { error = string.Format( "Breaking change to GVFS disk layout has been made since cloning. \r\nEnlistment disk layout version: {0} \r\nGVFS disk layout version: {1} \r\nMinimum supported version: {2}", majorVersion, GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion, GVFSPlatform.Instance.DiskLayoutUpgrade.Version.MinimumSupportedMajorVersion); return false; } else if (majorVersion > GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion) { error = string.Format( "Changes to GVFS disk layout do not allow mounting after downgrade. Try mounting again using a more recent version of GVFS. \r\nEnlistment disk layout version: {0} \r\nGVFS disk layout version: {1}", majorVersion, GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion); return false; } else if (majorVersion != GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion) { error = string.Format( "GVFS disk layout version doesn't match current version. Try running 'gvfs mount' to upgrade. \r\nEnlistment disk layout version: {0}.{1} \r\nGVFS disk layout version: {2}.{3}", majorVersion, minorVersion, GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion, GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion); return false; } return true; } } finally { RepoMetadata.Shutdown(); } error = "Failed to read disk layout version. " + ConsoleHelper.GetGVFSLogMessage(enlistmentRoot); return false; } public abstract bool TryUpgrade(ITracer tracer, string enlistmentRoot); protected bool TryDeleteFolder(ITracer tracer, string folderName) { try { PhysicalFileSystem fileSystem = new PhysicalFileSystem(); fileSystem.DeleteDirectory(folderName); } catch (Exception e) { tracer.RelatedError("Failed to delete folder {0}: {1}", folderName, e.ToString()); return true; } return true; } protected bool TryDeleteFile(ITracer tracer, string fileName) { try { File.Delete(fileName); } catch (Exception e) { tracer.RelatedError("Failed to delete file {0}: {1}", fileName, e.ToString()); return true; } return true; } protected bool TryRenameFolderForDelete(ITracer tracer, string folderName, out string backupFolder) { backupFolder = folderName + ".deleteme"; tracer.RelatedInfo("Moving " + folderName + " to " + backupFolder); try { Directory.Move(folderName, backupFolder); } catch (Exception e) { tracer.RelatedError("Failed to move {0} to {1}: {2}", folderName, backupFolder, e.ToString()); return false; } return true; } protected bool TrySetGitConfig(ITracer tracer, string enlistmentRoot, Dictionary configSettings) { GVFSEnlistment enlistment; try { enlistment = GVFSEnlistment.CreateFromDirectory( enlistmentRoot, GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath(), authentication: null); } catch (InvalidRepoException e) { EventMetadata metadata = new EventMetadata(); metadata.Add("Exception", e.ToString()); metadata.Add(nameof(enlistmentRoot), enlistmentRoot); tracer.RelatedError(metadata, $"{nameof(this.TrySetGitConfig)}: Failed to create GVFSEnlistment from directory"); return false; } GitProcess git = enlistment.CreateGitProcess(); foreach (string key in configSettings.Keys) { GitProcess.Result result = git.SetInLocalConfig(key, configSettings[key]); if (result.ExitCodeIsFailure) { tracer.RelatedError("Could not set git config setting {0}. Error: {1}", key, result.Errors); return false; } } return true; } private static void RegisterUpgrade(DiskLayoutUpgrade upgrade) { if (upgrade.IsMajorUpgrade) { majorVersionUpgrades.Add(upgrade.SourceMajorVersion, (MajorUpgrade)upgrade); } else { if (minorVersionUpgrades.ContainsKey(upgrade.SourceMajorVersion)) { minorVersionUpgrades[upgrade.SourceMajorVersion].Add(upgrade.SourceMinorVersion, (MinorUpgrade)upgrade); } else { minorVersionUpgrades.Add(upgrade.SourceMajorVersion, new Dictionary { { upgrade.SourceMinorVersion, (MinorUpgrade)upgrade } }); } } } private static bool CheckLayoutVersionWasIncremented(JsonTracer tracer, string enlistmentRoot, DiskLayoutUpgrade upgrade) { string error; int actualMajorVersion; int actualMinorVersion; if (!TryGetDiskLayoutVersion(tracer, enlistmentRoot, out actualMajorVersion, out actualMinorVersion, out error)) { tracer.RelatedError(error); return false; } int expectedMajorVersion = upgrade.IsMajorUpgrade ? upgrade.SourceMajorVersion + 1 : upgrade.SourceMajorVersion; int expectedMinorVersion = upgrade.IsMajorUpgrade ? 0 : upgrade.SourceMinorVersion + 1; if (actualMajorVersion != expectedMajorVersion || actualMinorVersion != expectedMinorVersion) { throw new InvalidDataException(string.Format( "Disk layout upgrade did not increment layout version. Expected: {0}.{1}, Actual: {2}.{3}", expectedMajorVersion, expectedMinorVersion, actualMajorVersion, actualMinorVersion)); } return true; } private static bool TryFindUpgrade(JsonTracer tracer, string enlistmentRoot, out DiskLayoutUpgrade upgrade) { int majorVersion; int minorVersion; string error; if (!TryGetDiskLayoutVersion(tracer, enlistmentRoot, out majorVersion, out minorVersion, out error)) { StartLogFile(enlistmentRoot, tracer); tracer.RelatedError(error); upgrade = null; return false; } Dictionary minorVersionUpgradesForCurrentMajorVersion; if (minorVersionUpgrades.TryGetValue(majorVersion, out minorVersionUpgradesForCurrentMajorVersion)) { MinorUpgrade minorUpgrade; if (minorVersionUpgradesForCurrentMajorVersion.TryGetValue(minorVersion, out minorUpgrade)) { StartLogFile(enlistmentRoot, tracer); tracer.RelatedInfo( "Upgrading from disk layout {0}.{1} to {0}.{2}", majorVersion, minorVersion, minorVersion + 1); upgrade = minorUpgrade; return true; } } MajorUpgrade majorUpgrade; if (majorVersionUpgrades.TryGetValue(majorVersion, out majorUpgrade)) { StartLogFile(enlistmentRoot, tracer); tracer.RelatedInfo("Upgrading from disk layout {0} to {1}", majorVersion, majorVersion + 1); upgrade = majorUpgrade; return true; } // return true to indicate that we succeeded, and no upgrader was found upgrade = null; return true; } private static bool TryGetDiskLayoutVersion( ITracer tracer, string enlistmentRoot, out int majorVersion, out int minorVersion, out string error) { majorVersion = 0; minorVersion = 0; string dotGVFSPath = Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); if (!GVFSPlatform.Instance.DiskLayoutUpgrade.TryParseLegacyDiskLayoutVersion(dotGVFSPath, out majorVersion)) { if (!RepoMetadata.TryInitialize(tracer, dotGVFSPath, out error)) { majorVersion = 0; return false; } if (!RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error)) { return false; } } error = null; return true; } private static void StartLogFile(string enlistmentRoot, JsonTracer tracer) { if (!tracer.HasLogFileEventListener) { tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName( Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot, GVFSConstants.DotGVFS.LogName), GVFSConstants.LogFileTypes.MountUpgrade), EventLevel.Informational, Keywords.Any); tracer.WriteStartEvent(enlistmentRoot, repoUrl: "N/A", cacheServerUrl: "N/A"); } } public abstract class MajorUpgrade : DiskLayoutUpgrade { protected sealed override bool IsMajorUpgrade { get { return true; } } protected sealed override int SourceMinorVersion { get { throw new NotSupportedException(); } } protected bool TryIncrementMajorVersion(ITracer tracer, string enlistmentRoot) { string newMajorVersion = (this.SourceMajorVersion + 1).ToString(); string dotGVFSPath = Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); string error; if (!RepoMetadata.TryInitialize(tracer, dotGVFSPath, out error)) { tracer.RelatedError("Could not initialize repo metadata: " + error); return false; } RepoMetadata.Instance.SetEntry(RepoMetadata.Keys.DiskLayoutMajorVersion, newMajorVersion); RepoMetadata.Instance.SetEntry(RepoMetadata.Keys.DiskLayoutMinorVersion, "0"); tracer.RelatedInfo("Disk layout version is now: " + newMajorVersion); return true; } } public abstract class MinorUpgrade : DiskLayoutUpgrade { protected sealed override bool IsMajorUpgrade { get { return false; } } protected bool TryIncrementMinorVersion(ITracer tracer, string enlistmentRoot) { string newMinorVersion = (this.SourceMinorVersion + 1).ToString(); string dotGVFSPath = Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); string error; if (!RepoMetadata.TryInitialize(tracer, dotGVFSPath, out error)) { tracer.RelatedError("Could not initialize repo metadata: " + error); return false; } RepoMetadata.Instance.SetEntry(RepoMetadata.Keys.DiskLayoutMinorVersion, newMinorVersion); tracer.RelatedInfo("Disk layout version is now: {0}.{1}", this.SourceMajorVersion, newMinorVersion); return true; } } } } ================================================ FILE: GVFS/GVFS.Common/DiskLayoutUpgrades/DiskLayoutUpgrade_SqlitePlaceholders.cs ================================================ using GVFS.Common.Database; using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using GVFS.DiskLayoutUpgrades; using System; using System.Collections.Generic; using System.IO; namespace GVFS.Common.DiskLayoutUpgrades { public abstract class DiskLayoutUpgrade_SqlitePlaceholders : DiskLayoutUpgrade.MajorUpgrade { public override bool TryUpgrade(ITracer tracer, string enlistmentRoot) { string dotGVFSRoot = Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); try { PhysicalFileSystem fileSystem = new PhysicalFileSystem(); string error; LegacyPlaceholderListDatabase placeholderList; if (!LegacyPlaceholderListDatabase.TryCreate( tracer, Path.Combine(dotGVFSRoot, GVFSConstants.DotGVFS.Databases.PlaceholderList), fileSystem, out placeholderList, out error)) { tracer.RelatedError("Failed to open placeholder list database: " + error); return false; } using (placeholderList) using (GVFSDatabase database = new GVFSDatabase(fileSystem, enlistmentRoot, new SqliteDatabase())) { PlaceholderTable placeholders = new PlaceholderTable(database); List oldPlaceholderEntries = placeholderList.GetAllEntries(); foreach (IPlaceholderData entry in oldPlaceholderEntries) { placeholders.AddPlaceholderData(entry); } } } catch (Exception ex) { tracer.RelatedError("Error updating placeholder list database to SQLite: " + ex.ToString()); return false; } if (!this.TryIncrementMajorVersion(tracer, enlistmentRoot)) { return false; } return true; } } } ================================================ FILE: GVFS/GVFS.Common/DiskLayoutUpgrades/DiskLayoutVersion.cs ================================================ namespace GVFS.Common { public class DiskLayoutVersion { public DiskLayoutVersion(int currentMajorVersion, int currentMinorVersion, int minimumSupportedMajorVersion) { this.CurrentMajorVersion = currentMajorVersion; this.CurrentMinorVersion = currentMinorVersion; this.MinimumSupportedMajorVersion = minimumSupportedMajorVersion; } // 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 int CurrentMajorVersion { get; } // 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. public int CurrentMinorVersion { get; } // This is the last time GVFS made a breaking change that required a reclone. This should not // be incremented on platforms that have released a v1.0 as all their format changes should be // supported with an upgrade step. public int MinimumSupportedMajorVersion { get; } } } ================================================ FILE: GVFS/GVFS.Common/Enlistment.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Git; using System; using System.IO; namespace GVFS.Common { public abstract class Enlistment { protected Enlistment( string enlistmentRoot, string workingDirectoryRoot, string workingDirectoryBackingRoot, string repoUrl, string gitBinPath, bool flushFileBuffersForPacks, GitAuthentication authentication) { if (string.IsNullOrWhiteSpace(gitBinPath)) { throw new ArgumentException("Path to git.exe must be set"); } this.EnlistmentRoot = enlistmentRoot; this.WorkingDirectoryRoot = workingDirectoryRoot; this.WorkingDirectoryBackingRoot = workingDirectoryBackingRoot; this.DotGitRoot = Path.Combine(this.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Root); this.GitBinPath = gitBinPath; this.FlushFileBuffersForPacks = flushFileBuffersForPacks; GitProcess gitProcess = new GitProcess(this); if (repoUrl != null) { this.RepoUrl = repoUrl; } else { GitProcess.ConfigResult originResult = gitProcess.GetOriginUrl(); if (!originResult.TryParseAsString(out string originUrl, out string error)) { throw new InvalidRepoException("Could not get origin url. git error: " + error); } if (originUrl == null) { throw new InvalidRepoException("Could not get origin url. remote 'origin' is not configured for this repo.'"); } this.RepoUrl = originUrl.Trim(); } this.Authentication = authentication ?? new GitAuthentication(gitProcess, this.RepoUrl); } public string EnlistmentRoot { get; } // Path to the root of the working (i.e. "src") directory. // On platforms where the contents of the working directory are stored // at a different location (e.g. Linux), WorkingDirectoryBackingRoot is the path of that backing // storage location. On all other platforms WorkingDirectoryRoot and WorkingDirectoryBackingRoot // are the same. public string WorkingDirectoryRoot { get; } public string WorkingDirectoryBackingRoot { get; } public string DotGitRoot { get; protected set; } public abstract string GitObjectsRoot { get; protected set; } public abstract string LocalObjectsRoot { get; protected set; } public abstract string GitPackRoot { get; protected set; } /// /// Path to the git index file. Override for worktree-specific paths. /// public virtual string GitIndexPath { get { return Path.Combine(this.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Index); } } public string RepoUrl { get; } public bool FlushFileBuffersForPacks { get; } public string GitBinPath { get; } public GitAuthentication Authentication { get; } public static string GetNewLogFileName( string logsRoot, string prefix, string logId = null, PhysicalFileSystem fileSystem = null) { fileSystem = fileSystem ?? new PhysicalFileSystem(); // TODO: Remove Directory.CreateDirectory() code from here // Don't change the state from an accessor. if (!fileSystem.DirectoryExists(logsRoot)) { fileSystem.CreateDirectory(logsRoot); } logId = logId ?? DateTime.Now.ToString("yyyyMMdd_HHmmss"); string name = prefix + "_" + logId; string fullPath = Path.Combine( logsRoot, name + ".log"); if (fileSystem.FileExists(fullPath)) { fullPath = Path.Combine( logsRoot, name + "_" + Guid.NewGuid().ToString("N") + ".log"); } return fullPath; } public virtual GitProcess CreateGitProcess() { return new GitProcess(this); } } } ================================================ FILE: GVFS/GVFS.Common/EpochConverter.cs ================================================ using System; namespace GVFS.Common { public static class EpochConverter { private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); public static long ToUnixEpochSeconds(DateTime datetime) { return Convert.ToInt64(Math.Truncate((datetime - UnixEpoch).TotalSeconds)); } public static DateTime FromUnixEpochSeconds(long secondsSinceEpoch) { return UnixEpoch.AddSeconds(secondsSinceEpoch); } } } ================================================ FILE: GVFS/GVFS.Common/FileBasedCollection.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Text; using System.Threading; namespace GVFS.Common { public abstract class FileBasedCollection : IDisposable { private const string EtwArea = nameof(FileBasedCollection); private const string AddEntryPrefix = "A "; private const string RemoveEntryPrefix = "D "; // Use the same newline separator regardless of platform private const string NewLine = "\r\n"; private const int IoFailureRetryDelayMS = 50; private const int IoFailureLoggingThreshold = 500; /// /// If true, this FileBasedCollection appends directly to dataFileHandle stream /// If false, this FileBasedCollection only using .tmp + rename to update data on disk /// private readonly bool collectionAppendsDirectlyToFile; private readonly object fileLock = new object(); private readonly PhysicalFileSystem fileSystem; private readonly string dataDirectoryPath; private readonly string tempFilePath; private Stream dataFileHandle; protected FileBasedCollection(ITracer tracer, PhysicalFileSystem fileSystem, string dataFilePath, bool collectionAppendsDirectlyToFile) { this.Tracer = tracer; this.fileSystem = fileSystem; this.DataFilePath = dataFilePath; this.tempFilePath = this.DataFilePath + ".tmp"; this.dataDirectoryPath = Path.GetDirectoryName(this.DataFilePath); this.collectionAppendsDirectlyToFile = collectionAppendsDirectlyToFile; } protected delegate bool TryParseAdd(string line, out TKey key, out TValue value, out string error); protected delegate bool TryParseRemove(string line, out TKey key, out string error); public string DataFilePath { get; } protected ITracer Tracer { get; } public void Dispose() { lock (this.fileLock) { this.CloseDataFile(); } } public void ForceFlush() { if (this.dataFileHandle != null) { FileStream fs = this.dataFileHandle as FileStream; if (fs != null) { fs.Flush(flushToDisk: true); } } } protected void WriteAndReplaceDataFile(Func> getDataLines) { lock (this.fileLock) { try { this.CloseDataFile(); bool tmpFileCreated = false; int tmpFileCreateAttempts = 0; bool tmpFileMoved = false; int tmpFileMoveAttempts = 0; Exception lastException = null; while (!tmpFileCreated || !tmpFileMoved) { if (!tmpFileCreated) { tmpFileCreated = this.TryWriteTempFile(getDataLines, out lastException); if (!tmpFileCreated) { if (this.Tracer != null && tmpFileCreateAttempts % IoFailureLoggingThreshold == 0) { EventMetadata metadata = CreateEventMetadata(lastException); metadata.Add("tmpFileCreateAttempts", tmpFileCreateAttempts); this.Tracer.RelatedWarning(metadata, nameof(this.WriteAndReplaceDataFile) + ": Failed to create tmp file ... retrying"); } ++tmpFileCreateAttempts; Thread.Sleep(IoFailureRetryDelayMS); } } if (tmpFileCreated) { try { if (this.fileSystem.FileExists(this.tempFilePath)) { this.fileSystem.MoveAndOverwriteFile(this.tempFilePath, this.DataFilePath); tmpFileMoved = true; } else { if (this.Tracer != null) { EventMetadata metadata = CreateEventMetadata(); metadata.Add("tmpFileMoveAttempts", tmpFileMoveAttempts); this.Tracer.RelatedWarning(metadata, nameof(this.WriteAndReplaceDataFile) + ": tmp file is missing. Recreating tmp file."); } tmpFileCreated = false; } } catch (Win32Exception e) { if (this.Tracer != null && tmpFileMoveAttempts % IoFailureLoggingThreshold == 0) { EventMetadata metadata = CreateEventMetadata(e); metadata.Add("tmpFileMoveAttempts", tmpFileMoveAttempts); this.Tracer.RelatedWarning(metadata, nameof(this.WriteAndReplaceDataFile) + ": Failed to overwrite data file ... retrying"); } ++tmpFileMoveAttempts; Thread.Sleep(IoFailureRetryDelayMS); } } } if (this.collectionAppendsDirectlyToFile) { this.OpenOrCreateDataFile(retryUntilSuccess: true); } } catch (Exception e) { throw new FileBasedCollectionException(e); } } } protected string FormatAddLine(string line) { return AddEntryPrefix + line; } protected string FormatRemoveLine(string line) { return RemoveEntryPrefix + line; } /// An optional callback to be run as soon as the fileLock is taken. protected void WriteAddEntry(string value, Action synchronizedAction = null) { lock (this.fileLock) { string line = this.FormatAddLine(value); if (synchronizedAction != null) { synchronizedAction(); } this.WriteToDisk(line); } } /// An optional callback to be run as soon as the fileLock is taken. protected void WriteRemoveEntry(string key, Action synchronizedAction = null) { lock (this.fileLock) { string line = this.FormatRemoveLine(key); if (synchronizedAction != null) { synchronizedAction(); } this.WriteToDisk(line); } } protected void DeleteDataFileIfCondition(Func condition) { if (!this.collectionAppendsDirectlyToFile) { throw new InvalidOperationException(nameof(this.DeleteDataFileIfCondition) + " requires that collectionAppendsDirectlyToFile be true"); } lock (this.fileLock) { if (condition()) { this.dataFileHandle.SetLength(0); } } } /// An optional callback to be run as soon as the fileLock is taken protected bool TryLoadFromDisk( TryParseAdd tryParseAdd, TryParseRemove tryParseRemove, Action add, out string error, Action synchronizedAction = null) { lock (this.fileLock) { try { if (synchronizedAction != null) { synchronizedAction(); } this.fileSystem.CreateDirectory(this.dataDirectoryPath); this.OpenOrCreateDataFile(retryUntilSuccess: false); if (this.collectionAppendsDirectlyToFile) { this.RemoveLastEntryIfInvalid(); } long lineCount = 0; this.dataFileHandle.Seek(0, SeekOrigin.Begin); StreamReader reader = new StreamReader(this.dataFileHandle); Dictionary parsedEntries = new Dictionary(); while (!reader.EndOfStream) { lineCount++; // StreamReader strips the trailing /r/n string line = reader.ReadLine(); if (line.StartsWith(RemoveEntryPrefix)) { TKey key; if (!tryParseRemove(line.Substring(RemoveEntryPrefix.Length), out key, out error)) { error = string.Format("{0} is corrupt on line {1}: {2}", this.GetType().Name, lineCount, error); return false; } parsedEntries.Remove(key); } else if (line.StartsWith(AddEntryPrefix)) { TKey key; TValue value; if (!tryParseAdd(line.Substring(AddEntryPrefix.Length), out key, out value, out error)) { error = string.Format("{0} is corrupt on line {1}: {2}", this.GetType().Name, lineCount, error); return false; } parsedEntries[key] = value; } else { error = string.Format("{0} is corrupt on line {1}: Invalid Prefix '{2}'", this.GetType().Name, lineCount, line[0]); return false; } } foreach (KeyValuePair kvp in parsedEntries) { add(kvp.Key, kvp.Value); } if (!this.collectionAppendsDirectlyToFile) { this.CloseDataFile(); } } catch (IOException ex) { error = ex.ToString(); this.CloseDataFile(); return false; } catch (Exception e) { this.CloseDataFile(); throw new FileBasedCollectionException(e); } error = null; return true; } } private static EventMetadata CreateEventMetadata(Exception e = null) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", EtwArea); if (e != null) { metadata.Add("Exception", e.ToString()); } return metadata; } /// /// Closes dataFileHandle. Requires fileLock. /// private void CloseDataFile() { if (this.dataFileHandle != null) { this.dataFileHandle.Dispose(); this.dataFileHandle = null; } } /// /// Opens dataFileHandle for ReadWrite. Requires fileLock. /// /// If true, OpenOrCreateDataFile will continue to retry until it succeeds /// If retryUntilSuccess is true, OpenOrCreateDataFile will only attempt to retry when the error is non-fatal private void OpenOrCreateDataFile(bool retryUntilSuccess) { int attempts = 0; Exception lastException = null; while (true) { try { if (this.dataFileHandle == null) { this.dataFileHandle = this.fileSystem.OpenFileStream( this.DataFilePath, FileMode.OpenOrCreate, this.collectionAppendsDirectlyToFile ? FileAccess.ReadWrite : FileAccess.Read, FileShare.Read, callFlushFileBuffers: false); } this.dataFileHandle.Seek(0, SeekOrigin.End); return; } catch (IOException e) { lastException = e; } catch (UnauthorizedAccessException e) { lastException = e; } if (retryUntilSuccess) { if (this.Tracer != null && attempts % IoFailureLoggingThreshold == 0) { EventMetadata metadata = CreateEventMetadata(lastException); metadata.Add("attempts", attempts); this.Tracer.RelatedWarning(metadata, nameof(this.OpenOrCreateDataFile) + ": Failed to open data file stream ... retrying"); } ++attempts; Thread.Sleep(IoFailureRetryDelayMS); } else { throw lastException; } } } /// /// Writes data as UTF8 to dataFileHandle. fileLock will be acquired. /// private void WriteToDisk(string value) { if (!this.collectionAppendsDirectlyToFile) { throw new InvalidOperationException(nameof(this.WriteToDisk) + " requires that collectionAppendsDirectlyToFile be true"); } byte[] bytes = Encoding.UTF8.GetBytes(value + NewLine); lock (this.fileLock) { this.dataFileHandle.Write(bytes, 0, bytes.Length); this.dataFileHandle.Flush(); } } /// /// Reads entries from dataFileHandle, removing any data after the last NewLine ("\r\n"). Requires fileLock. /// private void RemoveLastEntryIfInvalid() { if (this.dataFileHandle.Length > 2) { this.dataFileHandle.Seek(-2, SeekOrigin.End); if (this.dataFileHandle.ReadByte() != '\r' || this.dataFileHandle.ReadByte() != '\n') { this.dataFileHandle.Seek(0, SeekOrigin.Begin); long lastLineEnding = 0; while (this.dataFileHandle.Position < this.dataFileHandle.Length) { if (this.dataFileHandle.ReadByte() == '\r' && this.dataFileHandle.ReadByte() == '\n') { lastLineEnding = this.dataFileHandle.Position; } } this.dataFileHandle.SetLength(lastLineEnding); } } } /// /// Attempts to write all data lines to tmp file /// /// Method that returns the dataLines to write as an IEnumerable /// Output parameter that's set when TryWriteTempFile catches a non-fatal exception /// True if the write succeeded and false otherwise /// If a fatal exception is encountered while trying to write the temp file, this method will not catch it. private bool TryWriteTempFile(Func> getDataLines, out Exception handledException) { handledException = null; try { using (Stream tempFile = this.fileSystem.OpenFileStream(this.tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, callFlushFileBuffers: true)) using (StreamWriter writer = new StreamWriter(tempFile)) { foreach (string line in getDataLines()) { writer.Write(line + NewLine); } tempFile.Flush(); } return true; } catch (IOException e) { handledException = e; return false; } catch (UnauthorizedAccessException e) { handledException = e; return false; } } } } ================================================ FILE: GVFS/GVFS.Common/FileBasedCollectionException.cs ================================================ using System; namespace GVFS.Common { public class FileBasedCollectionException : Exception { public FileBasedCollectionException(Exception innerException) : base(innerException.Message, innerException) { } } } ================================================ FILE: GVFS/GVFS.Common/FileBasedDictionary.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; namespace GVFS.Common { public class FileBasedDictionary : FileBasedCollection { private ConcurrentDictionary data; private FileBasedDictionary( ITracer tracer, PhysicalFileSystem fileSystem, string dataFilePath, IEqualityComparer keyComparer) : base(tracer, fileSystem, dataFilePath, collectionAppendsDirectlyToFile: false) { this.data = new ConcurrentDictionary(keyComparer); } public static bool TryCreate( ITracer tracer, string dictionaryPath, PhysicalFileSystem fileSystem, out FileBasedDictionary output, out string error, IEqualityComparer keyComparer = null) { output = new FileBasedDictionary( tracer, fileSystem, dictionaryPath, keyComparer ?? EqualityComparer.Default); if (!output.TryLoadFromDisk( output.TryParseAddLine, output.TryParseRemoveLine, output.HandleAddLine, out error)) { output = null; return false; } return true; } public void SetValuesAndFlush(IEnumerable> values) { try { foreach (KeyValuePair kvp in values) { this.data[kvp.Key] = kvp.Value; } this.Flush(); } catch (Exception e) { throw new FileBasedCollectionException(e); } } public void SetValueAndFlush(TKey key, TValue value) { try { this.data[key] = value; this.Flush(); } catch (Exception e) { throw new FileBasedCollectionException(e); } } public bool TryGetValue(TKey key, out TValue value) { try { return this.data.TryGetValue(key, out value); } catch (Exception e) { throw new FileBasedCollectionException(e); } } public void RemoveAndFlush(TKey key) { try { TValue value; if (this.data.TryRemove(key, out value)) { this.Flush(); } } catch (Exception e) { throw new FileBasedCollectionException(e); } } public Dictionary GetAllKeysAndValues() { return new Dictionary(this.data); } private void Flush() { this.WriteAndReplaceDataFile(this.GenerateDataLines); } private bool TryParseAddLine(string line, out TKey key, out TValue value, out string error) { try { KeyValuePair kvp = JsonConvert.DeserializeObject>(line); key = kvp.Key; value = kvp.Value; } catch (JsonException ex) { key = default(TKey); value = default(TValue); error = "Could not deserialize JSON for add line: " + ex.Message; return false; } error = null; return true; } private bool TryParseRemoveLine(string line, out TKey key, out string error) { try { key = JsonConvert.DeserializeObject(line); } catch (JsonException ex) { key = default(TKey); error = "Could not deserialize JSON for delete line: " + ex.Message; return false; } error = null; return true; } private void HandleAddLine(TKey key, TValue value) { this.data.TryAdd(key, value); } private IEnumerable GenerateDataLines() { foreach (KeyValuePair kvp in this.data) { yield return this.FormatAddLine(JsonConvert.SerializeObject(kvp).Trim()); } } } } ================================================ FILE: GVFS/GVFS.Common/FileBasedLock.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using System; namespace GVFS.Common { public abstract class FileBasedLock : IDisposable { public FileBasedLock( PhysicalFileSystem fileSystem, ITracer tracer, string lockPath) { this.FileSystem = fileSystem; this.Tracer = tracer; this.LockPath = lockPath; } protected PhysicalFileSystem FileSystem { get; } protected string LockPath { get; } protected ITracer Tracer { get; } public bool TryAcquireLock() { return this.TryAcquireLock(out _); } /// /// Attempts to acquire the lock, providing the exception that prevented acquisition. /// /// /// When the method returns false, contains the exception that prevented lock acquisition. /// Callers can pattern-match on the exception type to distinguish lock contention /// (e.g. with a sharing violation HResult) from /// permission errors () or other failures. /// Null when the method returns true. /// /// True if the lock was acquired, false otherwise. public abstract bool TryAcquireLock(out Exception lockException); public abstract void Dispose(); } } ================================================ FILE: GVFS/GVFS.Common/FileSystem/DirectoryItemInfo.cs ================================================ namespace GVFS.Common.FileSystem { public class DirectoryItemInfo { public string Name { get; set; } public string FullName { get; set; } public long Length { get; set; } public bool IsDirectory { get; set; } } } ================================================ FILE: GVFS/GVFS.Common/FileSystem/FileProperties.cs ================================================ using System; using System.IO; namespace GVFS.Common.FileSystem { public class FileProperties { public static readonly FileProperties DefaultFile = new FileProperties(FileAttributes.Normal, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, 0); public static readonly FileProperties DefaultDirectory = new FileProperties(FileAttributes.Directory, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, 0); public FileProperties(FileAttributes attributes, DateTime creationTimeUTC, DateTime lastAccessTimeUTC, DateTime lastWriteTimeUTC, long length) { this.FileAttributes = attributes; this.CreationTimeUTC = creationTimeUTC; this.LastAccessTimeUTC = lastAccessTimeUTC; this.LastWriteTimeUTC = lastWriteTimeUTC; this.Length = length; } public FileAttributes FileAttributes { get; private set; } public DateTime CreationTimeUTC { get; private set; } public DateTime LastAccessTimeUTC { get; private set; } public DateTime LastWriteTimeUTC { get; private set; } public long Length { get; private set; } } } ================================================ FILE: GVFS/GVFS.Common/FileSystem/FlushToDiskFileStream.cs ================================================ using System.IO; namespace GVFS.Common.FileSystem { public class FlushToDiskFileStream : FileStream { public FlushToDiskFileStream(string path, FileMode mode) : base(path, mode) { } public FlushToDiskFileStream(string path, FileMode mode, FileAccess access, FileShare share) : base(path, mode, access, share) { } public FlushToDiskFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) : base(path, mode, access, share, bufferSize, options) { } public override void Flush() { // Ensure that all buffered data in intermediate file buffers is written to disk // Passing in true below results in a call to FlushFileBuffers base.Flush(true); } } } ================================================ FILE: GVFS/GVFS.Common/FileSystem/HooksInstaller.cs ================================================ using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Threading; namespace GVFS.Common.FileSystem { public static class HooksInstaller { private static readonly string ExecutingDirectory; private static readonly HookData[] NativeHooks = new[] { new HookData(GVFSConstants.DotGit.Hooks.ReadObjectName, GVFSConstants.DotGit.Hooks.ReadObjectPath, GVFSPlatform.Instance.Constants.GVFSReadObjectHookExecutableName), new HookData(GVFSConstants.DotGit.Hooks.VirtualFileSystemName, GVFSConstants.DotGit.Hooks.VirtualFileSystemPath, GVFSPlatform.Instance.Constants.GVFSVirtualFileSystemHookExecutableName), new HookData(GVFSConstants.DotGit.Hooks.PostIndexChangedName, GVFSConstants.DotGit.Hooks.PostIndexChangedPath, GVFSPlatform.Instance.Constants.GVFSPostIndexChangedHookExecutableName), }; static HooksInstaller() { ExecutingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); } public static string MergeHooksData(string[] defaultHooksLines, string filename, string hookName) { IEnumerable valuableHooksLines = defaultHooksLines.Where(line => !string.IsNullOrEmpty(line.Trim())); /* Wrap in quotes to handle spaces in the path */ string absolutePathToHooksExecutable = $"\"{Path.Combine(ExecutingDirectory, GVFSPlatform.Instance.Constants.GVFSHooksExecutableName)}\""; if (valuableHooksLines.Contains(GVFSPlatform.Instance.Constants.GVFSHooksExecutableName, GVFSPlatform.Instance.Constants.PathComparer)) { throw new HooksConfigurationException( $"{GVFSPlatform.Instance.Constants.GVFSHooksExecutableName} should not be specified in the configuration for " + GVFSConstants.DotGit.Hooks.PostCommandHookName + " hooks (" + filename + ")."); } else if (!valuableHooksLines.Any()) { return absolutePathToHooksExecutable; } else if (hookName.Equals(GVFSConstants.DotGit.Hooks.PostCommandHookName)) { return string.Join("\n", new string[] { absolutePathToHooksExecutable }.Concat(valuableHooksLines)); } else { return string.Join("\n", valuableHooksLines.Concat(new string[] { absolutePathToHooksExecutable })); } } public static bool InstallHooks(GVFSContext context, out string error) { error = string.Empty; try { foreach (HookData hook in NativeHooks) { string installedHookPath = Path.Combine(ExecutingDirectory, hook.ExecutableName); string targetHookPath = Path.Combine(context.Enlistment.WorkingDirectoryBackingRoot, hook.Path + GVFSPlatform.Instance.Constants.ExecutableExtension); if (!TryHooksInstallationAction(() => CopyHook(context, installedHookPath, targetHookPath), out error)) { error = "Failed to copy " + installedHookPath + "\n" + error; return false; } } string precommandHookPath = Path.Combine(context.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Hooks.PreCommandPath); if (!GVFSPlatform.Instance.TryInstallGitCommandHooks(context, ExecutingDirectory, GVFSConstants.DotGit.Hooks.PreCommandHookName, precommandHookPath, out error)) { return false; } string postcommandHookPath = Path.Combine(context.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Hooks.PostCommandPath); if (!GVFSPlatform.Instance.TryInstallGitCommandHooks(context, ExecutingDirectory, GVFSConstants.DotGit.Hooks.PostCommandHookName, postcommandHookPath, out error)) { return false; } } catch (Exception e) { error = e.ToString(); return false; } return true; } public static bool TryUpdateHooks(GVFSContext context, out string errorMessage) { errorMessage = string.Empty; foreach (HookData hook in NativeHooks) { if (!TryUpdateHook(context, hook, out errorMessage)) { return false; } } return true; } public static void CopyHook(GVFSContext context, string sourcePath, string destinationPath) { Exception ex; if (!context.FileSystem.TryCopyToTempFileAndRename(sourcePath, destinationPath, out ex)) { throw new RetryableException($"Error installing {sourcePath} to {destinationPath}", ex); } } /// /// Try to perform the specified action. The action will be retried (with backoff) up to 3 times. /// /// Action to perform /// Error message /// True if the action succeeded and false otherwise /// This method is optimized for the hooks installation process and should not be used /// as a generic retry mechanism. See RetryWrapper for a general purpose retry mechanism public static bool TryHooksInstallationAction(Action action, out string errorMessage) { int retriesLeft = 3; int retryWaitMillis = 500; // Will grow exponentially on each retry attempt errorMessage = null; while (true) { try { action(); return true; } catch (RetryableException re) { if (retriesLeft == 0) { errorMessage = re.InnerException.ToString(); return false; } Thread.Sleep(retryWaitMillis); retriesLeft -= 1; retryWaitMillis *= 2; } catch (Exception e) { errorMessage = e.ToString(); return false; } } } private static bool TryUpdateHook( GVFSContext context, HookData hook, out string errorMessage) { bool copyHook = false; string enlistmentHookPath = Path.Combine(context.Enlistment.WorkingDirectoryBackingRoot, hook.Path + GVFSPlatform.Instance.Constants.ExecutableExtension); string installedHookPath = Path.Combine(ExecutingDirectory, hook.ExecutableName); if (!context.FileSystem.FileExists(installedHookPath)) { errorMessage = hook.ExecutableName + " cannot be found at " + installedHookPath; return false; } if (!context.FileSystem.FileExists(enlistmentHookPath)) { copyHook = true; EventMetadata metadata = new EventMetadata(); metadata.Add("Area", "Mount"); metadata.Add(nameof(enlistmentHookPath), enlistmentHookPath); metadata.Add(nameof(installedHookPath), installedHookPath); metadata.Add(TracingConstants.MessageKey.WarningMessage, hook.Name + " not found in enlistment, copying from installation folder"); context.Tracer.RelatedWarning(hook.Name + " MissingFromEnlistment", metadata); } else { try { FileVersionInfo enlistmentVersion = FileVersionInfo.GetVersionInfo(enlistmentHookPath); FileVersionInfo installedVersion = FileVersionInfo.GetVersionInfo(installedHookPath); copyHook = enlistmentVersion.FileVersion != installedVersion.FileVersion; } catch (Exception e) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", "Mount"); metadata.Add(nameof(enlistmentHookPath), enlistmentHookPath); metadata.Add(nameof(installedHookPath), installedHookPath); metadata.Add("Exception", e.ToString()); context.Tracer.RelatedError(metadata, "Failed to compare " + hook.Name + " version"); errorMessage = "Error comparing " + hook.Name + " versions. " + ConsoleHelper.GetGVFSLogMessage(context.Enlistment.EnlistmentRoot); return false; } } if (copyHook) { try { CopyHook(context, installedHookPath, enlistmentHookPath); } catch (Exception e) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", "Mount"); metadata.Add(nameof(enlistmentHookPath), enlistmentHookPath); metadata.Add(nameof(installedHookPath), installedHookPath); metadata.Add("Exception", e.ToString()); context.Tracer.RelatedError(metadata, "Failed to copy " + hook.Name + " to enlistment"); errorMessage = "Error copying " + hook.Name + " to enlistment. " + ConsoleHelper.GetGVFSLogMessage(context.Enlistment.EnlistmentRoot); return false; } } errorMessage = null; return true; } public class HooksConfigurationException : Exception { public HooksConfigurationException(string message) : base(message) { } } private class HookData { public HookData(string name, string path, string executableName) { this.Name = name; this.Path = path; this.ExecutableName = executableName; } public string Name { get; } public string Path { get; } public string ExecutableName { get; } } } } ================================================ FILE: GVFS/GVFS.Common/FileSystem/IKernelDriver.cs ================================================ using GVFS.Common.Tracing; using System; using System.IO; namespace GVFS.Common.FileSystem { public interface IKernelDriver { bool EnumerationExpandsDirectories { get; } /// /// Gets a value indicating whether file sizes required to write/update placeholders /// bool EmptyPlaceholdersRequireFileSize { get; } string LogsFolderPath { get; } bool IsSupported(string normalizedEnlistmentRootPath, out string warning, out string error); bool TryFlushLogs(out string errors); bool TryPrepareFolderForCallbacks(string folderPath, out string error, out Exception exception); bool IsReady(JsonTracer tracer, string enlistmentRoot, TextWriter output, out string error); bool IsGVFSUpgradeSupported(); bool RegisterForOfflineIO(); bool UnregisterForOfflineIO(); } } ================================================ FILE: GVFS/GVFS.Common/FileSystem/IPlatformFileSystem.cs ================================================ using GVFS.Common.Tracing; using System; namespace GVFS.Common.FileSystem { public interface IPlatformFileSystem { bool SupportsFileMode { get; } void FlushFileBuffers(string path); void MoveAndOverwriteFile(string sourceFileName, string destinationFilename); bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage); void SetDirectoryLastWriteTime(string path, DateTime lastWriteTime, out bool directoryExists); void ChangeMode(string path, ushort mode); bool HydrateFile(string fileName, byte[] buffer); bool IsExecutable(string filePath); bool IsSocket(string filePath); bool TryCreateDirectoryAccessibleByAuthUsers(string directoryPath, out string error, ITracer tracer = null); bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error); bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error); bool IsFileSystemSupported(string path, out string error); void EnsureDirectoryIsOwnedByCurrentUser(string workingDirectoryRoot); } } ================================================ FILE: GVFS/GVFS.Common/FileSystem/PhysicalFileSystem.cs ================================================ using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Security; using System.Threading; namespace GVFS.Common.FileSystem { public class PhysicalFileSystem { public const int DefaultStreamBufferSize = 8192; public virtual void DeleteDirectory(string path, bool recursive = true, bool ignoreDirectoryDeleteExceptions = false) { if (!Directory.Exists(path)) { return; } DirectoryInfo directory = new DirectoryInfo(path); if (recursive) { foreach (FileInfo file in directory.GetFiles()) { file.Attributes = FileAttributes.Normal; file.Delete(); } foreach (DirectoryInfo subDirectory in directory.GetDirectories()) { this.DeleteDirectory(subDirectory.FullName, recursive, ignoreDirectoryDeleteExceptions); } } try { directory.Delete(); } catch (Exception) { if (!ignoreDirectoryDeleteExceptions) { throw; } } } public virtual void MoveDirectory(string sourceDirName, string destDirName) { Directory.Move(sourceDirName, destDirName); } public virtual void CopyDirectoryRecursive( string srcDirectoryPath, string dstDirectoryPath, HashSet excludeDirectories = null) { DirectoryInfo srcDirectory = new DirectoryInfo(srcDirectoryPath); if (!this.DirectoryExists(dstDirectoryPath)) { this.CreateDirectory(dstDirectoryPath); } foreach (FileInfo file in srcDirectory.EnumerateFiles()) { this.CopyFile(file.FullName, Path.Combine(dstDirectoryPath, file.Name), overwrite: true); } foreach (DirectoryInfo subDirectory in srcDirectory.EnumerateDirectories()) { if (excludeDirectories == null || !excludeDirectories.Contains(subDirectory.FullName)) { this.CopyDirectoryRecursive( subDirectory.FullName, Path.Combine(dstDirectoryPath, subDirectory.Name), excludeDirectories); } } } public virtual bool FileExists(string path) { return File.Exists(path); } public virtual bool DirectoryExists(string path) { return Directory.Exists(path); } public virtual void CopyFile(string sourcePath, string destinationPath, bool overwrite) { File.Copy(sourcePath, destinationPath, overwrite); } public virtual void DeleteFile(string path) { File.Delete(path); } public virtual string ReadAllText(string path) { return File.ReadAllText(path); } public virtual byte[] ReadAllBytes(string path) { return File.ReadAllBytes(path); } public virtual IEnumerable ReadLines(string path) { return File.ReadLines(path); } public virtual void WriteAllText(string path, string contents) { File.WriteAllText(path, contents); } public virtual bool TryWriteAllText(string path, string contents) { try { this.WriteAllText(path, contents); return true; } catch (IOException) { return false; } catch (UnauthorizedAccessException) { return false; } catch (SecurityException) { return false; } } public Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, bool callFlushFileBuffers) { return this.OpenFileStream(path, fileMode, fileAccess, shareMode, FileOptions.None, callFlushFileBuffers); } public virtual void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) { GVFSPlatform.Instance.FileSystem.MoveAndOverwriteFile(sourceFileName, destinationFilename); } public virtual bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) { return GVFSPlatform.Instance.FileSystem.TryGetNormalizedPath(path, out normalizedPath, out errorMessage); } public virtual Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool callFlushFileBuffers) { if (callFlushFileBuffers) { return new FlushToDiskFileStream(path, fileMode, fileAccess, shareMode, DefaultStreamBufferSize, options); } return new FileStream(path, fileMode, fileAccess, shareMode, DefaultStreamBufferSize, options); } public virtual void FlushFileBuffers(string path) { GVFSPlatform.Instance.FileSystem.FlushFileBuffers(path); } public virtual void CreateDirectory(string path) { Directory.CreateDirectory(path); } public virtual bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error) { return GVFSPlatform.Instance.FileSystem.TryCreateDirectoryWithAdminAndUserModifyPermissions(directoryPath, out error); } public virtual bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error) { return GVFSPlatform.Instance.FileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissions(tracer, directoryPath, out error); } public virtual bool IsSymLink(string path) { return (this.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint && NativeMethods.IsSymLink(path); } public virtual IEnumerable ItemsInDirectory(string path) { DirectoryInfo ntfsDirectory = new DirectoryInfo(path); foreach (FileSystemInfo ntfsItem in ntfsDirectory.GetFileSystemInfos()) { DirectoryItemInfo itemInfo = new DirectoryItemInfo() { FullName = ntfsItem.FullName, Name = ntfsItem.Name, IsDirectory = (ntfsItem.Attributes & FileAttributes.Directory) != 0 }; if (!itemInfo.IsDirectory) { itemInfo.Length = ((FileInfo)ntfsItem).Length; } yield return itemInfo; } } public virtual IEnumerable EnumerateDirectories(string path) { return Directory.EnumerateDirectories(path); } public virtual FileProperties GetFileProperties(string path) { FileInfo entry = new FileInfo(path); if (entry.Exists) { return new FileProperties( entry.Attributes, entry.CreationTimeUtc, entry.LastAccessTimeUtc, entry.LastWriteTimeUtc, entry.Length); } else { return FileProperties.DefaultFile; } } public virtual FileAttributes GetAttributes(string path) { return File.GetAttributes(path); } public virtual void SetAttributes(string path, FileAttributes fileAttributes) { File.SetAttributes(path, fileAttributes); } public virtual void MoveFile(string sourcePath, string targetPath) { File.Move(sourcePath, targetPath); } public virtual string[] GetFiles(string directoryPath, string mask) { return Directory.GetFiles(directoryPath, mask); } public virtual FileVersionInfo GetVersionInfo(string path) { return FileVersionInfo.GetVersionInfo(path); } public virtual bool FileVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2) { return versionInfo1.FileVersion == versionInfo2.FileVersion; } public virtual bool ProductVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2) { return versionInfo1.ProductVersion == versionInfo2.ProductVersion; } public bool TryWriteTempFileAndRename(string destinationPath, string contents, out Exception handledException) { handledException = null; string tempFilePath = destinationPath + ".temp"; string parentPath = Path.GetDirectoryName(tempFilePath); this.CreateDirectory(parentPath); try { using (Stream tempFile = this.OpenFileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, callFlushFileBuffers: true)) using (StreamWriter writer = new StreamWriter(tempFile)) { writer.Write(contents); tempFile.Flush(); } this.MoveAndOverwriteFile(tempFilePath, destinationPath); return true; } catch (Win32Exception e) { handledException = e; return false; } catch (IOException e) { handledException = e; return false; } catch (UnauthorizedAccessException e) { handledException = e; return false; } } public bool TryCopyToTempFileAndRename(string sourcePath, string destinationPath, out Exception handledException) { handledException = null; string tempFilePath = destinationPath + ".temp"; try { File.Copy(sourcePath, tempFilePath, overwrite: true); GVFSPlatform.Instance.FileSystem.FlushFileBuffers(tempFilePath); this.MoveAndOverwriteFile(tempFilePath, destinationPath); return true; } catch (Win32Exception e) { handledException = e; return false; } catch (IOException e) { handledException = e; return false; } catch (UnauthorizedAccessException e) { handledException = e; return false; } } public bool TryCreateDirectory(string path, out Exception exception) { try { Directory.CreateDirectory(path); } catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is ArgumentException || e is NotSupportedException) { exception = e; return false; } exception = null; return true; } /// /// Recursively deletes a directory and all contained contents. /// public bool TryDeleteDirectory(string path, out Exception exception) { try { this.DeleteDirectory(path); } catch (DirectoryNotFoundException) { // The directory does not exist - follow the // convention of this class and report success } catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is ArgumentException) { exception = e; return false; } exception = null; return true; } /// /// Attempts to delete a file /// /// Path of file to delete /// True if the delete succeed, and false otherwise /// The files attributes will be set to Normal before deleting the file public bool TryDeleteFile(string path) { Exception exception; return this.TryDeleteFile(path, out exception); } /// /// Attempts to delete a file /// /// Path of file to delete /// Exception thrown, if any, while attempting to delete file (or reset file attributes) /// True if the delete succeed, and false otherwise /// The files attributes will be set to Normal before deleting the file public bool TryDeleteFile(string path, out Exception exception) { exception = null; try { if (this.FileExists(path)) { this.SetAttributes(path, FileAttributes.Normal); this.DeleteFile(path); } return true; } catch (FileNotFoundException) { // SetAttributes could not find the file return true; } catch (IOException e) { exception = e; return false; } catch (UnauthorizedAccessException e) { exception = e; return false; } } /// /// Attempts to delete a file /// /// Path of file to delete /// Prefix to be used on keys when new entries are added to the metadata /// Metadata for recording failed deletes /// The files attributes will be set to Normal before deleting the file public bool TryDeleteFile(string path, string metadataKey, EventMetadata metadata) { Exception deleteException = null; if (!this.TryDeleteFile(path, out deleteException)) { metadata.Add($"{metadataKey}_DeleteFailed", "true"); if (deleteException != null) { metadata.Add($"{metadataKey}_DeleteException", deleteException.ToString()); } return false; } return true; } /// /// Retry delete until it succeeds (or maximum number of retries have failed) /// /// ITracer for logging and telemetry, can be null /// Path of file to delete /// /// Amount of time to wait between each delete attempt. If 0, there will be no delays between attempts /// /// Maximum number of retries (if 0, a single attempt will be made) /// /// Number of retries to attempt before logging a failure. First and last failure is always logged if tracer is not null. /// /// True if the delete succeed, and false otherwise /// The files attributes will be set to Normal before deleting the file public bool TryWaitForDelete( ITracer tracer, string path, int retryDelayMs, int maxRetries, int retryLoggingThreshold) { int failureCount = 0; while (this.FileExists(path)) { Exception exception = null; if (!this.TryDeleteFile(path, out exception)) { if (failureCount == maxRetries) { if (tracer != null) { EventMetadata metadata = new EventMetadata(); if (exception != null) { metadata.Add("Exception", exception.ToString()); } metadata.Add("path", path); metadata.Add("failureCount", failureCount + 1); metadata.Add("maxRetries", maxRetries); tracer.RelatedWarning(metadata, $"{nameof(this.TryWaitForDelete)}: Failed to delete file."); } return false; } else { if (tracer != null && failureCount % retryLoggingThreshold == 0) { EventMetadata metadata = new EventMetadata(); metadata.Add("Exception", exception.ToString()); metadata.Add("path", path); metadata.Add("failureCount", failureCount + 1); metadata.Add("maxRetries", maxRetries); tracer.RelatedWarning(metadata, $"{nameof(this.TryWaitForDelete)}: Failed to delete file, retrying ..."); } } ++failureCount; if (retryDelayMs > 0) { Thread.Sleep(retryDelayMs); } } } return true; } } } ================================================ FILE: GVFS/GVFS.Common/GVFS.Common.csproj ================================================  net471 true ================================================ FILE: GVFS/GVFS.Common/GVFSConstants.cs ================================================ using System.IO; namespace GVFS.Common { public static partial class GVFSConstants { public const int ShaStringLength = 40; public const int MaxPath = 260; public const string AllZeroSha = "0000000000000000000000000000000000000000"; public const char GitPathSeparator = '/'; public const string GitPathSeparatorString = "/"; public const char GitCommentSign = '#'; public const string PrefetchPackPrefix = "prefetch"; public const string InProgressPrefetchMarkerExtension = ".incomplete"; public const string GVFSEtwProviderName = "Microsoft.Git.GVFS"; public const string WorkingDirectoryRootName = "src"; public const string UnattendedEnvironmentVariable = "GVFS_UNATTENDED"; public const string DefaultGVFSCacheFolderName = ".gvfsCache"; public const string GitIsNotInstalledError = "Could not find git.exe. Ensure that Git is installed."; 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"; public const string DeprecatedCacheEndpointSuffix = ".cache-server-url"; public const string HooksPrefix = GitConfig.GVFSPrefix + "clone.default-"; public const string GVFSTelemetryId = GitConfig.GVFSPrefix + "telemetry-id"; public const string GVFSTelemetryPipe = GitConfig.GVFSPrefix + "telemetry-pipe"; public const string IKey = GitConfig.GVFSPrefix + "ikey"; public const string HooksExtension = ".hooks"; /* Intended to be a temporary config to allow testing of distrusting pack indexes from cache server * before it is enabled by default. */ public const string TrustPackIndexes = GVFSPrefix + "trust-pack-indexes"; public const bool TrustPackIndexesDefault = true; public const string ShowHydrationStatus = GVFSPrefix + "show-hydration-status"; public const bool ShowHydrationStatusDefault = false; public const string MaxHttpConnectionsConfig = GVFSPrefix + "max-http-connections"; } public static class LocalGVFSConfig { public const string UpgradeRing = "upgrade.ring"; public const string UpgradeFeedPackageName = "upgrade.feedpackagename"; public const string UpgradeFeedUrl = "upgrade.feedurl"; public const string OrgInfoServerUrl = "upgrade.orgInfoServerUrl"; public const string USNJournalUpdates = "usn.updateDirectories"; } public static class GitStatusCache { public const string EnableGitStatusCacheTokenFile = "EnableGitStatusCacheToken.dat"; } public static class Service { public const string ServiceName = "GVFS.Service"; public const string LogDirectory = "Logs"; } public static class MediaTypes { public const string PrefetchPackFilesAndIndexesMediaType = "application/x-gvfs-timestamped-packfiles-indexes"; public const string LooseObjectMediaType = "application/x-git-loose-object"; public const string CustomLooseObjectsMediaType = "application/x-gvfs-loose-objects"; public const string PackFileMediaType = "application/x-git-packfile"; } public static class Endpoints { public const string GVFSConfig = "/gvfs/config"; public const string GVFSObjects = "/gvfs/objects"; public const string GVFSPrefetch = "/gvfs/prefetch"; public const string GVFSSizes = "/gvfs/sizes"; public const string InfoRefs = "/info/refs?service=git-upload-pack"; } public static class SpecialGitFiles { public const string GitAttributes = ".gitattributes"; public const string GitIgnore = ".gitignore"; } public static class LogFileTypes { public const string MountPrefix = "mount"; public const string UpgradePrefix = "productupgrade"; public const string Clone = "clone"; public const string Dehydrate = "dehydrate"; public const string Health = "health"; public const string MountVerb = MountPrefix + "_verb"; public const string MountProcess = MountPrefix + "_process"; public const string MountUpgrade = MountPrefix + "_repoupgrade"; public const string Prefetch = "prefetch"; public const string Repair = "repair"; public const string Service = "service"; public const string Sparse = "sparse"; public const string UpgradeVerb = UpgradePrefix + "_verb"; public const string UpgradeProcess = UpgradePrefix + "_process"; public const string UpgradeSystemInstaller = UpgradePrefix + "_system_installer"; } public static class DotGVFS { public const string CorruptObjectsName = "CorruptObjects"; public const string LogName = "logs"; public const string MountLock = "mount.lock"; public static class Databases { public const string Name = "databases"; public static readonly string BackgroundFileSystemTasks = Path.Combine(Name, "BackgroundGitOperations.dat"); public static readonly string PlaceholderList = Path.Combine(Name, "PlaceholderList.dat"); public static readonly string ModifiedPaths = Path.Combine(Name, "ModifiedPaths.dat"); public static readonly string RepoMetadata = Path.Combine(Name, "RepoMetadata.dat"); public static readonly string VFSForGit = Path.Combine(Name, "VFSForGit.sqlite"); } public static class GitStatusCache { public const string Name = "gitStatusCache"; public static readonly string CachePath = Path.Combine(Name, "GitStatusCache.dat"); } public static class HydrationStatus { public static readonly string DisabledMarkerFile = Path.Combine("gitStatusCache", "HydrationStatusDisabled.dat"); } } public static class DotGit { public const string Root = ".git"; public const string HeadName = "HEAD"; public const string GitDirPrefix = "gitdir: "; public const string CommonDirName = "commondir"; public const string SkipCleanCheckName = "skip-clean-check"; public const string IndexName = "index"; public const string PackedRefsName = "packed-refs"; public const string LockExtension = ".lock"; public static readonly string Config = Path.Combine(DotGit.Root, "config"); public static readonly string Head = Path.Combine(DotGit.Root, HeadName); public static readonly string BisectStart = Path.Combine(DotGit.Root, "BISECT_START"); public static readonly string CherryPickHead = Path.Combine(DotGit.Root, "CHERRY_PICK_HEAD"); public static readonly string MergeHead = Path.Combine(DotGit.Root, "MERGE_HEAD"); public static readonly string RevertHead = Path.Combine(DotGit.Root, "REVERT_HEAD"); public static readonly string RebaseApply = Path.Combine(DotGit.Root, "rebase_apply"); public static readonly string Index = Path.Combine(DotGit.Root, IndexName); public static readonly string IndexLock = Path.Combine(DotGit.Root, IndexName + LockExtension); public static readonly string PackedRefs = Path.Combine(DotGit.Root, PackedRefsName); public static readonly string Shallow = Path.Combine(DotGit.Root, "shallow"); public static class Logs { public const string RootName = "logs"; public static readonly string HeadName = "HEAD"; public static readonly string Root = Path.Combine(DotGit.Root, RootName); public static readonly string Head = Path.Combine(Logs.Root, Logs.HeadName); /// Path relative to the git directory (e.g., "logs/HEAD"). public static readonly string HeadRelativePath = Path.Combine(RootName, HeadName); } public static class Hooks { public const string LoaderExecutable = "GitHooksLoader.exe"; public const string PreCommandHookName = "pre-command"; public const string PostCommandHookName = "post-command"; public const string ReadObjectName = "read-object"; public const string VirtualFileSystemName = "virtual-filesystem"; public const string PostIndexChangedName = "post-index-change"; public const string RootName = "hooks"; public static readonly string Root = Path.Combine(DotGit.Root, RootName); public static readonly string PreCommandPath = Path.Combine(Hooks.Root, PreCommandHookName); public static readonly string PostCommandPath = Path.Combine(Hooks.Root, PostCommandHookName); public static readonly string ReadObjectPath = Path.Combine(Hooks.Root, ReadObjectName); public static readonly string VirtualFileSystemPath = Path.Combine(Hooks.Root, VirtualFileSystemName); public static readonly string PostIndexChangedPath = Path.Combine(Hooks.Root, PostIndexChangedName); } public static class Info { public const string Name = "info"; public const string ExcludeName = "exclude"; public const string AlwaysExcludeName = "always_exclude"; public const string SparseCheckoutName = "sparse-checkout"; public static readonly string Root = Path.Combine(DotGit.Root, Info.Name); public static readonly string SparseCheckoutPath = Path.Combine(Info.Root, Info.SparseCheckoutName); public static readonly string ExcludePath = Path.Combine(Info.Root, ExcludeName); public static readonly string AlwaysExcludePath = Path.Combine(Info.Root, AlwaysExcludeName); } public static class Objects { public static readonly string Root = Path.Combine(DotGit.Root, "objects"); public static class Info { public static readonly string Root = Path.Combine(Objects.Root, "info"); public static readonly string Alternates = Path.Combine(Info.Root, "alternates"); /// Path relative to the git directory (e.g., "objects/info/alternates"). public static readonly string AlternatesRelativePath = Path.Combine("objects", "info", "alternates"); } public static class Pack { public static readonly string Name = "pack"; public static readonly string Root = Path.Combine(Objects.Root, Name); } } public static class Refs { public static readonly string Root = Path.Combine(DotGit.Root, "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; } } } public static class InstallationCapabilityFiles { public const string OnDiskVersion16CapableInstallation = "OnDiskVersion16CapableInstallation.dat"; } public static class VerbParameters { public const string InternalUseOnly = "internal_use_only"; public static class Mount { public const string StartedByService = "StartedByService"; public const string StartedByVerb = "StartedByVerb"; public const string Verbosity = "verbosity"; public const string Keywords = "keywords"; public const string DebugWindow = "debug-window"; public const string DefaultVerbosity = "Informational"; public const string DefaultKeywords = "Any"; } public static class Unmount { public const string SkipLock = "skip-wait-for-lock"; } } public static class UpgradeVerbMessages { public const string GVFSUpgrade = "`gvfs upgrade`"; public const string GVFSUpgradeDryRun = "`gvfs upgrade --dry-run`"; public const string NoUpgradeCheckPerformed = "No upgrade check was performed."; public const string NoneRingConsoleAlert = "Upgrade ring set to \"None\". " + NoUpgradeCheckPerformed; public const string NoRingConfigConsoleAlert = "Upgrade ring is not set. " + NoUpgradeCheckPerformed; public const string InvalidRingConsoleAlert = "Upgrade ring set to unknown value. " + NoUpgradeCheckPerformed; public const string SetUpgradeRingCommand = "To set or change upgrade ring, run `gvfs config " + LocalGVFSConfig.UpgradeRing + " [\"Fast\"|\"Slow\"|\"None\"]` from a command prompt."; public const string UnmountRepoWarning = "Upgrade will unmount and remount gvfs repos, ensure you are at a stopping point."; } } } ================================================ FILE: GVFS/GVFS.Common/GVFSContext.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Tracing; using System; namespace GVFS.Common { public class GVFSContext : IDisposable { private bool disposedValue = false; public GVFSContext(ITracer tracer, PhysicalFileSystem fileSystem, GitRepo repository, GVFSEnlistment enlistment) { this.Tracer = tracer; this.FileSystem = fileSystem; this.Enlistment = enlistment; this.Repository = repository; this.Unattended = GVFSEnlistment.IsUnattended(this.Tracer); } public ITracer Tracer { get; private set; } public PhysicalFileSystem FileSystem { get; private set; } public GitRepo Repository { get; private set; } public GVFSEnlistment Enlistment { get; private set; } public bool Unattended { get; private set; } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!this.disposedValue) { if (disposing) { this.Repository.Dispose(); this.Tracer.Dispose(); this.Tracer = null; } this.disposedValue = true; } } } } ================================================ FILE: GVFS/GVFS.Common/GVFSEnlistment.Shared.cs ================================================ using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; using System.Security; namespace GVFS.Common { public partial class GVFSEnlistment { public static bool IsUnattended(ITracer tracer) { try { return Environment.GetEnvironmentVariable(GVFSConstants.UnattendedEnvironmentVariable) == "1"; } catch (SecurityException e) { if (tracer != null) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", nameof(GVFSEnlistment)); metadata.Add("Exception", e.ToString()); tracer.RelatedError(metadata, "Unable to read environment variable " + GVFSConstants.UnattendedEnvironmentVariable); } return false; } } /// /// Returns true if is equal to or a subdirectory of /// (case-insensitive). Both paths are /// canonicalized with to resolve /// relative segments (e.g. "/../") before comparison. /// public static bool IsPathInsideDirectory(string path, string directory) { string normalizedPath = Path.GetFullPath(path) .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); string normalizedDirectory = Path.GetFullPath(directory) .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); return normalizedPath.StartsWith(normalizedDirectory + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) || normalizedPath.Equals(normalizedDirectory, StringComparison.OrdinalIgnoreCase); } /// /// Detects if the given directory is a git worktree by checking for /// a .git file (not directory) containing "gitdir: path/.git/worktrees/name". /// Returns a pipe name suffix like "_WT_NAME" if so, or null if not a worktree. /// public static string GetWorktreePipeSuffix(string directory) { WorktreeInfo info = TryGetWorktreeInfo(directory); return info?.PipeSuffix; } /// /// Detects if the given directory (or any ancestor) is a git worktree. /// Walks up from looking for a .git /// file (not directory) containing a gitdir: pointer. Returns /// null if not inside a worktree. /// public static WorktreeInfo TryGetWorktreeInfo(string directory) { return TryGetWorktreeInfo(directory, out _); } /// /// Detects if the given directory (or any ancestor) is a git worktree. /// Walks up from looking for a .git /// file (not directory) containing a gitdir: pointer. Returns /// null if not inside a worktree, with an error message if an I/O /// error prevented detection. /// public static WorktreeInfo TryGetWorktreeInfo(string directory, out string error) { error = null; if (string.IsNullOrEmpty(directory)) { return null; } // Canonicalize to an absolute path so walk-up and Path.Combine // behave consistently regardless of the caller's CWD. string current = Path.GetFullPath(directory); while (current != null) { string dotGitPath = Path.Combine(current, ".git"); if (Directory.Exists(dotGitPath)) { // Found a real .git directory — this is a primary worktree, not a linked worktree return null; } if (File.Exists(dotGitPath)) { return TryParseWorktreeGitFile(current, dotGitPath, out error); } string parent = Path.GetDirectoryName(current); if (parent == current) { break; } current = parent; } return null; } private static WorktreeInfo TryParseWorktreeGitFile(string worktreeRoot, string dotGitPath, out string error) { error = null; try { string gitdirLine = File.ReadAllText(dotGitPath).Trim(); if (!gitdirLine.StartsWith(GVFSConstants.DotGit.GitDirPrefix)) { return null; } string gitdirPath = gitdirLine.Substring(GVFSConstants.DotGit.GitDirPrefix.Length).Trim(); gitdirPath = gitdirPath.Replace('/', Path.DirectorySeparatorChar); // Resolve relative paths against the worktree directory if (!Path.IsPathRooted(gitdirPath)) { gitdirPath = Path.GetFullPath(Path.Combine(worktreeRoot, gitdirPath)); } string worktreeName = Path.GetFileName(gitdirPath); if (string.IsNullOrEmpty(worktreeName)) { return null; } // Read commondir to find the shared .git/ directory. // All valid worktrees must have a commondir file. string commondirFile = Path.Combine(gitdirPath, GVFSConstants.DotGit.CommonDirName); if (!File.Exists(commondirFile)) { return null; } string commondirContent = File.ReadAllText(commondirFile).Trim(); string sharedGitDir = Path.GetFullPath(Path.Combine(gitdirPath, commondirContent)); return new WorktreeInfo { Name = worktreeName, WorktreePath = worktreeRoot, WorktreeGitDir = gitdirPath, SharedGitDir = sharedGitDir, PipeSuffix = "_WT_" + worktreeName.ToUpper(), }; } catch (IOException e) { error = e.Message; return null; } catch (UnauthorizedAccessException e) { error = e.Message; return null; } } /// /// Returns the working directory paths of all worktrees registered /// under /worktrees by reading each entry's /// gitdir file. The primary worktree is not included. /// public static string[] GetKnownWorktreePaths(string gitDir) { string worktreesDir = Path.Combine(gitDir, "worktrees"); if (!Directory.Exists(worktreesDir)) { return new string[0]; } List paths = new List(); foreach (string entry in Directory.GetDirectories(worktreesDir)) { string gitdirFile = Path.Combine(entry, "gitdir"); if (!File.Exists(gitdirFile)) { continue; } try { string gitdirContent = File.ReadAllText(gitdirFile).Trim(); gitdirContent = gitdirContent.Replace('/', Path.DirectorySeparatorChar); string worktreeDir = Path.GetDirectoryName(gitdirContent); if (!string.IsNullOrEmpty(worktreeDir)) { paths.Add(Path.GetFullPath(worktreeDir)); } } catch { } } return paths.ToArray(); } public class WorktreeInfo { public const string EnlistmentRootFileName = "gvfs-enlistment-root"; public string Name { get; set; } public string WorktreePath { get; set; } public string WorktreeGitDir { get; set; } public string SharedGitDir { get; set; } public string PipeSuffix { get; set; } /// /// Returns the primary enlistment root, either from a stored /// marker file or by deriving it from SharedGitDir. /// public string GetEnlistmentRoot() { // Prefer the explicit marker written during worktree creation string markerPath = Path.Combine(this.WorktreeGitDir, EnlistmentRootFileName); if (File.Exists(markerPath)) { string root = File.ReadAllText(markerPath).Trim(); if (!string.IsNullOrEmpty(root)) { return root; } } // Fallback: derive from SharedGitDir (assumes /src/.git) if (this.SharedGitDir != null) { string srcDir = Path.GetDirectoryName(this.SharedGitDir); if (srcDir != null) { return Path.GetDirectoryName(srcDir); } } return null; } } } } ================================================ FILE: GVFS/GVFS.Common/GVFSEnlistment.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using Newtonsoft.Json; using System; using System.IO; using System.Threading; namespace GVFS.Common { public partial class GVFSEnlistment : Enlistment { public const string BlobSizesCacheName = "blobSizes"; private const string GitObjectCacheName = "gitObjects"; private string gitVersion; private string gvfsVersion; private string gvfsHooksVersion; // New enlistment public GVFSEnlistment(string enlistmentRoot, string repoUrl, string gitBinPath, GitAuthentication authentication) : base( enlistmentRoot, Path.Combine(enlistmentRoot, GVFSConstants.WorkingDirectoryRootName), Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.WorkingDirectoryBackingRootPath), repoUrl, gitBinPath, flushFileBuffersForPacks: true, authentication: authentication) { this.NamedPipeName = GVFSPlatform.Instance.GetNamedPipeName(this.EnlistmentRoot); this.DotGVFSRoot = Path.Combine(this.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); 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, GVFSPlatform.Instance.Constants.DotGVFSRoot, GVFSConstants.DotGVFS.LogName); this.LocalObjectsRoot = Path.Combine(this.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Objects.Root); } // Existing, configured enlistment private GVFSEnlistment(string enlistmentRoot, string gitBinPath, GitAuthentication authentication) : this( enlistmentRoot, null, gitBinPath, authentication) { } // Worktree enlistment — overrides working directory, pipe name, and metadata paths private GVFSEnlistment(string enlistmentRoot, string gitBinPath, GitAuthentication authentication, WorktreeInfo worktreeInfo, string repoUrl = null) : base( enlistmentRoot, worktreeInfo.WorktreePath, worktreeInfo.WorktreePath, repoUrl, gitBinPath, flushFileBuffersForPacks: true, authentication: authentication) { this.Worktree = worktreeInfo; // Override DotGitRoot to point to the shared .git directory. // The base constructor sets it to WorkingDirectoryBackingRoot/.git // which is a file (not directory) in worktrees. this.DotGitRoot = worktreeInfo.SharedGitDir; this.DotGVFSRoot = Path.Combine(worktreeInfo.WorktreeGitDir, GVFSPlatform.Instance.Constants.DotGVFSRoot); this.NamedPipeName = GVFSPlatform.Instance.GetNamedPipeName(enlistmentRoot) + worktreeInfo.PipeSuffix; 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.DotGVFSRoot, GVFSConstants.DotGVFS.LogName); this.LocalObjectsRoot = Path.Combine(worktreeInfo.SharedGitDir, "objects"); } public string NamedPipeName { get; } public string DotGVFSRoot { get; } public string GVFSLogsRoot { get; } public WorktreeInfo Worktree { get; } public bool IsWorktree => this.Worktree != null; /// /// Path to the git index file. For worktrees this is in the /// per-worktree git dir, not in the working directory. /// public override string GitIndexPath { get { if (this.IsWorktree) { return Path.Combine(this.Worktree.WorktreeGitDir, GVFSConstants.DotGit.IndexName); } return base.GitIndexPath; } } public string LocalCacheRoot { get; private set; } public string BlobSizesRoot { get; private set; } 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 { get { return this.gitVersion; } } public string GVFSVersion { get { return this.gvfsVersion; } } public string GVFSHooksVersion { get { return this.gvfsHooksVersion; } } public static GVFSEnlistment CreateFromDirectory( string directory, string gitBinRoot, GitAuthentication authentication, bool createWithoutRepoURL = false) { if (Directory.Exists(directory)) { // Always check for worktree first. A worktree directory may // be under the enlistment tree, so TryGetGVFSEnlistmentRoot // can succeed by walking up — but we need a worktree enlistment. string worktreeError; WorktreeInfo wtInfo = TryGetWorktreeInfo(directory, out worktreeError); if (worktreeError != null) { throw new InvalidRepoException($"Failed to check worktree status for '{directory}': {worktreeError}"); } if (wtInfo?.SharedGitDir != null) { string primaryRoot = wtInfo.GetEnlistmentRoot(); if (primaryRoot != null) { // Read origin URL via the shared .git dir (not the worktree's // .git file) because the base Enlistment constructor runs // git config before we can override DotGitRoot. string srcDir = Path.GetDirectoryName(wtInfo.SharedGitDir); string repoUrl = null; if (srcDir != null) { GitProcess git = new GitProcess(gitBinRoot, srcDir); GitProcess.ConfigResult urlResult = git.GetOriginUrl(); urlResult.TryParseAsString(out repoUrl, out _); } return CreateForWorktree(primaryRoot, gitBinRoot, authentication, wtInfo, repoUrl?.Trim()); } } string errorMessage; string enlistmentRoot; if (!GVFSPlatform.Instance.TryGetGVFSEnlistmentRoot(directory, out enlistmentRoot, out errorMessage)) { throw new InvalidRepoException($"Could not get enlistment root. Error: {errorMessage}"); } if (createWithoutRepoURL) { return new GVFSEnlistment(enlistmentRoot, string.Empty, gitBinRoot, authentication); } return new GVFSEnlistment(enlistmentRoot, gitBinRoot, authentication); } throw new InvalidRepoException($"Directory '{directory}' does not exist"); } /// /// Creates a GVFSEnlistment for a git worktree. Uses the primary /// enlistment root for shared config but maps working directory, /// metadata, and pipe name to the worktree. /// public static GVFSEnlistment CreateForWorktree( string primaryEnlistmentRoot, string gitBinRoot, GitAuthentication authentication, WorktreeInfo worktreeInfo, string repoUrl = null) { return new GVFSEnlistment(primaryEnlistmentRoot, gitBinRoot, authentication, worktreeInfo, repoUrl); } public static string GetNewGVFSLogFileName( string logsRoot, string logFileType, string logId = null, PhysicalFileSystem fileSystem = null) { return Enlistment.GetNewLogFileName( logsRoot, "gvfs_" + logFileType, logId: logId, fileSystem: fileSystem); } public static bool WaitUntilMounted(ITracer tracer, string enlistmentRoot, bool unattended, out string errorMessage) { string pipeName = GVFSPlatform.Instance.GetNamedPipeName(enlistmentRoot); return WaitUntilMounted(tracer, pipeName, enlistmentRoot, unattended, out errorMessage); } public static bool WaitUntilMounted(ITracer tracer, string pipeName, string enlistmentRoot, bool unattended, out string errorMessage) { tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Creating NamedPipeClient for pipe '{pipeName}'"); errorMessage = null; using (NamedPipeClient pipeClient = new NamedPipeClient(pipeName)) { tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Connecting to '{pipeName}'"); int timeout = unattended ? 300000 : 60000; if (!pipeClient.Connect(timeout)) { tracer.RelatedError($"{nameof(WaitUntilMounted)}: Failed to connect to '{pipeName}' after {timeout} ms"); errorMessage = "Unable to mount because the GVFS.Mount process is not responding."; return false; } tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Connected to '{pipeName}'"); while (true) { string response = string.Empty; try { pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); response = pipeClient.ReadRawResponse(); NamedPipeMessages.GetStatus.Response getStatusResponse = NamedPipeMessages.GetStatus.Response.FromJson(response); if (getStatusResponse.MountStatus == NamedPipeMessages.GetStatus.Ready) { tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Mount process ready"); return true; } else if (getStatusResponse.MountStatus == NamedPipeMessages.GetStatus.MountFailed) { errorMessage = string.Format("Failed to mount at {0}", enlistmentRoot); tracer.RelatedError($"{nameof(WaitUntilMounted)}: Mount failed: {errorMessage}"); return false; } else { tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Waiting 500ms for mount process to be ready"); Thread.Sleep(500); } } catch (BrokenPipeException e) { errorMessage = string.Format("Could not connect to GVFS.Mount: {0}", e); tracer.RelatedError($"{nameof(WaitUntilMounted)}: {errorMessage}"); return false; } catch (JsonReaderException e) { errorMessage = string.Format("Failed to parse response from GVFS.Mount.\n {0}", e); tracer.RelatedError($"{nameof(WaitUntilMounted)}: {errorMessage}"); return false; } } } } public void SetGitVersion(string gitVersion) { this.SetOnce(gitVersion, ref this.gitVersion); } public void SetGVFSVersion(string gvfsVersion) { this.SetOnce(gvfsVersion, ref this.gvfsVersion); } public void SetGVFSHooksVersion(string gvfsHooksVersion) { this.SetOnce(gvfsHooksVersion, ref this.gvfsHooksVersion); } public void InitializeCachePathsFromKey(string localCacheRoot, string localCacheKey) { this.InitializeCachePaths( localCacheRoot, Path.Combine(localCacheRoot, localCacheKey, GitObjectCacheName), Path.Combine(localCacheRoot, localCacheKey, BlobSizesCacheName)); } public void InitializeCachePaths(string localCacheRoot, string gitObjectsRoot, string blobSizesRoot) { this.LocalCacheRoot = localCacheRoot; this.GitObjectsRoot = gitObjectsRoot; this.GitPackRoot = Path.Combine(this.GitObjectsRoot, GVFSConstants.DotGit.Objects.Pack.Name); this.BlobSizesRoot = blobSizesRoot; } public bool TryCreateEnlistmentSubFolders() { try { GVFSPlatform.Instance.FileSystem.EnsureDirectoryIsOwnedByCurrentUser(this.WorkingDirectoryRoot); this.CreateHiddenDirectory(this.DotGVFSRoot); } catch (IOException) { return false; } return true; } public string GetMountId() { return this.GetId(GVFSConstants.GitConfig.MountId); } public string GetEnlistmentId() { return this.GetId(GVFSConstants.GitConfig.EnlistmentId); } private void SetOnce(T value, ref T valueToSet) { if (valueToSet != null) { throw new InvalidOperationException("Value already set."); } valueToSet = value; } /// /// Creates a hidden directory @ the given path. /// If directory already exists, hides it. /// /// Path to desired hidden directory private void CreateHiddenDirectory(string path) { DirectoryInfo dir = Directory.CreateDirectory(path); dir.Attributes = FileAttributes.Hidden; } private string GetId(string key) { GitProcess.ConfigResult configResult = this.CreateGitProcess().GetFromLocalConfig(key); string value; string error; configResult.TryParseAsString(out value, out error, defaultValue: string.Empty); return value.Trim(); } } } ================================================ FILE: GVFS/GVFS.Common/GVFSLock.Shared.cs ================================================ using GVFS.Common.NamedPipes; using System; using System.Diagnostics; using System.Threading; namespace GVFS.Common { // This file contains methods that are used by GVFS.Hooks (compiled both by GVFS.Common and GVFS.Hooks). public partial class GVFSLock { public static bool TryAcquireGVFSLockForProcess( bool unattended, NamedPipeClient pipeClient, string fullCommand, int pid, bool isElevated, bool isConsoleOutputRedirectedToFile, bool checkAvailabilityOnly, string gvfsEnlistmentRoot, string gitCommandSessionId, out string result) { NamedPipeMessages.LockRequest request = new NamedPipeMessages.LockRequest(pid, isElevated, checkAvailabilityOnly, fullCommand, gitCommandSessionId); NamedPipeMessages.Message requestMessage = request.CreateMessage(NamedPipeMessages.AcquireLock.AcquireRequest); pipeClient.SendRequest(requestMessage); NamedPipeMessages.AcquireLock.Response response = new NamedPipeMessages.AcquireLock.Response(pipeClient.ReadResponse()); string message = string.Empty; switch (response.Result) { case NamedPipeMessages.AcquireLock.AcceptResult: case NamedPipeMessages.AcquireLock.AvailableResult: return CheckAcceptResponse(response, checkAvailabilityOnly, out result); case NamedPipeMessages.AcquireLock.MountNotReadyResult: result = "GVFS has not finished initializing, please wait a few seconds and try again."; return false; case NamedPipeMessages.AcquireLock.UnmountInProgressResult: result = "GVFS is unmounting."; return false; case NamedPipeMessages.AcquireLock.DenyGVFSResult: message = response.DenyGVFSMessage; break; case NamedPipeMessages.AcquireLock.DenyGitResult: message = string.Format("Waiting for '{0}' to release the lock", response.ResponseData.ParsedCommand); break; default: result = "Error when acquiring the lock. Unrecognized response: " + response.CreateMessage(); return false; } Func waitForLock = () => { while (true) { Thread.Sleep(250); pipeClient.SendRequest(requestMessage); response = new NamedPipeMessages.AcquireLock.Response(pipeClient.ReadResponse()); switch (response.Result) { case NamedPipeMessages.AcquireLock.AcceptResult: case NamedPipeMessages.AcquireLock.AvailableResult: return CheckAcceptResponse(response, checkAvailabilityOnly, out _); case NamedPipeMessages.AcquireLock.UnmountInProgressResult: return false; default: break; } } }; bool isSuccessfulLockResult; if (unattended) { isSuccessfulLockResult = waitForLock(); } else { isSuccessfulLockResult = ConsoleHelper.ShowStatusWhileRunning( waitForLock, message, output: Console.Out, showSpinner: !isConsoleOutputRedirectedToFile, gvfsLogEnlistmentRoot: gvfsEnlistmentRoot); } result = null; return isSuccessfulLockResult; } public static void ReleaseGVFSLock( bool unattended, NamedPipeClient pipeClient, string fullCommand, int pid, bool isElevated, bool isConsoleOutputRedirectedToFile, Action responseHandler, string gvfsEnlistmentRoot, string waitingMessage = "", int spinnerDelay = 0) { NamedPipeMessages.LockRequest request = new NamedPipeMessages.LockRequest(pid, isElevated, checkAvailabilityOnly: false, parsedCommand: fullCommand, gitCommandSessionId: string.Empty); NamedPipeMessages.Message requestMessage = request.CreateMessage(NamedPipeMessages.ReleaseLock.Request); pipeClient.SendRequest(requestMessage); NamedPipeMessages.ReleaseLock.Response response = null; Func releaseLock = () => { response = new NamedPipeMessages.ReleaseLock.Response(pipeClient.ReadResponse()); responseHandler(response); return ConsoleHelper.ActionResult.Success; }; if (unattended || isConsoleOutputRedirectedToFile) { releaseLock(); } else { ConsoleHelper.ShowStatusWhileRunning( releaseLock, waitingMessage, output: Console.Out, showSpinner: true, gvfsLogEnlistmentRoot: gvfsEnlistmentRoot, initialDelayMs: spinnerDelay); } } private static bool CheckAcceptResponse(NamedPipeMessages.AcquireLock.Response response, bool checkAvailabilityOnly, out string message) { switch (response.Result) { case NamedPipeMessages.AcquireLock.AcceptResult: if (!checkAvailabilityOnly) { message = null; return true; } else { message = "Error when acquiring the lock. Unexpected response: " + response.CreateMessage(); return false; } case NamedPipeMessages.AcquireLock.AvailableResult: if (checkAvailabilityOnly) { message = null; return true; } else { message = "Error when acquiring the lock. Unexpected response: " + response.CreateMessage(); return false; } default: message = "Error when acquiring the lock. Not an Accept result: " + response.CreateMessage(); return false; } } } } ================================================ FILE: GVFS/GVFS.Common/GVFSLock.cs ================================================ using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using System; using System.Diagnostics; using System.Threading; namespace GVFS.Common { public partial class GVFSLock { private readonly object acquisitionLock = new object(); private readonly ITracer tracer; private readonly LockHolder currentLockHolder = new LockHolder(); public GVFSLock(ITracer tracer) { this.tracer = tracer; this.Stats = new ActiveGitCommandStats(); } public ActiveGitCommandStats Stats { get; private set; } /// /// Allows external callers (non-GVFS) to acquire the lock. /// /// The data for the external acquisition request. /// The current holder of the lock if the acquisition fails. /// True if the lock was acquired, false otherwise. public bool TryAcquireLockForExternalRequestor( NamedPipeMessages.LockData requestor, out NamedPipeMessages.LockData existingExternalHolder) { EventMetadata metadata = new EventMetadata(); EventLevel eventLevel = EventLevel.Verbose; metadata.Add("LockRequest", requestor.ToString()); metadata.Add("IsElevated", requestor.IsElevated); existingExternalHolder = null; try { lock (this.acquisitionLock) { if (this.currentLockHolder.IsGVFS) { metadata.Add("CurrentLockHolder", "GVFS"); metadata.Add("Result", "Denied"); return false; } existingExternalHolder = this.GetExternalHolder(); if (existingExternalHolder != null) { metadata.Add("CurrentLockHolder", existingExternalHolder.ToString()); metadata.Add("Result", "Denied"); return false; } metadata.Add("Result", "Accepted"); eventLevel = EventLevel.Informational; this.currentLockHolder.AcquireForExternalRequestor(requestor); this.Stats = new ActiveGitCommandStats(); return true; } } finally { this.tracer.RelatedEvent(eventLevel, "TryAcquireLockExternal", metadata); } } /// /// Allow GVFS to acquire the lock. /// /// True if GVFS was able to acquire the lock or if it already held it. False othwerwise. public bool TryAcquireLockForGVFS() { EventMetadata metadata = new EventMetadata(); try { lock (this.acquisitionLock) { if (this.currentLockHolder.IsGVFS) { return true; } NamedPipeMessages.LockData existingExternalHolder = this.GetExternalHolder(); if (existingExternalHolder != null) { metadata.Add("CurrentLockHolder", existingExternalHolder.ToString()); metadata.Add("Result", "Denied"); return false; } this.currentLockHolder.AcquireForGVFS(); metadata.Add("Result", "Accepted"); return true; } } finally { this.tracer.RelatedEvent(EventLevel.Verbose, "TryAcquireLockInternal", metadata); } } public void ReleaseLockHeldByGVFS() { lock (this.acquisitionLock) { if (!this.currentLockHolder.IsGVFS) { throw new InvalidOperationException("Cannot release lock that is not held by GVFS"); } this.tracer.RelatedEvent(EventLevel.Verbose, nameof(this.ReleaseLockHeldByGVFS), new EventMetadata()); this.currentLockHolder.Release(); } } public bool ReleaseLockHeldByExternalProcess(int pid) { return this.ReleaseExternalLock(pid, nameof(this.ReleaseLockHeldByExternalProcess)); } public NamedPipeMessages.LockData GetExternalHolder() { NamedPipeMessages.LockData externalHolder; this.IsLockAvailable(checkExternalHolderOnly: true, existingExternalHolder: out externalHolder); return externalHolder; } public bool IsLockAvailableForExternalRequestor(out NamedPipeMessages.LockData existingExternalHolder) { return this.IsLockAvailable(checkExternalHolderOnly: false, existingExternalHolder: out existingExternalHolder); } public string GetLockedGitCommand() { // In this code path, we don't care if the process terminated without releasing the lock. The calling code // is asking us about this lock so that it can determine if git was the cause of certain IO events. Even // if the git process has terminated, the answer to that question does not change. NamedPipeMessages.LockData currentHolder = this.currentLockHolder.GetExternalHolder(); if (currentHolder != null) { return currentHolder.ParsedCommand; } return null; } public string GetStatus() { lock (this.acquisitionLock) { if (this.currentLockHolder.IsGVFS) { return "Held by GVFS."; } NamedPipeMessages.LockData externalHolder = this.GetExternalHolder(); if (externalHolder != null) { return string.Format("Held by {0} (PID:{1})", externalHolder.ParsedCommand, externalHolder.PID); } } return "Free"; } private bool IsLockAvailable(bool checkExternalHolderOnly, out NamedPipeMessages.LockData existingExternalHolder) { lock (this.acquisitionLock) { if (!checkExternalHolderOnly && this.currentLockHolder.IsGVFS) { existingExternalHolder = null; return false; } bool externalHolderTerminatedWithoutReleasingLock; existingExternalHolder = this.currentLockHolder.GetExternalHolder( out externalHolderTerminatedWithoutReleasingLock); if (externalHolderTerminatedWithoutReleasingLock) { this.ReleaseLockForTerminatedProcess(existingExternalHolder.PID); this.tracer.SetGitCommandSessionId(string.Empty); existingExternalHolder = null; } return existingExternalHolder == null; } } private bool ReleaseExternalLock(int pid, string eventName) { lock (this.acquisitionLock) { EventMetadata metadata = new EventMetadata(); try { if (this.currentLockHolder.IsGVFS) { metadata.Add("IsLockedByGVFS", "true"); return false; } // We don't care if the process has already terminated. We're just trying to record the info for the last holder. NamedPipeMessages.LockData previousExternalHolder = this.currentLockHolder.GetExternalHolder(); if (previousExternalHolder == null) { metadata.Add("Result", "Failed (no current holder, requested PID=" + pid + ")"); return false; } metadata.Add("CurrentLockHolder", previousExternalHolder.ToString()); metadata.Add("IsElevated", previousExternalHolder.IsElevated); metadata.Add(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId); if (previousExternalHolder.PID != pid) { metadata.Add("pid", pid); metadata.Add("Result", "Failed (wrong PID)"); return false; } this.currentLockHolder.Release(); metadata.Add("Result", "Released"); this.Stats.AddStatsToTelemetry(metadata); return true; } finally { this.tracer.RelatedEvent(EventLevel.Informational, eventName, metadata, Keywords.Telemetry); } } } private void ReleaseLockForTerminatedProcess(int pid) { this.ReleaseExternalLock(pid, "ExternalLockHolderExited"); } // The lock release event is a convenient place to record stats about things that happened while a git command was running, // such as duration/count of object downloads during a git command, cache hits during a git command, etc. public class ActiveGitCommandStats { private Stopwatch lockAcquiredTime; private long lockHeldExternallyTimeMs; private long placeholderTotalUpdateTimeMs; private long placeholderUpdateFilesTimeMs; private long placeholderUpdateFoldersTimeMs; private long placeholderWriteAndFlushTimeMs; private int deleteFolderPlacehoderAttempted; private int folderPlaceholdersDeleted; private int folderPlaceholdersPathNotFound; private int folderPlaceholdersShaUpdate; private long parseGitIndexTimeMs; private long projectionWriteLockHeldMs; private int numBlobs; private long blobDownloadTimeMs; private int numCommitsAndTrees; private long commitAndTreeDownloadTimeMs; private int numSizeQueries; private long sizeQueryTimeMs; public ActiveGitCommandStats() { this.lockAcquiredTime = Stopwatch.StartNew(); } public void RecordReleaseExternalLockRequested() { this.lockHeldExternallyTimeMs = this.lockAcquiredTime.ElapsedMilliseconds; } public void RecordUpdatePlaceholders( long durationMs, long updateFilesMs, long updateFoldersMs, long writeAndFlushMs, int deleteFolderPlacehoderAttempted, int folderPlaceholdersDeleted, int folderPlaceholdersPathNotFound, int folderPlaceholdersShaUpdate) { this.placeholderTotalUpdateTimeMs = durationMs; this.placeholderUpdateFilesTimeMs = updateFilesMs; this.placeholderUpdateFoldersTimeMs = updateFoldersMs; this.placeholderWriteAndFlushTimeMs = writeAndFlushMs; this.deleteFolderPlacehoderAttempted = deleteFolderPlacehoderAttempted; this.folderPlaceholdersDeleted = folderPlaceholdersDeleted; this.folderPlaceholdersPathNotFound = folderPlaceholdersPathNotFound; this.folderPlaceholdersShaUpdate = folderPlaceholdersShaUpdate; } public void RecordProjectionWriteLockHeld(long durationMs) { this.projectionWriteLockHeldMs = durationMs; } public void RecordParseGitIndex(long durationMs) { this.parseGitIndexTimeMs = durationMs; } public void RecordObjectDownload(bool isBlob, long downloadTimeMs) { if (isBlob) { Interlocked.Increment(ref this.numBlobs); Interlocked.Add(ref this.blobDownloadTimeMs, downloadTimeMs); } else { Interlocked.Increment(ref this.numCommitsAndTrees); Interlocked.Add(ref this.commitAndTreeDownloadTimeMs, downloadTimeMs); } } public void RecordSizeQuery(long queryTimeMs) { Interlocked.Increment(ref this.numSizeQueries); Interlocked.Add(ref this.sizeQueryTimeMs, queryTimeMs); } public void AddStatsToTelemetry(EventMetadata metadata) { metadata.Add("DurationMS", this.lockAcquiredTime.ElapsedMilliseconds); metadata.Add("LockHeldExternallyMS", this.lockHeldExternallyTimeMs); metadata.Add("ParseGitIndexMS", this.parseGitIndexTimeMs); metadata.Add("UpdatePlaceholdersMS", this.placeholderTotalUpdateTimeMs); metadata.Add("UpdateFilePlaceholdersMS", this.placeholderUpdateFilesTimeMs); metadata.Add("UpdateFolderPlaceholdersMS", this.placeholderUpdateFoldersTimeMs); metadata.Add("DeleteFolderPlacehoderAttempted", this.deleteFolderPlacehoderAttempted); metadata.Add("FolderPlaceholdersDeleted", this.folderPlaceholdersDeleted); metadata.Add("FolderPlaceholdersShaUpdate", this.folderPlaceholdersShaUpdate); metadata.Add("FolderPlaceholdersPathNotFound", this.folderPlaceholdersPathNotFound); metadata.Add("PlaceholdersWriteAndFlushMS", this.placeholderWriteAndFlushTimeMs); metadata.Add("ProjectionWriteLockHeldMs", this.projectionWriteLockHeldMs); metadata.Add("BlobsDownloaded", this.numBlobs); metadata.Add("BlobDownloadTimeMS", this.blobDownloadTimeMs); metadata.Add("CommitsAndTreesDownloaded", this.numCommitsAndTrees); metadata.Add("CommitsAndTreesDownloadTimeMS", this.commitAndTreeDownloadTimeMs); metadata.Add("SizeQueries", this.numSizeQueries); metadata.Add("SizeQueryTimeMS", this.sizeQueryTimeMs); } } /// /// This class manages the state of which process currently owns the GVFS lock. This code is complicated because /// the lock can be held by us or by an external process, and because the external process that holds the lock /// can terminate without releasing the lock. If that happens, we implicitly release the lock the next time we /// check to see who is holding it. /// /// The goal of this class is to make it impossible for the rest of GVFSLock to read the external holder without being /// aware of the fact that it could have terminated. /// /// This class assumes that the caller is handling all synchronization. /// private class LockHolder { private NamedPipeMessages.LockData externalLockHolder; public bool IsFree { get { return !this.IsGVFS && this.externalLockHolder == null; } } public bool IsGVFS { get; private set; } public void AcquireForGVFS() { if (this.externalLockHolder != null) { throw new InvalidOperationException("Cannot acquire for GVFS because there is an external holder"); } this.IsGVFS = true; } public void AcquireForExternalRequestor(NamedPipeMessages.LockData externalLockHolder) { if (this.IsGVFS || this.externalLockHolder != null) { throw new InvalidOperationException("Cannot acquire a lock that is already held"); } this.externalLockHolder = externalLockHolder; } public void Release() { this.IsGVFS = false; this.externalLockHolder = null; } public NamedPipeMessages.LockData GetExternalHolder() { return this.externalLockHolder; } public NamedPipeMessages.LockData GetExternalHolder(out bool externalHolderTerminatedWithoutReleasingLock) { externalHolderTerminatedWithoutReleasingLock = false; if (this.externalLockHolder != null) { int pid = this.externalLockHolder.PID; externalHolderTerminatedWithoutReleasingLock = !GVFSPlatform.Instance.IsProcessActive(pid); } return this.externalLockHolder; } } } } ================================================ FILE: GVFS/GVFS.Common/GVFSPlatform.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.IO.Pipes; namespace GVFS.Common { public abstract class GVFSPlatform { public GVFSPlatform(UnderConstructionFlags underConstruction) { this.UnderConstruction = underConstruction; } public static GVFSPlatform Instance { get; private set; } public abstract IKernelDriver KernelDriver { get; } public abstract IGitInstallation GitInstallation { get; } public abstract IDiskLayoutUpgradeData DiskLayoutUpgrade { get; } public abstract IPlatformFileSystem FileSystem { get; } public abstract GVFSPlatformConstants Constants { get; } public UnderConstructionFlags UnderConstruction { get; } public abstract string Name { get; } public abstract string GVFSConfigPath { get; } /// /// Returns true if the platform keeps a system-wide installer log. /// public abstract bool SupportsSystemInstallLog { get; } public static void Register(GVFSPlatform platform) { if (GVFSPlatform.Instance != null) { throw new InvalidOperationException("Cannot register more than one platform"); } GVFSPlatform.Instance = platform; } /// /// Starts a VFS for Git process in the background. /// /// /// This method should only be called by processes whose code we own as the background process must /// do some extra work after it starts. /// public abstract void StartBackgroundVFS4GProcess(ITracer tracer, string programName, string[] args); /// /// Adjusts the current process for running in the background. /// /// /// This method should be called after starting by processes launched using /// /// /// Failed to prepare process to run in background. /// public abstract void PrepareProcessToRunInBackground(); public abstract bool IsProcessActive(int processId); public abstract void IsServiceInstalledAndRunning(string name, out bool installed, out bool running); public abstract string GetNamedPipeName(string enlistmentRoot); public abstract string GetGVFSServiceNamedPipeName(string serviceName); public abstract NamedPipeServerStream CreatePipeByName(string pipeName); public abstract string GetOSVersionInformation(); public abstract string GetSecureDataRootForGVFS(); public abstract string GetSecureDataRootForGVFSComponent(string componentName); public abstract string GetCommonAppDataRootForGVFS(); public abstract string GetLogsDirectoryForGVFSComponent(string componentName); public abstract bool IsElevated(); public abstract string GetCurrentUser(); public abstract string GetUserIdFromLoginSessionId(int sessionId, ITracer tracer); public abstract string GetSystemInstallerLogPath(); public abstract void ConfigureVisualStudio(string gitBinPath, ITracer tracer); public abstract bool TryCopyPanicLogs(string copyToDir, out string error); public abstract bool TryGetGVFSHooksVersion(out string hooksVersion, out string error); public abstract bool TryInstallGitCommandHooks(GVFSContext context, string executingDirectory, string hookName, string commandHookPath, out string errorMessage); public abstract Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly); public abstract bool IsConsoleOutputRedirectedToFile(); public abstract bool TryKillProcessTree(int processId, out int exitCode, out string error); public abstract bool TryGetGVFSEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage); public abstract bool TryGetDefaultLocalCacheRoot(string enlistmentRoot, out string localCacheRoot, out string localCacheRootError); public abstract bool IsGitStatusCacheSupported(); public abstract FileBasedLock CreateFileBasedLock( PhysicalFileSystem fileSystem, ITracer tracer, string lockPath); public bool TryGetNormalizedPathRoot(string path, out string pathRoot, out string errorMessage) { pathRoot = null; errorMessage = null; string normalizedPath = null; if (!this.FileSystem.TryGetNormalizedPath(path, out normalizedPath, out errorMessage)) { return false; } pathRoot = Path.GetPathRoot(normalizedPath); return true; } public abstract class GVFSPlatformConstants { public static readonly char PathSeparator = Path.DirectorySeparatorChar; public abstract int MaxPipePathLength { get; } public abstract string ExecutableExtension { get; } public abstract string InstallerExtension { get; } /// /// Indicates whether the platform supports running the upgrade application while /// the upgrade verb is running. /// public abstract bool SupportsUpgradeWhileRunning { get; } public abstract string UpgradeInstallAdviceMessage { get; } public abstract string RunUpdateMessage { get; } public abstract string UpgradeConfirmCommandMessage { get; } public abstract string StartServiceCommandMessage { get; } public abstract string WorkingDirectoryBackingRootPath { get; } public abstract string DotGVFSRoot { get; } public abstract string GVFSBinDirectoryPath { get; } public abstract string GVFSBinDirectoryName { get; } public abstract string GVFSExecutableName { get; } /// /// Different platforms can have different requirements /// around which processes can block upgrade. For example, /// on Windows, we will block upgrade if any GVFS commands /// are running, but on POSIX platforms, we relax this /// constraint to allow upgrade to run while the upgrade /// command is running. Another example is that /// Non-windows platforms do not block upgrade when bash /// is running. /// public abstract HashSet UpgradeBlockingProcesses { get; } public abstract bool CaseSensitiveFileSystem { get; } public StringComparison PathComparison { get { return this.CaseSensitiveFileSystem ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; } } public StringComparer PathComparer { get { return this.CaseSensitiveFileSystem ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; } } public string GVFSHooksExecutableName { get { return "GVFS.Hooks" + this.ExecutableExtension; } } public string GVFSReadObjectHookExecutableName { get { return "GVFS.ReadObjectHook" + this.ExecutableExtension; } } public string GVFSVirtualFileSystemHookExecutableName { get { return "GVFS.VirtualFileSystemHook" + this.ExecutableExtension; } } public string GVFSPostIndexChangedHookExecutableName { get { return "GVFS.PostIndexChangedHook" + this.ExecutableExtension; } } public string MountExecutableName { get { return "GVFS.Mount" + this.ExecutableExtension; } } } public class UnderConstructionFlags { public UnderConstructionFlags( bool supportsGVFSUpgrade = true, bool supportsGVFSConfig = true, bool supportsNuGetEncryption = true, bool supportsNuGetVerification = true) { this.SupportsGVFSUpgrade = supportsGVFSUpgrade; this.SupportsGVFSConfig = supportsGVFSConfig; this.SupportsNuGetEncryption = supportsNuGetEncryption; this.SupportsNuGetVerification = supportsNuGetVerification; } public bool SupportsGVFSUpgrade { get; } public bool SupportsGVFSConfig { get; } public bool SupportsNuGetEncryption { get; } public bool SupportsNuGetVerification { get; } } } } ================================================ FILE: GVFS/GVFS.Common/Git/DiffTreeResult.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace GVFS.Common.Git { public class DiffTreeResult { public const string TreeMarker = "tree "; public const string BlobMarker = "blob "; public const int TypeMarkerStartIndex = 7; private const ushort SymLinkFileIndexEntry = 0xA000; private static readonly HashSet ValidTreeModes = new HashSet() { "040000" }; public enum Operations { Unknown, CopyEdit, RenameEdit, Modify, Delete, Add, Unmerged, TypeChange, } public Operations Operation { get; set; } public bool SourceIsDirectory { get; set; } public bool TargetIsDirectory { get; set; } public bool TargetIsSymLink { get; set; } public string TargetPath { get; set; } public string SourceSha { get; set; } public string TargetSha { get; set; } public ushort SourceMode { get; set; } public ushort TargetMode { get; set; } public static DiffTreeResult ParseFromDiffTreeLine(string line) { if (string.IsNullOrEmpty(line)) { throw new ArgumentException("Line to parse cannot be null or empty", nameof(line)); } /* * The lines passed to this method should be the result of a call to git diff-tree -r -t (sourceTreeish) (targetTreeish) * * Example output lines from git diff-tree * :000000 040000 0000000000000000000000000000000000000000 cee82f9d431bf610404f67bcdda3fee76f0c1dd5 A\tGVFS/FastFetch/Git * :000000 100644 0000000000000000000000000000000000000000 cdc036f9d561f14d908e0a0c337105b53c778e5e A\tGVFS/FastFetch/Git/FastFetchGitObjects.cs * :040000 000000 f68b90da732791438d67c0326997a2d26e4c2de4 0000000000000000000000000000000000000000 D\tGVFS/GVFS.CLI * :100644 000000 1242fc97c612ff286a5f1221d569508600ca5e06 0000000000000000000000000000000000000000 D\tGVFS/GVFS.CLI/GVFS.CLI.csproj * :040000 040000 3823348f91113a619eed8f48fe597cc9c7d088d8 fd56ff77b12a0b76567cb55ed4950272eac8b8f6 M\tGVFS/GVFS.Common * :100644 100644 57d9c737c8a48632cfbb12cae00c97d512b9f155 524d7dbcebd33e4007c52711d3f21b17373de454 M\tGVFS/GVFS.Common/GVFS.Common.csproj * ^-[0] ^-[1] ^-[2] ^-[3] ^-[4] * ^-tab * ^-[5] * * This output will only happen if -C or -M is passed to the diff-tree command * Since we are not passing those options we shouldn't have to handle this format. * :100644 100644 3ac7d60a25bb772af1d5843c76e8a070c062dc5d c31a95125b8a6efd401488839a7ed1288ce01634 R094\tGVFS/GVFS.CLI/CommandLine/CloneVerb.cs\tGVFS/GVFS/CommandLine/CloneVerb.cs */ if (!line.StartsWith(":")) { throw new ArgumentException($"diff-tree lines should start with a :", nameof(line)); } // Skip the colon at the front line = line.Substring(1); // Filenames may contain spaces, but always follow a \t. Other fields are space delimited. // Splitting on \t will give us the mode, sha, operation in parts[0] and that path in parts[1] and optionally in paths[2] string[] parts = line.Split(new[] { '\t' }, count: 2); // Take the mode, sha, operation part and split on a space then add the paths that were split on a tab to the end parts = parts[0].Split(' ').Concat(parts.Skip(1)).ToArray(); if (parts.Length != 6 || parts[5].Contains('\t')) { // Look at file history to see how -C -M with 7 parts could be handled throw new ArgumentException($"diff-tree lines should have 6 parts unless passed -C or -M which this method doesn't handle", nameof(line)); } DiffTreeResult result = new DiffTreeResult(); result.SourceIsDirectory = ValidTreeModes.Contains(parts[0]); result.TargetIsDirectory = ValidTreeModes.Contains(parts[1]); result.SourceMode = Convert.ToUInt16(parts[0], 8); result.TargetMode = Convert.ToUInt16(parts[1], 8); if (!result.TargetIsDirectory) { result.TargetIsSymLink = result.TargetMode == SymLinkFileIndexEntry; } result.SourceSha = parts[2]; result.TargetSha = parts[3]; result.Operation = DiffTreeResult.ParseOperation(parts[4]); result.TargetPath = ConvertPathToUtf8Path(parts[5]); if (result.TargetIsDirectory || result.SourceIsDirectory) { // Since diff-tree is not doing rename detection, file->directory or directory->file transformations are always multiple lines // with a delete line and an add line // :000000 040000 0000000000000000000000000000000000000000 cee82f9d431bf610404f67bcdda3fee76f0c1dd5 A\tGVFS/FastFetch/Git // :040000 040000 3823348f91113a619eed8f48fe597cc9c7d088d8 fd56ff77b12a0b76567cb55ed4950272eac8b8f6 M\tGVFS/GVFS.Common // :040000 000000 f68b90da732791438d67c0326997a2d26e4c2de4 0000000000000000000000000000000000000000 D\tGVFS/GVFS.CLI result.TargetPath = AppendPathSeparatorIfNeeded(result.TargetPath); } return result; } /// /// Parse the output of calling git ls-tree /// /// A line that was output from calling git ls-tree /// A DiffTreeResult build from the output line /// /// The call to ls-tree could be any of the following /// git ls-tree (treeish) /// git ls-tree -r (treeish) /// git ls-tree -t (treeish) /// git ls-tree -r -t (treeish) /// public static DiffTreeResult ParseFromLsTreeLine(string line) { if (string.IsNullOrEmpty(line)) { throw new ArgumentException("Line to parse cannot be null or empty", nameof(line)); } /* * Example output lines from ls-tree * * 040000 tree 73b881d52b607b0f3e9e620d36f556d3d233a11d\tGVFS * 100644 blob 44c5f5cba4b29d31c2ad06eed51ea02af76c27c0\tReadme.md * 100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts/BuildGVFSForMac.sh * ^-mode ^-marker ^-tab * ^-sha ^-path */ // Everything from ls-tree is an add. if (IsLsTreeLineOfType(line, TreeMarker)) { DiffTreeResult treeAdd = new DiffTreeResult(); treeAdd.TargetIsDirectory = true; treeAdd.TargetPath = AppendPathSeparatorIfNeeded(ConvertPathToUtf8Path(line.Substring(line.LastIndexOf("\t") + 1))); treeAdd.Operation = DiffTreeResult.Operations.Add; return treeAdd; } else { if (IsLsTreeLineOfType(line, BlobMarker)) { DiffTreeResult blobAdd = new DiffTreeResult(); blobAdd.TargetMode = Convert.ToUInt16(line.Substring(0, 6), 8); blobAdd.TargetIsSymLink = blobAdd.TargetMode == SymLinkFileIndexEntry; blobAdd.TargetSha = line.Substring(TypeMarkerStartIndex + BlobMarker.Length, GVFSConstants.ShaStringLength); blobAdd.TargetPath = ConvertPathToUtf8Path(line.Substring(line.LastIndexOf("\t") + 1)); blobAdd.Operation = DiffTreeResult.Operations.Add; return blobAdd; } else { return null; } } } public static bool IsLsTreeLineOfType(string line, string typeMarker) { if (line.Length <= TypeMarkerStartIndex + typeMarker.Length) { return false; } return line.IndexOf(typeMarker, TypeMarkerStartIndex, typeMarker.Length, StringComparison.OrdinalIgnoreCase) == TypeMarkerStartIndex; } private static string AppendPathSeparatorIfNeeded(string path) { return path.Last() == Path.DirectorySeparatorChar ? path : path + Path.DirectorySeparatorChar; } private static Operations ParseOperation(string gitOperationString) { switch (gitOperationString) { case "U": return Operations.Unmerged; case "M": return Operations.Modify; case "A": return Operations.Add; case "D": return Operations.Delete; case "X": return Operations.Unknown; case "T": return Operations.TypeChange; default: if (gitOperationString.StartsWith("C")) { return Operations.CopyEdit; } else if (gitOperationString.StartsWith("R")) { return Operations.RenameEdit; } throw new InvalidDataException("Unrecognized diff-tree operation: " + gitOperationString); } } private static string ConvertPathToUtf8Path(string relativePath) { return GitPathConverter.ConvertPathOctetsToUtf8(relativePath.Trim('"')).Replace(GVFSConstants.GitPathSeparator, Path.DirectorySeparatorChar); } } } ================================================ FILE: GVFS/GVFS.Common/Git/EndianHelper.cs ================================================ namespace GVFS.Common.Git { public static class EndianHelper { public static short Swap(short source) { return (short)Swap((ushort)source); } public static int Swap(int source) { return (int)Swap((uint)source); } public static long Swap(long source) { return (long)((ulong)source); } public static ushort Swap(ushort source) { return (ushort)(((source & 0x000000FF) << 8) | ((source & 0x0000FF00) >> 8)); } public static uint Swap(uint source) { return ((source & 0x000000FF) << 24) | ((source & 0x0000FF00) << 8) | ((source & 0x00FF0000) >> 8) | ((source & 0xFF000000) >> 24); } public static ulong Swap(ulong source) { return ((source & 0x00000000000000FF) << 56) | ((source & 0x000000000000FF00) << 40) | ((source & 0x0000000000FF0000) << 24) | ((source & 0x00000000FF000000) << 8) | ((source & 0x000000FF00000000) >> 8) | ((source & 0x0000FF0000000000) >> 24) | ((source & 0x00FF000000000000) >> 40) | ((source & 0xFF00000000000000) >> 56); } } } ================================================ FILE: GVFS/GVFS.Common/Git/GVFSGitObjects.cs ================================================ using GVFS.Common.Http; using GVFS.Common.Tracing; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; using System.Threading; namespace GVFS.Common.Git { public class GVFSGitObjects : GitObjects { private static readonly TimeSpan NegativeCacheTTL = TimeSpan.FromSeconds(30); private ConcurrentDictionary objectNegativeCache; internal ConcurrentDictionary> inflightDownloads; public GVFSGitObjects(GVFSContext context, GitObjectsHttpRequestor objectRequestor) : base(context.Tracer, context.Enlistment, objectRequestor, context.FileSystem) { this.Context = context; this.objectNegativeCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); this.inflightDownloads = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); } public enum RequestSource { Invalid = 0, FileStreamCallback, GVFSVerb, NamedPipeMessage, SymLinkCreation, } protected GVFSContext Context { get; private set; } public virtual bool TryCopyBlobContentStream( string sha, CancellationToken cancellationToken, RequestSource requestSource, Action writeAction) { RetryWrapper retrier = new RetryWrapper(this.GitObjectRequestor.RetryConfig.MaxAttempts, cancellationToken); retrier.OnFailure += errorArgs => { EventMetadata metadata = new EventMetadata(); metadata.Add("sha", sha); metadata.Add("AttemptNumber", errorArgs.TryCount); metadata.Add("WillRetry", errorArgs.WillRetry); if (errorArgs.Error != null) { metadata.Add("Exception", errorArgs.Error.ToString()); } string message = "TryCopyBlobContentStream: Failed to provide blob contents"; if (errorArgs.WillRetry) { this.Tracer.RelatedWarning(metadata, message, Keywords.Telemetry); } else { this.Tracer.RelatedError(metadata, message); } }; RetryWrapper.InvocationResult invokeResult = retrier.Invoke( tryCount => { bool success = this.Context.Repository.TryCopyBlobContentStream(sha, writeAction); if (success) { return new RetryWrapper.CallbackResult(true); } else { // Pass in false for retryOnFailure because the retrier in this method manages multiple attempts if (this.TryDownloadAndSaveObject(sha, cancellationToken, requestSource, retryOnFailure: false) == DownloadAndSaveObjectResult.Success) { if (this.Context.Repository.TryCopyBlobContentStream(sha, writeAction)) { return new RetryWrapper.CallbackResult(true); } } return new RetryWrapper.CallbackResult(error: null, shouldRetry: true); } }); return invokeResult.Result; } public DownloadAndSaveObjectResult TryDownloadAndSaveObject(string objectId, RequestSource requestSource) { return this.TryDownloadAndSaveObject(objectId, CancellationToken.None, requestSource, retryOnFailure: true); } public bool TryGetBlobSizeLocally(string sha, out long length) { return this.Context.Repository.TryGetBlobLength(sha, out length); } public List GetFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) { return this.GitObjectRequestor.QueryForFileSizes(objectIds, cancellationToken); } private DownloadAndSaveObjectResult TryDownloadAndSaveObject( string objectId, CancellationToken cancellationToken, RequestSource requestSource, bool retryOnFailure) { if (objectId == GVFSConstants.AllZeroSha) { return DownloadAndSaveObjectResult.Error; } DateTime negativeCacheRequestTime; if (this.objectNegativeCache.TryGetValue(objectId, out negativeCacheRequestTime)) { if (negativeCacheRequestTime > DateTime.Now.Subtract(NegativeCacheTTL)) { return DownloadAndSaveObjectResult.ObjectNotOnServer; } this.objectNegativeCache.TryRemove(objectId, out negativeCacheRequestTime); } // Coalesce concurrent requests for the same objectId so that only one HTTP // download runs per SHA at a time. All concurrent callers share the result. // Note: the first caller's cancellationToken and retryOnFailure settings are // captured by the Lazy factory. Subsequent coalesced callers inherit those // settings. In practice this is fine because the primary concurrent path // (NamedPipeMessage from git.exe) always uses CancellationToken.None. Lazy newLazy = new Lazy( () => this.DoDownloadAndSaveObject(objectId, cancellationToken, requestSource, retryOnFailure)); Lazy lazy = this.inflightDownloads.GetOrAdd(objectId, newLazy); if (!ReferenceEquals(lazy, newLazy)) { EventMetadata metadata = new EventMetadata(); metadata.Add("objectId", objectId); metadata.Add("requestSource", requestSource.ToString()); this.Context.Tracer.RelatedEvent(EventLevel.Informational, "TryDownloadAndSaveObject_CoalescedRequest", metadata); } try { return lazy.Value; } finally { this.TryRemoveInflightDownload(objectId, lazy); } } /// /// Removes the inflight download entry only if the current value matches the /// expected Lazy instance. This prevents an ABA race where a straggling thread's /// finally block could remove a newer Lazy created by a later wave of requests. /// Uses ICollection<KVP>.Remove which is the value-aware atomic removal on /// .NET Framework 4.7.1. When we upgrade to .NET 10 (backlog), this can be /// replaced with ConcurrentDictionary.TryRemove(KeyValuePair). /// private bool TryRemoveInflightDownload(string objectId, Lazy lazy) { return ((ICollection>>)this.inflightDownloads) .Remove(new KeyValuePair>(objectId, lazy)); } private DownloadAndSaveObjectResult DoDownloadAndSaveObject( string objectId, CancellationToken cancellationToken, RequestSource requestSource, bool retryOnFailure) { // To reduce allocations, reuse the same buffer when writing objects in this batch byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; RetryWrapper.InvocationResult output = this.GitObjectRequestor.TryDownloadLooseObject( objectId, retryOnFailure, cancellationToken, requestSource.ToString(), onSuccess: (tryCount, response) => { // If the request is from git.exe (i.e. NamedPipeMessage) then we should assume that if there is an // object on disk it's corrupt somehow (which is why git is asking for it) this.WriteLooseObject( response.Stream, objectId, overwriteExistingObject: requestSource == RequestSource.NamedPipeMessage, bufToCopyWith: bufToCopyWith); return new RetryWrapper.CallbackResult(new GitObjectsHttpRequestor.GitObjectTaskResult(true)); }); if (output.Result != null) { if (output.Succeeded && output.Result.Success) { return DownloadAndSaveObjectResult.Success; } if (output.Result.HttpStatusCodeResult == HttpStatusCode.NotFound) { this.objectNegativeCache.AddOrUpdate(objectId, DateTime.Now, (unused1, unused2) => DateTime.Now); return DownloadAndSaveObjectResult.ObjectNotOnServer; } } return DownloadAndSaveObjectResult.Error; } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitAuthentication.cs ================================================ using GVFS.Common.Http; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Security.Cryptography.X509Certificates; using System.Text; namespace GVFS.Common.Git { public class GitAuthentication { private const double MaxBackoffSeconds = 30; private readonly object gitAuthLock = new object(); private readonly ICredentialStore credentialStore; private readonly string repoUrl; private int numberOfAttempts = 0; private DateTime lastAuthAttempt = DateTime.MinValue; private string cachedCredentialString; private bool isCachedCredentialStringApproved = false; private bool isInitialized; public GitAuthentication(GitProcess git, string repoUrl) { this.credentialStore = git; this.repoUrl = repoUrl; if (git.TryGetConfigUrlMatch("http", this.repoUrl, out Dictionary configSettings)) { this.GitSsl = new GitSsl(configSettings); } } public bool IsBackingOff { get { return this.GetNextAuthAttemptTime() > DateTime.Now; } } public bool IsAnonymous { get; private set; } = true; private GitSsl GitSsl { get; } public void ApproveCredentials(ITracer tracer, string credentialString) { lock (this.gitAuthLock) { // Don't reset the backoff if this is for a different credential than we have cached if (credentialString == this.cachedCredentialString) { this.numberOfAttempts = 0; this.lastAuthAttempt = DateTime.MinValue; // Tell Git to store the valid credential if we haven't already // done so for this cached credential. if (!this.isCachedCredentialStringApproved) { string username; string password; if (TryParseCredentialString(this.cachedCredentialString, out username, out password)) { if (!this.credentialStore.TryStoreCredential(tracer, this.repoUrl, username, password, out string error)) { // Storing credentials is best effort attempt - log failure, but do not fail tracer.RelatedWarning("Failed to store credential string: {0}", error); } this.isCachedCredentialStringApproved = true; } else { EventMetadata metadata = new EventMetadata(new Dictionary { ["RepoUrl"] = this.repoUrl, }); tracer.RelatedError(metadata, "Failed to parse credential string for approval"); } } } } } public void RejectCredentials(ITracer tracer, string credentialString) { lock (this.gitAuthLock) { string cachedCredentialAtStartOfReject = this.cachedCredentialString; // Don't stomp a different credential if (credentialString == cachedCredentialAtStartOfReject && cachedCredentialAtStartOfReject != null) { // We can't assume that the credential store's cached credential is the same as the one we have. // Reload the credential from the store to ensure we're rejecting the correct one. int attemptsBeforeCheckingExistingCredential = this.numberOfAttempts; if (this.TryCallGitCredential(tracer, out string getCredentialError)) { if (this.cachedCredentialString != cachedCredentialAtStartOfReject) { // If the store already had a different credential, we don't want to reject it without trying it. this.isCachedCredentialStringApproved = false; return; } } else { tracer.RelatedWarning(getCredentialError); } // If we can we should pass the actual username/password values we used (and found to be invalid) // to `git-credential reject` so the credential helpers can attempt to check if they're erasing // the expected credentials, if they so choose to. string username; string password; if (TryParseCredentialString(this.cachedCredentialString, out username, out password)) { if (!this.credentialStore.TryDeleteCredential(tracer, this.repoUrl, username, password, out string error)) { // Deleting credentials is best effort attempt - log failure, but do not fail tracer.RelatedWarning("Failed to delete credential string: {0}", error); } } else { // We failed to parse the credential string so instead (as a recovery) we try to erase without // specifying the particular username/password. EventMetadata metadata = new EventMetadata(new Dictionary { ["RepoUrl"] = this.repoUrl, }); tracer.RelatedWarning(metadata, "Failed to parse credential string for rejection. Rejecting any credential for this repo URL."); this.credentialStore.TryDeleteCredential(tracer, this.repoUrl, username: null, password: null, error: out string error); } this.cachedCredentialString = null; this.isCachedCredentialStringApproved = false; // Backoff may have already been incremented by a failure in TryCallGitCredential if (attemptsBeforeCheckingExistingCredential == this.numberOfAttempts) { this.UpdateBackoff(); } } } } public bool TryGetCredentials(ITracer tracer, out string credentialString, out string errorMessage) { if (!this.isInitialized) { throw new InvalidOperationException("This auth instance must be initialized before it can be used"); } credentialString = this.cachedCredentialString; if (credentialString == null) { lock (this.gitAuthLock) { if (this.cachedCredentialString == null) { if (this.IsBackingOff) { errorMessage = "Auth failed. No retries will be made until: " + this.GetNextAuthAttemptTime(); return false; } if (!this.TryCallGitCredential(tracer, out errorMessage)) { return false; } } credentialString = this.cachedCredentialString; } } errorMessage = null; return true; } /// /// Initialize authentication by probing the server. Determines whether /// anonymous access is supported and, if not, fetches credentials. /// Callers that also need the GVFS config should use /// instead to avoid a /// redundant HTTP round-trip. /// public bool TryInitialize(ITracer tracer, Enlistment enlistment, out string errorMessage) { // Delegate to the combined method, discarding the config result. // This avoids duplicating the anonymous-probe + credential-fetch logic. return this.TryInitializeAndQueryGVFSConfig( tracer, enlistment, new RetryConfig(), out _, out errorMessage); } /// /// Combines authentication initialization with the GVFS config query, /// eliminating a redundant HTTP round-trip. The anonymous probe and /// config query use the same request to /gvfs/config: /// 1. Config query → /gvfs/config → 200 (anonymous) or 401 /// 2. If 401: credential fetch, then retry → 200 /// This saves one HTTP request compared to probing auth separately /// and then querying config, and reuses the same TCP/TLS connection. /// public bool TryInitializeAndQueryGVFSConfig( ITracer tracer, Enlistment enlistment, RetryConfig retryConfig, out ServerGVFSConfig serverGVFSConfig, out string errorMessage) { if (this.isInitialized) { throw new InvalidOperationException("Already initialized"); } serverGVFSConfig = null; errorMessage = null; using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig)) { HttpStatusCode? httpStatus; // First attempt without credentials. If anonymous access works, // we get the config in a single request. if (configRequestor.TryQueryGVFSConfig(false, out serverGVFSConfig, out httpStatus, out _)) { this.IsAnonymous = true; this.isInitialized = true; tracer.RelatedInfo("{0}: Anonymous access succeeded, config obtained in one request", nameof(this.TryInitializeAndQueryGVFSConfig)); return true; } if (httpStatus != HttpStatusCode.Unauthorized) { errorMessage = "Unable to query /gvfs/config"; tracer.RelatedWarning("{0}: Config query failed with status {1}", nameof(this.TryInitializeAndQueryGVFSConfig), httpStatus?.ToString() ?? "None"); return false; } // Server requires authentication — fetch credentials this.IsAnonymous = false; if (!this.TryCallGitCredential(tracer, out errorMessage)) { tracer.RelatedWarning("{0}: Credential fetch failed: {1}", nameof(this.TryInitializeAndQueryGVFSConfig), errorMessage); return false; } this.isInitialized = true; // Retry with credentials using the same ConfigHttpRequestor (reuses HttpClient/connection) if (configRequestor.TryQueryGVFSConfig(true, out serverGVFSConfig, out _, out errorMessage)) { tracer.RelatedInfo("{0}: Config obtained with credentials", nameof(this.TryInitializeAndQueryGVFSConfig)); return true; } tracer.RelatedWarning("{0}: Config query failed with credentials: {1}", nameof(this.TryInitializeAndQueryGVFSConfig), errorMessage); return false; } } /// /// Test-only initialization that skips the network probe and goes /// straight to credential fetch. Not for production use. /// internal bool TryInitializeAndRequireAuth(ITracer tracer, out string errorMessage) { if (this.isInitialized) { throw new InvalidOperationException("Already initialized"); } if (this.TryCallGitCredential(tracer, out errorMessage)) { this.isInitialized = true; return true; } return false; } public void ConfigureHttpClientHandlerSslIfNeeded(ITracer tracer, HttpClientHandler httpClientHandler, GitProcess gitProcess) { X509Certificate2 cert = this.GitSsl?.GetCertificate(tracer, gitProcess); if (cert != null) { if (this.GitSsl != null && !this.GitSsl.ShouldVerify) { httpClientHandler.ServerCertificateCustomValidationCallback = // CodeQL [SM02184] TLS verification can be disabled by Git itself, so this is just mirroring a feature already exposed. (httpRequestMessage, c, cetChain, policyErrors) => { return true; }; } httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual; httpClientHandler.ClientCertificates.Add(cert); } } private static bool TryParseCredentialString(string credentialString, out string username, out string password) { if (credentialString != null) { byte[] data = Convert.FromBase64String(credentialString); string rawCredString = Encoding.ASCII.GetString(data); string[] usernamePassword = rawCredString.Split(':'); if (usernamePassword.Length == 2) { username = usernamePassword[0]; password = usernamePassword[1]; return true; } } username = null; password = null; return false; } private DateTime GetNextAuthAttemptTime() { if (this.numberOfAttempts <= 1) { return DateTime.MinValue; } double backoffSeconds = RetryBackoff.CalculateBackoffSeconds(this.numberOfAttempts, MaxBackoffSeconds); return this.lastAuthAttempt + TimeSpan.FromSeconds(backoffSeconds); } private void UpdateBackoff() { this.lastAuthAttempt = DateTime.Now; this.numberOfAttempts++; } private bool TryCallGitCredential(ITracer tracer, out string errorMessage) { string gitUsername; string gitPassword; if (!this.credentialStore.TryGetCredential(tracer, this.repoUrl, out gitUsername, out gitPassword, out errorMessage)) { this.UpdateBackoff(); return false; } if (!string.IsNullOrEmpty(gitUsername) && !string.IsNullOrEmpty(gitPassword)) { this.cachedCredentialString = Convert.ToBase64String(Encoding.ASCII.GetBytes(gitUsername + ":" + gitPassword)); this.isCachedCredentialStringApproved = false; } else { errorMessage = "Got back empty credentials from git"; return false; } return true; } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitConfigHelper.cs ================================================ using System; using System.Collections.Generic; namespace GVFS.Common.Git { /// /// Helper methods for git config-style file reading and parsing. /// public static class GitConfigHelper { /// /// Sanitizes lines read from Git config files: /// - Removes leading and trailing whitespace /// - Removes comments /// /// Input line from config file /// Sanitized config file line /// true if sanitizedLine has content, false if there is no content left after sanitizing public static bool TrySanitizeConfigFileLine(string fileLine, out string sanitizedLine) { sanitizedLine = fileLine; int commentIndex = sanitizedLine.IndexOf(GVFSConstants.GitCommentSign); if (commentIndex >= 0) { sanitizedLine = sanitizedLine.Substring(0, commentIndex); } sanitizedLine = sanitizedLine.Trim(); return !string.IsNullOrWhiteSpace(sanitizedLine); } /// /// Get the settings for a section in a given config file. /// /// The contents of a config file, one line per entry. /// The name of the section to grab the settings from. /// A dictionary of settings, keyed off the setting name. public static Dictionary GetSettings(string[] configLines, string sectionName) { List linesToParse = new List(); int currentLineIndex = 0; string sectionTag = "[" + sectionName + "]"; // There can be multiple occurrences of the same section in a config file. while (currentLineIndex < configLines.Length) { while (currentLineIndex < configLines.Length && !string.Equals(configLines[currentLineIndex].Trim(), sectionTag, StringComparison.OrdinalIgnoreCase)) { currentLineIndex++; } if (currentLineIndex < configLines.Length) { // skip [sectionName] line currentLineIndex++; while (currentLineIndex < configLines.Length && !configLines[currentLineIndex].StartsWith("[")) { string currentLineValue = configLines[currentLineIndex].Trim(); if (!string.IsNullOrEmpty(currentLineValue)) { linesToParse.Add(currentLineValue); } currentLineIndex++; } } } return ParseKeyValues(linesToParse); } /// /// Returns a list of settings based on a collection of lines of text in the form: /// settingName = settingValue /// or /// section.settingName=settingValue /// /// The lines of text with the settings to parse. /// The delimiter char, separating key from value /// A dictionary of settings, keyed off the setting name representing the settings parsed from input. public static Dictionary ParseKeyValues(IEnumerable input, char delimiter = '=') { Dictionary configSettings = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (string line in input) { string[] fields = line.Split(new[] { delimiter }, 2, StringSplitOptions.None); if (fields.Length > 0) { string key = fields[0].Trim(); string value = string.Empty; if (fields.Length > 1) { value = fields[1].Trim(); } if (!string.IsNullOrEmpty(key)) { if (!configSettings.ContainsKey(key) && fields.Length == 2) { GitConfigSetting setting = new GitConfigSetting(key, value); configSettings.Add(key, setting); } else if (fields.Length == 2) { configSettings[key].Add(value); } } } } return configSettings; } /// /// Returns a list of settings based on input of the form: /// settingName1 = settingValue1 /// settingName2 = settingValue2 /// settingName3 = settingValue3 /// settingNameN = settingValueN /// or /// section.settingName1=settingValue1 /// section.settingName2=settingValue2 /// section.settingName3=settingValue3 /// section.settingNameN=settingValueN /// /// The settings as text. /// The delimiter char, separating key from value /// A dictionary of settings, keyed off the setting name representing the settings parsed from input. public static Dictionary ParseKeyValues(string input, char delimiter = '=') { return ParseKeyValues(input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), delimiter); } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitConfigSetting.cs ================================================ using System.Collections.Generic; namespace GVFS.Common.Git { public class GitConfigSetting { public const string CoreVirtualizeObjectsName = "core.virtualizeobjects"; public const string CoreVirtualFileSystemName = "core.virtualfilesystem"; public const string CredentialUseHttpPath = "credential.\"https://dev.azure.com\".useHttpPath"; public const string HttpSslCert = "http.sslcert"; public const string HttpSslVerify = "http.sslverify"; public const string HttpSslCertPasswordProtected = "http.sslcertpasswordprotected"; public GitConfigSetting(string name, params string[] values) { this.Name = name; this.Values = new HashSet(values); } public string Name { get; } public HashSet Values { get; } public bool HasValue(string value) { return this.Values.Contains(value); } public void Add(string value) { this.Values.Add(value); } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitCoreGVFSFlags.cs ================================================ using System; namespace GVFS.Common.Git { [Flags] public enum GitCoreGVFSFlags { // GVFS_SKIP_SHA_ON_INDEX // Disables the calculation of the sha when writing the index SkipShaOnIndex = 1 << 0, // GVFS_BLOCK_COMMANDS // Blocks git commands that are not allowed in a GVFS/Scalar repo BlockCommands = 1 << 1, // GVFS_MISSING_OK // Normally git write-tree ensures that the objects referenced by the // directory exist in the object database.This option disables this check. MissingOk = 1 << 2, // GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT // When marking entries to remove from the index and the working // directory this option will take into account what the // skip-worktree bit was set to so that if the entry has the // skip-worktree bit set it will not be removed from the working // directory. This will allow virtualized working directories to // detect the change to HEAD and use the new commit tree to show // the files that are in the working directory. NoDeleteOutsideSparseCheckout = 1 << 3, // GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK // While performing a fetch with a virtual file system we know // that there will be missing objects and we don't want to download // them just because of the reachability of the commits. We also // don't want to download a pack file with commits, trees, and blobs // since these will be downloaded on demand. This flag will skip the // checks on the reachability of objects during a fetch as well as // the upload pack so that extraneous objects don't get downloaded. FetchSkipReachabilityAndUploadPack = 1 << 4, // 1 << 5 has been deprecated // GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS // With a virtual file system we only know the file size before any // CRLF or smudge/clean filters processing is done on the client. // To prevent file corruption due to truncation or expansion with // garbage at the end, these filters must not run when the file // is first accessed and brought down to the client. Git.exe can't // currently tell the first access vs subsequent accesses so this // flag just blocks them from occurring at all. BlockFiltersAndEolConversions = 1 << 6, // GVFS_PREFETCH_DURING_FETCH // While performing a `git fetch` command, use the gvfs-helper to // perform a "prefetch" of commits and trees. PrefetchDuringFetch = 1 << 7, // GVFS_SUPPORTS_WORKTREES // Signals that this GVFS version supports git worktrees, // allowing `git worktree add/remove` on VFS-enabled repos. SupportsWorktrees = 1 << 8, } } ================================================ FILE: GVFS/GVFS.Common/Git/GitIndexGenerator.cs ================================================ using GVFS.Common.Tracing; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; namespace GVFS.Common.Git { public class GitIndexGenerator { private const long EntryCountOffset = 8; private const ushort ExtendedBit = 0x4000; private const ushort SkipWorktreeBit = 0x4000; private static readonly byte[] PaddingBytes = new byte[8]; private static readonly byte[] IndexHeader = new byte[] { (byte)'D', (byte)'I', (byte)'R', (byte)'C', // Magic Signature }; // We can't accurated fill times and length in realtime, so we block write the zeroes and probably save time. private static readonly byte[] EntryHeader = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, // ctime 0, 0, 0, 0, 0, 0, 0, 0, // mtime 0, 0, 0, 0, // stat(2) dev 0, 0, 0, 0, // stat(2) ino 0, 0, 0x81, 0xA4, // filemode (0x81A4 in little endian) 0, 0, 0, 0, // stat(2) uid 0, 0, 0, 0, // stat(2) gid 0, 0, 0, 0 // file length }; private readonly string indexLockPath; private Enlistment enlistment; private ITracer tracer; private bool shouldHashIndex; private uint entryCount = 0; private BlockingCollection entryQueue = new BlockingCollection(); public GitIndexGenerator(ITracer tracer, Enlistment enlistment, bool shouldHashIndex) { this.tracer = tracer; this.enlistment = enlistment; this.shouldHashIndex = shouldHashIndex; // The extension 'lock2' is chosen simply to not be '.lock' because, although this class reasonably // conforms to how index.lock is supposed to be used, its callers continue to do things to the tree // and the working tree and even the before this class comes along and after this class has been released. // FastFetch.IndexLock bodges around this by creating an empty file in the index.lock position, so we // need to create a different file. See FastFetch.IndexLock for a proposed design to fix this. // // Note that there are two callers of this - one is from FastFetch, which we just discussed, and the // other is from the 'gvfs repair' verb. That environment is special in that it only runs on unmounted // repo's, so 'index.lock' is irrelevant as a locking mechanism in that context. There can't be git // commands to lock out. this.indexLockPath = Path.Combine(enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName + ".lock2"); } public string TemporaryIndexFilePath => this.indexLockPath; public bool HasFailures { get; private set; } /// Builds an index from scratch based on the current head pointer. /// The index version see https://git-scm.com/docs/index-format for details on what this means. /// /// If true, the index file will be written during this operation. If not, the new index will be /// left in . /// /// /// The index created by this class has no data from the working tree, so when 'git status' is run, it /// will calculate the hash of everything in the working tree. /// public void CreateFromRef(string refName, uint indexVersion, bool isFinal) { using (ITracer updateIndexActivity = this.tracer.StartActivity("CreateFromHeadTree", EventLevel.Informational)) { Thread entryWritingThread = new Thread(() => this.WriteAllEntries(indexVersion, isFinal)); entryWritingThread.Start(); GitProcess git = new GitProcess(this.enlistment); GitProcess.Result result = git.LsTree( refName, this.EnqueueEntriesFromLsTree, recursive: true, showAllTrees: false); if (result.ExitCodeIsFailure) { this.tracer.RelatedError("LsTree failed during index generation: {0}", result.Errors); this.HasFailures = true; } this.entryQueue.CompleteAdding(); entryWritingThread.Join(); } } private void EnqueueEntriesFromLsTree(string line) { LsTreeEntry entry = LsTreeEntry.ParseFromLsTreeLine(line); if (entry != null) { this.entryQueue.Add(entry); } } private void WriteAllEntries(uint version, bool isFinal) { try { using (Stream indexStream = new FileStream(this.indexLockPath, FileMode.Create, FileAccess.Write, FileShare.None)) using (BinaryWriter writer = new BinaryWriter(indexStream)) { writer.Write(IndexHeader); writer.Write(EndianHelper.Swap(version)); writer.Write((uint)0); // Number of entries placeholder uint lastStringLength = 0; LsTreeEntry entry; while (this.entryQueue.TryTake(out entry, Timeout.Infinite)) { this.WriteEntry(writer, version, entry.Sha, entry.Filename, ref lastStringLength); } // Update entry count writer.BaseStream.Position = EntryCountOffset; writer.Write(EndianHelper.Swap(this.entryCount)); writer.Flush(); } this.AppendIndexSha(); if (isFinal) { this.ReplaceExistingIndex(); } } catch (Exception e) { this.tracer.RelatedError("Failed to generate index: {0}", e.ToString()); this.HasFailures = true; } } private void WriteEntry(BinaryWriter writer, uint version, string sha, string filename, ref uint lastStringLength) { long startPosition = writer.BaseStream.Position; this.entryCount++; writer.Write(EntryHeader, 0, EntryHeader.Length); writer.Write(SHA1Util.BytesFromHexString(sha)); byte[] filenameBytes = Encoding.UTF8.GetBytes(filename); ushort flags = (ushort)(filenameBytes.Length & 0xFFF); writer.Write(EndianHelper.Swap(flags)); if (version >= 4) { this.WriteReplaceLength(writer, lastStringLength); lastStringLength = (uint)filenameBytes.Length; } writer.Write(filenameBytes); writer.Flush(); long endPosition = writer.BaseStream.Position; // Version 4 requires a nul-terminated string. int numPaddingBytes = 1; if (version < 4) { // Version 2-3 has between 1 and 8 padding bytes including nul-terminator. numPaddingBytes = 8 - ((int)(endPosition - startPosition) % 8); if (numPaddingBytes == 0) { numPaddingBytes = 8; } } writer.Write(PaddingBytes, 0, numPaddingBytes); writer.Flush(); } private void WriteReplaceLength(BinaryWriter writer, uint value) { List bytes = new List(); do { byte nextByte = (byte)(value & 0x7F); value = value >> 7; bytes.Add(nextByte); } while (value != 0); bytes.Reverse(); for (int i = 0; i < bytes.Count; ++i) { byte toWrite = bytes[i]; if (i < bytes.Count - 1) { toWrite -= 1; toWrite |= 0x80; } writer.Write(toWrite); } } private void AppendIndexSha() { byte[] sha = this.GetIndexHash(); using (Stream indexStream = new FileStream(this.indexLockPath, FileMode.Open, FileAccess.Write, FileShare.None)) { indexStream.Seek(0, SeekOrigin.End); indexStream.Write(sha, 0, sha.Length); } } private byte[] GetIndexHash() { if (this.shouldHashIndex) { using (Stream fileStream = new FileStream(this.indexLockPath, FileMode.Open, FileAccess.Read, FileShare.Write)) using (HashingStream hasher = new HashingStream(fileStream)) { hasher.CopyTo(Stream.Null); return hasher.Hash; } } return new byte[20]; } private void ReplaceExistingIndex() { string indexPath = Path.Combine(this.enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName); File.Delete(indexPath); File.Move(this.indexLockPath, indexPath); } private class LsTreeEntry { public LsTreeEntry() { this.Filename = string.Empty; } public string Filename { get; private set; } public string Sha { get; private set; } public static LsTreeEntry ParseFromLsTreeLine(string line) { if (DiffTreeResult.IsLsTreeLineOfType(line, DiffTreeResult.BlobMarker)) { LsTreeEntry blobEntry = new LsTreeEntry(); blobEntry.Sha = line.Substring(DiffTreeResult.TypeMarkerStartIndex + DiffTreeResult.BlobMarker.Length, GVFSConstants.ShaStringLength); blobEntry.Filename = GitPathConverter.ConvertPathOctetsToUtf8(line.Substring(line.LastIndexOf("\t") + 1).Trim('"')); return blobEntry; } return null; } } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitObjectContentType.cs ================================================ namespace GVFS.Common.Git { public enum GitObjectContentType { None, LooseObject, BatchedLooseObjects, PackFile } } ================================================ FILE: GVFS/GVFS.Common/Git/GitObjects.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Http; using GVFS.Common.NetworkStreams; using GVFS.Common.Tracing; using ICSharpCode.SharpZipLib; using ICSharpCode.SharpZipLib.Zip.Compression.Streams; using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Security; using System.Threading; using System.Threading.Tasks; namespace GVFS.Common.Git { public abstract class GitObjects { protected readonly ITracer Tracer; protected readonly GitObjectsHttpRequestor GitObjectRequestor; protected readonly Enlistment Enlistment; /// /// Used only for testing. /// protected bool checkData; private const string EtwArea = nameof(GitObjects); private const string TempPackFolder = "tempPacks"; private const string TempIdxExtension = ".tempidx"; private readonly PhysicalFileSystem fileSystem; public GitObjects(ITracer tracer, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor, PhysicalFileSystem fileSystem = null) { this.Tracer = tracer; this.Enlistment = enlistment; this.GitObjectRequestor = objectRequestor; this.fileSystem = fileSystem ?? new PhysicalFileSystem(); this.checkData = true; } public enum DownloadAndSaveObjectResult { Success, ObjectNotOnServer, Error } public static bool IsLooseObjectsDirectory(string value) { return value.Length == 2 && value.All(c => Uri.IsHexDigit(c)); } public virtual bool TryDownloadCommit(string commitSha) { const bool PreferLooseObjects = false; IEnumerable objectIds = new[] { commitSha }; GitProcess gitProcess = new GitProcess(this.Enlistment); RetryWrapper.InvocationResult output = this.GitObjectRequestor.TryDownloadObjects( objectIds, onSuccess: (tryCount, response) => this.TrySavePackOrLooseObject(objectIds, PreferLooseObjects, response, gitProcess), onFailure: (eArgs) => { EventMetadata metadata = CreateEventMetadata(eArgs.Error); metadata.Add("Operation", "DownloadAndSaveObjects"); metadata.Add("WillRetry", eArgs.WillRetry); if (eArgs.WillRetry) { this.Tracer.RelatedWarning(metadata, eArgs.Error.ToString(), Keywords.Network | Keywords.Telemetry); } else { this.Tracer.RelatedError(metadata, eArgs.Error.ToString(), Keywords.Network); } }, preferBatchedLooseObjects: PreferLooseObjects); return output.Succeeded && output.Result.Success; } public virtual void DeleteStaleTempPrefetchPackAndIdxs() { string[] staleTempPacks = this.ReadPackFileNames(Path.Combine(this.Enlistment.GitPackRoot, GitObjects.TempPackFolder), GVFSConstants.PrefetchPackPrefix); foreach (string stalePackPath in staleTempPacks) { string staleIdxPath = Path.ChangeExtension(stalePackPath, ".idx"); string staleTempIdxPath = Path.ChangeExtension(stalePackPath, TempIdxExtension); EventMetadata metadata = CreateEventMetadata(); metadata.Add("stalePackPath", stalePackPath); metadata.Add("staleIdxPath", staleIdxPath); metadata.Add("staleTempIdxPath", staleTempIdxPath); metadata.Add(TracingConstants.MessageKey.InfoMessage, "Deleting stale temp pack and/or idx file"); this.fileSystem.TryDeleteFile(staleTempIdxPath, metadataKey: nameof(staleTempIdxPath), metadata: metadata); this.fileSystem.TryDeleteFile(staleIdxPath, metadataKey: nameof(staleIdxPath), metadata: metadata); this.fileSystem.TryDeleteFile(stalePackPath, metadataKey: nameof(stalePackPath), metadata: metadata); this.Tracer.RelatedEvent(EventLevel.Informational, nameof(this.DeleteStaleTempPrefetchPackAndIdxs), metadata); } } private void DeleteStaleIncompletePrefetchPackAndIdxs() { string[] packFiles = this.ReadPackFileNames(this.Enlistment.GitPackRoot, GVFSConstants.PrefetchPackPrefix); foreach (string packPath in packFiles) { string markerPath = Path.ChangeExtension(packPath, GVFSConstants.InProgressPrefetchMarkerExtension); if (!this.fileSystem.FileExists(markerPath)) { continue; } string idxPath = GetIndexForPack(packPath); EventMetadata metadata = CreateEventMetadata(); metadata.Add("packPath", packPath); metadata.Add("idxPath", idxPath); metadata.Add("markerPath", markerPath); metadata.Add(TracingConstants.MessageKey.InfoMessage, "Deleting stale temp pack and/or idx file"); /* Delete the index first (which makes git stop using the pack), then the pack, * last the marker. */ if (this.fileSystem.TryDeleteFile(idxPath, metadataKey: nameof(idxPath), metadata: metadata) && this.fileSystem.TryDeleteFile(packPath, metadataKey: nameof(packPath), metadata: metadata)) { this.fileSystem.TryDeleteFile(markerPath, metadataKey: nameof(markerPath), metadata: metadata); } this.Tracer.RelatedEvent(EventLevel.Informational, nameof(this.DeleteStaleIncompletePrefetchPackAndIdxs), metadata); } } public virtual void DeleteTemporaryFiles() { string[] temporaryFiles = this.fileSystem.GetFiles(this.Enlistment.GitPackRoot, "tmp_*"); foreach (string temporaryFilePath in temporaryFiles) { EventMetadata metadata = CreateEventMetadata(); metadata.Add(nameof(temporaryFilePath), temporaryFilePath); metadata.Add(TracingConstants.MessageKey.InfoMessage, "Deleting temporary file"); this.fileSystem.TryDeleteFile(temporaryFilePath, metadataKey: nameof(temporaryFilePath), metadata: metadata); this.Tracer.RelatedEvent(EventLevel.Informational, nameof(this.DeleteTemporaryFiles), metadata); } } public virtual bool TryDownloadPrefetchPacks(GitProcess gitProcess, long latestTimestamp, bool trustPackIndexes, out List packIndexes) { EventMetadata metadata = CreateEventMetadata(); metadata.Add("latestTimestamp", latestTimestamp); using (ITracer activity = this.Tracer.StartActivity("TryDownloadPrefetchPacks", EventLevel.Informational, Keywords.Telemetry, metadata)) { long bytesDownloaded = 0; /* Distrusting the indexes from the server is a security feature to prevent a compromised server from sending a * pack file and an index file that do not match. * Eventually we will make this the default, but it has a high performance cost for the first prefetch after * cloning a large repository, so it must be explicitly enabled for now. */ metadata.Add("trustPackIndexes", trustPackIndexes); long requestId = HttpRequestor.GetNewRequestId(); List innerPackIndexes = null; RetryWrapper.InvocationResult result = this.GitObjectRequestor.TrySendProtocolRequest( requestId: requestId, onSuccess: (tryCount, response) => this.DeserializePrefetchPacks(response, ref latestTimestamp, ref bytesDownloaded, ref innerPackIndexes, gitProcess, trustPackIndexes), onFailure: RetryWrapper.StandardErrorHandler(activity, requestId, "TryDownloadPrefetchPacks"), method: HttpMethod.Get, endPointGenerator: () => new Uri( string.Format( "{0}?lastPackTimestamp={1}", this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl, latestTimestamp)), requestBodyGenerator: () => null, cancellationToken: CancellationToken.None, acceptType: new MediaTypeWithQualityHeaderValue(GVFSConstants.MediaTypes.PrefetchPackFilesAndIndexesMediaType)); packIndexes = innerPackIndexes; if (!result.Succeeded) { if (result.Result != null && result.Result.HttpStatusCodeResult == HttpStatusCode.NotFound) { EventMetadata warning = CreateEventMetadata(); warning.Add(TracingConstants.MessageKey.WarningMessage, "The server does not support " + GVFSConstants.Endpoints.GVFSPrefetch); warning.Add(nameof(this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl), this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl); activity.RelatedEvent(EventLevel.Warning, "CommandNotSupported", warning); } else { EventMetadata error = CreateEventMetadata(result.Error); error.Add("latestTimestamp", latestTimestamp); error.Add(nameof(this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl), this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl); activity.RelatedWarning(error, "DownloadPrefetchPacks failed.", Keywords.Telemetry); } } activity.Stop(new EventMetadata { { "Area", EtwArea }, { "Success", result.Succeeded }, { "Attempts", result.Attempts }, { "BytesDownloaded", bytesDownloaded }, { "LatestPrefetchPackTimestamp", latestTimestamp }, }); return result.Succeeded; } } public virtual string WriteLooseObject(Stream responseStream, string sha, bool overwriteExistingObject, byte[] bufToCopyWith) { try { LooseObjectToWrite toWrite = this.GetLooseObjectDestination(sha); if (this.checkData) { try { using (Stream fileStream = this.OpenTempLooseObjectStream(toWrite.TempFile)) using (SideChannelStream sideChannel = new SideChannelStream(from: responseStream, to: fileStream)) using (InflaterInputStream inflate = new InflaterInputStream(sideChannel)) using (HashingStream hashing = new HashingStream(inflate)) using (NoOpStream devNull = new NoOpStream()) { hashing.CopyTo(devNull); string actualSha = SHA1Util.HexStringFromBytes(hashing.Hash); if (!sha.Equals(actualSha, StringComparison.OrdinalIgnoreCase)) { string message = $"Requested object with hash {sha} but received object with hash {actualSha}."; message += $"\nFind the incorrect data at '{toWrite.TempFile}'"; this.Tracer.RelatedError(message); throw new SecurityException(message); } } } catch (SharpZipBaseException) { string message = $"Requested object with hash {sha} but received data that failed decompression."; message += $"\nFind the incorrect data at '{toWrite.TempFile}'"; this.Tracer.RelatedError(message); throw new RetryableException(message); } } else { using (Stream fileStream = this.OpenTempLooseObjectStream(toWrite.TempFile)) { StreamUtil.CopyToWithBuffer(responseStream, fileStream, bufToCopyWith); fileStream.Flush(); } } this.FinalizeTempFile(sha, toWrite, overwriteExistingObject); return toWrite.ActualFile; } catch (IOException e) { throw new RetryableException("IOException while writing loose object. See inner exception for details.", e); } catch (UnauthorizedAccessException e) { throw new RetryableException("UnauthorizedAccessException while writing loose object. See inner exception for details.", e); } catch (Win32Exception e) { throw new RetryableException("Win32Exception while writing loose object. See inner exception for details.", e); } } public virtual string WriteTempPackFile(Stream stream) { string fileName = Path.GetRandomFileName(); string fullPath = Path.Combine(this.Enlistment.GitPackRoot, fileName); Task flushTask; long fileLength; this.TryWriteTempFile( tracer: null, source: stream, tempFilePath: fullPath, fileLength: out fileLength, flushTask: out flushTask, throwOnError: true); flushTask?.Wait(); return fullPath; } public virtual bool TryWriteTempFile( ITracer tracer, Stream source, string tempFilePath, out long fileLength, out Task flushTask, bool throwOnError = false) { fileLength = 0; flushTask = null; try { Stream fileStream = null; try { fileStream = this.fileSystem.OpenFileStream( tempFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read, callFlushFileBuffers: false); // Any flushing to disk will be done asynchronously StreamUtil.CopyToWithBuffer(source, fileStream); fileLength = fileStream.Length; if (this.Enlistment.FlushFileBuffersForPacks) { // Flush any data buffered in FileStream to the file system fileStream.Flush(); // FlushFileBuffers using FlushAsync // Do this last to ensure that the stream is not being accessed after it's been disposed flushTask = fileStream.FlushAsync().ContinueWith((result) => fileStream.Dispose()); } } finally { if (flushTask == null && fileStream != null) { fileStream.Dispose(); } } this.ValidateTempFile(tempFilePath, tempFilePath); } catch (Exception ex) { if (flushTask != null) { flushTask.Wait(); flushTask = null; } this.CleanupTempFile(this.Tracer, tempFilePath); if (tracer != null) { EventMetadata metadata = CreateEventMetadata(ex); metadata.Add("tempFilePath", tempFilePath); tracer.RelatedWarning(metadata, $"{nameof(this.TryWriteTempFile)}: Exception caught while writing temp file", Keywords.Telemetry); } if (throwOnError) { throw; } else { return false; } } return true; } public virtual GitProcess.Result IndexTempPackFile(string tempPackPath, GitProcess gitProcess = null) { string packfilePath = GetRandomPackName(this.Enlistment.GitPackRoot); Exception moveFileException = null; try { // We're indexing a pack file that was saved to a temp file name, and so it must be renamed // to its final name before indexing ('git index-pack' requires that the pack file name end with .pack) this.fileSystem.MoveFile(tempPackPath, packfilePath); } catch (IOException e) { moveFileException = e; } catch (UnauthorizedAccessException e) { moveFileException = e; } if (moveFileException != null) { EventMetadata failureMetadata = CreateEventMetadata(moveFileException); failureMetadata.Add("tempPackPath", tempPackPath); failureMetadata.Add("packfilePath", packfilePath); this.fileSystem.TryDeleteFile(tempPackPath, metadataKey: nameof(tempPackPath), metadata: failureMetadata); this.Tracer.RelatedWarning(failureMetadata, $"{nameof(this.IndexTempPackFile): Exception caught while trying to move temp pack file}"); return new GitProcess.Result( string.Empty, moveFileException != null ? moveFileException.Message : "Failed to move temp pack file to final path", GitProcess.Result.GenericFailureCode); } // TryBuildIndex will delete the pack file if indexing fails GitProcess.Result result; this.TryBuildIndex(this.Tracer, packfilePath, out result, gitProcess); return result; } public virtual GitProcess.Result IndexPackFile(string packfilePath, GitProcess gitProcess) { string tempIdxPath = Path.ChangeExtension(packfilePath, TempIdxExtension); string idxPath = Path.ChangeExtension(packfilePath, ".idx"); Exception indexPackException = null; try { if (gitProcess == null) { gitProcess = new GitProcess(this.Enlistment); } GitProcess.Result result = gitProcess.IndexPack(packfilePath, tempIdxPath); if (result.ExitCodeIsFailure) { Exception exception; if (!this.fileSystem.TryDeleteFile(tempIdxPath, exception: out exception)) { EventMetadata metadata = CreateEventMetadata(exception); metadata.Add("tempIdxPath", tempIdxPath); this.Tracer.RelatedWarning(metadata, $"{nameof(this.IndexPackFile)}: Failed to cleanup temp idx file after index pack failure"); } } else { if (this.Enlistment.FlushFileBuffersForPacks) { Exception exception; string error; if (!this.TryFlushFileBuffers(tempIdxPath, out exception, out error)) { EventMetadata metadata = CreateEventMetadata(exception); metadata.Add("packfilePath", packfilePath); metadata.Add("tempIndexPath", tempIdxPath); metadata.Add("error", error); this.Tracer.RelatedWarning(metadata, $"{nameof(this.IndexPackFile)}: Failed to flush temp idx file buffers"); } } this.fileSystem.MoveAndOverwriteFile(tempIdxPath, idxPath); } return result; } catch (Win32Exception e) { indexPackException = e; } catch (IOException e) { indexPackException = e; } catch (UnauthorizedAccessException e) { indexPackException = e; } EventMetadata failureMetadata = CreateEventMetadata(indexPackException); failureMetadata.Add("packfilePath", packfilePath); failureMetadata.Add("tempIdxPath", tempIdxPath); failureMetadata.Add("idxPath", idxPath); this.fileSystem.TryDeleteFile(tempIdxPath, metadataKey: nameof(tempIdxPath), metadata: failureMetadata); this.fileSystem.TryDeleteFile(idxPath, metadataKey: nameof(idxPath), metadata: failureMetadata); this.Tracer.RelatedWarning(failureMetadata, $"{nameof(this.IndexPackFile): Exception caught while trying to index pack file}"); return new GitProcess.Result( string.Empty, indexPackException != null ? indexPackException.Message : "Failed to index pack file", GitProcess.Result.GenericFailureCode); } public virtual string[] ReadPackFileNames(string packFolderPath, string prefixFilter = "") { if (this.fileSystem.DirectoryExists(packFolderPath)) { try { return this.fileSystem.GetFiles(packFolderPath, prefixFilter + "*.pack"); } catch (DirectoryNotFoundException e) { EventMetadata metadata = CreateEventMetadata(e); metadata.Add("packFolderPath", packFolderPath); metadata.Add("prefixFilter", prefixFilter); metadata.Add(TracingConstants.MessageKey.InfoMessage, "${nameof(this.ReadPackFileNames)}: Caught DirectoryNotFoundException exception"); this.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.ReadPackFileNames)}_DirectoryNotFound", metadata); return new string[0]; } } 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"; return Path.Combine(packRoot, packName); } private static EventMetadata CreateEventMetadata(Exception e = null) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", EtwArea); if (e != null) { metadata.Add("Exception", e.ToString()); } return metadata; } private static string GetIndexForPack(string packNameOrPath) { return Path.ChangeExtension(packNameOrPath, ".idx"); } private bool TryMovePackAndIdx(string sourcePackPath, string targetPackPath, out Exception exception) { exception = null; string sourceIdxPath = GetIndexForPack(sourcePackPath); string targetIdxPath = GetIndexForPack(targetPackPath); try { /* Make sure there's not an existing index first to prevent race condition where index may not * match the pack file. */ this.fileSystem.DeleteFile(targetIdxPath); this.fileSystem.MoveAndOverwriteFile(sourcePackPath, targetPackPath); this.fileSystem.MoveAndOverwriteFile(sourceIdxPath, targetIdxPath); } catch (Win32Exception e) { exception = e; EventMetadata metadata = CreateEventMetadata(e); metadata.Add("packName", Path.GetFileName(sourcePackPath)); metadata.Add("packTempPath", sourcePackPath); metadata.Add("idxName", Path.GetFileName(sourceIdxPath)); metadata.Add("idxTempPath", sourceIdxPath); /* Delete target idx first, to make sure target pack is inaccessible if present. */ this.fileSystem.TryDeleteFile(targetIdxPath, metadataKey: nameof(targetIdxPath), metadata: metadata); this.fileSystem.TryDeleteFile(sourceIdxPath, metadataKey: nameof(sourceIdxPath), metadata: metadata); this.fileSystem.TryDeleteFile(sourcePackPath, metadataKey: nameof(sourcePackPath), metadata: metadata); this.fileSystem.TryDeleteFile(targetPackPath, metadataKey: nameof(targetPackPath), metadata: metadata); this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryMovePackAndIdx): Failed to move pack and idx from temp folder}"); return false; } return true; } private bool TryFlushFileBuffers(string path, out Exception exception, out string error) { error = null; FileAttributes originalAttributes; if (!this.TryGetAttributes(path, out originalAttributes, out exception)) { error = "Failed to get original attributes, skipping flush"; return false; } bool readOnly = (originalAttributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; if (readOnly) { if (!this.TrySetAttributes(path, originalAttributes & ~FileAttributes.ReadOnly, out exception)) { error = "Failed to clear read-only attribute, skipping flush"; return false; } } bool flushedBuffers = false; try { GVFSPlatform.Instance.FileSystem.FlushFileBuffers(path); flushedBuffers = true; } catch (Win32Exception e) { exception = e; error = "Win32Exception while trying to flush file buffers"; } if (readOnly) { Exception setAttributesException; if (!this.TrySetAttributes(path, originalAttributes, out setAttributesException)) { EventMetadata metadata = CreateEventMetadata(setAttributesException); metadata.Add("path", path); this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryFlushFileBuffers)}: Failed to re-enable read-only bit"); } } return flushedBuffers; } private bool TryGetAttributes(string path, out FileAttributes attributes, out Exception exception) { attributes = 0; exception = null; try { attributes = this.fileSystem.GetAttributes(path); return true; } catch (IOException e) { exception = e; } catch (UnauthorizedAccessException e) { exception = e; } return false; } private bool TrySetAttributes(string path, FileAttributes attributes, out Exception exception) { exception = null; try { this.fileSystem.SetAttributes(path, attributes); return true; } catch (IOException e) { exception = e; } catch (UnauthorizedAccessException e) { exception = e; } return false; } private Stream OpenTempLooseObjectStream(string path) { return this.fileSystem.OpenFileStream( path, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.SequentialScan, callFlushFileBuffers: false); } private LooseObjectToWrite GetLooseObjectDestination(string sha) { // Ensure SHA path is lowercase for case-sensitive filesystems if (GVFSPlatform.Instance.Constants.CaseSensitiveFileSystem) { sha = sha.ToLower(); } string firstTwoDigits = sha.Substring(0, 2); string remainingDigits = sha.Substring(2); string twoLetterFolderName = Path.Combine(this.Enlistment.GitObjectsRoot, firstTwoDigits); this.fileSystem.CreateDirectory(twoLetterFolderName); return new LooseObjectToWrite( tempFile: Path.Combine(twoLetterFolderName, Path.GetRandomFileName()), actualFile: Path.Combine(twoLetterFolderName, remainingDigits)); } /// /// Uses a to read the packs from the stream. /// private RetryWrapper.CallbackResult DeserializePrefetchPacks( GitEndPointResponseData response, ref long latestTimestamp, ref long bytesDownloaded, ref List packIndexes, GitProcess gitProcess, bool trustPackIndexes) { if (packIndexes == null) { packIndexes = new List(); } using (ITracer activity = this.Tracer.StartActivity("DeserializePrefetchPacks", EventLevel.Informational)) { PrefetchPacksDeserializer deserializer = new PrefetchPacksDeserializer(response.Stream); string tempPackFolderPath = Path.Combine(this.Enlistment.GitPackRoot, TempPackFolder); this.fileSystem.CreateDirectory(tempPackFolderPath); var tempPackOperations = new List(); // Future: We could manage cancellation of index building tasks if one fails (to stop indexing of later // files if an early one fails), but in practice the first pack file takes the majority of the time and // all the others will finish long before it so there would be no benefit to doing so. bool allSucceeded = true; // Read each pack from the stream to a temp file, and start a task to index it. Task previousPackTask = Task.CompletedTask; foreach (PrefetchPacksDeserializer.PackAndIndex packHandle in deserializer.EnumeratePacks()) { var pack = packHandle; // Capture packHandle in a new variable to avoid closure issues with async index task long packLength; // Write the temp and index to a temp folder to avoid putting corrupt files in the pack folder // As the files are validated and flushed they can be moved to the pack folder, along with a marker file. // When the previous pack has completed, the marker file for the current pack is deleted. // This allows users to access the most recent pack files as soon as they are ready even though // the oldest, largest pack file may still be in progress, and it allows us to know on a future // prefetch if the previous one was interrupted and needs to be started over. string packName = string.Format("{0}-{1}-{2}.pack", GVFSConstants.PrefetchPackPrefix, pack.Timestamp, pack.UniqueId); string packTempPath = Path.Combine(tempPackFolderPath, packName); EventMetadata data = CreateEventMetadata(); data["timestamp"] = pack.Timestamp.ToString(); data["uniqueId"] = pack.UniqueId; activity.RelatedEvent(EventLevel.Informational, "Receiving Pack/Index", data); // Write the pack // If it fails, TryWriteTempFile cleans up the file and we retry the prefetch Task packFlushTask; if (!this.TryWriteTempFile(activity, pack.PackStream, packTempPath, out packLength, out packFlushTask)) { bytesDownloaded += packLength; allSucceeded = false; break; } var currentOperation = new TempPrefetchPackAndIdx(pack.Timestamp, packName, packTempPath, packFlushTask); tempPackOperations.Add(currentOperation); bytesDownloaded += packLength; if (trustPackIndexes && pack.IndexStream != null) { if (this.TryWriteTempFile(activity, pack.IndexStream, currentOperation.IndexTempPath, out var indexLength, out var indexFlushTask)) { bytesDownloaded += indexLength; currentOperation.ReadyTask = Task.WhenAll(currentOperation.ReadyTask, indexFlushTask); previousPackTask = AddFinalizationTasks(currentOperation, previousPackTask); } else { bytesDownloaded += indexLength; // we can try to build the index ourself, and if it's successful then on the retry we can pick up from that point. var indexTask = StartPackIndexAsync(activity, packTempPath); currentOperation.ReadyTask = Task.WhenAll(currentOperation.ReadyTask, indexTask); previousPackTask = AddFinalizationTasks(currentOperation, previousPackTask); // but we need to stop trying to read from the download stream as that has failed. allSucceeded = false; break; } } else { // Either we can't trust the index file from the server, or the server didn't provide one, so we will build our own. // For performance, we run the index build in the background while we continue downloading the next pack. var indexTask = StartPackIndexAsync(activity, packTempPath); currentOperation.ReadyTask = Task.WhenAll(currentOperation.ReadyTask, indexTask); previousPackTask = AddFinalizationTasks(currentOperation, previousPackTask); // If the server provided an index stream, we still need to consume and handle any exceptions it even // though we are otherwise ignoring it. if (pack.IndexStream != null) { try { bytesDownloaded += pack.IndexStream.Length; if (pack.IndexStream.CanSeek) { pack.IndexStream.Seek(0, SeekOrigin.End); } else { pack.IndexStream.CopyTo(Stream.Null); } } catch (Exception e) { EventMetadata metadata = CreateEventMetadata(e); activity.RelatedWarning(metadata, "Failed to read to end of index stream"); allSucceeded = false; break; } } } } Exception exception = null; if (!this.WaitForPacks(tempPackOperations, ref latestTimestamp, out exception)) { allSucceeded = false; } packIndexes.AddRange(tempPackOperations .Where(x => x.ReadyTask.Status == TaskStatus.RanToCompletion) .Select(x => x.IdxName)); if (allSucceeded) { /* Clean up any pack files from a previous incomplete prefetch */ DeleteStaleIncompletePrefetchPackAndIdxs(); return new RetryWrapper.CallbackResult( new GitObjectsHttpRequestor.GitObjectTaskResult(success: true)); } else { return new RetryWrapper.CallbackResult(exception, shouldRetry: true); } } } private Task AddFinalizationTasks(TempPrefetchPackAndIdx currentOperation, Task previousPackTask) { currentOperation.ReadyTask = FinalizePackFileAsync(currentOperation, previousPackTask); return currentOperation.ReadyTask; } private async Task FinalizePackFileAsync(TempPrefetchPackAndIdx currentOperation, Task previousPackTask) { await currentOperation.ReadyTask; /* Before moving this pack and index from the temp folder to the live pack folder, create a marker * file. This file is when getting the latest good pack timestamp to ignore the file. * Delete it after the previous pack has finished. * This lets us have the smaller, later pack files ready for use while the big initial * file finishes indexing, while still making sure we start at the beginning next time * if this prefetch is interrupted. */ string markerFilePath = Path.Combine( this.Enlistment.GitPackRoot, Path.ChangeExtension(currentOperation.PackName, GVFSConstants.InProgressPrefetchMarkerExtension)); this.fileSystem.OpenFileStream(markerFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, true) .Dispose(); string packDestination = Path.Combine(this.Enlistment.GitPackRoot, currentOperation.PackName); if (!TryMovePackAndIdx(currentOperation.PackTempPath, packDestination, out Exception ex)) { this.fileSystem.TryDeleteFile(markerFilePath); throw ex; } await previousPackTask; this.fileSystem.DeleteFile(markerFilePath); } private Task StartPackIndexAsync(ITracer activity, string packTempPath) { var indexTask = Task.Run(() => { // GitProcess only permits one process per instance at a time, so we need to duplicate it to run the index build in parallel. // This is safe because each process is only accessing the pack file we direct it to which is not yet part // of the enlistment. GitProcess gitProcessForIndex = new GitProcess(this.Enlistment); if (this.TryBuildIndex(activity, packTempPath, out var _, gitProcessForIndex)) { return; } else { throw new InvalidDataException(); } }); return indexTask; } private bool WaitForPacks(List tempPacks, ref long latestTimestamp, out Exception exception) { exception = null; bool anyFailed = false; var exceptions = new List(); foreach (TempPrefetchPackAndIdx tempPack in tempPacks) { if (tempPack.ReadyTask != null) { try { tempPack.ReadyTask.Wait(); } catch (AggregateException ex) { exceptions.AddRange(ex.InnerExceptions); anyFailed = true; } catch (Exception ex) { exceptions.Add(ex); anyFailed = true; } if (!anyFailed) { latestTimestamp = tempPack.Timestamp; } } } if (exceptions.Count == 1) { exception = exceptions[0]; } else if (exceptions.Count > 1) { exception = new AggregateException(exceptions); } return !anyFailed; } /// /// Attempts to build an index for the specified path. If building the index fails, the pack file is deleted /// private bool TryBuildIndex( ITracer activity, string packFullPath, out GitProcess.Result result, GitProcess gitProcess) { result = this.IndexPackFile(packFullPath, gitProcess); if (result.ExitCodeIsFailure) { EventMetadata errorMetadata = CreateEventMetadata(); Exception exception; if (!this.fileSystem.TryDeleteFile(packFullPath, exception: out exception)) { if (exception != null) { errorMetadata.Add("deleteException", exception.ToString()); } errorMetadata.Add("deletedBadPack", "false"); } errorMetadata.Add("Operation", nameof(this.TryBuildIndex)); errorMetadata.Add("packFullPath", packFullPath); activity.RelatedWarning(errorMetadata, result.Errors, Keywords.Telemetry); } return result.ExitCodeIsSuccess; } private void CleanupTempFile(ITracer activity, string fullPath) { Exception e; if (!this.fileSystem.TryDeleteFile(fullPath, exception: out e)) { EventMetadata info = CreateEventMetadata(e); info.Add("file", fullPath); activity.RelatedWarning(info, "Failed to cleanup temp file"); } } private void FinalizeTempFile(string sha, LooseObjectToWrite toWrite, bool overwriteExistingObject) { try { // Checking for existence reduces warning outputs when a streamed download tries. if (this.fileSystem.FileExists(toWrite.ActualFile)) { if (overwriteExistingObject) { EventMetadata metadata = CreateEventMetadata(); metadata.Add("file", toWrite.ActualFile); metadata.Add("tempFile", toWrite.TempFile); metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.FinalizeTempFile)}: Overwriting existing loose object"); this.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.FinalizeTempFile)}_OverwriteExistingObject", metadata); this.ValidateTempFile(toWrite.TempFile, sha); this.fileSystem.MoveAndOverwriteFile(toWrite.TempFile, toWrite.ActualFile); } } else { this.ValidateTempFile(toWrite.TempFile, sha); try { this.fileSystem.MoveFile(toWrite.TempFile, toWrite.ActualFile); } catch (IOException ex) { // IOExceptions happen when someone else is writing to our object. // That implies they are doing what we're doing, which should be a success EventMetadata info = CreateEventMetadata(ex); info.Add("file", toWrite.ActualFile); this.Tracer.RelatedWarning(info, $"{nameof(this.FinalizeTempFile)}: Exception moving temp file"); } } } finally { this.CleanupTempFile(this.Tracer, toWrite.TempFile); } } private void ValidateTempFile(string tempFilePath, string finalFilePath) { using (Stream fs = this.fileSystem.OpenFileStream(tempFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, callFlushFileBuffers: false)) { if (fs.Length == 0) { throw new RetryableException($"Temp file '{tempFilePath}' for '{finalFilePath}' was written with 0 bytes"); } else { byte[] buffer = new byte[10]; // Temp files should always have at least one non-zero byte int bytesRead = fs.Read(buffer, 0, buffer.Length); if (buffer.All(b => b == 0)) { RetryableException ex = new RetryableException( $"Temp file '{tempFilePath}' for '{finalFilePath}' was written with {bytesRead} null bytes"); EventMetadata eventInfo = CreateEventMetadata(ex); eventInfo.Add("file", tempFilePath); eventInfo.Add("finalFilePath", finalFilePath); this.Tracer.RelatedWarning(eventInfo, $"{nameof(this.ValidateTempFile)}: Temp file invalid"); throw ex; } } } } private RetryWrapper.CallbackResult TrySavePackOrLooseObject( IEnumerable objectShas, bool unpackObjects, GitEndPointResponseData responseData, GitProcess gitProcess) { if (responseData.ContentType == GitObjectContentType.LooseObject) { List objectShaList = objectShas.Distinct().ToList(); if (objectShaList.Count != 1) { return new RetryWrapper.CallbackResult(new InvalidOperationException("Received loose object when multiple objects were requested."), shouldRetry: false); } // To reduce allocations, reuse the same buffer when writing objects in this batch byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; this.WriteLooseObject(responseData.Stream, objectShaList[0], overwriteExistingObject: false, bufToCopyWith: bufToCopyWith); } else if (responseData.ContentType == GitObjectContentType.BatchedLooseObjects) { // To reduce allocations, reuse the same buffer when writing objects in this batch byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; BatchedLooseObjectDeserializer deserializer = new BatchedLooseObjectDeserializer( responseData.Stream, (stream, sha) => this.WriteLooseObject(stream, sha, overwriteExistingObject: false, bufToCopyWith: bufToCopyWith)); deserializer.ProcessObjects(); } else { GitProcess.Result result = this.TryAddPackFile(responseData.Stream, unpackObjects, gitProcess); if (result.ExitCodeIsFailure) { return new RetryWrapper.CallbackResult(new InvalidOperationException("Could not add pack file: " + result.Errors), shouldRetry: false); } } return new RetryWrapper.CallbackResult(new GitObjectsHttpRequestor.GitObjectTaskResult(true)); } private GitProcess.Result TryAddPackFile(Stream contents, bool unpackObjects, GitProcess gitProcess) { GitProcess.Result result; this.fileSystem.CreateDirectory(this.Enlistment.GitPackRoot); if (unpackObjects) { result = new GitProcess(this.Enlistment).UnpackObjects(contents); } else { string tempPackPath = this.WriteTempPackFile(contents); return this.IndexTempPackFile(tempPackPath, gitProcess); } return result; } private struct LooseObjectToWrite { public readonly string TempFile; public readonly string ActualFile; public LooseObjectToWrite(string tempFile, string actualFile) { this.TempFile = tempFile; this.ActualFile = actualFile; } } private class TempPrefetchPackAndIdx { public TempPrefetchPackAndIdx( long timestamp, string packName, string packFullPath, Task flushTask) { this.Timestamp = timestamp; this.PackName = packName; this.PackTempPath = packFullPath; this.ReadyTask = flushTask; } public long Timestamp { get; } /// /// The final name of the pack file. /// public string PackName { get; } /// /// The location the pack file is at the end of , which may not have the same name as PackName. /// public string PackTempPath { get; set; } /// /// The final name of the index file. /// public string IdxName => GetIndexForPack(PackName); /// /// The location the index file is at the end of , which may not have the same name as IdxName. /// public string IndexTempPath => GetIndexForPack(PackTempPath); /// /// A task indicating the files at and are ready. /// public Task ReadyTask { get; set; } } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitOid.cs ================================================ using System.Runtime.InteropServices; namespace GVFS.Common.Git { [StructLayout(LayoutKind.Sequential)] public struct GitOid { // OIDs are 20 bytes long [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] public byte[] Id; public override string ToString() { return SHA1Util.HexStringFromBytes(this.Id); } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitPathConverter.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace GVFS.Common.Git { public static class GitPathConverter { private const int CharsInOctet = 3; private const char OctetIndicator = '\\'; public static string ConvertPathOctetsToUtf8(string filePath) { if (filePath == null) { return null; } int octetIndicatorIndex = filePath.IndexOf(OctetIndicator); if (octetIndicatorIndex == -1) { return filePath; } StringBuilder converted = new StringBuilder(); List octets = new List(); int index = 0; while (octetIndicatorIndex != -1) { converted.Append(filePath.Substring(index, octetIndicatorIndex - index)); while (octetIndicatorIndex < filePath.Length && filePath[octetIndicatorIndex] == OctetIndicator) { string octet = filePath.Substring(octetIndicatorIndex + 1, CharsInOctet); octets.Add(Convert.ToByte(octet, 8)); octetIndicatorIndex += CharsInOctet + 1; } AddOctetsAsUtf8(converted, octets); index = octetIndicatorIndex; octetIndicatorIndex = filePath.IndexOf(OctetIndicator, octetIndicatorIndex); } AddOctetsAsUtf8(converted, octets); converted.Append(filePath.Substring(index)); return converted.ToString(); } private static void AddOctetsAsUtf8(StringBuilder converted, List octets) { if (octets.Count > 0) { converted.Append(Encoding.UTF8.GetChars(octets.ToArray())); octets.Clear(); } } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitProcess.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; namespace GVFS.Common.Git { public class GitProcess : ICredentialStore { private const int HResultEHANDLE = -2147024890; // 0x80070006 E_HANDLE private static readonly Encoding UTF8NoBOM = new UTF8Encoding(false); private static bool failedToSetEncoding = false; private static string expireTimeDateString; /// /// Lock taken for duration of running executingProcess. /// private object executionLock = new object(); /// /// Lock taken when changing the running state of executingProcess. /// /// Can be taken within executionLock. /// private object processLock = new object(); private string gitBinPath; private string workingDirectoryRoot; private string dotGitRoot; private Process executingProcess; private bool stopping; static GitProcess() { // If the encoding is UTF8, .Net's default behavior will include a BOM // We need to use the BOM-less encoding because Git doesn't understand it if (Console.InputEncoding.CodePage == UTF8NoBOM.CodePage) { try { Console.InputEncoding = UTF8NoBOM; } catch (IOException ex) when (ex.HResult == HResultEHANDLE) { // If the standard input for a console is redirected / not available, // then we might not be able to set the InputEncoding here. // In practice, this can happen if we attempt to run a GitProcess from within a Service, // such as GVFS.Service. // Record that we failed to set the encoding, but do not quite the process. // This means that git commands that use stdin will not work, but // for our scenarios, we do not expect these calls at this this time. // We will check and fail if we attempt to write to stdin in in a git call below. GitProcess.failedToSetEncoding = true; } } } public GitProcess(Enlistment enlistment) : this(enlistment.GitBinPath, enlistment.WorkingDirectoryBackingRoot) { } public GitProcess(string gitBinPath, string workingDirectoryRoot) { if (string.IsNullOrWhiteSpace(gitBinPath)) { throw new ArgumentException(nameof(gitBinPath)); } this.gitBinPath = gitBinPath; this.workingDirectoryRoot = workingDirectoryRoot; if (this.workingDirectoryRoot != null) { this.dotGitRoot = Path.Combine(this.workingDirectoryRoot, GVFSConstants.DotGit.Root); } } public static string ExpireTimeDateString { get { if (expireTimeDateString == null) { expireTimeDateString = DateTime.Now.Subtract(TimeSpan.FromDays(1)).ToShortDateString(); } return expireTimeDateString; } } public bool LowerPriority { get; set; } public static Result Init(Enlistment enlistment) { return new GitProcess(enlistment).InvokeGitOutsideEnlistment("init \"" + enlistment.WorkingDirectoryBackingRoot + "\""); } public static ConfigResult GetFromGlobalConfig(string gitBinPath, string settingName) { return new ConfigResult( new GitProcess(gitBinPath, workingDirectoryRoot: null).InvokeGitOutsideEnlistment("config --global " + settingName), settingName); } public static ConfigResult GetFromSystemConfig(string gitBinPath, string settingName) { return new ConfigResult( new GitProcess(gitBinPath, workingDirectoryRoot: null).InvokeGitOutsideEnlistment("config --system " + settingName), settingName); } public static ConfigResult GetFromFileConfig(string gitBinPath, string configFile, string settingName) { return new ConfigResult( new GitProcess(gitBinPath, workingDirectoryRoot: null).InvokeGitOutsideEnlistment("config --file " + configFile + " " + settingName), settingName); } public static bool TryGetVersion(string gitBinPath, out GitVersion gitVersion, out string error) { GitProcess gitProcess = new GitProcess(gitBinPath, null); Result result = gitProcess.InvokeGitOutsideEnlistment("--version"); string version = result.Output; if (result.ExitCodeIsFailure || !GitVersion.TryParseGitVersionCommandResult(version, out gitVersion)) { gitVersion = null; error = "Unable to determine installed git version. " + version; return false; } error = null; return true; } /// /// Tries to kill the run git process. Make sure you only use this on git processes that can safely be killed! /// /// Name of the running process /// Exit code of the kill. -1 means there was no running process. /// Error message of the kill /// public bool TryKillRunningProcess(out string processName, out int exitCode, out string error) { this.stopping = true; processName = null; exitCode = -1; error = null; lock (this.processLock) { Process process = this.executingProcess; if (process != null) { processName = process.ProcessName; return GVFSPlatform.Instance.TryKillProcessTree(process.Id, out exitCode, out error); } return true; } } public virtual bool TryDeleteCredential(ITracer tracer, string repoUrl, string username, string password, out string errorMessage) { StringBuilder sb = new StringBuilder(); sb.AppendFormat("url={0}\n", repoUrl); // Passing the username and password that we want to signal rejection for is optional. // Credential helpers that support it can use the provided username/password values to // perform a check that they're being asked to delete the same stored credential that // the caller is asking them to erase. // Ideally, we would provide these values if available, however it does not work as expected // with our main credential helper - Windows GCM. With GCM for Windows, the credential acquired // with credential fill for dev.azure.com URLs are not erased when the user name / password are passed in. // Until the default credential helper works with this pattern, reject credential with just the URL. sb.Append("\n"); string stdinConfig = sb.ToString(); Result result = this.InvokeGitAgainstDotGitFolder( GenerateCredentialVerbCommand("reject"), stdin => stdin.Write(stdinConfig), null); if (result.ExitCodeIsFailure) { tracer.RelatedWarning("Git could not reject credentials: {0}", result.Errors); errorMessage = result.Errors; return false; } errorMessage = null; return true; } public virtual bool TryStoreCredential(ITracer tracer, string repoUrl, string username, string password, out string errorMessage) { StringBuilder sb = new StringBuilder(); sb.AppendFormat("url={0}\n", repoUrl); sb.AppendFormat("username={0}\n", username); sb.AppendFormat("password={0}\n", password); sb.Append("\n"); string stdinConfig = sb.ToString(); Result result = this.InvokeGitAgainstDotGitFolder( GenerateCredentialVerbCommand("approve"), stdin => stdin.Write(stdinConfig), null); if (result.ExitCodeIsFailure) { tracer.RelatedWarning("Git could not approve credentials: {0}", result.Errors); errorMessage = result.Errors; return false; } errorMessage = null; return true; } /// /// Input for certificate credentials looks like /// protocol=cert /// path=[http.sslCert value] /// username = /// public virtual bool TryGetCertificatePassword( ITracer tracer, string certificatePath, out string password, out string errorMessage) { password = null; errorMessage = null; using (ITracer activity = tracer.StartActivity("TryGetCertificatePassword", EventLevel.Informational)) { Result gitCredentialOutput = this.InvokeGitAgainstDotGitFolder( "credential fill", stdin => stdin.Write("protocol=cert\npath=" + certificatePath + "\nusername=\n\n"), parseStdOutLine: null); if (gitCredentialOutput.ExitCodeIsFailure) { EventMetadata errorData = new EventMetadata(); errorData.Add("CertificatePath", certificatePath); tracer.RelatedWarning( errorData, "Git could not get credentials: " + gitCredentialOutput.Errors, Keywords.Network | Keywords.Telemetry); errorMessage = gitCredentialOutput.Errors; return false; } password = ParseValue(gitCredentialOutput.Output, "password="); bool success = password != null; EventMetadata metadata = new EventMetadata { { "Success", success }, { "CertificatePath", certificatePath } }; if (!success) { metadata.Add("Output", gitCredentialOutput.Output); } activity.Stop(metadata); return success; } } public virtual bool TryGetCredential( ITracer tracer, string repoUrl, out string username, out string password, out string errorMessage) { username = null; password = null; errorMessage = null; using (ITracer activity = tracer.StartActivity(nameof(this.TryGetCredential), EventLevel.Informational)) { Result gitCredentialOutput = this.InvokeGitAgainstDotGitFolder( GenerateCredentialVerbCommand("fill"), stdin => stdin.Write($"url={repoUrl}\n\n"), parseStdOutLine: null); if (gitCredentialOutput.ExitCodeIsFailure) { EventMetadata errorData = new EventMetadata(); tracer.RelatedWarning( errorData, "Git could not get credentials: " + gitCredentialOutput.Errors, Keywords.Network | Keywords.Telemetry); errorMessage = gitCredentialOutput.Errors; return false; } username = ParseValue(gitCredentialOutput.Output, "username="); password = ParseValue(gitCredentialOutput.Output, "password="); bool success = username != null && password != null; EventMetadata metadata = new EventMetadata(); metadata.Add("Success", success); if (!success) { metadata.Add("Output", gitCredentialOutput.Output); } activity.Stop(metadata); return success; } } public bool IsValidRepo() { Result result = this.InvokeGitAgainstDotGitFolder("rev-parse --show-toplevel"); return result.ExitCodeIsSuccess; } public Result RevParse(string gitRef) { return this.InvokeGitAgainstDotGitFolder("rev-parse " + gitRef); } public Result GetCurrentBranchName() { return this.InvokeGitAgainstDotGitFolder("name-rev --name-only HEAD"); } public void DeleteFromLocalConfig(string settingName) { this.InvokeGitAgainstDotGitFolder("config --local --unset-all " + settingName); } public Result SetInLocalConfig(string settingName, string value, bool replaceAll = false) { return this.InvokeGitAgainstDotGitFolder(string.Format( "config --local {0} \"{1}\" \"{2}\"", replaceAll ? "--replace-all " : string.Empty, settingName, value)); } public Result AddInLocalConfig(string settingName, string value) { return this.InvokeGitAgainstDotGitFolder(string.Format( "config --local --add {0} {1}", settingName, value)); } public Result SetInFileConfig(string configFile, string settingName, string value, bool replaceAll = false) { return this.InvokeGitOutsideEnlistment(string.Format( "config --file {0} {1} \"{2}\" \"{3}\"", configFile, replaceAll ? "--replace-all " : string.Empty, settingName, value)); } public bool TryGetConfigUrlMatch(string section, string repositoryUrl, out Dictionary configSettings) { Result result = this.InvokeGitAgainstDotGitFolder($"config --get-urlmatch {section} {repositoryUrl}"); if (result.ExitCodeIsFailure) { configSettings = null; return false; } configSettings = GitConfigHelper.ParseKeyValues(result.Output, ' '); return true; } public bool TryGetAllConfig(bool localOnly, out Dictionary configSettings) { configSettings = null; string localParameter = localOnly ? "--local" : string.Empty; ConfigResult result = new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --list " + localParameter), "--list"); if (result.TryParseAsString(out string output, out string _, string.Empty)) { configSettings = GitConfigHelper.ParseKeyValues(output); return true; } return false; } /// /// Get the config value give a setting name /// /// The name of the config setting /// /// If false, will run the call from inside the enlistment if the working dir found, /// otherwise it will run it from outside the enlistment. /// /// The value found for the setting. public virtual ConfigResult GetFromConfig(string settingName, bool forceOutsideEnlistment = false, PhysicalFileSystem fileSystem = null) { string command = string.Format("config {0}", settingName); fileSystem = fileSystem ?? new PhysicalFileSystem(); // This method is called at clone time, so the physical repo may not exist yet. return fileSystem.DirectoryExists(this.workingDirectoryRoot) && !forceOutsideEnlistment ? new ConfigResult(this.InvokeGitAgainstDotGitFolder(command), settingName) : new ConfigResult(this.InvokeGitOutsideEnlistment(command), settingName); } public ConfigResult GetFromLocalConfig(string settingName) { return new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --local " + settingName), settingName); } /// /// Safely gets the config value give a setting name /// /// The name of the config setting /// /// If false, will run the call from inside the enlistment if the working dir found, /// otherwise it will run it from outside the enlistment. /// /// The value found for the config setting. /// True if the config call was successful, false otherwise. public bool TryGetFromConfig(string settingName, bool forceOutsideEnlistment, out string value, PhysicalFileSystem fileSystem = null) { value = null; try { ConfigResult result = this.GetFromConfig(settingName, forceOutsideEnlistment, fileSystem); return result.TryParseAsString(out value, out string _); } catch { } return false; } public ConfigResult GetOriginUrl() { /* Disable precommand hook because this config call is used during mounting process * which needs to be able to fix a bad precommand hook configuration. */ return new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --local remote.origin.url", usePreCommandHook: false), "remote.origin.url"); } public Result DiffTree(string sourceTreeish, string targetTreeish, Action onResult) { return this.InvokeGitAgainstDotGitFolder("diff-tree -r -t " + sourceTreeish + " " + targetTreeish, null, onResult); } public Result CreateBranchWithUpstream(string branchToCreate, string upstreamBranch) { return this.InvokeGitAgainstDotGitFolder("branch " + branchToCreate + " --track " + upstreamBranch); } public Result ForceCheckout(string target) { return this.InvokeGitInWorkingDirectoryRoot("checkout -f " + target, useReadObjectHook: false); } public Result Reset(string target, string paths) { return this.InvokeGitInWorkingDirectoryRoot($"reset {target} {paths}", useReadObjectHook: false); } public Result Status(bool allowObjectDownloads, bool useStatusCache, bool showUntracked = false) { string command = "status"; if (!useStatusCache) { command += " --no-deserialize"; } if (showUntracked) { command += " -uall"; } return this.InvokeGitInWorkingDirectoryRoot(command, useReadObjectHook: allowObjectDownloads); } public Result StatusPorcelain() { string command = "status -uall --porcelain -z"; return this.InvokeGitInWorkingDirectoryRoot(command, useReadObjectHook: false); } /// /// Returns staged file changes (index vs HEAD) as null-separated pairs of /// status and path: "A\0path1\0M\0path2\0D\0path3\0". /// Status codes: A=added, M=modified, D=deleted, R=renamed, C=copied. /// /// Inline pathspecs to scope the diff, or null for all. /// /// Path to a file containing additional pathspecs (one per line), forwarded /// as --pathspec-from-file to git. Null if not used. /// /// /// When true and pathspecFromFile is set, pathspec entries in the file are /// separated by NUL instead of newline (--pathspec-file-nul). /// public Result DiffCachedNameStatus(string[] pathspecs = null, string pathspecFromFile = null, bool pathspecFileNul = false) { string command = "diff --cached --name-status -z --no-renames"; if (pathspecFromFile != null) { command += " --pathspec-from-file=" + QuoteGitPath(pathspecFromFile); if (pathspecFileNul) { command += " --pathspec-file-nul"; } } if (pathspecs != null && pathspecs.Length > 0) { command += " -- " + string.Join(" ", pathspecs.Select(p => QuoteGitPath(p))); } return this.InvokeGitInWorkingDirectoryRoot(command, useReadObjectHook: false); } /// /// Writes the staged (index) version of the specified files to the working /// tree with correct line endings and attributes. Batches multiple paths into /// a single git process invocation where possible, respecting the Windows /// command line length limit. /// public List CheckoutIndexForFiles(IEnumerable paths) { // Windows command line limit is 32,767 characters. Leave headroom for // the base command and other arguments. const int MaxCommandLength = 30000; const string BaseCommand = "-c core.hookspath= checkout-index --force --"; List results = new List(); StringBuilder command = new StringBuilder(BaseCommand); foreach (string path in paths) { string quotedPath = " " + QuoteGitPath(path); if (command.Length + quotedPath.Length > MaxCommandLength && command.Length > BaseCommand.Length) { // Flush current batch results.Add(this.InvokeGitInWorkingDirectoryRoot(command.ToString(), useReadObjectHook: false)); command.Clear(); command.Append(BaseCommand); } command.Append(quotedPath); } // Flush remaining paths if (command.Length > BaseCommand.Length) { results.Add(this.InvokeGitInWorkingDirectoryRoot(command.ToString(), useReadObjectHook: false)); } return results; } /// /// Wraps a path in double quotes for use as a git command argument, /// escaping any embedded double quotes and any backslashes that /// immediately precede a double quote (to prevent them from being /// interpreted as escape characters by the Windows C runtime argument /// parser). Lone backslashes used as path separators are left as-is. /// public static string QuoteGitPath(string path) { StringBuilder sb = new StringBuilder(path.Length + 4); sb.Append('"'); for (int i = 0; i < path.Length; i++) { if (path[i] == '"') { sb.Append('\\'); sb.Append('"'); } else if (path[i] == '\\') { // Count consecutive backslashes int backslashCount = 0; while (i < path.Length && path[i] == '\\') { backslashCount++; i++; } if (i < path.Length && path[i] == '"') { // Backslashes before a quote: double them all, then escape the quote sb.Append('\\', backslashCount * 2); sb.Append('\\'); sb.Append('"'); } else if (i == path.Length) { // Backslashes at end of string (before closing quote): double them sb.Append('\\', backslashCount * 2); } else { // Backslashes not before a quote: keep as-is (path separators) sb.Append('\\', backslashCount); i--; // Re-process current non-backslash char } } else { sb.Append(path[i]); } } sb.Append('"'); return sb.ToString(); } 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) { return this.InvokeGitAgainstDotGitFolder( "unpack-objects", stdin => { packFileStream.CopyTo(stdin.BaseStream); stdin.Write('\n'); }, null); } public Result PackObjects(string filenamePrefix, string gitObjectsDirectory, Action packFileStream) { string packFilePath = Path.Combine(gitObjectsDirectory, GVFSConstants.DotGit.Objects.Pack.Name, filenamePrefix); // Since we don't provide paths we won't be able to complete good deltas // avoid the unnecessary computation by setting window/depth to 0 return this.InvokeGitAgainstDotGitFolder( $"pack-objects {packFilePath} --non-empty --window=0 --depth=0 -q", packFileStream, parseStdOutLine: null, gitObjectsDirectory: gitObjectsDirectory); } /// /// Write a new commit graph in the specified pack directory. Crawl the given pack- /// indexes for commits and then close under everything reachable or exists in the /// previous graph file. /// /// This will update the graph-head file to point to the new commit graph and delete /// any expired graph files that previously existed. /// public Result WriteCommitGraph(string objectDir, List packs) { // Do not expire commit-graph files that have been modified in the last hour. // This will prevent deleting any commit-graph files that are currently in the commit-graph-chain. string command = $"commit-graph write --stdin-packs --split --size-multiple=4 --expire-time={ExpireTimeDateString} --object-dir \"{objectDir}\""; return this.InvokeGitInWorkingDirectoryRoot( command, useReadObjectHook: true, writeStdIn: writer => { foreach (string packIndex in packs) { writer.WriteLine(packIndex); } // We need to close stdin or else the process will not terminate. writer.Close(); }); } public Result VerifyCommitGraph(string objectDir) { string command = "commit-graph verify --shallow --object-dir \"" + objectDir + "\""; return this.InvokeGitInWorkingDirectoryRoot(command, useReadObjectHook: true); } public Result IndexPack(string packfilePath, string idxOutputPath) { /* Git's default thread count is Environment.ProcessorCount / 2, with a maximum of 20. * Testing shows that we can get a 5% decrease in gvfs clone time for large repositories by using more threads, but * we won't go over ProcessorCount or 20. */ var threadCount = Math.Min(Environment.ProcessorCount, 20); string command = $"index-pack --threads={threadCount} -o \"{idxOutputPath}\" \"{packfilePath}\""; // If index-pack is invoked within an enlistment, then it reads all the other objects and pack indexes available // in the enlistment in order to verify references from within this pack file, even if --verify or similar // options are not passed. // Since we aren't verifying, we invoke index-pack outside the enlistment for performance. return this.InvokeGitOutsideEnlistment(command); } /// /// Write a new multi-pack-index (MIDX) in the specified pack directory. /// /// If no new packfiles are found, then this is a no-op. /// public Result WriteMultiPackIndex(string objectDir) { // We override the config settings so we keep writing the MIDX file even if it is disabled for reads. return this.InvokeGitAgainstDotGitFolder("-c core.multiPackIndex=true multi-pack-index write --object-dir=\"" + objectDir + "\" --no-progress"); } public Result VerifyMultiPackIndex(string objectDir) { return this.InvokeGitAgainstDotGitFolder("-c core.multiPackIndex=true multi-pack-index verify --object-dir=\"" + objectDir + "\" --no-progress"); } public Result RemoteAdd(string remoteName, string url) { return this.InvokeGitAgainstDotGitFolder("remote add " + remoteName + " " + url); } public Result CatFileGetType(string objectId) { return this.InvokeGitAgainstDotGitFolder("cat-file -t " + objectId); } public Result LsTree(string treeish, Action parseStdOutLine, bool recursive, bool showAllTrees = false, bool showDirectories = false) { return this.InvokeGitAgainstDotGitFolder( "ls-tree " + (recursive ? "-r " : string.Empty) + (showAllTrees ? "-t " : string.Empty) + (showDirectories ? "-d " : string.Empty) + treeish, null, parseStdOutLine); } public Result LsFiles(Action parseStdOutLine) { return this.InvokeGitInWorkingDirectoryRoot( "ls-files -v", useReadObjectHook: false, parseStdOutLine: parseStdOutLine); } public Result SetUpstream(string branchName, string upstream) { return this.InvokeGitAgainstDotGitFolder("branch --set-upstream-to=" + upstream + " " + branchName); } public Result UpdateBranchSymbolicRef(string refToUpdate, string targetRef) { return this.InvokeGitAgainstDotGitFolder("symbolic-ref " + refToUpdate + " " + targetRef); } public Result UpdateBranchSha(string refToUpdate, string targetSha) { // If oldCommitResult doesn't fail, then the branch exists and update-ref will want the old sha Result oldCommitResult = this.RevParse(refToUpdate); string oldSha = string.Empty; if (oldCommitResult.ExitCodeIsSuccess) { oldSha = oldCommitResult.Output.TrimEnd('\n'); } return this.InvokeGitAgainstDotGitFolder("update-ref --no-deref " + refToUpdate + " " + targetSha + " " + oldSha); } public Result ReadTree(string treeIsh) { return this.InvokeGitAgainstDotGitFolder("read-tree " + treeIsh); } public Result PrunePacked(string gitObjectDirectory) { return this.InvokeGitAgainstDotGitFolder( "prune-packed -q", writeStdIn: null, parseStdOutLine: null, gitObjectsDirectory: gitObjectDirectory); } public Result MultiPackIndexExpire(string gitObjectDirectory) { return this.InvokeGitAgainstDotGitFolder($"multi-pack-index expire --object-dir=\"{gitObjectDirectory}\" --no-progress"); } public Result MultiPackIndexRepack(string gitObjectDirectory, string batchSize) { return this.InvokeGitAgainstDotGitFolder($"-c pack.threads=1 -c repack.packKeptObjects=true multi-pack-index repack --object-dir=\"{gitObjectDirectory}\" --batch-size={batchSize} --no-progress"); } public Process GetGitProcess(string command, string workingDirectory, string dotGitDirectory, bool useReadObjectHook, bool redirectStandardError, string gitObjectsDirectory, bool usePreCommandHook) { ProcessStartInfo processInfo = new ProcessStartInfo(this.gitBinPath); processInfo.WorkingDirectory = workingDirectory; processInfo.UseShellExecute = false; processInfo.RedirectStandardInput = true; processInfo.RedirectStandardOutput = true; processInfo.RedirectStandardError = redirectStandardError; processInfo.WindowStyle = ProcessWindowStyle.Hidden; processInfo.CreateNoWindow = true; processInfo.StandardOutputEncoding = UTF8NoBOM; processInfo.StandardErrorEncoding = UTF8NoBOM; // Removing trace variables that might change git output and break parsing // List of environment variables: https://git-scm.com/book/gr/v2/Git-Internals-Environment-Variables foreach (string key in processInfo.EnvironmentVariables.Keys.Cast().ToList()) { // If GIT_TRACE is set to a fully-rooted path, then Git sends the trace // output to that path instead of stdout (GIT_TRACE=1) or stderr (GIT_TRACE=2). if (key.StartsWith("GIT_TRACE", StringComparison.OrdinalIgnoreCase)) { try { if (!Path.IsPathRooted(processInfo.EnvironmentVariables[key])) { processInfo.EnvironmentVariables.Remove(key); } } catch (ArgumentException) { processInfo.EnvironmentVariables.Remove(key); } } } processInfo.EnvironmentVariables["GIT_TERMINAL_PROMPT"] = "0"; processInfo.EnvironmentVariables["GCM_VALIDATE"] = "0"; if (gitObjectsDirectory != null) { processInfo.EnvironmentVariables["GIT_OBJECT_DIRECTORY"] = gitObjectsDirectory; } if (!useReadObjectHook) { command = "-c " + GitConfigSetting.CoreVirtualizeObjectsName + "=false " + command; } if (!usePreCommandHook) { processInfo.EnvironmentVariables["COMMAND_HOOK_LOCK"] = "true"; } if (!string.IsNullOrEmpty(dotGitDirectory)) { command = "--git-dir=\"" + dotGitDirectory + "\" " + command; } processInfo.Arguments = command; Process executingProcess = new Process(); executingProcess.StartInfo = processInfo; return executingProcess; } protected virtual Result InvokeGitImpl( string command, string workingDirectory, string dotGitDirectory, bool useReadObjectHook, Action writeStdIn, Action parseStdOutLine, int timeoutMs, string gitObjectsDirectory = null, bool usePreCommandHook = true) { if (failedToSetEncoding && writeStdIn != null) { return new Result(string.Empty, "Attempting to use to stdin, but the process does not have the right input encodings set.", Result.GenericFailureCode); } try { // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx // To avoid deadlocks, use asynchronous read operations on at least one of the streams. // Do not perform a synchronous read to the end of both redirected streams. using (this.executingProcess = this.GetGitProcess(command, workingDirectory, dotGitDirectory, useReadObjectHook, redirectStandardError: true, gitObjectsDirectory: gitObjectsDirectory, usePreCommandHook: usePreCommandHook)) { StringBuilder output = new StringBuilder(); StringBuilder errors = new StringBuilder(); this.executingProcess.ErrorDataReceived += (sender, args) => { if (args.Data != null) { errors.Append(args.Data + "\n"); } }; this.executingProcess.OutputDataReceived += (sender, args) => { if (args.Data != null) { if (parseStdOutLine != null) { parseStdOutLine(args.Data); } else { output.Append(args.Data + "\n"); } } }; lock (this.executionLock) { lock (this.processLock) { if (this.stopping) { return new Result(string.Empty, nameof(GitProcess) + " is stopping", Result.GenericFailureCode); } this.executingProcess.Start(); if (this.LowerPriority) { try { this.executingProcess.PriorityClass = ProcessPriorityClass.BelowNormal; } catch (InvalidOperationException) { // This is thrown if the process completes before we can set its priority. } } } writeStdIn?.Invoke(this.executingProcess.StandardInput); this.executingProcess.StandardInput.Close(); this.executingProcess.BeginOutputReadLine(); this.executingProcess.BeginErrorReadLine(); if (!this.executingProcess.WaitForExit(timeoutMs)) { this.executingProcess.Kill(); return new Result(output.ToString(), "Operation timed out: " + errors.ToString(), Result.GenericFailureCode); } } return new Result(output.ToString(), errors.ToString(), this.executingProcess.ExitCode); } } catch (Win32Exception e) { return new Result(string.Empty, e.Message, Result.GenericFailureCode); } finally { this.executingProcess = null; } } private static string GenerateCredentialVerbCommand(string verb) { return $"-c {GitConfigSetting.CredentialUseHttpPath}=true credential {verb}"; } private static string ParseValue(string contents, string prefix) { int startIndex = contents.IndexOf(prefix) + prefix.Length; if (startIndex >= 0 && startIndex < contents.Length) { int endIndex = contents.IndexOf('\n', startIndex); if (endIndex >= 0 && endIndex < contents.Length) { return contents .Substring(startIndex, endIndex - startIndex) .Trim('\r'); } } return null; } /// /// Invokes git.exe without a working directory set. /// /// /// For commands where git doesn't need to be (or can't be) run from inside an enlistment. /// eg. 'git init' or 'git version' /// private Result InvokeGitOutsideEnlistment(string command) { return this.InvokeGitOutsideEnlistment(command, null, null); } private Result InvokeGitOutsideEnlistment( string command, Action writeStdIn, Action parseStdOutLine, int timeout = -1) { return this.InvokeGitImpl( command, workingDirectory: Environment.SystemDirectory, dotGitDirectory: null, useReadObjectHook: false, writeStdIn: writeStdIn, parseStdOutLine: parseStdOutLine, timeoutMs: timeout); } /// /// Invokes git.exe from an enlistment's repository root /// private Result InvokeGitInWorkingDirectoryRoot( string command, bool useReadObjectHook, Action writeStdIn = null, Action parseStdOutLine = null) { return this.InvokeGitImpl( command, workingDirectory: this.workingDirectoryRoot, dotGitDirectory: null, useReadObjectHook: useReadObjectHook, writeStdIn: writeStdIn, parseStdOutLine: parseStdOutLine, timeoutMs: -1); } /// /// Invokes git.exe against an enlistment's .git folder. /// This method should be used only with git-commands that ignore the working directory /// private Result InvokeGitAgainstDotGitFolder(string command, bool usePreCommandHook = true) { return this.InvokeGitAgainstDotGitFolder(command, null, null, usePreCommandHook: usePreCommandHook); } private Result InvokeGitAgainstDotGitFolder( string command, Action writeStdIn, Action parseStdOutLine, bool usePreCommandHook = true, string gitObjectsDirectory = null) { // This git command should not need/use the working directory of the repo. // Run git.exe in Environment.SystemDirectory to ensure the git.exe process // does not touch the working directory return this.InvokeGitImpl( command, workingDirectory: Environment.SystemDirectory, dotGitDirectory: this.dotGitRoot, useReadObjectHook: false, writeStdIn: writeStdIn, parseStdOutLine: parseStdOutLine, timeoutMs: -1, gitObjectsDirectory: gitObjectsDirectory, usePreCommandHook: usePreCommandHook); } public class Result { public const int SuccessCode = 0; public const int GenericFailureCode = 1; public Result(string stdout, string stderr, int exitCode) { this.Output = stdout; this.Errors = stderr; this.ExitCode = exitCode; } public string Output { get; } public string Errors { get; } public int ExitCode { get; } public bool ExitCodeIsSuccess { get { return this.ExitCode == Result.SuccessCode; } } public bool ExitCodeIsFailure { get { return !this.ExitCodeIsSuccess; } } public bool StderrContainsErrors() { if (!string.IsNullOrWhiteSpace(this.Errors)) { return !this.Errors .Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) .All(line => line.TrimStart().StartsWith("warning:", StringComparison.OrdinalIgnoreCase)); } return false; } } public class ConfigResult { private readonly Result result; private readonly string configName; public ConfigResult(Result result, string configName) { this.result = result; this.configName = configName; } public bool TryParseAsString(out string value, out string error, string defaultValue = null) { value = defaultValue; error = string.Empty; if (this.result.ExitCodeIsFailure && this.result.StderrContainsErrors()) { error = "Error while reading '" + this.configName + "' from config: " + this.result.Errors; return false; } if (this.result.ExitCodeIsSuccess) { value = this.result.Output?.TrimEnd('\n'); } return true; } public bool TryParseAsInt(int defaultValue, int minValue, out int value, out string error) { value = defaultValue; error = string.Empty; if (!this.TryParseAsString(out string valueString, out error)) { return false; } 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}` as an int", this.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, this.configName, minValue); return false; } return true; } } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitRefs.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Text; namespace GVFS.Common.Git { public class GitRefs { private const string Head = "HEAD\0"; private const string Master = "master"; private const string HeadRefPrefix = "refs/heads/"; private const string TagsRefPrefix = "refs/tags/"; private const string OriginRemoteRefPrefix = "refs/remotes/origin/"; private Dictionary commitsPerRef; private string remoteHeadCommitId = null; public GitRefs(IEnumerable infoRefsResponse, string branch) { // First 4 characters of a given line are the length of the line and not part of the commit id so // skip them (https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols) this.commitsPerRef = infoRefsResponse .Where(line => line.Contains(" " + HeadRefPrefix) || (line.Contains(" " + TagsRefPrefix) && !line.Contains("^"))) .Where(line => branch == null || line.EndsWith(HeadRefPrefix + branch)) .Select(line => line.Split(' ')) .ToDictionary( line => line[1].Replace(HeadRefPrefix, OriginRemoteRefPrefix), line => line[0].Substring(4)); string lineWithHeadCommit = infoRefsResponse.FirstOrDefault(line => line.Contains(Head)); if (lineWithHeadCommit != null) { string[] tokens = lineWithHeadCommit.Split(' '); if (tokens.Length >= 2 && tokens[1].StartsWith(Head)) { // First 8 characters are not part of the commit id so skip them this.remoteHeadCommitId = tokens[0].Substring(8); } } } public int Count { get { return this.commitsPerRef.Count; } } public string GetTipCommitId(string branch) { return this.commitsPerRef[OriginRemoteRefPrefix + branch]; } public string GetDefaultBranch() { IEnumerable> headRefMatches = this.commitsPerRef.Where(reference => reference.Value == this.remoteHeadCommitId && reference.Key.StartsWith(OriginRemoteRefPrefix)); if (headRefMatches.Count() == 0 || headRefMatches.Count(reference => reference.Key == (OriginRemoteRefPrefix + Master)) > 0) { // Default to master if no HEAD or if the commit ID or the dafult branch matches master (this is // the same behavior as git.exe) return Master; } // If the HEAD commit ID does not match master grab the first branch that matches string defaultBranch = headRefMatches.First().Key; if (defaultBranch.Length < OriginRemoteRefPrefix.Length) { return Master; } return defaultBranch.Substring(OriginRemoteRefPrefix.Length); } /// /// Checks if the specified branch exists (case sensitive) /// public bool HasBranch(string branch) { string branchRef = OriginRemoteRefPrefix + branch; return this.commitsPerRef.ContainsKey(branchRef); } public IEnumerable> GetBranchRefPairs() { return this.commitsPerRef.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)); } public string ToPackedRefs() { StringBuilder sb = new StringBuilder(); const string LF = "\n"; sb.Append("# pack-refs with: peeled fully-peeled" + LF); foreach (string refName in this.commitsPerRef.Keys.OrderBy(key => key)) { sb.Append(this.commitsPerRef[refName] + " " + refName + LF); } return sb.ToString(); } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitRepo.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using System; using System.IO; using System.IO.Compression; using System.Linq; using static GVFS.Common.Git.LibGit2Repo; namespace GVFS.Common.Git { public class GitRepo : IDisposable { private static readonly byte[] LooseBlobHeader = new byte[] { (byte)'b', (byte)'l', (byte)'o', (byte)'b', (byte)' ' }; private ITracer tracer; private PhysicalFileSystem fileSystem; private LibGit2RepoInvoker libgit2RepoInvoker; private Enlistment enlistment; public GitRepo(ITracer tracer, Enlistment enlistment, PhysicalFileSystem fileSystem, Func repoFactory = null) { this.tracer = tracer; this.enlistment = enlistment; this.fileSystem = fileSystem; this.GVFSLock = new GVFSLock(tracer); this.libgit2RepoInvoker = new LibGit2RepoInvoker( tracer, repoFactory ?? (() => new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot))); } // For Unit Testing protected GitRepo(ITracer tracer) { this.GVFSLock = new GVFSLock(tracer); } private enum LooseBlobState { Invalid, Missing, Exists, Corrupt, Unknown, } public GVFSLock GVFSLock { get; private set; } internal LibGit2RepoInvoker LibGit2RepoInvoker { get { return this.libgit2RepoInvoker; } } public void CloseActiveRepo() { this.libgit2RepoInvoker?.DisposeSharedRepo(); } public void OpenRepo() { this.libgit2RepoInvoker?.InitializeSharedRepo(); } public bool TryGetObjectType(string sha, out Native.ObjectTypes? objectType) { return this.libgit2RepoInvoker.TryInvoke(repo => repo.GetObjectType(sha), out objectType); } public virtual bool TryCopyBlobContentStream(string blobSha, Action writeAction) { LooseBlobState state = this.GetLooseBlobState(blobSha, writeAction, out long size); if (state == LooseBlobState.Exists) { return true; } else if (state != LooseBlobState.Missing) { return false; } if (!this.libgit2RepoInvoker.TryInvoke(repo => repo.TryCopyBlob(blobSha, writeAction), out bool copyBlobResult)) { return false; } return copyBlobResult; } public virtual bool CommitAndRootTreeExists(string commitSha, out string rootTreeSha) { bool output = false; string treeShaLocal = null; this.libgit2RepoInvoker.TryInvoke(repo => repo.CommitAndRootTreeExists(commitSha, out treeShaLocal), out output); rootTreeSha = treeShaLocal; return output; } public virtual bool ObjectExists(string blobSha) { bool output = false; this.libgit2RepoInvoker.TryInvoke(repo => repo.ObjectExists(blobSha), out output); return output; } /// /// Try to find the size of a given blob by SHA1 hash. /// /// Returns true iff the blob exists as a loose object. /// public virtual bool TryGetBlobLength(string blobSha, out long size) { return this.GetLooseBlobState(blobSha, null, out size) == LooseBlobState.Exists; } /// /// Try to find the SHAs of subtrees missing from the given tree. /// /// Tree to look up /// SHAs of subtrees of this tree which are not downloaded yet. /// public virtual bool TryGetMissingSubTrees(string treeSha, out string[] subtrees) { string[] missingSubtrees = null; var succeeded = this.libgit2RepoInvoker.TryInvoke(repo => repo.GetMissingSubTrees(treeSha), out missingSubtrees); subtrees = missingSubtrees; return succeeded; } public void Dispose() { if (this.libgit2RepoInvoker != null) { this.libgit2RepoInvoker.Dispose(); this.libgit2RepoInvoker = null; } } private static bool ReadLooseObjectHeader(Stream input, out long size) { size = 0; byte[] buffer = new byte[5]; input.Read(buffer, 0, buffer.Length); if (!Enumerable.SequenceEqual(buffer, LooseBlobHeader)) { return false; } while (true) { int v = input.ReadByte(); if (v == -1) { return false; } if (v == '\0') { break; } size = (size * 10) + (v - '0'); } return true; } private LooseBlobState GetLooseBlobStateAtPath(string blobPath, Action writeAction, out long size) { bool corruptLooseObject = false; try { if (this.fileSystem.FileExists(blobPath)) { using (Stream file = this.fileSystem.OpenFileStream(blobPath, FileMode.Open, FileAccess.Read, FileShare.Read, callFlushFileBuffers: false)) { // The DeflateStream header starts 2 bytes into the gzip header, but they are otherwise compatible file.Position = 2; using (DeflateStream deflate = new DeflateStream(file, CompressionMode.Decompress)) { if (!ReadLooseObjectHeader(deflate, out size)) { corruptLooseObject = true; return LooseBlobState.Corrupt; } writeAction?.Invoke(deflate, size); return LooseBlobState.Exists; } } } size = -1; return LooseBlobState.Missing; } catch (InvalidDataException ex) { corruptLooseObject = true; EventMetadata metadata = new EventMetadata(); metadata.Add("blobPath", blobPath); metadata.Add("Exception", ex.ToString()); this.tracer.RelatedWarning(metadata, nameof(this.GetLooseBlobStateAtPath) + ": Failed to stream blob (InvalidDataException)", Keywords.Telemetry); size = -1; return LooseBlobState.Corrupt; } catch (IOException ex) { EventMetadata metadata = new EventMetadata(); metadata.Add("blobPath", blobPath); metadata.Add("Exception", ex.ToString()); this.tracer.RelatedWarning(metadata, nameof(this.GetLooseBlobStateAtPath) + ": Failed to stream blob from disk", Keywords.Telemetry); size = -1; return LooseBlobState.Unknown; } finally { if (corruptLooseObject) { string corruptBlobsFolderPath = Path.Combine(this.enlistment.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot, GVFSConstants.DotGVFS.CorruptObjectsName); string corruptBlobPath = Path.Combine(corruptBlobsFolderPath, Path.GetRandomFileName()); EventMetadata metadata = new EventMetadata(); metadata.Add("blobPath", blobPath); metadata.Add("corruptBlobPath", corruptBlobPath); metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.GetLooseBlobStateAtPath) + ": Renaming corrupt loose object"); this.tracer.RelatedEvent(EventLevel.Informational, nameof(this.GetLooseBlobStateAtPath) + "_RenameCorruptObject", metadata); try { this.fileSystem.CreateDirectory(corruptBlobsFolderPath); this.fileSystem.MoveFile(blobPath, corruptBlobPath); } catch (Exception e) { metadata = new EventMetadata(); metadata.Add("blobPath", blobPath); metadata.Add("blobBackupPath", corruptBlobPath); metadata.Add("Exception", e.ToString()); metadata.Add(TracingConstants.MessageKey.WarningMessage, nameof(this.GetLooseBlobStateAtPath) + ": Failed to rename corrupt loose object"); this.tracer.RelatedEvent(EventLevel.Warning, nameof(this.GetLooseBlobStateAtPath) + "_RenameCorruptObjectFailed", metadata, Keywords.Telemetry); } } } } private LooseBlobState GetLooseBlobState(string blobSha, Action writeAction, out long size) { // Ensure SHA path is lowercase for case-sensitive filesystems if (GVFSPlatform.Instance.Constants.CaseSensitiveFileSystem) { blobSha = blobSha.ToLower(); } string blobPath = Path.Combine( this.enlistment.GitObjectsRoot, blobSha.Substring(0, 2), blobSha.Substring(2)); LooseBlobState state = this.GetLooseBlobStateAtPath(blobPath, writeAction, out size); if (state == LooseBlobState.Missing) { blobPath = Path.Combine( this.enlistment.LocalObjectsRoot, blobSha.Substring(0, 2), blobSha.Substring(2)); state = this.GetLooseBlobStateAtPath(blobPath, writeAction, out size); } return state; } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitSsl.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using GVFS.Common.X509Certificates; namespace GVFS.Common.Git { public class GitSsl { private readonly string certificatePathOrSubjectCommonName; private readonly bool isCertificatePasswordProtected; private readonly Func createCertificateStore; private readonly CertificateVerifier certificateVerifier; private readonly PhysicalFileSystem fileSystem; public GitSsl( IDictionary configSettings, Func createCertificateStore = null, CertificateVerifier certificateVerifier = null, PhysicalFileSystem fileSystem = null) : this(createCertificateStore, certificateVerifier, fileSystem) { if (configSettings != null) { if (configSettings.TryGetValue(GitConfigSetting.HttpSslCert, out GitConfigSetting sslCerts)) { this.certificatePathOrSubjectCommonName = sslCerts.Values.Last(); } this.isCertificatePasswordProtected = SetBoolSettingOrThrow(configSettings, GitConfigSetting.HttpSslCertPasswordProtected, this.isCertificatePasswordProtected); this.ShouldVerify = SetBoolSettingOrThrow(configSettings, GitConfigSetting.HttpSslVerify, this.ShouldVerify); } } private GitSsl(Func createCertificateStore, CertificateVerifier certificateVerifier, PhysicalFileSystem fileSystem) { this.fileSystem = fileSystem ?? new PhysicalFileSystem(); this.createCertificateStore = createCertificateStore ?? (() => new SystemCertificateStore()); this.certificateVerifier = certificateVerifier ?? new CertificateVerifier(); this.certificatePathOrSubjectCommonName = null; this.isCertificatePasswordProtected = false; // True by default, both to have good default security settings and to match git behavior. // https://git-scm.com/docs/git-config#git-config-httpsslVerify this.ShouldVerify = true; } /// /// Gets a value indicating whether SSL certificates being loaded should be verified. Also used to determine, whether client should verify server SSL certificate. True by default. /// /// true if should verify SSL certificates; otherwise, false. public bool ShouldVerify { get; } public X509Certificate2 GetCertificate(ITracer tracer, GitProcess gitProcess) { if (string.IsNullOrEmpty(this.certificatePathOrSubjectCommonName)) { return null; } EventMetadata metadata = new EventMetadata { { "CertificatePathOrSubjectCommonName", this.certificatePathOrSubjectCommonName }, { "IsCertificatePasswordProtected", this.isCertificatePasswordProtected }, { "ShouldVerify", this.ShouldVerify } }; X509Certificate2 result = this.GetCertificateFromFile(tracer, metadata, gitProcess) ?? this.GetCertificateFromStore(tracer, metadata); if (result == null) { tracer.RelatedError(metadata, $"Certificate {this.certificatePathOrSubjectCommonName} not found"); } return result; } private static bool SetBoolSettingOrThrow(IDictionary configSettings, string settingName, bool currentValue) { if (configSettings.TryGetValue(settingName, out GitConfigSetting settingValues)) { try { return bool.Parse(settingValues.Values.Last()); } catch (FormatException) { throw new InvalidRepoException($"{settingName} git setting did not have a bool-parsable value. Found: {string.Join(" ", settingValues.Values)}"); } } return currentValue; } private static void LogWithAppropriateLevel(ITracer tracer, EventMetadata metadata, IEnumerable certificates, string logMessage) { int numberOfCertificates = certificates.Count(); switch (numberOfCertificates) { case 0: tracer.RelatedError(metadata, logMessage); break; case 1: tracer.RelatedInfo(metadata, logMessage); break; default: tracer.RelatedWarning(metadata, logMessage); break; } } private static string GetSubjectNameLineForLogging(IEnumerable certificates) { return string.Join( Environment.NewLine, certificates.Select(x => x.Subject)); } private string GetCertificatePassword(ITracer tracer, GitProcess git) { if (git.TryGetCertificatePassword(tracer, this.certificatePathOrSubjectCommonName, out string password, out string error)) { return password; } return null; } private X509Certificate2 GetCertificateFromFile(ITracer tracer, EventMetadata metadata, GitProcess gitProcess) { string certificatePassword = null; if (this.isCertificatePasswordProtected) { certificatePassword = this.GetCertificatePassword(tracer, gitProcess); if (string.IsNullOrEmpty(certificatePassword)) { tracer.RelatedWarning( metadata, "Git config indicates, that certificate is password protected, but retrieved password was null or empty!"); } metadata.Add("isPasswordSpecified", string.IsNullOrEmpty(certificatePassword)); } if (this.fileSystem.FileExists(this.certificatePathOrSubjectCommonName)) { try { byte[] certificateContent = this.fileSystem.ReadAllBytes(this.certificatePathOrSubjectCommonName); X509Certificate2 cert = new X509Certificate2(certificateContent, certificatePassword); if (this.ShouldVerify && cert != null && !this.certificateVerifier.Verify(cert)) { tracer.RelatedWarning(metadata, "Certficate was found, but is invalid."); return null; } return cert; } catch (CryptographicException cryptEx) { metadata.Add("Exception", cryptEx.ToString()); tracer.RelatedError(metadata, "Error, while loading certificate from disk"); return null; } } return null; } private X509Certificate2 GetCertificateFromStore(ITracer tracer, EventMetadata metadata) { try { using (SystemCertificateStore certificateStore = this.createCertificateStore()) { X509Certificate2Collection findResults = certificateStore.Find( X509FindType.FindBySubjectName, this.certificatePathOrSubjectCommonName, this.ShouldVerify); if (findResults?.Count > 0) { LogWithAppropriateLevel( tracer, metadata, findResults.OfType(), string.Format( "Found {0} certificates by provided name. Matching DNs: {1}", findResults.Count, GetSubjectNameLineForLogging(findResults.OfType()))); X509Certificate2[] certsWithMatchingCns = findResults .OfType() .Where(x => x.HasPrivateKey && Regex.IsMatch(x.Subject, string.Format("(^|,\\s?)CN={0}(,|$)", this.certificatePathOrSubjectCommonName))) // We only want certificates, that have private keys, as we need them. We also want a complete CN match .OrderByDescending(x => this.certificateVerifier.Verify(x)) // Ordering by validity in a descending order will bring valid certificates to the beginning .ThenBy(x => x.NotBefore) // We take the one, that was issued earliest, first .ThenByDescending(x => x.NotAfter) // We then take the one, that is valid for the longest period .ToArray(); LogWithAppropriateLevel( tracer, metadata, certsWithMatchingCns, string.Format( "Found {0} certificates with a private key and an exact CN match. DNs (sorted by priority, will take first): {1}", certsWithMatchingCns.Length, GetSubjectNameLineForLogging(certsWithMatchingCns))); return certsWithMatchingCns.FirstOrDefault(); } } } catch (CryptographicException cryptEx) { metadata.Add("Exception", cryptEx.ToString()); tracer.RelatedError(metadata, "Error, while searching for certificate in store"); return null; } return null; } } } ================================================ FILE: GVFS/GVFS.Common/Git/GitVersion.cs ================================================ using System; namespace GVFS.Common.Git { public class GitVersion { public GitVersion(int major, int minor, int build, string platform = null, int revision = 0, int minorRevision = 0) : this(major, minor, build, null, platform, revision, minorRevision) { } public GitVersion(int major, int minor, int build, int? releaseCandidate = null, string platform = null, int revision = 0, int minorRevision = 0) { this.Major = major; this.Minor = minor; this.Build = build; this.ReleaseCandidate = releaseCandidate; this.Platform = platform; this.Revision = revision; this.MinorRevision = minorRevision; } public int Major { get; private set; } public int Minor { get; private set; } public int Build { get; private set; } public int? ReleaseCandidate { get; private set; } public string Platform { get; private set; } public int Revision { get; private set; } public int MinorRevision { get; private set; } public static bool TryParseGitVersionCommandResult(string input, out GitVersion version) { // git version output is of the form // git version 2.17.0.gvfs.1.preview.3 const string GitVersionExpectedPrefix = "git version "; if (input.StartsWith(GitVersionExpectedPrefix)) { input = input.Substring(GitVersionExpectedPrefix.Length); } return TryParseVersion(input, out version); } public static bool TryParseVersion(string input, out GitVersion version) { version = null; int major, minor, build, revision = 0, minorRevision = 0; int? releaseCandidate = null; string platform = null; if (string.IsNullOrWhiteSpace(input)) { return false; } string[] parsedComponents = input.Split('.'); int numComponents = parsedComponents.Length; // We minimally accept the official Git version number format which // consists of three components: "major.minor.build[.rcN]". // // The other supported formats are the Git for Windows and Microsoft Git // formats which look like: "major.minor.build[.rcN].platform.revision.minorRevision" // 0 1 2 3 4 5 // len 1 2 3 4 5 6 // if (numComponents < 3) { return false; } // Major version if (!TryParseComponent(parsedComponents[0], out major)) { return false; } // Minor version if (!TryParseComponent(parsedComponents[1], out minor)) { return false; } // Build number if (!TryParseComponent(parsedComponents[2], out build)) { return false; } // Release candidate and/or platform // Both of these are optional, but the release candidate is expected to be of the format 'rcN' // where N is a number, helping us distinguish it from a platform string. int platformIdx = 3; if (numComponents >= 4) { string tag = parsedComponents[3]; // Release candidate 'rcN' if (tag.StartsWith("rc", StringComparison.OrdinalIgnoreCase) && tag.Length > 2 && int.TryParse(tag.Substring(2), out int rc) && rc >= 0) { releaseCandidate = rc; // The next component will now be the (optional) platform. // Subsequent components will be revision and minor revision so we need to adjust // the platform index to account for the release candidate. platformIdx = 4; if (numComponents >= 5) { platform = parsedComponents[4]; } } else // Platform string only { platform = tag; } } // Platform revision if (numComponents > platformIdx + 1) { if (!TryParseComponent(parsedComponents[platformIdx + 1], out revision)) { revision = 0; } } // Minor platform revision if (numComponents > platformIdx + 2) { if (!TryParseComponent(parsedComponents[platformIdx + 2], out minorRevision)) { minorRevision = 0; } } version = new GitVersion(major, minor, build, releaseCandidate, platform, revision, minorRevision); return true; } public bool IsEqualTo(GitVersion other) { if (this.Platform != other.Platform) { return false; } return this.CompareVersionNumbers(other) == 0; } public bool IsLessThan(GitVersion other) { return this.CompareVersionNumbers(other) < 0; } public override string ToString() { if (ReleaseCandidate is null) { return $"{Major}.{Minor}.{Build}.{Platform}.{Revision}.{MinorRevision}"; } return $"{Major}.{Minor}.{Build}.rc{ReleaseCandidate}.{Platform}.{Revision}.{MinorRevision}"; } private static bool TryParseComponent(string component, out int parsedComponent) { if (!int.TryParse(component, out parsedComponent)) { return false; } if (parsedComponent < 0) { return false; } return true; } private int CompareVersionNumbers(GitVersion other) { if (other == null) { return -1; } if (this.Major != other.Major) { return this.Major.CompareTo(other.Major); } if (this.Minor != other.Minor) { return this.Minor.CompareTo(other.Minor); } if (this.Build != other.Build) { return this.Build.CompareTo(other.Build); } if (this.ReleaseCandidate != other.ReleaseCandidate) { if (this.ReleaseCandidate.HasValue && other.ReleaseCandidate.HasValue) { return this.ReleaseCandidate.Value.CompareTo(other.ReleaseCandidate.Value); } // If one version has a release candidate and the other does not, // the one without a release candidate is considered "greater than" the one with. return other.ReleaseCandidate.HasValue ? 1 : -1; } if (this.Revision != other.Revision) { return this.Revision.CompareTo(other.Revision); } if (this.MinorRevision != other.MinorRevision) { return this.MinorRevision.CompareTo(other.MinorRevision); } return 0; } } } ================================================ FILE: GVFS/GVFS.Common/Git/HashingStream.cs ================================================ using System; using System.IO; using System.Security.Cryptography; namespace GVFS.Common.Git { public class HashingStream : Stream { private readonly HashAlgorithm hash; private Stream stream; private bool closed; private byte[] hashResult; public HashingStream(Stream stream) { this.stream = stream; this.hash = SHA1.Create(); // CodeQL [SM02196] SHA-1 is acceptable here because this is Git's hashing algorithm, not used for cryptographic purposes this.hashResult = null; this.hash.Initialize(); this.closed = false; } public override bool CanSeek { get { return false; } } public byte[] Hash { get { this.FinishHash(); return this.hashResult; } } public override bool CanRead { get { return true; } } public override long Length { get { return this.stream.Length; } } public override long Position { get { return this.stream.Position; } set { throw new NotImplementedException(); } } public override bool CanWrite { get { return false; } } public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } public override void SetLength(long value) { throw new NotSupportedException(); } public override void Close() { if (!this.closed) { this.FinishHash(); this.closed = true; if (this.stream != null) { this.stream.Close(); } } base.Close(); } public override int Read(byte[] buffer, int offset, int count) { int bytesRead = this.stream.Read(buffer, offset, count); if (bytesRead > 0) { this.hash.TransformBlock(buffer, offset, bytesRead, null, 0); } return bytesRead; } public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } public override void Flush() { throw new NotImplementedException(); } protected override void Dispose(bool disposing) { if (disposing) { if (this.hash != null) { this.hash.Dispose(); } if (this.stream != null) { this.stream.Dispose(); this.stream = null; } } base.Dispose(disposing); } private void FinishHash() { if (this.hashResult == null) { this.hash.TransformFinalBlock(new byte[0], 0, 0); this.hashResult = this.hash.Hash; } } } } ================================================ FILE: GVFS/GVFS.Common/Git/ICredentialStore.cs ================================================ using GVFS.Common.Tracing; namespace GVFS.Common.Git { public interface ICredentialStore { bool TryGetCredential(ITracer tracer, string url, out string username, out string password, out string error); bool TryStoreCredential(ITracer tracer, string url, string username, string password, out string error); bool TryDeleteCredential(ITracer tracer, string url, string username, string password, out string error); } } ================================================ FILE: GVFS/GVFS.Common/Git/IGitInstallation.cs ================================================ namespace GVFS.Common.Git { public interface IGitInstallation { bool GitExists(string gitBinPath); string GetInstalledGitBinPath(); } } ================================================ FILE: GVFS/GVFS.Common/Git/LibGit2Exception.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace GVFS.Common.Git { public class LibGit2Exception : Exception { public LibGit2Exception(string message) : base(message) { } public LibGit2Exception(string message, Exception innerException) : base(message, innerException) { } } } ================================================ FILE: GVFS/GVFS.Common/Git/LibGit2Repo.cs ================================================ using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; namespace GVFS.Common.Git { public class LibGit2Repo : IDisposable { private bool disposedValue = false; public delegate void MultiVarConfigCallback(string value); public LibGit2Repo(ITracer tracer, string repoPath) { this.Tracer = tracer; InitNative(); IntPtr repoHandle; if (TryOpenRepo(repoPath, out repoHandle) != Native.ResultCode.Success) { string reason = GetLastNativeError(); string message = "Couldn't open repo at " + repoPath + ": " + reason; tracer.RelatedWarning(message); if (!reason.EndsWith(" is not owned by current user") || !CheckSafeDirectoryConfigForCaseSensitivityIssue(tracer, repoPath, out repoHandle)) { ShutdownNative(); throw new InvalidDataException(message); } } this.RepoHandle = repoHandle; } protected LibGit2Repo() { this.Tracer = NullTracer.Instance; } ~LibGit2Repo() { this.Dispose(false); } protected ITracer Tracer { get; } protected IntPtr RepoHandle { get; private set; } public Native.ObjectTypes? GetObjectType(string sha) { IntPtr objHandle; if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.ResultCode.Success) { return null; } try { return Native.Object.GetType(objHandle); } finally { Native.Object.Free(objHandle); } } public virtual string GetTreeSha(string commitish) { IntPtr objHandle; if (Native.RevParseSingle(out objHandle, this.RepoHandle, commitish) != Native.ResultCode.Success) { return null; } try { switch (Native.Object.GetType(objHandle)) { case Native.ObjectTypes.Commit: GitOid output = Native.IntPtrToGitOid(Native.Commit.GetTreeId(objHandle)); return output.ToString(); } } finally { Native.Object.Free(objHandle); } return null; } public virtual bool CommitAndRootTreeExists(string commitish, out string treeSha) { treeSha = this.GetTreeSha(commitish); if (treeSha == null) { return false; } return this.ObjectExists(treeSha.ToString()); } public virtual bool ObjectExists(string sha) { IntPtr objHandle; if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.ResultCode.Success) { return false; } Native.Object.Free(objHandle); return true; } public virtual bool TryCopyBlob(string sha, Action writeAction) { IntPtr objHandle; if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.ResultCode.Success) { return false; } try { unsafe { switch (Native.Object.GetType(objHandle)) { case Native.ObjectTypes.Blob: byte* originalData = Native.Blob.GetRawContent(objHandle); long originalSize = Native.Blob.GetRawSize(objHandle); // TODO 938696: UnmanagedMemoryStream marshals content even for CopyTo // If GetRawContent changed to return IntPtr and ProjFS changed WriteBuffer to expose an IntPtr, // We could probably pinvoke memcpy and avoid marshalling. using (Stream mem = new UnmanagedMemoryStream(originalData, originalSize)) { writeAction(mem, originalSize); } break; default: throw new NotSupportedException("Copying object types other than blobs is not supported."); } } } finally { Native.Object.Free(objHandle); } return true; } /// /// Get the list of missing subtrees for the given treeSha. /// /// Tree to look up /// SHAs of subtrees of this tree which are not downloaded yet. public virtual string[] GetMissingSubTrees(string treeSha) { List missingSubtreesList = new List(); IntPtr treeHandle; if (Native.RevParseSingle(out treeHandle, this.RepoHandle, treeSha) != Native.ResultCode.Success || treeHandle == IntPtr.Zero) { return Array.Empty(); } try { if (Native.Object.GetType(treeHandle) != Native.ObjectTypes.Tree) { return Array.Empty(); } uint entryCount = Native.Tree.GetEntryCount(treeHandle); for (uint i = 0; i < entryCount; i++) { if (this.IsMissingSubtree(treeHandle, i, out string entrySha)) { missingSubtreesList.Add(entrySha); } } } finally { Native.Object.Free(treeHandle); } return missingSubtreesList.ToArray(); } /// /// Get a config value from the repo's git config. /// /// Name of the config entry /// The config value, or null if not found. public virtual string GetConfigString(string name) { IntPtr configHandle; if (Native.Config.GetConfig(out configHandle, this.RepoHandle) != Native.ResultCode.Success) { throw new LibGit2Exception($"Failed to get config handle: {Native.GetLastError()}"); } try { string value; Native.ResultCode resultCode = Native.Config.GetString(out value, configHandle, name); if (resultCode == Native.ResultCode.NotFound) { return null; } else if (resultCode != Native.ResultCode.Success) { throw new LibGit2Exception($"Failed to get config value for '{name}': {Native.GetLastError()}"); } return value; } finally { Native.Config.Free(configHandle); } } public virtual bool? GetConfigBool(string name) { IntPtr configHandle; if (Native.Config.GetConfig(out configHandle, this.RepoHandle) != Native.ResultCode.Success) { throw new LibGit2Exception($"Failed to get config handle: {Native.GetLastError()}"); } try { bool value; Native.ResultCode resultCode = Native.Config.GetBool(out value, configHandle, name); if (resultCode == Native.ResultCode.NotFound) { return null; } else if (resultCode != Native.ResultCode.Success) { throw new LibGit2Exception($"Failed to get config value for '{name}': {Native.GetLastError()}"); } return value; } finally { Native.Config.Free(configHandle); } } public void ForEachMultiVarConfig(string key, MultiVarConfigCallback callback) { if (Native.Config.GetConfig(out IntPtr configHandle, this.RepoHandle) != Native.ResultCode.Success) { throw new LibGit2Exception($"Failed to get config handle: {Native.GetLastError()}"); } try { ForEachMultiVarConfig(configHandle, key, callback); } finally { Native.Config.Free(configHandle); } } public static void ForEachMultiVarConfigInGlobalAndSystemConfig(string key, MultiVarConfigCallback callback) { if (Native.Config.GetGlobalAndSystemConfig(out IntPtr configHandle) != Native.ResultCode.Success) { throw new LibGit2Exception($"Failed to get global and system config handle: {Native.GetLastError()}"); } try { ForEachMultiVarConfig(configHandle, key, callback); } finally { Native.Config.Free(configHandle); } } private static void ForEachMultiVarConfig(IntPtr configHandle, string key, MultiVarConfigCallback callback) { Native.Config.GitConfigMultivarCallback nativeCallback = (entryPtr, payload) => { try { var entry = Marshal.PtrToStructure(entryPtr); callback(entry.GetValue()); } catch (Exception) { return Native.ResultCode.Failure; } return 0; }; if (Native.Config.GetMultivarForeach( configHandle, key, regex:"", nativeCallback, IntPtr.Zero) != Native.ResultCode.Success) { throw new LibGit2Exception($"Failed to get multivar config for '{key}': {Native.GetLastError()}"); } } /// /// Determine if the given index of a tree is a subtree and if it is missing. /// If it is a missing subtree, return the SHA of the subtree. /// private bool IsMissingSubtree(IntPtr treeHandle, uint i, out string entrySha) { entrySha = null; IntPtr entryHandle = Native.Tree.GetEntryByIndex(treeHandle, i); if (entryHandle == IntPtr.Zero) { return false; } var entryMode = Native.Tree.GetEntryFileMode(entryHandle); if (entryMode != Native.Tree.TreeEntryFileModeDirectory) { return false; } var entryId = Native.Tree.GetEntryId(entryHandle); if (entryId == IntPtr.Zero) { return false; } var rawEntrySha = Native.IntPtrToGitOid(entryId); entrySha = rawEntrySha.ToString(); if (this.ObjectExists(entrySha)) { return false; } return true; /* Both the entryHandle and the entryId handle are owned by the treeHandle, so we shouldn't free them or it will lead to corruption of the later entries */ } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!this.disposedValue) { Native.Repo.Free(this.RepoHandle); Native.Shutdown(); this.disposedValue = true; } } /// /// Normalize a path for case-insensitive safe.directory comparison: /// replace backslashes with forward slashes, convert to upper-case, /// and trim trailing slashes. /// internal static string NormalizePathForSafeDirectoryComparison(string path) { if (string.IsNullOrEmpty(path)) { return path; } string normalized = path.Replace('\\', '/').ToUpperInvariant(); return normalized.TrimEnd('/'); } /// /// Retrieve all configured safe.directory values from global and system git config. /// Virtual so tests can provide fake entries without touching real config. /// protected virtual void GetSafeDirectoryConfigEntries(MultiVarConfigCallback callback) { ForEachMultiVarConfigInGlobalAndSystemConfig("safe.directory", callback); } /// /// Try to open a repository at the given path. Virtual so tests can /// avoid the native P/Invoke call. /// protected virtual Native.ResultCode TryOpenRepo(string path, out IntPtr repoHandle) { return Native.Repo.Open(out repoHandle, path); } protected virtual void InitNative() { Native.Init(); } protected virtual void ShutdownNative() { Native.Shutdown(); } protected virtual string GetLastNativeError() { return Native.GetLastError(); } protected bool CheckSafeDirectoryConfigForCaseSensitivityIssue(ITracer tracer, string repoPath, out IntPtr repoHandle) { /* Libgit2 has a bug where it is case sensitive for safe.directory (especially the * drive letter) when git.exe isn't. Until a fix can be made and propagated, work * around it by matching the repo path we request to the configured safe directory. * * See https://github.com/libgit2/libgit2/issues/7037 */ repoHandle = IntPtr.Zero; string normalizedRequestedPath = NormalizePathForSafeDirectoryComparison(repoPath); string configuredMatchingDirectory = null; GetSafeDirectoryConfigEntries((string value) => { string normalizedConfiguredPath = NormalizePathForSafeDirectoryComparison(value); if (normalizedConfiguredPath == normalizedRequestedPath) { configuredMatchingDirectory = value; } }); return configuredMatchingDirectory != null && TryOpenRepo(configuredMatchingDirectory, out repoHandle) == Native.ResultCode.Success; } public static class Native { public enum ResultCode : int { Success = 0, Failure = -1, NotFound = -3, } public const string Git2NativeLibName = GVFSConstants.LibGit2LibraryName; public enum ObjectTypes { Commit = 1, Tree = 2, Blob = 3, } public static GitOid IntPtrToGitOid(IntPtr oidPtr) { return Marshal.PtrToStructure(oidPtr); } [DllImport(Git2NativeLibName, EntryPoint = "git_libgit2_init")] public static extern void Init(); [DllImport(Git2NativeLibName, EntryPoint = "git_libgit2_shutdown")] public static extern int Shutdown(); [DllImport(Git2NativeLibName, EntryPoint = "git_revparse_single")] public static extern ResultCode RevParseSingle(out IntPtr objectHandle, IntPtr repoHandle, string oid); public static string GetLastError() { IntPtr ptr = GetLastGitError(); if (ptr == IntPtr.Zero) { return "Operation was successful"; } return Marshal.PtrToStructure(ptr).Message; } [DllImport(Git2NativeLibName, EntryPoint = "giterr_last")] private static extern IntPtr GetLastGitError(); [StructLayout(LayoutKind.Sequential)] private struct GitError { [MarshalAs(UnmanagedType.LPStr)] public string Message; public int Klass; } public static class Repo { [DllImport(Git2NativeLibName, EntryPoint = "git_repository_open")] public static extern ResultCode Open(out IntPtr repoHandle, string path); [DllImport(Git2NativeLibName, EntryPoint = "git_repository_free")] public static extern void Free(IntPtr repoHandle); } public static class Config { [DllImport(Git2NativeLibName, EntryPoint = "git_repository_config")] public static extern ResultCode GetConfig(out IntPtr configHandle, IntPtr repoHandle); [DllImport(Git2NativeLibName, EntryPoint = "git_config_open_default")] public static extern ResultCode GetGlobalAndSystemConfig(out IntPtr configHandle); [DllImport(Git2NativeLibName, EntryPoint = "git_config_get_string")] public static extern ResultCode GetString(out string value, IntPtr configHandle, string name); [DllImport(Git2NativeLibName, EntryPoint = "git_config_get_multivar_foreach")] public static extern ResultCode GetMultivarForeach( IntPtr configHandle, string name, string regex, GitConfigMultivarCallback callback, IntPtr payload); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate ResultCode GitConfigMultivarCallback( IntPtr entryPtr, IntPtr payload); [StructLayout(LayoutKind.Sequential)] public struct GitConfigEntry { public IntPtr Name; public IntPtr Value; public IntPtr BackendType; public IntPtr OriginPath; public uint IncludeDepth; public int Level; public string GetValue() { return Value != IntPtr.Zero ? MarshalUtf8String(Value) : null; } public string GetName() { return Name != IntPtr.Zero ? MarshalUtf8String(Name) : null; } private static string MarshalUtf8String(IntPtr ptr) { if (ptr == IntPtr.Zero) { return null; } int length = 0; while (Marshal.ReadByte(ptr, length) != 0) { length++; } byte[] buffer = new byte[length]; Marshal.Copy(ptr, buffer, 0, length); return System.Text.Encoding.UTF8.GetString(buffer); } } [DllImport(Git2NativeLibName, EntryPoint = "git_config_get_bool")] public static extern ResultCode GetBool(out bool value, IntPtr configHandle, string name); [DllImport(Git2NativeLibName, EntryPoint = "git_config_free")] public static extern void Free(IntPtr configHandle); } public static class Object { [DllImport(Git2NativeLibName, EntryPoint = "git_object_type")] public static extern ObjectTypes GetType(IntPtr objectHandle); [DllImport(Git2NativeLibName, EntryPoint = "git_object_free")] public static extern void Free(IntPtr objHandle); } public static class Commit { /// A handle to an oid owned by LibGit2 [DllImport(Git2NativeLibName, EntryPoint = "git_commit_tree_id")] public static extern IntPtr GetTreeId(IntPtr commitHandle); } public static class Blob { [DllImport(Git2NativeLibName, EntryPoint = "git_blob_rawsize")] [return: MarshalAs(UnmanagedType.U8)] public static extern long GetRawSize(IntPtr objectHandle); [DllImport(Git2NativeLibName, EntryPoint = "git_blob_rawcontent")] public static unsafe extern byte* GetRawContent(IntPtr objectHandle); } public static class Tree { [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entrycount")] public static extern uint GetEntryCount(IntPtr treeHandle); [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_byindex")] public static extern IntPtr GetEntryByIndex(IntPtr treeHandle, uint index); [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_id")] public static extern IntPtr GetEntryId(IntPtr entryHandle); /* git_tree_entry_type requires the object to exist, so we can't use it to check if * a missing entry is a tree. Instead, we can use the file mode to determine if it is a tree. */ [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_filemode")] public static extern uint GetEntryFileMode(IntPtr entryHandle); public const uint TreeEntryFileModeDirectory = 0x4000; } } } } ================================================ FILE: GVFS/GVFS.Common/Git/LibGit2RepoInvoker.cs ================================================ using GVFS.Common.Tracing; using System; using System.Threading; namespace GVFS.Common.Git { public class LibGit2RepoInvoker : IDisposable { private readonly Func createRepo; private readonly ITracer tracer; private readonly object sharedRepoLock = new object(); private volatile bool disposing; private volatile int activeCallers; private LibGit2Repo sharedRepo; public LibGit2RepoInvoker(ITracer tracer, string repoPath) : this(tracer, () => new LibGit2Repo(tracer, repoPath)) { } public LibGit2RepoInvoker(ITracer tracer, Func createRepo) { this.tracer = tracer; this.createRepo = createRepo; this.InitializeSharedRepo(); } public void Dispose() { this.disposing = true; lock (this.sharedRepoLock) { this.sharedRepo?.Dispose(); this.sharedRepo = null; } } public bool TryInvoke(Func function, out TResult result) { try { Interlocked.Increment(ref this.activeCallers); LibGit2Repo repo = this.GetSharedRepo(); if (repo != null) { result = function(repo); return true; } result = default(TResult); return false; } catch (Exception e) { this.tracer.RelatedWarning("Exception while invoking libgit2: " + e.ToString(), Keywords.Telemetry); throw; } finally { Interlocked.Decrement(ref this.activeCallers); } } public void DisposeSharedRepo() { lock (this.sharedRepoLock) { if (this.disposing || this.activeCallers > 0) { return; } this.sharedRepo?.Dispose(); this.sharedRepo = null; } } public void InitializeSharedRepo() { // Run a test on the shared repo to ensure the object store // is loaded, as that is what takes a long time with many packs. // Using a potentially-real object id is important, as the empty // SHA will stop early instead of loading the object store. this.GetSharedRepo()?.ObjectExists("30380be3963a75e4a34e10726795d644659e1129"); } public bool GetConfigBoolOrDefault(string key, bool defaultValue) { bool? value = defaultValue; if (this.TryInvoke(repo => repo.GetConfigBool(key), out value)) { return value ?? defaultValue; } return defaultValue; } private LibGit2Repo GetSharedRepo() { lock (this.sharedRepoLock) { if (this.disposing) { return null; } if (this.sharedRepo == null) { this.sharedRepo = this.createRepo(); } return this.sharedRepo; } } } } ================================================ FILE: GVFS/GVFS.Common/Git/NoOpStream.cs ================================================ using System; using System.IO; namespace GVFS.Common.Git { public class NoOpStream : Stream { public override bool CanRead => false; public override bool CanSeek => false; public override bool CanWrite => true; public override long Length => 0; public override long Position { get => 0; set => throw new NotImplementedException(); } public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } public override void SetLength(long value) { throw new NotImplementedException(); } public override void Write(byte[] buffer, int offset, int count) { } } } ================================================ FILE: GVFS/GVFS.Common/Git/RefLogEntry.cs ================================================ namespace GVFS.Common.Git { public class RefLogEntry { public RefLogEntry(string sourceSha, string targetSha, string reason) { this.SourceSha = sourceSha; this.TargetSha = targetSha; this.Reason = reason; } public string SourceSha { get; } public string TargetSha { get; } public string Reason { get; } public static bool TryParse(string line, out RefLogEntry entry) { entry = null; if (string.IsNullOrEmpty(line)) { return false; } if (line.Length < GVFSConstants.ShaStringLength + 1 + GVFSConstants.ShaStringLength) { return false; } string sourceSha = line.Substring(0, GVFSConstants.ShaStringLength); string targetSha = line.Substring(GVFSConstants.ShaStringLength + 1, GVFSConstants.ShaStringLength); int reasonStart = line.LastIndexOf("\t"); if (reasonStart < 0) { return false; } string reason = line.Substring(reasonStart + 1); entry = new RefLogEntry(sourceSha, targetSha, reason); return true; } } } ================================================ FILE: GVFS/GVFS.Common/Git/RequiredGitConfig.cs ================================================ using System; using System.Collections.Generic; using System.IO; namespace GVFS.Common.Git { /// /// Single source of truth for the git config settings required by GVFS. /// These settings are enforced during clone, mount, and repair. /// public static class RequiredGitConfig { /// /// Returns the dictionary of required git config settings for a GVFS enlistment. /// These settings override any existing local configuration values. /// public static Dictionary GetRequiredSettings(Enlistment enlistment) { string expectedHooksPath = Path.Combine(enlistment.DotGitRoot, GVFSConstants.DotGit.Hooks.RootName); expectedHooksPath = Paths.ConvertPathToGitFormat(expectedHooksPath); // Single-quote the path: git executes core.virtualfilesystem via the // shell (use_shell=1 in virtualfilesystem.c), so spaces in an absolute // path would split the command. Git's config parser strips double quotes // but preserves single quotes, and bash treats single-quoted strings as // a single token. string virtualFileSystemPath = "'" + Paths.ConvertPathToGitFormat( Path.Combine(enlistment.DotGitRoot, GVFSConstants.DotGit.Hooks.RootName, GVFSConstants.DotGit.Hooks.VirtualFileSystemName)) + "'"; string gitStatusCachePath = null; if (!GVFSEnlistment.IsUnattended(tracer: null) && GVFSPlatform.Instance.IsGitStatusCacheSupported()) { gitStatusCachePath = Path.Combine( enlistment.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot, GVFSConstants.DotGVFS.GitStatusCache.CachePath); gitStatusCachePath = Paths.ConvertPathToGitFormat(gitStatusCachePath); } string coreGVFSFlags = Convert.ToInt32( GitCoreGVFSFlags.SkipShaOnIndex | GitCoreGVFSFlags.BlockCommands | GitCoreGVFSFlags.MissingOk | GitCoreGVFSFlags.NoDeleteOutsideSparseCheckout | GitCoreGVFSFlags.FetchSkipReachabilityAndUploadPack | GitCoreGVFSFlags.BlockFiltersAndEolConversions | GitCoreGVFSFlags.SupportsWorktrees) .ToString(); return new Dictionary { // When running 'git am' it will remove the CRs from the patch file by default. This causes the patch to fail to apply because the // file that is getting the patch applied will still have the CRs. There is a --keep-cr option that you can pass the 'git am' command // but since we always want to keep CRs it is better to just set the config setting to always keep them so the user doesn't have to // remember to pass the flag. { "am.keepcr", "true" }, // Update git settings to enable optimizations in git 2.20 // Set 'checkout.optimizeNewBranch=true' to enable optimized 'checkout -b' { "checkout.optimizenewbranch", "true" }, // Enable parallel checkout by auto-detecting the number of workers based on CPU count. { "checkout.workers", "0" }, // We don't support line ending conversions - automatic conversion of LF to Crlf by git would cause un-necessary hydration. Disabling it. { "core.autocrlf", "false" }, // Enable commit graph. https://devblogs.microsoft.com/devops/supercharging-the-git-commit-graph/ { "core.commitGraph", "true" }, // Perf - Git for Windows uses this to bulk-read and cache lstat data of entire directories (instead of doing lstat file by file). { "core.fscache", "true" }, // Turns on all special gvfs logic. https://github.com/microsoft/git/blob/be5e0bb969495c428e219091e6976b52fb33b301/gvfs.h { "core.gvfs", coreGVFSFlags }, // Use 'multi-pack-index' builtin instead of 'midx' to match upstream implementation { "core.multiPackIndex", "true" }, // Perf - Enable parallel index preload for operations like git diff { "core.preloadIndex", "true" }, // VFS4G never wants git to adjust line endings (causes un-necessary hydration of files)- explicitly setting core.safecrlf to false. { "core.safecrlf", "false" }, // Possibly cause hydration while creating untrackedCache. { "core.untrackedCache", "false" }, // This is to match what git init does. { "core.repositoryformatversion", "0" }, // Turn on support for file modes on Mac & Linux. { "core.filemode", GVFSPlatform.Instance.FileSystem.SupportsFileMode ? "true" : "false" }, // For consistency with git init. { "core.bare", "false" }, // For consistency with git init. { "core.logallrefupdates", "true" }, // Git to download objects on demand. { GitConfigSetting.CoreVirtualizeObjectsName, "true" }, // Configure hook that git calls to get the paths git needs to consider for changes or untracked files { GitConfigSetting.CoreVirtualFileSystemName, virtualFileSystemPath }, // Ensure hooks path is configured correctly. { "core.hookspath", expectedHooksPath }, // Hostname is no longer sufficent for VSTS authentication. VSTS now requires dev.azure.com/account to determine the tenant. // By setting useHttpPath, credential managers will get the path which contains the account as the first parameter. They can then use this information for auth appropriately. { GitConfigSetting.CredentialUseHttpPath, "true" }, // Turn off credential validation(https://github.com/microsoft/Git-Credential-Manager-for-Windows/blob/master/Docs/Configuration.md#validate). // We already have logic to call git credential if we get back a 401, so there's no need to validate the PAT each time we ask for it. { "credential.validate", "false" }, // This setting is not needed anymore, because current version of gvfs does not use index.lock. // (This change was introduced initially to prevent `git diff` from acquiring index.lock file.) // Explicitly setting this to true (which also is the default value) because the repo could have been // cloned in the past when autoRefreshIndex used to be set to false. { "diff.autoRefreshIndex", "true" }, // In Git 2.24.0, some new config settings were created. Disable them locally in VFS for Git repos in case a user has set them globally. // https://github.com/microsoft/VFSForGit/pull/1594 // This applies to feature.manyFiles, feature.experimental and fetch.writeCommitGraph settings. { "feature.manyFiles", "false" }, { "feature.experimental", "false" }, { "fetch.writeCommitGraph", "false" }, // Turn off of git garbage collection. Git garbage collection does not work with virtualized object. // We do run maintenance jobs now that do the packing of loose objects so in theory we shouldn't need // this - but it is not hurting anything and it will prevent a gc from getting kicked off if for some // reason the maintenance jobs have not been running and there are too many loose objects { "gc.auto", "0" }, // Prevent git GUI from displaying GC warnings. { "gui.gcwarning", "false" }, // Update git settings to enable optimizations in git 2.20 // Set 'index.threads=true' to enable multi-threaded index reads { "index.threads", "true" }, // index parsing code in VFSForGit currently only supports version 4. { "index.version", "4" }, // Perf - avoid un-necessary blob downloads during a merge. { "merge.stat", "false" }, // Perf - avoid un-necessary blob downloads while git tries to search and find renamed files. { "merge.renames", "false" }, // Don't use bitmaps to determine pack file contents, because we use MIDX for this. { "pack.useBitmaps", "false" }, // Update Git to include sparse push algorithm { "pack.useSparse", "true" }, // Stop automatic git GC { "receive.autogc", "false" }, // Update git settings to enable optimizations in git 2.20 // Set 'reset.quiet=true' to speed up 'git reset " { "reset.quiet", "true" }, // Configure git to use our serialize status file - make git use the serialized status file rather than compute the status by // parsing the index file and going through the files to determine changes. { "status.deserializePath", gitStatusCachePath }, // The GVFS Protocol forbids submodules, so prevent a user's // global config of "status.submoduleSummary=true" from causing // extreme slowness in "git status" { "status.submoduleSummary", "false" }, // Generation number v2 isn't ready for full use. Wait for v3. { "commitGraph.generationVersion", "1" }, // Disable the builtin FS Monitor in case it was enabled globally. { "core.useBuiltinFSMonitor", "false" }, }; } } } ================================================ FILE: GVFS/GVFS.Common/Git/Sha1Id.cs ================================================ using System; using System.Runtime.InteropServices; namespace GVFS.Common.Git { [StructLayout(LayoutKind.Explicit, Size = ShaBufferLength, Pack = 1)] public struct Sha1Id { public static readonly Sha1Id None = new Sha1Id(); private const int ShaBufferLength = (2 * sizeof(ulong)) + sizeof(uint); private const int ShaStringLength = 2 * ShaBufferLength; [FieldOffset(0)] private ulong shaBytes1Through8; [FieldOffset(8)] private ulong shaBytes9Through16; [FieldOffset(16)] private uint shaBytes17Through20; public Sha1Id(ulong shaBytes1Through8, ulong shaBytes9Through16, uint shaBytes17Through20) { this.shaBytes1Through8 = shaBytes1Through8; this.shaBytes9Through16 = shaBytes9Through16; this.shaBytes17Through20 = shaBytes17Through20; } public Sha1Id(string sha) { if (sha == null) { throw new ArgumentNullException(nameof(sha)); } if (sha.Length != ShaStringLength) { throw new ArgumentException($"Must be length {ShaStringLength}", nameof(sha)); } this.shaBytes1Through8 = ShaSubStringToULong(sha.Substring(0, 16)); this.shaBytes9Through16 = ShaSubStringToULong(sha.Substring(16, 16)); this.shaBytes17Through20 = ShaSubStringToUInt(sha.Substring(32, 8)); } public static bool TryParse(string sha, out Sha1Id sha1, out string error) { error = null; try { sha1 = new Sha1Id(sha); return true; } catch (Exception e) { error = e.Message; } sha1 = new Sha1Id(0, 0, 0); return false; } public static void ShaBufferToParts( byte[] shaBuffer, out ulong shaBytes1Through8, out ulong shaBytes9Through16, out uint shaBytes17Through20) { if (shaBuffer == null) { throw new ArgumentNullException(nameof(shaBuffer)); } if (shaBuffer.Length != ShaBufferLength) { throw new ArgumentException($"Must be length {ShaBufferLength}", nameof(shaBuffer)); } unsafe { fixed (byte* firstChunk = &shaBuffer[0], secondChunk = &shaBuffer[sizeof(ulong)], thirdChunk = &shaBuffer[sizeof(ulong) * 2]) { shaBytes1Through8 = *(ulong*)firstChunk; shaBytes9Through16 = *(ulong*)secondChunk; shaBytes17Through20 = *(uint*)thirdChunk; } } } public void ToBuffer(byte[] shaBuffer) { unsafe { fixed (byte* firstChunk = &shaBuffer[0], secondChunk = &shaBuffer[sizeof(ulong)], thirdChunk = &shaBuffer[sizeof(ulong) * 2]) { *(ulong*)firstChunk = this.shaBytes1Through8; *(ulong*)secondChunk = this.shaBytes9Through16; *(uint*)thirdChunk = this.shaBytes17Through20; } } } public override string ToString() { char[] shaString = new char[ShaStringLength]; BytesToCharArray(shaString, 0, this.shaBytes1Through8, sizeof(ulong)); BytesToCharArray(shaString, 2 * sizeof(ulong), this.shaBytes9Through16, sizeof(ulong)); BytesToCharArray(shaString, 2 * (2 * sizeof(ulong)), this.shaBytes17Through20, sizeof(uint)); return new string(shaString, 0, shaString.Length); } private static void BytesToCharArray(char[] shaString, int startIndex, ulong shaBytes, int numBytes) { byte b; int firstArrayIndex; for (int i = 0; i < numBytes; ++i) { b = (byte)(shaBytes >> (i * 8)); firstArrayIndex = startIndex + (i * 2); shaString[firstArrayIndex] = GetHexValue(b / 16); shaString[firstArrayIndex + 1] = GetHexValue(b % 16); } } private static ulong ShaSubStringToULong(string shaSubString) { if (shaSubString == null) { throw new ArgumentNullException(nameof(shaSubString)); } if (shaSubString.Length != sizeof(ulong) * 2) { throw new ArgumentException($"Must be length {sizeof(ulong) * 2}", nameof(shaSubString)); } ulong bytes = 0; string upperCaseSha = shaSubString.ToUpper(); int stringIndex = 0; for (int i = 0; i < sizeof(ulong); ++i) { stringIndex = i * 2; char firstChar = shaSubString[stringIndex]; char secondChar = shaSubString[stringIndex + 1]; byte nextByte = (byte)(CharToByte(firstChar) << 4 | CharToByte(secondChar)); bytes = bytes | ((ulong)nextByte << (i * 8)); } return bytes; } private static uint ShaSubStringToUInt(string shaSubString) { if (shaSubString == null) { throw new ArgumentNullException(nameof(shaSubString)); } if (shaSubString.Length != sizeof(uint) * 2) { throw new ArgumentException($"Must be length {sizeof(uint) * 2}", nameof(shaSubString)); } uint bytes = 0; string upperCaseSha = shaSubString.ToUpper(); int stringIndex = 0; for (int i = 0; i < sizeof(uint); ++i) { stringIndex = i * 2; char firstChar = shaSubString[stringIndex]; char secondChar = shaSubString[stringIndex + 1]; byte nextByte = (byte)(CharToByte(firstChar) << 4 | CharToByte(secondChar)); bytes = bytes | ((uint)nextByte << (i * 8)); } return bytes; } private static char GetHexValue(int i) { if (i < 10) { return (char)(i + '0'); } return (char)(i - 10 + 'A'); } private static byte CharToByte(char c) { if (c >= '0' && c <= '9') { return (byte)(c - '0'); } if (c >= 'A' && c <= 'F') { return (byte)(10 + (c - 'A')); } throw new ArgumentException($"Invalid character {c}", nameof(c)); } } } ================================================ FILE: GVFS/GVFS.Common/Git/SideChannelStream.cs ================================================ using System; using System.IO; namespace GVFS.Common.Git { /// /// As you read from a SideChannelStream, we read from the inner /// 'from' stream and write that data to the inner 'to' stream /// before passing the bytes out to the reader. /// public class SideChannelStream : Stream { protected readonly Stream from; protected readonly Stream to; public SideChannelStream(Stream from, Stream to) { this.from = from; this.to = to; } public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => 0; public override long Position { get => 0; set => throw new NotImplementedException(); } public override void Flush() { this.from.Flush(); this.to.Flush(); } public override int Read(byte[] buffer, int offset, int count) { int n = this.from.Read(buffer, offset, count); this.to.Write(buffer, offset, n); return n; } public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } public override void SetLength(long value) { throw new NotImplementedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } } } ================================================ FILE: GVFS/GVFS.Common/GitCommandLineParser.cs ================================================ using System; using System.Linq; namespace GVFS.Common { public class GitCommandLineParser { private const int GitIndex = 0; private const int VerbIndex = 1; private const int ArgumentsOffset = 2; private readonly string[] parts; private Verbs commandVerb; public GitCommandLineParser(string command) { if (!string.IsNullOrWhiteSpace(command)) { this.parts = command.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (this.parts.Length < VerbIndex + 1 || this.parts[GitIndex] != "git") { this.parts = null; } else { this.commandVerb = this.StringToVerbs(this.parts[VerbIndex]); } } } [Flags] public enum Verbs { Other = 1 << 0, AddOrStage = 1 << 1, Checkout = 1 << 2, Commit = 1 << 3, Move = 1 << 4, Reset = 1 << 5, Status = 1 << 6, UpdateIndex = 1 << 7, Restore = 1 << 8, Switch = 1 << 9, } public bool IsValidGitCommand { get { return this.parts != null; } } public bool IsResetMixed() { return this.IsResetSoftOrMixed() && !this.HasArgument("--soft"); } public bool IsResetSoftOrMixed() { return this.IsVerb(Verbs.Reset) && !this.HasArgument("--hard") && !this.HasArgument("--keep") && !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. /// /// True if file paths were detected, otherwise false public bool IsCheckoutWithFilePaths() { if (this.IsVerb(Verbs.Checkout)) { int numArguments = this.parts.Length - ArgumentsOffset; // The simplest way to know that we're dealing with file paths is if there are any arguments after a -- // e.g. git checkout branchName -- fileName int dashDashIndex; if (this.HasAnyArgument(arg => arg == "--", out dashDashIndex) && numArguments > dashDashIndex + 1) { return true; } // We also special case one usage with HEAD, as long as there are no other arguments with - or -- that might // result in behavior we haven't tested. // e.g. git checkout HEAD fileName if (numArguments >= 2 && !this.HasAnyArgument(arg => arg.StartsWith("-")) && this.HasArgumentAtIndex(GVFSConstants.DotGit.HeadName, argumentIndex: 0)) { return true; } // Note: we have definitely missed some cases of file paths, e.g.: // git checkout branchName fileName (detecting this reliably requires more complicated parsing) // git checkout --patch (we currently have no need to optimize this scenario) } if (this.IsVerb(Verbs.Restore)) { return true; } return false; } public bool IsVerb(Verbs verbs) { if (!this.IsValidGitCommand) { return false; } return (verbs & this.commandVerb) == this.commandVerb; } private Verbs StringToVerbs(string verb) { switch (verb) { case "add": return Verbs.AddOrStage; case "checkout": return Verbs.Checkout; case "commit": return Verbs.Commit; case "mv": return Verbs.Move; case "reset": return Verbs.Reset; case "restore": return Verbs.Restore; case "stage": return Verbs.AddOrStage; case "status": return Verbs.Status; case "switch": return Verbs.Switch; case "update-index": return Verbs.UpdateIndex; default: return Verbs.Other; } } 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; return this.parts.Length > actualIndex && this.parts[actualIndex] == argument; } private bool HasAnyArgument(Predicate argumentPredicate) { int argumentIndex; return this.HasAnyArgument(argumentPredicate, out argumentIndex); } private bool HasAnyArgument(Predicate argumentPredicate, out int argumentIndex) { argumentIndex = -1; if (!this.IsValidGitCommand) { return false; } for (int i = ArgumentsOffset; i < this.parts.Length; i++) { if (argumentPredicate(this.parts[i])) { argumentIndex = i - ArgumentsOffset; return true; } } return false; } } } ================================================ FILE: GVFS/GVFS.Common/GitStatusCache.cs ================================================ 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; // arbitrary value used when deciding whether to print // a message about a delayed status scan. private readonly TimeSpan delayThreshold = TimeSpan.FromSeconds(0.5); 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 CancellationTokenSource shutdownTokenSource; private Task activeHydrationTask; private volatile EnlistmentHydrationSummary cachedHydrationSummary; private Func projectedFolderCountProvider; private volatile CacheState cacheState = CacheState.Dirty; private object cacheFileLock = new object(); internal static bool? TEST_EnableHydrationSummaryOverride = null; 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.shutdownTokenSource = new CancellationTokenSource(); this.wakeUpThread = new AutoResetEvent(false); } /// /// Sets the provider used to get the total projected folder count for hydration /// summary computation. Must be called before for /// hydration summary to function. /// /// /// This is set post-construction because of a circular dependency: /// InProcessMount creates GitStatusCache before FileSystemCallbacks, /// but the provider requires GitIndexProjection, which is created /// inside FileSystemCallbacks. FileSystemCallbacks calls this method /// after GitIndexProjection is available. /// public void SetProjectedFolderCountProvider(Func provider) { this.projectedFolderCountProvider = provider; } 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; this.shutdownTokenSource.Cancel(); 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); } /// /// Returns the cached hydration summary if one has been computed, /// or null if no valid summary is available yet. /// public EnlistmentHydrationSummary GetCachedHydrationSummary() { return this.cachedHydrationSummary; } /// /// 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.RelatedWarning("GitStatusCache.IsReadyForExternalAcquireLockRequests: request blocked"); } return shouldAllowExternalRequest; } public virtual void Dispose() { this.Shutdown(); // Wait for the hydration task to complete before disposing the // token source it may still be using. Task hydrationTask = Interlocked.Exchange(ref this.activeHydrationTask, null); if (hydrationTask != null) { try { hydrationTask.Wait(TimeSpan.FromSeconds(5)); } catch (AggregateException) { } } if (this.shutdownTokenSource != null) { this.shutdownTokenSource.Dispose(); this.shutdownTokenSource = null; } 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 startTime; lock (this.cacheFileLock) { CacheState cacheState = this.cacheState; startTime = 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 && (startTime - 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 = startTime; } // 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) { this.statistics.RecordBackgroundStatusScanRun(); // Run hydration summary in parallel with git status — they are independent // operations and neither should delay the other. Task hydrationTask = Task.Run(() => this.UpdateHydrationSummary()); Interlocked.Exchange(ref this.activeHydrationTask, hydrationTask); bool rebuildStatusCacheSucceeded = this.TryRebuildStatusCache(); // Wait for hydration to complete before logging final stats. // Exceptions are observed here to avoid unobserved task exceptions. try { hydrationTask.Wait(); } catch (AggregateException ex) { EventMetadata errorMetadata = new EventMetadata(); errorMetadata.Add("Area", EtwArea); errorMetadata.Add("Exception", ex.InnerException?.ToString()); this.context.Tracer.RelatedError( errorMetadata, $"{nameof(GitStatusCache)}.{nameof(RebuildStatusCacheIfNeeded)}: Unhandled exception in hydration summary task."); } TimeSpan delayedTime = startTime - this.initialDelayTime; TimeSpan statusRunTime = DateTime.UtcNow - startTime; string message = string.Format( "GitStatusCache.RebuildStatusCacheIfNeeded: Done generating status. Cache state: {0}. Status scan time: {1:0.##}s.", this.cacheState, statusRunTime.TotalSeconds); if (delayedTime > this.backoffTime + this.delayThreshold) { message += string.Format(" Status scan was delayed for: {0:0.##}s.", delayedTime.TotalSeconds); } this.context.Tracer.RelatedInfo(message); this.initialDelayTime = DateTime.MinValue; } } private void UpdateHydrationSummary() { if (this.projectedFolderCountProvider == null) { return; } bool enabled = TEST_EnableHydrationSummaryOverride ?? this.context.Repository.LibGit2RepoInvoker.GetConfigBoolOrDefault(GVFSConstants.GitConfig.ShowHydrationStatus, GVFSConstants.GitConfig.ShowHydrationStatusDefault); if (!enabled) { return; } HydrationStatusCircuitBreaker circuitBreaker = new HydrationStatusCircuitBreaker( this.context.Enlistment.DotGVFSRoot, this.context.Tracer); if (circuitBreaker.IsDisabled()) { return; } try { /* While not strictly part of git status, enlistment hydration summary is used * in "git status" pre-command hook, and can take several seconds to compute on very large repos. * Accessing it here ensures that the value is cached for when a user invokes "git status", * and this is also a convenient place to log telemetry for it. */ EnlistmentHydrationSummary hydrationSummary = EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem, this.context.Tracer, this.projectedFolderCountProvider, this.shutdownTokenSource.Token); EventMetadata metadata = new EventMetadata(); metadata.Add("Area", EtwArea); if (hydrationSummary.IsValid) { this.cachedHydrationSummary = hydrationSummary; metadata[nameof(hydrationSummary.TotalFolderCount)] = hydrationSummary.TotalFolderCount; metadata[nameof(hydrationSummary.TotalFileCount)] = hydrationSummary.TotalFileCount; metadata[nameof(hydrationSummary.HydratedFolderCount)] = hydrationSummary.HydratedFolderCount; metadata[nameof(hydrationSummary.HydratedFileCount)] = hydrationSummary.HydratedFileCount; this.context.Tracer.RelatedEvent( EventLevel.Informational, nameof(EnlistmentHydrationSummary), metadata, Keywords.Telemetry); } else if (hydrationSummary.Error != null) { this.cachedHydrationSummary = null; circuitBreaker.RecordFailure(); metadata["Exception"] = hydrationSummary.Error.ToString(); this.context.Tracer.RelatedWarning( metadata, $"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: hydration summary could not be calculated.", Keywords.Telemetry); } else { // Invalid summary with no error — likely cancelled during shutdown this.cachedHydrationSummary = null; this.context.Tracer.RelatedInfo( $"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: hydration summary was cancelled."); } } catch (Exception ex) { this.cachedHydrationSummary = null; circuitBreaker.RecordFailure(); EventMetadata metadata = new EventMetadata(); metadata.Add("Area", EtwArea); metadata.Add("Exception", ex.ToString()); this.context.Tracer.RelatedError( metadata, $"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: Exception trying to update hydration summary cache.", Keywords.Telemetry); } } /// /// Rebuild the status cache. This will run the background status to /// generate status results, and update the serialized status cache /// file. /// private bool TryRebuildStatusCache() { try { this.context.FileSystem.CreateDirectory(this.context.Enlistment.GitStatusCacheFolder); } 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.RelatedWarning( metadata, string.Format("GitStatusCache is unable to create git status cache folder at {0}.", this.context.Enlistment.GitStatusCacheFolder)); return false; } // 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.ExitCodeIsSuccess) { 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.RelatedWarning( metadata, string.Format("GitStatusCache encountered exception attempting to delete cache file at {0}.", this.serializedGitStatusFilePath), Keywords.Telemetry); 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; } } } } ================================================ FILE: GVFS/GVFS.Common/GitStatusCacheConfig.cs ================================================ 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) { GitProcess.ConfigResult result = git.GetFromConfig(configName); return result.TryParseAsInt(defaultValue, minValue, out value, out error); } } } ================================================ FILE: GVFS/GVFS.Common/HealthCalculator/EnlistmentHealthCalculator.cs ================================================ using System.Collections.Generic; using System.Linq; namespace GVFS.Common { /// /// Class responsible for the business logic involved in calculating the health statistics /// of a gvfs enlistment. Constructed with the lists of paths for the enlistment, and then /// internally stores the calculated information. Compute or recompute via CalculateStatistics /// with an optional parameter to only look for paths which are under the specified directory /// public class EnlistmentHealthCalculator { // In the context of this class, hydrated files are placeholders or modified paths // The total number of hydrated files is this.PlaceholderCount + this.ModifiedPathsCount private readonly EnlistmentPathData enlistmentPathData; public EnlistmentHealthCalculator(EnlistmentPathData pathData) { this.enlistmentPathData = pathData; } public EnlistmentHealthData CalculateStatistics(string parentDirectory) { int gitTrackedItemsCount = 0; int placeholderCount = 0; int modifiedPathsCount = 0; Dictionary gitTrackedItemsDirectoryTally = new Dictionary(GVFSPlatform.Instance.Constants.PathComparer); Dictionary hydratedFilesDirectoryTally = new Dictionary(GVFSPlatform.Instance.Constants.PathComparer); // Parent directory is a path relative to the root of the repository which is already in git format if (!parentDirectory.EndsWith(GVFSConstants.GitPathSeparatorString) && parentDirectory.Length > 0) { parentDirectory += GVFSConstants.GitPathSeparator; } if (parentDirectory.StartsWith(GVFSConstants.GitPathSeparatorString)) { parentDirectory = parentDirectory.TrimStart(GVFSConstants.GitPathSeparator); } gitTrackedItemsCount += this.CategorizePaths(this.enlistmentPathData.GitFolderPaths, gitTrackedItemsDirectoryTally, parentDirectory); gitTrackedItemsCount += this.CategorizePaths(this.enlistmentPathData.GitFilePaths, gitTrackedItemsDirectoryTally, parentDirectory); placeholderCount += this.CategorizePaths(this.enlistmentPathData.PlaceholderFolderPaths, hydratedFilesDirectoryTally, parentDirectory); placeholderCount += this.CategorizePaths(this.enlistmentPathData.PlaceholderFilePaths, hydratedFilesDirectoryTally, parentDirectory); modifiedPathsCount += this.CategorizePaths(this.enlistmentPathData.ModifiedFolderPaths, hydratedFilesDirectoryTally, parentDirectory); modifiedPathsCount += this.CategorizePaths(this.enlistmentPathData.ModifiedFilePaths, hydratedFilesDirectoryTally, parentDirectory); Dictionary mostHydratedDirectories = new Dictionary(GVFSPlatform.Instance.Constants.PathComparer); // Map directory names to the corresponding health data from gitTrackedItemsDirectoryTally and hydratedFilesDirectoryTally foreach (KeyValuePair pair in gitTrackedItemsDirectoryTally) { if (hydratedFilesDirectoryTally.TryGetValue(pair.Key, out int hydratedFiles)) { // In-lining this for now until a better "health" calculation is created // Another possibility is the ability to pass a function to use for health (might not be applicable) mostHydratedDirectories.Add(pair.Key, new SubDirectoryInfo(pair.Key, hydratedFiles, pair.Value)); } else { mostHydratedDirectories.Add(pair.Key, new SubDirectoryInfo(pair.Key, 0, pair.Value)); } } return new EnlistmentHealthData( parentDirectory, gitTrackedItemsCount, placeholderCount, modifiedPathsCount, this.CalculateHealthMetric(placeholderCount + modifiedPathsCount, gitTrackedItemsCount), mostHydratedDirectories.OrderByDescending(kp => kp.Value.HydratedFileCount).Select(item => item.Value).ToList()); } /// /// Take a file path and get the top level directory from it, or GVFSConstants.GitPathSeparator if it is not in a directory /// /// The path to a file to parse for the top level directory containing it /// A string containing the top level directory from the provided path, or GVFSConstants.GitPathSeparator if the path is for an item in the root private string ParseTopDirectory(string path) { int whackLocation = path.IndexOf(GVFSConstants.GitPathSeparator); if (whackLocation == -1) { return GVFSConstants.GitPathSeparatorString; } return path.Substring(0, whackLocation); } /// /// Categorizes a list of paths given as strings by mapping them to the top level directory in their path /// Modifies the directoryTracking dictionary to have an accurate count of the files underneath a top level directory /// /// /// The distinction between files and directories is important -- /// If the path to a file doesn't contain a GVFSConstants.GitPathSeparator, then that means it falls within the root /// However if a directory's path doesn't contain a GVFSConstants.GitPathSeparator, it doesn't count towards its own hydration /// /// An enumerable containing paths as strings /// A dictionary used to track the number of files per top level directory /// Paths will only be categorized if they are descendants of the parentDirectory private int CategorizePaths(IEnumerable paths, Dictionary directoryTracking, string parentDirectory) { int count = 0; foreach (string path in paths) { // Only categorize if descendent of the parentDirectory if (path.StartsWith(parentDirectory, GVFSPlatform.Instance.Constants.PathComparison)) { count++; // If the path is to the parentDirectory, ignore it to avoid adding string.Empty to the data structures if (!parentDirectory.Equals(path, GVFSPlatform.Instance.Constants.PathComparison)) { // Trim the path to parent directory string topDir = this.ParseTopDirectory(this.TrimDirectoryFromPath(path, parentDirectory)); if (!topDir.Equals(GVFSConstants.GitPathSeparatorString)) { this.IncreaseDictionaryCounterByKey(directoryTracking, topDir); } } } } return count; } /// /// Trim the relative path to a directory from the front of a specified path which is its child /// /// Precondition: 'directoryTarget' must be an ancestor of 'path' /// The path being trimmed /// The directory target whose path to trim from the path /// The newly formatted path with the directory trimmed private string TrimDirectoryFromPath(string path, string directoryTarget) { return path.Substring(directoryTarget.Length); } private void IncreaseDictionaryCounterByKey(Dictionary countingDictionary, string key) { if (!countingDictionary.TryGetValue(key, out int count)) { count = 0; } countingDictionary[key] = ++count; } private decimal CalculateHealthMetric(int hydratedFileCount, int totalFileCount) { if (totalFileCount == 0) { return 0; } return (decimal)hydratedFileCount / (decimal)totalFileCount; } public class SubDirectoryInfo { public SubDirectoryInfo(string name, int hydratedFileCount, int totalFileCount) { this.Name = name; this.HydratedFileCount = hydratedFileCount; this.TotalFileCount = totalFileCount; } public string Name { get; private set; } public int HydratedFileCount { get; private set; } public int TotalFileCount { get; private set; } } } } ================================================ FILE: GVFS/GVFS.Common/HealthCalculator/EnlistmentHealthData.cs ================================================ using System.Collections.Generic; namespace GVFS.Common { public class EnlistmentHealthData { public EnlistmentHealthData( string targetDirectory, int gitItemsCount, int placeholderCount, int modifiedPathsCount, decimal healthMetric, List directoryHydrationLevels) { this.TargetDirectory = targetDirectory; this.GitTrackedItemsCount = gitItemsCount; this.PlaceholderCount = placeholderCount; this.ModifiedPathsCount = modifiedPathsCount; this.HealthMetric = healthMetric; this.DirectoryHydrationLevels = directoryHydrationLevels; } public string TargetDirectory { get; private set; } public int GitTrackedItemsCount { get; private set; } public int PlaceholderCount { get; private set; } public int ModifiedPathsCount { get; private set; } public List DirectoryHydrationLevels { get; private set; } public decimal HealthMetric { get; private set; } public decimal PlaceholderPercentage { get { if (this.GitTrackedItemsCount == 0) { return 0; } return (decimal)this.PlaceholderCount / this.GitTrackedItemsCount; } } public decimal ModifiedPathsPercentage { get { if (this.GitTrackedItemsCount == 0) { return 0; } return (decimal)this.ModifiedPathsCount / this.GitTrackedItemsCount; } } } } ================================================ FILE: GVFS/GVFS.Common/HealthCalculator/EnlistmentHydrationSummary.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using System; using System.Diagnostics; using System.IO; using System.Threading; namespace GVFS.Common { public class EnlistmentHydrationSummary { public int PlaceholderFileCount { get; private set; } public int PlaceholderFolderCount { get; private set; } public int ModifiedFileCount { get; private set; } public int ModifiedFolderCount { get; private set; } public int TotalFileCount { get; private set; } public int TotalFolderCount { get; private set; } public Exception Error { get; private set; } = null; public int HydratedFileCount => PlaceholderFileCount + ModifiedFileCount; public int HydratedFolderCount => PlaceholderFolderCount + ModifiedFolderCount; public bool IsValid { get { return PlaceholderFileCount >= 0 && PlaceholderFolderCount >= 0 && ModifiedFileCount >= 0 && ModifiedFolderCount >= 0 && TotalFileCount >= HydratedFileCount && TotalFolderCount >= HydratedFolderCount; } } public string ToMessage() { if (!IsValid) { return "Error calculating hydration summary. Run 'gvfs health' at the repository root for hydration status details."; } int fileHydrationPercent = TotalFileCount == 0 ? 0 : (int)((100L * HydratedFileCount) / TotalFileCount); int folderHydrationPercent = TotalFolderCount == 0 ? 0 : (int)((100L * HydratedFolderCount) / TotalFolderCount); return $"{fileHydrationPercent}% of files and {folderHydrationPercent}% of folders hydrated. Run 'gvfs health' at the repository root for details."; } public static EnlistmentHydrationSummary CreateSummary( GVFSEnlistment enlistment, PhysicalFileSystem fileSystem, ITracer tracer, Func projectedFolderCountProvider, CancellationToken cancellationToken = default) { Stopwatch totalStopwatch = Stopwatch.StartNew(); Stopwatch phaseStopwatch = new Stopwatch(); try { /* Getting all the file paths from git index is slow and we only need the total count, * so we read the index file header instead of calling GetPathsFromGitIndex */ phaseStopwatch.Restart(); int totalFileCount = GetIndexFileCount(enlistment, fileSystem); long indexReadMs = phaseStopwatch.ElapsedMilliseconds; cancellationToken.ThrowIfCancellationRequested(); EnlistmentPathData pathData = new EnlistmentPathData(); /* FUTURE: These could be optimized to only deal with counts instead of full path lists */ phaseStopwatch.Restart(); pathData.LoadPlaceholdersFromDatabase(enlistment); long placeholderLoadMs = phaseStopwatch.ElapsedMilliseconds; cancellationToken.ThrowIfCancellationRequested(); phaseStopwatch.Restart(); pathData.LoadModifiedPaths(enlistment, tracer); long modifiedPathsLoadMs = phaseStopwatch.ElapsedMilliseconds; cancellationToken.ThrowIfCancellationRequested(); int placeholderFileCount = pathData.PlaceholderFilePaths.Count; int placeholderFolderCount = pathData.PlaceholderFolderPaths.Count; int modifiedFileCount = pathData.ModifiedFilePaths.Count; int modifiedFolderCount = pathData.ModifiedFolderPaths.Count; /* Getting the head tree count (used for TotalFolderCount) is potentially slower than the other parts * of the operation, so we do it last and check that the other parts would succeed before running it. */ var soFar = new EnlistmentHydrationSummary() { PlaceholderFileCount = placeholderFileCount, PlaceholderFolderCount = placeholderFolderCount, ModifiedFileCount = modifiedFileCount, ModifiedFolderCount = modifiedFolderCount, TotalFileCount = totalFileCount, TotalFolderCount = placeholderFolderCount + modifiedFolderCount + 1, // Not calculated yet, use a dummy valid value. }; if (!soFar.IsValid) { soFar.TotalFolderCount = 0; // Set to default invalid value to avoid confusion with the dummy value above. tracer.RelatedWarning( $"Hydration summary early exit: data invalid before tree count. " + $"TotalFileCount={totalFileCount}, PlaceholderFileCount={placeholderFileCount}, " + $"ModifiedFileCount={modifiedFileCount}, PlaceholderFolderCount={placeholderFolderCount}, " + $"ModifiedFolderCount={modifiedFolderCount}"); EmitDurationTelemetry(tracer, totalStopwatch.ElapsedMilliseconds, indexReadMs, placeholderLoadMs, modifiedPathsLoadMs, treeCountMs: 0, earlyExit: true); return soFar; } /* Get the total folder count from the caller-provided function. * In the mount process, this comes from the in-memory projection (essentially free). * In gvfs health --status fallback, this parses the git index via GitIndexProjection. */ cancellationToken.ThrowIfCancellationRequested(); phaseStopwatch.Restart(); int totalFolderCount = projectedFolderCountProvider(); long treeCountMs = phaseStopwatch.ElapsedMilliseconds; EmitDurationTelemetry(tracer, totalStopwatch.ElapsedMilliseconds, indexReadMs, placeholderLoadMs, modifiedPathsLoadMs, treeCountMs, earlyExit: false); return new EnlistmentHydrationSummary() { PlaceholderFileCount = placeholderFileCount, PlaceholderFolderCount = placeholderFolderCount, ModifiedFileCount = modifiedFileCount, ModifiedFolderCount = modifiedFolderCount, TotalFileCount = totalFileCount, TotalFolderCount = totalFolderCount, }; } catch (OperationCanceledException) { tracer.RelatedInfo($"Hydration summary cancelled after {totalStopwatch.ElapsedMilliseconds}ms"); return new EnlistmentHydrationSummary() { PlaceholderFileCount = -1, PlaceholderFolderCount = -1, ModifiedFileCount = -1, ModifiedFolderCount = -1, TotalFileCount = -1, TotalFolderCount = -1, }; } catch (Exception e) { tracer.RelatedError($"Hydration summary failed with exception after {totalStopwatch.ElapsedMilliseconds}ms: {e.Message}"); return new EnlistmentHydrationSummary() { PlaceholderFileCount = -1, PlaceholderFolderCount = -1, ModifiedFileCount = -1, ModifiedFolderCount = -1, TotalFileCount = -1, TotalFolderCount = -1, Error = e, }; } } private static void EmitDurationTelemetry( ITracer tracer, long totalMs, long indexReadMs, long placeholderLoadMs, long modifiedPathsLoadMs, long treeCountMs, bool earlyExit) { EventMetadata metadata = new EventMetadata(); metadata["TotalMs"] = totalMs; metadata["IndexReadMs"] = indexReadMs; metadata["PlaceholderLoadMs"] = placeholderLoadMs; metadata["ModifiedPathsLoadMs"] = modifiedPathsLoadMs; metadata["TreeCountMs"] = treeCountMs; metadata["EarlyExit"] = earlyExit; tracer.RelatedEvent( EventLevel.Informational, "HydrationSummaryDuration", metadata, Keywords.Telemetry); } /// /// Get the total number of files in the index. /// internal static int GetIndexFileCount(GVFSEnlistment enlistment, PhysicalFileSystem fileSystem) { string indexPath = enlistment.GitIndexPath; using (var indexFile = fileSystem.OpenFileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, callFlushFileBuffers: false)) { if (indexFile.Length < 12) { return -1; } /* The number of files in the index is a big-endian integer from * the 4 bytes at offsets 8-11 of the index file. */ indexFile.Position = 8; var bytes = new byte[4]; indexFile.Read( bytes, // Destination buffer offset: 0, // Offset in destination buffer, not in indexFile count: 4); if (BitConverter.IsLittleEndian) { Array.Reverse(bytes); } int count = BitConverter.ToInt32(bytes, 0); return count; } } } } ================================================ FILE: GVFS/GVFS.Common/HealthCalculator/EnlistmentPathData.cs ================================================ using GVFS.Common.Database; using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace GVFS.Common { public class EnlistmentPathData { public List GitFolderPaths; public List GitFilePaths; public List PlaceholderFolderPaths; public List PlaceholderFilePaths; public List ModifiedFolderPaths; public List ModifiedFilePaths; public List GitTrackingPaths; public EnlistmentPathData() { this.GitFolderPaths = new List(); this.GitFilePaths = new List(); this.PlaceholderFolderPaths = new List(); this.PlaceholderFilePaths = new List(); this.ModifiedFolderPaths = new List(); this.ModifiedFilePaths = new List(); this.GitTrackingPaths = new List(); } public void NormalizeAllPaths() { this.NormalizePaths(this.GitFolderPaths); this.NormalizePaths(this.GitFilePaths); this.NormalizePaths(this.PlaceholderFolderPaths); this.NormalizePaths(this.PlaceholderFilePaths); this.NormalizePaths(this.ModifiedFolderPaths); this.NormalizePaths(this.ModifiedFilePaths); this.NormalizePaths(this.GitTrackingPaths); this.ModifiedFilePaths = this.ModifiedFilePaths.Union(this.GitTrackingPaths).ToList(); } /// /// Get two lists of placeholders, one containing the files and the other the directories /// Goes to the SQLite database for the placeholder lists /// /// The current GVFS enlistment being operated on public void LoadPlaceholdersFromDatabase(GVFSEnlistment enlistment) { List filePlaceholders = new List(); List folderPlaceholders = new List(); using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistment.EnlistmentRoot, new SqliteDatabase())) { PlaceholderTable placeholderTable = new PlaceholderTable(database); placeholderTable.GetAllEntries(out filePlaceholders, out folderPlaceholders); } this.PlaceholderFilePaths.AddRange(filePlaceholders.Select(placeholderData => placeholderData.Path)); this.PlaceholderFolderPaths.AddRange(folderPlaceholders.Select(placeholderData => placeholderData.Path)); } /// /// Call 'git ls-files' and 'git ls-tree' to get a list of all files and directories in the enlistment /// /// The current GVFS enlistmetn being operated on public void LoadPathsFromGitIndex(GVFSEnlistment enlistment) { List skipWorktreeFiles = new List(); GitProcess gitProcess = new GitProcess(enlistment); GitProcess.Result fileResult = gitProcess.LsFiles( line => { if (line.First() == 'H') { skipWorktreeFiles.Add(TrimGitIndexLineWithSkipWorktree(line)); } this.GitFilePaths.Add(TrimGitIndexLineWithSkipWorktree(line)); }); GitProcess.Result folderResult = gitProcess.LsTree( GVFSConstants.DotGit.HeadName, line => { this.GitFolderPaths.Add(TrimGitIndexLine(line)); }, recursive: true, showDirectories: true); this.GitTrackingPaths.AddRange(skipWorktreeFiles); } public void LoadModifiedPaths(GVFSEnlistment enlistment, ITracer tracer) { if (TryLoadModifiedPathsFromPipe(enlistment, tracer)) { return; } // Most likely GVFS is not mounted. Give a basic effort to read the modified paths database. string filePath = Path.Combine(enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.ModifiedPaths); try { using (FileStream file = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read)) using (StreamReader reader = new StreamReader(file)) { AddModifiedPaths(ReadModifiedPathDatabaseLines(reader)); } } catch (Exception ex) { tracer.RelatedWarning($"Failed to read modified paths file at {filePath}: {ex.Message}"); } } private IEnumerable ReadModifiedPathDatabaseLines(StreamReader r) { while (!r.EndOfStream) { string line = r.ReadLine(); if (line == null) { continue; } const string LinePrefix = "A "; if (line.StartsWith(LinePrefix)) { line = line.Substring(LinePrefix.Length); } yield return line; } } /// /// Talk to the mount process across the named pipe to get a list of the modified paths /// /// If/when modified paths are moved to SQLite go there instead /// The enlistment being operated on /// An array containing all of the modified paths in string format private bool TryLoadModifiedPathsFromPipe(GVFSEnlistment enlistment, ITracer tracer) { using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) { string[] modifiedPathsList = Array.Empty(); if (!pipeClient.Connect()) { return false; } try { NamedPipeMessages.Message modifiedPathsMessage = new NamedPipeMessages.Message(NamedPipeMessages.ModifiedPaths.ListRequest, NamedPipeMessages.ModifiedPaths.CurrentVersion); pipeClient.SendRequest(modifiedPathsMessage); NamedPipeMessages.Message modifiedPathsResponse = pipeClient.ReadResponse(); if (!modifiedPathsResponse.Header.Equals(NamedPipeMessages.ModifiedPaths.SuccessResult)) { return false; } modifiedPathsList = modifiedPathsResponse.Body.Split(new char[] { '\0' }, StringSplitOptions.RemoveEmptyEntries); } catch (Exception ex) { tracer.RelatedWarning($"Failed to load modified paths via named pipe: {ex.Message}"); return false; } AddModifiedPaths(modifiedPathsList); return true; } } private void AddModifiedPaths(IEnumerable modifiedPathsList) { foreach (string path in modifiedPathsList) { if (path.Last() == GVFSConstants.GitPathSeparator) { path.TrimEnd(GVFSConstants.GitPathSeparator); this.ModifiedFolderPaths.Add(path); } else { this.ModifiedFilePaths.Add(path); } } } /// /// Parse a line of the git index coming from the ls-files endpoint in the git process to get the path to that files /// These paths begin with 'S' or 'H' depending on if they have the skip-worktree bit set /// /// The line from the output of the git index /// The path extracted from the provided line of the git index private static string TrimGitIndexLineWithSkipWorktree(string line) { return line.Substring(line.IndexOf(' ') + 1); } private void NormalizePaths(List paths) { for (int i = 0; i < paths.Count; i++) { paths[i] = paths[i].Replace(GVFSPlatform.GVFSPlatformConstants.PathSeparator, GVFSConstants.GitPathSeparator); paths[i] = paths[i].Trim(GVFSConstants.GitPathSeparator); } } /// /// Parse a line of the git index coming from the ls-tree endpoint in the git process to get the path to that file /// /// The line from the output of the git index /// The path extracted from the provided line of the git index private static string TrimGitIndexLine(string line) { return line.Substring(line.IndexOf('\t') + 1); } } } ================================================ FILE: GVFS/GVFS.Common/HealthCalculator/HydrationStatusCircuitBreaker.cs ================================================ using GVFS.Common.Tracing; using System; using System.IO; namespace GVFS.Common { /// /// Tracks hydration status computation failures and auto-disables the feature /// after repeated failures to protect users from persistent performance issues. /// /// The circuit breaker resets when: /// - A new calendar day begins (UTC) /// - The GVFS version changes (indicating an update that may fix the issue) /// /// This class intentionally avoids dependencies on PhysicalFileSystem so it can /// be file-linked into lightweight projects like GVFS.Hooks. /// public class HydrationStatusCircuitBreaker { public const int MaxFailuresPerDay = 3; private readonly string markerFilePath; private readonly ITracer tracer; public HydrationStatusCircuitBreaker( string dotGVFSRoot, ITracer tracer) { this.markerFilePath = Path.Combine( dotGVFSRoot, GVFSConstants.DotGVFS.HydrationStatus.DisabledMarkerFile); this.tracer = tracer; } /// /// Returns true if the hydration status feature should be skipped due to /// too many recent failures. /// public bool IsDisabled() { try { if (!File.Exists(this.markerFilePath)) { return false; } string content = File.ReadAllText(this.markerFilePath); if (!TryParseMarkerFile(content, out string markerDate, out string markerVersion, out int failureCount)) { return false; } string today = DateTime.UtcNow.ToString("yyyy-MM-dd"); string currentVersion = ProcessHelper.GetCurrentProcessVersion(); // Stale marker from a previous day or version — not disabled. // RecordFailure will reset the count when it next runs. if (markerDate != today || markerVersion != currentVersion) { return false; } return failureCount >= MaxFailuresPerDay; } catch (Exception ex) { this.tracer.RelatedWarning($"Error reading hydration status circuit breaker: {ex.Message}"); return false; } } /// /// Records a failure. After failures in a day, /// the circuit breaker trips and returns true. /// Uses exclusive file access to prevent concurrent processes from losing counts. /// public void RecordFailure() { try { int failureCount = 1; string today = DateTime.UtcNow.ToString("yyyy-MM-dd"); string currentVersion = ProcessHelper.GetCurrentProcessVersion(); Directory.CreateDirectory(Path.GetDirectoryName(this.markerFilePath)); // Use exclusive file access to prevent concurrent read-modify-write races. // If another process holds the file, we skip this failure rather than block. try { using (FileStream fs = new FileStream( this.markerFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) { string existingContent; using (StreamReader reader = new StreamReader(fs, System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 256, leaveOpen: true)) { existingContent = reader.ReadToEnd(); } if (TryParseMarkerFile(existingContent, out string markerDate, out string markerVersion, out int existingCount) && markerDate == today && markerVersion == currentVersion) { failureCount = existingCount + 1; } // Reset to beginning and write new content fs.Position = 0; fs.SetLength(0); using (StreamWriter writer = new StreamWriter(fs)) { writer.Write($"{today}\n{currentVersion}\n{failureCount}"); } } } catch (IOException) { // Another process holds the file — skip this failure count return; } if (failureCount >= MaxFailuresPerDay) { this.tracer.RelatedWarning( $"Hydration status circuit breaker tripped after {failureCount} failures today. " + $"Feature will be disabled until tomorrow or a GVFS update."); } } catch (Exception ex) { this.tracer.RelatedWarning($"Error writing hydration status circuit breaker: {ex.Message}"); } } /// /// Parses the marker file format: date\nversion\ncount /// internal static bool TryParseMarkerFile(string content, out string date, out string version, out int failureCount) { date = null; version = null; failureCount = 0; if (string.IsNullOrEmpty(content)) { return false; } string[] lines = content.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); if (lines.Length < 3) { return false; } date = lines[0]; version = lines[1]; return int.TryParse(lines[2], out failureCount); } } } ================================================ FILE: GVFS/GVFS.Common/HeartbeatThread.cs ================================================ using GVFS.Common.Tracing; using System; using System.Threading; namespace GVFS.Common { public class HeartbeatThread { private static readonly TimeSpan HeartBeatWaitTime = TimeSpan.FromMinutes(60); private readonly ITracer tracer; private readonly IHeartBeatMetadataProvider dataProvider; private Timer timer; private DateTime startTime; private DateTime lastHeartBeatTime; public HeartbeatThread(ITracer tracer, IHeartBeatMetadataProvider dataProvider) { this.tracer = tracer; this.dataProvider = dataProvider; } public void Start() { this.startTime = DateTime.Now; this.lastHeartBeatTime = DateTime.Now; this.timer = new Timer( this.EmitHeartbeat, state: null, dueTime: HeartBeatWaitTime, period: HeartBeatWaitTime); } public void Stop() { using (WaitHandle waitHandle = new ManualResetEvent(false)) { if (this.timer.Dispose(waitHandle)) { waitHandle.WaitOne(); waitHandle.Close(); } } this.EmitHeartbeat(unusedState: null); } private void EmitHeartbeat(object unusedState) { try { EventMetadata metadata = this.dataProvider.GetAndResetHeartBeatMetadata(out bool writeToLogFile) ?? new EventMetadata(); EventLevel eventLevel = writeToLogFile ? EventLevel.Informational : EventLevel.Verbose; DateTime now = DateTime.Now; metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); metadata.Add("MinutesUptime", (long)(now - this.startTime).TotalMinutes); metadata.Add("MinutesSinceLast", (int)(now - this.lastHeartBeatTime).TotalMinutes); this.lastHeartBeatTime = now; this.tracer.RelatedEvent(eventLevel, "Heartbeat", metadata, Keywords.Telemetry); } catch (Exception e) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", "HeartbeatThread"); metadata.Add("Exception", e.ToString()); this.tracer.RelatedWarning(metadata, "Swallowing unhandled exception in EmitHeartbeat", Keywords.Telemetry); } } } } ================================================ FILE: GVFS/GVFS.Common/Http/CacheServerInfo.cs ================================================ using Newtonsoft.Json; using System; namespace GVFS.Common.Http { public class CacheServerInfo { private const string ObjectsEndpointSuffix = "/gvfs/objects"; private const string PrefetchEndpointSuffix = "/gvfs/prefetch"; private const string SizesEndpointSuffix = "/gvfs/sizes"; [JsonConstructor] public CacheServerInfo(string url, string name, bool globalDefault = false) { this.Url = url; this.Name = name; this.GlobalDefault = globalDefault; if (this.Url != null) { this.ObjectsEndpointUrl = this.Url + ObjectsEndpointSuffix; this.PrefetchEndpointUrl = this.Url + PrefetchEndpointSuffix; this.SizesEndpointUrl = this.Url + SizesEndpointSuffix; } } public string Url { get; } public string Name { get; } public bool GlobalDefault { get; } public string ObjectsEndpointUrl { get; } public string PrefetchEndpointUrl { get; } public string SizesEndpointUrl { get; } 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)) { return this.Url; } if (string.IsNullOrWhiteSpace(this.Url)) { return this.Name; } return string.Format("{0} ({1})", this.Name, this.Url); } public static class ReservedNames { public const string None = "None"; public const string Default = "Default"; public const string UserDefined = "User Defined"; } } } ================================================ FILE: GVFS/GVFS.Common/Http/CacheServerResolver.cs ================================================ using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Linq; namespace GVFS.Common.Http { public class CacheServerResolver { private ITracer tracer; private Enlistment enlistment; public CacheServerResolver( ITracer tracer, Enlistment enlistment) { this.tracer = tracer; this.enlistment = enlistment; } public static CacheServerInfo GetCacheServerFromConfig(Enlistment enlistment) { string url = GetUrlFromConfig(enlistment); return new CacheServerInfo( url, url == enlistment.RepoUrl ? CacheServerInfo.ReservedNames.None : null); } public static string GetUrlFromConfig(Enlistment enlistment) { GitProcess git = enlistment.CreateGitProcess(); // TODO 1057500: Remove support for encoded-repo-url cache config setting return GetValueFromConfig(git, GVFSConstants.GitConfig.CacheServer, localOnly: true) ?? GetValueFromConfig(git, GetDeprecatedCacheConfigSettingName(enlistment), localOnly: false) ?? enlistment.RepoUrl; } public bool TryResolveUrlFromRemote( string cacheServerName, ServerGVFSConfig serverGVFSConfig, out CacheServerInfo cacheServer, out string error) { if (string.IsNullOrWhiteSpace(cacheServerName)) { throw new InvalidOperationException("An empty name is not supported"); } cacheServer = null; error = null; if (cacheServerName.Equals(CacheServerInfo.ReservedNames.Default, StringComparison.OrdinalIgnoreCase)) { cacheServer = serverGVFSConfig?.CacheServers.FirstOrDefault(cache => cache.GlobalDefault) ?? this.CreateNone(); } else { cacheServer = serverGVFSConfig?.CacheServers.FirstOrDefault(cache => cache.Name.Equals(cacheServerName, StringComparison.OrdinalIgnoreCase)); if (cacheServer == null) { error = "No cache server found with name " + cacheServerName; return false; } } return true; } public CacheServerInfo ResolveNameFromRemote( string cacheServerUrl, ServerGVFSConfig serverGVFSConfig) { if (string.IsNullOrWhiteSpace(cacheServerUrl)) { throw new InvalidOperationException("An empty url is not supported"); } if (this.InputMatchesEnlistmentUrl(cacheServerUrl)) { return this.CreateNone(); } return serverGVFSConfig?.CacheServers.FirstOrDefault(cache => cache.Url.Equals(cacheServerUrl, StringComparison.OrdinalIgnoreCase)) ?? new CacheServerInfo(cacheServerUrl, CacheServerInfo.ReservedNames.UserDefined); } public CacheServerInfo ParseUrlOrFriendlyName(string userInput) { if (userInput == null) { return new CacheServerInfo(null, CacheServerInfo.ReservedNames.Default); } if (string.IsNullOrWhiteSpace(userInput)) { throw new InvalidOperationException("A missing input (null) is fine, but an empty input (empty string) is not supported"); } if (this.InputMatchesEnlistmentUrl(userInput) || userInput.Equals(CacheServerInfo.ReservedNames.None, StringComparison.OrdinalIgnoreCase)) { return this.CreateNone(); } Uri uri; if (Uri.TryCreate(userInput, UriKind.Absolute, out uri)) { return new CacheServerInfo(userInput, CacheServerInfo.ReservedNames.UserDefined); } else { return new CacheServerInfo(null, userInput); } } public bool TrySaveUrlToLocalConfig(CacheServerInfo cache, out string error) { GitProcess git = this.enlistment.CreateGitProcess(); GitProcess.Result result = git.SetInLocalConfig(GVFSConstants.GitConfig.CacheServer, cache.Url, replaceAll: true); error = result.Errors; return result.ExitCodeIsSuccess; } private static string GetValueFromConfig(GitProcess git, string configName, bool localOnly) { GitProcess.ConfigResult result = localOnly ? git.GetFromLocalConfig(configName) : git.GetFromConfig(configName); if (!result.TryParseAsString(out string value, out string error)) { throw new InvalidRepoException(error); } return value; } private static string GetDeprecatedCacheConfigSettingName(Enlistment enlistment) { string sectionUrl = enlistment.RepoUrl.ToLowerInvariant() .Replace("https://", string.Empty) .Replace("http://", string.Empty) .Replace('/', '.'); return GVFSConstants.GitConfig.GVFSPrefix + sectionUrl + GVFSConstants.GitConfig.DeprecatedCacheEndpointSuffix; } private CacheServerInfo CreateNone() { return new CacheServerInfo(this.enlistment.RepoUrl, CacheServerInfo.ReservedNames.None); } private bool InputMatchesEnlistmentUrl(string userInput) { return this.enlistment.RepoUrl.TrimEnd('/').Equals(userInput.TrimEnd('/'), StringComparison.OrdinalIgnoreCase); } } } ================================================ FILE: GVFS/GVFS.Common/Http/ConfigHttpRequestor.cs ================================================ using GVFS.Common.Tracing; using Newtonsoft.Json; using System; using System.Net; using System.Net.Http; using System.Threading; namespace GVFS.Common.Http { public class ConfigHttpRequestor : HttpRequestor { private readonly string repoUrl; public ConfigHttpRequestor(ITracer tracer, Enlistment enlistment, RetryConfig retryConfig) : base(tracer, retryConfig, enlistment) { this.repoUrl = enlistment.RepoUrl; } public bool TryQueryGVFSConfig(bool logErrors, out ServerGVFSConfig serverGVFSConfig, out HttpStatusCode? httpStatus, out string errorMessage) { serverGVFSConfig = null; httpStatus = null; errorMessage = null; Uri gvfsConfigEndpoint; string gvfsConfigEndpointString = this.repoUrl + GVFSConstants.Endpoints.GVFSConfig; try { gvfsConfigEndpoint = new Uri(gvfsConfigEndpointString); } catch (UriFormatException e) { EventMetadata metadata = new EventMetadata(); metadata.Add("Method", nameof(this.TryQueryGVFSConfig)); metadata.Add("Exception", e.ToString()); metadata.Add("Url", gvfsConfigEndpointString); this.Tracer.RelatedError(metadata, "UriFormatException when constructing Uri", Keywords.Network); return false; } long requestId = HttpRequestor.GetNewRequestId(); RetryWrapper retrier = new RetryWrapper(this.RetryConfig.MaxAttempts, CancellationToken.None); if (logErrors) { retrier.OnFailure += RetryWrapper.StandardErrorHandler(this.Tracer, requestId, "QueryGvfsConfig"); } RetryWrapper.InvocationResult output = retrier.Invoke( tryCount => { using (GitEndPointResponseData response = this.SendRequest( requestId, gvfsConfigEndpoint, HttpMethod.Get, requestContent: null, cancellationToken: CancellationToken.None)) { if (response.HasErrors) { return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry); } try { string configString = response.RetryableReadToEnd(); ServerGVFSConfig config = JsonConvert.DeserializeObject(configString); return new RetryWrapper.CallbackResult(config); } catch (JsonReaderException e) { return new RetryWrapper.CallbackResult(e, shouldRetry: false); } } }); if (output.Succeeded) { serverGVFSConfig = output.Result; httpStatus = HttpStatusCode.OK; return true; } GitObjectsHttpException httpException = output.Error as GitObjectsHttpException; if (httpException != null) { httpStatus = httpException.StatusCode; errorMessage = httpException.Message; } if (logErrors) { this.Tracer.RelatedError( new EventMetadata { { "Exception", output.Error.ToString() } }, $"{nameof(this.TryQueryGVFSConfig)} failed"); } return false; } } } ================================================ FILE: GVFS/GVFS.Common/Http/GitEndPointResponseData.cs ================================================ using GVFS.Common.Git; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; namespace GVFS.Common.Http { public class GitEndPointResponseData : IDisposable { private HttpResponseMessage message; private Action onResponseDisposed; /// /// Constructor used when GitEndPointResponseData contains an error response /// public GitEndPointResponseData(HttpStatusCode statusCode, Exception error, bool shouldRetry, HttpResponseMessage message, Action onResponseDisposed) { this.StatusCode = statusCode; this.Error = error; this.ShouldRetry = shouldRetry; this.message = message; this.onResponseDisposed = onResponseDisposed; } /// /// Constructor used when GitEndPointResponseData contains a successful response /// public GitEndPointResponseData(HttpStatusCode statusCode, string contentType, Stream responseStream, HttpResponseMessage message, Action onResponseDisposed) : this(statusCode, null, false, message, onResponseDisposed) { this.Stream = responseStream; this.ContentType = MapContentType(contentType); } public Exception Error { get; } public bool ShouldRetry { get; } public HttpStatusCode StatusCode { get; } public Stream Stream { get; private set; } public bool HasErrors { get { return this.StatusCode != HttpStatusCode.OK; } } public GitObjectContentType ContentType { get; } /// /// Reads the underlying stream until it ends returning all content as a string. /// public string RetryableReadToEnd() { if (this.Stream == null) { throw new RetryableException("Stream is null (this could be a result of network flakiness), retrying."); } if (!this.Stream.CanRead) { throw new RetryableException("Stream is not readable (this could be a result of network flakiness), retrying."); } using (StreamReader contentStreamReader = new StreamReader(this.Stream)) { try { return contentStreamReader.ReadToEnd(); } catch (Exception ex) { // All exceptions potentially from network should be retried throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); } } } /// /// Reads the stream until it ends returning each line as a string. /// public List RetryableReadAllLines() { using (StreamReader contentStreamReader = new StreamReader(this.Stream)) { List output = new List(); while (true) { string line; try { if (contentStreamReader.EndOfStream) { break; } line = contentStreamReader.ReadLine(); } catch (Exception ex) { // All exceptions potentially from network should be retried throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); } output.Add(line); } return output; } } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } public void Dispose(bool disposing) { if (disposing) { if (this.message != null) { this.message.Dispose(); this.message = null; } if (this.Stream != null) { this.Stream.Dispose(); this.Stream = null; } if (this.onResponseDisposed != null) { this.onResponseDisposed(); this.onResponseDisposed = null; } } } /// /// Convert from a string-based Content-Type to /// private static GitObjectContentType MapContentType(string contentType) { switch (contentType) { case GVFSConstants.MediaTypes.LooseObjectMediaType: return GitObjectContentType.LooseObject; case GVFSConstants.MediaTypes.CustomLooseObjectsMediaType: return GitObjectContentType.BatchedLooseObjects; case GVFSConstants.MediaTypes.PackFileMediaType: return GitObjectContentType.PackFile; default: return GitObjectContentType.None; } } } } ================================================ FILE: GVFS/GVFS.Common/Http/GitObjectsHttpException.cs ================================================ using System; using System.Net; namespace GVFS.Common.Http { public class GitObjectsHttpException : Exception { public GitObjectsHttpException(HttpStatusCode statusCode, string ex) : base(ex) { this.StatusCode = statusCode; } public HttpStatusCode StatusCode { get; } } } ================================================ FILE: GVFS/GVFS.Common/Http/GitObjectsHttpRequestor.cs ================================================ using GVFS.Common.Git; using GVFS.Common.Tracing; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; namespace GVFS.Common.Http { public class GitObjectsHttpRequestor : HttpRequestor { private static readonly MediaTypeWithQualityHeaderValue CustomLooseObjectsHeader = new MediaTypeWithQualityHeaderValue(GVFSConstants.MediaTypes.CustomLooseObjectsMediaType); private Enlistment enlistment; private DateTime nextCacheServerAttemptTime = DateTime.Now; public GitObjectsHttpRequestor(ITracer tracer, Enlistment enlistment, CacheServerInfo cacheServer, RetryConfig retryConfig) : base(tracer, retryConfig, enlistment) { this.enlistment = enlistment; this.CacheServer = cacheServer; } public CacheServerInfo CacheServer { get; private set; } public virtual List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) { long requestId = HttpRequestor.GetNewRequestId(); string objectIdsJson = ToJsonList(objectIds); Uri cacheServerEndpoint = new Uri(this.CacheServer.SizesEndpointUrl); Uri originEndpoint = new Uri(this.enlistment.RepoUrl + GVFSConstants.Endpoints.GVFSSizes); EventMetadata metadata = new EventMetadata(); metadata.Add("RequestId", requestId); int objectIdCount = objectIds.Count(); if (objectIdCount > 10) { metadata.Add("ObjectIdCount", objectIdCount); } else { metadata.Add("ObjectIdJson", objectIdsJson); } this.Tracer.RelatedEvent(EventLevel.Informational, "QueryFileSizes", metadata, Keywords.Network); RetryWrapper> retrier = new RetryWrapper>(this.RetryConfig.MaxAttempts, cancellationToken); retrier.OnFailure += RetryWrapper>.StandardErrorHandler(this.Tracer, requestId, "QueryFileSizes"); RetryWrapper>.InvocationResult requestTask = retrier.Invoke( tryCount => { Uri gvfsEndpoint; if (this.nextCacheServerAttemptTime < DateTime.Now) { gvfsEndpoint = cacheServerEndpoint; } else { gvfsEndpoint = originEndpoint; } using (GitEndPointResponseData response = this.SendRequest(requestId, gvfsEndpoint, HttpMethod.Post, objectIdsJson, cancellationToken)) { if (response.StatusCode == HttpStatusCode.NotFound) { this.nextCacheServerAttemptTime = DateTime.Now.AddDays(1); return new RetryWrapper>.CallbackResult(response.Error, true); } if (response.HasErrors) { return new RetryWrapper>.CallbackResult(response.Error, response.ShouldRetry); } string objectSizesString = response.RetryableReadToEnd(); List objectSizes = JsonConvert.DeserializeObject>(objectSizesString); return new RetryWrapper>.CallbackResult(objectSizes); } }); return requestTask.Result ?? new List(0); } public virtual GitRefs QueryInfoRefs(string branch) { long requestId = HttpRequestor.GetNewRequestId(); Uri infoRefsEndpoint; try { infoRefsEndpoint = new Uri(this.enlistment.RepoUrl + GVFSConstants.Endpoints.InfoRefs); } catch (UriFormatException) { return null; } RetryWrapper retrier = new RetryWrapper(this.RetryConfig.MaxAttempts, CancellationToken.None); retrier.OnFailure += RetryWrapper.StandardErrorHandler(this.Tracer, requestId, "QueryInfoRefs"); RetryWrapper.InvocationResult output = retrier.Invoke( tryCount => { using (GitEndPointResponseData response = this.SendRequest( requestId, infoRefsEndpoint, HttpMethod.Get, requestContent: null, cancellationToken: CancellationToken.None)) { if (response.HasErrors) { return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry); } List infoRefsResponse = response.RetryableReadAllLines(); return new RetryWrapper.CallbackResult(new GitRefs(infoRefsResponse, branch)); } }); return output.Result; } public virtual RetryWrapper.InvocationResult TryDownloadLooseObject( string objectId, bool retryOnFailure, CancellationToken cancellationToken, string requestSource, Func.CallbackResult> onSuccess) { long requestId = HttpRequestor.GetNewRequestId(); EventMetadata metadata = new EventMetadata(); metadata.Add("objectId", objectId); metadata.Add("retryOnFailure", retryOnFailure); metadata.Add("requestId", requestId); metadata.Add("requestSource", requestSource); this.Tracer.RelatedEvent(EventLevel.Informational, "DownloadLooseObject", metadata, Keywords.Network); return this.TrySendProtocolRequest( requestId, onSuccess, eArgs => this.HandleDownloadAndSaveObjectError(retryOnFailure, requestId, eArgs), HttpMethod.Get, new Uri(this.CacheServer.ObjectsEndpointUrl + "/" + objectId), cancellationToken, requestBody: null, acceptType: null, retryOnFailure: retryOnFailure); } public virtual RetryWrapper.InvocationResult TryDownloadObjects( Func> objectIdGenerator, Func.CallbackResult> onSuccess, Action.ErrorEventArgs> onFailure, bool preferBatchedLooseObjects) { // We pass the query generator in as a function because we don't want the consumer to know about JSON or network retry logic, // but we still want the consumer to be able to change the query on each retry if we fail during their onSuccess handler. long requestId = HttpRequestor.GetNewRequestId(); return this.TrySendProtocolRequest( requestId, onSuccess, onFailure, HttpMethod.Post, new Uri(this.CacheServer.ObjectsEndpointUrl), CancellationToken.None, () => this.ObjectIdsJsonGenerator(requestId, objectIdGenerator), preferBatchedLooseObjects ? CustomLooseObjectsHeader : null); } public virtual RetryWrapper.InvocationResult TryDownloadObjects( IEnumerable objectIds, Func.CallbackResult> onSuccess, Action.ErrorEventArgs> onFailure, bool preferBatchedLooseObjects) { long requestId = HttpRequestor.GetNewRequestId(); string objectIdsJson = CreateObjectIdJson(objectIds); int objectCount = objectIds.Count(); EventMetadata metadata = new EventMetadata(); metadata.Add("RequestId", requestId); if (objectCount < 10) { metadata.Add("ObjectIds", string.Join(", ", objectIds)); } else { metadata.Add("ObjectIdCount", objectCount); } this.Tracer.RelatedEvent(EventLevel.Informational, "DownloadObjects", metadata, Keywords.Network); return this.TrySendProtocolRequest( requestId, onSuccess, onFailure, HttpMethod.Post, new Uri(this.CacheServer.ObjectsEndpointUrl), CancellationToken.None, objectIdsJson, preferBatchedLooseObjects ? CustomLooseObjectsHeader : null); } public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( long requestId, Func.CallbackResult> onSuccess, Action.ErrorEventArgs> onFailure, HttpMethod method, Uri endPoint, CancellationToken cancellationToken, string requestBody = null, MediaTypeWithQualityHeaderValue acceptType = null, bool retryOnFailure = true) { return this.TrySendProtocolRequest( requestId, onSuccess, onFailure, method, endPoint, cancellationToken, () => requestBody, acceptType, retryOnFailure); } public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( long requestId, Func.CallbackResult> onSuccess, Action.ErrorEventArgs> onFailure, HttpMethod method, Uri endPoint, CancellationToken cancellationToken, Func requestBodyGenerator, MediaTypeWithQualityHeaderValue acceptType = null, bool retryOnFailure = true) { return this.TrySendProtocolRequest( requestId, onSuccess, onFailure, method, () => endPoint, requestBodyGenerator, cancellationToken, acceptType, retryOnFailure); } public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( long requestId, Func.CallbackResult> onSuccess, Action.ErrorEventArgs> onFailure, HttpMethod method, Func endPointGenerator, Func requestBodyGenerator, CancellationToken cancellationToken, MediaTypeWithQualityHeaderValue acceptType = null, bool retryOnFailure = true) { RetryWrapper retrier = new RetryWrapper( retryOnFailure ? this.RetryConfig.MaxAttempts : 1, cancellationToken); if (onFailure != null) { retrier.OnFailure += onFailure; } return retrier.Invoke( tryCount => { using (GitEndPointResponseData response = this.SendRequest( requestId, endPointGenerator(), method, requestBodyGenerator(), cancellationToken, acceptType)) { if (response.HasErrors) { return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry, new GitObjectTaskResult(response.StatusCode)); } return onSuccess(tryCount, response); } }); } private static string ToJsonList(IEnumerable strings) { return "[\"" + string.Join("\",\"", strings) + "\"]"; } private static string CreateObjectIdJson(IEnumerable strings) { return "{\"commitDepth\": 1, \"objectIds\":" + ToJsonList(strings) + "}"; } private void HandleDownloadAndSaveObjectError(bool retryOnFailure, long requestId, RetryWrapper.ErrorEventArgs errorArgs) { // Silence logging 404's for object downloads. They are far more likely to be git checking for the // previous existence of a new object than a truly missing object. GitObjectsHttpException ex = errorArgs.Error as GitObjectsHttpException; if (ex != null && ex.StatusCode == HttpStatusCode.NotFound) { return; } // If the caller has requested that we not retry on failure, caller must handle logging errors bool forceLogAsWarning = !retryOnFailure; RetryWrapper.StandardErrorHandler(this.Tracer, requestId, nameof(this.TryDownloadLooseObject), forceLogAsWarning)(errorArgs); } private string ObjectIdsJsonGenerator(long requestId, Func> objectIdGenerator) { IEnumerable objectIds = objectIdGenerator(); string objectIdsJson = CreateObjectIdJson(objectIds); int objectCount = objectIds.Count(); EventMetadata metadata = new EventMetadata(); metadata.Add("RequestId", requestId); if (objectCount < 10) { metadata.Add("ObjectIds", string.Join(", ", objectIds)); } else { metadata.Add("ObjectIdCount", objectCount); } this.Tracer.RelatedEvent(EventLevel.Informational, "DownloadObjects", metadata, Keywords.Network); return objectIdsJson; } public class GitObjectSize { public readonly string Id; public readonly long Size; [JsonConstructor] public GitObjectSize(string id, long size) { this.Id = id; this.Size = size; } } public class GitObjectTaskResult { public GitObjectTaskResult(bool success) { this.Success = success; } public GitObjectTaskResult(HttpStatusCode statusCode) : this(statusCode == HttpStatusCode.OK) { this.HttpStatusCodeResult = statusCode; } public bool Success { get; } public HttpStatusCode HttpStatusCodeResult { get; } } } } ================================================ FILE: GVFS/GVFS.Common/Http/HttpRequestor.cs ================================================ using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; namespace GVFS.Common.Http { public abstract class HttpRequestor : IDisposable { private const int ConnectionPoolWaitTimeoutMs = 30_000; private const int ConnectionPoolContentionThresholdMs = 100; private static long requestCount = 0; private static SemaphoreSlim availableConnections; private static int connectionLimitConfigured = 0; private readonly ProductInfoHeaderValue userAgentHeader; private readonly GitAuthentication authentication; private HttpClient client; static HttpRequestor() { /* If machine.config is locked, then initializing ServicePointManager will fail and be unrecoverable. * Machine.config locking is typically very brief (~1ms by the antivirus scanner) so we can attempt to lock * it ourselves (by opening it for read) *beforehand and briefly wait if it's locked */ using (var machineConfigLock = GetMachineConfigLock()) { ServicePointManager.SecurityProtocol = ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12; // HTTP downloads are I/O-bound, not CPU-bound, so we default to // 2x ProcessorCount. Can be overridden via gvfs.max-http-connections. int connectionLimit = 2 * Environment.ProcessorCount; ServicePointManager.DefaultConnectionLimit = connectionLimit; availableConnections = new SemaphoreSlim(connectionLimit); } } protected HttpRequestor(ITracer tracer, RetryConfig retryConfig, Enlistment enlistment) { this.RetryConfig = retryConfig; this.authentication = enlistment.Authentication; this.Tracer = tracer; // On first instantiation, check git config for a custom connection limit. // This runs before any requests are made (during mount initialization). if (Interlocked.CompareExchange(ref connectionLimitConfigured, 1, 0) == 0) { TryApplyConnectionLimitFromConfig(tracer, enlistment); } HttpClientHandler httpClientHandler = new HttpClientHandler() { UseDefaultCredentials = true }; this.authentication.ConfigureHttpClientHandlerSslIfNeeded(this.Tracer, httpClientHandler, enlistment.CreateGitProcess()); this.client = new HttpClient(httpClientHandler) { Timeout = retryConfig.Timeout }; this.userAgentHeader = new ProductInfoHeaderValue(ProcessHelper.GetEntryClassName(), ProcessHelper.GetCurrentProcessVersion()); } public RetryConfig RetryConfig { get; } protected ITracer Tracer { get; } public static long GetNewRequestId() { return Interlocked.Increment(ref requestCount); } public void Dispose() { if (this.client != null) { this.client.Dispose(); this.client = null; } } protected GitEndPointResponseData SendRequest( long requestId, Uri requestUri, HttpMethod httpMethod, string requestContent, CancellationToken cancellationToken, MediaTypeWithQualityHeaderValue acceptType = null) { string authString = null; string errorMessage; if (!this.authentication.IsAnonymous && !this.authentication.TryGetCredentials(this.Tracer, out authString, out errorMessage)) { return new GitEndPointResponseData( HttpStatusCode.Unauthorized, new GitObjectsHttpException(HttpStatusCode.Unauthorized, errorMessage), shouldRetry: true, message: null, onResponseDisposed: null); } HttpRequestMessage request = new HttpRequestMessage(httpMethod, requestUri); // By default, VSTS auth failures result in redirects to SPS to reauthenticate. // To provide more consistent behavior when using the GCM, have them send us 401s instead request.Headers.Add("X-TFS-FedAuthRedirect", "Suppress"); request.Headers.UserAgent.Add(this.userAgentHeader); if (!this.authentication.IsAnonymous) { request.Headers.Authorization = new AuthenticationHeaderValue("Basic", authString); } if (acceptType != null) { request.Headers.Accept.Add(acceptType); } if (requestContent != null) { request.Content = new StringContent(requestContent, Encoding.UTF8, "application/json"); } EventMetadata responseMetadata = new EventMetadata(); responseMetadata.Add("RequestId", requestId); responseMetadata.Add("availableConnections", availableConnections.CurrentCount); Stopwatch requestStopwatch = Stopwatch.StartNew(); if (!availableConnections.Wait(ConnectionPoolWaitTimeoutMs, cancellationToken)) { TimeSpan connectionWaitTime = requestStopwatch.Elapsed; responseMetadata.Add("connectionWaitTimeMS", $"{connectionWaitTime.TotalMilliseconds:F4}"); this.Tracer.RelatedWarning(responseMetadata, "SendRequest: Connection pool exhausted, all connections busy"); return new GitEndPointResponseData( HttpStatusCode.ServiceUnavailable, new GitObjectsHttpException(HttpStatusCode.ServiceUnavailable, "Connection pool exhausted - all connections busy"), shouldRetry: true, message: null, onResponseDisposed: null); } TimeSpan connectionWaitTimeElapsed = requestStopwatch.Elapsed; if (connectionWaitTimeElapsed.TotalMilliseconds > ConnectionPoolContentionThresholdMs) { EventMetadata contentionMetadata = new EventMetadata(); contentionMetadata.Add("RequestId", requestId); contentionMetadata.Add("availableConnections", availableConnections.CurrentCount); contentionMetadata.Add("connectionWaitTimeMS", $"{connectionWaitTimeElapsed.TotalMilliseconds:F4}"); this.Tracer.RelatedWarning(contentionMetadata, "SendRequest: Connection pool contention detected"); } TimeSpan responseWaitTime = default(TimeSpan); GitEndPointResponseData gitEndPointResponseData = null; HttpResponseMessage response = null; try { requestStopwatch.Restart(); try { response = this.client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).GetAwaiter().GetResult(); } catch (HttpRequestException httpRequestException) when (TryGetResponseMessageFromHttpRequestException(httpRequestException, request, out response)) { /* HttpClientHandler will automatically resubmit in certain circumstances, such as a 401 unauthorized response when UseDefaultCredentials * is true but another credential was provided. This resubmit can throw (instead of returning a proper status code) in some case cases, such * as when there is an exception loading the default credentials. * If we can extract the original response message from the exception, we can continue and process the original failed status code. */ Tracer.RelatedWarning(responseMetadata, $"An exception occurred while resubmitting the request, but the original response is available."); } finally { responseWaitTime = requestStopwatch.Elapsed; } responseMetadata.Add("CacheName", GetSingleHeaderOrEmpty(response.Headers, "X-Cache-Name")); responseMetadata.Add("StatusCode", response.StatusCode); if (response.StatusCode == HttpStatusCode.OK) { string contentType = GetSingleHeaderOrEmpty(response.Content.Headers, "Content-Type"); responseMetadata.Add("ContentType", contentType); if (!this.authentication.IsAnonymous) { this.authentication.ApproveCredentials(this.Tracer, authString); } Stream responseStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); gitEndPointResponseData = new GitEndPointResponseData( response.StatusCode, contentType, responseStream, message: response, onResponseDisposed: () => availableConnections.Release()); } else { errorMessage = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); int statusInt = (int)response.StatusCode; bool shouldRetry = ShouldRetry(response.StatusCode); if (response.StatusCode == HttpStatusCode.Unauthorized && this.authentication.IsAnonymous) { shouldRetry = false; errorMessage = "Anonymous request was rejected with a 401"; } else if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.BadRequest || response.StatusCode == HttpStatusCode.Redirect) { this.authentication.RejectCredentials(this.Tracer, authString); if (!this.authentication.IsBackingOff) { errorMessage = string.Format("Server returned error code {0} ({1}). Your PAT may be expired and we are asking for a new one. Original error message from server: {2}", statusInt, response.StatusCode, errorMessage); } else { errorMessage = string.Format("Server returned error code {0} ({1}) after successfully renewing your PAT. You may not have access to this repo. Original error message from server: {2}", statusInt, response.StatusCode, errorMessage); } } else { errorMessage = string.Format("Server returned error code {0} ({1}). Original error message from server: {2}", statusInt, response.StatusCode, errorMessage); } gitEndPointResponseData = new GitEndPointResponseData( response.StatusCode, new GitObjectsHttpException(response.StatusCode, errorMessage), shouldRetry, message: response, onResponseDisposed: () => availableConnections.Release()); } } catch (TaskCanceledException) { cancellationToken.ThrowIfCancellationRequested(); errorMessage = string.Format("Request to {0} timed out", requestUri); gitEndPointResponseData = new GitEndPointResponseData( HttpStatusCode.RequestTimeout, new GitObjectsHttpException(HttpStatusCode.RequestTimeout, errorMessage), shouldRetry: true, message: response, onResponseDisposed: () => availableConnections.Release()); } catch (HttpRequestException httpRequestException) when (httpRequestException.InnerException is System.Security.Authentication.AuthenticationException) { // This exception is thrown on OSX, when user declines to give permission to access certificate gitEndPointResponseData = new GitEndPointResponseData( HttpStatusCode.Unauthorized, httpRequestException.InnerException, shouldRetry: false, message: response, onResponseDisposed: () => availableConnections.Release()); } catch (WebException ex) { gitEndPointResponseData = new GitEndPointResponseData( HttpStatusCode.InternalServerError, ex, shouldRetry: true, message: response, onResponseDisposed: () => availableConnections.Release()); } finally { responseMetadata.Add("connectionWaitTimeMS", $"{connectionWaitTimeElapsed.TotalMilliseconds:F4}"); responseMetadata.Add("responseWaitTimeMS", $"{responseWaitTime.TotalMilliseconds:F4}"); this.Tracer.RelatedEvent(EventLevel.Informational, "NetworkResponse", responseMetadata); if (gitEndPointResponseData == null) { // If gitEndPointResponseData is null there was an unhandled exception if (response != null) { response.Dispose(); } availableConnections.Release(); } } return gitEndPointResponseData; } private static bool ShouldRetry(HttpStatusCode statusCode) { // Retry timeout, Unauthorized, 429 (Too Many Requests), and 5xx errors int statusInt = (int)statusCode; if (statusCode == HttpStatusCode.RequestTimeout || statusCode == HttpStatusCode.Unauthorized || statusInt == 429 || (statusInt >= 500 && statusInt < 600)) { return true; } return false; } private static string GetSingleHeaderOrEmpty(HttpHeaders headers, string headerName) { IEnumerable values; if (headers.TryGetValues(headerName, out values)) { return values.First(); } return string.Empty; } /// /// This method is based on a private method System.Net.Http.HttpClientHandler.CreateResponseMessage /// private static bool TryGetResponseMessageFromHttpRequestException(HttpRequestException httpRequestException, HttpRequestMessage request, out HttpResponseMessage httpResponseMessage) { var webResponse = (httpRequestException?.InnerException as WebException)?.Response as HttpWebResponse; if (webResponse == null) { httpResponseMessage = null; return false; } httpResponseMessage = new HttpResponseMessage(webResponse.StatusCode); httpResponseMessage.ReasonPhrase = webResponse.StatusDescription; httpResponseMessage.Version = webResponse.ProtocolVersion; httpResponseMessage.RequestMessage = request; httpResponseMessage.Content = new StreamContent(webResponse.GetResponseStream()); request.RequestUri = webResponse.ResponseUri; WebHeaderCollection rawHeaders = webResponse.Headers; HttpContentHeaders responseContentHeaders = httpResponseMessage.Content.Headers; HttpResponseHeaders responseHeaders = httpResponseMessage.Headers; if (webResponse.ContentLength >= 0) { responseContentHeaders.ContentLength = webResponse.ContentLength; } for (int i = 0; i < rawHeaders.Count; i++) { string key = rawHeaders.GetKey(i); if (string.Compare(key, "Content-Length", StringComparison.OrdinalIgnoreCase) != 0) { string[] values = rawHeaders.GetValues(i); if (!responseHeaders.TryAddWithoutValidation(key, values)) { bool flag = responseContentHeaders.TryAddWithoutValidation(key, values); } } } return true; } private static void TryApplyConnectionLimitFromConfig(ITracer tracer, Enlistment enlistment) { try { GitProcess.ConfigResult result = enlistment.CreateGitProcess().GetFromConfig(GVFSConstants.GitConfig.MaxHttpConnectionsConfig); string error; int configuredLimit; if (!result.TryParseAsInt(0, 1, out configuredLimit, out error)) { EventMetadata metadata = new EventMetadata(); metadata.Add("error", error); tracer.RelatedWarning(metadata, "HttpRequestor: Invalid gvfs.max-http-connections config value, using default"); return; } if (configuredLimit > 0) { int currentLimit = ServicePointManager.DefaultConnectionLimit; ServicePointManager.DefaultConnectionLimit = configuredLimit; // Adjust the existing semaphore rather than replacing it, so any // in-flight waiters release permits to the correct instance. int delta = configuredLimit - currentLimit; if (delta > 0) { for (int i = 0; i < delta; i++) { availableConnections.Release(); } } else if (delta < 0) { for (int i = 0; i < -delta; i++) { availableConnections.Wait(); } } EventMetadata metadata = new EventMetadata(); metadata.Add("configuredLimit", configuredLimit); metadata.Add("previousLimit", currentLimit); tracer.RelatedEvent(EventLevel.Informational, "HttpRequestor_ConnectionLimitConfigured", metadata); } } catch (Exception e) { EventMetadata metadata = new EventMetadata(); metadata.Add("Exception", e.ToString()); tracer.RelatedWarning(metadata, "HttpRequestor: Failed to read gvfs.max-http-connections config, using default"); } } private static FileStream GetMachineConfigLock() { var machineConfigLocation = RuntimeEnvironment.SystemConfigurationFile; var tries = 0; var maxTries = 3; while (tries++ < maxTries) { try { /* Opening with FileShare.Read will fail if another process (eg antivirus) has opened the file for write, but will still let ServicePointManager read the file.*/ FileStream stream = File.Open(machineConfigLocation, FileMode.Open, FileAccess.Read, FileShare.Read); return stream; } catch (IOException e) when ((uint)e.HResult == 0x80070020) // SHARING_VIOLATION { Thread.Sleep(10); } } /* Couldn't get the lock - the process will likely fail. */ return null; } } } ================================================ FILE: GVFS/GVFS.Common/IDiskLayoutUpgradeData.cs ================================================ using GVFS.DiskLayoutUpgrades; using System; namespace GVFS.Common { public interface IDiskLayoutUpgradeData { DiskLayoutUpgrade[] Upgrades { get; } DiskLayoutVersion Version { get; } bool TryParseLegacyDiskLayoutVersion(string dotGVFSPath, out int majorVersion); } } ================================================ FILE: GVFS/GVFS.Common/IHeartBeatMetadataProvider.cs ================================================ using GVFS.Common.Tracing; namespace GVFS.Common { public interface IHeartBeatMetadataProvider { EventMetadata GetAndResetHeartBeatMetadata(out bool logToFile); } } ================================================ FILE: GVFS/GVFS.Common/IProcessRunner.cs ================================================ namespace GVFS.Common { /// /// Interface around process helper methods. This is to enable /// testing of components that interact with the ProcessHelper /// static class. /// public interface IProcessRunner { ProcessResult Run(string programName, string args, bool redirectOutput); } } ================================================ FILE: GVFS/GVFS.Common/InternalVerbParameters.cs ================================================ using Newtonsoft.Json; namespace GVFS.Common { public class InternalVerbParameters { public InternalVerbParameters( string serviceName = null, bool startedByService = true, string maintenanceJob = null, string packfileMaintenanceBatchSize = null) { this.ServiceName = serviceName; this.StartedByService = startedByService; this.MaintenanceJob = maintenanceJob; this.PackfileMaintenanceBatchSize = packfileMaintenanceBatchSize; } public string ServiceName { get; private set; } public bool StartedByService { get; private set; } public string MaintenanceJob { get; private set; } public string PackfileMaintenanceBatchSize { get; private set; } public static InternalVerbParameters FromJson(string json) { return JsonConvert.DeserializeObject(json); } public string ToJson() { return JsonConvert.SerializeObject(this); } } } ================================================ FILE: GVFS/GVFS.Common/InternalsVisibleTo.cs ================================================ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("GVFS.UnitTests")] ================================================ FILE: GVFS/GVFS.Common/InvalidRepoException.cs ================================================ using System; namespace GVFS.Common { public class InvalidRepoException : Exception { public InvalidRepoException(string message) : base(message) { } } } ================================================ FILE: GVFS/GVFS.Common/LegacyPlaceholderListDatabase.cs ================================================ using GVFS.Common.Database; using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; namespace GVFS.Common { public class LegacyPlaceholderListDatabase : FileBasedCollection, IPlaceholderCollection { // Special folder values must: // - Be 40 characters long // - Not be a valid SHA-1 value (to avoid collisions with files) public const string PartialFolderValue = " PARTIAL FOLDER"; public const string ExpandedFolderValue = " EXPANDED FOLDER"; public const string PossibleTombstoneFolderValue = " POSSIBLE TOMBSTONE FOLDER"; private const char PathTerminator = '\0'; // This list holds placeholder entries that are created between calls to // GetAllEntries and WriteAllEntriesAndFlush. // // Example: // // 1) VFS4G parses the updated index (as part of a projection change) // 2) VFS4G starts the work to update placeholders // 3) VFS4G calls GetAllEntries // 4) VFS4G starts updating placeholders // 5) Some application reads a pure-virtual file (creating a new placeholder) while VFS4G is updating existing placeholders. // That new placeholder is added to placeholderChangesWhileRebuildingList. // 6) VFS4G completes updating the placeholders and calls WriteAllEntriesAndFlush. // Note: this list does *not* include the placeholders created in step 5, as the were not included in GetAllEntries. // 7) WriteAllEntriesAndFlush writes *both* the entires in placeholderDataEntries and those that were passed in as the parameter. // // This scenario is covered in the unit test PlaceholderDatabaseTests.HandlesRaceBetweenAddAndWriteAllEntries // // Because of this list, callers must always call WriteAllEntries after calling GetAllEntries. // // This list must always be accessed from inside one of FileBasedCollection's synchronizedAction callbacks because // there is race potential between creating the queue, adding to the queue, and writing to the data file. private List placeholderChangesWhileRebuildingList; private int count; private LegacyPlaceholderListDatabase(ITracer tracer, PhysicalFileSystem fileSystem, string dataFilePath) : base(tracer, fileSystem, dataFilePath, collectionAppendsDirectlyToFile: true) { } public static bool TryCreate(ITracer tracer, string dataFilePath, PhysicalFileSystem fileSystem, out LegacyPlaceholderListDatabase output, out string error) { LegacyPlaceholderListDatabase temp = new LegacyPlaceholderListDatabase(tracer, fileSystem, dataFilePath); // We don't want to cache placeholders so this just serves to validate early and populate count. if (!temp.TryLoadFromDisk( temp.TryParseAddLine, temp.TryParseRemoveLine, (key, value) => temp.count++, out error)) { temp = null; output = null; return false; } error = null; output = temp; return true; } /// /// The Count is "estimated" because it's simply (# adds - # deletes). There is nothing to prevent /// multiple adds or deletes of the same path from being double counted /// public int GetCount() { return this.count; } public void AddFile(string path, string sha) { this.AddAndFlush(path, sha); } public void AddPartialFolder(string path, string sha) { this.AddAndFlush(path, PartialFolderValue); } public void AddExpandedFolder(string path) { this.AddAndFlush(path, ExpandedFolderValue); } public void AddPossibleTombstoneFolder(string path) { this.AddAndFlush(path, PossibleTombstoneFolderValue); } public void Remove(string path) { try { this.WriteRemoveEntry( path, () => { this.count--; if (this.placeholderChangesWhileRebuildingList != null) { this.placeholderChangesWhileRebuildingList.Add(new PlaceholderDataEntry(path)); } }); } catch (Exception e) { throw new FileBasedCollectionException(e); } } /// /// Gets all entries and prepares the PlaceholderListDatabase for a call to WriteAllEntriesAndFlush. /// /// /// GetAllEntries was called (a second time) without first calling WriteAllEntriesAndFlush. /// /// /// Usage notes: /// - All calls to GetAllEntries must be paired with a subsequent call to WriteAllEntriesAndFlush /// - If WriteAllEntriesAndFlush is *not* called entries that were added to the PlaceholderListDatabase after /// calling GetAllEntries will be lost /// public List GetAllEntries() { try { List placeholders = new List(Math.Max(1, this.count)); string error; if (!this.TryLoadFromDisk( this.TryParseAddLine, this.TryParseRemoveLine, (key, value) => placeholders.Add(new PlaceholderData(path: key, fileShaOrFolderValue: value)), out error, () => { if (this.placeholderChangesWhileRebuildingList != null) { throw new InvalidOperationException($"PlaceholderListDatabase should always flush queue placeholders using WriteAllEntriesAndFlush before calling {nameof(this.GetAllEntries)} again."); } this.placeholderChangesWhileRebuildingList = new List(); })) { throw new InvalidDataException(error); } return placeholders; } catch (Exception e) { throw new FileBasedCollectionException(e); } } /// /// Gets all entries and prepares the PlaceholderListDatabase for a call to WriteAllEntriesAndFlush. /// /// /// GetAllEntries was called (a second time) without first calling WriteAllEntriesAndFlush. /// /// /// Usage notes: /// - All calls to GetAllEntries must be paired with a subsequent call to WriteAllEntriesAndFlush /// - If WriteAllEntriesAndFlush is *not* called entries that were added to the PlaceholderListDatabase after /// calling GetAllEntries will be lost /// public void GetAllEntries(out List filePlaceholders, out List folderPlaceholders) { try { List filePlaceholdersFromDisk = new List(Math.Max(1, this.count)); List folderPlaceholdersFromDisk = new List(Math.Max(1, (int)(this.count * .3))); string error; if (!this.TryLoadFromDisk( this.TryParseAddLine, this.TryParseRemoveLine, (key, value) => { if (PlaceholderData.IsShaAFolder(value)) { folderPlaceholdersFromDisk.Add(new PlaceholderData(path: key, fileShaOrFolderValue: value)); } else { filePlaceholdersFromDisk.Add(new PlaceholderData(path: key, fileShaOrFolderValue: value)); } }, out error, () => { if (this.placeholderChangesWhileRebuildingList != null) { throw new InvalidOperationException($"PlaceholderListDatabase should always flush queue placeholders using WriteAllEntriesAndFlush before calling {(nameof(this.GetAllEntries))} again."); } this.placeholderChangesWhileRebuildingList = new List(); })) { throw new InvalidDataException(error); } filePlaceholders = filePlaceholdersFromDisk; folderPlaceholders = folderPlaceholdersFromDisk; } catch (Exception e) { throw new FileBasedCollectionException(e); } } public HashSet GetAllFilePaths() { try { HashSet filePlaceholderPaths = new HashSet(StringComparer.Ordinal); string error; if (!this.TryLoadFromDisk( this.TryParseAddLine, this.TryParseRemoveLine, (key, value) => { if (!PlaceholderData.IsShaAFolder(value)) { filePlaceholderPaths.Add(key); } }, out error)) { throw new InvalidDataException(error); } return filePlaceholderPaths; } catch (Exception e) { throw new FileBasedCollectionException(e); } } public int GetFilePlaceholdersCount() { throw new NotImplementedException(); } public int GetFolderPlaceholdersCount() { throw new NotImplementedException(); } public void WriteAllEntriesAndFlush(IEnumerable updatedPlaceholders) { try { this.WriteAndReplaceDataFile(() => this.GenerateDataLines(updatedPlaceholders)); } catch (Exception e) { throw new FileBasedCollectionException(e); } } List IPlaceholderCollection.RemoveAllEntriesForFolder(string path) { throw new NotImplementedException(); } public void AddPlaceholderData(IPlaceholderData data) { throw new NotImplementedException(); } private IEnumerable GenerateDataLines(IEnumerable updatedPlaceholders) { HashSet keys = new HashSet(GVFSPlatform.Instance.Constants.PathComparer); this.count = 0; foreach (IPlaceholderData updated in updatedPlaceholders) { if (keys.Add(updated.Path)) { this.count++; } yield return this.FormatAddLine(updated.Path + PathTerminator + updated.Sha); } if (this.placeholderChangesWhileRebuildingList != null) { foreach (PlaceholderDataEntry entry in this.placeholderChangesWhileRebuildingList) { if (entry.DeleteEntry) { if (keys.Remove(entry.Path)) { this.count--; yield return this.FormatRemoveLine(entry.Path); } } else { if (keys.Add(entry.Path)) { this.count++; } yield return this.FormatAddLine(entry.Path + PathTerminator + entry.Sha); } } this.placeholderChangesWhileRebuildingList = null; } } private void AddAndFlush(string path, string sha) { try { this.WriteAddEntry( path + PathTerminator + sha, () => { this.count++; if (this.placeholderChangesWhileRebuildingList != null) { this.placeholderChangesWhileRebuildingList.Add(new PlaceholderDataEntry(path, sha)); } }); } catch (Exception e) { throw new FileBasedCollectionException(e); } } private bool TryParseAddLine(string line, out string key, out string value, out string error) { // Expected: \0<40-Char-SHA1> int idx = line.IndexOf(PathTerminator); if (idx < 0) { key = null; value = null; error = "Add line missing path terminator: " + line; return false; } if (idx + 1 + GVFSConstants.ShaStringLength != line.Length) { key = null; value = null; error = $"Invalid SHA1 length {line.Length - idx - 1}: " + line; return false; } key = line.Substring(0, idx); value = line.Substring(idx + 1, GVFSConstants.ShaStringLength); error = null; return true; } private bool TryParseRemoveLine(string line, out string key, out string error) { // The key is a path taking the entire line. key = line; error = null; return true; } public class PlaceholderData : IPlaceholderData { public PlaceholderData(string path, string fileShaOrFolderValue) { this.Path = path; this.Sha = fileShaOrFolderValue; } public string Path { get; } public string Sha { get; set; } public bool IsFolder { get { return IsShaAFolder(this.Sha); } } public bool IsExpandedFolder { get { return this.Sha.Equals(ExpandedFolderValue, StringComparison.Ordinal); } } public bool IsPossibleTombstoneFolder { get { return this.Sha.Equals(PossibleTombstoneFolderValue, StringComparison.Ordinal); } } public static bool IsShaAFolder(string shaValue) { return shaValue.Equals(PartialFolderValue, StringComparison.Ordinal) || shaValue.Equals(ExpandedFolderValue, StringComparison.Ordinal) || shaValue.Equals(PossibleTombstoneFolderValue, StringComparison.Ordinal); } } private class PlaceholderDataEntry { public PlaceholderDataEntry(string path, string sha) { this.Path = path; this.Sha = sha; this.DeleteEntry = false; } public PlaceholderDataEntry(string path) { this.Path = path; this.Sha = null; this.DeleteEntry = true; } public string Path { get; } public string Sha { get; } public bool DeleteEntry { get; } } } } ================================================ FILE: GVFS/GVFS.Common/LocalCacheResolver.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Http; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; using System.Threading; namespace GVFS.Common { public class LocalCacheResolver { private const string EtwArea = nameof(LocalCacheResolver); private const string MappingFile = "mapping.dat"; private const string MappingVersionKey = "GVFS_LocalCache_MappingVersion"; private const string CurrentMappingDataVersion = "1"; private GVFSEnlistment enlistment; private PhysicalFileSystem fileSystem; public LocalCacheResolver(GVFSEnlistment enlistment, PhysicalFileSystem fileSystem = null) { this.fileSystem = fileSystem ?? new PhysicalFileSystem(); this.enlistment = enlistment; } public static bool TryGetDefaultLocalCacheRoot(GVFSEnlistment enlistment, out string localCacheRoot, out string localCacheRootError) { if (GVFSEnlistment.IsUnattended(tracer: null)) { localCacheRoot = Path.Combine(enlistment.DotGVFSRoot, GVFSConstants.DefaultGVFSCacheFolderName); localCacheRootError = null; return true; } return GVFSPlatform.Instance.TryGetDefaultLocalCacheRoot(enlistment.EnlistmentRoot, out localCacheRoot, out localCacheRootError); } public bool TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers( ITracer tracer, ServerGVFSConfig serverGVFSConfig, CacheServerInfo currentCacheServer, string localCacheRoot, out string localCacheKey, out string errorMessage) { if (serverGVFSConfig == null) { throw new ArgumentNullException(nameof(serverGVFSConfig)); } localCacheKey = null; errorMessage = string.Empty; try { // A lock is required because FileBasedDictionary is not multi-process safe, neither is the act of adding a new cache string lockPath = Path.Combine(localCacheRoot, MappingFile + ".lock"); string createDirectoryError; if (!GVFSPlatform.Instance.FileSystem.TryCreateDirectoryAccessibleByAuthUsers(localCacheRoot, out createDirectoryError, tracer)) { errorMessage = $"Failed to create '{localCacheRoot}': {createDirectoryError}"; return false; } using (FileBasedLock mappingLock = GVFSPlatform.Instance.CreateFileBasedLock( this.fileSystem, tracer, lockPath)) { if (!this.TryAcquireLockWithRetries(tracer, mappingLock)) { errorMessage = "Failed to acquire lock file at " + lockPath; tracer.RelatedError(nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": " + errorMessage); return false; } FileBasedDictionary mappingFile; if (this.TryOpenMappingFile(tracer, localCacheRoot, out mappingFile, out errorMessage)) { try { string mappingDataVersion; if (mappingFile.TryGetValue(MappingVersionKey, out mappingDataVersion)) { if (mappingDataVersion != CurrentMappingDataVersion) { errorMessage = string.Format("Mapping file has different version than expected: {0} Actual: {1}", CurrentMappingDataVersion, mappingDataVersion); tracer.RelatedError(nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": " + errorMessage); return false; } } else { mappingFile.SetValueAndFlush(MappingVersionKey, CurrentMappingDataVersion); } if (mappingFile.TryGetValue(this.ToMappingKey(this.enlistment.RepoUrl), out localCacheKey) || (currentCacheServer.HasValidUrl() && mappingFile.TryGetValue(this.ToMappingKey(currentCacheServer.Url), out localCacheKey))) { EventMetadata metadata = CreateEventMetadata(); metadata.Add("localCacheKey", localCacheKey); metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); metadata.Add("currentCacheServer", currentCacheServer.ToString()); metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": Found existing local cache key"); tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_ExistingKey", metadata); return true; } else { EventMetadata metadata = CreateEventMetadata(); metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); metadata.Add("currentCacheServer", currentCacheServer.ToString()); string getLocalCacheKeyError; if (this.TryGetLocalCacheKeyFromRemoteCacheServers(tracer, serverGVFSConfig, currentCacheServer, mappingFile, out localCacheKey, out getLocalCacheKeyError)) { metadata.Add("localCacheKey", localCacheKey); metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": Generated new local cache key"); tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_NewKey", metadata); return true; } metadata.Add("getLocalCacheKeyError", getLocalCacheKeyError); tracer.RelatedError(metadata, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": TryGetLocalCacheKeyFromRemoteCacheServers failed"); errorMessage = "Failed to generate local cache key"; return false; } } finally { mappingFile.Dispose(); } } return false; } } catch (Exception e) { EventMetadata metadata = CreateEventMetadata(e); metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); metadata.Add("currentCacheServer", currentCacheServer.ToString()); tracer.RelatedError(metadata, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": Caught exception"); errorMessage = string.Format("Exception while getting local cache key: {0}", e.Message); return false; } } private static EventMetadata CreateEventMetadata(Exception e = null) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", EtwArea); if (e != null) { metadata.Add("Exception", e.ToString()); } return metadata; } private bool TryOpenMappingFile(ITracer tracer, string localCacheRoot, out FileBasedDictionary mappingFile, out string errorMessage) { mappingFile = null; errorMessage = string.Empty; string error; string mappingFilePath = Path.Combine(localCacheRoot, MappingFile); if (!FileBasedDictionary.TryCreate( tracer, mappingFilePath, this.fileSystem, out mappingFile, out error)) { errorMessage = "Could not open mapping file for local cache: " + error; EventMetadata metadata = CreateEventMetadata(); metadata.Add("mappingFilePath", mappingFilePath); metadata.Add("error", error); tracer.RelatedError(metadata, "TryOpenMappingFile: Could not open mapping file for local cache"); return false; } return true; } private bool TryGetLocalCacheKeyFromRemoteCacheServers( ITracer tracer, ServerGVFSConfig serverGVFSConfig, CacheServerInfo currentCacheServer, FileBasedDictionary mappingFile, out string localCacheKey, out string error) { error = null; localCacheKey = null; try { if (this.TryFindExistingLocalCacheKey(mappingFile, serverGVFSConfig.CacheServers, out localCacheKey)) { EventMetadata metadata = CreateEventMetadata(); metadata.Add("currentCacheServer", currentCacheServer.ToString()); metadata.Add("localCacheKey", localCacheKey); metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Found an existing a local key by cross referencing"); tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_ExistingKeyFromCrossReferencing", metadata); } else { localCacheKey = Guid.NewGuid().ToString("N"); EventMetadata metadata = CreateEventMetadata(); metadata.Add("currentCacheServer", currentCacheServer.ToString()); metadata.Add("localCacheKey", localCacheKey); metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Generated a new local key after cross referencing"); tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_NewKeyAfterCrossReferencing", metadata); } List> mappingFileUpdates = new List>(); mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(this.enlistment.RepoUrl), localCacheKey)); if (currentCacheServer.HasValidUrl()) { mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(currentCacheServer.Url), localCacheKey)); } foreach (CacheServerInfo cacheServer in serverGVFSConfig.CacheServers) { string persistedLocalCacheKey; if (mappingFile.TryGetValue(this.ToMappingKey(cacheServer.Url), out persistedLocalCacheKey)) { if (!string.Equals(persistedLocalCacheKey, localCacheKey, StringComparison.OrdinalIgnoreCase)) { EventMetadata metadata = CreateEventMetadata(); metadata.Add("cacheServer", cacheServer.ToString()); metadata.Add("persistedLocalCacheKey", persistedLocalCacheKey); metadata.Add("localCacheKey", localCacheKey); metadata.Add("currentCacheServer", currentCacheServer.ToString()); metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); tracer.RelatedWarning(metadata, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Overwriting persisted cache key with new value"); mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(cacheServer.Url), localCacheKey)); } } else { mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(cacheServer.Url), localCacheKey)); } } mappingFile.SetValuesAndFlush(mappingFileUpdates); } catch (Exception e) { EventMetadata metadata = CreateEventMetadata(e); tracer.RelatedError(metadata, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Caught exception while getting local key"); error = string.Format("Exception while getting local cache key: {0}", e.Message); return false; } return true; } private bool TryAcquireLockWithRetries(ITracer tracer, FileBasedLock mappingLock) { const int NumRetries = 100; const int WaitTimeMs = 100; for (int i = 0; i < NumRetries; ++i) { if (mappingLock.TryAcquireLock()) { return true; } else if (i < NumRetries - 1) { Thread.Sleep(WaitTimeMs); if (i % 20 == 0) { tracer.RelatedInfo("Waiting to acquire local cacke metadata lock file"); } } } return false; } private string ToMappingKey(string url) { return url.ToLowerInvariant(); } private bool TryFindExistingLocalCacheKey(FileBasedDictionary mappings, IEnumerable cacheServers, out string localCacheKey) { foreach (CacheServerInfo cacheServer in cacheServers) { if (mappings.TryGetValue(this.ToMappingKey(cacheServer.Url), out localCacheKey)) { return true; } } localCacheKey = null; return false; } } } ================================================ FILE: GVFS/GVFS.Common/LocalGVFSConfig.cs ================================================ using GVFS.Common.FileSystem; using System; using System.Collections.Generic; namespace GVFS.Common { public class LocalGVFSConfig { public const string FileName = "gvfs.config"; private readonly string configFile; private readonly PhysicalFileSystem fileSystem; private FileBasedDictionary allSettings; public LocalGVFSConfig() { this.configFile = GVFSPlatform.Instance.GVFSConfigPath; this.fileSystem = new PhysicalFileSystem(); } public virtual bool TryGetAllConfig(out Dictionary allConfig, out string error) { if (!this.fileSystem.FileExists(this.configFile)) { allConfig = new Dictionary(); error = null; return true; } Dictionary configCopy = null; if (!this.TryPerformAction( () => configCopy = this.allSettings.GetAllKeysAndValues(), out error)) { allConfig = null; return false; } allConfig = configCopy; error = null; return true; } public virtual bool TryGetConfig( string name, out string value, out string error) { if (!this.fileSystem.FileExists(this.configFile)) { value = null; error = null; return true; } string valueFromDict = null; if (!this.TryPerformAction( () => this.allSettings.TryGetValue(name, out valueFromDict), out error)) { value = null; error = $"Error reading config {name}. {error}"; return false; } value = valueFromDict; return true; } public virtual bool TrySetConfig( string name, string value, out string error) { if (!this.TryPerformAction( () => this.allSettings.SetValueAndFlush(name, value), out error)) { error = $"Error writing config {name}={value}. {error}"; return false; } return true; } public virtual bool TryRemoveConfig(string name, out string error) { if (!this.TryPerformAction( () => this.allSettings.RemoveAndFlush(name), out error)) { error = $"Error deleting config {name}. {error}"; return false; } return true; } private bool TryPerformAction(Action action, out string error) { if (!this.TryLoadSettings(out error)) { error = $"Error loading config settings. {error}"; return false; } try { action(); error = null; return true; } catch (FileBasedCollectionException exception) { error = exception.Message; } return false; } private bool TryLoadSettings(out string error) { if (this.allSettings == null) { FileBasedDictionary config = null; if (FileBasedDictionary.TryCreate( tracer: null, dictionaryPath: this.configFile, fileSystem: this.fileSystem, output: out config, error: out error, keyComparer: StringComparer.OrdinalIgnoreCase)) { this.allSettings = config; return true; } return false; } error = null; return true; } } } ================================================ FILE: GVFS/GVFS.Common/Maintenance/GitMaintenanceQueue.cs ================================================ using GVFS.Common.Tracing; using System; using System.Collections.Concurrent; using System.IO; using System.Threading; namespace GVFS.Common.Maintenance { public class GitMaintenanceQueue { private readonly object queueLock = new object(); private GVFSContext context; private BlockingCollection queue = new BlockingCollection(); private GitMaintenanceStep currentStep; public GitMaintenanceQueue(GVFSContext context) { this.context = context; Thread worker = new Thread(() => this.RunQueue()); worker.Name = "MaintenanceWorker"; worker.IsBackground = true; worker.Start(); } public bool TryEnqueue(GitMaintenanceStep step) { try { lock (this.queueLock) { if (this.queue == null) { return false; } this.queue.Add(step); return true; } } catch (InvalidOperationException) { // We called queue.CompleteAdding() } return false; } public void Stop() { lock (this.queueLock) { this.queue?.CompleteAdding(); } this.currentStep?.Stop(); } /// /// This method is public for test purposes only. /// public bool EnlistmentRootReady() { return GitMaintenanceStep.EnlistmentRootReady(this.context); } private void RunQueue() { while (true) { // We cannot take the lock here, as TryTake is blocking. // However, this is the place to set 'this.queue' to null. if (!this.queue.TryTake(out this.currentStep, Timeout.Infinite) || this.queue.IsAddingCompleted) { lock (this.queueLock) { // A stop was requested this.queue?.Dispose(); this.queue = null; return; } } if (this.EnlistmentRootReady()) { try { this.currentStep.Execute(); } catch (Exception e) { this.LogErrorAndExit( area: nameof(GitMaintenanceQueue), methodName: nameof(this.RunQueue), exception: e); } } } } private void LogError(string area, string methodName, Exception exception) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", area); metadata.Add("Method", methodName); metadata.Add("ExceptionMessage", exception.Message); metadata.Add("StackTrace", exception.StackTrace); this.context.Tracer.RelatedError( metadata: metadata, message: area + ": Unexpected Exception while running maintenance steps (fatal): " + exception.Message, keywords: Keywords.Telemetry); } private void LogErrorAndExit(string area, string methodName, Exception exception) { this.LogError(area, methodName, exception); Environment.Exit((int)ReturnCode.GenericError); } } } ================================================ FILE: GVFS/GVFS.Common/Maintenance/GitMaintenanceScheduler.cs ================================================ using GVFS.Common.Git; using System; using System.Collections.Generic; using System.Threading; namespace GVFS.Common.Maintenance { public class GitMaintenanceScheduler : IDisposable { private readonly TimeSpan looseObjectsDueTime = TimeSpan.FromMinutes(5); private readonly TimeSpan looseObjectsPeriod = TimeSpan.FromHours(6); private readonly TimeSpan packfileDueTime = TimeSpan.FromMinutes(30); private readonly TimeSpan packfilePeriod = TimeSpan.FromHours(12); private readonly TimeSpan prefetchPeriod = TimeSpan.FromMinutes(15); private List stepTimers; private GVFSContext context; private GitObjects gitObjects; private GitMaintenanceQueue queue; public GitMaintenanceScheduler(GVFSContext context, GitObjects gitObjects) { this.context = context; this.gitObjects = gitObjects; this.stepTimers = new List(); this.queue = new GitMaintenanceQueue(context); this.ScheduleRecurringSteps(); } public void EnqueueOneTimeStep(GitMaintenanceStep step) { this.queue.TryEnqueue(step); } public void Dispose() { this.queue.Stop(); foreach (Timer timer in this.stepTimers) { timer?.Dispose(); } this.stepTimers = null; } private void ScheduleRecurringSteps() { if (this.context.Unattended) { return; } if (this.gitObjects.IsUsingCacheServer()) { TimeSpan prefetchPeriod = TimeSpan.FromMinutes(15); this.stepTimers.Add(new Timer( (state) => this.queue.TryEnqueue(new PrefetchStep(this.context, this.gitObjects)), state: null, dueTime: this.prefetchPeriod, period: this.prefetchPeriod)); } this.stepTimers.Add(new Timer( (state) => this.queue.TryEnqueue(new LooseObjectsStep(this.context)), state: null, dueTime: this.looseObjectsDueTime, period: this.looseObjectsPeriod)); this.stepTimers.Add(new Timer( (state) => this.queue.TryEnqueue(new PackfileMaintenanceStep(this.context)), state: null, dueTime: this.packfileDueTime, period: this.packfilePeriod)); } } } ================================================ FILE: GVFS/GVFS.Common/Maintenance/GitMaintenanceStep.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; namespace GVFS.Common.Maintenance { public abstract class GitMaintenanceStep { public const string ObjectCacheLock = "git-maintenance-step.lock"; private readonly object gitProcessLock = new object(); public GitMaintenanceStep(GVFSContext context, bool requireObjectCacheLock, GitProcessChecker gitProcessChecker = null) { this.Context = context; this.RequireObjectCacheLock = requireObjectCacheLock; this.GitProcessChecker = gitProcessChecker ?? new GitProcessChecker(); } public abstract string Area { get; } protected virtual TimeSpan TimeBetweenRuns { get; } protected virtual string LastRunTimeFilePath { get; set; } protected GVFSContext Context { get; } protected GitProcess MaintenanceGitProcess { get; private set; } protected bool Stopping { get; private set; } protected bool RequireObjectCacheLock { get; } protected GitProcessChecker GitProcessChecker { get; } public static bool EnlistmentRootReady(GVFSContext context) { // If a user locks their drive or disconnects an external drive while the mount process // is running, then it will appear as if the directories below do not exist or throw // a "Device is not ready" error. try { return context.FileSystem.DirectoryExists(context.Enlistment.EnlistmentRoot) && context.FileSystem.DirectoryExists(context.Enlistment.GitObjectsRoot); } catch (IOException) { return false; } } public bool EnlistmentRootReady() { return EnlistmentRootReady(this.Context); } public void Execute() { try { if (this.RequireObjectCacheLock) { using (FileBasedLock cacheLock = GVFSPlatform.Instance.CreateFileBasedLock( this.Context.FileSystem, this.Context.Tracer, Path.Combine(this.Context.Enlistment.GitObjectsRoot, ObjectCacheLock))) { if (!cacheLock.TryAcquireLock()) { this.Context.Tracer.RelatedInfo(this.Area + ": Skipping work since another process holds the lock"); return; } this.CreateProcessAndRun(); } } else { this.CreateProcessAndRun(); } } catch (IOException e) { this.Context.Tracer.RelatedWarning( metadata: this.CreateEventMetadata(e), message: "IOException while running action: " + e.Message, keywords: Keywords.Telemetry); } catch (Exception e) { if (this.EnlistmentRootReady()) { this.Context.Tracer.RelatedError( metadata: this.CreateEventMetadata(e), message: "Exception while running action: " + e.Message, keywords: Keywords.Telemetry); } else { this.Context.Tracer.RelatedWarning( metadata: this.CreateEventMetadata(e), message: "Exception while running action inside a repo that's not ready: " + e.Message); } Environment.Exit((int)ReturnCode.GenericError); } } public void Stop() { lock (this.gitProcessLock) { this.Stopping = true; GitProcess process = this.MaintenanceGitProcess; if (process != null) { if (process.TryKillRunningProcess(out string processName, out int exitCode, out string error)) { this.Context.Tracer.RelatedEvent( EventLevel.Informational, string.Format( "{0}: killed background process {1} during {2}", this.Area, processName, nameof(this.Stop)), metadata: null); } else { this.Context.Tracer.RelatedEvent( EventLevel.Informational, string.Format( "{0}: failed to kill background process {1} during {2}. ExitCode:{3} Error:{4}", this.Area, processName, nameof(this.Stop), exitCode, error), metadata: null); } } } } // public only for unit tests public void GetPackFilesInfo(out int count, out long size, out bool hasKeep) { count = 0; size = 0; hasKeep = false; foreach (DirectoryItemInfo info in this.Context.FileSystem.ItemsInDirectory(this.Context.Enlistment.GitPackRoot)) { string extension = Path.GetExtension(info.Name); if (string.Equals(extension, ".pack", GVFSPlatform.Instance.Constants.PathComparison)) { count++; size += info.Length; } else if (string.Equals(extension, ".keep", GVFSPlatform.Instance.Constants.PathComparison)) { hasKeep = true; } } } /// /// Implement this method perform the mainteance actions. If the object-cache lock is required /// (as specified by ), then this step is not run unless we /// hold the lock. /// protected abstract void PerformMaintenance(); protected GitProcess.Result RunGitCommand(Func work, string gitCommand) { EventMetadata metadata = this.CreateEventMetadata(); metadata.Add("gitCommand", gitCommand); using (ITracer activity = this.Context.Tracer.StartActivity("RunGitCommand", EventLevel.Informational, metadata)) { if (this.Stopping) { this.Context.Tracer.RelatedWarning( metadata: null, message: $"{this.Area}: Not launching Git process {gitCommand} because the mount is stopping", keywords: Keywords.Telemetry); throw new StoppingException(); } GitProcess.Result result = work.Invoke(this.MaintenanceGitProcess); if (this.Stopping) { throw new StoppingException(); } if (result?.ExitCodeIsFailure == true) { string errorMessage = result?.Errors == null ? string.Empty : result.Errors; if (errorMessage.Length > 1000) { // For large error messages, we show the first and last 500 chars errorMessage = $"beginning: {errorMessage.Substring(0, 500)} ending: {errorMessage.Substring(errorMessage.Length - 500)}"; } this.Context.Tracer.RelatedWarning( metadata: null, message: $"{this.Area}: Git process {gitCommand} failed with errors: {errorMessage}", keywords: Keywords.Telemetry); return result; } return result; } } protected EventMetadata CreateEventMetadata(Exception e = null) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", this.Area); if (e != null) { metadata.Add("Exception", e.ToString()); } return metadata; } protected bool EnoughTimeBetweenRuns() { if (!this.Context.FileSystem.FileExists(this.LastRunTimeFilePath)) { return true; } string lastRunTime = this.Context.FileSystem.ReadAllText(this.LastRunTimeFilePath); if (!long.TryParse(lastRunTime, out long result)) { this.Context.Tracer.RelatedError("Failed to parse long: {0}", lastRunTime); return true; } if (DateTime.UtcNow.Subtract(EpochConverter.FromUnixEpochSeconds(result)) >= this.TimeBetweenRuns) { return true; } return false; } protected void SaveLastRunTimeToFile() { if (!this.Context.FileSystem.TryWriteTempFileAndRename( this.LastRunTimeFilePath, EpochConverter.ToUnixEpochSeconds(DateTime.UtcNow).ToString(), out Exception handledException)) { this.Context.Tracer.RelatedError(this.CreateEventMetadata(handledException), "Failed to record run time"); } } protected void LogErrorAndRewriteMultiPackIndex(ITracer activity) { EventMetadata errorMetadata = this.CreateEventMetadata(); string multiPackIndexPath = Path.Combine(this.Context.Enlistment.GitPackRoot, "multi-pack-index"); errorMetadata["TryDeleteFileResult"] = this.Context.FileSystem.TryDeleteFile(multiPackIndexPath); GitProcess.Result rewriteResult = this.RunGitCommand((process) => process.WriteMultiPackIndex(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.WriteMultiPackIndex)); errorMetadata["RewriteResultExitCode"] = rewriteResult.ExitCode; activity.RelatedWarning(errorMetadata, "multi-pack-index is corrupt after write. Deleting and rewriting.", Keywords.Telemetry); } protected void LogErrorAndRewriteCommitGraph(ITracer activity, List packs) { EventMetadata errorMetadata = this.CreateEventMetadata(); string commitGraphPath = Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", "commit-graph"); errorMetadata["TryDeleteFileResult"] = this.Context.FileSystem.TryDeleteFile(commitGraphPath); string commitGraphsPath = Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", "commit-graphs"); errorMetadata["TryDeleteDirectoryResult"] = this.Context.FileSystem.TryDeleteDirectory(commitGraphsPath, out Exception dirException); if (dirException != null) { errorMetadata["TryDeleteDirectoryError"] = dirException.Message; } GitProcess.Result rewriteResult = this.RunGitCommand((process) => process.WriteCommitGraph(this.Context.Enlistment.GitObjectsRoot, packs), nameof(GitProcess.WriteCommitGraph)); errorMetadata["RewriteResultExitCode"] = rewriteResult.ExitCode; activity.RelatedWarning(errorMetadata, "commit-graph is corrupt after write. Deleting and rewriting.", Keywords.Telemetry); } private void CreateProcessAndRun() { lock (this.gitProcessLock) { if (this.Stopping) { return; } this.MaintenanceGitProcess = this.Context.Enlistment.CreateGitProcess(); this.MaintenanceGitProcess.LowerPriority = true; } try { this.PerformMaintenance(); } catch (StoppingException) { // Part of shutdown, skipped commands have already been logged } } protected class StoppingException : Exception { } } } ================================================ FILE: GVFS/GVFS.Common/Maintenance/GitProcessChecker.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace GVFS.Common.Maintenance { public class GitProcessChecker { public virtual IEnumerable GetRunningGitProcessIds() { Process[] allProcesses = Process.GetProcesses(); return allProcesses .Where(x => x.ProcessName.Equals("git", GVFSPlatform.Instance.Constants.PathComparison)) .Select(x => x.Id); } } } ================================================ FILE: GVFS/GVFS.Common/Maintenance/LooseObjectsStep.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace GVFS.Common.Maintenance { // Performs LooseObject Maintenace // 1. Removes loose objects that appear in packfiles // 2. Packs loose objects into a packfile public class LooseObjectsStep : GitMaintenanceStep { public const string LooseObjectsLastRunFileName = "loose-objects.time"; private readonly bool forceRun; public LooseObjectsStep( GVFSContext context, bool requireCacheLock = true, bool forceRun = false, GitProcessChecker gitProcessChecker = null) : base(context, requireCacheLock, gitProcessChecker) { this.forceRun = forceRun; } public enum CreatePackResult { Succeess, UnknownFailure, CorruptObject } public override string Area => nameof(LooseObjectsStep); // 50,000 was found to be the optimal time taking ~5 minutes public int MaxLooseObjectsInPack { get; set; } = 50000; protected override string LastRunTimeFilePath => Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", LooseObjectsLastRunFileName); protected override TimeSpan TimeBetweenRuns => TimeSpan.FromDays(1); public void CountLooseObjects(out int count, out long size) { count = 0; size = 0; foreach (string directoryPath in this.Context.FileSystem.EnumerateDirectories(this.Context.Enlistment.GitObjectsRoot)) { string directoryName = directoryPath.TrimEnd(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Last(); if (GitObjects.IsLooseObjectsDirectory(directoryName)) { List dirItems = this.Context.FileSystem.ItemsInDirectory(directoryPath).ToList(); count += dirItems.Count; size += dirItems.Sum(item => item.Length); } } } public IEnumerable GetBatchOfLooseObjects(int batchSize) { // Find loose Objects foreach (DirectoryItemInfo directoryItemInfo in this.Context.FileSystem.ItemsInDirectory(this.Context.Enlistment.GitObjectsRoot)) { if (directoryItemInfo.IsDirectory) { string directoryName = directoryItemInfo.Name; if (GitObjects.IsLooseObjectsDirectory(directoryName)) { string[] looseObjectFileNamesInDir = this.Context.FileSystem.GetFiles(directoryItemInfo.FullName, "*"); foreach (string filePath in looseObjectFileNamesInDir) { if (!this.TryGetLooseObjectId(directoryName, filePath, out string objectId)) { this.Context.Tracer.RelatedWarning($"Invalid ObjectId {objectId} using directory {directoryName} and path {filePath}"); continue; } batchSize--; yield return objectId; if (batchSize <= 0) { yield break; } } } } } } /// /// Writes loose object Ids to streamWriter /// /// Writer to which SHAs are written /// The number of loose objects SHAs written to the stream public int WriteLooseObjectIds(StreamWriter streamWriter) { int count = 0; foreach (string objectId in this.GetBatchOfLooseObjects(this.MaxLooseObjectsInPack)) { streamWriter.Write(objectId + "\n"); count++; } return count; } public bool TryGetLooseObjectId(string directoryName, string filePath, out string objectId) { objectId = directoryName + Path.GetFileName(filePath); if (!SHA1Util.IsValidShaFormat(objectId)) { return false; } return true; } /// /// Creates a pack file from loose objects /// /// The number of loose objects added to the pack file public CreatePackResult TryCreateLooseObjectsPackFile(out int objectsAddedToPack) { int localObjectCount = 0; GitProcess.Result result = this.RunGitCommand( (process) => process.PackObjects( "from-loose", this.Context.Enlistment.GitObjectsRoot, (StreamWriter writer) => localObjectCount = this.WriteLooseObjectIds(writer)), nameof(GitProcess.PackObjects)); if (result.ExitCodeIsSuccess) { objectsAddedToPack = localObjectCount; return CreatePackResult.Succeess; } else { objectsAddedToPack = 0; if (result.Errors.Contains("is corrupt")) { return CreatePackResult.CorruptObject; } return CreatePackResult.UnknownFailure; } } public string GetLooseObjectFileName(string objectId) { return Path.Combine( this.Context.Enlistment.GitObjectsRoot, objectId.Substring(0, 2), objectId.Substring(2, GVFSConstants.ShaStringLength - 2)); } public void ClearCorruptLooseObjects(EventMetadata metadata) { int numDeletedObjects = 0; int numFailedDeletes = 0; // Double the batch size to look beyond the current batch for bad objects, as there // may be more bad objects in the next batch after deleting the corrupt objects. foreach (string objectId in this.GetBatchOfLooseObjects(2 * this.MaxLooseObjectsInPack)) { if (!this.Context.Repository.ObjectExists(objectId)) { string objectFile = this.GetLooseObjectFileName(objectId); if (this.Context.FileSystem.TryDeleteFile(objectFile)) { numDeletedObjects++; } else { numFailedDeletes++; } } } metadata.Add("RemovedCorruptObjects", numDeletedObjects); metadata.Add("NumFailedDeletes", numFailedDeletes); } protected override void PerformMaintenance() { using (ITracer activity = this.Context.Tracer.StartActivity(this.Area, EventLevel.Informational, Keywords.Telemetry, metadata: null)) { try { // forceRun is only currently true for functional tests if (!this.forceRun) { if (!this.EnoughTimeBetweenRuns()) { activity.RelatedWarning($"Skipping {nameof(LooseObjectsStep)} due to not enough time between runs"); return; } IEnumerable processIds = this.GitProcessChecker.GetRunningGitProcessIds(); if (processIds.Any()) { activity.RelatedWarning($"Skipping {nameof(LooseObjectsStep)} due to git pids {string.Join(",", processIds)}", Keywords.Telemetry); return; } } this.CountLooseObjects(out int beforeLooseObjectsCount, out long beforeLooseObjectsSize); this.GetPackFilesInfo(out int beforePackCount, out long beforePackSize, out bool _); GitProcess.Result gitResult = this.RunGitCommand((process) => process.PrunePacked(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.PrunePacked)); CreatePackResult createPackResult = this.TryCreateLooseObjectsPackFile(out int objectsAddedToPack); this.CountLooseObjects(out int afterLooseObjectsCount, out long afterLooseObjectsSize); this.GetPackFilesInfo(out int afterPackCount, out long afterPackSize, out bool _); EventMetadata metadata = new EventMetadata(); metadata.Add("GitObjectsRoot", this.Context.Enlistment.GitObjectsRoot); metadata.Add("PrunedPackedExitCode", gitResult.ExitCode); metadata.Add("StartingCount", beforeLooseObjectsCount); metadata.Add("EndingCount", afterLooseObjectsCount); metadata.Add("StartingPackCount", beforePackCount); metadata.Add("EndingPackCount", afterPackCount); metadata.Add("StartingSize", beforeLooseObjectsSize); metadata.Add("EndingSize", afterLooseObjectsSize); metadata.Add("StartingPackSize", beforePackSize); metadata.Add("EndingPackSize", afterPackSize); metadata.Add("RemovedCount", beforeLooseObjectsCount - afterLooseObjectsCount); metadata.Add("LooseObjectsPutIntoPackFile", objectsAddedToPack); metadata.Add("CreatePackResult", createPackResult.ToString()); if (createPackResult == CreatePackResult.CorruptObject) { this.ClearCorruptLooseObjects(metadata); } activity.RelatedEvent(EventLevel.Informational, $"{this.Area}_{nameof(this.PerformMaintenance)}", metadata, Keywords.Telemetry); this.SaveLastRunTimeToFile(); } catch (Exception e) { activity.RelatedWarning(this.CreateEventMetadata(e), "Failed to run LooseObjectsStep", Keywords.Telemetry); } } } } } ================================================ FILE: GVFS/GVFS.Common/Maintenance/PackfileMaintenanceStep.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace GVFS.Common.Maintenance { /// /// This step maintains the packfiles in the object cache. /// /// This is done in two steps: /// /// git multi-pack-index expire: This deletes the pack-files whose objects /// appear in newer pack-files. The multi-pack-index prevents git from /// looking at these packs. Rewrites the multi-pack-index to no longer /// refer to these (deleted) packs. /// /// git multi-pack-index repack --batch-size= inspects packs covered by the /// multi-pack-index in modified-time order(ascending). Greedily selects a /// batch of packs whose file sizes are all less than "size", but that sum /// up to at least "size". Then generate a new pack-file containing the /// objects that are uniquely referenced by the multi-pack-index. /// public class PackfileMaintenanceStep : GitMaintenanceStep { public const string PackfileLastRunFileName = "pack-maintenance.time"; public const string DefaultBatchSize = "2g"; private const string MultiPackIndexLock = "multi-pack-index.lock"; private readonly bool forceRun; private readonly string batchSize; public PackfileMaintenanceStep( GVFSContext context, bool requireObjectCacheLock = true, bool forceRun = false, string batchSize = DefaultBatchSize, GitProcessChecker gitProcessChecker = null) : base(context, requireObjectCacheLock, gitProcessChecker) { this.forceRun = forceRun; this.batchSize = batchSize; } public override string Area => nameof(PackfileMaintenanceStep); protected override string LastRunTimeFilePath => Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", PackfileLastRunFileName); protected override TimeSpan TimeBetweenRuns => TimeSpan.FromDays(1); // public only for unit tests public List CleanStaleIdxFiles(out int numDeletionBlocked) { List packDirContents = this.Context .FileSystem .ItemsInDirectory(this.Context.Enlistment.GitPackRoot) .ToList(); numDeletionBlocked = 0; List deletedIdxFiles = new List(); // If something (probably VFS for Git) has a handle open to a ".idx" file, then // the 'git multi-pack-index expire' command cannot delete it. We should come in // later and try to clean these up. Count those that we are able to delete and // those we still can't. foreach (DirectoryItemInfo info in packDirContents) { if (string.Equals(Path.GetExtension(info.Name), ".idx", GVFSPlatform.Instance.Constants.PathComparison)) { string pairedPack = Path.ChangeExtension(info.FullName, ".pack"); if (!this.Context.FileSystem.FileExists(pairedPack)) { if (this.Context.FileSystem.TryDeleteFile(info.FullName)) { deletedIdxFiles.Add(info.Name); } else { numDeletionBlocked++; } } } } return deletedIdxFiles; } protected override void PerformMaintenance() { using (ITracer activity = this.Context.Tracer.StartActivity(this.Area, EventLevel.Informational, Keywords.Telemetry, metadata: null)) { // forceRun is only currently true for functional tests if (!this.forceRun) { if (!this.EnoughTimeBetweenRuns()) { activity.RelatedWarning($"Skipping {nameof(PackfileMaintenanceStep)} due to not enough time between runs"); return; } IEnumerable processIds = this.GitProcessChecker.GetRunningGitProcessIds(); if (processIds.Any()) { activity.RelatedWarning($"Skipping {nameof(PackfileMaintenanceStep)} due to git pids {string.Join(",", processIds)}", Keywords.Telemetry); return; } } this.GetPackFilesInfo(out int beforeCount, out long beforeSize, out bool hasKeep); if (!hasKeep) { activity.RelatedWarning(this.CreateEventMetadata(), "Skipping pack maintenance due to no .keep file."); return; } string multiPackIndexLockPath = Path.Combine(this.Context.Enlistment.GitPackRoot, MultiPackIndexLock); this.Context.FileSystem.TryDeleteFile(multiPackIndexLockPath); this.RunGitCommand((process) => process.WriteMultiPackIndex(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.WriteMultiPackIndex)); // If a LibGit2Repo is active, then it may hold handles to the .idx and .pack files we want // to delete during the 'git multi-pack-index expire' step. If one starts during the step, // then it can still block those deletions, but we will clean them up in the next run. By // running CloseActiveRepos() here, we ensure that we do not run twice with the same // LibGit2Repo active across two calls. A "new" repo should not hold handles to .idx files // that do not have corresponding .pack files, so we will clean them up in CleanStaleIdxFiles(). this.Context.Repository.CloseActiveRepo(); GitProcess.Result expireResult = this.RunGitCommand((process) => process.MultiPackIndexExpire(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.MultiPackIndexExpire)); this.Context.Repository.OpenRepo(); List staleIdxFiles = this.CleanStaleIdxFiles(out int numDeletionBlocked); this.GetPackFilesInfo(out int expireCount, out long expireSize, out hasKeep); GitProcess.Result verifyAfterExpire = this.RunGitCommand((process) => process.VerifyMultiPackIndex(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.VerifyMultiPackIndex)); if (!this.Stopping && verifyAfterExpire.ExitCodeIsFailure) { this.LogErrorAndRewriteMultiPackIndex(activity); } GitProcess.Result repackResult = this.RunGitCommand((process) => process.MultiPackIndexRepack(this.Context.Enlistment.GitObjectsRoot, this.batchSize), nameof(GitProcess.MultiPackIndexRepack)); this.GetPackFilesInfo(out int afterCount, out long afterSize, out hasKeep); GitProcess.Result verifyAfterRepack = this.RunGitCommand((process) => process.VerifyMultiPackIndex(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.VerifyMultiPackIndex)); if (!this.Stopping && verifyAfterRepack.ExitCodeIsFailure) { this.LogErrorAndRewriteMultiPackIndex(activity); } EventMetadata metadata = new EventMetadata(); metadata.Add("GitObjectsRoot", this.Context.Enlistment.GitObjectsRoot); metadata.Add("BatchSize", this.batchSize); metadata.Add(nameof(beforeCount), beforeCount); metadata.Add(nameof(beforeSize), beforeSize); metadata.Add(nameof(expireCount), expireCount); metadata.Add(nameof(expireSize), expireSize); metadata.Add(nameof(afterCount), afterCount); metadata.Add(nameof(afterSize), afterSize); metadata.Add("VerifyAfterExpireExitCode", verifyAfterExpire.ExitCode); metadata.Add("VerifyAfterRepackExitCode", verifyAfterRepack.ExitCode); metadata.Add("NumStaleIdxFiles", staleIdxFiles.Count); metadata.Add("NumIdxDeletionsBlocked", numDeletionBlocked); activity.RelatedEvent(EventLevel.Informational, $"{this.Area}_{nameof(this.PerformMaintenance)}", metadata, Keywords.Telemetry); this.SaveLastRunTimeToFile(); } } } } ================================================ FILE: GVFS/GVFS.Common/Maintenance/PostFetchStep.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Tracing; using System.Collections.Generic; using System.IO; using System.Text; namespace GVFS.Common.Maintenance { public class PostFetchStep : GitMaintenanceStep { private const string CommitGraphChainLock = "commit-graph-chain.lock"; private List packIndexes; public PostFetchStep(GVFSContext context, List packIndexes, bool requireObjectCacheLock = true) : base(context, requireObjectCacheLock) { this.packIndexes = packIndexes; } public override string Area => "PostFetchMaintenanceStep"; protected override void PerformMaintenance() { if (this.packIndexes == null || this.packIndexes.Count == 0) { this.Context.Tracer.RelatedInfo(this.Area + ": Skipping commit-graph write due to no new packfiles"); return; } using (ITracer activity = this.Context.Tracer.StartActivity("TryWriteGitCommitGraph", EventLevel.Informational)) { string commitGraphLockPath = Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", "commit-graphs", CommitGraphChainLock); this.Context.FileSystem.TryDeleteFile(commitGraphLockPath); GitProcess.Result writeResult = this.RunGitCommand((process) => process.WriteCommitGraph(this.Context.Enlistment.GitObjectsRoot, this.packIndexes), nameof(GitProcess.WriteCommitGraph)); StringBuilder sb = new StringBuilder(); string commitGraphsDir = Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", "commit-graphs"); if (this.Context.FileSystem.DirectoryExists(commitGraphsDir)) { foreach (DirectoryItemInfo info in this.Context.FileSystem.ItemsInDirectory(commitGraphsDir)) { sb.Append(info.Name); sb.Append(";"); } } activity.RelatedInfo($"commit-graph list after write: {sb}"); if (writeResult.ExitCodeIsFailure) { this.LogErrorAndRewriteCommitGraph(activity, this.packIndexes); } GitProcess.Result verifyResult = this.RunGitCommand((process) => process.VerifyCommitGraph(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.VerifyCommitGraph)); // Currently, Git does not fail when looking for the commit-graphs in the chain of // incremental files. This is by design, as there is a race condition otherwise. // However, 'git commit-graph verify' should change this behavior to fail if we // cannot find all commit-graph files. Until that change happens in Git, look for // the error message to get out of this state. if (!this.Stopping && (verifyResult.ExitCodeIsFailure || verifyResult.Errors.Contains("unable to find all commit-graph files"))) { this.LogErrorAndRewriteCommitGraph(activity, this.packIndexes); } } } } } ================================================ FILE: GVFS/GVFS.Common/Maintenance/PrefetchStep.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; namespace GVFS.Common.Maintenance { public class PrefetchStep : GitMaintenanceStep { private const int IoFailureRetryDelayMS = 50; private const int LockWaitTimeMs = 100; private const int WaitingOnLockLogThreshold = 50; private const string PrefetchCommitsAndTreesLock = "prefetch-commits-trees.lock"; private const int NoExistingPrefetchPacks = -1; private readonly TimeSpan timeBetweenPrefetches = TimeSpan.FromMinutes(70); public PrefetchStep(GVFSContext context, GitObjects gitObjects, bool requireCacheLock = true) : base(context, requireCacheLock) { this.GitObjects = gitObjects; } public override string Area => "PrefetchStep"; protected GitObjects GitObjects { get; } public bool TryPrefetchCommitsAndTrees(out string error, GitProcess gitProcess = null) { if (gitProcess == null) { gitProcess = new GitProcess(this.Context.Enlistment); } List packIndexes; // We take our own lock here to keep background and foreground prefetches // from running at the same time. using (FileBasedLock prefetchLock = GVFSPlatform.Instance.CreateFileBasedLock( this.Context.FileSystem, this.Context.Tracer, Path.Combine(this.Context.Enlistment.GitPackRoot, PrefetchCommitsAndTreesLock))) { WaitUntilLockIsAcquired(this.Context.Tracer, prefetchLock); long maxGoodTimeStamp; this.GitObjects.DeleteStaleTempPrefetchPackAndIdxs(); this.GitObjects.DeleteTemporaryFiles(); if (!this.TryGetMaxGoodPrefetchTimestamp(out maxGoodTimeStamp, out error)) { return false; } var trustPackIndexes = this.Context.Repository.LibGit2RepoInvoker.GetConfigBoolOrDefault(GVFSConstants.GitConfig.TrustPackIndexes, GVFSConstants.GitConfig.TrustPackIndexesDefault); if (!this.GitObjects.TryDownloadPrefetchPacks(gitProcess, maxGoodTimeStamp, trustPackIndexes, out packIndexes)) { error = "Failed to download prefetch packs"; return false; } this.UpdateKeepPacks(); } this.SchedulePostFetchJob(packIndexes); return true; } protected override void PerformMaintenance() { long last; string error = null; if (!this.TryGetMaxGoodPrefetchTimestamp(out last, out error)) { this.Context.Tracer.RelatedError(error); return; } if (last == NoExistingPrefetchPacks) { /* If there are no existing prefetch packs, that means that either the * first prefetch is still in progress or the clone was run with "--no-prefetch". * In either case, we should not run prefetch as a maintenance task. * If users want to prefetch after cloning with "--no-prefetch", they can run * "gvfs prefetch" manually. Also, "git pull" and "git fetch" will run prefetch * as a pre-command hook. */ this.Context.Tracer.RelatedInfo(this.Area + ": Skipping prefetch since there are no existing prefetch packs"); return; } DateTime lastDateTime = EpochConverter.FromUnixEpochSeconds(last); DateTime now = DateTime.UtcNow; if (now <= lastDateTime + this.timeBetweenPrefetches) { this.Context.Tracer.RelatedInfo(this.Area + ": Skipping prefetch since most-recent prefetch ({0}) is too close to now ({1})", lastDateTime, now); return; } this.RunGitCommand( process => { this.TryPrefetchCommitsAndTrees(out error, process); return null; }, nameof(this.TryPrefetchCommitsAndTrees)); if (!string.IsNullOrEmpty(error)) { this.Context.Tracer.RelatedWarning( metadata: this.CreateEventMetadata(), message: $"{nameof(this.TryPrefetchCommitsAndTrees)} failed with error '{error}'", keywords: Keywords.Telemetry); } } private static long? GetTimestamp(string packName) { string filename = Path.GetFileName(packName); if (!filename.StartsWith(GVFSConstants.PrefetchPackPrefix)) { return null; } string[] parts = filename.Split('-'); long parsed; if (parts.Length > 1 && long.TryParse(parts[1], out parsed)) { return parsed; } return null; } private static void WaitUntilLockIsAcquired(ITracer tracer, FileBasedLock fileBasedLock) { int attempt = 0; while (!fileBasedLock.TryAcquireLock()) { Thread.Sleep(LockWaitTimeMs); ++attempt; if (attempt == WaitingOnLockLogThreshold) { attempt = 0; tracer.RelatedInfo("WaitUntilLockIsAcquired: Waiting to acquire prefetch lock"); } } } private bool TryGetMaxGoodPrefetchTimestamp(out long maxGoodTimestamp, out string error) { this.Context.FileSystem.CreateDirectory(this.Context.Enlistment.GitPackRoot); string[] packs = this.GitObjects.ReadPackFileNames(this.Context.Enlistment.GitPackRoot, GVFSConstants.PrefetchPackPrefix); List orderedPacks = packs .Where(pack => GetTimestamp(pack).HasValue && !this.Context.FileSystem.FileExists( Path.ChangeExtension(pack, GVFSConstants.InProgressPrefetchMarkerExtension))) .Select(pack => new PrefetchPackInfo(GetTimestamp(pack).Value, pack)) .OrderBy(packInfo => packInfo.Timestamp) .ToList(); maxGoodTimestamp = NoExistingPrefetchPacks; int firstBadPack = -1; for (int i = 0; i < orderedPacks.Count; ++i) { long timestamp = orderedPacks[i].Timestamp; string packPath = orderedPacks[i].Path; string idxPath = Path.ChangeExtension(packPath, ".idx"); if (!this.Context.FileSystem.FileExists(idxPath)) { EventMetadata metadata = this.CreateEventMetadata(); metadata.Add("pack", packPath); metadata.Add("idxPath", idxPath); metadata.Add("timestamp", timestamp); GitProcess.Result indexResult = this.RunGitCommand(process => this.GitObjects.IndexPackFile(packPath, process), nameof(this.GitObjects.IndexPackFile)); if (indexResult.ExitCodeIsFailure) { firstBadPack = i; this.Context.Tracer.RelatedWarning(metadata, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}: Found pack file that's missing idx file, and failed to regenerate idx"); break; } else { maxGoodTimestamp = timestamp; metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}: Found pack file that's missing idx file, and regenerated idx"); this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_RebuildIdx", metadata); } } else { maxGoodTimestamp = timestamp; } } if (this.Stopping) { throw new StoppingException(); } if (firstBadPack != -1) { const int MaxDeleteRetries = 200; // 200 * IoFailureRetryDelayMS (50ms) = 10 seconds const int RetryLoggingThreshold = 40; // 40 * IoFailureRetryDelayMS (50ms) = 2 seconds // Before we delete _any_ pack-files, we need to delete the multi-pack-index, which // may refer to those packs. EventMetadata metadata = this.CreateEventMetadata(); string midxPath = Path.Combine(this.Context.Enlistment.GitPackRoot, "multi-pack-index"); metadata.Add("path", midxPath); metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)} deleting multi-pack-index"); this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_DeleteMultiPack_index", metadata); if (!this.Context.FileSystem.TryWaitForDelete(this.Context.Tracer, midxPath, IoFailureRetryDelayMS, MaxDeleteRetries, RetryLoggingThreshold)) { error = $"Unable to delete {midxPath}"; return false; } // Delete packs and indexes in reverse order so that if prefetch is killed, subseqeuent prefetch commands will // find the right starting spot. for (int i = orderedPacks.Count - 1; i >= firstBadPack; --i) { if (this.Stopping) { throw new StoppingException(); } string packPath = orderedPacks[i].Path; string idxPath = Path.ChangeExtension(packPath, ".idx"); metadata = this.CreateEventMetadata(); metadata.Add("path", idxPath); metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)} deleting bad idx file"); this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_DeleteBadIdx", metadata); // We need to close the LibGit2 repo data in order to delete .idx files. // Close inside the loop to only close if necessary, reopen outside the loop // to minimize initializations. this.Context.Repository.CloseActiveRepo(); if (!this.Context.FileSystem.TryWaitForDelete(this.Context.Tracer, idxPath, IoFailureRetryDelayMS, MaxDeleteRetries, RetryLoggingThreshold)) { error = $"Unable to delete {idxPath}"; return false; } metadata = this.CreateEventMetadata(); metadata.Add("path", packPath); metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)} deleting bad pack file"); this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_DeleteBadPack", metadata); if (!this.Context.FileSystem.TryWaitForDelete(this.Context.Tracer, packPath, IoFailureRetryDelayMS, MaxDeleteRetries, RetryLoggingThreshold)) { error = $"Unable to delete {packPath}"; return false; } } this.Context.Repository.OpenRepo(); } error = null; return true; } private void SchedulePostFetchJob(List packIndexes) { if (packIndexes.Count == 0) { return; } // We make a best-effort request to run MIDX and commit-graph writes using (NamedPipeClient pipeClient = new NamedPipeClient(this.Context.Enlistment.NamedPipeName)) { if (!pipeClient.Connect()) { this.Context.Tracer.RelatedWarning( metadata: this.CreateEventMetadata(), message: "Failed to connect to GVFS.Mount process. Skipping post-fetch job request.", keywords: Keywords.Telemetry); return; } NamedPipeMessages.RunPostFetchJob.Request request = new NamedPipeMessages.RunPostFetchJob.Request(packIndexes); if (pipeClient.TrySendRequest(request.CreateMessage())) { NamedPipeMessages.Message response; if (pipeClient.TryReadResponse(out response)) { this.Context.Tracer.RelatedInfo("Requested post-fetch job with resonse '{0}'", response.Header); } else { this.Context.Tracer.RelatedWarning( metadata: this.CreateEventMetadata(), message: "Requested post-fetch job failed to respond", keywords: Keywords.Telemetry); } } else { this.Context.Tracer.RelatedWarning( metadata: this.CreateEventMetadata(), message: "Message to named pipe failed to send, skipping post-fetch job request.", keywords: Keywords.Telemetry); } } } /// /// Ensure the prefetch pack with most-recent timestamp has an associated /// ".keep" file. This prevents any Git command from deleting the pack. /// /// Delete the previous ".keep" file(s) so that pack can be deleted when they /// are not the most-recent pack. /// private void UpdateKeepPacks() { if (!this.TryGetMaxGoodPrefetchTimestamp(out long maxGoodTimeStamp, out string error)) { return; } string prefix = $"prefetch-{maxGoodTimeStamp}-"; DirectoryItemInfo info = this.Context .FileSystem .ItemsInDirectory(this.Context.Enlistment.GitPackRoot) .Where(item => item.Name.StartsWith(prefix) && string.Equals(Path.GetExtension(item.Name), ".pack", GVFSPlatform.Instance.Constants.PathComparison)) .FirstOrDefault(); if (info == null) { this.Context.Tracer.RelatedWarning(this.CreateEventMetadata(), $"Could not find latest prefetch pack, starting with {prefix}"); return; } string newKeepFile = Path.ChangeExtension(info.FullName, ".keep"); if (!this.Context.FileSystem.TryWriteAllText(newKeepFile, string.Empty)) { this.Context.Tracer.RelatedWarning(this.CreateEventMetadata(), $"Failed to create .keep file at {newKeepFile}"); return; } foreach (string keepFile in this.Context .FileSystem .ItemsInDirectory(this.Context.Enlistment.GitPackRoot) .Where(item => item.Name.EndsWith(".keep")) .Select(item => item.FullName)) { if (!keepFile.Equals(newKeepFile)) { this.Context.FileSystem.TryDeleteFile(keepFile); } } } private class PrefetchPackInfo { public PrefetchPackInfo(long timestamp, string path) { this.Timestamp = timestamp; this.Path = path; } public long Timestamp { get; } public string Path { get; } } } } ================================================ FILE: GVFS/GVFS.Common/MissingTreeTracker.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using GVFS.Common.Tracing; namespace GVFS.Common { /// /// Tracks missing trees per commit to support batching tree downloads. /// Maintains LRU eviction based on commits (not individual trees). /// A single tree SHA may be shared across multiple commits. /// public class MissingTreeTracker { private const string EtwArea = nameof(MissingTreeTracker); private readonly int treeCapacity; private readonly ITracer tracer; private readonly object syncLock = new object(); // Primary storage: commit -> set of missing trees private readonly Dictionary> missingTreesByCommit; // Reverse lookup: tree -> set of commits (for fast lookups) private readonly Dictionary> commitsByTree; // LRU ordering based on commits private readonly LinkedList commitOrder; private readonly Dictionary> commitNodes; public MissingTreeTracker(ITracer tracer, int treeCapacity) { this.tracer = tracer; this.treeCapacity = treeCapacity; this.missingTreesByCommit = new Dictionary>(StringComparer.OrdinalIgnoreCase); this.commitsByTree = new Dictionary>(StringComparer.OrdinalIgnoreCase); this.commitOrder = new LinkedList(); this.commitNodes = new Dictionary>(StringComparer.OrdinalIgnoreCase); } /// /// Records a missing root tree for a commit. Marks the commit as recently used. /// A tree may be associated with multiple commits. /// public void AddMissingRootTree(string treeSha, string commitSha) { lock (this.syncLock) { this.EnsureCommitTracked(commitSha); this.AddTreeToCommit(treeSha, commitSha); } } /// /// Records missing sub-trees discovered while processing a parent tree. /// Each sub-tree is associated with all commits currently tracking the parent tree. /// public void AddMissingSubTrees(string parentTreeSha, string[] subTreeShas) { lock (this.syncLock) { if (!this.commitsByTree.TryGetValue(parentTreeSha, out var commits)) { return; } // Snapshot the set because AddTreeToCommit may modify commitsByTree indirectly string[] commitSnapshot = commits.ToArray(); foreach (string subTreeSha in subTreeShas) { foreach (string commitSha in commitSnapshot) { /* Ensure it wasn't evicted earlier in the loop. */ if (!this.missingTreesByCommit.ContainsKey(commitSha)) { continue; } /* Ensure we don't evict this commit while trying to add a tree to it. */ this.MarkCommitAsUsed(commitSha); this.AddTreeToCommit(subTreeSha, commitSha); } } } } /// /// Tries to get all commits associated with a tree SHA. /// Marks all found commits as recently used. /// public bool TryGetCommits(string treeSha, out string[] commitShas) { lock (this.syncLock) { if (this.commitsByTree.TryGetValue(treeSha, out var commits)) { commitShas = commits.ToArray(); foreach (string commitSha in commitShas) { this.MarkCommitAsUsed(commitSha); } return true; } commitShas = null; return false; } } /// /// Given a set of commits, finds the one with the most missing trees. /// public int GetHighestMissingTreeCount(string[] commitShas, out string highestCountCommitSha) { lock (this.syncLock) { highestCountCommitSha = null; int highestCount = 0; foreach (string commitSha in commitShas) { if (this.missingTreesByCommit.TryGetValue(commitSha, out var trees) && trees.Count > highestCount) { highestCount = trees.Count; highestCountCommitSha = commitSha; } } return highestCount; } } /// /// Marks a commit as complete (e.g. its pack was downloaded successfully). /// Because the trees are now available, they are also removed from tracking /// for any other commits that shared them, and those commits are cleaned up /// if they become empty. /// public void MarkCommitComplete(string commitSha) { lock (this.syncLock) { this.RemoveCommitWithCascade(commitSha); EventMetadata metadata = new EventMetadata(); metadata.Add("Area", EtwArea); metadata.Add("CompletedCommit", commitSha); metadata.Add("RemainingCommits", this.commitNodes.Count); metadata.Add("RemainingTrees", this.commitsByTree.Count); this.tracer.RelatedEvent(EventLevel.Informational, nameof(this.MarkCommitComplete), metadata, Keywords.Telemetry); } } private void EnsureCommitTracked(string commitSha) { if (!this.missingTreesByCommit.TryGetValue(commitSha, out _)) { this.missingTreesByCommit[commitSha] = new HashSet(StringComparer.OrdinalIgnoreCase); var node = this.commitOrder.AddFirst(commitSha); this.commitNodes[commitSha] = node; } else { this.MarkCommitAsUsed(commitSha); } } private void AddTreeToCommit(string treeSha, string commitSha) { if (!this.commitsByTree.ContainsKey(treeSha)) { // Evict LRU commits until there is room for the new tree while (this.commitsByTree.Count >= this.treeCapacity) { // If evict fails it means we only have one commit left. if (!this.EvictLruCommit()) { break; } } this.commitsByTree[treeSha] = new HashSet(StringComparer.OrdinalIgnoreCase); } this.missingTreesByCommit[commitSha].Add(treeSha); this.commitsByTree[treeSha].Add(commitSha); } private void MarkCommitAsUsed(string commitSha) { if (this.commitNodes.TryGetValue(commitSha, out var node)) { this.commitOrder.Remove(node); var newNode = this.commitOrder.AddFirst(commitSha); this.commitNodes[commitSha] = newNode; } } private bool EvictLruCommit() { var last = this.commitOrder.Last; if (last != null && last.Value != this.commitOrder.First.Value) { string lruCommit = last.Value; var treeCountBefore = this.commitsByTree.Count; this.RemoveCommitNoCache(lruCommit); EventMetadata metadata = new EventMetadata(); metadata.Add("Area", EtwArea); metadata.Add("EvictedCommit", lruCommit); metadata.Add("TreeCountBefore", treeCountBefore); metadata.Add("TreeCountAfter", this.commitsByTree.Count); this.tracer.RelatedEvent(EventLevel.Informational, nameof(this.EvictLruCommit), metadata, Keywords.Telemetry); return true; } EventMetadata warnMetadata = new EventMetadata(); warnMetadata.Add("Area", EtwArea); warnMetadata.Add("TreeCount", this.commitsByTree.Count); warnMetadata.Add("CommitCount", this.commitNodes.Count); this.tracer.RelatedEvent(EventLevel.Warning, $"{nameof(this.EvictLruCommit)}CouldNotEvict", warnMetadata, Keywords.Telemetry); return false; } /// /// Removes a commit without cascading tree removal to other commits. /// Used during LRU eviction: the trees are still missing, so other commits /// that share those trees should continue to track them. /// private void RemoveCommitNoCache(string commitSha) { if (!this.missingTreesByCommit.TryGetValue(commitSha, out var trees)) { return; } foreach (string treeSha in trees) { if (this.commitsByTree.TryGetValue(treeSha, out var commits)) { commits.Remove(commitSha); if (commits.Count == 0) { this.commitsByTree.Remove(treeSha); } } } this.missingTreesByCommit.Remove(commitSha); this.RemoveFromLruOrder(commitSha); } /// /// Removes a commit and cascades: trees that were in this commit's set are /// also removed from all other commits that shared them. Any commit that /// becomes empty as a result is also removed (without further cascade). /// private void RemoveCommitWithCascade(string commitSha) { if (!this.missingTreesByCommit.TryGetValue(commitSha, out var trees)) { return; } // Collect commits that may become empty after we remove the shared trees. // We don't cascade further than one level. var commitsToCheck = new HashSet(); foreach (string treeSha in trees) { if (this.commitsByTree.TryGetValue(treeSha, out var sharingCommits)) { sharingCommits.Remove(commitSha); foreach (string otherCommit in sharingCommits) { if (this.missingTreesByCommit.TryGetValue(otherCommit, out var otherTrees)) { otherTrees.Remove(treeSha); if (otherTrees.Count == 0) { commitsToCheck.Add(otherCommit); } } } sharingCommits.Clear(); this.commitsByTree.Remove(treeSha); } } this.missingTreesByCommit.Remove(commitSha); this.RemoveFromLruOrder(commitSha); // Clean up any commits that became empty due to the cascade foreach (string emptyCommit in commitsToCheck) { if (this.missingTreesByCommit.TryGetValue(emptyCommit, out var remaining) && remaining.Count == 0) { this.missingTreesByCommit.Remove(emptyCommit); this.RemoveFromLruOrder(emptyCommit); } } } private void RemoveFromLruOrder(string commitSha) { if (this.commitNodes.TryGetValue(commitSha, out var node)) { this.commitOrder.Remove(node); this.commitNodes.Remove(commitSha); } } } } ================================================ FILE: GVFS/GVFS.Common/ModifiedPathsDatabase.cs ================================================ using System; using System.Collections.Generic; using System.IO; using GVFS.Common.FileSystem; using GVFS.Common.Tracing; namespace GVFS.Common { /// /// The modified paths database is the list of files and folders that /// git is now responsible for keeping up to date. Files and folders are added /// to this list by being created, edited, deleted, or renamed. /// public class ModifiedPathsDatabase : FileBasedCollection { private ConcurrentHashSet modifiedPaths; protected ModifiedPathsDatabase(ITracer tracer, PhysicalFileSystem fileSystem, string dataFilePath) : base(tracer, fileSystem, dataFilePath, collectionAppendsDirectlyToFile: true) { this.modifiedPaths = new ConcurrentHashSet(GVFSPlatform.Instance.Constants.PathComparer); } public int Count { get { return this.modifiedPaths.Count; } } public static bool TryLoadOrCreate(ITracer tracer, string dataDirectory, PhysicalFileSystem fileSystem, out ModifiedPathsDatabase output, out string error) { ModifiedPathsDatabase temp = new ModifiedPathsDatabase(tracer, fileSystem, dataDirectory); if (!temp.TryLoadFromDisk( temp.TryParseAddLine, temp.TryParseRemoveLine, (key, value) => temp.modifiedPaths.Add(key), out error)) { temp = null; output = null; return false; } if (temp.Count == 0) { bool isRetryable; temp.TryAdd(GVFSConstants.SpecialGitFiles.GitAttributes, isFolder: false, isRetryable: out isRetryable); } error = null; output = temp; return true; } /// /// This method will examine the modified paths to check if there is already a parent folder entry in /// the modified paths. If there is a parent folder the entry does not need to be in the modified paths /// and will be removed because the parent folder is recursive and covers any children. /// public void RemoveEntriesWithParentFolderEntry(ITracer tracer) { int startingCount = this.modifiedPaths.Count; using (ITracer activity = tracer.StartActivity(nameof(this.RemoveEntriesWithParentFolderEntry), EventLevel.Informational)) { foreach (string modifiedPath in this.modifiedPaths) { if (this.ContainsParentFolderWithNormalizedPath(modifiedPath)) { this.modifiedPaths.TryRemove(modifiedPath); } } EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(startingCount), startingCount); metadata.Add("EndCount", this.modifiedPaths.Count); activity.Stop(metadata); } } public bool Contains(string path, bool isFolder) { string entry = this.NormalizeEntryString(path, isFolder); return this.modifiedPaths.Contains(entry); } public bool ContainsParentFolder(string path, out string parentFolder) { string entry = this.NormalizeEntryString(path, isFolder: false); return this.ContainsParentFolderWithNormalizedPath(entry, out parentFolder); } public IEnumerable GetAllModifiedPaths() { return this.modifiedPaths; } public bool TryAdd(string path, bool isFolder, out bool isRetryable) { isRetryable = true; string entry = this.NormalizeEntryString(path, isFolder); if (!this.modifiedPaths.Contains(entry) && !this.ContainsParentFolderWithNormalizedPath(entry)) { try { this.WriteAddEntry(entry, () => this.modifiedPaths.Add(entry)); } catch (IOException e) { this.TraceWarning(isFolder, entry, e, nameof(this.TryAdd)); return false; } catch (Exception e) { this.TraceError(isFolder, entry, e, nameof(this.TryAdd)); isRetryable = false; return false; } } return true; } public List RemoveAllEntriesForFolder(string path) { List removedEntries = new List(); string entry = this.NormalizeEntryString(path, isFolder: true); foreach (string modifiedPath in this.modifiedPaths) { if (modifiedPath.StartsWith(entry, GVFSPlatform.Instance.Constants.PathComparison)) { if (this.modifiedPaths.TryRemove(modifiedPath)) { removedEntries.Add(modifiedPath); } } } this.WriteAllEntriesAndFlush(); return removedEntries; } public bool TryRemove(string path, bool isFolder, out bool isRetryable) { isRetryable = true; string entry = this.NormalizeEntryString(path, isFolder); if (this.modifiedPaths.Contains(entry)) { isRetryable = true; try { this.WriteRemoveEntry(entry, () => this.modifiedPaths.TryRemove(entry)); } catch (IOException e) { this.TraceWarning(isFolder, entry, e, nameof(this.TryRemove)); return false; } catch (Exception e) { this.TraceError(isFolder, entry, e, nameof(this.TryRemove)); isRetryable = false; return false; } } return true; } public void WriteAllEntriesAndFlush() { try { this.WriteAndReplaceDataFile(this.GenerateDataLines); } catch (Exception e) { throw new FileBasedCollectionException(e); } } private static EventMetadata CreateEventMetadata(bool isFolder, string entry, Exception e) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", "ModifiedPathsDatabase"); metadata.Add(nameof(entry), entry); metadata.Add(nameof(isFolder), isFolder); if (e != null) { metadata.Add("Exception", e.ToString()); } return metadata; } private IEnumerable GenerateDataLines() { foreach (string entry in this.modifiedPaths) { yield return this.FormatAddLine(entry); } } private void TraceWarning(bool isFolder, string entry, Exception e, string method) { if (this.Tracer != null) { EventMetadata metadata = CreateEventMetadata(isFolder, entry, e); this.Tracer.RelatedWarning(metadata, $"{e.GetType().Name} caught while processing {method}"); } } private void TraceError(bool isFolder, string entry, Exception e, string method) { if (this.Tracer != null) { EventMetadata metadata = CreateEventMetadata(isFolder, entry, e); this.Tracer.RelatedError(metadata, $"{e.GetType().Name} caught while processing {method}"); } } private bool TryParseAddLine(string line, out string key, out string value, out string error) { key = line; value = null; error = null; return true; } private bool TryParseRemoveLine(string line, out string key, out string error) { key = line; error = null; return true; } private bool ContainsParentFolderWithNormalizedPath(string modifiedPath) { return this.ContainsParentFolderWithNormalizedPath(modifiedPath, out _); } private bool ContainsParentFolderWithNormalizedPath(string modifiedPath, out string parentFolder) { string[] pathParts = modifiedPath.Split(new char[] { GVFSConstants.GitPathSeparator }, StringSplitOptions.RemoveEmptyEntries); parentFolder = string.Empty; for (int i = 0; i < pathParts.Length - 1; i++) { parentFolder += pathParts[i] + GVFSConstants.GitPathSeparatorString; if (this.modifiedPaths.Contains(parentFolder)) { return true; } } return false; } private string NormalizeEntryString(string virtualPath, bool isFolder) { return virtualPath.Replace(Path.DirectorySeparatorChar, GVFSConstants.GitPathSeparator).Trim(GVFSConstants.GitPathSeparator) + (isFolder ? GVFSConstants.GitPathSeparatorString : string.Empty); } } } ================================================ FILE: GVFS/GVFS.Common/NamedPipes/AllowAllLocksNamedPipeServer.cs ================================================ using GVFS.Common.Tracing; namespace GVFS.Common.NamedPipes { public class AllowAllLocksNamedPipeServer { public static NamedPipeServer Create(ITracer tracer, GVFSEnlistment enlistment) { return NamedPipeServer.StartNewServer(enlistment.NamedPipeName, tracer, AllowAllLocksNamedPipeServer.HandleRequest); } private static void HandleRequest(ITracer tracer, string request, NamedPipeServer.Connection connection) { NamedPipeMessages.Message message = NamedPipeMessages.Message.FromString(request); switch (message.Header) { case NamedPipeMessages.AcquireLock.AcquireRequest: NamedPipeMessages.AcquireLock.Response response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.AcceptResult); connection.TrySendResponse(response.CreateMessage()); break; case NamedPipeMessages.ReleaseLock.Request: connection.TrySendResponse(NamedPipeMessages.ReleaseLock.SuccessResult); break; case NamedPipeMessages.ModifiedPaths.ListRequest: string gitAttributes = GVFSConstants.SpecialGitFiles.GitAttributes + "\0"; NamedPipeMessages.ModifiedPaths.Response listResponse = new NamedPipeMessages.ModifiedPaths.Response(NamedPipeMessages.ModifiedPaths.SuccessResult, gitAttributes); connection.TrySendResponse(listResponse.CreateMessage()); break; default: connection.TrySendResponse(NamedPipeMessages.UnknownRequest); if (tracer != null) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", "AllowAllLocksNamedPipeServer"); metadata.Add("Header", message.Header); tracer.RelatedWarning(metadata, "HandleRequest: Unknown request", Keywords.Telemetry); } break; } } } } ================================================ FILE: GVFS/GVFS.Common/NamedPipes/BrokenPipeException.cs ================================================ using System; using System.IO; namespace GVFS.Common.NamedPipes { public class BrokenPipeException : Exception { public BrokenPipeException(string message, IOException innerException) : base(message, innerException) { } } } ================================================ FILE: GVFS/GVFS.Common/NamedPipes/HydrationStatusNamedPipeMessages.cs ================================================ using System; namespace GVFS.Common.NamedPipes { public static partial class NamedPipeMessages { public static class HydrationStatus { public const string Request = "GetHydration"; public const string SuccessResult = "S"; public const string NotAvailableResult = "NA"; /// /// Wire format: PlaceholderFileCount,PlaceholderFolderCount,ModifiedFileCount,ModifiedFolderCount,TotalFileCount,TotalFolderCount /// public class Response { public int PlaceholderFileCount { get; set; } public int PlaceholderFolderCount { get; set; } public int ModifiedFileCount { get; set; } public int ModifiedFolderCount { get; set; } public int TotalFileCount { get; set; } public int TotalFolderCount { get; set; } public int HydratedFileCount => this.PlaceholderFileCount + this.ModifiedFileCount; public int HydratedFolderCount => this.PlaceholderFolderCount + this.ModifiedFolderCount; public bool IsValid => this.PlaceholderFileCount >= 0 && this.PlaceholderFolderCount >= 0 && this.ModifiedFileCount >= 0 && this.ModifiedFolderCount >= 0 && this.TotalFileCount >= this.HydratedFileCount && this.TotalFolderCount >= this.HydratedFolderCount; public string ToDisplayMessage() { if (!this.IsValid) { return null; } int filePercent = this.TotalFileCount == 0 ? 0 : (int)((100L * this.HydratedFileCount) / this.TotalFileCount); int folderPercent = this.TotalFolderCount == 0 ? 0 : (int)((100L * this.HydratedFolderCount) / this.TotalFolderCount); return $"{filePercent}% of files and {folderPercent}% of folders hydrated. Run 'gvfs health' at the repository root for details."; } public string ToBody() { return string.Join(",", this.PlaceholderFileCount, this.PlaceholderFolderCount, this.ModifiedFileCount, this.ModifiedFolderCount, this.TotalFileCount, this.TotalFolderCount); } public static bool TryParse(string body, out Response response) { response = null; if (string.IsNullOrEmpty(body)) { return false; } string[] parts = body.Split(','); if (parts.Length < 6) { return false; } if (!int.TryParse(parts[0], out int placeholderFileCount) || !int.TryParse(parts[1], out int placeholderFolderCount) || !int.TryParse(parts[2], out int modifiedFileCount) || !int.TryParse(parts[3], out int modifiedFolderCount) || !int.TryParse(parts[4], out int totalFileCount) || !int.TryParse(parts[5], out int totalFolderCount)) { return false; } response = new Response { PlaceholderFileCount = placeholderFileCount, PlaceholderFolderCount = placeholderFolderCount, ModifiedFileCount = modifiedFileCount, ModifiedFolderCount = modifiedFolderCount, TotalFileCount = totalFileCount, TotalFolderCount = totalFolderCount, }; return response.IsValid; } } } } } ================================================ FILE: GVFS/GVFS.Common/NamedPipes/LockNamedPipeMessages.cs ================================================ using System; using System.Collections.Generic; namespace GVFS.Common.NamedPipes { /// /// Define messages used to communicate via the named-pipe in GVFS. /// /// /// This file contains only the message types that GVFS.Hooks is interested /// in using. For all other messages see GVFS.Common/NamedPipeMessages. /// public static partial class NamedPipeMessages { public const string UnknownRequest = "UnknownRequest"; private const char MessageSeparator = '|'; public static class AcquireLock { public const string AcquireRequest = "AcquireLock"; public const string DenyGVFSResult = "LockDeniedGVFS"; public const string DenyGitResult = "LockDeniedGit"; public const string AcceptResult = "LockAcquired"; public const string AvailableResult = "LockAvailable"; public const string MountNotReadyResult = "MountNotReady"; public const string UnmountInProgressResult = "UnmountInProgress"; public class Response { public Response(string result, LockData responseData = null, string denyGVFSMessage = null) { this.Result = result; this.ResponseData = responseData; this.DenyGVFSMessage = denyGVFSMessage; } public Response(Message message) { this.Result = message.Header; if (this.Result == DenyGVFSResult) { this.DenyGVFSMessage = message.Body; } else { this.ResponseData = LockData.FromBody(message.Body); } } public string Result { get; } public string DenyGVFSMessage { get; } public LockData ResponseData { get; } public Message CreateMessage() { string messageBody = null; if (this.ResponseData != null) { messageBody = this.ResponseData.ToMessage(); } else if (this.DenyGVFSMessage != null) { messageBody = this.DenyGVFSMessage; } return new Message(this.Result, messageBody); } } } public static class ReleaseLock { public const string Request = "ReleaseLock"; public const string SuccessResult = "LockReleased"; public const string FailureResult = "ReleaseDenied"; public class Response { public Response(string result, ReleaseLockData responseData = null) { this.Result = result; this.ResponseData = responseData; } public Response(Message message) { this.Result = message.Header; this.ResponseData = ReleaseLockData.FromBody(message.Body); } public string Result { get; } public ReleaseLockData ResponseData { get; } public Message CreateMessage() { string messageBody = null; if (this.ResponseData != null) { messageBody = this.ResponseData.ToMessage(); } return new Message(this.Result, messageBody); } } public class ReleaseLockData { // Message Format // FailedUpdateCount failedToUpdateFileList, List failedToDeleteFileList) : this( failedToUpdateFileList.Count, failedToDeleteFileList.Count, (failedToUpdateFileList.Count + failedToDeleteFileList.Count <= MaxReportedFileNames) ? failedToUpdateFileList : new List(), (failedToUpdateFileList.Count + failedToDeleteFileList.Count <= MaxReportedFileNames) ? failedToDeleteFileList : new List()) { } private ReleaseLockData( int failedToUpdateCount, int failedToDeleteCount, List failedToUpdateFileList, List failedToDeleteFileList) { this.FailedToUpdateCount = failedToUpdateCount; this.FailedToDeleteCount = failedToDeleteCount; this.FailedToUpdateFileList = failedToUpdateFileList; this.FailedToDeleteFileList = failedToDeleteFileList; } public bool HasFailures { get { return this.FailedToUpdateCount > 0 || this.FailedToDeleteCount > 0; } } public int FailedToUpdateCount { get; private set; } public int FailedToDeleteCount { get; private set; } public bool FailureCountExceedsMaxFileNames { get { return (this.FailedToUpdateCount + this.FailedToDeleteCount) > MaxReportedFileNames; } } public List FailedToUpdateFileList { get; private set; } public List FailedToDeleteFileList { get; private set; } /// /// Parses ReleaseLockData from the provided string. /// /// Message body (containing ReleaseLockData in string format) /// /// - ReleaseLockData when body is successfully parsed /// - null when there is a parsing error /// internal static ReleaseLockData FromBody(string body) { if (!string.IsNullOrEmpty(body)) { string[] sections = body.Split(new char[] { SectionSeparator }); if (sections.Length != 4) { return null; } int failedToUpdateCount; if (!int.TryParse(sections[0], out failedToUpdateCount)) { return null; } int failedToDeleteCount; if (!int.TryParse(sections[1], out failedToDeleteCount)) { return null; } List failedToUpdateFileList = null; string[] updateParts = sections[2].Split(new char[] { MessageSeparator }, StringSplitOptions.RemoveEmptyEntries); if (updateParts.Length > 0) { failedToUpdateFileList = new List(updateParts); } List failedToDeleteFileList = null; string[] deleteParts = sections[3].Split(new char[] { MessageSeparator }, StringSplitOptions.RemoveEmptyEntries); if (deleteParts.Length > 0) { failedToDeleteFileList = new List(deleteParts); } return new ReleaseLockData(failedToUpdateCount, failedToDeleteCount, failedToUpdateFileList, failedToDeleteFileList); } return new ReleaseLockData(failedToUpdateCount: 0, failedToDeleteCount: 0, failedToUpdateFileList: null, failedToDeleteFileList: null); } internal string ToMessage() { return this.FailedToUpdateCount.ToString() + SectionSeparator + this.FailedToDeleteCount.ToString() + SectionSeparator + string.Join(MessageSeparator.ToString(), this.FailedToUpdateFileList) + SectionSeparator + string.Join(MessageSeparator.ToString(), this.FailedToDeleteFileList); } } } public class LockRequest { public LockRequest(string messageBody) { this.RequestData = LockData.FromBody(messageBody); } public LockRequest(int pid, bool isElevated, bool checkAvailabilityOnly, string parsedCommand, string gitCommandSessionId) { this.RequestData = new LockData(pid, isElevated, checkAvailabilityOnly, parsedCommand, gitCommandSessionId); } public LockData RequestData { get; } public Message CreateMessage(string header) { return new Message(header, this.RequestData.ToMessage()); } } public class LockData { public LockData(int pid, bool isElevated, bool checkAvailabilityOnly, string parsedCommand, string gitCommandSessionId) { this.PID = pid; this.GitCommandSessionId = gitCommandSessionId; this.IsElevated = isElevated; this.CheckAvailabilityOnly = checkAvailabilityOnly; this.ParsedCommand = parsedCommand; } public int PID { get; set; } public string GitCommandSessionId { get; set; } public bool IsElevated { get; set; } /// /// Should the command actually acquire the GVFSLock or /// only check if the lock is available. /// public bool CheckAvailabilityOnly { get; set; } /// /// The command line requesting the lock, built internally for parsing purposes. /// e.g. "git status", "git rebase" /// public string ParsedCommand { get; set; } public override string ToString() { return this.ParsedCommand + " (" + this.PID + ")"; } internal static LockData FromBody(string body) { if (!string.IsNullOrEmpty(body)) { // This mesage is stored using the MessageSeperator delimiter for performance reasons // Format of the body uses length prefixed string so that the strings can have the delimiter in them // Examples: // "123|true|false|13|parsedCommand|9|sessionId" // "321|false|true|30|parsedCommand with | delimiter|26|sessionId with | delimiter" string[] dataParts = body.Split(MessageSeparator); int pid; bool isElevated = false; bool checkAvailabilityOnly = false; string parsedCommand = null; if (dataParts.Length < 7) { throw new InvalidOperationException(string.Format("Invalid lock message. Expected at least 7 parts, got: {0} from message: '{1}'", dataParts.Length, body)); } if (!int.TryParse(dataParts[0], out pid)) { throw new InvalidOperationException(string.Format("Invalid lock message. Expected PID, got: {0} from message: '{1}'", dataParts[0], body)); } if (!bool.TryParse(dataParts[1], out isElevated)) { throw new InvalidOperationException(string.Format("Invalid lock message. Expected bool for isElevated, got: {0} from message: '{1}'", dataParts[1], body)); } if (!bool.TryParse(dataParts[2], out checkAvailabilityOnly)) { throw new InvalidOperationException(string.Format("Invalid lock message. Expected bool for checkAvailabilityOnly, got: {0} from message: '{1}'", dataParts[2], body)); } if (!int.TryParse(dataParts[3], out int parsedCommandLength)) { throw new InvalidOperationException(string.Format("Invalid lock message. Expected command length, got: {0} from message: '{1}'", dataParts[3], body)); } // ParsedCommandLength should be the length of the string at the end of the message // Add the length of the previous parts, plus delimiters int commandStartingSpot = dataParts[0].Length + dataParts[1].Length + dataParts[2].Length + dataParts[3].Length + 4; if ((commandStartingSpot + parsedCommandLength) >= body.Length) { throw new InvalidOperationException(string.Format("Invalid lock message. The parsedCommand is an unexpected length, got: {0} from message: '{1}'", parsedCommandLength, body)); } parsedCommand = body.Substring(commandStartingSpot, parsedCommandLength); // The session Id is after the parsed command with the length of the session Id string coming first // Use the string after the parsed command string to get the session Id data string sessionIdSubString = body.Substring(commandStartingSpot + parsedCommandLength + 1); string[] sessionIdParts = sessionIdSubString.Split(MessageSeparator); if (!int.TryParse(sessionIdParts[0], out int sessionIdLength)) { throw new InvalidOperationException(string.Format("Invalid lock message. Expected session id length, got: {0} from message: '{1}'", sessionIdParts[0], body)); } // Validate the session Id data does not exceed the body of the message by using the previous // command starting position and length and adding length of the part for the size of the session id plus the 2 delimiters int sessionIdStartingSpot = commandStartingSpot + parsedCommandLength + sessionIdParts[0].Length + 2; if ((sessionIdStartingSpot + sessionIdLength) != body.Length) { throw new InvalidOperationException(string.Format("Invalid lock message. The sessionId is an unexpected length, got: {0} from message: '{1}'", sessionIdLength, body)); } string sessionId = body.Substring(sessionIdStartingSpot, sessionIdLength); return new LockData(pid, isElevated, checkAvailabilityOnly, parsedCommand, sessionId); } return null; } internal string ToMessage() { return string.Join( MessageSeparator.ToString(), this.PID, this.IsElevated, this.CheckAvailabilityOnly, this.ParsedCommand.Length, this.ParsedCommand, this.GitCommandSessionId.Length, this.GitCommandSessionId); } } public class Message { public Message(string header, string body) { this.Header = header; this.Body = body; } public string Header { get; } public string Body { get; } public static Message FromString(string message) { string header = null; string body = null; if (!string.IsNullOrEmpty(message)) { string[] parts = message.Split(new[] { NamedPipeMessages.MessageSeparator }, count: 2); header = parts[0]; if (parts.Length > 1) { body = parts[1]; } } return new Message(header, body); } public override string ToString() { string result = string.Empty; if (!string.IsNullOrEmpty(this.Header)) { result = this.Header; } if (this.Body != null) { result = result + NamedPipeMessages.MessageSeparator + this.Body; } return result; } } } } ================================================ FILE: GVFS/GVFS.Common/NamedPipes/NamedPipeClient.cs ================================================ using System; using System.IO; using System.IO.Pipes; namespace GVFS.Common.NamedPipes { public class NamedPipeClient : IDisposable { private string pipeName; private NamedPipeClientStream clientStream; private NamedPipeStreamReader reader; private NamedPipeStreamWriter writer; public NamedPipeClient(string pipeName) { this.pipeName = pipeName; } public bool Connect(int timeoutMilliseconds = 3000) { if (this.clientStream != null) { throw new InvalidOperationException(); } try { this.clientStream = new NamedPipeClientStream(this.pipeName); this.clientStream.Connect(timeoutMilliseconds); } catch (TimeoutException) { return false; } catch (IOException) { return false; } this.reader = new NamedPipeStreamReader(this.clientStream); this.writer = new NamedPipeStreamWriter(this.clientStream); return true; } public bool TrySendRequest(NamedPipeMessages.Message message) { try { this.SendRequest(message); return true; } catch (BrokenPipeException) { } return false; } public void SendRequest(NamedPipeMessages.Message message) { this.SendRequest(message.ToString()); } public void SendRequest(string message) { this.ValidateConnection(); try { this.writer.WriteMessage(message); } catch (IOException e) { throw new BrokenPipeException("Unable to send: " + message, e); } } public string ReadRawResponse() { try { string response = this.reader.ReadMessage(); if (response == null) { throw new BrokenPipeException("Unable to read from pipe", null); } return response; } catch (IOException e) { throw new BrokenPipeException("Unable to read from pipe", e); } } public NamedPipeMessages.Message ReadResponse() { return NamedPipeMessages.Message.FromString(this.ReadRawResponse()); } public bool TryReadResponse(out NamedPipeMessages.Message message) { try { message = NamedPipeMessages.Message.FromString(this.ReadRawResponse()); return true; } catch (BrokenPipeException) { message = null; return false; } } public void Dispose() { this.ValidateConnection(); if (this.clientStream != null) { this.clientStream.Dispose(); this.clientStream = null; } this.reader = null; this.writer = null; } private void ValidateConnection() { if (this.clientStream == null) { throw new InvalidOperationException("There is no connection"); } } } } ================================================ FILE: GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs ================================================ using Newtonsoft.Json; using System; using System.Collections.Generic; namespace GVFS.Common.NamedPipes { /// /// Define messages used to communicate via the named-pipe in GVFS. /// /// /// This class is defined as partial so that GVFS.Hooks /// can compile the portions of it that it cares about (see LockedNamedPipeMessages). /// public static partial class NamedPipeMessages { public const string UnknownGVFSState = "UnknownGVFSState"; public const string MountNotReadyResult = "MountNotReady"; private const string ResponseSuffix = "Response"; public enum CompletionState { NotCompleted, Success, Failure } public static class GetStatus { public const string Request = "GetStatus"; public const string Mounting = "Mounting"; public const string Ready = "Ready"; public const string Unmounting = "Unmounting"; public const string MountFailed = "MountFailed"; public class Response { public string MountStatus { get; set; } public string EnlistmentRoot { get; set; } public string LocalCacheRoot { get; set; } public string RepoUrl { get; set; } public string CacheServer { get; set; } public int BackgroundOperationCount { get; set; } public string LockStatus { get; set; } public string DiskLayoutVersion { get; set; } public static Response FromJson(string json) { return JsonConvert.DeserializeObject(json); } public string ToJson() { return JsonConvert.SerializeObject(this); } } } public static class Unmount { public const string Request = "Unmount"; public const string NotMounted = "NotMounted"; public const string Acknowledged = "Ack"; public const string Completed = "Complete"; public const string AlreadyUnmounting = "AlreadyUnmounting"; public const string MountFailed = "MountFailed"; } public static class ModifiedPaths { public const string ListRequest = "MPL"; public const string InvalidVersion = "InvalidVersion"; public const string SuccessResult = "S"; public const string CurrentVersion = "1"; public class Request { public Request(Message message) { this.Version = message.Body; } public string Version { get; } } public class Response { public Response(string result, string data = "") { this.Result = result; this.Data = data; } public string Result { get; } public string Data { get; } public Message CreateMessage() { return new Message(this.Result, this.Data); } } } public static class DownloadObject { public const string DownloadRequest = "DLO"; public const string SuccessResult = "S"; public const string DownloadFailed = "F"; public const string InvalidSHAResult = "InvalidSHA"; public class Request { public Request(Message message) { this.RequestSha = message.Body; } public string RequestSha { get; } public Message CreateMessage() { return new Message(DownloadRequest, this.RequestSha); } } public class Response { public Response(string result) { this.Result = result; } public string Result { get; } public Message CreateMessage() { return new Message(this.Result, null); } } } public static class PostIndexChanged { public const string NotificationRequest = "PICN"; public const string SuccessResult = "S"; public const string FailureResult = "F"; public class Request { public Request(Message message) { if (message.Body.Length != 2) { throw new InvalidOperationException($"Invalid PostIndexChanged message. Expected 2 characters, got: {message.Body.Length} from message: '{message.Body}'"); } this.UpdatedWorkingDirectory = message.Body[0] == '1'; this.UpdatedSkipWorktreeBits = message.Body[1] == '1'; } public Request(bool updatedWorkingDirectory, bool updatedSkipWorktreeBits) { this.UpdatedWorkingDirectory = updatedWorkingDirectory; this.UpdatedSkipWorktreeBits = updatedSkipWorktreeBits; } public bool UpdatedWorkingDirectory { get; } public bool UpdatedSkipWorktreeBits { get; } public Message CreateMessage() { return new Message(NotificationRequest, $"{this.BoolToString(this.UpdatedWorkingDirectory)}{this.BoolToString(this.UpdatedSkipWorktreeBits)}"); } private string BoolToString(bool value) { return value ? "1" : "0"; } } public class Response { public Response(string result) { this.Result = result; } public string Result { get; } public Message CreateMessage() { return new Message(this.Result, null); } } } public static class DehydrateFolders { public const string Dehydrate = "Dehydrate"; public const string DehydratedResult = "Dehydrated"; public const string MountNotReadyResult = "MountNotReady"; public class Request { public Request(string backupFolderPath, string folders) { this.Folders = folders; this.BackupFolderPath = backupFolderPath; } public static Request FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); } public string Folders { get; } public string BackupFolderPath { get; } public Message CreateMessage() { return new Message(Dehydrate, JsonConvert.SerializeObject(this)); } } public class Response { public Response(string result) { this.Result = result; this.SuccessfulFolders = new List(); this.FailedFolders = new List(); } public string Result { get; } public List SuccessfulFolders { get; } public List FailedFolders { get; } public static Response FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); } public Message CreateMessage() { return new Message(this.Result, JsonConvert.SerializeObject(this)); } } } public static class RunPostFetchJob { public const string PostFetchJob = "PostFetch"; public const string QueuedResult = "Queued"; public const string MountNotReadyResult = "MountNotReady"; public class Request { public Request(List packIndexes) { this.PackIndexList = JsonConvert.SerializeObject(packIndexes); } public Request(Message message) { this.PackIndexList = message.Body; } /// /// The PackIndexList data is a JSON-formatted list of strings, /// where each string is the name of an IDX file in the shared /// object cache. /// public string PackIndexList { get; set; } public Message CreateMessage() { return new Message(PostFetchJob, this.PackIndexList); } } public class Response { public Response(string result) { this.Result = result; } public string Result { get; } public Message CreateMessage() { return new Message(this.Result, null); } } } public static class Notification { public class Request { public const string Header = nameof(Notification); public enum Identifier { AutomountStart, MountSuccess, MountFailure, UpgradeAvailable } public Identifier Id { get; set; } public string Title { get; set; } public string Message { get; set; } public string Enlistment { get; set; } public int EnlistmentCount { get; set; } public string NewVersion { get; set; } public static Request FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); } public Message ToMessage() { return new Message(Header, JsonConvert.SerializeObject(this)); } } } public class UnregisterRepoRequest { public const string Header = nameof(UnregisterRepoRequest); public string EnlistmentRoot { get; set; } public static UnregisterRepoRequest FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); } public Message ToMessage() { return new Message(Header, JsonConvert.SerializeObject(this)); } public class Response : BaseResponse { public static Response FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); } } } public class RegisterRepoRequest { public const string Header = nameof(RegisterRepoRequest); public string EnlistmentRoot { get; set; } public string OwnerSID { get; set; } public static RegisterRepoRequest FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); } public Message ToMessage() { return new Message(Header, JsonConvert.SerializeObject(this)); } public class Response : BaseResponse { public static Response FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); } } } public class EnableAndAttachProjFSRequest { public const string Header = nameof(EnableAndAttachProjFSRequest); public string EnlistmentRoot { get; set; } public static EnableAndAttachProjFSRequest FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); } public Message ToMessage() { return new Message(Header, JsonConvert.SerializeObject(this)); } public class Response : BaseResponse { public static Response FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); } } } public class GetActiveRepoListRequest { public const string Header = nameof(GetActiveRepoListRequest); public static GetActiveRepoListRequest FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); } public Message ToMessage() { return new Message(Header, JsonConvert.SerializeObject(this)); } public class Response : BaseResponse { public List RepoList { get; set; } public static Response FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); } } } public class BaseResponse { public const string Header = nameof(TRequest) + ResponseSuffix; public CompletionState State { get; set; } public string ErrorMessage { get; set; } public Message ToMessage() { return new Message(Header, JsonConvert.SerializeObject(this)); } } } } ================================================ FILE: GVFS/GVFS.Common/NamedPipes/NamedPipeServer.cs ================================================ using GVFS.Common.Tracing; using System; using System.IO; using System.IO.Pipes; using System.Threading; namespace GVFS.Common.NamedPipes { /// /// The server side of a Named Pipe used for interprocess communication. /// /// Named Pipe protocol: /// The client / server process sends a "message" (or line) of data as a /// sequence of bytes terminated by a 0x3 byte (ASCII control code for /// End of text). Text is encoded as UTF-8 to be sent as bytes across the wire. /// /// This format was chosen so that: /// 1) A reasonable range of values can be transmitted across the pipe, /// including null and bytes that represent newline characters. /// 2) It would be easy to implement in multiple places, as we /// have managed and native implementations. /// public class NamedPipeServer : IDisposable { private bool isStopping; private string pipeName; private Action handleConnection; private ITracer tracer; private NamedPipeServerStream listeningPipe; private NamedPipeServer(string pipeName, ITracer tracer, Action handleConnection) { this.pipeName = pipeName; this.tracer = tracer; this.handleConnection = handleConnection; this.isStopping = false; } public static NamedPipeServer StartNewServer(string pipeName, ITracer tracer, Action handleRequest) { if (pipeName.Length > GVFSPlatform.Instance.Constants.MaxPipePathLength) { throw new PipeNameLengthException(string.Format("The pipe name ({0}) exceeds the max length allowed({1})", pipeName, GVFSPlatform.Instance.Constants.MaxPipePathLength)); } NamedPipeServer pipeServer = new NamedPipeServer(pipeName, tracer, connection => HandleConnection(tracer, connection, handleRequest)); pipeServer.OpenListeningPipe(); return pipeServer; } public void Dispose() { this.isStopping = true; NamedPipeServerStream pipe = Interlocked.Exchange(ref this.listeningPipe, null); if (pipe != null) { pipe.Dispose(); } } private static void HandleConnection(ITracer tracer, Connection connection, Action handleRequest) { while (connection.IsConnected) { string request = connection.ReadRequest(); if (request == null || !connection.IsConnected) { break; } handleRequest(tracer, request, connection); } } private void OpenListeningPipe() { try { if (this.listeningPipe != null) { throw new InvalidOperationException("There is already a pipe listening for a connection"); } this.listeningPipe = GVFSPlatform.Instance.CreatePipeByName(this.pipeName); this.listeningPipe.BeginWaitForConnection(this.OnNewConnection, this.listeningPipe); } catch (Exception e) { this.LogErrorAndExit("OpenListeningPipe caught unhandled exception, exiting process", e); } } private void OnNewConnection(IAsyncResult ar) { if (!this.isStopping) { this.OnNewConnection(ar, createNewThreadIfSynchronous: true); } } private void OnNewConnection(IAsyncResult ar, bool createNewThreadIfSynchronous) { if (createNewThreadIfSynchronous && ar.CompletedSynchronously) { // if this callback got called synchronously, we must not do any blocking IO on this thread // or we will block the original caller. Moving to a new thread so that it will be safe // to call a blocking Read on the NamedPipeServerStream new Thread(() => this.OnNewConnection(ar, createNewThreadIfSynchronous: false)).Start(); return; } this.listeningPipe = null; bool connectionBroken = false; NamedPipeServerStream pipe = (NamedPipeServerStream)ar.AsyncState; try { try { pipe.EndWaitForConnection(ar); } catch (IOException e) { connectionBroken = true; EventMetadata metadata = new EventMetadata(); metadata.Add("Area", "NamedPipeServer"); metadata.Add("Exception", e.ToString()); metadata.Add(TracingConstants.MessageKey.WarningMessage, "OnNewConnection: Connection broken"); this.tracer.RelatedEvent(EventLevel.Warning, "OnNewConnectionn_EndWaitForConnection_IOException", metadata); } catch (Exception e) { this.LogErrorAndExit("OnNewConnection caught unhandled exception, exiting process", e); } if (!this.isStopping) { this.OpenListeningPipe(); if (!connectionBroken) { try { this.handleConnection(new Connection(pipe, this.tracer, () => this.isStopping)); } catch (Exception e) { this.LogErrorAndExit("Unhandled exception in connection handler", e); } } } } finally { pipe.Dispose(); } } private void LogErrorAndExit(string message, Exception e) { if (this.tracer != null) { EventMetadata metadata = new EventMetadata(); metadata.Add("Area", "NamedPipeServer"); if (e != null) { metadata.Add("Exception", e.ToString()); } this.tracer.RelatedError(metadata, message); } Environment.Exit((int)ReturnCode.GenericError); } public class Connection { private NamedPipeServerStream serverStream; private NamedPipeStreamReader reader; private NamedPipeStreamWriter writer; private ITracer tracer; private Func isStopping; public Connection(NamedPipeServerStream serverStream, ITracer tracer, Func isStopping) { this.serverStream = serverStream; this.tracer = tracer; this.isStopping = isStopping; this.reader = new NamedPipeStreamReader(this.serverStream); this.writer = new NamedPipeStreamWriter(this.serverStream); } public bool IsConnected { get { return !this.isStopping() && this.serverStream.IsConnected; } } public NamedPipeMessages.Message ReadMessage() { return NamedPipeMessages.Message.FromString(this.ReadRequest()); } public string ReadRequest() { try { return this.reader.ReadMessage(); } catch (IOException e) { EventMetadata metadata = new EventMetadata(); metadata.Add("ExceptionMessage", e.Message); metadata.Add("StackTrace", e.StackTrace); this.tracer.RelatedWarning( metadata: metadata, message: $"Error reading message from NamedPipe: {e.Message}", keywords: Keywords.Telemetry); return null; } } public virtual bool TrySendResponse(string message) { try { this.writer.WriteMessage(message); return true; } catch (IOException) { return false; } } public bool TrySendResponse(NamedPipeMessages.Message message) { return this.TrySendResponse(message.ToString()); } } } } ================================================ FILE: GVFS/GVFS.Common/NamedPipes/NamedPipeStreamReader.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace GVFS.Common.NamedPipes { /// /// Implements the NamedPipe protocol as described in NamedPipeServer. /// public class NamedPipeStreamReader { private const int InitialListSize = 1024; private const byte TerminatorByte = 0x3; private readonly byte[] buffer; private Stream stream; public NamedPipeStreamReader(Stream stream) { this.stream = stream; this.buffer = new byte[1]; } /// /// Read a message from the stream. /// /// The message read from the stream, or null if the end of the input stream has been reached. public string ReadMessage() { byte currentByte; bool streamOpen = this.TryReadByte(out currentByte); if (!streamOpen) { // The end of the stream has been reached - return null to indicate this. return null; } List bytes = new List(InitialListSize); do { bytes.Add(currentByte); streamOpen = this.TryReadByte(out currentByte); if (!streamOpen) { // We have read a partial message (the last byte received does not indicate that // this was the end of the message), but the stream has been closed. Throw an exception // and let upper layer deal with this condition. throw new IOException("Incomplete message read from stream. The end of the stream was reached without the expected terminating byte."); } } while (currentByte != TerminatorByte); return Encoding.UTF8.GetString(bytes.ToArray()); } /// /// Read a byte from the stream. /// /// The byte read from the stream /// True if byte read, false if end of stream has been reached private bool TryReadByte(out byte readByte) { this.buffer[0] = 0; int numBytesRead = this.stream.Read(this.buffer, 0, 1); readByte = this.buffer[0]; return numBytesRead == 1; } } } ================================================ FILE: GVFS/GVFS.Common/NamedPipes/NamedPipeStreamWriter.cs ================================================ using System.IO; using System.Text; namespace GVFS.Common.NamedPipes { public class NamedPipeStreamWriter { private const byte TerminatorByte = 0x3; private const string TerminatorByteString = "\x3"; private Stream stream; public NamedPipeStreamWriter(Stream stream) { this.stream = stream; } public void WriteMessage(string message) { byte[] byteBuffer = Encoding.UTF8.GetBytes(message + TerminatorByteString); this.stream.Write(byteBuffer, 0, byteBuffer.Length); this.stream.Flush(); } } } ================================================ FILE: GVFS/GVFS.Common/NamedPipes/PipeNameLengthException.cs ================================================ using System; namespace GVFS.Common.NamedPipes { public class PipeNameLengthException : Exception { public PipeNameLengthException(string message) : base(message) { } } } ================================================ FILE: GVFS/GVFS.Common/NamedPipes/UnstageNamedPipeMessages.cs ================================================ namespace GVFS.Common.NamedPipes { public static partial class NamedPipeMessages { public static class PrepareForUnstage { public const string Request = "PreUnstage"; public const string SuccessResult = "S"; public const string FailureResult = "F"; public class Response { public Response(string result) { this.Result = result; } public string Result { get; } public Message CreateMessage() { return new Message(this.Result, null); } } } } } ================================================ FILE: GVFS/GVFS.Common/NativeMethods.Shared.cs ================================================ using Microsoft.Win32.SafeHandles; using System; using System.ComponentModel; using System.IO; using System.Runtime.InteropServices; using System.Text; namespace GVFS.Common { public static partial class NativeMethods { public enum FileAttributes : uint { FILE_ATTRIBUTE_READONLY = 1, FILE_ATTRIBUTE_HIDDEN = 2, FILE_ATTRIBUTE_SYSTEM = 4, FILE_ATTRIBUTE_DIRECTORY = 16, FILE_ATTRIBUTE_ARCHIVE = 32, FILE_ATTRIBUTE_DEVICE = 64, FILE_ATTRIBUTE_NORMAL = 128, FILE_ATTRIBUTE_TEMPORARY = 256, FILE_ATTRIBUTE_SPARSEFILE = 512, FILE_ATTRIBUTE_REPARSEPOINT = 1024, FILE_ATTRIBUTE_COMPRESSED = 2048, FILE_ATTRIBUTE_OFFLINE = 4096, FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 8192, FILE_ATTRIBUTE_ENCRYPTED = 16384, FILE_FLAG_FIRST_PIPE_INSTANCE = 524288, FILE_FLAG_OPEN_NO_RECALL = 1048576, FILE_FLAG_OPEN_REPARSE_POINT = 2097152, FILE_FLAG_POSIX_SEMANTICS = 16777216, FILE_FLAG_BACKUP_SEMANTICS = 33554432, FILE_FLAG_DELETE_ON_CLOSE = 67108864, FILE_FLAG_SEQUENTIAL_SCAN = 134217728, FILE_FLAG_RANDOM_ACCESS = 268435456, FILE_FLAG_NO_BUFFERING = 536870912, FILE_FLAG_OVERLAPPED = 1073741824, FILE_FLAG_WRITE_THROUGH = 2147483648 } public enum FileAccess : uint { FILE_READ_DATA = 1, FILE_LIST_DIRECTORY = 1, FILE_WRITE_DATA = 2, FILE_ADD_FILE = 2, FILE_APPEND_DATA = 4, FILE_ADD_SUBDIRECTORY = 4, FILE_CREATE_PIPE_INSTANCE = 4, FILE_READ_EA = 8, FILE_WRITE_EA = 16, FILE_EXECUTE = 32, FILE_TRAVERSE = 32, FILE_DELETE_CHILD = 64, FILE_READ_ATTRIBUTES = 128, FILE_WRITE_ATTRIBUTES = 256, SPECIFIC_RIGHTS_ALL = 65535, DELETE = 65536, READ_CONTROL = 131072, STANDARD_RIGHTS_READ = 131072, STANDARD_RIGHTS_WRITE = 131072, STANDARD_RIGHTS_EXECUTE = 131072, WRITE_DAC = 262144, WRITE_OWNER = 524288, STANDARD_RIGHTS_REQUIRED = 983040, SYNCHRONIZE = 1048576, FILE_GENERIC_READ = 1179785, FILE_GENERIC_EXECUTE = 1179808, FILE_GENERIC_WRITE = 1179926, STANDARD_RIGHTS_ALL = 2031616, FILE_ALL_ACCESS = 2032127, ACCESS_SYSTEM_SECURITY = 16777216, MAXIMUM_ALLOWED = 33554432, GENERIC_ALL = 268435456, GENERIC_EXECUTE = 536870912, GENERIC_WRITE = 1073741824, GENERIC_READ = 2147483648 } [Flags] public enum ProcessAccessFlags : uint { All = 0x001F0FFF, Terminate = 0x00000001, CreateThread = 0x00000002, VirtualMemoryOperation = 0x00000008, VirtualMemoryRead = 0x00000010, VirtualMemoryWrite = 0x00000020, DuplicateHandle = 0x00000040, CreateProcess = 0x000000080, SetQuota = 0x00000100, SetInformation = 0x00000200, QueryInformation = 0x00000400, QueryLimitedInformation = 0x00001000, Synchronize = 0x00100000 } public static string GetFinalPathName(string path) { // Using FILE_FLAG_BACKUP_SEMANTICS as it works with file as well as folder path // According to MSDN, https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx, // we must set this flag to obtain a handle to a directory using (SafeFileHandle fileHandle = CreateFile( path, FileAccess.FILE_READ_ATTRIBUTES, FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, FileAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero)) { if (fileHandle.IsInvalid) { ThrowLastWin32Exception($"Invalid file handle for {path}"); } int finalPathSize = GetFinalPathNameByHandle(fileHandle, null, 0, 0); StringBuilder finalPath = new StringBuilder(finalPathSize + 1); // GetFinalPathNameByHandle buffer size should not include a NULL termination character finalPathSize = GetFinalPathNameByHandle(fileHandle, finalPath, finalPathSize, 0); if (finalPathSize == 0) { ThrowLastWin32Exception($"Failed to get final path size for {finalPath}"); } string pathString = finalPath.ToString(); // The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\" or "\\?\UNC\" // More information the prefixes is here http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx const string PathPrefix = @"\\?\"; const string UncPrefix = @"\\?\UNC\"; if (pathString.StartsWith(UncPrefix, StringComparison.Ordinal)) { pathString = @"\\" + pathString.Substring(UncPrefix.Length); } else if (pathString.StartsWith(PathPrefix, StringComparison.Ordinal)) { pathString = pathString.Substring(PathPrefix.Length); } return pathString; } } public static void ThrowLastWin32Exception(string message) { throw new Win32Exception(Marshal.GetLastWin32Error(), message); } [DllImport("kernel32.dll", SetLastError = true)] public static extern SafeFileHandle OpenProcess( ProcessAccessFlags processAccess, bool bInheritHandle, int processId); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetExitCodeProcess(SafeFileHandle hProcess, out uint lpExitCode); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern SafeFileHandle CreateFile( [In] string lpFileName, [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess, FileShare dwShareMode, [In] IntPtr lpSecurityAttributes, [MarshalAs(UnmanagedType.U4)]FileMode dwCreationDisposition, [MarshalAs(UnmanagedType.U4)]FileAttributes dwFlagsAndAttributes, [In] IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern int GetFinalPathNameByHandle( SafeFileHandle hFile, [Out] StringBuilder lpszFilePath, int cchFilePath, int dwFlags); } } ================================================ FILE: GVFS/GVFS.Common/NativeMethods.cs ================================================ using Microsoft.Win32.SafeHandles; using System; using System.ComponentModel; using System.IO; using System.Runtime.InteropServices; using System.Text; namespace GVFS.Common { public static partial class NativeMethods { private const uint EVENT_TRACE_CONTROL_FLUSH = 3; private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; private const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C; private const uint FSCTL_GET_REPARSE_POINT = 0x000900a8; private const int ReparseDataPathBufferLength = 1000; private const int ERROR_FILE_NOT_FOUND = 0x2; private const int ERROR_PATH_NOT_FOUND = 0x3; [Flags] public enum MoveFileFlags : uint { MoveFileReplaceExisting = 0x00000001, // MOVEFILE_REPLACE_EXISTING MoveFileCopyAllowed = 0x00000002, // MOVEFILE_COPY_ALLOWED MoveFileDelayUntilReboot = 0x00000004, // MOVEFILE_DELAY_UNTIL_REBOOT MoveFileWriteThrough = 0x00000008, // MOVEFILE_WRITE_THROUGH MoveFileCreateHardlink = 0x00000010, // MOVEFILE_CREATE_HARDLINK MoveFileFailIfNotTrackable = 0x00000020, // MOVEFILE_FAIL_IF_NOT_TRACKABLE } [Flags] public enum FileSystemFlags : uint { FILE_RETURNS_CLEANUP_RESULT_INFO = 0x00000200 } public static void FlushFileBuffers(string path) { using (SafeFileHandle fileHandle = CreateFile( path, FileAccess.GENERIC_WRITE, FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, FileAttributes.FILE_ATTRIBUTE_NORMAL, IntPtr.Zero)) { if (fileHandle.IsInvalid) { ThrowLastWin32Exception($"Invalid handle for '{path}'"); } if (!FlushFileBuffers(fileHandle)) { ThrowLastWin32Exception($"Failed to flush buffers for '{path}'"); } } } public static bool IsFeatureSupportedByVolume(string volumeRoot, FileSystemFlags flags) { uint volumeSerialNumber; uint maximumComponentLength; uint fileSystemFlags; if (!GetVolumeInformation( volumeRoot, null, 0, out volumeSerialNumber, out maximumComponentLength, out fileSystemFlags, null, 0)) { ThrowLastWin32Exception($"Failed to get volume information for '{volumeRoot}'"); } return (fileSystemFlags & (uint)flags) == (uint)flags; } public static uint FlushTraceLogger(string sessionName, string sessionGuid, out string logfileName) { EventTraceProperties properties = new EventTraceProperties(); properties.Wnode.BufferSize = (uint)Marshal.SizeOf(properties); properties.Wnode.Guid = new Guid(sessionGuid); properties.LoggerNameOffset = (uint)Marshal.OffsetOf(typeof(EventTraceProperties), "LoggerName"); properties.LogFileNameOffset = (uint)Marshal.OffsetOf(typeof(EventTraceProperties), "LogFileName"); uint result = ControlTrace(0, sessionName, ref properties, EVENT_TRACE_CONTROL_FLUSH); logfileName = properties.LogFileName; return result; } public static void MoveFile(string existingFileName, string newFileName, MoveFileFlags flags) { if (!MoveFileEx(existingFileName, newFileName, (uint)flags)) { ThrowLastWin32Exception($"Failed to move '{existingFileName}' to '{newFileName}'"); } } public static void SetDirectoryLastWriteTime(string path, DateTime lastWriteTime, out bool directoryExists) { // We can't use Directory.SetLastWriteTime as it requests GENERIC_WRITE access // which will fail for directory placeholders. The only access requried by SetFileTime // is FILE_WRITE_ATTRIBUTES (which ProjFS does allow for placeholders) using (SafeFileHandle handle = CreateFile( path, FileAccess.FILE_WRITE_ATTRIBUTES, FileShare.ReadWrite | FileShare.Delete, IntPtr.Zero, FileMode.Open, FileAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero)) { if (handle.IsInvalid) { int error = Marshal.GetLastWin32Error(); if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) { directoryExists = false; return; } throw new Win32Exception(error, $"{nameof(SetDirectoryLastWriteTime)}: Failed to open handle for '{path}'"); } // SetFileTime will not update times with value 0 long creationFileTime = 0; long lastAccessFileTime = 0; long lastWriteFileTime = lastWriteTime.ToFileTime(); if (!SetFileTime(handle, ref creationFileTime, ref lastAccessFileTime, ref lastWriteFileTime)) { ThrowLastWin32Exception($"{nameof(SetDirectoryLastWriteTime)}: Failed to update last write time for '{path}'"); } } directoryExists = true; return; } /// /// Get the build number of the OS /// /// Build number /// /// For this method to work correctly, the calling application must have a manifest file /// that indicates the application supports Windows 10. /// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms724451(v=vs.85).aspx for details /// public static uint GetWindowsBuildNumber() { OSVersionInfo versionInfo = new OSVersionInfo(); versionInfo.OSVersionInfoSize = (uint)Marshal.SizeOf(versionInfo); if (!GetVersionEx(ref versionInfo)) { ThrowLastWin32Exception($"Failed to get OS version info"); } return versionInfo.BuildNumber; } public static bool IsSymLink(string path) { using (SafeFileHandle output = CreateFile( path, FileAccess.FILE_READ_ATTRIBUTES, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.FILE_FLAG_BACKUP_SEMANTICS | FileAttributes.FILE_FLAG_OPEN_REPARSE_POINT, IntPtr.Zero)) { if (output.IsInvalid) { ThrowLastWin32Exception($"Invalid handle for '{path}' as symlink"); } REPARSE_DATA_BUFFER reparseData = new REPARSE_DATA_BUFFER(); reparseData.ReparseDataLength = (4 * sizeof(ushort)) + ReparseDataPathBufferLength; uint bytesReturned; if (!DeviceIoControl(output, FSCTL_GET_REPARSE_POINT, IntPtr.Zero, 0, out reparseData, (uint)Marshal.SizeOf(reparseData), out bytesReturned, IntPtr.Zero)) { ThrowLastWin32Exception($"Failed to place reparse point for '{path}'"); } return reparseData.ReparseTag == IO_REPARSE_TAG_SYMLINK || reparseData.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT; } } 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, string newFileName, uint flags); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool FlushFileBuffers(SafeFileHandle hFile); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool GetVolumeInformation( string rootPathName, StringBuilder volumeNameBuffer, int volumeNameSize, out uint volumeSerialNumber, out uint maximumComponentLength, out uint fileSystemFlags, StringBuilder fileSystemNameBuffer, int nFileSystemNameSize); [DllImport("advapi32.dll", EntryPoint = "ControlTraceW", CharSet = CharSet.Unicode)] private static extern uint ControlTrace( [In] ulong sessionHandle, [In] string sessionName, [In, Out] ref EventTraceProperties properties, [In] uint controlCode); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern bool GetVersionEx([In, Out] ref OSVersionInfo versionInfo); // For use with FSCTL_GET_REPARSE_POINT [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool DeviceIoControl( SafeFileHandle hDevice, uint IoControlCode, [In] IntPtr InBuffer, uint nInBufferSize, [Out] out REPARSE_DATA_BUFFER OutBuffer, uint nOutBufferSize, out uint pBytesReturned, [In] IntPtr Overlapped); [DllImport("kernel32.dll")] private static extern ulong GetTickCount64(); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetFileTime( SafeFileHandle hFile, [In] ref long creationTime, [In] ref long lastAccessTime, [In] ref long lastWriteTime); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct REPARSE_DATA_BUFFER { public uint ReparseTag; public ushort ReparseDataLength; public ushort Reserved; public ushort SubstituteNameOffset; public ushort SubstituteNameLength; public ushort PrintNameOffset; public ushort PrintNameLength; [MarshalAs(UnmanagedType.ByValArray, SizeConst = ReparseDataPathBufferLength)] public byte[] PathBuffer; } [StructLayout(LayoutKind.Sequential)] private struct WNodeHeader { public uint BufferSize; public uint ProviderId; public ulong HistoricalContext; public ulong TimeStamp; public Guid Guid; public uint ClientContext; public uint Flags; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct EventTraceProperties { public WNodeHeader Wnode; public uint BufferSize; public uint MinimumBuffers; public uint MaximumBuffers; public uint MaximumFileSize; public uint LogFileMode; public uint FlushTimer; public uint EnableFlags; public int AgeLimit; public uint NumberOfBuffers; public uint FreeBuffers; public uint EventsLost; public uint BuffersWritten; public uint LogBuffersLost; public uint RealTimeBuffersLost; public IntPtr LoggerThreadId; public uint LogFileNameOffset; public uint LoggerNameOffset; // "You can use the maximum session name (1024 characters) and maximum log file name (1024 characters) lengths to calculate the buffer size and offsets if not known" // https://msdn.microsoft.com/en-us/library/windows/desktop/aa363696(v=vs.85).aspx [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] public string LoggerName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] public string LogFileName; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct OSVersionInfo { public uint OSVersionInfoSize; public uint MajorVersion; public uint MinorVersion; public uint BuildNumber; public uint PlatformId; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string CSDVersion; } } } ================================================ FILE: GVFS/GVFS.Common/NetworkStreams/BatchedLooseObjectDeserializer.cs ================================================ using System; using System.IO; using System.Linq; using System.Text; namespace GVFS.Common.NetworkStreams { /// /// Deserializer for concatenated loose objects. /// public class BatchedLooseObjectDeserializer { private const int NumObjectIdBytes = 20; private const int NumObjectHeaderBytes = NumObjectIdBytes + sizeof(long); private static readonly byte[] ExpectedHeader = new byte[] { (byte)'G', (byte)'V', (byte)'F', (byte)'S', (byte)' ', // Magic 1 // Version }; private readonly Stream source; private readonly OnLooseObject onLooseObject; public BatchedLooseObjectDeserializer(Stream source, OnLooseObject onLooseObject) { this.source = source; this.onLooseObject = onLooseObject; } /// /// Invoked when the full content of a single loose object is available. /// public delegate void OnLooseObject(Stream objectStream, string sha1); /// /// Read all the objects from the source stream and call for each. /// /// The total number of objects read public int ProcessObjects() { this.ValidateHeader(); // Start reading objects int numObjectsRead = 0; byte[] curObjectHeader = new byte[NumObjectHeaderBytes]; while (true) { bool keepReading = this.ShouldContinueReading(curObjectHeader); if (!keepReading) { break; } // Get the length long curLength = BitConverter.ToInt64(curObjectHeader, NumObjectIdBytes); // Handle the loose object using (Stream rawObjectData = new RestrictedStream(this.source, curLength)) { string objectId = SHA1Util.HexStringFromBytes(curObjectHeader, NumObjectIdBytes); if (objectId.Equals(GVFSConstants.AllZeroSha)) { throw new RetryableException("Received all-zero SHA before end of stream"); } this.onLooseObject(rawObjectData, objectId); numObjectsRead++; } } return numObjectsRead; } /// /// Parse the current object header to check if we've reached the end. /// /// true if the end of the stream has been reached, false if not private bool ShouldContinueReading(byte[] curObjectHeader) { int totalBytes = StreamUtil.TryReadGreedy( this.source, curObjectHeader, 0, curObjectHeader.Length); if (totalBytes == NumObjectHeaderBytes) { // Successful header read return true; } else if (totalBytes == NumObjectIdBytes) { // We may have finished reading all the objects for (int i = 0; i < NumObjectIdBytes; i++) { if (curObjectHeader[i] != 0) { throw new RetryableException( string.Format( "Reached end of stream before we got the expected zero-object ID Buffer: {0}", SHA1Util.HexStringFromBytes(curObjectHeader))); } } return false; } else { throw new RetryableException( string.Format( "Reached end of stream before expected {0} or {1} bytes. Got {2}. Buffer: {3}", NumObjectHeaderBytes, NumObjectIdBytes, totalBytes, SHA1Util.HexStringFromBytes(curObjectHeader))); } } private void ValidateHeader() { byte[] headerBuf = new byte[ExpectedHeader.Length]; StreamUtil.TryReadGreedy(this.source, headerBuf, 0, headerBuf.Length); if (!headerBuf.SequenceEqual(ExpectedHeader)) { throw new InvalidDataException("Unexpected header: " + Encoding.UTF8.GetString(headerBuf)); } } } } ================================================ FILE: GVFS/GVFS.Common/NetworkStreams/PrefetchPacksDeserializer.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace GVFS.Common.NetworkStreams { /// /// Deserializer for packs and indexes for prefetch packs. /// public class PrefetchPacksDeserializer { private const int NumPackHeaderBytes = 3 * sizeof(long); private static readonly byte[] PrefetchPackExpectedHeader = new byte[] { (byte)'G', (byte)'P', (byte)'R', (byte)'E', (byte)' ', // Magic 1 // Version }; private readonly Stream source; public PrefetchPacksDeserializer(Stream source) { this.source = source; } /// /// Read all the packs and indexes from the source stream and return a for each pack /// and index. Caller must consume pack stream fully before the index stream. /// public IEnumerable EnumeratePacks() { this.ValidateHeader(); byte[] buffer = new byte[NumPackHeaderBytes]; int packCount = this.ReadPackCount(buffer); for (int i = 0; i < packCount; i++) { long timestamp; long packLength; long indexLength; this.ReadPackHeader(buffer, out timestamp, out packLength, out indexLength); using (Stream packData = new RestrictedStream(this.source, packLength)) using (Stream indexData = indexLength > 0 ? new RestrictedStream(this.source, indexLength) : null) { yield return new PackAndIndex(packData, indexData, timestamp); } } } /// /// Read the ushort pack count /// private ushort ReadPackCount(byte[] buffer) { StreamUtil.TryReadGreedy(this.source, buffer, 0, 2); return BitConverter.ToUInt16(buffer, 0); } /// /// Parse the current pack header /// private void ReadPackHeader( byte[] buffer, out long timestamp, out long packLength, out long indexLength) { int totalBytes = StreamUtil.TryReadGreedy( this.source, buffer, 0, NumPackHeaderBytes); if (totalBytes == NumPackHeaderBytes) { timestamp = BitConverter.ToInt64(buffer, 0); packLength = BitConverter.ToInt64(buffer, 8); indexLength = BitConverter.ToInt64(buffer, 16); } else { throw new RetryableException( string.Format( "Reached end of stream before expected {0} bytes. Got {1}. Buffer: {2}", NumPackHeaderBytes, totalBytes, SHA1Util.HexStringFromBytes(buffer))); } } private void ValidateHeader() { byte[] headerBuf = new byte[PrefetchPackExpectedHeader.Length]; StreamUtil.TryReadGreedy(this.source, headerBuf, 0, headerBuf.Length); if (!headerBuf.SequenceEqual(PrefetchPackExpectedHeader)) { throw new InvalidDataException("Unexpected header: " + Encoding.UTF8.GetString(headerBuf)); } } public class PackAndIndex { public PackAndIndex(Stream packStream, Stream idxStream, long timestamp) { this.PackStream = packStream; this.IndexStream = idxStream; this.Timestamp = timestamp; this.UniqueId = Guid.NewGuid().ToString("N"); } public Stream PackStream { get; } public Stream IndexStream { get; } public long Timestamp { get; } public string UniqueId { get; } } } } ================================================ FILE: GVFS/GVFS.Common/NetworkStreams/RestrictedStream.cs ================================================ using System; using System.IO; namespace GVFS.Common.NetworkStreams { /// /// Stream wrapper for a length-limited subview of another stream. /// internal class RestrictedStream : Stream { private readonly Stream stream; private readonly long length; private readonly bool leaveOpen; private long position; private bool closed; public RestrictedStream(Stream stream, long length, bool leaveOpen = true) { this.stream = stream; this.length = length; this.leaveOpen = leaveOpen; } public override bool CanRead { get { return true; } } public override bool CanSeek { get { return this.stream.CanSeek; } } public override bool CanWrite { get { return false; } } public override long Length { get { return this.length; } } public override long Position { get { return this.position; } set { this.Seek(value, SeekOrigin.Begin); } } public override void Close() { if (!this.closed) { this.closed = true; if (!this.leaveOpen) { this.stream.Close(); } } base.Close(); } public override int Read(byte[] buffer, int offset, int count) { int bytesToRead = (int)(Math.Min(this.position + count, this.length) - this.position); // Some streams like HttpContent.ReadOnlyStream throw InvalidOperationException // when reading 0 bytes from huge streams. If that changes we can remove this check. if (bytesToRead == 0) { return 0; } int toReturn = this.stream.Read(buffer, offset, bytesToRead); this.position += toReturn; return toReturn; } public override long Seek(long offset, SeekOrigin origin) { if (!this.stream.CanSeek) { throw new InvalidOperationException(); } long newPosition; switch (origin) { case SeekOrigin.Begin: newPosition = offset; break; case SeekOrigin.Current: newPosition = this.position + offset; break; case SeekOrigin.End: newPosition = this.length + offset; break; default: throw new InvalidOperationException(); } newPosition = Math.Max(Math.Min(this.length, newPosition), 0); this.stream.Seek(newPosition - this.position, SeekOrigin.Current); this.position = newPosition; return newPosition; } public override void Flush() { throw new NotSupportedException(); } public override void SetLength(long value) { throw new NotSupportedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } } } ================================================ FILE: GVFS/GVFS.Common/OrgInfoApiClient.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Web; namespace GVFS.Common { /// /// Class that handles communication with a server that contains version information. /// public class OrgInfoApiClient { private const string VersionApi = "/api/GetLatestVersion"; private HttpClient client; private string baseUrl; public OrgInfoApiClient(HttpClient client, string baseUrl) { this.client = client; this.baseUrl = baseUrl; } private string VersionUrl { get { return this.baseUrl + VersionApi; } } public Version QueryNewestVersion(string orgName, string platform, string ring) { Dictionary queryParams = new Dictionary() { { "Organization", orgName }, { "Platform", platform }, { "Ring", ring }, }; string responseString = this.client.GetStringAsync(this.ConstructRequest(this.VersionUrl, queryParams)).GetAwaiter().GetResult(); VersionResponse versionResponse = VersionResponse.FromJsonString(responseString); if (string.IsNullOrEmpty(versionResponse.Version)) { return null; } return new Version(versionResponse.Version); } private string ConstructRequest(string baseUrl, Dictionary queryParams) { StringBuilder sb = new StringBuilder(baseUrl); if (queryParams.Any()) { sb.Append("?"); } bool isFirst = true; foreach (KeyValuePair kvp in queryParams) { if (!isFirst) { sb.Append("&"); } isFirst = false; sb.Append($"{HttpUtility.UrlEncode(kvp.Key)}={HttpUtility.UrlEncode(kvp.Value)}"); } return sb.ToString(); } } } ================================================ FILE: GVFS/GVFS.Common/Paths.Shared.cs ================================================ using System; using System.IO; using System.Linq; namespace GVFS.Common { public static class Paths { public static string GetGitEnlistmentRoot(string directory) { return GetRoot(directory, GVFSConstants.DotGit.Root); } public static string GetRoot(string startingDirectory, string rootName) { startingDirectory = startingDirectory.TrimEnd(Path.DirectorySeparatorChar); DirectoryInfo dirInfo; try { dirInfo = new DirectoryInfo(startingDirectory); } catch (Exception) { return null; } while (dirInfo != null) { if (dirInfo.Exists) { DirectoryInfo[] dotGVFSDirs = new DirectoryInfo[0]; try { dotGVFSDirs = dirInfo.GetDirectories(rootName); } catch (IOException) { } if (dotGVFSDirs.Count() == 1) { return dirInfo.FullName; } } dirInfo = dirInfo.Parent; } return null; } public static string ConvertPathToGitFormat(string path) { return path.Replace(Path.DirectorySeparatorChar, GVFSConstants.GitPathSeparator); } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/BlobPrefetcher.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.Prefetch.Git; using GVFS.Common.Prefetch.Pipeline; using GVFS.Common.Tracing; using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; namespace GVFS.Common.Prefetch { public class BlobPrefetcher { protected const string RefsHeadsGitPath = "refs/heads/"; protected readonly Enlistment Enlistment; protected readonly GitObjectsHttpRequestor ObjectRequestor; protected readonly GitObjects GitObjects; protected readonly ITracer Tracer; protected readonly int ChunkSize; protected readonly int SearchThreadCount; protected readonly int DownloadThreadCount; protected readonly int IndexThreadCount; protected readonly bool SkipConfigUpdate; private const string AreaPath = nameof(BlobPrefetcher); private static string pathSeparatorString = Path.DirectorySeparatorChar.ToString(); private FileBasedDictionary lastPrefetchArgs; public BlobPrefetcher( ITracer tracer, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor, int chunkSize, int searchThreadCount, int downloadThreadCount, int indexThreadCount) : this(tracer, enlistment, objectRequestor, null, null, null, chunkSize, searchThreadCount, downloadThreadCount, indexThreadCount) { } public BlobPrefetcher( ITracer tracer, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor, List fileList, List folderList, FileBasedDictionary lastPrefetchArgs, int chunkSize, int searchThreadCount, int downloadThreadCount, int indexThreadCount) { this.SearchThreadCount = searchThreadCount; this.DownloadThreadCount = downloadThreadCount; this.IndexThreadCount = indexThreadCount; this.ChunkSize = chunkSize; this.Tracer = tracer; this.Enlistment = enlistment; this.ObjectRequestor = objectRequestor; this.GitObjects = new PrefetchGitObjects(tracer, enlistment, this.ObjectRequestor); this.FileList = fileList ?? new List(); this.FolderList = folderList ?? new List(); this.lastPrefetchArgs = lastPrefetchArgs; // We never want to update config settings for a GVFSEnlistment this.SkipConfigUpdate = enlistment is GVFSEnlistment; } public bool HasFailures { get; protected set; } public List FileList { get; } public List FolderList { get; } public static bool TryLoadFolderList(Enlistment enlistment, string foldersInput, string folderListFile, List folderListOutput, bool readListFromStdIn, out string error) { return TryLoadFileOrFolderList( enlistment, foldersInput, folderListFile, isFolder: true, readListFromStdIn: readListFromStdIn, output: folderListOutput, elementValidationFunction: s => s.Contains("*") ? "Wildcards are not supported for folders. Invalid entry: " + s : null, error: out error); } public static bool TryLoadFileList(Enlistment enlistment, string filesInput, string filesListFile, List fileListOutput, bool readListFromStdIn, out string error) { return TryLoadFileOrFolderList( enlistment, filesInput, filesListFile, readListFromStdIn: readListFromStdIn, isFolder: false, output: fileListOutput, elementValidationFunction: s => { if (s.IndexOf('*', 1) != -1) { return "Only prefix wildcards are supported. Invalid entry: " + s; } if (s.EndsWith(GVFSConstants.GitPathSeparatorString) || s.EndsWith(pathSeparatorString)) { return "Folders are not allowed in the file list. Invalid entry: " + s; } return null; }, error: out error); } public static bool IsNoopPrefetch( ITracer tracer, FileBasedDictionary lastPrefetchArgs, string commitId, List files, List folders, bool hydrateFilesAfterDownload) { if (lastPrefetchArgs != null && lastPrefetchArgs.TryGetValue(PrefetchArgs.CommitId, out string lastCommitId) && lastPrefetchArgs.TryGetValue(PrefetchArgs.Files, out string lastFilesString) && lastPrefetchArgs.TryGetValue(PrefetchArgs.Folders, out string lastFoldersString) && lastPrefetchArgs.TryGetValue(PrefetchArgs.Hydrate, out string lastHydrateString)) { string newFilesString = JsonConvert.SerializeObject(files); string newFoldersString = JsonConvert.SerializeObject(folders); bool isNoop = commitId == lastCommitId && hydrateFilesAfterDownload.ToString() == lastHydrateString && newFilesString == lastFilesString && newFoldersString == lastFoldersString; tracer.RelatedEvent( EventLevel.Informational, "BlobPrefetcher.IsNoopPrefetch", new EventMetadata { { "Last" + PrefetchArgs.CommitId, lastCommitId }, { "Last" + PrefetchArgs.Files, lastFilesString }, { "Last" + PrefetchArgs.Folders, lastFoldersString }, { "Last" + PrefetchArgs.Hydrate, lastHydrateString }, { "New" + PrefetchArgs.CommitId, commitId }, { "New" + PrefetchArgs.Files, newFilesString }, { "New" + PrefetchArgs.Folders, newFoldersString }, { "New" + PrefetchArgs.Hydrate, hydrateFilesAfterDownload.ToString() }, { "Result", isNoop }, }); return isNoop; } return false; } public static void AppendToNewlineSeparatedFile(string filename, string newContent) { AppendToNewlineSeparatedFile(new PhysicalFileSystem(), filename, newContent); } public static void AppendToNewlineSeparatedFile(PhysicalFileSystem fileSystem, string filename, string newContent) { using (Stream fileStream = fileSystem.OpenFileStream(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, false)) { using (StreamReader reader = new StreamReader(fileStream)) using (StreamWriter writer = new StreamWriter(fileStream)) { long position = reader.BaseStream.Seek(0, SeekOrigin.End); if (position > 0) { reader.BaseStream.Seek(position - 1, SeekOrigin.Begin); } string lastCharacter = reader.ReadToEnd(); if (lastCharacter != "\n" && lastCharacter != string.Empty) { writer.Write("\n"); } writer.Write(newContent.Trim()); writer.Write("\n"); } fileStream.Close(); } } /// A specific branch to filter for, or null for all branches returned from info/refs public virtual void Prefetch(string branchOrCommit, bool isBranch) { int matchedBlobCount; int downloadedBlobCount; int hydratedFileCount; this.PrefetchWithStats(branchOrCommit, isBranch, false, out matchedBlobCount, out downloadedBlobCount, out hydratedFileCount); } public void PrefetchWithStats( string branchOrCommit, bool isBranch, bool hydrateFilesAfterDownload, out int matchedBlobCount, out int downloadedBlobCount, out int hydratedFileCount) { matchedBlobCount = 0; downloadedBlobCount = 0; hydratedFileCount = 0; if (string.IsNullOrWhiteSpace(branchOrCommit)) { throw new FetchException("Must specify branch or commit to fetch"); } GitRefs refs = null; string commitToFetch; if (isBranch) { refs = this.ObjectRequestor.QueryInfoRefs(branchOrCommit); if (refs == null) { throw new FetchException("Could not query info/refs from: {0}", this.Enlistment.RepoUrl); } else if (refs.Count == 0) { throw new FetchException("Could not find branch {0} in info/refs from: {1}", branchOrCommit, this.Enlistment.RepoUrl); } commitToFetch = refs.GetTipCommitId(branchOrCommit); } else { commitToFetch = branchOrCommit; } this.DownloadMissingCommit(commitToFetch, this.GitObjects); // For FastFetch only, examine the shallow file to determine the previous commit that had been fetched string shallowFile = Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Shallow); string previousCommit = null; // Use the shallow file to find a recent commit to diff against to try and reduce the number of SHAs to check. if (File.Exists(shallowFile)) { previousCommit = File.ReadAllLines(shallowFile).Where(line => !string.IsNullOrWhiteSpace(line)).LastOrDefault(); if (string.IsNullOrWhiteSpace(previousCommit)) { this.Tracer.RelatedError("Shallow file exists, but contains no valid SHAs."); this.HasFailures = true; return; } } BlockingCollection availableBlobs = new BlockingCollection(); //// // First create the pipeline // // diff ---> blobFinder ---> downloader ---> packIndexer // | | | | // ------------------------------------------------------> fileHydrator //// // diff // Inputs: // * files/folders // * commit id // Outputs: // * RequiredBlobs (property): Blob ids required to satisfy desired paths // * FileAddOperations (property): Repo-relative paths corresponding to those blob ids DiffHelper diff = new DiffHelper(this.Tracer, this.Enlistment, this.FileList, this.FolderList, includeSymLinks: false); // blobFinder // Inputs: // * requiredBlobs (in param): Blob ids from output of `diff` // Outputs: // * availableBlobs (out param): Locally available blob ids (shared between `blobFinder`, `downloader`, and `packIndexer`, all add blob ids to the list as they are locally available) // * MissingBlobs (property): Blob ids that are missing and need to be downloaded // * AvailableBlobs (property): Same as availableBlobs FindBlobsStage blobFinder = new FindBlobsStage(this.SearchThreadCount, diff.RequiredBlobs, availableBlobs, this.Tracer, this.Enlistment); // downloader // Inputs: // * missingBlobs (in param): Blob ids from output of `blobFinder` // Outputs: // * availableBlobs (out param): Loose objects that have completed downloading (shared between `blobFinder`, `downloader`, and `packIndexer`, all add blob ids to the list as they are locally available) // * AvailableObjects (property): Same as availableBlobs // * AvailablePacks (property): Packfiles that have completed downloading BatchObjectDownloadStage downloader = new BatchObjectDownloadStage(this.DownloadThreadCount, this.ChunkSize, blobFinder.MissingBlobs, availableBlobs, this.Tracer, this.Enlistment, this.ObjectRequestor, this.GitObjects); // packIndexer // Inputs: // * availablePacks (in param): Packfiles that have completed downloading from output of `downloader` // Outputs: // * availableBlobs (out param): Blobs that have completed downloading and indexing (shared between `blobFinder`, `downloader`, and `packIndexer`, all add blob ids to the list as they are locally available) IndexPackStage packIndexer = new IndexPackStage(this.IndexThreadCount, downloader.AvailablePacks, availableBlobs, this.Tracer, this.GitObjects); // fileHydrator // Inputs: // * workingDirectoryRoot (in param): the root of the working directory where hydration takes place // * blobIdsToPaths (in param): paths of all blob ids that need to be hydrated from output of `diff` // * availableBlobs (in param): blobs id that are available locally, from whatever source // Outputs: // * Hydrated files on disk. HydrateFilesStage fileHydrator = new HydrateFilesStage(Environment.ProcessorCount * 2, this.Enlistment.WorkingDirectoryRoot, diff.FileAddOperations, availableBlobs, this.Tracer); // All the stages of the pipeline are created and wired up, now kick them off in the proper sequence ThreadStart performDiff = () => { diff.PerformDiff(previousCommit, commitToFetch); this.HasFailures |= diff.HasFailures; }; if (hydrateFilesAfterDownload) { // Call synchronously to ensure that diff.FileAddOperations // is completely populated when fileHydrator starts performDiff(); } else { new Thread(performDiff).Start(); } blobFinder.Start(); downloader.Start(); if (hydrateFilesAfterDownload) { fileHydrator.Start(); } // If indexing happens during searching, searching progressively gets slower, so wait on searching before indexing. blobFinder.WaitForCompletion(); this.HasFailures |= blobFinder.HasFailures; packIndexer.Start(); downloader.WaitForCompletion(); this.HasFailures |= downloader.HasFailures; packIndexer.WaitForCompletion(); this.HasFailures |= packIndexer.HasFailures; availableBlobs.CompleteAdding(); if (hydrateFilesAfterDownload) { fileHydrator.WaitForCompletion(); this.HasFailures |= fileHydrator.HasFailures; } matchedBlobCount = blobFinder.AvailableBlobCount + blobFinder.MissingBlobCount; downloadedBlobCount = blobFinder.MissingBlobCount; hydratedFileCount = fileHydrator.ReadFileCount; if (!this.SkipConfigUpdate && !this.HasFailures) { this.UpdateRefs(branchOrCommit, isBranch, refs); if (isBranch) { this.HasFailures |= !this.UpdateRefSpec(this.Tracer, this.Enlistment, branchOrCommit, refs); } } if (!this.HasFailures) { this.SavePrefetchArgs(commitToFetch, hydrateFilesAfterDownload); } } protected bool UpdateRefSpec(ITracer tracer, Enlistment enlistment, string branchOrCommit, GitRefs refs) { using (ITracer activity = tracer.StartActivity("UpdateRefSpec", EventLevel.Informational, Keywords.Telemetry, metadata: null)) { const string OriginRefMapSettingName = "remote.origin.fetch"; // We must update the refspec to get proper "git pull" functionality. string localBranch = branchOrCommit.StartsWith(RefsHeadsGitPath) ? branchOrCommit : (RefsHeadsGitPath + branchOrCommit); string remoteBranch = refs.GetBranchRefPairs().Single().Key; string refSpec = "+" + localBranch + ":" + remoteBranch; GitProcess git = new GitProcess(enlistment); // Replace all ref-specs this // * ensures the default refspec (remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*) is removed which avoids some "git fetch/pull" failures // * gives added "git fetch" performance since git will only fetch the branch provided in the refspec. GitProcess.Result setResult = git.SetInLocalConfig(OriginRefMapSettingName, refSpec, replaceAll: true); if (setResult.ExitCodeIsFailure) { activity.RelatedError("Could not update ref spec to {0}: {1}", refSpec, setResult.Errors); return false; } } return true; } /// /// * Updates any remote branch (N/A for fetch of detached commit) /// * Updates shallow file /// protected virtual void UpdateRefs(string branchOrCommit, bool isBranch, GitRefs refs) { string commitSha = null; if (isBranch) { KeyValuePair remoteRef = refs.GetBranchRefPairs().Single(); string remoteBranch = remoteRef.Key; commitSha = remoteRef.Value; this.HasFailures |= !this.UpdateRef(this.Tracer, remoteBranch, commitSha); } else { commitSha = branchOrCommit; } // Update shallow file to ensure this is a valid shallow repo AppendToNewlineSeparatedFile(Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Shallow), commitSha); } protected bool UpdateRef(ITracer tracer, string refName, string targetCommitish) { EventMetadata metadata = new EventMetadata(); metadata.Add("RefName", refName); metadata.Add("TargetCommitish", targetCommitish); using (ITracer activity = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata)) { GitProcess gitProcess = new GitProcess(this.Enlistment); GitProcess.Result result = null; if (this.IsSymbolicRef(targetCommitish)) { // Using update-ref with a branch name will leave a SHA in the ref file which detaches HEAD, so use symbolic-ref instead. result = gitProcess.UpdateBranchSymbolicRef(refName, targetCommitish); } else { result = gitProcess.UpdateBranchSha(refName, targetCommitish); } if (result.ExitCodeIsFailure) { activity.RelatedError(result.Errors); return false; } return true; } } protected void DownloadMissingCommit(string commitSha, GitObjects gitObjects) { EventMetadata startMetadata = new EventMetadata(); startMetadata.Add("CommitSha", commitSha); using (ITracer activity = this.Tracer.StartActivity("DownloadTrees", EventLevel.Informational, Keywords.Telemetry, startMetadata)) { using (LibGit2Repo repo = new LibGit2Repo(this.Tracer, this.Enlistment.WorkingDirectoryBackingRoot)) { if (!repo.ObjectExists(commitSha)) { if (!gitObjects.TryDownloadCommit(commitSha)) { EventMetadata metadata = new EventMetadata(); metadata.Add("ObjectsEndpointUrl", this.ObjectRequestor.CacheServer.ObjectsEndpointUrl); activity.RelatedError(metadata, "Could not download commits"); throw new FetchException("Could not download commits from {0}", this.ObjectRequestor.CacheServer.ObjectsEndpointUrl); } } } } } private static IEnumerable GetFilesFromVerbParameter(string valueString) { return valueString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); } private static IEnumerable GetFilesFromFile(string fileName, out string error) { error = null; if (string.IsNullOrWhiteSpace(fileName)) { return Enumerable.Empty(); } if (!File.Exists(fileName)) { error = string.Format("Could not find '{0}' list file.", fileName); return Enumerable.Empty(); } return File.ReadAllLines(fileName) .Select(line => line.Trim()); } private static IEnumerable GetFilesFromStdin(bool shouldRead) { if (!shouldRead) { yield break; } string line; while ((line = Console.In.ReadLine()) != null) { yield return line.Trim(); } } private static bool TryLoadFileOrFolderList(Enlistment enlistment, string valueString, string listFileName, bool readListFromStdIn, bool isFolder, List output, Func elementValidationFunction, out string error) { output.AddRange( GetFilesFromVerbParameter(valueString) .Union(GetFilesFromFile(listFileName, out string fileReadError)) .Union(GetFilesFromStdin(readListFromStdIn)) .Where(path => !path.StartsWith(GVFSConstants.GitCommentSign.ToString())) .Where(path => !string.IsNullOrWhiteSpace(path)) .Select(path => BlobPrefetcher.ToFilterPath(path, isFolder: isFolder))); if (!string.IsNullOrWhiteSpace(fileReadError)) { error = fileReadError; return false; } string[] errorArray = output .Select(elementValidationFunction) .Where(s => !string.IsNullOrWhiteSpace(s)) .ToArray(); if (errorArray != null && errorArray.Length > 0) { error = string.Join("\n", errorArray); return false; } error = null; return true; } private static string ToFilterPath(string path, bool isFolder) { string filterPath = path.StartsWith("*") ? path : path.Replace(GVFSConstants.GitPathSeparator, Path.DirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar); if (isFolder && filterPath.Length > 0 && !filterPath.EndsWith(pathSeparatorString)) { filterPath += pathSeparatorString; } return filterPath; } private bool IsSymbolicRef(string targetCommitish) { return targetCommitish.StartsWith("refs/", GVFSPlatform.Instance.Constants.PathComparison); } private void SavePrefetchArgs(string targetCommit, bool hydrate) { if (this.lastPrefetchArgs != null) { this.lastPrefetchArgs.SetValuesAndFlush( new[] { new KeyValuePair(PrefetchArgs.CommitId, targetCommit), new KeyValuePair(PrefetchArgs.Files, JsonConvert.SerializeObject(this.FileList)), new KeyValuePair(PrefetchArgs.Folders, JsonConvert.SerializeObject(this.FolderList)), new KeyValuePair(PrefetchArgs.Hydrate, hydrate.ToString()), }); } } public class FetchException : Exception { public FetchException(string format, params object[] args) : base(string.Format(format, args)) { } } private static class PrefetchArgs { public const string CommitId = "CommitId"; public const string Files = "Files"; public const string Folders = "Folders"; public const string Hydrate = "Hydrate"; } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/Git/DiffHelper.cs ================================================ using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; namespace GVFS.Common.Prefetch.Git { public class DiffHelper { private const string AreaPath = nameof(DiffHelper); private ITracer tracer; private HashSet exactFileList; private List patternList; private List folderList; private HashSet filesAdded = new HashSet(GVFSPlatform.Instance.Constants.PathComparer); private HashSet stagedDirectoryOperations = new HashSet(new DiffTreeByNameComparer()); private HashSet stagedFileDeletes = new HashSet(GVFSPlatform.Instance.Constants.PathComparer); private Enlistment enlistment; private GitProcess git; public DiffHelper(ITracer tracer, Enlistment enlistment, IEnumerable fileList, IEnumerable folderList, bool includeSymLinks) : this(tracer, enlistment, new GitProcess(enlistment), fileList, folderList, includeSymLinks) { } public DiffHelper(ITracer tracer, Enlistment enlistment, GitProcess git, IEnumerable fileList, IEnumerable folderList, bool includeSymLinks) { this.tracer = tracer; this.exactFileList = new HashSet(fileList.Where(x => !x.StartsWith("*")), GVFSPlatform.Instance.Constants.PathComparer); this.patternList = fileList.Where(x => x.StartsWith("*")).ToList(); this.folderList = new List(folderList); this.enlistment = enlistment; this.git = git; this.ShouldIncludeSymLinks = includeSymLinks; this.DirectoryOperations = new ConcurrentQueue(); this.FileDeleteOperations = new ConcurrentQueue(); this.FileAddOperations = new ConcurrentDictionary>(GVFSPlatform.Instance.Constants.PathComparer); this.RequiredBlobs = new BlockingCollection(); } public bool ShouldIncludeSymLinks { get; set; } public bool HasFailures { get; private set; } public ConcurrentQueue DirectoryOperations { get; } public ConcurrentQueue FileDeleteOperations { get; } /// /// Mapping from available sha to filenames where blob should be written /// public ConcurrentDictionary> FileAddOperations { get; } /// /// Blobs required to perform a checkout of the destination /// public BlockingCollection RequiredBlobs { get; } public int TotalDirectoryOperations { get { return this.stagedDirectoryOperations.Count; } } public int TotalFileDeletes { get { return this.stagedFileDeletes.Count; } } /// /// Returns true if the whole tree was updated /// public bool UpdatedWholeTree { get; internal set; } = false; public void PerformDiff(string targetCommitSha) { string targetTreeSha; string headTreeSha; using (LibGit2Repo repo = new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot)) { targetTreeSha = repo.GetTreeSha(targetCommitSha); headTreeSha = repo.GetTreeSha("HEAD"); } this.PerformDiff(headTreeSha, targetTreeSha); } public void PerformDiff(string sourceTreeSha, string targetTreeSha) { EventMetadata metadata = new EventMetadata(); metadata.Add("TargetTreeSha", targetTreeSha); metadata.Add("HeadTreeSha", sourceTreeSha); using (ITracer activity = this.tracer.StartActivity("PerformDiff", EventLevel.Informational, Keywords.Telemetry, metadata)) { metadata = new EventMetadata(); if (sourceTreeSha == null) { this.UpdatedWholeTree = true; // Nothing is checked out (fresh git init), so we must search the entire tree. GitProcess.Result result = this.git.LsTree( targetTreeSha, line => this.EnqueueOperationsFromLsTreeLine(activity, line), recursive: true, showAllTrees: true); if (result.ExitCodeIsFailure) { this.HasFailures = true; metadata.Add("Errors", result.Errors); metadata.Add("Output", result.Output.Length > 1024 ? result.Output.Substring(1024) : result.Output); } metadata.Add("Operation", "LsTree"); } else { // Diff head and target, determine what needs to be done. GitProcess.Result result = this.git.DiffTree( sourceTreeSha, targetTreeSha, line => this.EnqueueOperationsFromDiffTreeLine(this.tracer, line)); if (result.ExitCodeIsFailure) { this.HasFailures = true; metadata.Add("Errors", result.Errors); metadata.Add("Output", result.Output.Length > 1024 ? result.Output.Substring(1024) : result.Output); } metadata.Add("Operation", "DiffTree"); } this.FlushStagedQueues(); metadata.Add("Success", !this.HasFailures); metadata.Add("DirectoryOperationsCount", this.TotalDirectoryOperations); metadata.Add("FileDeleteOperationsCount", this.TotalFileDeletes); metadata.Add("RequiredBlobsCount", this.RequiredBlobs.Count); metadata.Add("FileAddOperationsCount", this.FileAddOperations.Sum(kvp => kvp.Value.Count)); activity.Stop(metadata); } } public void ParseDiffFile(string filename) { using (ITracer activity = this.tracer.StartActivity("PerformDiff", EventLevel.Informational)) { using (StreamReader file = new StreamReader(File.OpenRead(filename))) { while (!file.EndOfStream) { this.EnqueueOperationsFromDiffTreeLine(activity, file.ReadLine()); } } this.FlushStagedQueues(); } } private void FlushStagedQueues() { using (ITracer activity = this.tracer.StartActivity("FlushStagedQueues", EventLevel.Informational)) { HashSet deletedDirectories = new HashSet( this.stagedDirectoryOperations .Where(d => d.Operation == DiffTreeResult.Operations.Delete) .Select(d => d.TargetPath.TrimEnd(Path.DirectorySeparatorChar)), GVFSPlatform.Instance.Constants.PathComparer); foreach (DiffTreeResult result in this.stagedDirectoryOperations) { string parentPath = Path.GetDirectoryName(result.TargetPath.TrimEnd(Path.DirectorySeparatorChar)); if (deletedDirectories.Contains(parentPath)) { if (result.Operation != DiffTreeResult.Operations.Delete) { EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(result.TargetPath), result.TargetPath); metadata.Add(TracingConstants.MessageKey.WarningMessage, "An operation is intended to go inside of a deleted folder"); activity.RelatedError("InvalidOperation", metadata); } } else { this.DirectoryOperations.Enqueue(result); } } foreach (string filePath in this.stagedFileDeletes) { string parentPath = Path.GetDirectoryName(filePath); if (!deletedDirectories.Contains(parentPath)) { this.FileDeleteOperations.Enqueue(filePath); } } this.RequiredBlobs.CompleteAdding(); } } private void EnqueueOperationsFromLsTreeLine(ITracer activity, string line) { DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(line); if (result == null) { this.tracer.RelatedError("Unrecognized ls-tree line: {0}", line); } if (!this.ShouldIncludeResult(result)) { return; } if (result.TargetIsDirectory) { if (!this.stagedDirectoryOperations.Add(result)) { EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(result.TargetPath), result.TargetPath); metadata.Add(TracingConstants.MessageKey.WarningMessage, "File exists in tree with two different cases. Taking the last one."); this.tracer.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); // Since we match only on filename, re-adding is the easiest way to update the set. this.stagedDirectoryOperations.Remove(result); this.stagedDirectoryOperations.Add(result); } } else { this.EnqueueFileAddOperation(activity, result); } } private void EnqueueOperationsFromDiffTreeLine(ITracer activity, string line) { if (!line.StartsWith(":")) { // Diff-tree starts with metadata we can ignore. // Real diff lines always start with a colon return; } DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(line); if (!this.ShouldIncludeResult(result)) { return; } if (result.Operation == DiffTreeResult.Operations.Unknown || result.Operation == DiffTreeResult.Operations.Unmerged || result.Operation == DiffTreeResult.Operations.CopyEdit || result.Operation == DiffTreeResult.Operations.RenameEdit) { EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(result.TargetPath), result.TargetPath); metadata.Add(nameof(line), line); activity.RelatedError(metadata, "Unexpected diff operation: " + result.Operation); this.HasFailures = true; return; } // Separate and enqueue all directory operations first. if (result.SourceIsDirectory || result.TargetIsDirectory) { switch (result.Operation) { case DiffTreeResult.Operations.Delete: if (!this.stagedDirectoryOperations.Add(result)) { EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(result.TargetPath), result.TargetPath); metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); } break; case DiffTreeResult.Operations.Add: case DiffTreeResult.Operations.Modify: if (!this.stagedDirectoryOperations.Add(result)) { EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(result.TargetPath), result.TargetPath); metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); // Replace the delete with the add to make sure we don't delete a folder from under ourselves this.stagedDirectoryOperations.Remove(result); this.stagedDirectoryOperations.Add(result); } break; default: activity.RelatedError("Unexpected diff operation from line: {0}", line); break; } } else { switch (result.Operation) { case DiffTreeResult.Operations.Delete: this.EnqueueFileDeleteOperation(activity, result.TargetPath); break; case DiffTreeResult.Operations.Modify: case DiffTreeResult.Operations.Add: this.EnqueueFileAddOperation(activity, result); break; default: activity.RelatedError("Unexpected diff operation from line: {0}", line); break; } } } private bool ShouldIncludeResult(DiffTreeResult blobAdd) { if (blobAdd.TargetIsSymLink && !this.ShouldIncludeSymLinks) { return false; } if (blobAdd.TargetPath == null) { return true; } if (this.exactFileList.Count == 0 && this.patternList.Count == 0 && this.folderList.Count == 0) { return true; } if (this.exactFileList.Contains(blobAdd.TargetPath) || this.patternList.Any(path => blobAdd.TargetPath.EndsWith(path.Substring(1), GVFSPlatform.Instance.Constants.PathComparison))) { return true; } if (this.folderList.Any(path => blobAdd.TargetPath.StartsWith(path, GVFSPlatform.Instance.Constants.PathComparison))) { return true; } return false; } private void EnqueueFileDeleteOperation(ITracer activity, string targetPath) { if (this.filesAdded.Contains(targetPath)) { EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(targetPath), targetPath); metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); return; } this.stagedFileDeletes.Add(targetPath); } /// /// This is not used in a multithreaded method, it doesn't need to be thread-safe /// private void EnqueueFileAddOperation(ITracer activity, DiffTreeResult operation) { // Each filepath should be unique according to GVFSPlatform.Instance.Constants.PathComparer. // If there are duplicates, only the last parsed one should remain. if (!this.filesAdded.Add(operation.TargetPath)) { foreach (KeyValuePair> kvp in this.FileAddOperations) { PathWithMode tempPathWithMode = new PathWithMode(operation.TargetPath, 0x0000); if (kvp.Value.Remove(tempPathWithMode)) { break; } } } if (this.stagedFileDeletes.Remove(operation.TargetPath)) { EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(operation.TargetPath), operation.TargetPath); metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); } this.FileAddOperations.AddOrUpdate( operation.TargetSha, new HashSet { new PathWithMode(operation.TargetPath, operation.TargetMode) }, (key, oldValue) => { oldValue.Add(new PathWithMode(operation.TargetPath, operation.TargetMode)); return oldValue; }); this.RequiredBlobs.Add(operation.TargetSha); } private class DiffTreeByNameComparer : IEqualityComparer { public bool Equals(DiffTreeResult x, DiffTreeResult y) { if (x.TargetPath != null) { if (y.TargetPath != null) { return x.TargetPath.Equals(y.TargetPath, GVFSPlatform.Instance.Constants.PathComparison); } return false; } else { // both null means they're equal return y.TargetPath == null; } } public int GetHashCode(DiffTreeResult obj) { return obj.TargetPath != null ? GVFSPlatform.Instance.Constants.PathComparer.GetHashCode(obj.TargetPath) : 0; } } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/Git/PathWithMode.cs ================================================ using System; namespace GVFS.Common.Prefetch.Git { public class PathWithMode { public PathWithMode(string path, ushort mode) { this.Path = path; this.Mode = mode; } public ushort Mode { get; } public string Path { get; } public override bool Equals(object obj) { PathWithMode x = obj as PathWithMode; if (x == null) { return false; } return x.Path.Equals(this.Path, GVFSPlatform.Instance.Constants.PathComparison); } public override int GetHashCode() { return GVFSPlatform.Instance.Constants.PathComparer.GetHashCode(this.Path); } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/Git/PrefetchGitObjects.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.Tracing; namespace GVFS.Common.Prefetch.Git { public class PrefetchGitObjects : GitObjects { public PrefetchGitObjects(ITracer tracer, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor, PhysicalFileSystem fileSystem = null) : base(tracer, enlistment, objectRequestor, fileSystem) { } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/Pipeline/BatchObjectDownloadStage.cs ================================================ using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.NetworkStreams; using GVFS.Common.Prefetch.Pipeline.Data; using GVFS.Common.Tracing; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; namespace GVFS.Common.Prefetch.Pipeline { /// /// Takes in blocks of object shas, downloads object shas as a pack or loose object, outputs pack locations (if applicable). /// public class BatchObjectDownloadStage : PrefetchPipelineStage { private const string AreaPath = nameof(BatchObjectDownloadStage); private const string DownloadAreaPath = "Download"; private static readonly TimeSpan HeartBeatPeriod = TimeSpan.FromSeconds(20); private readonly DownloadRequestAggregator downloadRequests; private int activeDownloadCount; private ITracer tracer; private Enlistment enlistment; private GitObjectsHttpRequestor objectRequestor; private GitObjects gitObjects; private Timer heartbeat; private long bytesDownloaded = 0; public BatchObjectDownloadStage( int maxParallel, int chunkSize, BlockingCollection missingBlobs, BlockingCollection availableBlobs, ITracer tracer, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor, GitObjects gitObjects) : base(maxParallel) { this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata: null); this.downloadRequests = new DownloadRequestAggregator(missingBlobs, chunkSize); this.enlistment = enlistment; this.objectRequestor = objectRequestor; this.gitObjects = gitObjects; this.AvailablePacks = new BlockingCollection(); this.AvailableObjects = availableBlobs; } public BlockingCollection AvailablePacks { get; } public BlockingCollection AvailableObjects { get; } protected override void DoBeforeWork() { this.heartbeat = new Timer(this.EmitHeartbeat, null, TimeSpan.Zero, HeartBeatPeriod); base.DoBeforeWork(); } protected override void DoWork() { BlobDownloadRequest request; while (this.downloadRequests.TryTake(out request)) { Interlocked.Increment(ref this.activeDownloadCount); EventMetadata metadata = new EventMetadata(); metadata.Add("RequestId", request.RequestId); metadata.Add("ActiveDownloads", this.activeDownloadCount); metadata.Add("NumberOfObjects", request.ObjectIds.Count); using (ITracer activity = this.tracer.StartActivity(DownloadAreaPath, EventLevel.Informational, Keywords.Telemetry, metadata)) { try { HashSet successfulDownloads = new HashSet(StringComparer.OrdinalIgnoreCase); RetryWrapper.InvocationResult result = this.objectRequestor.TryDownloadObjects( () => request.ObjectIds.Except(successfulDownloads), onSuccess: (tryCount, response) => this.WriteObjectOrPack(request, tryCount, response, successfulDownloads), onFailure: RetryWrapper.StandardErrorHandler(activity, request.RequestId, DownloadAreaPath), preferBatchedLooseObjects: true); if (!result.Succeeded) { this.HasFailures = true; } metadata.Add("Success", result.Succeeded); metadata.Add("AttemptNumber", result.Attempts); metadata["ActiveDownloads"] = this.activeDownloadCount - 1; activity.Stop(metadata); } finally { Interlocked.Decrement(ref this.activeDownloadCount); } } } } protected override void DoAfterWork() { this.heartbeat.Dispose(); this.heartbeat = null; this.AvailablePacks.CompleteAdding(); EventMetadata metadata = new EventMetadata(); metadata.Add("RequestCount", BlobDownloadRequest.TotalRequests); metadata.Add("BytesDownloaded", this.bytesDownloaded); this.tracer.Stop(metadata); } private RetryWrapper.CallbackResult WriteObjectOrPack( BlobDownloadRequest request, int tryCount, GitEndPointResponseData response, HashSet successfulDownloads = null) { // To reduce allocations, reuse the same buffer when writing objects in this batch byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; string fileName = null; switch (response.ContentType) { case GitObjectContentType.LooseObject: string sha = request.ObjectIds.First(); fileName = this.gitObjects.WriteLooseObject( response.Stream, sha, overwriteExistingObject: false, bufToCopyWith: bufToCopyWith); this.AvailableObjects.Add(sha); break; case GitObjectContentType.PackFile: fileName = this.gitObjects.WriteTempPackFile(response.Stream); this.AvailablePacks.Add(new IndexPackRequest(fileName, request)); break; case GitObjectContentType.BatchedLooseObjects: BatchedLooseObjectDeserializer.OnLooseObject onLooseObject = (objectStream, sha1) => { this.gitObjects.WriteLooseObject( objectStream, sha1, overwriteExistingObject: false, bufToCopyWith: bufToCopyWith); this.AvailableObjects.Add(sha1); if (successfulDownloads != null) { successfulDownloads.Add(sha1); } // This isn't strictly correct because we don't add object header bytes, // just the actual compressed content length, but we expect the amount of // header data to be negligible compared to the objects themselves. Interlocked.Add(ref this.bytesDownloaded, objectStream.Length); }; new BatchedLooseObjectDeserializer(response.Stream, onLooseObject).ProcessObjects(); break; } if (fileName != null) { // NOTE: If we are writing a file as part of this method, the only case // where it's not expected to exist is when running unit tests FileInfo info = new FileInfo(fileName); if (info.Exists) { Interlocked.Add(ref this.bytesDownloaded, info.Length); } else { return new RetryWrapper.CallbackResult( new GitObjectsHttpRequestor.GitObjectTaskResult(false)); } } return new RetryWrapper.CallbackResult( new GitObjectsHttpRequestor.GitObjectTaskResult(true)); } private void EmitHeartbeat(object state) { EventMetadata metadata = new EventMetadata(); metadata["ActiveDownloads"] = this.activeDownloadCount; this.tracer.RelatedEvent(EventLevel.Verbose, "DownloadHeartbeat", metadata); } private class DownloadRequestAggregator { private BlockingCollection missingBlobs; private int chunkSize; public DownloadRequestAggregator(BlockingCollection missingBlobs, int chunkSize) { this.missingBlobs = missingBlobs; this.chunkSize = chunkSize; } public bool TryTake(out BlobDownloadRequest request) { List blobsInChunk = new List(); for (int i = 0; i < this.chunkSize;) { // Only wait a short while for new work to show up, otherwise go ahead and download what we have accumulated so far const int TimeoutMs = 100; string blobId; if (this.missingBlobs.TryTake(out blobId, TimeoutMs)) { blobsInChunk.Add(blobId); // Only increment if a blob was added. Otherwise, if no blobs are added during TimeoutMs * chunkSize, // this will exit early and blobs added later will not be downloaded. ++i; } else if (blobsInChunk.Count > 0 || this.missingBlobs.IsAddingCompleted) { break; } } if (blobsInChunk.Count > 0) { request = new BlobDownloadRequest(blobsInChunk); return true; } request = null; return false; } } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/Pipeline/Data/BlobDownloadRequest.cs ================================================ using System.Collections.Generic; using System.Threading; namespace GVFS.Common.Prefetch.Pipeline.Data { public class BlobDownloadRequest { private static int requestCounter = 0; public BlobDownloadRequest(IReadOnlyList objectIds) { this.ObjectIds = objectIds; this.RequestId = Interlocked.Increment(ref requestCounter); } public static int TotalRequests { get { return requestCounter; } } public IReadOnlyList ObjectIds { get; } public int RequestId { get; } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/Pipeline/Data/IndexPackRequest.cs ================================================ namespace GVFS.Common.Prefetch.Pipeline.Data { public class IndexPackRequest { public IndexPackRequest(string tempPackFile, BlobDownloadRequest downloadRequest) { this.TempPackFile = tempPackFile; this.DownloadRequest = downloadRequest; } public BlobDownloadRequest DownloadRequest { get; } public string TempPackFile { get; } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/Pipeline/Data/TreeSearchRequest.cs ================================================ namespace GVFS.Common.Prefetch.Pipeline.Data { public class SearchTreeRequest { public SearchTreeRequest(string treeSha, string rootPath, bool shouldRecurse) { this.TreeSha = treeSha; this.RootPath = rootPath; this.ShouldRecurse = shouldRecurse; } public bool ShouldRecurse { get; } public string TreeSha { get; } public string RootPath { get; } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/Pipeline/FindBlobsStage.cs ================================================ using GVFS.Common.Git; using GVFS.Common.Prefetch.Git; using GVFS.Common.Tracing; using System.Collections.Concurrent; using System.Threading; namespace GVFS.Common.Prefetch.Pipeline { /// /// Takes in search requests, searches each tree as requested, outputs blocks of missing blob shas. /// public class FindBlobsStage : PrefetchPipelineStage { private const string AreaPath = nameof(FindBlobsStage); private ITracer tracer; private Enlistment enlistment; private int missingBlobCount; private int availableBlobCount; private BlockingCollection requiredBlobs; private ConcurrentHashSet alreadyFoundBlobIds; public FindBlobsStage( int maxParallel, BlockingCollection requiredBlobs, BlockingCollection availableBlobs, ITracer tracer, Enlistment enlistment) : base(maxParallel) { this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata: null); this.requiredBlobs = requiredBlobs; this.enlistment = enlistment; this.alreadyFoundBlobIds = new ConcurrentHashSet(); this.MissingBlobs = new BlockingCollection(); this.AvailableBlobs = availableBlobs; } public BlockingCollection MissingBlobs { get; } public BlockingCollection AvailableBlobs { get; } public int MissingBlobCount { get { return this.missingBlobCount; } } public int AvailableBlobCount { get { return this.availableBlobCount; } } protected override void DoWork() { string blobId; using (LibGit2Repo repo = new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot)) { while (this.requiredBlobs.TryTake(out blobId, Timeout.Infinite)) { if (this.alreadyFoundBlobIds.Add(blobId)) { if (!repo.ObjectExists(blobId)) { Interlocked.Increment(ref this.missingBlobCount); this.MissingBlobs.Add(blobId); } else { Interlocked.Increment(ref this.availableBlobCount); this.AvailableBlobs.Add(blobId); } } } } } protected override void DoAfterWork() { this.MissingBlobs.CompleteAdding(); EventMetadata metadata = new EventMetadata(); metadata.Add("TotalMissingObjects", this.missingBlobCount); metadata.Add("AvailableObjects", this.availableBlobCount); this.tracer.Stop(metadata); } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/Pipeline/HydrateFilesStage.cs ================================================ using GVFS.Common.Prefetch.Git; using GVFS.Common.Tracing; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Threading; namespace GVFS.Common.Prefetch.Pipeline { public class HydrateFilesStage : PrefetchPipelineStage { private readonly string workingDirectoryRoot; private readonly ConcurrentDictionary> blobIdToPaths; private readonly BlockingCollection availableBlobs; private ITracer tracer; private int readFileCount; public HydrateFilesStage(int maxThreads, string workingDirectoryRoot, ConcurrentDictionary> blobIdToPaths, BlockingCollection availableBlobs, ITracer tracer) : base(maxThreads) { this.workingDirectoryRoot = workingDirectoryRoot; this.blobIdToPaths = blobIdToPaths; this.availableBlobs = availableBlobs; this.tracer = tracer; } public int ReadFileCount { get { return this.readFileCount; } } protected override void DoWork() { using (ITracer activity = this.tracer.StartActivity("ReadFiles", EventLevel.Informational)) { int readFilesCurrentThread = 0; int failedFilesCurrentThread = 0; byte[] buffer = new byte[1]; string blobId; while (this.availableBlobs.TryTake(out blobId, Timeout.Infinite)) { foreach (PathWithMode modeAndPath in this.blobIdToPaths[blobId]) { bool succeeded = GVFSPlatform.Instance.FileSystem.HydrateFile(Path.Combine(this.workingDirectoryRoot, modeAndPath.Path), buffer); if (succeeded) { Interlocked.Increment(ref this.readFileCount); readFilesCurrentThread++; } else { activity.RelatedError("Failed to read " + modeAndPath.Path); failedFilesCurrentThread++; this.HasFailures = true; } } } activity.Stop( new EventMetadata { { "FilesRead", readFilesCurrentThread }, { "Failures", failedFilesCurrentThread }, }); } } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/Pipeline/IndexPackStage.cs ================================================ using GVFS.Common.Git; using GVFS.Common.Prefetch.Pipeline.Data; using GVFS.Common.Tracing; using System.Collections.Concurrent; using System.Threading; namespace GVFS.Common.Prefetch.Pipeline { public class IndexPackStage : PrefetchPipelineStage { private const string AreaPath = nameof(IndexPackStage); private const string IndexPackAreaPath = "IndexPack"; private readonly BlockingCollection availablePacks; private ITracer tracer; private GitObjects gitObjects; private long shasIndexed = 0; public IndexPackStage( int maxParallel, BlockingCollection availablePacks, BlockingCollection availableBlobs, ITracer tracer, GitObjects gitObjects) : base(maxParallel) { this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata: null); this.availablePacks = availablePacks; this.gitObjects = gitObjects; this.AvailableBlobs = availableBlobs; } public BlockingCollection AvailableBlobs { get; } protected override void DoWork() { IndexPackRequest request; while (this.availablePacks.TryTake(out request, Timeout.Infinite)) { EventMetadata metadata = new EventMetadata(); metadata.Add("RequestId", request.DownloadRequest.RequestId); using (ITracer activity = this.tracer.StartActivity(IndexPackAreaPath, EventLevel.Informational, Keywords.Telemetry, metadata)) { GitProcess.Result result = this.gitObjects.IndexTempPackFile(request.TempPackFile); if (result.ExitCodeIsFailure) { EventMetadata errorMetadata = new EventMetadata(); errorMetadata.Add("RequestId", request.DownloadRequest.RequestId); activity.RelatedError(errorMetadata, result.Errors); this.HasFailures = true; } if (!this.HasFailures) { foreach (string blobId in request.DownloadRequest.ObjectIds) { this.AvailableBlobs.Add(blobId); Interlocked.Increment(ref this.shasIndexed); } } metadata.Add("Success", !this.HasFailures); activity.Stop(metadata); } } } protected override void DoAfterWork() { EventMetadata metadata = new EventMetadata(); metadata.Add("ShasIndexed", this.shasIndexed); this.tracer.Stop(metadata); } } } ================================================ FILE: GVFS/GVFS.Common/Prefetch/Pipeline/PrefetchPipelineStage.cs ================================================ using System; using System.Threading; namespace GVFS.Common.Prefetch.Pipeline { public abstract class PrefetchPipelineStage { private int maxParallel; private Thread[] workers; public PrefetchPipelineStage(int maxParallel) { this.maxParallel = maxParallel; } public bool HasFailures { get; protected set; } public void Start() { if (this.workers != null) { throw new InvalidOperationException("Cannot call start twice"); } this.DoBeforeWork(); this.workers = new Thread[this.maxParallel]; for (int i = 0; i < this.workers.Length; ++i) { this.workers[i] = new Thread(this.DoWork); this.workers[i].Start(); } } public void WaitForCompletion() { if (this.workers == null) { throw new InvalidOperationException("Cannot wait for completion before start is called"); } foreach (Thread t in this.workers) { t.Join(); } this.DoAfterWork(); this.workers = null; } protected virtual void DoBeforeWork() { } protected abstract void DoWork(); protected virtual void DoAfterWork() { } } } ================================================ FILE: GVFS/GVFS.Common/ProcessHelper.cs ================================================ using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; namespace GVFS.Common { public static class ProcessHelper { private static string currentProcessVersion = null; public static ProcessResult Run(string programName, string args, bool redirectOutput = true) { ProcessStartInfo processInfo = new ProcessStartInfo(programName); processInfo.UseShellExecute = false; processInfo.RedirectStandardInput = true; processInfo.RedirectStandardOutput = redirectOutput; processInfo.RedirectStandardError = redirectOutput; processInfo.WindowStyle = ProcessWindowStyle.Hidden; processInfo.CreateNoWindow = redirectOutput; processInfo.Arguments = args; return Run(processInfo); } public static string GetCurrentProcessLocation() { Assembly assembly = Assembly.GetExecutingAssembly(); return Path.GetDirectoryName(assembly.Location); } public static string GetEntryClassName() { Assembly assembly = Assembly.GetEntryAssembly(); if (assembly == null) { // The PR build tests doesn't produce an entry assembly because it is run from unmanaged code, // so we'll fall back on using this assembly. This should never ever happen for a normal exe invocation. assembly = Assembly.GetExecutingAssembly(); } return assembly.GetName().Name; } public static string GetCurrentProcessVersion() { if (currentProcessVersion == null) { Assembly assembly = Assembly.GetExecutingAssembly(); FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); currentProcessVersion = fileVersionInfo.ProductVersion; } return currentProcessVersion; } public static bool IsDevelopmentVersion() { // Official CI builds use version numbers where major > 0. // Development builds always start with 0. string version = ProcessHelper.GetCurrentProcessVersion(); return version.StartsWith("0."); } public static string GetProgramLocation(string programLocaterCommand, string processName) { ProcessResult result = ProcessHelper.Run(programLocaterCommand, processName); if (result.ExitCode != 0) { return null; } string firstPath = string.IsNullOrWhiteSpace(result.Output) ? null : result.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); if (firstPath == null) { return null; } try { return Path.GetDirectoryName(firstPath); } catch (IOException) { return null; } } public static ProcessResult Run(ProcessStartInfo processInfo, string errorMsgDelimeter = "\r\n", object executionLock = null) { using (Process executingProcess = new Process()) { string output = string.Empty; string errors = string.Empty; // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx // To avoid deadlocks, use asynchronous read operations on at least one of the streams. // Do not perform a synchronous read to the end of both redirected streams. executingProcess.StartInfo = processInfo; executingProcess.ErrorDataReceived += (sender, args) => { if (args.Data != null) { errors = errors + args.Data + errorMsgDelimeter; } }; if (executionLock != null) { lock (executionLock) { output = StartProcess(executingProcess); } } else { output = StartProcess(executingProcess); } return new ProcessResult(output.ToString(), errors.ToString(), executingProcess.ExitCode); } } private static string StartProcess(Process executingProcess) { executingProcess.Start(); if (executingProcess.StartInfo.RedirectStandardError) { executingProcess.BeginErrorReadLine(); } string output = string.Empty; if (executingProcess.StartInfo.RedirectStandardOutput) { output = executingProcess.StandardOutput.ReadToEnd(); } executingProcess.WaitForExit(); return output; } } } ================================================ FILE: GVFS/GVFS.Common/ProcessResult.cs ================================================ namespace GVFS.Common { public class ProcessResult { public ProcessResult(string output, string errors, int exitCode) { this.Output = output; this.Errors = errors; this.ExitCode = exitCode; } public string Output { get; } public string Errors { get; } public int ExitCode { get; } } } ================================================ FILE: GVFS/GVFS.Common/ProcessRunnerImpl.cs ================================================ namespace GVFS.Common { /// /// Default product implementation of IProcessRunner /// interface. Delegates calls to static ProcessHelper class. This /// class can be used to enable testing of components that call /// into the ProcessHelper functionality. /// public class ProcessRunnerImpl : IProcessRunner { public ProcessResult Run(string programName, string args, bool redirectOutput) { return ProcessHelper.Run(programName, args, redirectOutput); } } } ================================================ FILE: GVFS/GVFS.Common/RepoMetadata.cs ================================================ using GVFS.Common.FileSystem; using GVFS.Common.Tracing; using System; using System.Collections.Generic; using System.IO; namespace GVFS.Common { public class RepoMetadata { private FileBasedDictionary repoMetadata; private ITracer tracer; private RepoMetadata(ITracer tracer) { this.tracer = tracer; } public static RepoMetadata Instance { get; private set; } public string EnlistmentId { get { string value; if (!this.repoMetadata.TryGetValue(Keys.EnlistmentId, out value)) { value = CreateNewEnlistmentId(this.tracer); this.repoMetadata.SetValueAndFlush(Keys.EnlistmentId, value); } return value; } } public string DataFilePath { get { return this.repoMetadata.DataFilePath; } } public static bool TryInitialize(ITracer tracer, string dotGVFSPath, out string error) { return TryInitialize(tracer, new PhysicalFileSystem(), dotGVFSPath, out error); } public static bool TryInitialize(ITracer tracer, PhysicalFileSystem fileSystem, string dotGVFSPath, out string error) { string dictionaryPath = Path.Combine(dotGVFSPath, GVFSConstants.DotGVFS.Databases.RepoMetadata); if (Instance != null) { if (!Instance.repoMetadata.DataFilePath.Equals(dictionaryPath, GVFSPlatform.Instance.Constants.PathComparison)) { throw new InvalidOperationException( string.Format( "TryInitialize should never be called twice with different parameters. Expected: '{0}' Actual: '{1}'", Instance.repoMetadata.DataFilePath, dictionaryPath)); } } else { Instance = new RepoMetadata(tracer); if (!FileBasedDictionary.TryCreate( tracer, dictionaryPath, fileSystem, out Instance.repoMetadata, out error)) { return false; } } error = null; return true; } public static void Shutdown() { if (Instance != null) { if (Instance.repoMetadata != null) { Instance.repoMetadata.Dispose(); Instance.repoMetadata = null; } Instance = null; } } public bool TryGetOnDiskLayoutVersion(out int majorVersion, out int minorVersion, out string error) { majorVersion = 0; minorVersion = 0; try { string value; if (!this.repoMetadata.TryGetValue(Keys.DiskLayoutMajorVersion, out value)) { error = "Enlistment disk layout version not found, check if a breaking change has been made to GVFS since cloning this enlistment."; return false; } if (!int.TryParse(value, out majorVersion)) { error = "Failed to parse persisted disk layout version number: " + value; return false; } // The minor version is optional, e.g. it could be missing during an upgrade if (this.repoMetadata.TryGetValue(Keys.DiskLayoutMinorVersion, out value)) { if (!int.TryParse(value, out minorVersion)) { minorVersion = 0; } } } catch (FileBasedCollectionException ex) { error = ex.Message; return false; } error = null; return true; } public void SaveCloneMetadata(ITracer tracer, GVFSEnlistment enlistment) { this.repoMetadata.SetValuesAndFlush( new[] { new KeyValuePair(Keys.DiskLayoutMajorVersion, GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion.ToString()), new KeyValuePair(Keys.DiskLayoutMinorVersion, GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion.ToString()), new KeyValuePair(Keys.GitObjectsRoot, enlistment.GitObjectsRoot), new KeyValuePair(Keys.LocalCacheRoot, enlistment.LocalCacheRoot), new KeyValuePair(Keys.BlobSizesRoot, enlistment.BlobSizesRoot), new KeyValuePair(Keys.EnlistmentId, CreateNewEnlistmentId(tracer)), }); } public void SetProjectionInvalid(bool invalid) { this.SetInvalid(Keys.ProjectionInvalid, invalid); } public bool GetProjectionInvalid() { return this.HasEntry(Keys.ProjectionInvalid); } public void SetPlaceholdersNeedUpdate(bool needUpdate) { this.SetInvalid(Keys.PlaceholdersNeedUpdate, needUpdate); } public bool GetPlaceholdersNeedUpdate() { return this.HasEntry(Keys.PlaceholdersNeedUpdate); } public void SetProjectionInvalidAndPlaceholdersNeedUpdate() { this.repoMetadata.SetValuesAndFlush( new[] { new KeyValuePair(Keys.ProjectionInvalid, bool.TrueString), new KeyValuePair(Keys.PlaceholdersNeedUpdate, bool.TrueString) }); } public bool TryGetGitObjectsRoot(out string gitObjectsRoot, out string error) { gitObjectsRoot = null; try { if (!this.repoMetadata.TryGetValue(Keys.GitObjectsRoot, out gitObjectsRoot)) { error = "Git objects root not found"; return false; } } catch (FileBasedCollectionException ex) { error = ex.Message; return false; } error = null; return true; } public void SetGitObjectsRoot(string gitObjectsRoot) { this.repoMetadata.SetValueAndFlush(Keys.GitObjectsRoot, gitObjectsRoot); } public bool TryGetLocalCacheRoot(out string localCacheRoot, out string error) { localCacheRoot = null; try { if (!this.repoMetadata.TryGetValue(Keys.LocalCacheRoot, out localCacheRoot)) { error = "Local cache root not found"; return false; } } catch (FileBasedCollectionException ex) { error = ex.Message; return false; } error = null; return true; } public void SetLocalCacheRoot(string localCacheRoot) { this.repoMetadata.SetValueAndFlush(Keys.LocalCacheRoot, localCacheRoot); } public bool TryGetBlobSizesRoot(out string blobSizesRoot, out string error) { blobSizesRoot = null; try { if (!this.repoMetadata.TryGetValue(Keys.BlobSizesRoot, out blobSizesRoot)) { error = "Blob sizes root not found"; return false; } } catch (FileBasedCollectionException ex) { error = ex.Message; return false; } error = null; return true; } public void SetBlobSizesRoot(string blobSizesRoot) { this.repoMetadata.SetValueAndFlush(Keys.BlobSizesRoot, blobSizesRoot); } public void SetEntry(string keyName, string valueName) { this.repoMetadata.SetValueAndFlush(keyName, valueName); } private static string CreateNewEnlistmentId(ITracer tracer) { string enlistmentId = Guid.NewGuid().ToString("N"); EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(enlistmentId), enlistmentId); tracer.RelatedEvent(EventLevel.Informational, nameof(CreateNewEnlistmentId), metadata); return enlistmentId; } private void SetInvalid(string keyName, bool invalid) { if (invalid) { this.repoMetadata.SetValueAndFlush(keyName, bool.TrueString); } else { this.repoMetadata.RemoveAndFlush(keyName); } } private bool HasEntry(string keyName) { string value; if (this.repoMetadata.TryGetValue(keyName, out value)) { return true; } return false; } public static class Keys { public const string ProjectionInvalid = "ProjectionInvalid"; public const string PlaceholdersInvalid = "PlaceholdersInvalid"; public const string DiskLayoutMajorVersion = "DiskLayoutVersion"; public const string DiskLayoutMinorVersion = "DiskLayoutMinorVersion"; public const string PlaceholdersNeedUpdate = "PlaceholdersNeedUpdate"; public const string GitObjectsRoot = "GitObjectsRoot"; public const string LocalCacheRoot = "LocalCacheRoot"; public const string BlobSizesRoot = "BlobSizesRoot"; public const string EnlistmentId = "EnlistmentId"; } } } ================================================ FILE: GVFS/GVFS.Common/RetryBackoff.cs ================================================ using System; namespace GVFS.Common { public static class RetryBackoff { public const double DefaultExponentialBackoffBase = 2; [ThreadStatic] private static Random threadLocalRandom; private static Random ThreadLocalRandom { get { if (threadLocalRandom == null) { threadLocalRandom = new Random(); } return threadLocalRandom; } } /// /// Computes the next backoff value in seconds. /// /// /// Current failed attempt using 1-based counting. (i.e. currentFailedAttempt should be 1 if the first attempt just failed /// /// Maximum allowed backoff /// Time to backoff in seconds /// Computed backoff is randomly adjusted by +- 10% to help prevent clients from hitting servers at the same time public static double CalculateBackoffSeconds(int currentFailedAttempt, double maxBackoffSeconds, double exponentialBackoffBase = DefaultExponentialBackoffBase) { if (currentFailedAttempt <= 1) { return 0; } // Exponential backoff double backOffSeconds = Math.Min(Math.Pow(exponentialBackoffBase, currentFailedAttempt), maxBackoffSeconds); // Timeout usually happens when the server is overloaded. If we give all machines the same timeout they will all make // another request at approximately the same time causing the problem to happen again and again. To avoid that we // introduce a random timeout. To avoid scaling it too high or too low, it is +- 10% of the average backoff backOffSeconds *= .9 + (ThreadLocalRandom.NextDouble() * .2); return backOffSeconds; } } } ================================================ FILE: GVFS/GVFS.Common/RetryCircuitBreaker.cs ================================================ using System; using System.Threading; namespace GVFS.Common { /// /// Global circuit breaker for retry operations. When too many consecutive failures /// occur (e.g., during system-wide resource exhaustion), the circuit opens and /// subsequent retry attempts fail fast instead of consuming connections and adding /// backoff delays that worsen the resource pressure. /// public static class RetryCircuitBreaker { public const int DefaultFailureThreshold = 15; public const int DefaultCooldownMs = 30_000; private static int failureThreshold = DefaultFailureThreshold; private static int cooldownMs = DefaultCooldownMs; private static int consecutiveFailures = 0; private static long circuitOpenedAtUtcTicks = 0; public static bool IsOpen { get { if (Volatile.Read(ref consecutiveFailures) < failureThreshold) { return false; } long openedAt = Volatile.Read(ref circuitOpenedAtUtcTicks); return (DateTime.UtcNow.Ticks - openedAt) < TimeSpan.FromMilliseconds(cooldownMs).Ticks; } } public static int ConsecutiveFailures => Volatile.Read(ref consecutiveFailures); public static void RecordSuccess() { Interlocked.Exchange(ref consecutiveFailures, 0); } public static void RecordFailure() { int failures = Interlocked.Increment(ref consecutiveFailures); if (failures >= failureThreshold) { Volatile.Write(ref circuitOpenedAtUtcTicks, DateTime.UtcNow.Ticks); } } /// /// Resets the circuit breaker to its initial state. Intended for testing. /// public static void Reset() { Volatile.Write(ref consecutiveFailures, 0); Volatile.Write(ref circuitOpenedAtUtcTicks, 0); Volatile.Write(ref failureThreshold, DefaultFailureThreshold); Volatile.Write(ref cooldownMs, DefaultCooldownMs); } /// /// Configures the circuit breaker thresholds. Intended for testing. /// public static void Configure(int threshold, int cooldownMilliseconds) { Volatile.Write(ref failureThreshold, threshold); Volatile.Write(ref cooldownMs, cooldownMilliseconds); } } } ================================================ FILE: GVFS/GVFS.Common/RetryConfig.cs ================================================ using GVFS.Common.Git; using GVFS.Common.Tracing; using System; using System.Linq; namespace GVFS.Common { public class RetryConfig { public const int DefaultMaxRetries = 6; public const int DefaultTimeoutSeconds = 30; public const int FetchAndCloneTimeoutMinutes = 10; private const string EtwArea = nameof(RetryConfig); private const int MinRetries = 0; private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(DefaultTimeoutSeconds); public RetryConfig(int maxRetries = DefaultMaxRetries) : this(maxRetries, DefaultTimeout) { } public RetryConfig(int maxRetries, TimeSpan timeout) { this.MaxRetries = maxRetries; this.Timeout = timeout; } public int MaxRetries { get; } public int MaxAttempts { get { return this.MaxRetries + 1; } } public TimeSpan Timeout { get; set; } public static bool TryLoadFromGitConfig(ITracer tracer, Enlistment enlistment, out RetryConfig retryConfig, out string error) { return TryLoadFromGitConfig(tracer, new GitProcess(enlistment), out retryConfig, out error); } public static bool TryLoadFromGitConfig(ITracer tracer, GitProcess git, out RetryConfig retryConfig, out string error) { retryConfig = null; int maxRetries; if (!TryLoadMaxRetries(git, out maxRetries, out error)) { if (tracer != null) { tracer.RelatedError( new EventMetadata { { "Area", EtwArea }, { "error", error } }, "TryLoadConfig: TryLoadMaxRetries failed"); } return false; } TimeSpan timeout; if (!TryLoadTimeout(git, out timeout, out error)) { if (tracer != null) { tracer.RelatedError( new EventMetadata { { "Area", EtwArea }, { "maxRetries", maxRetries }, { "error", error } }, "TryLoadConfig: TryLoadTimeout failed"); } return false; } retryConfig = new RetryConfig(maxRetries, timeout); if (tracer != null) { tracer.RelatedEvent( EventLevel.Informational, "RetryConfig_LoadedRetryConfig", new EventMetadata { { "Area", EtwArea }, { "Timeout", retryConfig.Timeout }, { "MaxRetries", retryConfig.MaxRetries }, { TracingConstants.MessageKey.InfoMessage, "RetryConfigLoaded" } }); } return true; } private static bool TryLoadMaxRetries(GitProcess git, out int attempts, out string error) { return TryGetFromGitConfig( git, GVFSConstants.GitConfig.MaxRetriesConfig, DefaultMaxRetries, MinRetries, out attempts, out error); } private static bool TryLoadTimeout(GitProcess git, out TimeSpan timeout, out string error) { timeout = TimeSpan.FromSeconds(0); int timeoutSeconds; if (!TryGetFromGitConfig( git, GVFSConstants.GitConfig.TimeoutSecondsConfig, DefaultTimeoutSeconds, 0, out timeoutSeconds, out error)) { return false; } timeout = TimeSpan.FromSeconds(timeoutSeconds); return true; } private static bool TryGetFromGitConfig(GitProcess git, string configName, int defaultValue, int minValue, out int value, out string error) { GitProcess.ConfigResult result = git.GetFromConfig(configName); return result.TryParseAsInt(defaultValue, minValue, out value, out error); } } } ================================================ FILE: GVFS/GVFS.Common/RetryWrapper.cs ================================================ using GVFS.Common.Tracing; using System; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace GVFS.Common { public class RetryWrapper { private const float MaxBackoffInSeconds = 300; // 5 minutes private readonly int maxAttempts; private readonly double exponentialBackoffBase; private readonly CancellationToken cancellationToken; public RetryWrapper(int maxAttempts, CancellationToken cancellationToken, double exponentialBackoffBase = RetryBackoff.DefaultExponentialBackoffBase) { this.maxAttempts = maxAttempts; this.cancellationToken = cancellationToken; this.exponentialBackoffBase = exponentialBackoffBase; } public event Action OnFailure = delegate { }; public static Action StandardErrorHandler(ITracer tracer, long requestId, string actionName, bool forceLogAsWarning = false) { return eArgs => { EventMetadata metadata = new EventMetadata(); metadata.Add("RequestId", requestId); metadata.Add("AttemptNumber", eArgs.TryCount); metadata.Add("Operation", actionName); metadata.Add("WillRetry", eArgs.WillRetry); string message = null; if (eArgs.Error != null) { message = eArgs.Error.Message; metadata.Add("Exception", eArgs.Error.ToString()); int innerCounter = 1; Exception e = eArgs.Error.InnerException; while (e != null) { metadata.Add("InnerException" + innerCounter++, e.ToString()); e = e.InnerException; } } if (eArgs.WillRetry || forceLogAsWarning) { tracer.RelatedWarning(metadata, message, Keywords.Network); } else { tracer.RelatedError(metadata, message, Keywords.Network); } }; } public InvocationResult Invoke(Func toInvoke) { // NOTE: Cascade risk — connection pool timeouts (HttpRequestor returns // ServiceUnavailable when the semaphore wait expires) flow through here // as callback errors with shouldRetry=true and count toward the circuit // breaker. Under sustained pool exhaustion, 15 timeouts can trip the // breaker and fail-fast ALL retry operations for 30 seconds — including // requests that might have succeeded. In practice, request coalescing // (GVFSGitObjects) and the larger pool size drastically reduce the // likelihood of sustained pool exhaustion. If telemetry shows this // cascade occurring, consider excluding local resource pressure (pool // timeouts) from circuit breaker failure counts. if (RetryCircuitBreaker.IsOpen) { RetryableException circuitOpenError = new RetryableException( "Circuit breaker is open - too many consecutive failures. Fast-failing to prevent resource exhaustion."); this.OnFailure(new ErrorEventArgs(circuitOpenError, tryCount: 1, willRetry: false)); return new InvocationResult(1, circuitOpenError); } // Use 1-based counting. This makes reporting look a lot nicer and saves a lot of +1s for (int tryCount = 1; tryCount <= this.maxAttempts; ++tryCount) { this.cancellationToken.ThrowIfCancellationRequested(); try { CallbackResult result = toInvoke(tryCount); if (result.HasErrors) { if (result.ShouldRetry) { RetryCircuitBreaker.RecordFailure(); } if (!this.ShouldRetry(tryCount, null, result)) { return new InvocationResult(tryCount, result.Error, result.Result); } } else { RetryCircuitBreaker.RecordSuccess(); return new InvocationResult(tryCount, true, result.Result); } } catch (Exception e) { Exception exceptionToReport = e is AggregateException ? ((AggregateException)e).Flatten().InnerException : e; if (!this.IsHandlableException(exceptionToReport)) { throw; } RetryCircuitBreaker.RecordFailure(); if (!this.ShouldRetry(tryCount, exceptionToReport, null)) { return new InvocationResult(tryCount, exceptionToReport); } } // Don't wait for the first retry, since it might just be transient. // Don't wait after the last try. tryCount is 1-based, so last attempt is tryCount == maxAttempts if (tryCount > 1 && tryCount < this.maxAttempts) { double backOffSeconds = RetryBackoff.CalculateBackoffSeconds(tryCount, MaxBackoffInSeconds, this.exponentialBackoffBase); try { Task.Delay(TimeSpan.FromSeconds(backOffSeconds), this.cancellationToken).GetAwaiter().GetResult(); } catch (TaskCanceledException) { throw new OperationCanceledException(this.cancellationToken); } } } // This shouldn't be hit because ShouldRetry will cause a more useful message first. return new InvocationResult(this.maxAttempts, new Exception("Unexpected failure after retrying")); } private bool IsHandlableException(Exception e) { return e is HttpRequestException || e is IOException || e is RetryableException; } private bool ShouldRetry(int tryCount, Exception e, CallbackResult result) { bool willRetry = tryCount < this.maxAttempts && (result == null || result.ShouldRetry); if (e != null) { this.OnFailure(new ErrorEventArgs(e, tryCount, willRetry)); } else { this.OnFailure(new ErrorEventArgs(result.Error, tryCount, willRetry)); } return willRetry; } public class ErrorEventArgs { public ErrorEventArgs(Exception error, int tryCount, bool willRetry) { this.Error = error; this.TryCount = tryCount; this.WillRetry = willRetry; } public bool WillRetry { get; } public int TryCount { get; } public Exception Error { get; } } public class InvocationResult { public InvocationResult(int tryCount, bool succeeded, T result) { this.Attempts = tryCount; this.Succeeded = true; this.Result = result; } public InvocationResult(int tryCount, Exception error) { this.Attempts = tryCount; this.Succeeded = false; this.Error = error; } public InvocationResult(int tryCount, Exception error, T result) : this(tryCount, error) { this.Result = result; } public T Result { get; } public int Attempts { get; } public bool Succeeded { get; } public Exception Error { get; } } public class CallbackResult { public CallbackResult(T result) { this.Result = result; } public CallbackResult(Exception error, bool shouldRetry) { this.HasErrors = true; this.Error = error; this.ShouldRetry = shouldRetry; } public CallbackResult(Exception error, bool shouldRetry, T result) : this(error, shouldRetry) { this.Result = result; } public bool HasErrors { get; } public Exception Error { get; } public bool ShouldRetry { get; } public T Result { get; } } } } ================================================ FILE: GVFS/GVFS.Common/RetryableException.cs ================================================ using System; namespace GVFS.Common { public class RetryableException : Exception { public RetryableException(string message, Exception inner) : base(message, inner) { } public RetryableException(string message) : base(message) { } } } ================================================ FILE: GVFS/GVFS.Common/ReturnCode.cs ================================================ namespace GVFS.Common { public enum ReturnCode { Success = 0, ParsingError = 1, RebootRequired = 2, GenericError = 3, FilterError = 4, NullRequestData = 5, UnableToRegisterForOfflineIO = 6, DehydrateFolderFailures = 7, MountAlreadyRunning = 8, } } ================================================ FILE: GVFS/GVFS.Common/SHA1Util.cs ================================================ using System; using System.Linq; using System.Security.Cryptography; using System.Text; namespace GVFS.Common { public static class SHA1Util { public static bool IsValidShaFormat(string sha) { return sha.Length == 40 && sha.All(c => Uri.IsHexDigit(c)); } public static string SHA1HashStringForUTF8String(string s) { return HexStringFromBytes(SHA1ForUTF8String(s)); } public static byte[] SHA1ForUTF8String(string s) { byte[] bytes = Encoding.UTF8.GetBytes(s); using (SHA1 sha1 = SHA1.Create()) // CodeQL [SM02196] SHA-1 is acceptable here because this is Git's hashing algorithm, not used for cryptographic purposes { return sha1.ComputeHash(bytes); } } /// /// Returns a string representation of a byte array from the first /// bytes of the buffer. /// public static string HexStringFromBytes(byte[] buf, int numBytes = -1) { unsafe { numBytes = numBytes == -1 ? buf.Length : numBytes; fixed (byte* unsafeBuf = buf) { int charIndex = 0; byte* currentByte = unsafeBuf; char[] chars = new char[numBytes * 2]; for (int i = 0; i < numBytes; i++) { char first = (char)(((*currentByte >> 4) & 0x0F) + 0x30); char second = (char)((*currentByte & 0x0F) + 0x30); chars[charIndex++] = first >= 0x3A ? (char)(first + 0x27) : first; chars[charIndex++] = second >= 0x3A ? (char)(second + 0x27) : second; currentByte++; } return new string(chars); } } } public static byte[] BytesFromHexString(string sha) { byte[] arr = new byte[sha.Length / 2]; for (int i = 0; i < arr.Length; ++i) { arr[i] = (byte)((GetHexVal(sha[i << 1]) << 4) + GetHexVal(sha[(i << 1) + 1])); } return arr; } private static int GetHexVal(char hex) { int val = (int)hex; return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); } } } ================================================ FILE: GVFS/GVFS.Common/ServerGVFSConfig.cs ================================================ using GVFS.Common.Http; using System; using System.Collections.Generic; using System.Linq; namespace GVFS.Common { public class ServerGVFSConfig { public IEnumerable AllowedGVFSClientVersions { get; set; } public IEnumerable CacheServers { get; set; } = Enumerable.Empty(); public class VersionRange { public Version Min { get; set; } public Version Max { get; set; } } } } ================================================ FILE: GVFS/GVFS.Common/StreamUtil.cs ================================================ using System; using System.IO; namespace GVFS.Common { public class StreamUtil { /// /// .NET default buffer size uses as of 8/30/16 /// public const int DefaultCopyBufferSize = 81920; /// /// Copies all bytes from the source stream to the destination stream. This is an exact copy /// of Stream.CopyTo(), but can uses the supplied buffer instead of allocating a new one. /// /// /// As of .NET 4.6, each call to Stream.CopyTo() allocates a new 80K byte[] buffer, which /// consumes many more resources than reusing one we already have if the scenario allows it. /// /// Source stream to copy from /// Destination stream to copy to /// /// Shared buffer to use. If null, we allocate one with the same size .NET would otherwise use. /// public static void CopyToWithBuffer(Stream source, Stream destination, byte[] buffer = null) { buffer = buffer ?? new byte[DefaultCopyBufferSize]; int read; while (true) { try { read = source.Read(buffer, 0, buffer.Length); } catch (Exception ex) { // All exceptions potentially from network should be retried throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); } if (read == 0) { break; } destination.Write(buffer, 0, read); } } /// /// Call until either bytes are read or /// the end of is reached. /// /// Buffer to read bytes into. /// Offset in to start reading into. /// Maximum number of bytes to read. /// /// Number of bytes read, may be less than if end was reached. /// public static int TryReadGreedy(Stream stream, byte[] buf, int offset, int count) { int totalRead = 0; int read = 0; while (totalRead < count) { int start = offset + totalRead; int length = count - totalRead; try { read = stream.Read(buf, start, length); } catch (Exception ex) { // All exceptions potentially from network should be retried throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); } if (read == 0) { break; } totalRead += read; } return totalRead; } } } ================================================ FILE: GVFS/GVFS.Common/Tracing/DiagnosticConsoleEventListener.cs ================================================ using System; namespace GVFS.Common.Tracing { /// /// An event listener that will print all telemetry messages to the console with timestamps. /// The format of the message is designed for completeness and parsability, but not for beauty. /// public class DiagnosticConsoleEventListener : EventListener { public DiagnosticConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) : base(maxVerbosity, keywordFilter, eventSink) { } protected override void RecordMessageInternal(TraceEventMessage message) { Console.WriteLine(this.GetLogString(message.EventName, message.Opcode, message.Payload)); } } } ================================================ FILE: GVFS/GVFS.Common/Tracing/EventLevel.cs ================================================ namespace GVFS.Common.Tracing { // The default EventLevel is Verbose, which does not go to log files by default. // If you want to log to a file, you need to raise EventLevel to at least Informational public enum EventLevel { LogAlways = 0, Critical = 1, Error = 2, Warning = 3, Informational = 4, Verbose = 5 } } ================================================ FILE: GVFS/GVFS.Common/Tracing/EventListener.cs ================================================ using System; using System.Text; namespace GVFS.Common.Tracing { public abstract class EventListener : IDisposable { private readonly EventLevel maxVerbosity; private readonly Keywords keywordFilter; private readonly IEventListenerEventSink eventSink; protected EventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) { this.maxVerbosity = maxVerbosity; this.keywordFilter = keywordFilter; this.eventSink = eventSink; } public virtual void Dispose() { } public void RecordMessage(TraceEventMessage message) { if (this.IsEnabled(message.Level, message.Keywords)) { try { this.RecordMessageInternal(message); } catch (Exception ex) { this.RaiseListenerFailure(ex.ToString()); } } } protected abstract void RecordMessageInternal(TraceEventMessage message); protected string GetLogString(string eventName, EventOpcode opcode, string jsonPayload) { // Make a smarter guess (than 16 characters) about initial size to reduce allocations StringBuilder message = new StringBuilder(1024); message.AppendFormat("[{0:yyyy-MM-dd HH:mm:ss.ffff zzz}] {1}", DateTime.Now, eventName); if (opcode != 0) { message.Append(" (" + opcode + ")"); } if (!string.IsNullOrEmpty(jsonPayload)) { message.Append(" " + jsonPayload); } return message.ToString(); } protected bool IsEnabled(EventLevel level, Keywords keyword) { return this.keywordFilter != Keywords.None && this.maxVerbosity >= level && (this.keywordFilter & keyword) != 0; } protected void RaiseListenerRecovery() { this.eventSink?.OnListenerRecovery(this); } protected void RaiseListenerFailure(string errorMessage) { this.eventSink?.OnListenerFailure(this, errorMessage); } } } ================================================ FILE: GVFS/GVFS.Common/Tracing/EventMetadata.cs ================================================ using System.Collections.Generic; namespace GVFS.Common.Tracing { // This is a convenience class to make code around event metadata look nicer. // It's more obvious to see EventMetadata than Dictionary everywhere. public class EventMetadata : Dictionary { public EventMetadata() { } public EventMetadata(Dictionary metadata) : base(metadata) { } } } ================================================ FILE: GVFS/GVFS.Common/Tracing/EventOpcode.cs ================================================ namespace GVFS.Common.Tracing { // Copied from Microsoft.Diagnostics.Tracing.EventOpcode public enum EventOpcode { // Summary: // An informational event Info = 0, // Summary: // An activity start event Start = 1, // Summary: // An activity end event Stop = 2, } } ================================================ FILE: GVFS/GVFS.Common/Tracing/IEventListenerEventSink.cs ================================================ namespace GVFS.Common.Tracing { public interface IEventListenerEventSink { void OnListenerRecovery(EventListener listener); void OnListenerFailure(EventListener listener, string errorMessage); } } ================================================ FILE: GVFS/GVFS.Common/Tracing/IQueuedPipeStringWriterEventSink.cs ================================================ using System; namespace GVFS.Common.Tracing { public interface IQueuedPipeStringWriterEventSink { void OnStateChanged(QueuedPipeStringWriter writer, QueuedPipeStringWriterState state, Exception exception); } } ================================================ FILE: GVFS/GVFS.Common/Tracing/ITracer.cs ================================================ using System; namespace GVFS.Common.Tracing { public interface ITracer : IDisposable { ITracer StartActivity(string activityName, EventLevel level); ITracer StartActivity(string activityName, EventLevel level, EventMetadata metadata); ITracer StartActivity(string activityName, EventLevel level, Keywords startStopKeywords, EventMetadata metadata); void SetGitCommandSessionId(string sessionId); void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata); void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata, Keywords keywords); void RelatedInfo(string message); void RelatedInfo(string format, params object[] args); void RelatedInfo(EventMetadata metadata, string message); void RelatedWarning(EventMetadata metadata, string message); void RelatedWarning(EventMetadata metadata, string message, Keywords keywords); void RelatedWarning(string message); void RelatedWarning(string format, params object[] args); void RelatedError(EventMetadata metadata, string message); void RelatedError(EventMetadata metadata, string message, Keywords keywords); void RelatedError(string message); void RelatedError(string format, params object[] args); TimeSpan Stop(EventMetadata metadata); } } ================================================ FILE: GVFS/GVFS.Common/Tracing/JsonTracer.cs ================================================ using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; namespace GVFS.Common.Tracing { public class JsonTracer : ITracer, IEventListenerEventSink { public const string NetworkErrorEventName = "NetworkError"; private readonly ConcurrentBag listeners; private readonly ConcurrentDictionary failedListeners = new ConcurrentDictionary(); private readonly string activityName; private readonly Guid parentActivityId; private readonly Guid activityId; private readonly Stopwatch duration = Stopwatch.StartNew(); private readonly EventLevel startStopLevel; private readonly Keywords startStopKeywords; private bool isDisposed = false; private bool stopped = false; public JsonTracer(string providerName, string activityName, bool disableTelemetry = false) : this(providerName, Guid.Empty, activityName, enlistmentId: null, mountId: null, disableTelemetry: disableTelemetry) { } public JsonTracer(string providerName, string activityName, string enlistmentId, string mountId, bool disableTelemetry = false) : this(providerName, Guid.Empty, activityName, enlistmentId, mountId, disableTelemetry) { } public JsonTracer(string providerName, Guid providerActivityId, string activityName, string enlistmentId, string mountId, bool disableTelemetry = false) : this( null, providerActivityId, activityName, EventLevel.Informational, Keywords.Telemetry) { if (!disableTelemetry) { string gitBinRoot = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); // If we do not have a git binary, then we cannot check if we should set up telemetry // We also cannot log this, as we are setting up tracer. if (string.IsNullOrEmpty(gitBinRoot)) { return; } TelemetryDaemonEventListener daemonListener = TelemetryDaemonEventListener.CreateIfEnabled(gitBinRoot, providerName, enlistmentId, mountId, this); if (daemonListener != null) { this.listeners.Add(daemonListener); } } } private JsonTracer(ConcurrentBag listeners, Guid parentActivityId, string activityName, EventLevel startStopLevel, Keywords startStopKeywords) { this.listeners = listeners ?? new ConcurrentBag(); this.parentActivityId = parentActivityId; this.activityName = activityName; this.startStopLevel = startStopLevel; this.startStopKeywords = startStopKeywords; this.activityId = Guid.NewGuid(); } public bool HasLogFileEventListener { get { return this.listeners.Any(listener => listener is LogFileEventListener); } } public void SetGitCommandSessionId(string sessionId) { TelemetryDaemonEventListener daemonListener = this.listeners.FirstOrDefault(x => x is TelemetryDaemonEventListener) as TelemetryDaemonEventListener; if (daemonListener != null) { daemonListener.GitCommandSessionId = sessionId; } } public void AddEventListener(EventListener listener) { if (this.isDisposed) { throw new ObjectDisposedException(nameof(JsonTracer)); } this.listeners.Add(listener); // Tell the new listener about others who have previously failed foreach (KeyValuePair kvp in this.failedListeners) { TraceEventMessage failureMessage = CreateListenerFailureMessage(kvp.Key, kvp.Value); listener.RecordMessage(failureMessage); } } public void AddDiagnosticConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter) { this.AddEventListener(new DiagnosticConsoleEventListener(maxVerbosity, keywordFilter, this)); } public void AddPrettyConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter) { this.AddEventListener(new PrettyConsoleEventListener(maxVerbosity, keywordFilter, this)); } public void AddLogFileEventListener(string logFilePath, EventLevel maxVerbosity, Keywords keywordFilter) { this.AddEventListener(new LogFileEventListener(logFilePath, maxVerbosity, keywordFilter, this)); } public void Dispose() { if (this.isDisposed) { // This instance has already been disposed return; } this.Stop(null); // If we have no parent, then we are the root tracer and should dispose our eventsource. if (this.parentActivityId == Guid.Empty) { // Empty the listener bag and dispose of the instances as we remove them. EventListener listener; while (this.listeners.TryTake(out listener)) { listener.Dispose(); } } this.isDisposed = true; } public virtual void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata) { this.RelatedEvent(level, eventName, metadata, Keywords.None); } public virtual void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata, Keywords keyword) { this.WriteEvent(eventName, level, keyword, metadata, opcode: 0); } public virtual void RelatedInfo(string format, params object[] args) { this.RelatedInfo(string.Format(format, args)); } public virtual void RelatedInfo(string message) { this.RelatedInfo(new EventMetadata(), message); } public virtual void RelatedInfo(EventMetadata metadata, string message) { metadata = metadata ?? new EventMetadata(); metadata.Add(TracingConstants.MessageKey.InfoMessage, message); this.RelatedEvent(EventLevel.Informational, "Information", metadata); } public virtual void RelatedWarning(EventMetadata metadata, string message) { this.RelatedWarning(metadata, message, Keywords.None); } public virtual void RelatedWarning(EventMetadata metadata, string message, Keywords keywords) { metadata = metadata ?? new EventMetadata(); metadata[TracingConstants.MessageKey.WarningMessage] = message; this.RelatedEvent(EventLevel.Warning, "Warning", metadata, keywords); } public virtual void RelatedWarning(string message) { EventMetadata metadata = new EventMetadata(); this.RelatedWarning(metadata, message); } public virtual void RelatedWarning(string format, params object[] args) { this.RelatedWarning(string.Format(format, args)); } public virtual void RelatedError(EventMetadata metadata, string message) { this.RelatedError(metadata, message, Keywords.Telemetry); } public virtual void RelatedError(EventMetadata metadata, string message, Keywords keywords) { metadata = metadata ?? new EventMetadata(); metadata[TracingConstants.MessageKey.ErrorMessage] = message; this.RelatedEvent(EventLevel.Error, GetCategorizedErrorEventName(keywords), metadata, keywords | Keywords.Telemetry); } public virtual void RelatedError(string message) { EventMetadata metadata = new EventMetadata(); this.RelatedError(metadata, message); } public virtual void RelatedError(string format, params object[] args) { this.RelatedError(string.Format(format, args)); } public TimeSpan Stop(EventMetadata metadata) { if (this.stopped) { return TimeSpan.Zero; } this.duration.Stop(); this.stopped = true; metadata = metadata ?? new EventMetadata(); metadata.Add("DurationMs", this.duration.ElapsedMilliseconds); this.WriteEvent(this.activityName, this.startStopLevel, this.startStopKeywords, metadata, EventOpcode.Stop); return this.duration.Elapsed; } public ITracer StartActivity(string childActivityName, EventLevel startStopLevel) { return this.StartActivity(childActivityName, startStopLevel, null); } public ITracer StartActivity(string childActivityName, EventLevel startStopLevel, EventMetadata startMetadata) { return this.StartActivity(childActivityName, startStopLevel, Keywords.None, startMetadata); } public ITracer StartActivity(string childActivityName, EventLevel startStopLevel, Keywords startStopKeywords, EventMetadata startMetadata) { JsonTracer subTracer = new JsonTracer(this.listeners, this.activityId, childActivityName, startStopLevel, startStopKeywords); // Write the start event, disabling the Telemetry keyword so we will only dispatch telemetry at the end event. subTracer.WriteStartEvent(startMetadata, startStopKeywords & ~Keywords.Telemetry); return subTracer; } public void WriteStartEvent( string enlistmentRoot, string repoUrl, string cacheServerUrl, EventMetadata additionalMetadata = null) { EventMetadata metadata = new EventMetadata(); metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); if (enlistmentRoot != null) { metadata.Add("EnlistmentRoot", enlistmentRoot); } if (repoUrl != null) { metadata.Add("Remote", Uri.EscapeUriString(repoUrl)); } if (cacheServerUrl != null) { // Changing this key to CacheServerUrl will mess with our telemetry, so it stays for historical reasons metadata.Add("ObjectsEndpoint", Uri.EscapeUriString(cacheServerUrl)); } if (additionalMetadata != null) { foreach (string key in additionalMetadata.Keys) { metadata.Add(key, additionalMetadata[key]); } } this.WriteStartEvent(metadata, Keywords.Telemetry); } public void WriteStartEvent(EventMetadata metadata, Keywords keywords) { this.WriteEvent(this.activityName, this.startStopLevel, keywords, metadata, EventOpcode.Start); } void IEventListenerEventSink.OnListenerRecovery(EventListener listener) { // Check ContainsKey first (rather than always calling TryRemove) because ContainsKey // is lock-free and recoveredListener should rarely be in failedListeners if (!this.failedListeners.ContainsKey(listener)) { // This listener has not failed since the last time it was called, so no need to log recovery return; } if (this.failedListeners.TryRemove(listener, out _)) { TraceEventMessage message = CreateListenerRecoveryMessage(listener); this.LogMessageToNonFailedListeners(message); } } void IEventListenerEventSink.OnListenerFailure(EventListener listener, string errorMessage) { if (!this.failedListeners.TryAdd(listener, errorMessage)) { // We've already logged that this listener has failed so there is no need to do it again return; } TraceEventMessage message = CreateListenerFailureMessage(listener, errorMessage); this.LogMessageToNonFailedListeners(message); } private static string GetCategorizedErrorEventName(Keywords keywords) { switch (keywords) { case Keywords.Network: return NetworkErrorEventName; default: return "Error"; } } private static TraceEventMessage CreateListenerRecoveryMessage(EventListener recoveredListener) { return new TraceEventMessage { EventName = "TraceEventListenerRecovery", Level = EventLevel.Informational, Keywords = Keywords.Any, Opcode = EventOpcode.Info, Payload = JsonConvert.SerializeObject(new Dictionary { ["EventListener"] = recoveredListener.GetType().Name }) }; } private static TraceEventMessage CreateListenerFailureMessage(EventListener failedListener, string errorMessage) { return new TraceEventMessage { EventName = "TraceEventListenerFailure", Level = EventLevel.Error, Keywords = Keywords.Any, Opcode = EventOpcode.Info, Payload = JsonConvert.SerializeObject(new Dictionary { ["EventListener"] = failedListener.GetType().Name, ["ErrorMessage"] = errorMessage, }) }; } private void WriteEvent(string eventName, EventLevel level, Keywords keywords, EventMetadata metadata, EventOpcode opcode) { string jsonPayload = metadata != null ? JsonConvert.SerializeObject(metadata) : null; if (this.isDisposed) { throw new ObjectDisposedException(nameof(JsonTracer)); } var message = new TraceEventMessage { EventName = eventName, ActivityId = this.activityId, ParentActivityId = this.parentActivityId, Level = level, Keywords = keywords, Opcode = opcode, Payload = jsonPayload }; // Iterating over the bag is thread-safe as the enumerator returned here // is of a snapshot of the bag. foreach (EventListener listener in this.listeners) { listener.RecordMessage(message); } } private void LogMessageToNonFailedListeners(TraceEventMessage message) { foreach (EventListener listener in this.listeners.Except(this.failedListeners.Keys)) { // To prevent infinitely recursive failures, we won't try and log that we failed to log that a listener failed :) listener.RecordMessage(message); } } } } ================================================ FILE: GVFS/GVFS.Common/Tracing/Keywords.cs ================================================ namespace GVFS.Common.Tracing { public enum Keywords : long { None = 1 << 0, Network = 1 << 1, DEPRECATED = 1 << 2, Telemetry = 1 << 3, Any = ~0, } } ================================================ FILE: GVFS/GVFS.Common/Tracing/LogFileEventListener.cs ================================================ using System; using System.IO; namespace GVFS.Common.Tracing { public class LogFileEventListener : EventListener { private FileStream logFile; private TextWriter writer; public LogFileEventListener(string logFilePath, EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) : base(maxVerbosity, keywordFilter, eventSink) { this.SetLogFilePath(logFilePath); } public override void Dispose() { if (this.writer != null) { this.writer.Dispose(); this.writer = null; } if (this.logFile != null) { this.logFile.Dispose(); this.logFile = null; } } protected override void RecordMessageInternal(TraceEventMessage message) { this.writer.WriteLine(this.GetLogString(message.EventName, message.Opcode, message.Payload)); this.writer.Flush(); } protected void SetLogFilePath(string newfilePath) { Directory.CreateDirectory(Path.GetDirectoryName(newfilePath)); this.logFile = File.Open(newfilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); this.logFile.Seek(0, SeekOrigin.End); this.writer = StreamWriter.Synchronized(new StreamWriter(this.logFile)); } } } ================================================ FILE: GVFS/GVFS.Common/Tracing/NullTracer.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace GVFS.Common.Tracing { /// /// Empty implementation of ITracer that does nothing /// public sealed class NullTracer : ITracer { private NullTracer() { } public static ITracer Instance { get; } = new NullTracer(); void IDisposable.Dispose() { } void ITracer.RelatedError(EventMetadata metadata, string message) { } void ITracer.RelatedError(EventMetadata metadata, string message, Keywords keywords) { } void ITracer.RelatedError(string message) { } void ITracer.RelatedError(string format, params object[] args) { } void ITracer.RelatedEvent(EventLevel level, string eventName, EventMetadata metadata) { } void ITracer.RelatedEvent(EventLevel level, string eventName, EventMetadata metadata, Keywords keywords) { } void ITracer.RelatedInfo(string message) { } void ITracer.RelatedInfo(string format, params object[] args) { } void ITracer.RelatedInfo(EventMetadata metadata, string message) { } void ITracer.RelatedWarning(EventMetadata metadata, string message) { } void ITracer.RelatedWarning(EventMetadata metadata, string message, Keywords keywords) { } void ITracer.RelatedWarning(string message) { } void ITracer.RelatedWarning(string format, params object[] args) { } void ITracer.SetGitCommandSessionId(string sessionId) { } ITracer ITracer. StartActivity(string activityName, EventLevel level) { return this; } ITracer ITracer. StartActivity(string activityName, EventLevel level, EventMetadata metadata) { return this; } ITracer ITracer. StartActivity(string activityName, EventLevel level, Keywords startStopKeywords, EventMetadata metadata) { return this; } TimeSpan ITracer.Stop(EventMetadata metadata) { return TimeSpan.Zero; } } } ================================================ FILE: GVFS/GVFS.Common/Tracing/PrettyConsoleEventListener.cs ================================================ using System; using Newtonsoft.Json; namespace GVFS.Common.Tracing { /// /// An event listener that will print any message that it can nicely format for the console /// that matches the verbosity level it is given. At the moment, this means only messages /// with an "ErrorMessage" attribute will get displayed. /// public class PrettyConsoleEventListener : EventListener { private static object consoleLock = new object(); public PrettyConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) : base(maxVerbosity, keywordFilter, eventSink) { } protected override void RecordMessageInternal(TraceEventMessage message) { if (string.IsNullOrEmpty(message.Payload)) { return; } ConsoleOutputPayload payload = JsonConvert.DeserializeObject(message.Payload); if (string.IsNullOrEmpty(payload.ErrorMessage)) { return; } // It's necessary to do a lock here because this can be called in a multi-threaded // environment and we want to make sure that ForegroundColor is restored correctly. lock (consoleLock) { ConsoleColor prevColor = Console.ForegroundColor; string prefix; switch (message.Level) { case EventLevel.Critical: case EventLevel.Error: case EventLevel.LogAlways: prefix = "Error"; Console.ForegroundColor = ConsoleColor.Red; break; case EventLevel.Warning: prefix = "Warning"; Console.ForegroundColor = ConsoleColor.Yellow; break; default: prefix = "Info"; break; } // The leading \r interacts with the spinner, which always leaves the // cursor at the end of the line, rather than the start. Console.WriteLine($"\r{prefix}: {payload.ErrorMessage}"); Console.ForegroundColor = prevColor; } } private class ConsoleOutputPayload { public string ErrorMessage { get; set; } } } } ================================================ FILE: GVFS/GVFS.Common/Tracing/QueuedPipeStringWriter.cs ================================================ using System; using System.Collections.Concurrent; using System.Diagnostics; using System.IO.Pipes; using System.Text; using System.Threading; namespace GVFS.Common.Tracing { public enum QueuedPipeStringWriterState { Unknown = 0, Stopped = 1, Failing = 2, Healthy = 3, } /// /// Accepts string messages from multiple threads and dispatches them over a named pipe from a /// background thread. /// public class QueuedPipeStringWriter : IDisposable { private const int DEFAULT_MAX_QUEUE_SIZE = 256; private readonly Func createPipeFunc; private readonly IQueuedPipeStringWriterEventSink eventSink; private readonly BlockingCollection queue; private Thread writerThread; private NamedPipeClientStream pipeClient; private QueuedPipeStringWriterState state = QueuedPipeStringWriterState.Unknown; private bool isDisposed; public QueuedPipeStringWriter(Func createPipeFunc, IQueuedPipeStringWriterEventSink eventSink, int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE) { this.createPipeFunc = createPipeFunc; this.eventSink = eventSink; this.queue = new BlockingCollection(new ConcurrentQueue(), boundedCapacity: maxQueueSize); } public void Start() { if (this.isDisposed) { throw new ObjectDisposedException(nameof(QueuedPipeStringWriter)); } if (this.writerThread != null) { return; } this.writerThread = new Thread(this.BackgroundWriterThreadProc) { Name = nameof(QueuedPipeStringWriter), IsBackground = true, }; this.writerThread.Start(); } public bool TryEnqueue(string message) { if (this.isDisposed) { throw new ObjectDisposedException(nameof(QueuedPipeStringWriter)); } return this.queue.TryAdd(message); } public void Stop() { if (this.isDisposed) { throw new ObjectDisposedException(nameof(QueuedPipeStringWriter)); } if (this.queue.IsAddingCompleted) { return; } // Signal to the queue draining thread that it should drain once more and then terminate. this.queue.CompleteAdding(); this.writerThread.Join(); Debug.Assert(this.queue.IsCompleted, "Message queue should be empty after being stopped"); } public void Dispose() { if (!this.isDisposed) { this.Stop(); this.pipeClient?.Dispose(); this.pipeClient = null; this.writerThread = null; this.queue.Dispose(); } this.isDisposed = true; } private void RaiseStateChanged(QueuedPipeStringWriterState newState, Exception ex) { if (this.state != newState) { this.state = newState; this.eventSink?.OnStateChanged(this, newState, ex); } } private void BackgroundWriterThreadProc() { // Drain the queue of all messages currently in the queue. // TryTake() using an infinite timeout will block until either a message is available (returns true) // or the queue has been marked as completed _and_ is empty (returns false). string message; while (this.queue.TryTake(out message, Timeout.Infinite)) { if (message != null) { this.WriteMessage(message); } } this.RaiseStateChanged(QueuedPipeStringWriterState.Stopped, null); } private void WriteMessage(string message) { // Create pipe if this is the first message, or if the last connection broke for any reason if (this.pipeClient == null) { try { // Create a new pipe stream instance using the provided factory NamedPipeClientStream pipe = this.createPipeFunc(); // Specify a instantaneous timeout because we don't want to hold up the // background thread loop if the pipe is not available; we will just drop this event. // The pipe server should already be running and waiting for connections from us. pipe.Connect(timeout: 0); // Keep a hold of this connected pipe for future messages this.pipeClient = pipe; } catch (Exception ex) { this.RaiseStateChanged(QueuedPipeStringWriterState.Failing, ex); return; } } try { // If we're in byte/stream transmission mode rather than message mode // we should signal the end of each message with a line-feed (LF) character. if (this.pipeClient.TransmissionMode == PipeTransmissionMode.Byte) { message += '\n'; } byte[] data = Encoding.UTF8.GetBytes(message); this.pipeClient.Write(data, 0, data.Length); this.pipeClient.Flush(); this.RaiseStateChanged(QueuedPipeStringWriterState.Healthy, null); } catch (Exception ex) { // We can't send this message for some reason (e.g., broken pipe); we attempt no recovery or retry // mechanism and drop this message. We will try to recreate/connect the pipe on the next message. this.pipeClient.Dispose(); this.pipeClient = null; this.RaiseStateChanged(QueuedPipeStringWriterState.Failing, ex); return; } } } } ================================================ FILE: GVFS/GVFS.Common/Tracing/TelemetryDaemonEventListener.cs ================================================ using System; using System.IO.Pipes; using GVFS.Common.Git; using Newtonsoft.Json; namespace GVFS.Common.Tracing { public class TelemetryDaemonEventListener : EventListener, IQueuedPipeStringWriterEventSink { private readonly string providerName; private readonly string enlistmentId; private readonly string mountId; private readonly string vfsVersion; private QueuedPipeStringWriter pipeWriter; private TelemetryDaemonEventListener( string providerName, string enlistmentId, string mountId, string pipeName, IEventListenerEventSink eventSink) : base(EventLevel.Verbose, Keywords.Telemetry, eventSink) { this.providerName = providerName; this.enlistmentId = enlistmentId; this.mountId = mountId; this.vfsVersion = ProcessHelper.GetCurrentProcessVersion(); this.pipeWriter = new QueuedPipeStringWriter( () => new NamedPipeClientStream(".", pipeName, PipeDirection.Out, PipeOptions.Asynchronous), this); this.pipeWriter.Start(); } public string GitCommandSessionId { get; set; } public static TelemetryDaemonEventListener CreateIfEnabled(string gitBinRoot, string providerName, string enlistmentId, string mountId, IEventListenerEventSink eventSink) { // This listener is disabled unless the user specifies the proper git config setting. string telemetryPipe = GetConfigValue(gitBinRoot, GVFSConstants.GitConfig.GVFSTelemetryPipe); if (!string.IsNullOrEmpty(telemetryPipe)) { return new TelemetryDaemonEventListener(providerName, enlistmentId, mountId, telemetryPipe, eventSink); } else { return null; } } public override void Dispose() { if (this.pipeWriter != null) { this.pipeWriter.Stop(); this.pipeWriter.Dispose(); this.pipeWriter = null; } base.Dispose(); } void IQueuedPipeStringWriterEventSink.OnStateChanged( QueuedPipeStringWriter writer, QueuedPipeStringWriterState state, Exception exception) { switch (state) { case QueuedPipeStringWriterState.Failing: this.RaiseListenerFailure(exception?.ToString()); break; case QueuedPipeStringWriterState.Healthy: this.RaiseListenerRecovery(); break; } } protected override void RecordMessageInternal(TraceEventMessage message) { string pipeMessage = this.CreatePipeMessage(message); bool dropped = !this.pipeWriter.TryEnqueue(pipeMessage); if (dropped) { this.RaiseListenerFailure("Pipe delivery queue is full. Message was dropped."); } } private static string GetConfigValue(string gitBinRoot, string configKey) { string value = string.Empty; string error; GitProcess.ConfigResult result = GitProcess.GetFromSystemConfig(gitBinRoot, configKey); if (!result.TryParseAsString(out value, out error, defaultValue: string.Empty) || string.IsNullOrWhiteSpace(value)) { result = GitProcess.GetFromGlobalConfig(gitBinRoot, configKey); result.TryParseAsString(out value, out error, defaultValue: string.Empty); } return value.TrimEnd('\r', '\n'); } private string CreatePipeMessage(TraceEventMessage message) { var pipeMessage = new PipeMessage { Version = this.vfsVersion, ProviderName = this.providerName, EventName = message.EventName, EventLevel = message.Level, EventOpcode = message.Opcode, Payload = new PipeMessage.PipeMessagePayload { EnlistmentId = this.enlistmentId, MountId = this.mountId, GitCommandSessionId = this.GitCommandSessionId, Json = message.Payload }, // Other TraceEventMessage properties are not used }; return pipeMessage.ToJson(); } public class PipeMessage { [JsonProperty("version")] public string Version { get; set; } [JsonProperty("providerName")] public string ProviderName { get; set; } [JsonProperty("eventName")] public string EventName { get; set; } [JsonProperty("eventLevel")] public EventLevel EventLevel { get; set; } [JsonProperty("eventOpcode")] public EventOpcode EventOpcode { get; set; } [JsonProperty("payload")] public PipeMessagePayload Payload { get; set; } public static PipeMessage FromJson(string json) { return JsonConvert.DeserializeObject(json); } public string ToJson() { return JsonConvert.SerializeObject(this); } public class PipeMessagePayload { [JsonProperty("enlistmentId")] public string EnlistmentId { get; set; } [JsonProperty("mountId")] public string MountId { get; set; } [JsonProperty("gitCommandSessionId")] public string GitCommandSessionId { get; set; } [JsonProperty("json")] public string Json { get; set; } } } } } ================================================ FILE: GVFS/GVFS.Common/Tracing/TraceEventMessage.cs ================================================ using System; namespace GVFS.Common.Tracing { public class TraceEventMessage { public string EventName { get; set; } public Guid ActivityId { get; set; } public Guid ParentActivityId { get; set; } public EventLevel Level { get; set; } public Keywords Keywords { get; set; } public EventOpcode Opcode { get; set; } public string Payload { get; set; } } } ================================================ FILE: GVFS/GVFS.Common/Tracing/TracingConstants.cs ================================================ namespace GVFS.Common.Tracing { public static class TracingConstants { public static class MessageKey { public const string LogAlwaysMessage = ErrorMessage; public const string CriticalMessage = ErrorMessage; public const string ErrorMessage = "ErrorMessage"; public const string WarningMessage = "WarningMessage"; public const string InfoMessage = "Message"; public const string VerboseMessage = InfoMessage; } } } ================================================ FILE: GVFS/GVFS.Common/VersionResponse.cs ================================================ using Newtonsoft.Json; namespace GVFS.Common { public class VersionResponse { public string Version { get; set; } public static VersionResponse FromJsonString(string jsonString) { return JsonConvert.DeserializeObject(jsonString); } } } ================================================ FILE: GVFS/GVFS.Common/WorktreeCommandParser.cs ================================================ using System; using System.Collections.Generic; namespace GVFS.Common { /// /// Parses git worktree command arguments from hook args arrays. /// Hook args format: [hooktype, "worktree", subcommand, options..., positional args..., --git-pid=N, --exit_code=N] /// /// Assumptions: /// - Args are passed by git exactly as the user typed them (no normalization). /// - --git-pid and --exit_code are always appended by git in =value form. /// - Single-letter flags may be combined (e.g., -fd for --force --detach). /// - -b/-B always consume the next arg as a branch name, even when combined (e.g., -fb branch). /// /// Future improvement: consider replacing with a POSIX-compatible arg parser /// library (e.g., Mono.Options, MIT license) to handle edge cases more robustly. /// public static class WorktreeCommandParser { private static readonly HashSet ShortOptionsWithValue = new HashSet { 'b', 'B' }; /// /// Gets the worktree subcommand (add, remove, move, list, etc.) from hook args. /// public static string GetSubcommand(string[] args) { // args[0] = hook type, args[1] = "worktree", args[2+] = subcommand and its args for (int i = 2; i < args.Length; i++) { if (!args[i].StartsWith("-")) { return args[i].ToLowerInvariant(); } } return null; } /// /// Gets a positional argument from git worktree subcommand args. /// For 'add': git worktree add [options] <path> [<commit-ish>] /// For 'remove': git worktree remove [options] <worktree> /// For 'move': git worktree move [options] <worktree> <new-path> /// /// Full hook args array (hooktype, command, subcommand, ...) /// 0-based index of the positional arg after the subcommand public static string GetPositionalArg(string[] args, int positionalIndex) { var longOptionsWithValue = new HashSet(StringComparer.OrdinalIgnoreCase) { "--reason" }; int found = -1; bool pastSubcommand = false; bool pastSeparator = false; for (int i = 2; i < args.Length; i++) { if (args[i].StartsWith("--git-pid") || args[i].StartsWith("--exit_code")) { // Always =value form, but skip either way if (!args[i].Contains("=") && i + 1 < args.Length) { i++; } continue; } if (args[i] == "--") { pastSeparator = true; continue; } if (!pastSeparator && args[i].StartsWith("--")) { // Long option — check if it takes a separate value if (longOptionsWithValue.Contains(args[i]) && i + 1 < args.Length) { i++; } continue; } if (!pastSeparator && args[i].StartsWith("-") && args[i].Length > 1) { // Short option(s), possibly combined (e.g., -fd, -fb branch). // A value-taking letter consumes the rest of the arg as its value. // Only consume the next arg if the first value-taking letter is // the last character (no baked-in value). // e.g., -bfd → b="fd" (baked), -fdb val → f,d booleans, b="val" // -Bb → B="b" (baked), -fBb → f boolean, B="b" (baked) string flags = args[i].Substring(1); bool consumesNextArg = false; for (int j = 0; j < flags.Length; j++) { if (ShortOptionsWithValue.Contains(flags[j])) { // This letter takes a value. If it's the last letter, // the value is the next arg. Otherwise the value is the // remaining characters (baked in) and we're done. consumesNextArg = (j == flags.Length - 1); break; } } if (consumesNextArg && i + 1 < args.Length) { i++; } continue; } if (!pastSubcommand) { pastSubcommand = true; continue; } found++; if (found == positionalIndex) { return args[i]; } } return null; } /// /// Gets the first positional argument (worktree path) from git worktree args. /// public static string GetPathArg(string[] args) { return GetPositionalArg(args, 0); } } } ================================================ FILE: GVFS/GVFS.Common/X509Certificates/CertificateVerifier.cs ================================================ using System.Security.Cryptography.X509Certificates; namespace GVFS.Common.X509Certificates { public class CertificateVerifier { public virtual bool Verify(X509Certificate2 certificate) { return certificate.Verify(); } } } ================================================ FILE: GVFS/GVFS.Common/X509Certificates/SystemCertificateStore.cs ================================================ using System; using System.Security.Cryptography.X509Certificates; namespace GVFS.Common.X509Certificates { public class SystemCertificateStore : IDisposable { private readonly X509Store store; private bool isOpen = false; public SystemCertificateStore() { this.store = new X509Store(); } public void Dispose() { this.store.Dispose(); } public virtual X509Certificate2Collection Find(X509FindType findType, string searchString, bool validOnly) { if (!this.isOpen) { this.store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly); this.isOpen = true; } return this.store.Certificates.Find(X509FindType.FindBySubjectName, searchString, validOnly); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/AssemblyAttributes.cs ================================================ using NUnit.Framework; [assembly: Parallelizable(ParallelScope.Fixtures)] ================================================ FILE: GVFS/GVFS.FunctionalTests/Categories.cs ================================================ namespace GVFS.FunctionalTests { public static class Categories { public const string ExtraCoverage = "ExtraCoverage"; public const string FastFetch = "FastFetch"; public const string GitCommands = "GitCommands"; public const string NeedsReactionInCI = "NeedsReactionInCI"; } } ================================================ FILE: GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs ================================================ using GVFS.FunctionalTests.Properties; using GVFS.Tests.Should; using NUnit.Framework; using System; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; namespace GVFS.FunctionalTests.FileSystemRunners { public class BashRunner : ShellRunner { private static string[] fileNotFoundMessages = new string[] { "cannot stat", "cannot remove", "No such file or directory" }; private static string[] invalidMovePathMessages = new string[] { "cannot move", "No such file or directory" }; private static string[] moveDirectoryNotSupportedMessage = new string[] { "Function not implemented" }; private static string[] windowsPermissionDeniedMessage = new string[] { "Permission denied" }; private static string[] macPermissionDeniedMessage = new string[] { "Resource temporarily unavailable" }; private readonly string pathToBash; public BashRunner() { if (File.Exists(Settings.Default.PathToBash)) { this.pathToBash = Settings.Default.PathToBash; } else { this.pathToBash = "bash.exe"; } } private enum FileType { Invalid, File, Directory, SymLink, } protected override string FileName { get { return this.pathToBash; } } public static void DeleteDirectoryWithUnlimitedRetries(string path) { BashRunner runner = new BashRunner(); bool pathExists = Directory.Exists(path); int retryCount = 0; while (pathExists) { string output = runner.DeleteDirectory(path); pathExists = Directory.Exists(path); if (pathExists) { ++retryCount; Thread.Sleep(500); if (retryCount > 10) { retryCount = 0; if (Debugger.IsAttached) { Debugger.Break(); } } } } } public bool IsSymbolicLink(string path) { return this.FileExistsOnDisk(path, FileType.SymLink); } public void CreateSymbolicLink(string newLinkFilePath, string existingFilePath) { string existingFileBashPath = this.ConvertWinPathToBashPath(existingFilePath); string newLinkBashPath = this.ConvertWinPathToBashPath(newLinkFilePath); this.RunProcess(string.Format("-c \"ln -s -f '{0}' '{1}'\"", existingFileBashPath, newLinkBashPath)); } public override bool FileExists(string path) { return this.FileExistsOnDisk(path, FileType.File); } public override string MoveFile(string sourcePath, string targetPath) { string sourceBashPath = this.ConvertWinPathToBashPath(sourcePath); string targetBashPath = this.ConvertWinPathToBashPath(targetPath); return this.RunProcess(string.Format("-c \"mv '{0}' '{1}'\"", sourceBashPath, targetBashPath)); } public override void MoveFileShouldFail(string sourcePath, string targetPath) { // BashRunner does nothing special when a failure is expected, so just confirm source file is still present this.MoveFile(sourcePath, targetPath); this.FileExists(sourcePath).ShouldBeTrue($"{sourcePath} does not exist when it should"); } public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(fileNotFoundMessages); } public override string ReplaceFile(string sourcePath, string targetPath) { string sourceBashPath = this.ConvertWinPathToBashPath(sourcePath); string targetBashPath = this.ConvertWinPathToBashPath(targetPath); return this.RunProcess(string.Format("-c \"mv -f '{0}' '{1}'\"", sourceBashPath, targetBashPath)); } public override void ReplaceFile_AccessShouldBeDenied(string sourcePath, string targetPath) { // bash does not report any error messages when access is denied, so just confirm the file still exists this.ReplaceFile(sourcePath, targetPath); this.FileExists(sourcePath).ShouldBeTrue($"{sourcePath} does not exist when it should"); this.FileExists(targetPath).ShouldBeFalse($"{targetPath} exists when it should not"); } public override string DeleteFile(string path) { string bashPath = this.ConvertWinPathToBashPath(path); return this.RunProcess(string.Format("-c \"rm '{0}'\"", bashPath)); } public override string ReadAllText(string path) { string bashPath = this.ConvertWinPathToBashPath(path); string output = this.RunProcess(string.Format("-c \"cat '{0}'\"", bashPath)); // Bash sometimes sticks a trailing "\n" at the end of the output that we need to remove // Until we can figure out why we cannot use this runner with files that have trailing newlines if (output.Length > 0 && output.Substring(output.Length - 1).Equals("\n", StringComparison.InvariantCultureIgnoreCase) && !(output.Length > 1 && output.Substring(output.Length - 2).Equals("\r\n", StringComparison.InvariantCultureIgnoreCase))) { output = output.Remove(output.Length - 1, 1); } return output; } public override void AppendAllText(string path, string contents) { string bashPath = this.ConvertWinPathToBashPath(path); this.RunProcess(string.Format("-c \"echo -n \\\"{0}\\\" >> '{1}'\"", contents, bashPath)); } public override void CreateEmptyFile(string path) { string bashPath = this.ConvertWinPathToBashPath(path); this.RunProcess(string.Format("-c \"touch '{0}'\"", bashPath)); } public override void CreateHardLink(string newLinkFilePath, string existingFilePath) { string existingFileBashPath = this.ConvertWinPathToBashPath(existingFilePath); string newLinkBashPath = this.ConvertWinPathToBashPath(newLinkFilePath); this.RunProcess(string.Format("-c \"ln '{0}' '{1}'\"", existingFileBashPath, newLinkBashPath)); } public override void WriteAllText(string path, string contents) { string bashPath = this.ConvertWinPathToBashPath(path); this.RunProcess(string.Format("-c \"echo \\\"{0}\\\" > '{1}'\"", contents, bashPath)); } public override void WriteAllTextShouldFail(string path, string contents) { // BashRunner does nothing special when a failure is expected this.WriteAllText(path, contents); } public override bool DirectoryExists(string path) { return this.FileExistsOnDisk(path, FileType.Directory); } public override void MoveDirectory(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath); } public override void RenameDirectory(string workingDirectory, string source, string target) { this.MoveDirectory(Path.Combine(workingDirectory, source), Path.Combine(workingDirectory, target)); } public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryNotSupportedMessage); } public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(invalidMovePathMessages); } public override void CreateDirectory(string path) { string bashPath = this.ConvertWinPathToBashPath(path); this.RunProcess(string.Format("-c \"mkdir '{0}'\"", bashPath)); } public override string DeleteDirectory(string path) { string bashPath = this.ConvertWinPathToBashPath(path); return this.RunProcess(string.Format("-c \"rm -rf '{0}'\"", bashPath)); } public override string EnumerateDirectory(string path) { string bashPath = this.ConvertWinPathToBashPath(path); return this.RunProcess(string.Format("-c \"ls '{0}'\"", bashPath)); } public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) { this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(fileNotFoundMessages); } public override void DeleteFile_FileShouldNotBeFound(string path) { this.DeleteFile(path).ShouldContainOneOf(fileNotFoundMessages); } public override void DeleteFile_AccessShouldBeDenied(string path) { // bash does not report any error messages when access is denied, so just confirm the file still exists this.DeleteFile(path); this.FileExists(path).ShouldBeTrue($"{path} does not exist when it should"); } public override void ReadAllText_FileShouldNotBeFound(string path) { this.ReadAllText(path).ShouldContainOneOf(fileNotFoundMessages); } public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) { // Delete directory silently succeeds when deleting a non-existent path this.DeleteDirectory(path); } public override void ChangeMode(string path, ushort mode) { string octalMode = Convert.ToString(mode, 8); string bashPath = this.ConvertWinPathToBashPath(path); string command = $"-c \"chmod {octalMode} '{bashPath}'\""; this.RunProcess(command); } public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) { Assert.Fail("Unlike the other runners, bash.exe does not check folder handle before recusively deleting"); } public override long FileSize(string path) { string bashPath = this.ConvertWinPathToBashPath(path); string statCommand = string.Format("-c \"stat --format \"%s\" '{0}'\"", bashPath); return long.Parse(this.RunProcess(statCommand)); } public override void CreateFileWithoutClose(string path) { throw new NotImplementedException(); } public override void OpenFileAndWriteWithoutClose(string path, string data) { throw new NotImplementedException(); } private bool FileExistsOnDisk(string path, FileType type) { string checkArgument = string.Empty; switch (type) { case FileType.File: checkArgument = "-f"; break; case FileType.Directory: checkArgument = "-d"; break; case FileType.SymLink: checkArgument = "-h"; break; default: Assert.Fail($"{nameof(this.FileExistsOnDisk)} does not support {nameof(FileType)} {type}"); break; } string bashPath = this.ConvertWinPathToBashPath(path); string command = $"-c \"[ {checkArgument} '{bashPath}' ] && echo {ShellRunner.SuccessOutput} || echo {ShellRunner.FailureOutput}\""; string output = this.RunProcess(command).Trim(); return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); } private string ConvertWinPathToBashPath(string winPath) { string bashPath = string.Concat("/", winPath); bashPath = bashPath.Replace(":\\", "/"); bashPath = bashPath.Replace('\\', '/'); return bashPath; } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/FileSystemRunners/CmdRunner.cs ================================================ using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using System; using System.Diagnostics; using System.IO; using System.Threading; namespace GVFS.FunctionalTests.FileSystemRunners { public class CmdRunner : ShellRunner { private const string ProcessName = "CMD.exe"; private static string[] missingFileErrorMessages = new string[] { "The system cannot find the file specified.", "The system cannot find the path specified.", "Could Not Find" }; private static string[] moveDirectoryFailureMessage = new string[] { "0 dir(s) moved" }; private static string[] fileUsedByAnotherProcessMessage = new string[] { "The process cannot access the file because it is being used by another process" }; protected override string FileName { get { return ProcessName; } } public static void DeleteDirectoryWithUnlimitedRetries(string path) { CmdRunner runner = new CmdRunner(); bool pathExists = Directory.Exists(path); int retryCount = 0; while (pathExists) { string output = runner.DeleteDirectory(path); pathExists = Directory.Exists(path); if (pathExists) { ++retryCount; Thread.Sleep(500); if (retryCount > 10) { retryCount = 0; if (Debugger.IsAttached) { Debugger.Break(); } } } } } public override bool FileExists(string path) { if (this.DirectoryExists(path)) { return false; } string output = this.RunProcess(string.Format("/C if exist \"{0}\" (echo {1}) else (echo {2})", path, ShellRunner.SuccessOutput, ShellRunner.FailureOutput)).Trim(); return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); } public override string MoveFile(string sourcePath, string targetPath) { return this.RunProcess(string.Format("/C move \"{0}\" \"{1}\"", sourcePath, targetPath)); } public override void MoveFileShouldFail(string sourcePath, string targetPath) { // CmdRunner does nothing special when a failure is expected this.MoveFile(sourcePath, targetPath); } public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); } public override string ReplaceFile(string sourcePath, string targetPath) { return this.RunProcess(string.Format("/C move /Y \"{0}\" \"{1}\"", sourcePath, targetPath)); } public override void ReplaceFile_AccessShouldBeDenied(string sourcePath, string targetPath) { // CMD does not report any error messages when access is denied, so just confirm the file still exists this.ReplaceFile(sourcePath, targetPath); this.FileExists(sourcePath).ShouldBeTrue($"{sourcePath} does not exist when it should"); this.FileExists(targetPath).ShouldBeFalse($"{targetPath} exists when it should not"); } public override string DeleteFile(string path) { return this.RunProcess(string.Format("/C del \"{0}\"", path)); } public override string ReadAllText(string path) { return this.RunProcess(string.Format("/C type \"{0}\"", path)); } public override void CreateEmptyFile(string path) { this.RunProcess(string.Format("/C type NUL > \"{0}\"", path)); } public override void CreateHardLink(string newLinkFilePath, string existingFilePath) { this.RunProcess(string.Format("/C mklink /H \"{0}\" \"{1}\"", newLinkFilePath, existingFilePath)); } public override void AppendAllText(string path, string contents) { // Use echo|set /p with "" to avoid adding any trailing whitespace or newline // to the contents this.RunProcess(string.Format("/C echo|set /p =\"{0}\" >> {1}", contents, path)); } public override void WriteAllText(string path, string contents) { // Use echo|set /p with "" to avoid adding any trailing whitespace or newline // to the contents this.RunProcess(string.Format("/C echo|set /p =\"{0}\" > {1}", contents, path)); } public override void WriteAllTextShouldFail(string path, string contents) { // CmdRunner does nothing special when a failure is expected this.WriteAllText(path, contents); } public override bool DirectoryExists(string path) { string parentDirectory = Path.GetDirectoryName(path); string targetName = Path.GetFileName(path); string output = this.RunProcess(string.Format("/C dir /A:d /B {0}", parentDirectory)); string[] directories = output.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); foreach (string directory in directories) { if (directory.Equals(targetName, FileSystemHelpers.PathComparison)) { return true; } } return false; } public override void CreateDirectory(string path) { this.RunProcess(string.Format("/C mkdir \"{0}\"", path)); } public override string DeleteDirectory(string path) { return this.RunProcess(string.Format("/C rmdir /q /s \"{0}\"", path)); } public override string EnumerateDirectory(string path) { return this.RunProcess(string.Format("/C dir \"{0}\"", path)); } public override void MoveDirectory(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath); } public override void RenameDirectory(string workingDirectory, string source, string target) { this.RunProcess(string.Format("/C ren \"{0}\" \"{1}\"", source, target), workingDirectory); } public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryFailureMessage); } public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryFailureMessage); } public string RunCommand(string command) { return this.RunProcess(string.Format("/C {0}", command)); } public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) { this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); } public override void DeleteFile_FileShouldNotBeFound(string path) { this.DeleteFile(path).ShouldContainOneOf(missingFileErrorMessages); } public override void DeleteFile_AccessShouldBeDenied(string path) { // CMD does not report any error messages when access is denied, so just confirm the file still exists this.DeleteFile(path); this.FileExists(path).ShouldBeTrue($"{path} does not exist when it should"); } public override void ReadAllText_FileShouldNotBeFound(string path) { this.ReadAllText(path).ShouldContainOneOf(missingFileErrorMessages); } public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) { this.DeleteDirectory(path).ShouldContainOneOf(missingFileErrorMessages); } public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) { this.DeleteDirectory(path).ShouldContain(fileUsedByAnotherProcessMessage); } public override void ChangeMode(string path, ushort mode) { throw new NotSupportedException(); } public override void CreateFileWithoutClose(string path) { throw new NotImplementedException(); } public override void OpenFileAndWriteWithoutClose(string path, string data) { throw new NotImplementedException(); } public override long FileSize(string path) { return long.Parse(this.RunProcess(string.Format("/C for %I in ({0}) do @echo %~zI", path))); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/FileSystemRunners/FileSystemRunner.cs ================================================ using NUnit.Framework; using System; namespace GVFS.FunctionalTests.FileSystemRunners { public abstract class FileSystemRunner { private static FileSystemRunner defaultRunner = new SystemIORunner(); public static object[] AllWindowsRunners { get; } = new[] { new object[] { new SystemIORunner() }, new object[] { new CmdRunner() }, new object[] { new PowerShellRunner() }, new object[] { new BashRunner() }, }; public static object[] AllMacRunners { get; } = new[] { new object[] { new SystemIORunner() }, new object[] { new BashRunner() }, }; public static object[] DefaultRunners { get; } = new[] { new object[] { defaultRunner } }; public static object[] Runners { get { return GVFSTestConfig.FileSystemRunners; } } /// /// Default runner to use (for tests that do not need to be run with multiple runners) /// public static FileSystemRunner DefaultRunner { get { return defaultRunner; } } // File methods public abstract bool FileExists(string path); public abstract string MoveFile(string sourcePath, string targetPath); /// /// Attempt to move the specified file to the specifed target path. By calling this method the caller is /// indicating that they expect the move to fail. However, the caller is responsible for verifying that /// the move failed. /// /// Path to existing file /// Path to target file (target of the move) public abstract void MoveFileShouldFail(string sourcePath, string targetPath); public abstract void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath); public abstract string ReplaceFile(string sourcePath, string targetPath); public abstract void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath); public abstract void ReplaceFile_AccessShouldBeDenied(string sourcePath, string targetPath); public abstract string DeleteFile(string path); public abstract void DeleteFile_FileShouldNotBeFound(string path); public abstract void DeleteFile_AccessShouldBeDenied(string path); public abstract string ReadAllText(string path); public abstract void ReadAllText_FileShouldNotBeFound(string path); public abstract void CreateEmptyFile(string path); public abstract void CreateHardLink(string newLinkFilePath, string existingFilePath); public abstract void ChangeMode(string path, ushort mode); /// /// Write the specified contents to the specified file. By calling this method the caller is /// indicating that they expect the write to succeed. However, the caller is responsible for verifying that /// the write succeeded. /// /// Path to file /// File contents public abstract void WriteAllText(string path, string contents); public abstract void CreateFileWithoutClose(string path); public abstract void OpenFileAndWriteWithoutClose(string path, string data); /// /// Append the specified contents to the specified file. By calling this method the caller is /// indicating that they expect the write to succeed. However, the caller is responsible for verifying that /// the write succeeded. /// /// Path to file /// File contents public abstract void AppendAllText(string path, string contents); /// /// Attempt to write the specified contents to the specified file. By calling this method the caller is /// indicating that they expect the write to fail. However, the caller is responsible for verifying that /// the write failed. /// /// Expected type of exception to be thrown /// Path to file /// File contents public abstract void WriteAllTextShouldFail(string path, string contents) where ExceptionType : Exception; // Directory methods public abstract bool DirectoryExists(string path); public abstract void MoveDirectory(string sourcePath, string targetPath); public abstract void RenameDirectory(string workingDirectory, string source, string target); public abstract void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath); public abstract void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath); public abstract void CreateDirectory(string path); public abstract string EnumerateDirectory(string path); public abstract long FileSize(string path); /// /// A recursive delete of a directory /// public abstract string DeleteDirectory(string path); public abstract void DeleteDirectory_DirectoryShouldNotBeFound(string path); public abstract void DeleteDirectory_ShouldBeBlockedByProcess(string path); } } ================================================ FILE: GVFS/GVFS.FunctionalTests/FileSystemRunners/PowerShellRunner.cs ================================================ using GVFS.Tests.Should; using System.IO; namespace GVFS.FunctionalTests.FileSystemRunners { public class PowerShellRunner : ShellRunner { private const string ProcessName = "powershell.exe"; private static string[] missingFileErrorMessages = new string[] { "Cannot find path" }; private static string[] invalidPathErrorMessages = new string[] { "Could not find a part of the path" }; private static string[] moveDirectoryNotSupportedMessage = new string[] { "The request is not supported." }; private static string[] fileUsedByAnotherProcessMessage = new string[] { "The process cannot access the file because it is being used by another process" }; private static string[] permissionDeniedMessage = new string[] { "PermissionDenied" }; protected override string FileName { get { return ProcessName; } } public override bool FileExists(string path) { string parentDirectory = Path.GetDirectoryName(path); string targetName = Path.GetFileName(path); // Use -force so that hidden items are returned as well string command = string.Format("-Command \"&{{ Get-ChildItem -force {0} | where {{$_.Attributes -NotLike '*Directory*'}} | where {{$_.Name -eq '{1}' }} }}\"", parentDirectory, targetName); string output = this.RunProcess(command).Trim(); if (output.Length == 0 || output.Contains("PathNotFound") || output.Contains("ItemNotFound")) { return false; } return true; } public override string MoveFile(string sourcePath, string targetPath) { return this.RunProcess(string.Format("-Command \"& {{ Move-Item {0} {1} -force}}\"", sourcePath, targetPath)); } public override void MoveFileShouldFail(string sourcePath, string targetPath) { // PowerShellRunner does nothing special when a failure is expected this.MoveFile(sourcePath, targetPath); } public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); } public override string ReplaceFile(string sourcePath, string targetPath) { return this.RunProcess(string.Format("-Command \"& {{ Move-Item {0} {1} -force }}\"", sourcePath, targetPath)); } public override void ReplaceFile_AccessShouldBeDenied(string sourcePath, string targetPath) { this.ReplaceFile(sourcePath, targetPath).ShouldContain(permissionDeniedMessage); this.FileExists(sourcePath).ShouldBeTrue($"{sourcePath} does not exist when it should"); this.FileExists(targetPath).ShouldBeFalse($"{targetPath} exists when it should not"); } public override string DeleteFile(string path) { return this.RunProcess(string.Format("-Command \"& {{ Remove-Item {0} }}\"", path)); } public override string ReadAllText(string path) { string output = this.RunProcess(string.Format("-Command \"& {{ Get-Content -Raw {0} }}\"", path), errorMsgDelimeter: "\r\n"); // Get-Content insists on sticking a trailing "\r\n" at the end of the output that we need to remove output.Length.ShouldBeAtLeast(2, $"File content was not long enough for {path}"); output.Substring(output.Length - 2).ShouldEqual("\r\n"); output = output.Remove(output.Length - 2, 2); return output; } public override void AppendAllText(string path, string contents) { this.RunProcess(string.Format("-Command \"&{{ Out-File -FilePath {0} -InputObject '{1}' -Encoding ascii -Append -NoNewline}}\"", path, contents)); } public override void CreateEmptyFile(string path) { this.RunProcess(string.Format("-Command \"&{{ New-Item -ItemType file {0}}}\"", path)); } public override void CreateHardLink(string newLinkFilePath, string existingFilePath) { this.RunProcess(string.Format("-Command \"&{{ New-Item -ItemType HardLink -Path {0} -Value {1}}}\"", newLinkFilePath, existingFilePath)); } public override void WriteAllText(string path, string contents) { this.RunProcess(string.Format("-Command \"&{{ Out-File -FilePath {0} -InputObject '{1}' -Encoding ascii -NoNewline}}\"", path, contents)); } public override void WriteAllTextShouldFail(string path, string contents) { // PowerShellRunner does nothing special when a failure is expected this.WriteAllText(path, contents); } public override bool DirectoryExists(string path) { string command = string.Format("-Command \"&{{ Test-Path {0} -PathType Container }}\"", path); string output = this.RunProcess(command).Trim(); if (output.Contains("True")) { return true; } return false; } public override void MoveDirectory(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath); } public override void RenameDirectory(string workingDirectory, string source, string target) { this.RunProcess(string.Format("-Command \"& {{ Rename-Item -Path {0} -NewName {1} -force }}\"", Path.Combine(workingDirectory, source), target)); } public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryNotSupportedMessage); } public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) { this.MoveFile(sourcePath, targetPath).ShouldContain(invalidPathErrorMessages); } public override void CreateDirectory(string path) { this.RunProcess(string.Format("-Command \"&{{ New-Item {0} -type directory}}\"", path)); } public override string DeleteDirectory(string path) { return this.RunProcess(string.Format("-Command \"&{{ Remove-Item -Force -Recurse {0} }}\"", path)); } public override string EnumerateDirectory(string path) { return this.RunProcess(string.Format("-Command \"&{{ Get-ChildItem {0} }}\"", path)); } public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) { this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); } public override void DeleteFile_FileShouldNotBeFound(string path) { this.DeleteFile(path).ShouldContainOneOf(missingFileErrorMessages); } public override void DeleteFile_AccessShouldBeDenied(string path) { this.DeleteFile(path).ShouldContain(permissionDeniedMessage); this.FileExists(path).ShouldBeTrue($"{path} does not exist when it should"); } public override void ReadAllText_FileShouldNotBeFound(string path) { this.ReadAllText(path).ShouldContainOneOf(missingFileErrorMessages); } public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) { this.DeleteDirectory(path).ShouldContainOneOf(missingFileErrorMessages); } public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) { this.DeleteDirectory(path).ShouldContain(fileUsedByAnotherProcessMessage); } public override long FileSize(string path) { return long.Parse(this.RunProcess(string.Format("-Command \"&{{ (Get-Item {0}).length}}\"", path))); } public override void ChangeMode(string path, ushort mode) { throw new System.NotSupportedException(); } public override void CreateFileWithoutClose(string path) { throw new System.NotSupportedException(); } public override void OpenFileAndWriteWithoutClose(string path, string data) { throw new System.NotSupportedException(); } protected override string RunProcess(string command, string workingDirectory = "", string errorMsgDelimeter = "") { return base.RunProcess("-NoProfile " + command, workingDirectory, errorMsgDelimeter); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/FileSystemRunners/ShellRunner.cs ================================================ using GVFS.FunctionalTests.Tools; using System.Diagnostics; namespace GVFS.FunctionalTests.FileSystemRunners { public abstract class ShellRunner : FileSystemRunner { protected const string SuccessOutput = "True"; protected const string FailureOutput = "False"; protected abstract string FileName { get; } protected virtual string RunProcess(string arguments, string workingDirectory = "", string errorMsgDelimeter = "") { ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.UseShellExecute = false; startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; startInfo.CreateNoWindow = true; startInfo.FileName = this.FileName; startInfo.Arguments = arguments; startInfo.WorkingDirectory = workingDirectory; ProcessResult result = ProcessHelper.Run(startInfo, errorMsgDelimeter: errorMsgDelimeter); return !string.IsNullOrEmpty(result.Output) ? result.Output : result.Errors; } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/FileSystemRunners/SystemIORunner.cs ================================================ using GVFS.Tests.Should; using NUnit.Framework; using System; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Threading; namespace GVFS.FunctionalTests.FileSystemRunners { public class SystemIORunner : FileSystemRunner { public override bool FileExists(string path) { return File.Exists(path); } public override string MoveFile(string sourcePath, string targetPath) { File.Move(sourcePath, targetPath); return string.Empty; } public override void CreateFileWithoutClose(string path) { File.Create(path); } public override void OpenFileAndWriteWithoutClose(string path, string content) { StreamWriter file = new StreamWriter(path); file.Write(content); } public override void MoveFileShouldFail(string sourcePath, string targetPath) { if (Debugger.IsAttached) { throw new InvalidOperationException("MoveFileShouldFail should not be run with the debugger attached"); } this.ShouldFail(() => { this.MoveFile(sourcePath, targetPath); }); } public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) { this.ShouldFail(() => { this.MoveFile(sourcePath, targetPath); }); } public override string ReplaceFile(string sourcePath, string targetPath) { File.Replace(sourcePath, targetPath, null); return string.Empty; } public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) { this.ShouldFail(() => { this.ReplaceFile(sourcePath, targetPath); }); } public override void ReplaceFile_AccessShouldBeDenied(string sourcePath, string targetPath) { this.ShouldFail(() => { this.ReplaceFile(sourcePath, targetPath); }); this.FileExists(sourcePath).ShouldBeTrue($"{sourcePath} does not exist when it should"); this.FileExists(targetPath).ShouldBeFalse($"{targetPath} exists when it should not"); } public override string DeleteFile(string path) { File.Delete(path); return string.Empty; } public override void DeleteFile_FileShouldNotBeFound(string path) { // Delete file silently succeeds when file is non-existent this.DeleteFile(path); } public override void DeleteFile_AccessShouldBeDenied(string path) { this.ShouldFail(() => { this.DeleteFile(path); }); this.FileExists(path).ShouldBeTrue($"{path} does not exist when it should"); } public override string ReadAllText(string path) { return File.ReadAllText(path); } public override void CreateEmptyFile(string path) { using (FileStream fs = File.Create(path)) { } } public override void CreateHardLink(string newLinkFilePath, string existingFilePath) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { WindowsCreateHardLink(newLinkFilePath, existingFilePath, IntPtr.Zero).ShouldBeTrue($"Failed to create hard link: {Marshal.GetLastWin32Error()}"); } else { MacCreateHardLink(existingFilePath, newLinkFilePath).ShouldEqual(0, $"Failed to create hard link: {Marshal.GetLastWin32Error()}"); } } public override void WriteAllText(string path, string contents) { File.WriteAllText(path, contents); } public override void AppendAllText(string path, string contents) { File.AppendAllText(path, contents); } public override void WriteAllTextShouldFail(string path, string contents) { if (Debugger.IsAttached) { throw new InvalidOperationException("WriteAllTextShouldFail should not be run with the debugger attached"); } this.ShouldFail(() => { this.WriteAllText(path, contents); }); } public override bool DirectoryExists(string path) { return Directory.Exists(path); } public override void MoveDirectory(string sourcePath, string targetPath) { Directory.Move(sourcePath, targetPath); } public override void RenameDirectory(string workingDirectory, string source, string target) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { MoveFileEx(Path.Combine(workingDirectory, source), Path.Combine(workingDirectory, target), 0); } else { Rename(Path.Combine(workingDirectory, source), Path.Combine(workingDirectory, target)); } } public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) { if (Debugger.IsAttached) { throw new InvalidOperationException("MoveDirectory_RequestShouldNotBeSupported should not be run with the debugger attached"); } Assert.Catch(() => this.MoveDirectory(sourcePath, targetPath)); } public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) { if (Debugger.IsAttached) { throw new InvalidOperationException("MoveDirectory_TargetShouldBeInvalid should not be run with the debugger attached"); } Assert.Catch(() => this.MoveDirectory(sourcePath, targetPath)); } public override void CreateDirectory(string path) { Directory.CreateDirectory(path); } public override string DeleteDirectory(string path) { DirectoryInfo directory = new DirectoryInfo(path); foreach (FileInfo file in directory.GetFiles()) { file.Attributes = FileAttributes.Normal; RetryOnException(() => file.Delete()); } foreach (DirectoryInfo subDirectory in directory.GetDirectories()) { this.DeleteDirectory(subDirectory.FullName); } RetryOnException(() => directory.Delete()); return string.Empty; } public override string EnumerateDirectory(string path) { return string.Join(Environment.NewLine, Directory.GetFileSystemEntries(path)); } public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) { this.ShouldFail(() => { this.DeleteDirectory(path); }); } public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) { Assert.Fail("DeleteDirectory_ShouldBeBlockedByProcess not supported by SystemIORunner"); } public override void ReadAllText_FileShouldNotBeFound(string path) { this.ShouldFail(() => { this.ReadAllText(path); }); } public override void ChangeMode(string path, ushort mode) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { throw new NotSupportedException(); } else { Chmod(path, mode).ShouldEqual(0, $"Failed to chmod: {Marshal.GetLastWin32Error()}"); } } public override long FileSize(string path) { return new FileInfo(path).Length; } [DllImport("kernel32", SetLastError = true)] private static extern bool MoveFileEx(string existingFileName, string newFileName, int flags); [DllImport("libc", EntryPoint = "link", SetLastError = true)] private static extern int MacCreateHardLink(string oldPath, string newPath); [DllImport("libc", EntryPoint = "chmod", SetLastError = true)] private static extern int Chmod(string pathname, ushort mode); [DllImport("libc", EntryPoint = "rename", SetLastError = true)] private static extern int Rename(string oldPath, string newPath); [DllImport("kernel32.dll", EntryPoint = "CreateHardLink", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool WindowsCreateHardLink( string newLinkFileName, string existingFileName, IntPtr securityAttributes); private static void RetryOnException(Action action) { for (int i = 0; i < 10; i++) { try { action(); break; } catch (IOException) { Thread.Sleep(500); } catch (UnauthorizedAccessException) { Thread.Sleep(500); } } } private void ShouldFail(Action action) where ExceptionType : Exception { Assert.Catch(() => action()); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj ================================================  net471 Exe false Content PreserveNewest false PreserveNewest PreserveNewest ================================================ FILE: GVFS/GVFS.FunctionalTests/GVFSTestConfig.cs ================================================ using System.IO; namespace GVFS.FunctionalTests { public static class GVFSTestConfig { public static string RepoToClone { get; set; } public static bool NoSharedCache { get; set; } public static string LocalCacheRoot { get; set; } public static object[] FileSystemRunners { get; set; } public static object[] GitRepoTestsValidateWorkTree { get; set; } public static bool ReplaceInboxProjFS { get; set; } public static bool IsDevMode { get; set; } public static string PathToGVFS { get { return Properties.Settings.Default.PathToGVFS; } } public static string DotGVFSRoot { get; set; } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/GlobalSetup.cs ================================================ using GVFS.FunctionalTests.Tests; using GVFS.FunctionalTests.Tools; using NUnit.Framework; using System; using System.IO; using System.Runtime.InteropServices; namespace GVFS.FunctionalTests { [SetUpFixture] public class GlobalSetup { [OneTimeSetUp] public void RunBeforeAnyTests() { } [OneTimeTearDown] public 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); } GVFSServiceProcess.UninstallService(); } PrintTestCaseStats.PrintRunTimeStats(); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Program.cs ================================================ using GVFS.Common; using GVFS.FunctionalTests.Properties; using GVFS.FunctionalTests.Tools; using GVFS.PlatformLoader; using GVFS.Tests; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; namespace GVFS.FunctionalTests { public class Program { public static void Main(string[] args) { Properties.Settings.Default.Initialize(); GVFSPlatformLoader.Initialize(); GVFSTestConfig.IsDevMode = Environment.GetEnvironmentVariable("GVFS_FUNCTIONAL_TEST_DEV_MODE") == "1"; Console.WriteLine("Settings.Default.CurrentDirectory: {0}", Settings.Default.CurrentDirectory); Console.WriteLine("Settings.Default.PathToGit: {0}", Settings.Default.PathToGit); Console.WriteLine("Settings.Default.PathToGVFS: {0}", Settings.Default.PathToGVFS); Console.WriteLine("Settings.Default.PathToGVFSService: {0}", Settings.Default.PathToGVFSService); if (GVFSTestConfig.IsDevMode) { Console.WriteLine("*** Dev mode enabled (GVFS_FUNCTIONAL_TEST_DEV_MODE=1) ***"); } NUnitRunner runner = new NUnitRunner(args); runner.AddGlobalSetupIfNeeded("GVFS.FunctionalTests.GlobalSetup"); if (runner.HasCustomArg("--debug")) { Debugger.Launch(); } if (runner.HasCustomArg("--no-shared-gvfs-cache")) { Console.WriteLine("Running without a shared git object cache"); GVFSTestConfig.NoSharedCache = true; } if (runner.HasCustomArg("--replace-inbox-projfs")) { Console.WriteLine("Tests will replace inbox ProjFS"); GVFSTestConfig.ReplaceInboxProjFS = true; } GVFSTestConfig.LocalCacheRoot = runner.GetCustomArgWithParam("--shared-gvfs-cache-root"); HashSet includeCategories = new HashSet(); HashSet excludeCategories = new HashSet(); if (runner.HasCustomArg("--full-suite")) { Console.WriteLine("Running the full suite of tests"); List modes = new List(); foreach (Settings.ValidateWorkingTreeMode mode in Enum.GetValues(typeof(Settings.ValidateWorkingTreeMode))) { modes.Add(new object[] { mode }); } GVFSTestConfig.GitRepoTestsValidateWorkTree = modes.ToArray(); GVFSTestConfig.FileSystemRunners = FileSystemRunners.FileSystemRunner.AllWindowsRunners; } else { Settings.ValidateWorkingTreeMode validateMode = Settings.ValidateWorkingTreeMode.Full; if (runner.HasCustomArg("--sparse-mode")) { validateMode = Settings.ValidateWorkingTreeMode.SparseMode; // Only test the git commands in sparse mode for splitting out tests in builds includeCategories.Add(Categories.GitCommands); } GVFSTestConfig.GitRepoTestsValidateWorkTree = new object[] { new object[] { validateMode }, }; if (runner.HasCustomArg("--extra-only")) { Console.WriteLine("Running only the tests marked as ExtraCoverage"); includeCategories.Add(Categories.ExtraCoverage); } else { excludeCategories.Add(Categories.ExtraCoverage); } // If we're running in CI exclude tests that are currently // flakey or broken when run in a CI environment. if (runner.HasCustomArg("--ci")) { excludeCategories.Add(Categories.NeedsReactionInCI); } GVFSTestConfig.FileSystemRunners = FileSystemRunners.FileSystemRunner.DefaultRunners; } (uint, uint)? testSlice = null; string testSliceArg = runner.GetCustomArgWithParam("--slice"); if (testSliceArg != null) { // split `testSliceArg` on a comma and parse the two values as uints string[] parts = testSliceArg.Split(','); uint sliceNumber; uint totalSlices; if (parts.Length != 2 || !uint.TryParse(parts[0], out sliceNumber) || !uint.TryParse(parts[1], out totalSlices) || totalSlices == 0 || sliceNumber >= totalSlices) { throw new Exception("Invalid argument to --slice. Expected format: X,Y where X is the slice number and Y is the total number of slices"); } testSlice = (sliceNumber, totalSlices); } GVFSTestConfig.DotGVFSRoot = ".gvfs"; GVFSTestConfig.RepoToClone = runner.GetCustomArgWithParam("--repo-to-clone") ?? Properties.Settings.Default.RepoToClone; RunBeforeAnyTests(); Environment.ExitCode = runner.RunTests(includeCategories, excludeCategories, testSlice); if (Debugger.IsAttached) { Console.WriteLine("Tests completed. Press Enter to exit."); Console.ReadLine(); } } private static void RunBeforeAnyTests() { if (GVFSTestConfig.ReplaceInboxProjFS) { ProjFSFilterInstaller.ReplaceInboxProjFS(); } GVFSServiceProcess.InstallService(); string serviceProgramDataDir = GVFSPlatform.Instance.GetSecureDataRootForGVFSComponent( GVFSConstants.Service.ServiceName); string statusCacheVersionTokenPath = Path.Combine( serviceProgramDataDir, "EnableGitStatusCacheToken.dat"); if (!File.Exists(statusCacheVersionTokenPath)) { Directory.CreateDirectory(serviceProgramDataDir); File.WriteAllText(statusCacheVersionTokenPath, string.Empty); } } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Settings.cs ================================================ using System; using System.IO; using System.Runtime.InteropServices; namespace GVFS.FunctionalTests.Properties { public static class Settings { public enum ValidateWorkingTreeMode { None = 0, Full = 1, SparseMode = 2, } public static class Default { public static string CurrentDirectory { get; private set; } public static string RepoToClone { get; set; } public static string PathToBash { get; set; } public static string PathToGVFS { get; set; } public static string Commitish { get; set; } public static string ControlGitRepoRoot { get; set; } public static string EnlistmentRoot { get; set; } public static string FastFetchBaseRoot { get; set; } public static string FastFetchRoot { get; set; } public static string FastFetchControl { get; set; } public static string PathToGit { get; set; } public static string PathToGVFSService { get; set; } public static string BinaryFileNameExtension { get; set; } public static void Initialize() { string testExec = System.Reflection.Assembly.GetEntryAssembly().Location; CurrentDirectory = Path.GetFullPath(Path.GetDirectoryName(testExec)); RepoToClone = @"https://gvfs.visualstudio.com/ci/_git/ForTests"; // HACK: This is only different from FunctionalTests/20180214 // in that it deletes the GVFlt_MoveFileTests/LongFileName folder, // which is causing problems in all tests due to a ProjFS // regression. Replace this with the expected default after // ProjFS is fixed and deployed to our build machines. Commitish = @"FunctionalTests/20201014"; EnlistmentRoot = @"C:\Repos\GVFSFunctionalTests\enlistment"; ControlGitRepoRoot = @"C:\Repos\GVFSFunctionalTests\ControlRepo"; FastFetchBaseRoot = @"C:\Repos\GVFSFunctionalTests\FastFetch"; FastFetchRoot = Path.Combine(FastFetchBaseRoot, "test"); FastFetchControl = Path.Combine(FastFetchBaseRoot, "control"); BinaryFileNameExtension = ".exe"; string devModeOutDir = Environment.GetEnvironmentVariable("GVFS_DEV_OUT_DIR"); if (!string.IsNullOrEmpty(devModeOutDir)) { string configuration = Environment.GetEnvironmentVariable("GVFS_DEV_CONFIGURATION") ?? "Debug"; string payloadDir = Path.Combine(devModeOutDir, "GVFS.Payload", "bin", configuration, "win-x64"); PathToGVFS = Path.Combine(payloadDir, "gvfs.exe"); PathToGVFSService = Path.Combine(payloadDir, "GVFS.Service.exe"); } else { PathToGVFS = @"C:\Program Files\VFS for Git\GVFS.exe"; PathToGVFSService = @"C:\Program Files\VFS for Git\GVFS.Service.exe"; } PathToGit = @"C:\Program Files\Git\cmd\git.exe"; PathToBash = @"C:\Program Files\Git\bin\bash.exe"; } } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Should/FileSystemShouldExtensions.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace GVFS.FunctionalTests.Should { public static class FileSystemShouldExtensions { // This attribute only appears in directory enumeration classes (FILE_DIRECTORY_INFORMATION, // FILE_BOTH_DIR_INFORMATION, etc.). When this attribute is set, it means that the file or // directory has no physical representation on the local system; the item is virtual. Opening the // item will be more expensive than normal, e.g. it will cause at least some of it to be fetched // from a remote store. // // #define FILE_ATTRIBUTE_RECALL_ON_OPEN 0x00040000 // winnt public const int FileAttributeRecallOnOpen = 0x00040000; // When this attribute is set, it means that the file or directory is not fully present locally. // For a file that means that not all of its data is on local storage (e.g. it is sparse with some // data still in remote storage). For a directory it means that some of the directory contents are // being virtualized from another location. Reading the file / enumerating the directory will be // more expensive than normal, e.g. it will cause at least some of the file/directory content to be // fetched from a remote store. Only kernel-mode callers can set this bit. // // #define FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS 0x00400000 // winnt public const int FileAttributeRecallOnDataAccess = 0x00400000; public static FileAdapter ShouldBeAFile(this string path, FileSystemRunner runner) { return new FileAdapter(path, runner); } public static FileAdapter ShouldBeAFile(this FileSystemInfo fileSystemInfo, FileSystemRunner runner) { return new FileAdapter(fileSystemInfo.FullName, runner); } public static DirectoryAdapter ShouldBeADirectory(this string path, FileSystemRunner runner) { return new DirectoryAdapter(path, runner); } public static DirectoryAdapter ShouldBeADirectory(this FileSystemInfo fileSystemInfo, FileSystemRunner runner) { return new DirectoryAdapter(fileSystemInfo.FullName, runner); } public static string ShouldNotExistOnDisk(this string path, FileSystemRunner runner) { runner.FileExists(path).ShouldEqual(false, "File " + path + " exists when it should not"); runner.DirectoryExists(path).ShouldEqual(false, "Directory " + path + " exists when it should not"); return path; } public class FileAdapter { private const int MaxWaitMS = 2000; private const int ThreadSleepMS = 100; private FileSystemRunner runner; public FileAdapter(string path, FileSystemRunner runner) { this.runner = runner; this.runner.FileExists(path).ShouldEqual(true, "Path does NOT exist: " + path); this.Path = path; } public string Path { get; private set; } public string WithContents() { return this.runner.ReadAllText(this.Path); } public FileAdapter WithContents(string expectedContents) { this.runner.ReadAllText(this.Path).ShouldEqual(expectedContents, "The contents of " + this.Path + " do not match what was expected"); return this; } public FileAdapter WithCaseMatchingName(string expectedName) { FileInfo fileInfo = new FileInfo(this.Path); string parentPath = System.IO.Path.GetDirectoryName(this.Path); DirectoryInfo parentInfo = new DirectoryInfo(parentPath); expectedName.Equals(parentInfo.GetFileSystemInfos(fileInfo.Name)[0].Name, StringComparison.Ordinal) .ShouldEqual(true, this.Path + " does not have the correct case"); return this; } public FileInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess) { FileInfo info = new FileInfo(this.Path); info.CreationTime.ShouldEqual(creation, "Creation time does not match"); info.LastAccessTime.ShouldEqual(lastAccess, "Last access time does not match"); info.LastWriteTime.ShouldEqual(lastWrite, "Last write time does not match"); return info; } public FileInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess, FileAttributes attributes) { FileInfo info = this.WithInfo(creation, lastWrite, lastAccess); info.Attributes.ShouldEqual(attributes, "Attributes do not match"); return info; } public FileInfo WithAttribute(FileAttributes attribute) { FileInfo info = new FileInfo(this.Path); info.Attributes.HasFlag(attribute).ShouldEqual(true, "Attributes do not have correct flag: " + attribute); return info; } public FileInfo WithoutAttribute(FileAttributes attribute) { FileInfo info = new FileInfo(this.Path); info.Attributes.HasFlag(attribute).ShouldEqual(false, "Attributes have incorrect flag: " + attribute); return info; } } public class DirectoryAdapter { private FileSystemRunner runner; public DirectoryAdapter(string path, FileSystemRunner runner) { this.runner = runner; this.runner.DirectoryExists(path).ShouldEqual(true, "Directory " + path + " does not exist"); this.Path = path; } public string Path { get; private set; } public void WithNoItems() { Directory.EnumerateFileSystemEntries(this.Path).ShouldBeEmpty(this.Path + " is not empty"); } public void WithNoItems(string searchPattern) { Directory.EnumerateFileSystemEntries(this.Path, searchPattern).ShouldBeEmpty(this.Path + " is not empty"); } public FileSystemInfo WithOneItem() { return this.WithItems(1).Single(); } public IEnumerable WithItems(int expectedCount) { IEnumerable items = this.WithItems(); items.Count().ShouldEqual(expectedCount, this.Path + " has an invalid number of items"); return items; } public IEnumerable WithItems() { return this.WithItems("*"); } public IEnumerable WithFiles() { IEnumerable items = this.WithItems(); IEnumerable files = items.Where(info => info is FileInfo).Cast(); files.Any().ShouldEqual(true, this.Path + " does not have any files. Contents: " + string.Join(",", items)); return files; } public IEnumerable WithDirectories() { IEnumerable items = this.WithItems(); IEnumerable directories = items.Where(info => info is DirectoryInfo).Cast(); directories.Any().ShouldEqual(true, this.Path + " does not have any directories. Contents: " + string.Join(",", items)); return directories; } public IEnumerable WithItems(string searchPattern) { DirectoryInfo directory = new DirectoryInfo(this.Path); IEnumerable items = directory.GetFileSystemInfos(searchPattern); items.Any().ShouldEqual(true, this.Path + " does not have any items"); return items; } public DirectoryAdapter WithDeepStructure( FileSystemRunner fileSystem, string otherPath, bool ignoreCase = false, bool compareContent = false, string[] withinPrefixes = null) { otherPath.ShouldBeADirectory(this.runner); CompareDirectories(fileSystem, otherPath, this.Path, ignoreCase, compareContent, withinPrefixes); return this; } public DirectoryAdapter WithCaseMatchingName(string expectedName) { DirectoryInfo info = new DirectoryInfo(this.Path); string parentPath = System.IO.Path.GetDirectoryName(this.Path); DirectoryInfo parentInfo = new DirectoryInfo(parentPath); expectedName.Equals(parentInfo.GetDirectories(info.Name)[0].Name, StringComparison.Ordinal) .ShouldEqual(true, this.Path + " does not have the correct case"); return this; } public DirectoryInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess) { DirectoryInfo info = new DirectoryInfo(this.Path); info.CreationTime.ShouldEqual(creation, "Creation time does not match"); info.LastAccessTime.ShouldEqual(lastAccess, "Last access time does not match"); info.LastWriteTime.ShouldEqual(lastWrite, "Last write time does not match"); return info; } public DirectoryInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess, FileAttributes attributes, bool ignoreRecallAttributes) { DirectoryInfo info = this.WithInfo(creation, lastWrite, lastAccess); if (ignoreRecallAttributes) { FileAttributes attributesWithoutRecall = info.Attributes & (FileAttributes)~(FileAttributeRecallOnOpen | FileAttributeRecallOnDataAccess); attributesWithoutRecall.ShouldEqual(attributes, "Attributes do not match"); } else { info.Attributes.ShouldEqual(attributes, "Attributes do not match"); } return info; } public DirectoryInfo WithAttribute(FileAttributes attribute) { DirectoryInfo info = new DirectoryInfo(this.Path); info.Attributes.HasFlag(attribute).ShouldEqual(true, "Attributes do not have correct flag: " + attribute); return info; } private static bool IsMatchedPath(FileSystemInfo info, string repoRoot, string[] prefixes, bool ignoreCase) { if (prefixes == null || prefixes.Length == 0) { return true; } string localPath = info.FullName.Substring(repoRoot.Length + 1); StringComparison pathComparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; if (localPath.Equals(".git", pathComparison)) { // Include _just_ the .git folder. // All sub-items are not included in the enumerator. return true; } if (!localPath.Contains(System.IO.Path.DirectorySeparatorChar) && (info.Attributes & FileAttributes.Directory) != FileAttributes.Directory) { // If it is a file in the root folder, then include it. return true; } foreach (string prefixDir in prefixes) { if (localPath.StartsWith(prefixDir, pathComparison)) { return true; } if (prefixDir.StartsWith(localPath, pathComparison) && Directory.Exists(info.FullName)) { // For example: localPath = "GVFS" and prefix is "GVFS\\GVFS". return true; } } return false; } private static void CompareDirectories( FileSystemRunner fileSystem, string expectedPath, string actualPath, bool ignoreCase, bool compareContent, string[] withinPrefixes) { IEnumerable expectedEntries = new DirectoryInfo(expectedPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories); IEnumerable actualEntries = new DirectoryInfo(actualPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories); string dotGitFolder = System.IO.Path.DirectorySeparatorChar + TestConstants.DotGit.Root + System.IO.Path.DirectorySeparatorChar; IEnumerator expectedEnumerator = expectedEntries .Where(x => !x.FullName.Contains(dotGitFolder)) .OrderBy(x => x.FullName) .Where(x => IsMatchedPath(x, expectedPath, withinPrefixes, ignoreCase)) .GetEnumerator(); IEnumerator actualEnumerator = actualEntries .Where(x => !x.FullName.Contains(dotGitFolder)) .OrderBy(x => x.FullName) .GetEnumerator(); bool expectedMoved = expectedEnumerator.MoveNext(); bool actualMoved = actualEnumerator.MoveNext(); while (expectedMoved && actualMoved) { bool nameIsEqual = false; if (ignoreCase) { nameIsEqual = actualEnumerator.Current.Name.Equals(expectedEnumerator.Current.Name, StringComparison.OrdinalIgnoreCase); } else { nameIsEqual = actualEnumerator.Current.Name.Equals(expectedEnumerator.Current.Name, StringComparison.Ordinal); } if (!nameIsEqual) { if ((expectedEnumerator.Current.Attributes & FileAttributes.Directory) == FileAttributes.Directory) { // ignoring directories that are empty in the control repo because GVFS does a better job at removing // empty directories because it is tracking placeholder folders and removes them // Only want to check for an empty directory if the names don't match. If the names match and // both expected and actual directories are empty that is okay if (Directory.GetFileSystemEntries(expectedEnumerator.Current.FullName, "*", SearchOption.TopDirectoryOnly).Length == 0) { expectedMoved = expectedEnumerator.MoveNext(); continue; } } Assert.Fail($"File names don't match: expected: {expectedEnumerator.Current.FullName} actual: {actualEnumerator.Current.FullName}"); } if ((expectedEnumerator.Current.Attributes & FileAttributes.Directory) == FileAttributes.Directory) { (actualEnumerator.Current.Attributes & FileAttributes.Directory).ShouldEqual(FileAttributes.Directory, $"expected directory path: {expectedEnumerator.Current.FullName} actual file path: {actualEnumerator.Current.FullName}"); } else { (actualEnumerator.Current.Attributes & FileAttributes.Directory).ShouldNotEqual(FileAttributes.Directory, $"expected file path: {expectedEnumerator.Current.FullName} actual directory path: {actualEnumerator.Current.FullName}"); FileInfo expectedFileInfo = (expectedEnumerator.Current as FileInfo).ShouldNotBeNull(); FileInfo actualFileInfo = (actualEnumerator.Current as FileInfo).ShouldNotBeNull(); actualFileInfo.Length.ShouldEqual(expectedFileInfo.Length, $"File lengths do not agree expected: {expectedEnumerator.Current.FullName} = {expectedFileInfo.Length} actual: {actualEnumerator.Current.FullName} = {actualFileInfo.Length}"); if (compareContent) { actualFileInfo.FullName.ShouldBeAFile(fileSystem).WithContents(expectedFileInfo.FullName.ShouldBeAFile(fileSystem).WithContents()); } } expectedMoved = expectedEnumerator.MoveNext(); actualMoved = actualEnumerator.MoveNext(); } StringBuilder errorEntries = new StringBuilder(); if (expectedMoved) { do { errorEntries.AppendLine(string.Format("Missing entry {0}", expectedEnumerator.Current.FullName)); } while (expectedEnumerator.MoveNext()); } while (actualEnumerator.MoveNext()) { errorEntries.AppendLine(string.Format("Extra entry {0}", actualEnumerator.Current.FullName)); } if (errorEntries.Length > 0) { Assert.Fail(errorEntries.ToString()); } } } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/DiskLayoutVersionTests.cs ================================================ using GVFS.FunctionalTests.Tests.EnlistmentPerTestCase; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; using System.Runtime.InteropServices; namespace GVFS.FunctionalTests.Tests { [TestFixture] [Category(Categories.ExtraCoverage)] public class DiskLayoutVersionTests : TestsWithEnlistmentPerTestCase { private const int CurrentDiskLayoutMinorVersion = 0; [TestCase] public void MountSucceedsIfMinorVersionHasAdvancedButNotMajorVersion() { // Advance the minor version, mount should still work this.Enlistment.UnmountGVFS(); GVFSHelpers.SaveDiskLayoutVersion( this.Enlistment.DotGVFSRoot, GVFSHelpers.GetCurrentDiskLayoutMajorVersion().ToString(), (CurrentDiskLayoutMinorVersion + 1).ToString()); this.Enlistment.TryMountGVFS().ShouldBeTrue("Mount should succeed because only the minor version advanced"); // Advance the major version, mount should fail this.Enlistment.UnmountGVFS(); GVFSHelpers.SaveDiskLayoutVersion( this.Enlistment.DotGVFSRoot, (GVFSHelpers.GetCurrentDiskLayoutMajorVersion() + 1).ToString(), CurrentDiskLayoutMinorVersion.ToString()); this.Enlistment.TryMountGVFS().ShouldBeFalse("Mount should fail because the major version has advanced"); } [TestCase] public void MountFailsIfBeforeMinimumVersion() { // Mount should fail if on disk version is below minimum supported version this.Enlistment.UnmountGVFS(); GVFSHelpers.SaveDiskLayoutVersion( this.Enlistment.DotGVFSRoot, (GVFSHelpers.GetCurrentDiskLayoutMinimumMajorVersion() - 1).ToString(), CurrentDiskLayoutMinorVersion.ToString()); this.Enlistment.TryMountGVFS().ShouldBeFalse("Mount should fail because we are before minimum version"); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/BasicFileSystemTests.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.FunctionalTests.Should; using GVFS.FunctionalTests.Tests.EnlistmentPerFixture; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; namespace GVFS.FunctionalTests.Tests.LongRunningEnlistment { [TestFixture] public class BasicFileSystemTests : TestsWithEnlistmentPerFixture { private const int FileAttributeSparseFile = 0x00000200; private const int FileAttributeReparsePoint = 0x00000400; private const int FileAttributeRecallOnDataAccess = 0x00400000; [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void ShrinkFileContents(FileSystemRunner fileSystem, string parentFolder) { string filename = Path.Combine(parentFolder, "ShrinkFileContents"); string originalVirtualContents = "0123456789"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(filename), originalVirtualContents); this.Enlistment.GetVirtualPathTo(filename).ShouldBeAFile(fileSystem).WithContents(originalVirtualContents); string newText = "112233"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(filename), newText); this.Enlistment.GetVirtualPathTo(filename).ShouldBeAFile(fileSystem).WithContents(newText); fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(filename)); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void GrowFileContents(FileSystemRunner fileSystem, string parentFolder) { string filename = Path.Combine(parentFolder, "GrowFileContents"); string originalVirtualContents = "112233"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(filename), originalVirtualContents); this.Enlistment.GetVirtualPathTo(filename).ShouldBeAFile(fileSystem).WithContents(originalVirtualContents); string newText = "0123456789"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(filename), newText); this.Enlistment.GetVirtualPathTo(filename).ShouldBeAFile(fileSystem).WithContents(newText); fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(filename)); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void FilesAreBufferedAndCanBeFlushed(FileSystemRunner fileSystem, string parentFolder) { string filename = Path.Combine(parentFolder, "FilesAreBufferedAndCanBeFlushed"); string filePath = this.Enlistment.GetVirtualPathTo(filename); byte[] buffer = System.Text.Encoding.ASCII.GetBytes("Some test data"); using (FileStream writeStream = File.Open(filePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite)) { writeStream.Write(buffer, 0, buffer.Length); using (FileStream readStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)) { readStream.Length.ShouldEqual(0); writeStream.Flush(); readStream.Length.ShouldEqual(buffer.Length); byte[] readBuffer = new byte[buffer.Length]; readStream.Read(readBuffer, 0, readBuffer.Length).ShouldEqual(readBuffer.Length); readBuffer.ShouldMatchInOrder(buffer); } } fileSystem.DeleteFile(filePath); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))] public void NewFileAttributesAreUpdated(string parentFolder) { string filename = Path.Combine(parentFolder, "FileAttributesAreUpdated"); FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; string virtualFile = this.Enlistment.GetVirtualPathTo(filename); virtualFile.ShouldNotExistOnDisk(fileSystem); File.Create(virtualFile).Dispose(); virtualFile.ShouldBeAFile(fileSystem); // Update defaults. FileInfo is not batched, so each of these will create a separate Open-Update-Close set. FileInfo before = new FileInfo(virtualFile); DateTime testValue = DateTime.Now + TimeSpan.FromDays(1); before.CreationTime = testValue; before.LastAccessTime = testValue; before.LastWriteTime = testValue; before.Attributes = FileAttributes.Hidden; // FileInfo caches information. We can refresh, but just to be absolutely sure... virtualFile.ShouldBeAFile(fileSystem).WithInfo(testValue, testValue, testValue, FileAttributes.Hidden); File.Delete(virtualFile); virtualFile.ShouldNotExistOnDisk(fileSystem); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))] public void NewFolderAttributesAreUpdated(string parentFolder) { string folderName = Path.Combine(parentFolder, "FolderAttributesAreUpdated"); string virtualFolder = this.Enlistment.GetVirtualPathTo(folderName); Directory.CreateDirectory(virtualFolder); FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; virtualFolder.ShouldBeADirectory(fileSystem); // Update defaults. DirectoryInfo is not batched, so each of these will create a separate Open-Update-Close set. DirectoryInfo before = new DirectoryInfo(virtualFolder); DateTime testValue = DateTime.Now + TimeSpan.FromDays(1); before.CreationTime = testValue; before.LastAccessTime = testValue; before.LastWriteTime = testValue; before.Attributes = FileAttributes.Hidden; // DirectoryInfo caches information. We can refresh, but just to be absolutely sure... virtualFolder.ShouldBeADirectory(fileSystem) .WithInfo(testValue, testValue, testValue, FileAttributes.Hidden | FileAttributes.Directory, ignoreRecallAttributes: false); Directory.Delete(virtualFolder); } [TestCase] public void ExpandedFileAttributesAreUpdated() { FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; string filename = Path.Combine("GVFS", "GVFS", "GVFS.csproj"); string virtualFile = this.Enlistment.GetVirtualPathTo(filename); // Update defaults. FileInfo is not batched, so each of these will create a separate Open-Update-Close set. FileInfo before = new FileInfo(virtualFile); DateTime testValue = DateTime.Now + TimeSpan.FromDays(1); // Setting the CreationTime results in a write handle being open to the file and the file being expanded before.CreationTime = testValue; before.LastAccessTime = testValue; before.LastWriteTime = testValue; before.Attributes = FileAttributes.Hidden; // FileInfo caches information. We can refresh, but just to be absolutely sure... FileInfo info = virtualFile.ShouldBeAFile(fileSystem).WithInfo(testValue, testValue, testValue); // Ignore the archive bit as it can be re-added to the file as part of its expansion to full FileAttributes attributes = info.Attributes & ~FileAttributes.Archive; int retryCount = 0; int maxRetries = 10; while (attributes != FileAttributes.Hidden && retryCount < maxRetries) { // ProjFS attributes are remoted asynchronously when files are converted to full FileAttributes attributesLessProjFS = attributes & (FileAttributes)~(FileAttributeSparseFile | FileAttributeReparsePoint | FileAttributeRecallOnDataAccess); attributesLessProjFS.ShouldEqual( FileAttributes.Hidden, $"Attributes (ignoring ProjFS attributes) do not match, expected: {FileAttributes.Hidden} actual: {attributesLessProjFS}"); ++retryCount; Thread.Sleep(500); info.Refresh(); attributes = info.Attributes & ~FileAttributes.Archive; } attributes.ShouldEqual(FileAttributes.Hidden, $"Attributes do not match, expected: {FileAttributes.Hidden} actual: {attributes}"); } [TestCase] public void UnhydratedFolderAttributesAreUpdated() { FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; string folderName = Path.Combine("GVFS", "GVFS", "CommandLine"); string virtualFolder = this.Enlistment.GetVirtualPathTo(folderName); // Update defaults. DirectoryInfo is not batched, so each of these will create a separate Open-Update-Close set. DirectoryInfo before = new DirectoryInfo(virtualFolder); DateTime testValue = DateTime.Now + TimeSpan.FromDays(1); before.CreationTime = testValue; before.LastAccessTime = testValue; before.LastWriteTime = testValue; before.Attributes = FileAttributes.Hidden; // DirectoryInfo caches information. We can refresh, but just to be absolutely sure... virtualFolder.ShouldBeADirectory(fileSystem) .WithInfo(testValue, testValue, testValue, FileAttributes.Hidden | FileAttributes.Directory, ignoreRecallAttributes: true); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void CannotWriteToReadOnlyFile(FileSystemRunner fileSystem, string parentFolder) { string filename = Path.Combine(parentFolder, "CannotWriteToReadOnlyFile"); string virtualFilePath = this.Enlistment.GetVirtualPathTo(filename); virtualFilePath.ShouldNotExistOnDisk(fileSystem); // Write initial contents string originalContents = "Contents of ReadOnly file"; fileSystem.WriteAllText(virtualFilePath, originalContents); virtualFilePath.ShouldBeAFile(fileSystem).WithContents(originalContents); // Make file read only FileInfo fileInfo = new FileInfo(virtualFilePath); fileInfo.Attributes = FileAttributes.ReadOnly; // Verify that file cannot be written to string newContents = "New contents for file"; fileSystem.WriteAllTextShouldFail(virtualFilePath, newContents); virtualFilePath.ShouldBeAFile(fileSystem).WithContents(originalContents); // Cleanup fileInfo.Attributes = FileAttributes.Normal; fileSystem.DeleteFile(virtualFilePath); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void ReadonlyCanBeSetAndUnset(FileSystemRunner fileSystem, string parentFolder) { string filename = Path.Combine(parentFolder, "ReadonlyCanBeSetAndUnset"); string virtualFilePath = this.Enlistment.GetVirtualPathTo(filename); virtualFilePath.ShouldNotExistOnDisk(fileSystem); string originalContents = "Contents of ReadOnly file"; fileSystem.WriteAllText(virtualFilePath, originalContents); // Make file read only FileInfo fileInfo = new FileInfo(virtualFilePath); fileInfo.Attributes = FileAttributes.ReadOnly; virtualFilePath.ShouldBeAFile(fileSystem).WithAttribute(FileAttributes.ReadOnly); // Clear read only fileInfo.Attributes = FileAttributes.Normal; virtualFilePath.ShouldBeAFile(fileSystem).WithoutAttribute(FileAttributes.ReadOnly); // Cleanup fileSystem.DeleteFile(virtualFilePath); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void ChangeVirtualNTFSFileNameCase(FileSystemRunner fileSystem, string parentFolder) { string oldFilename = Path.Combine(parentFolder, "ChangePhysicalFileNameCase.txt"); string newFilename = Path.Combine(parentFolder, "changephysicalfilenamecase.txt"); string fileContents = "Hello World"; FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, oldFilename, parentFolder); fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(oldFilename), fileContents); this.Enlistment.GetVirtualPathTo(oldFilename).ShouldBeAFile(fileSystem).WithContents(fileContents); this.Enlistment.GetVirtualPathTo(oldFilename).ShouldBeAFile(fileSystem).WithCaseMatchingName(Path.GetFileName(oldFilename)); fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), this.Enlistment.GetVirtualPathTo(newFilename)); this.Enlistment.GetVirtualPathTo(newFilename).ShouldBeAFile(fileSystem).WithContents(fileContents); this.Enlistment.GetVirtualPathTo(newFilename).ShouldBeAFile(fileSystem).WithCaseMatchingName(Path.GetFileName(newFilename)); fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(newFilename)); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, newFilename, parentFolder); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void ChangeVirtualNTFSFileName(FileSystemRunner fileSystem, string parentFolder) { string oldFilename = Path.Combine(parentFolder, "ChangePhysicalFileName.txt"); string newFilename = Path.Combine(parentFolder, "NewFileName.txt"); string fileContents = "Hello World"; FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, oldFilename, parentFolder); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, newFilename, parentFolder); fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(oldFilename), fileContents); this.Enlistment.GetVirtualPathTo(oldFilename).ShouldBeAFile(fileSystem).WithContents(fileContents); this.Enlistment.GetVirtualPathTo(newFilename).ShouldNotExistOnDisk(fileSystem); fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), this.Enlistment.GetVirtualPathTo(newFilename)); this.Enlistment.GetVirtualPathTo(newFilename).ShouldBeAFile(fileSystem).WithContents(fileContents); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, oldFilename, parentFolder); fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(newFilename)); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, newFilename, parentFolder); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void MoveVirtualNTFSFileToVirtualNTFSFolder(FileSystemRunner fileSystem, string parentFolder) { string testFolderName = Path.Combine(parentFolder, "test_folder"); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderName)); this.Enlistment.GetVirtualPathTo(testFolderName).ShouldBeADirectory(fileSystem); string testFileName = Path.Combine(parentFolder, "test.txt"); string testFileContents = "This is the contents of a test file"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(testFileName), testFileContents); this.Enlistment.GetVirtualPathTo(testFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); string newTestFileVirtualPath = Path.Combine( this.Enlistment.GetVirtualPathTo(testFolderName), Path.GetFileName(testFileName)); fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(testFileName), newTestFileVirtualPath); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFileName, parentFolder); newTestFileVirtualPath.ShouldBeAFile(fileSystem).WithContents(testFileContents); fileSystem.DeleteFile(newTestFileVirtualPath); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, Path.Combine(testFolderName, Path.GetFileName(testFileName)), parentFolder); fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(testFolderName)); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); } [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))] public void MoveWorkingDirectoryFileToDotGitFolder(FileSystemRunner fileSystem) { string testFolderName = ".git"; this.Enlistment.GetVirtualPathTo(testFolderName).ShouldBeADirectory(fileSystem); string testFileName = "test.txt"; this.Enlistment.GetVirtualPathTo(testFileName).ShouldNotExistOnDisk(fileSystem); string testFileContents = "This is the contents of a test file"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(testFileName), testFileContents); this.Enlistment.GetVirtualPathTo(testFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); string newTestFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(testFolderName), testFileName); fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(testFileName), newTestFileVirtualPath); this.Enlistment.GetVirtualPathTo(testFileName).ShouldNotExistOnDisk(fileSystem); newTestFileVirtualPath.ShouldBeAFile(fileSystem).WithContents(testFileContents); fileSystem.DeleteFile(newTestFileVirtualPath); newTestFileVirtualPath.ShouldNotExistOnDisk(fileSystem); } [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))] public void MoveDotGitFileToWorkingDirectoryFolder(FileSystemRunner fileSystem) { string testFolderName = "test_folder"; this.Enlistment.GetVirtualPathTo(testFolderName).ShouldNotExistOnDisk(fileSystem); fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderName)); this.Enlistment.GetVirtualPathTo(testFolderName).ShouldBeADirectory(fileSystem); string sourceFileFolder = ".git"; string testFileName = "config"; string sourceFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(sourceFileFolder), testFileName); string testFileContents = sourceFileVirtualPath.ShouldBeAFile(fileSystem).WithContents(); string targetTestFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(testFolderName), testFileName); fileSystem.MoveFile(sourceFileVirtualPath, targetTestFileVirtualPath); sourceFileVirtualPath.ShouldNotExistOnDisk(fileSystem); targetTestFileVirtualPath.ShouldBeAFile(fileSystem).WithContents(testFileContents); fileSystem.MoveFile(targetTestFileVirtualPath, sourceFileVirtualPath); sourceFileVirtualPath.ShouldBeAFile(fileSystem).WithContents(testFileContents); targetTestFileVirtualPath.ShouldNotExistOnDisk(fileSystem); fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(testFolderName)); this.Enlistment.GetVirtualPathTo(testFolderName).ShouldNotExistOnDisk(fileSystem); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void MoveVirtualNTFSFileToOverwriteVirtualNTFSFile(FileSystemRunner fileSystem, string parentFolder) { string targetFilename = Path.Combine(parentFolder, "TargetFile.txt"); string sourceFilename = Path.Combine(parentFolder, "SourceFile.txt"); string targetFileContents = "The Target"; string sourceFileContents = "The Source"; FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFilename, parentFolder); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, sourceFilename, parentFolder); fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(targetFilename), targetFileContents); this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(fileSystem).WithContents(targetFileContents); fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(sourceFilename), sourceFileContents); this.Enlistment.GetVirtualPathTo(sourceFilename).ShouldBeAFile(fileSystem).WithContents(sourceFileContents); fileSystem.ReplaceFile(this.Enlistment.GetVirtualPathTo(sourceFilename), this.Enlistment.GetVirtualPathTo(targetFilename)); this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(fileSystem).WithContents(sourceFileContents); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, sourceFilename, parentFolder); fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(targetFilename)); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFilename, parentFolder); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void MoveVirtualNTFSFileToInvalidFolder(FileSystemRunner fileSystem, string parentFolder) { string testFolderName = Path.Combine(parentFolder, "test_folder"); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); string testFileName = Path.Combine(parentFolder, "test.txt"); string testFileContents = "This is the contents of a test file"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(testFileName), testFileContents); this.Enlistment.GetVirtualPathTo(testFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); string newTestFileVirtualPath = Path.Combine( this.Enlistment.GetVirtualPathTo(testFolderName), Path.GetFileName(testFileName)); fileSystem.MoveFileShouldFail(this.Enlistment.GetVirtualPathTo(testFileName), newTestFileVirtualPath); newTestFileVirtualPath.ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(testFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(testFileName)); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFileName, parentFolder); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void DeletedFilesCanBeImmediatelyRecreated(FileSystemRunner fileSystem, string parentFolder) { string filename = Path.Combine(parentFolder, "DeletedFilesCanBeImmediatelyRecreated"); string filePath = this.Enlistment.GetVirtualPathTo(filename); filePath.ShouldNotExistOnDisk(fileSystem); string testData = "Some test data"; fileSystem.WriteAllText(filePath, testData); fileSystem.DeleteFile(filePath); // Do not check for delete. Doing so removes a race between deleting and writing. // This write will throw if the problem exists. fileSystem.WriteAllText(filePath, testData); filePath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(testData); fileSystem.DeleteFile(filePath); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.CanDeleteFilesWhileTheyAreOpenRunners))] public void CanDeleteFilesWhileTheyAreOpen(FileSystemRunner fileSystem, string parentFolder) { string filename = Path.Combine(parentFolder, "CanDeleteFilesWhileTheyAreOpen"); string filePath = this.Enlistment.GetVirtualPathTo(filename); byte[] buffer = System.Text.Encoding.ASCII.GetBytes("Some test data for writing"); using (FileStream deletableWriteStream = File.Open(filePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete)) { deletableWriteStream.Write(buffer, 0, buffer.Length); deletableWriteStream.Flush(); using (FileStream deletableReadStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete)) { byte[] readBuffer = new byte[buffer.Length]; deletableReadStream.Read(readBuffer, 0, readBuffer.Length).ShouldEqual(readBuffer.Length); readBuffer.ShouldMatchInOrder(buffer); fileSystem.DeleteFile(filePath); this.VerifyExistenceAfterDeleteWhileOpen(filePath, fileSystem); deletableWriteStream.Write(buffer, 0, buffer.Length); deletableWriteStream.Flush(); } } filePath.ShouldNotExistOnDisk(fileSystem); } [TestCase] public void CanDeleteHydratedFilesWhileTheyAreOpenForWrite() { FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; string fileName = "GVFS.sln"; string virtualPath = this.Enlistment.GetVirtualPathTo(fileName); virtualPath.ShouldBeAFile(fileSystem); using (Stream stream = new FileStream(virtualPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete)) using (StreamReader reader = new StreamReader(stream)) { // First line is empty, so read two lines string line = reader.ReadLine() + reader.ReadLine(); line.Length.ShouldNotEqual(0); File.Delete(virtualPath); this.VerifyExistenceAfterDeleteWhileOpen(virtualPath, fileSystem); using (StreamWriter writer = new StreamWriter(stream)) { writer.WriteLine("newline!"); writer.Flush(); this.VerifyExistenceAfterDeleteWhileOpen(virtualPath, fileSystem); } } virtualPath.ShouldNotExistOnDisk(fileSystem); } [TestCase] public void ProjectedBlobFileTimesMatchHead() { // TODO: 467539 - Update all runners to support getting create/modify/access times FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; string filename = "AuthoringTests.md"; string headFileName = Path.Combine(".git", "logs", "HEAD"); this.Enlistment.GetVirtualPathTo(headFileName).ShouldBeAFile(fileSystem); FileInfo headFileInfo = new FileInfo(this.Enlistment.GetVirtualPathTo(headFileName)); FileInfo fileInfo = new FileInfo(this.Enlistment.GetVirtualPathTo(filename)); fileInfo.CreationTime.ShouldEqual(headFileInfo.CreationTime); // Last access and last write can get set outside the test, make sure that are at least // as recent as the creation time on the HEAD file, and no later than now fileInfo.LastAccessTime.ShouldBeAtLeast(headFileInfo.CreationTime); fileInfo.LastWriteTime.ShouldBeAtLeast(headFileInfo.CreationTime); fileInfo.LastAccessTime.ShouldBeAtMost(DateTime.Now); fileInfo.LastWriteTime.ShouldBeAtMost(DateTime.Now); } [TestCase] public void ProjectedBlobFolderTimesMatchHead() { // TODO: 467539 - Update all runners to support getting create/modify/access times FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; string folderName = Path.Combine("GVFS", "GVFS.Tests"); string headFileName = Path.Combine(".git", "logs", "HEAD"); this.Enlistment.GetVirtualPathTo(headFileName).ShouldBeAFile(fileSystem); FileInfo headFileInfo = new FileInfo(this.Enlistment.GetVirtualPathTo(headFileName)); DirectoryInfo folderInfo = new DirectoryInfo(this.Enlistment.GetVirtualPathTo(folderName)); folderInfo.CreationTime.ShouldEqual(headFileInfo.CreationTime); // Last access and last write can get set outside the test, make sure that are at least // as recent as the creation time on the HEAD file, and no later than now folderInfo.LastAccessTime.ShouldBeAtLeast(headFileInfo.CreationTime); folderInfo.LastWriteTime.ShouldBeAtLeast(headFileInfo.CreationTime); folderInfo.LastAccessTime.ShouldBeAtMost(DateTime.Now); folderInfo.LastWriteTime.ShouldBeAtMost(DateTime.Now); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void NonExistentItemBehaviorIsCorrect(FileSystemRunner fileSystem, string parentFolder) { string nonExistentItem = Path.Combine(parentFolder, "BadFolderName"); string nonExistentItem2 = Path.Combine(parentFolder, "BadFolderName2"); string virtualPathToNonExistentItem = this.Enlistment.GetVirtualPathTo(nonExistentItem).ShouldNotExistOnDisk(fileSystem); string virtualPathToNonExistentItem2 = this.Enlistment.GetVirtualPathTo(nonExistentItem2).ShouldNotExistOnDisk(fileSystem); fileSystem.MoveFile_FileShouldNotBeFound(virtualPathToNonExistentItem, virtualPathToNonExistentItem2); fileSystem.DeleteFile_FileShouldNotBeFound(virtualPathToNonExistentItem); fileSystem.ReplaceFile_FileShouldNotBeFound(virtualPathToNonExistentItem, virtualPathToNonExistentItem2); fileSystem.ReadAllText_FileShouldNotBeFound(virtualPathToNonExistentItem); // TODO #457434 // fileSystem.MoveDirectoryShouldNotBeFound(nonExistentItem, true) fileSystem.DeleteDirectory_DirectoryShouldNotBeFound(virtualPathToNonExistentItem); // TODO #457434 // fileSystem.ReplaceDirectoryShouldNotBeFound(nonExistentItem, true) } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void RenameEmptyVirtualNTFSFolder(FileSystemRunner fileSystem, string parentFolder) { string testFolderName = Path.Combine(parentFolder, "test_folder"); string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName); testFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); fileSystem.CreateDirectory(testFolderVirtualPath); testFolderVirtualPath.ShouldBeADirectory(fileSystem); string newFolderName = Path.Combine(parentFolder, "test_folder_renamed"); string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(newFolderName); newFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); fileSystem.MoveDirectory(testFolderVirtualPath, newFolderVirtualPath); testFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); newFolderVirtualPath.ShouldBeADirectory(fileSystem); fileSystem.DeleteDirectory(newFolderVirtualPath); newFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void MoveVirtualNTFSFolderIntoVirtualNTFSFolder(FileSystemRunner fileSystem, string parentFolder) { string testFolderName = Path.Combine(parentFolder, "test_folder"); string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); fileSystem.CreateDirectory(testFolderVirtualPath); testFolderVirtualPath.ShouldBeADirectory(fileSystem); string targetFolderName = Path.Combine(parentFolder, "target_folder"); string targetFolderVirtualPath = this.Enlistment.GetVirtualPathTo(targetFolderName); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFolderName, parentFolder); fileSystem.CreateDirectory(targetFolderVirtualPath); targetFolderVirtualPath.ShouldBeADirectory(fileSystem); string testFileName = Path.Combine(testFolderName, "test.txt"); string testFileContents = "This is the contents of a test file"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(testFileName), testFileContents); this.Enlistment.GetVirtualPathTo(testFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); string newTestFolder = Path.Combine(targetFolderName, Path.GetFileName(testFolderName)); string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(newTestFolder); fileSystem.MoveDirectory(testFolderVirtualPath, newFolderVirtualPath); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); newFolderVirtualPath.ShouldBeADirectory(fileSystem); string newTestFileName = Path.Combine(newTestFolder, Path.GetFileName(testFileName)); this.Enlistment.GetVirtualPathTo(newTestFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); fileSystem.DeleteDirectory(targetFolderVirtualPath); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFolderName, parentFolder); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void RenameAndMoveVirtualNTFSFolderIntoVirtualNTFSFolder(FileSystemRunner fileSystem, string parentFolder) { string testFolderName = Path.Combine(parentFolder, "test_folder"); string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); fileSystem.CreateDirectory(testFolderVirtualPath); testFolderVirtualPath.ShouldBeADirectory(fileSystem); string targetFolderName = Path.Combine(parentFolder, "target_folder"); string targetFolderVirtualPath = this.Enlistment.GetVirtualPathTo(targetFolderName); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFolderName, parentFolder); fileSystem.CreateDirectory(targetFolderVirtualPath); targetFolderVirtualPath.ShouldBeADirectory(fileSystem); string testFileName = "test.txt"; string testFilePartialPath = Path.Combine(testFolderName, testFileName); string testFileContents = "This is the contents of a test file"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(testFilePartialPath), testFileContents); this.Enlistment.GetVirtualPathTo(testFilePartialPath).ShouldBeAFile(fileSystem).WithContents(testFileContents); string newTestFolder = Path.Combine(targetFolderName, "test_folder_renamed"); string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(newTestFolder); fileSystem.MoveDirectory(testFolderVirtualPath, newFolderVirtualPath); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); newFolderVirtualPath.ShouldBeADirectory(fileSystem); string newTestFileName = Path.Combine(newTestFolder, testFileName); this.Enlistment.GetVirtualPathTo(newTestFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); fileSystem.DeleteDirectory(targetFolderVirtualPath); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFolderName, parentFolder); } [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))] public void MoveVirtualNTFSFolderTreeIntoVirtualNTFSFolder(FileSystemRunner fileSystem) { string testFolderParent = "test_folder_parent"; string testFolderChild = "test_folder_child"; string testFolderGrandChild = "test_folder_grandchild"; string testFile = "test.txt"; this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem); // Create the folder tree (to move) fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent)); this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); string realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild); fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath)); this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); string realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath)); this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); string relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); string testFileContents = "This is the contents of a test file"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(relativeTestFilePath), testFileContents); this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); // Create the target string targetFolder = "target_folder"; this.Enlistment.GetVirtualPathTo(targetFolder).ShouldNotExistOnDisk(fileSystem); fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(targetFolder)); this.Enlistment.GetVirtualPathTo(targetFolder).ShouldBeADirectory(fileSystem); fileSystem.MoveDirectory( this.Enlistment.GetVirtualPathTo(testFolderParent), this.Enlistment.GetVirtualPathTo(Path.Combine(targetFolder, testFolderParent))); // The old tree structure should be gone this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldNotExistOnDisk(fileSystem); // The tree should have been moved under the target folder testFolderParent = Path.Combine(targetFolder, testFolderParent); realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild); realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); // Cleanup fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(targetFolder)); this.Enlistment.GetVirtualPathTo(targetFolder).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldNotExistOnDisk(fileSystem); } [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))] public void MoveDotGitFullFolderTreeToDotGitFullFolder(FileSystemRunner fileSystem) { string testFolderRoot = ".git"; string testFolderParent = "test_folder_parent"; string testFolderChild = "test_folder_child"; string testFolderGrandChild = "test_folder_grandchild"; string testFile = "test.txt"; this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderRoot, testFolderParent)).ShouldNotExistOnDisk(fileSystem); // Create the folder tree (to move) fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderRoot, testFolderParent))); this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderRoot, testFolderParent)).ShouldBeADirectory(fileSystem); string realtiveChildFolderPath = Path.Combine(testFolderRoot, testFolderParent, testFolderChild); fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath)); this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); string realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath)); this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); string relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); string testFileContents = "This is the contents of a test file"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(relativeTestFilePath), testFileContents); this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); // Create the target string targetFolder = Path.Combine(".git", "target_folder"); this.Enlistment.GetVirtualPathTo(targetFolder).ShouldNotExistOnDisk(fileSystem); fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(targetFolder)); this.Enlistment.GetVirtualPathTo(targetFolder).ShouldBeADirectory(fileSystem); fileSystem.MoveDirectory( this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderRoot, testFolderParent)), this.Enlistment.GetVirtualPathTo(Path.Combine(targetFolder, testFolderParent))); // The old tree structure should be gone this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderRoot, testFolderParent)).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldNotExistOnDisk(fileSystem); // The tree should have been moved under the target folder testFolderParent = Path.Combine(targetFolder, testFolderParent); realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild); realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); // Cleanup fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(targetFolder)); this.Enlistment.GetVirtualPathTo(targetFolder).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldNotExistOnDisk(fileSystem); this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldNotExistOnDisk(fileSystem); } [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))] public void DeleteIndexFileFails(FileSystemRunner fileSystem) { string indexFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(".git", "index")); indexFilePath.ShouldBeAFile(fileSystem); fileSystem.DeleteFile_AccessShouldBeDenied(indexFilePath); indexFilePath.ShouldBeAFile(fileSystem); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))] public void MoveVirtualNTFSFolderIntoInvalidFolder(FileSystemRunner fileSystem, string parentFolder) { string testFolderParent = Path.Combine(parentFolder, "test_folder_parent"); string testFolderChild = "test_folder_child"; string testFolderGrandChild = "test_folder_grandchild"; string testFile = "test.txt"; FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderParent, parentFolder); // Create the folder tree (to move) fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent)); this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); string realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild); fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath)); this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); string realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath)); this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); string relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); string testFileContents = "This is the contents of a test file"; fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(relativeTestFilePath), testFileContents); this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); string targetFolder = Path.Combine(parentFolder, "target_folder_does_not_exists"); this.Enlistment.GetVirtualPathTo(targetFolder).ShouldNotExistOnDisk(fileSystem); // This move should fail fileSystem.MoveDirectory_TargetShouldBeInvalid( this.Enlistment.GetVirtualPathTo(testFolderParent), this.Enlistment.GetVirtualPathTo(Path.Combine(targetFolder, Path.GetFileName(testFolderParent)))); // The old tree structure should still be there this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); // Cleanup fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent)); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderParent, parentFolder); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, realtiveChildFolderPath, parentFolder); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, realtiveGrandChildFolderPath, parentFolder); FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, relativeTestFilePath, parentFolder); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))] public void CreateFileInheritsParentDirectoryAttributes(string parentFolder) { string parentDirectoryPath = this.Enlistment.GetVirtualPathTo(Path.Combine(parentFolder, "CreateFileInheritsParentDirectoryAttributes")); FileSystemRunner.DefaultRunner.CreateDirectory(parentDirectoryPath); DirectoryInfo parentDirInfo = new DirectoryInfo(parentDirectoryPath); parentDirInfo.Attributes |= FileAttributes.NoScrubData; parentDirInfo.Attributes.HasFlag(FileAttributes.NoScrubData).ShouldEqual(true); string targetFilePath = Path.Combine(parentDirectoryPath, "TargetFile"); FileSystemRunner.DefaultRunner.WriteAllText(targetFilePath, "Some contents that don't matter"); targetFilePath.ShouldBeAFile(FileSystemRunner.DefaultRunner).WithAttribute(FileAttributes.NoScrubData); FileSystemRunner.DefaultRunner.DeleteDirectory(parentDirectoryPath); } [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))] public void CreateDirectoryInheritsParentDirectoryAttributes(string parentFolder) { string parentDirectoryPath = this.Enlistment.GetVirtualPathTo(Path.Combine(parentFolder, "CreateDirectoryInheritsParentDirectoryAttributes")); FileSystemRunner.DefaultRunner.CreateDirectory(parentDirectoryPath); DirectoryInfo parentDirInfo = new DirectoryInfo(parentDirectoryPath); parentDirInfo.Attributes |= FileAttributes.NoScrubData; parentDirInfo.Attributes.HasFlag(FileAttributes.NoScrubData).ShouldEqual(true); string targetDirPath = Path.Combine(parentDirectoryPath, "TargetDir"); FileSystemRunner.DefaultRunner.CreateDirectory(targetDirPath); targetDirPath.ShouldBeADirectory(FileSystemRunner.DefaultRunner).WithAttribute(FileAttributes.NoScrubData); FileSystemRunner.DefaultRunner.DeleteDirectory(parentDirectoryPath); } private void VerifyExistenceAfterDeleteWhileOpen(string filePath, FileSystemRunner fileSystem) { if (this.SupportsPosixDelete()) { filePath.ShouldNotExistOnDisk(fileSystem); } else { filePath.ShouldBeAFile(fileSystem); } } private bool SupportsPosixDelete() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724429(v=vs.85).aspx FileVersionInfo kernel32Info = FileVersionInfo.GetVersionInfo(Path.Combine(Environment.SystemDirectory, "kernel32.dll")); // 18362 is first build with posix delete as the default in windows if (kernel32Info.FileBuildPart >= 18362) { return true; } return false; } return true; } private class FileRunnersAndFolders { private const string DotGitFolder = ".git"; private static object[] allFolders = { new object[] { string.Empty }, new object[] { DotGitFolder }, }; public static object[] Runners { get { List runnersAndParentFolders = new List(); foreach (object[] runner in FileSystemRunner.Runners.ToList()) { runnersAndParentFolders.Add(new object[] { runner.ToList().First(), string.Empty }); runnersAndParentFolders.Add(new object[] { runner.ToList().First(), DotGitFolder }); } return runnersAndParentFolders.ToArray(); } } public static object[] CanDeleteFilesWhileTheyAreOpenRunners { get { // Don't use the BashRunner for the CanDeleteFilesWhileTheyAreOpen test as bash.exe (rm command) moves // the file to the recycle bin rather than deleting it if the file that is getting removed is currently open. List runnersAndParentFolders = new List(); foreach (object[] runner in FileSystemRunner.Runners.ToList()) { if (!(runner.ToList().First() is BashRunner)) { runnersAndParentFolders.Add(new object[] { runner.ToList().First(), string.Empty }); runnersAndParentFolders.Add(new object[] { runner.ToList().First(), DotGitFolder }); } } return runnersAndParentFolders.ToArray(); } } public static object[] Folders { get { return allFolders; } } public static void ShouldNotExistOnDisk(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem, string filename, string parentFolder) { enlistment.GetVirtualPathTo(filename).ShouldNotExistOnDisk(fileSystem); } } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/CacheServerTests.cs ================================================ using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixture] [Category(Categories.ExtraCoverage)] public class CacheServerTests : TestsWithEnlistmentPerFixture { private const string CustomUrl = "https://myCache"; [TestCase] public void SettingGitConfigChangesCacheServer() { ProcessResult result = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "config gvfs.cache-server " + CustomUrl); result.ExitCode.ShouldEqual(0, result.Errors); this.Enlistment.GetCacheServer().ShouldContain("Using cache server: User Defined (" + CustomUrl + ")"); } [TestCase] public void SetAndGetTests() { this.Enlistment.SetCacheServer("\"\"").ShouldContain("You must specify a value for the cache server"); string noneMessage = "Using cache server: None (" + this.Enlistment.RepoUrl + ")"; this.Enlistment.SetCacheServer("None").ShouldContain(noneMessage); this.Enlistment.GetCacheServer().ShouldContain(noneMessage); this.Enlistment.SetCacheServer(this.Enlistment.RepoUrl).ShouldContain(noneMessage); this.Enlistment.GetCacheServer().ShouldContain(noneMessage); this.Enlistment.SetCacheServer(CustomUrl).ShouldContain("Using cache server: User Defined (" + CustomUrl + ")"); this.Enlistment.GetCacheServer().ShouldContain("Using cache server: User Defined (" + CustomUrl + ")"); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/CloneTests.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.FunctionalTests.Should; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; using System; using System.Diagnostics; using System.IO; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixture] public class CloneTests : TestsWithEnlistmentPerFixture { private const int GVFSGenericError = 3; [TestCase] public void CloneInsideMountedEnlistment() { this.SubfolderCloneShouldFail(); } [TestCase] public void CloneInsideUnmountedEnlistment() { this.Enlistment.UnmountGVFS(); this.SubfolderCloneShouldFail(); this.Enlistment.MountGVFS(); } [TestCase] public void CloneWithLocalCachePathWithinSrc() { string newEnlistmentRoot = GVFSFunctionalTestEnlistment.GetUniqueEnlistmentRoot(); ProcessStartInfo processInfo = new ProcessStartInfo(GVFSTestConfig.PathToGVFS); processInfo.Arguments = $"clone {Properties.Settings.Default.RepoToClone} {newEnlistmentRoot} --local-cache-path {Path.Combine(newEnlistmentRoot, "src", ".gvfsCache")}"; processInfo.WindowStyle = ProcessWindowStyle.Hidden; processInfo.CreateNoWindow = true; processInfo.WorkingDirectory = Path.GetDirectoryName(this.Enlistment.EnlistmentRoot); processInfo.UseShellExecute = false; processInfo.RedirectStandardOutput = true; ProcessResult result = ProcessHelper.Run(processInfo); result.ExitCode.ShouldEqual(GVFSGenericError); result.Output.ShouldContain("'--local-cache-path' cannot be inside the src folder"); } [TestCase] public void CloneToPathWithSpaces() { GVFSFunctionalTestEnlistment enlistment = GVFSFunctionalTestEnlistment.CloneAndMountEnlistmentWithSpacesInPath(GVFSTestConfig.PathToGVFS); enlistment.UnmountAndDeleteAll(); } [TestCase] public void CloneCreatesCorrectFilesInRoot() { GVFSFunctionalTestEnlistment enlistment = GVFSFunctionalTestEnlistment.CloneAndMount(GVFSTestConfig.PathToGVFS); try { string[] files = Directory.GetFiles(enlistment.EnlistmentRoot); files.Length.ShouldEqual(1); files.ShouldContain(x => Path.GetFileName(x).Equals("git.cmd", StringComparison.Ordinal)); string[] directories = Directory.GetDirectories(enlistment.EnlistmentRoot); directories.Length.ShouldEqual(2); directories.ShouldContain(x => Path.GetFileName(x).Equals(GVFSTestConfig.DotGVFSRoot, StringComparison.Ordinal)); directories.ShouldContain(x => Path.GetFileName(x).Equals("src", StringComparison.Ordinal)); } finally { enlistment.UnmountAndDeleteAll(); } } private void SubfolderCloneShouldFail() { ProcessStartInfo processInfo = new ProcessStartInfo(GVFSTestConfig.PathToGVFS); processInfo.Arguments = "clone " + GVFSTestConfig.RepoToClone + " src\\gvfs\\test1"; processInfo.WindowStyle = ProcessWindowStyle.Hidden; processInfo.CreateNoWindow = true; processInfo.WorkingDirectory = this.Enlistment.EnlistmentRoot; processInfo.UseShellExecute = false; processInfo.RedirectStandardOutput = true; ProcessResult result = ProcessHelper.Run(processInfo); result.ExitCode.ShouldEqual(GVFSGenericError); result.Output.ShouldContain("You can't clone inside an existing GVFS repo"); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/DehydrateTests.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.FunctionalTests.Should; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using Microsoft.Win32.SafeHandles; using NUnit.Framework; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixture] [Category(Categories.ExtraCoverage)] public class DehydrateTests : TestsWithEnlistmentPerFixture { private const string FolderDehydrateSuccessfulMessage = "folder dehydrate successful."; private const int GVFSGenericError = 3; private const uint FileFlagBackupSemantics = 0x02000000; private FileSystemRunner fileSystem; // Set forcePerRepoObjectCache to true so that DehydrateShouldSucceedEvenIfObjectCacheIsDeleted does // not delete the shared local cache public DehydrateTests() : base(forcePerRepoObjectCache: true) { this.fileSystem = new SystemIORunner(); } [TearDown] public void TearDown() { string backupFolder = Path.Combine(this.Enlistment.EnlistmentRoot, "dehydrate_backup"); if (this.fileSystem.DirectoryExists(backupFolder)) { this.fileSystem.DeleteDirectory(backupFolder); } if (!this.Enlistment.IsMounted()) { this.Enlistment.MountGVFS(); } } [TestCase] public void DehydrateShouldExitWithoutConfirm() { this.DehydrateShouldSucceed(new[] { "To actually execute the dehydrate, run 'gvfs dehydrate --confirm'" }, confirm: false, noStatus: false); } [TestCase] public void DehydrateShouldSucceedInCommonCase() { this.DehydrateShouldSucceed(new[] { "folder dehydrate successful." }, confirm: true, noStatus: false); } [TestCase] public void FullDehydrateShouldExitWithoutConfirm() { this.DehydrateShouldSucceed(new[] { "To actually execute the dehydrate, run 'gvfs dehydrate --confirm --full'" }, confirm: false, noStatus: false, full: true); } [TestCase] public void FullDehydrateShouldSucceedInCommonCase() { this.DehydrateShouldSucceed(new[] { "The repo was successfully dehydrated and remounted" }, confirm: true, noStatus: false, full: true); } [TestCase] public void DehydrateShouldFailOnUnmountedRepoWithStatus() { this.Enlistment.UnmountGVFS(); this.DehydrateShouldFail(new[] { "Failed to run git status because the repo is not mounted" }, noStatus: false); } [TestCase] public void DehydrateShouldSucceedEvenIfObjectCacheIsDeleted() { this.Enlistment.UnmountGVFS(); RepositoryHelpers.DeleteTestDirectory(this.Enlistment.GetObjectRoot(this.fileSystem)); this.DehydrateShouldSucceed(new[] { "The repo was successfully dehydrated and remounted" }, confirm: true, noStatus: true, full: true); } [TestCase] public void DehydrateShouldBackupFiles() { this.DehydrateShouldSucceed(new[] { "The repo was successfully dehydrated and remounted" }, confirm: true, noStatus: false, full: true); string backupFolder = Path.Combine(this.Enlistment.EnlistmentRoot, "dehydrate_backup"); backupFolder.ShouldBeADirectory(this.fileSystem); string[] backupFolderItems = this.fileSystem.EnumerateDirectory(backupFolder).Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); backupFolderItems.Length.ShouldEqual(1); this.DirectoryShouldContain(backupFolderItems[0], ".git", GVFSTestConfig.DotGVFSRoot, "src"); // .git folder items string gitFolder = Path.Combine(backupFolderItems[0], ".git"); this.DirectoryShouldContain(gitFolder, "index"); // .gvfs folder items string gvfsFolder = Path.Combine(backupFolderItems[0], GVFSTestConfig.DotGVFSRoot); this.DirectoryShouldContain(gvfsFolder, "databases", "GVFS_projection"); string gvfsDatabasesFolder = Path.Combine(gvfsFolder, "databases"); this.DirectoryShouldContain(gvfsDatabasesFolder, "BackgroundGitOperations.dat", "ModifiedPaths.dat", "VFSForGit.sqlite"); } [TestCase] public void DehydrateShouldFailIfLocalCacheNotInMetadata() { this.Enlistment.UnmountGVFS(); string majorVersion; string minorVersion; GVFSHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotGVFSRoot, out majorVersion, out minorVersion); string objectsRoot = GVFSHelpers.GetPersistedGitObjectsRoot(this.Enlistment.DotGVFSRoot).ShouldNotBeNull(); string metadataPath = Path.Combine(this.Enlistment.DotGVFSRoot, GVFSHelpers.RepoMetadataName); string metadataBackupPath = metadataPath + ".backup"; this.fileSystem.MoveFile(metadataPath, metadataBackupPath); this.fileSystem.CreateEmptyFile(metadataPath); GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, majorVersion, minorVersion); GVFSHelpers.SaveGitObjectsRoot(this.Enlistment.DotGVFSRoot, objectsRoot); this.DehydrateShouldFail(new[] { "Failed to determine local cache path from repo metadata" }, noStatus: true, full: true); this.fileSystem.DeleteFile(metadataPath); this.fileSystem.MoveFile(metadataBackupPath, metadataPath); } [TestCase] public void DehydrateShouldFailIfGitObjectsRootNotInMetadata() { this.Enlistment.UnmountGVFS(); string majorVersion; string minorVersion; GVFSHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotGVFSRoot, out majorVersion, out minorVersion); string localCacheRoot = GVFSHelpers.GetPersistedLocalCacheRoot(this.Enlistment.DotGVFSRoot).ShouldNotBeNull(); string metadataPath = Path.Combine(this.Enlistment.DotGVFSRoot, GVFSHelpers.RepoMetadataName); string metadataBackupPath = metadataPath + ".backup"; this.fileSystem.MoveFile(metadataPath, metadataBackupPath); this.fileSystem.CreateEmptyFile(metadataPath); GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, majorVersion, minorVersion); GVFSHelpers.SaveLocalCacheRoot(this.Enlistment.DotGVFSRoot, localCacheRoot); this.DehydrateShouldFail(new[] { "Failed to determine git objects root from repo metadata" }, noStatus: true, full: true); this.fileSystem.DeleteFile(metadataPath); this.fileSystem.MoveFile(metadataBackupPath, metadataPath); } [TestCase] public void DehydrateShouldFailOnWrongDiskLayoutVersion() { this.Enlistment.UnmountGVFS(); string majorVersion; string minorVersion; GVFSHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotGVFSRoot, out majorVersion, out minorVersion); int majorVersionNum; int minorVersionNum; int.TryParse(majorVersion.ShouldNotBeNull(), out majorVersionNum).ShouldEqual(true); int.TryParse(minorVersion.ShouldNotBeNull(), out minorVersionNum).ShouldEqual(true); int previousMajorVersionNum = majorVersionNum - 1; if (previousMajorVersionNum >= GVFSHelpers.GetCurrentDiskLayoutMinimumMajorVersion()) { GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, previousMajorVersionNum.ToString(), "0"); this.DehydrateShouldFail(new[] { "disk layout version doesn't match current version" }, noStatus: true, full: true); } GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, (majorVersionNum + 1).ToString(), "0"); this.DehydrateShouldFail(new[] { "Changes to GVFS disk layout do not allow mounting after downgrade." }, noStatus: true, full: true); GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, majorVersionNum.ToString(), minorVersionNum.ToString()); } [TestCase] public void FolderDehydrateFolderThatWasEnumerated() { string folderToDehydrate = "GVFS"; TestPath folderToEnumerate = new TestPath(this.Enlistment, folderToDehydrate); TestPath subFolderToEnumerate = new TestPath(this.Enlistment, Path.Combine(folderToDehydrate, "GVFS")); this.fileSystem.EnumerateDirectory(folderToEnumerate.VirtualPath); this.fileSystem.EnumerateDirectory(subFolderToEnumerate.VirtualPath); this.DehydrateShouldSucceed(new[] { $"{folderToDehydrate} {FolderDehydrateSuccessfulMessage}" }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate); this.Enlistment.UnmountGVFS(); // Use the backing path because on some platforms // the virtual path is no longer accessible after unmounting. this.CheckDehydratedFolderAfterUnmount(folderToEnumerate.BackingPath); subFolderToEnumerate.BackingPath.ShouldNotExistOnDisk(this.fileSystem); } [TestCase] public void FolderDehydrateFolderWithFilesThatWerePlaceholders() { string folderToDehydrate = "GVFS"; TestPath folderToReadFiles = new TestPath(this.Enlistment, folderToDehydrate); TestPath fileToRead = new TestPath(this.Enlistment, Path.Combine(folderToDehydrate, "GVFS", "Program.cs")); using (File.OpenRead(fileToRead.VirtualPath)) { } this.DehydrateShouldSucceed(new[] { $"{folderToDehydrate} {FolderDehydrateSuccessfulMessage}" }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate); this.Enlistment.UnmountGVFS(); // Use the backing path because on some platforms // the virtual path is no longer accessible after unmounting. this.CheckDehydratedFolderAfterUnmount(folderToReadFiles.BackingPath); fileToRead.BackingPath.ShouldNotExistOnDisk(this.fileSystem); } [TestCase] public void FolderDehydrateFolderWithFilesThatWereRead() { string folderToDehydrate = "GVFS"; TestPath folderToReadFiles = new TestPath(this.Enlistment, folderToDehydrate); TestPath fileToRead = new TestPath(this.Enlistment, Path.Combine(folderToDehydrate, "GVFS", "Program.cs")); this.fileSystem.ReadAllText(fileToRead.VirtualPath); this.fileSystem.EnumerateDirectory(folderToReadFiles.VirtualPath); this.DehydrateShouldSucceed(new[] { $"{folderToDehydrate} {FolderDehydrateSuccessfulMessage}" }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate); this.Enlistment.UnmountGVFS(); // Use the backing path because on some platforms // the virtual path is no longer accessible after unmounting. this.CheckDehydratedFolderAfterUnmount(folderToReadFiles.BackingPath); fileToRead.BackingPath.ShouldNotExistOnDisk(this.fileSystem); } [TestCase] public void FolderDehydrateFolderWithFilesThatWereWrittenTo() { string folderToDehydrate = "GVFS"; TestPath folderToWriteFiles = new TestPath(this.Enlistment, folderToDehydrate); TestPath fileToWrite = new TestPath(this.Enlistment, Path.Combine(folderToDehydrate, "GVFS", "Program.cs")); this.fileSystem.AppendAllText(fileToWrite.VirtualPath, "Append content"); GitProcess.Invoke(this.Enlistment.RepoRoot, "add ."); GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test"); this.DehydrateShouldSucceed(new[] { $"{folderToDehydrate} {FolderDehydrateSuccessfulMessage}" }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate); this.Enlistment.UnmountGVFS(); // Use the backing path because on some platforms // the virtual path is no longer accessible after unmounting. this.CheckDehydratedFolderAfterUnmount(folderToWriteFiles.BackingPath); fileToWrite.BackingPath.ShouldNotExistOnDisk(this.fileSystem); } [TestCase] public void FolderDehydrateFolderThatWasDeleted() { string folderToDehydrate = "Scripts"; TestPath folderToDelete = new TestPath(this.Enlistment, folderToDehydrate); this.fileSystem.DeleteDirectory(folderToDelete.VirtualPath); GitProcess.Invoke(this.Enlistment.RepoRoot, $"checkout -- {folderToDehydrate}"); this.DehydrateShouldSucceed(new[] { $"{folderToDehydrate} {FolderDehydrateSuccessfulMessage}" }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate); this.Enlistment.UnmountGVFS(); // Use the backing path because on some platforms // the virtual path is no longer accessible after unmounting. this.CheckDehydratedFolderAfterUnmount(folderToDelete.BackingPath); Path.Combine(folderToDelete.BackingPath, "RunUnitTests.bat").ShouldNotExistOnDisk(this.fileSystem); } [TestCase] public void FolderDehydrateFolderThatIsLocked() { const string folderToDehydrate = "GVFS"; const string folderToLock = "GVFS.Service"; TestPath folderPathDehydrated = new TestPath(this.Enlistment, folderToDehydrate); TestPath folderPathToLock = new TestPath(this.Enlistment, Path.Combine(folderToDehydrate, folderToLock)); TestPath fileToWrite = new TestPath(this.Enlistment, Path.Combine(folderToDehydrate, folderToLock, "Program.cs")); this.fileSystem.AppendAllText(fileToWrite.VirtualPath, "Append content"); GitProcess.Invoke(this.Enlistment.RepoRoot, $"reset --hard"); using (SafeFileHandle handle = this.OpenFolderHandle(folderPathToLock.VirtualPath)) { handle.IsInvalid.ShouldEqual(false); this.DehydrateShouldSucceed(new[] { $"{folderToDehydrate} {FolderDehydrateSuccessfulMessage}" }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate); } this.Enlistment.UnmountGVFS(); folderPathToLock.BackingPath.ShouldBeADirectory(this.fileSystem).WithNoItems(); folderPathDehydrated.BackingPath.ShouldBeADirectory(this.fileSystem).WithOneItem().Name.ShouldEqual(folderToLock); } [TestCase] public void FolderDehydrateFolderThatIsSubstringOfExistingFolder() { string folderToDehydrate = Path.Combine("GVFS", "GVFS"); TestPath fileToReadThenDehydrate = new TestPath(this.Enlistment, Path.Combine(folderToDehydrate, "Program.cs")); TestPath fileToWriteThenDehydrate = new TestPath(this.Enlistment, Path.Combine(folderToDehydrate, "App.config")); this.fileSystem.ReadAllText(fileToReadThenDehydrate.VirtualPath); this.fileSystem.AppendAllText(fileToWriteThenDehydrate.VirtualPath, "Append content"); string folderToNotDehydrate = Path.Combine("GVFS", "GVFS.Common"); TestPath fileToReadThenNotDehydrate = new TestPath(this.Enlistment, Path.Combine(folderToNotDehydrate, "GVFSLock.cs")); TestPath fileToWriteThenNotDehydrate = new TestPath(this.Enlistment, Path.Combine(folderToNotDehydrate, "Enlistment.cs")); this.fileSystem.ReadAllText(fileToReadThenNotDehydrate.VirtualPath); this.fileSystem.AppendAllText(fileToWriteThenNotDehydrate.VirtualPath, "Append content"); GitProcess.Invoke(this.Enlistment.RepoRoot, $"reset --hard"); this.DehydrateShouldSucceed(new[] { $"{folderToDehydrate} {FolderDehydrateSuccessfulMessage}" }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate); this.PlaceholdersShouldNotContain(folderToDehydrate, fileToReadThenDehydrate.BasePath); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileToWriteThenDehydrate.BasePath.Replace(Path.DirectorySeparatorChar, TestConstants.GitPathSeparator)); this.PlaceholdersShouldContain(folderToNotDehydrate, fileToReadThenNotDehydrate.BasePath); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileToWriteThenNotDehydrate.BasePath.Replace(Path.DirectorySeparatorChar, TestConstants.GitPathSeparator)); this.Enlistment.UnmountGVFS(); // Use the backing path because on some platforms // the virtual path is no longer accessible after unmounting. fileToReadThenDehydrate.BackingPath.ShouldNotExistOnDisk(this.fileSystem); fileToWriteThenDehydrate.BackingPath.ShouldNotExistOnDisk(this.fileSystem); fileToReadThenNotDehydrate.BackingPath.ShouldBeAFile(this.fileSystem); fileToWriteThenNotDehydrate.BackingPath.ShouldBeAFile(this.fileSystem); } [TestCase] public void FolderDehydrateNestedFoldersChildBeforeParent() { string parentFolderToDehydrate = "GVFS"; string childFolderToDehydrate = Path.Combine(parentFolderToDehydrate, "GVFS.Mount"); TestPath fileToReadInChildFolder = new TestPath(this.Enlistment, Path.Combine(childFolderToDehydrate, "Program.cs")); TestPath fileToReadInOtherChildFolder = new TestPath(this.Enlistment, Path.Combine(parentFolderToDehydrate, "GVFS.UnitTests", "Program.cs")); this.fileSystem.ReadAllText(fileToReadInChildFolder.VirtualPath); this.fileSystem.ReadAllText(fileToReadInOtherChildFolder.VirtualPath); this.DehydrateShouldSucceed( new[] { $"{childFolderToDehydrate} {FolderDehydrateSuccessfulMessage}", $"{parentFolderToDehydrate} {FolderDehydrateSuccessfulMessage}" }, confirm: true, noStatus: false, foldersToDehydrate: string.Join(";", childFolderToDehydrate, parentFolderToDehydrate)); this.Enlistment.UnmountGVFS(); // Use the backing path because on some platforms // the virtual path is no longer accessible after unmounting. fileToReadInChildFolder.BackingPath.ShouldNotExistOnDisk(this.fileSystem); fileToReadInOtherChildFolder.BackingPath.ShouldNotExistOnDisk(this.fileSystem); } [TestCase] public void FolderDehydrateNestedFoldersParentBeforeChild() { string parentFolderToDehydrate = "GVFS"; string childFolderToDehydrate = Path.Combine(parentFolderToDehydrate, "GVFS.Mount"); TestPath fileToReadInChildFolder = new TestPath(this.Enlistment, Path.Combine(childFolderToDehydrate, "Program.cs")); TestPath fileToReadInOtherChildFolder = new TestPath(this.Enlistment, Path.Combine(parentFolderToDehydrate, "GVFS.UnitTests", "Program.cs")); this.fileSystem.ReadAllText(fileToReadInChildFolder.VirtualPath); this.fileSystem.ReadAllText(fileToReadInOtherChildFolder.VirtualPath); this.DehydrateShouldSucceed( new[] { $"{parentFolderToDehydrate} {FolderDehydrateSuccessfulMessage}", $"Cannot dehydrate folder '{childFolderToDehydrate}': '{childFolderToDehydrate}' does not exist." }, confirm: true, noStatus: false, foldersToDehydrate: string.Join(";", parentFolderToDehydrate, childFolderToDehydrate)); this.Enlistment.UnmountGVFS(); // Use the backing path because on some platforms // the virtual path is no longer accessible after unmounting. fileToReadInChildFolder.BackingPath.ShouldNotExistOnDisk(this.fileSystem); fileToReadInOtherChildFolder.BackingPath.ShouldNotExistOnDisk(this.fileSystem); } [TestCase] public void FolderDehydrateParentFolderInModifiedPathsShouldOutputMessage() { string folderToDehydrateParentFolder = "GitCommandsTests"; TestPath folderToDelete = new TestPath(this.Enlistment, folderToDehydrateParentFolder); this.fileSystem.DeleteDirectory(folderToDelete.VirtualPath); GitProcess.Invoke(this.Enlistment.RepoRoot, "reset --hard"); string folderToDehydrate = Path.Combine(folderToDehydrateParentFolder, "DeleteFileTests"); this.Enlistment.GetVirtualPathTo(folderToDehydrate).ShouldBeADirectory(this.fileSystem); this.DehydrateShouldSucceed(new[] { $"Cannot dehydrate folder '{folderToDehydrate}': Must dehydrate parent folder '{folderToDehydrateParentFolder}/'." }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate); } [TestCase] public void FolderDehydrateDirtyStatusShouldFail() { string folderToDehydrate = "GVFS"; TestPath fileToCreate = new TestPath(this.Enlistment, Path.Combine(folderToDehydrate, $"{nameof(this.FolderDehydrateDirtyStatusShouldFail)}.txt")); this.fileSystem.WriteAllText(fileToCreate.VirtualPath, "new file contents"); fileToCreate.VirtualPath.ShouldBeAFile(this.fileSystem); this.DehydrateShouldFail(new[] { "Running git status...Failed", "Untracked files:", "git status reported that you have dirty files" }, noStatus: false, foldersToDehydrate: folderToDehydrate); GitProcess.Invoke(this.Enlistment.RepoRoot, "clean -xdf"); } [TestCase] public void FolderDehydrateDirtyStatusWithNoStatusShouldFail() { string folderToDehydrate = "GVFS"; TestPath fileToCreate = new TestPath(this.Enlistment, Path.Combine(folderToDehydrate, $"{nameof(this.FolderDehydrateDirtyStatusWithNoStatusShouldFail)}.txt")); this.fileSystem.WriteAllText(fileToCreate.VirtualPath, "new file contents"); fileToCreate.VirtualPath.ShouldBeAFile(this.fileSystem); this.DehydrateShouldFail(new[] { "Dehydrate --no-status not valid with --folders" }, noStatus: true, foldersToDehydrate: folderToDehydrate); GitProcess.Invoke(this.Enlistment.RepoRoot, "clean -xdf"); } [TestCase] public void FolderDehydrateCannotDehydrateDotGitFolder() { this.DehydrateShouldSucceed(new[] { $"Cannot dehydrate folder '{TestConstants.DotGit.Root}': invalid folder path." }, confirm: true, noStatus: false, foldersToDehydrate: TestConstants.DotGit.Root); this.DehydrateShouldSucceed(new[] { $"Cannot dehydrate folder '{TestConstants.DotGit.Info.Root}': invalid folder path." }, confirm: true, noStatus: false, foldersToDehydrate: TestConstants.DotGit.Info.Root); } [TestCase] public void FolderDehydratePreviouslyDeletedFolders() { string folderToDehydrate = "TrailingSlashTests"; TestPath folderToDelete = new TestPath(this.Enlistment, folderToDehydrate); string secondFolderToDehydrate = "FilenameEncoding"; TestPath secondFolderToDelete = new TestPath(this.Enlistment, secondFolderToDehydrate); this.fileSystem.DeleteDirectory(folderToDelete.VirtualPath); this.fileSystem.DeleteDirectory(secondFolderToDelete.VirtualPath); GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -a -m \"Delete directories\""); folderToDelete.VirtualPath.ShouldNotExistOnDisk(this.fileSystem); secondFolderToDelete.VirtualPath.ShouldNotExistOnDisk(this.fileSystem); GitProcess.Invoke(this.Enlistment.RepoRoot, "checkout -f HEAD~1"); folderToDelete.VirtualPath.ShouldBeADirectory(this.fileSystem); secondFolderToDelete.VirtualPath.ShouldBeADirectory(this.fileSystem); this.DehydrateShouldSucceed( new[] { $"{folderToDehydrate} {FolderDehydrateSuccessfulMessage}", $"{secondFolderToDehydrate} {FolderDehydrateSuccessfulMessage}", }, confirm: true, noStatus: false, foldersToDehydrate: new[] { folderToDehydrate, secondFolderToDehydrate }); folderToDelete.VirtualPath.ShouldBeADirectory(this.fileSystem); secondFolderToDelete.VirtualPath.ShouldBeADirectory(this.fileSystem); } [TestCase] public void FolderDehydrateTombstone() { string folderToDehydrate = "TrailingSlashTests"; TestPath folderToDelete = new TestPath(this.Enlistment, folderToDehydrate); this.fileSystem.DeleteDirectory(folderToDelete.VirtualPath); GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -a -m \"Delete a directory\""); this.DehydrateShouldSucceed(new[] { $"{folderToDehydrate} {FolderDehydrateSuccessfulMessage}" }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate); folderToDelete.VirtualPath.ShouldNotExistOnDisk(this.fileSystem); GitProcess.Invoke(this.Enlistment.RepoRoot, "checkout HEAD~1"); folderToDelete.VirtualPath.ShouldBeADirectory(this.fileSystem); } [TestCase] public void FolderDehydrateRelativePaths() { string[] foldersToDehydrate = new[] { Path.Combine("..", ".gvfs"), Path.DirectorySeparatorChar + Path.Combine("..", ".gvfs"), Path.Combine("GVFS", "..", "..", ".gvfs"), Path.Combine("GVFS/../../.gvfs"), }; List errorMessages = new List(); foreach (string path in foldersToDehydrate) { errorMessages.Add($"Cannot dehydrate folder '{path}': invalid folder path."); } this.DehydrateShouldSucceed( errorMessages.ToArray(), confirm: true, noStatus: false, foldersToDehydrate: foldersToDehydrate); } [TestCase] public void FolderDehydrateFolderThatDoesNotExist() { string folderToDehydrate = "DoesNotExist"; this.DehydrateShouldSucceed(new[] { $"Cannot dehydrate folder '{folderToDehydrate}': '{folderToDehydrate}' does not exist." }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate); } [TestCase] public void FolderDehydrateNewlyCreatedFolderAndFile() { string folderToDehydrate = "NewFolder"; TestPath folderToCreate = new TestPath(this.Enlistment, folderToDehydrate); this.fileSystem.CreateDirectory(folderToCreate.VirtualPath); TestPath fileToCreate = new TestPath(this.Enlistment, Path.Combine(folderToDehydrate, "newfile.txt")); this.fileSystem.WriteAllText(fileToCreate.VirtualPath, "Test content"); GitProcess.Invoke(this.Enlistment.RepoRoot, "add ."); GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test"); this.DehydrateShouldSucceed(new[] { $"{folderToDehydrate} {FolderDehydrateSuccessfulMessage}" }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate); this.Enlistment.UnmountGVFS(); // Use the backing path because on some platforms // the virtual path is no longer accessible after unmounting. fileToCreate.BackingPath.ShouldNotExistOnDisk(this.fileSystem); this.CheckDehydratedFolderAfterUnmount(folderToCreate.BackingPath); } private void PlaceholdersShouldContain(params string[] paths) { string[] placeholderLines = this.GetPlaceholderDatabaseLines(); foreach (string path in paths) { placeholderLines.ShouldContain(x => x.StartsWith(path + GVFSHelpers.PlaceholderFieldDelimiter, FileSystemHelpers.PathComparison)); } } private void PlaceholdersShouldNotContain(params string[] paths) { string[] placeholderLines = this.GetPlaceholderDatabaseLines(); foreach (string path in paths) { placeholderLines.ShouldNotContain(x => x.StartsWith(path + Path.DirectorySeparatorChar, FileSystemHelpers.PathComparison) || x.Equals(path, FileSystemHelpers.PathComparison)); } } private string[] GetPlaceholderDatabaseLines() { string placeholderDatabase = Path.Combine(this.Enlistment.DotGVFSRoot, "databases", "VFSForGit.sqlite"); return GVFSHelpers.GetAllSQLitePlaceholdersAsString(placeholderDatabase).Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); } private void DirectoryShouldContain(string directory, params string[] fileOrFolders) { IEnumerable onDiskItems = this.fileSystem.EnumerateDirectory(directory) .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) .Select(path => Path.GetFileName(path)) .OrderByDescending(x => x); onDiskItems.ShouldMatchInOrder(fileOrFolders.OrderByDescending(x => x)); } private void CheckDehydratedFolderAfterUnmount(string path) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { path.ShouldNotExistOnDisk(this.fileSystem); } else { path.ShouldBeADirectory(this.fileSystem); } } private void DehydrateShouldSucceed(string[] expectedInOutput, bool confirm, bool noStatus, bool full = false, params string[] foldersToDehydrate) { ProcessResult result = this.RunDehydrateProcess(confirm, noStatus, full, foldersToDehydrate); result.ExitCode.ShouldEqual(0, $"mount exit code was {result.ExitCode}. Output: {result.Output}"); if (result.Output.Contains("Failed to move the src folder: Access to the path")) { string output = this.RunHandleProcess(Path.Combine(this.Enlistment.EnlistmentRoot, "src")); TestContext.Out.WriteLine(output); } result.Output.ShouldContain(expectedInOutput); } private void DehydrateShouldFail(string[] expectedErrorMessages, bool noStatus, bool full = false, params string[] foldersToDehydrate) { ProcessResult result = this.RunDehydrateProcess(confirm: true, noStatus: noStatus, full: full, foldersToDehydrate: foldersToDehydrate); result.ExitCode.ShouldEqual(GVFSGenericError, $"mount exit code was not {GVFSGenericError}"); result.Output.ShouldContain(expectedErrorMessages); } private ProcessResult RunDehydrateProcess(bool confirm, bool noStatus, bool full = false, params string[] foldersToDehydrate) { string dehydrateFlags = string.Empty; if (confirm) { dehydrateFlags += " --confirm "; } if (noStatus) { dehydrateFlags += " --no-status "; } if (full) { dehydrateFlags += " --full "; } if (foldersToDehydrate.Length > 0) { dehydrateFlags += $" --folders {string.Join(";", foldersToDehydrate)}"; } string enlistmentRoot = this.Enlistment.EnlistmentRoot; ProcessStartInfo processInfo = new ProcessStartInfo(GVFSTestConfig.PathToGVFS); processInfo.Arguments = "dehydrate " + dehydrateFlags + " " + TestConstants.InternalUseOnlyFlag + " " + GVFSHelpers.GetInternalParameter(); processInfo.WindowStyle = ProcessWindowStyle.Hidden; processInfo.WorkingDirectory = enlistmentRoot; processInfo.UseShellExecute = false; processInfo.RedirectStandardOutput = true; return ProcessHelper.Run(processInfo); } private SafeFileHandle OpenFolderHandle(string path) { return NativeMethods.CreateFile( path, (uint)FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileFlagBackupSemantics, IntPtr.Zero); } private string RunHandleProcess(string path) { try { ProcessStartInfo processInfo = new ProcessStartInfo("handle.exe"); processInfo.Arguments = "-p " + path; processInfo.WindowStyle = ProcessWindowStyle.Hidden; processInfo.WorkingDirectory = this.Enlistment.EnlistmentRoot; processInfo.UseShellExecute = false; processInfo.RedirectStandardOutput = true; return "handle.exe output: " + ProcessHelper.Run(processInfo).Output; } catch (Exception ex) { return $"Exception running handle.exe - {ex.Message}"; } } private class TestPath { public TestPath(GVFSFunctionalTestEnlistment enlistment, string basePath) { this.BasePath = basePath; this.VirtualPath = enlistment.GetVirtualPathTo(basePath); this.BackingPath = enlistment.GetBackingPathTo(basePath); } public string BasePath { get; } public string VirtualPath { get; } public string BackingPath { get; } } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/DiagnoseTests.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.Tests.Should; using NUnit.Framework; using System.Collections.Generic; using System.IO; using System.Linq; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixture] [NonParallelizable] [Category(Categories.ExtraCoverage)] public class DiagnoseTests : TestsWithEnlistmentPerFixture { private FileSystemRunner fileSystem; public DiagnoseTests() { this.fileSystem = new SystemIORunner(); } [TestCase] public void DiagnoseProducesZipFile() { Directory.Exists(this.Enlistment.DiagnosticsRoot).ShouldEqual(false); string output = this.Enlistment.Diagnose(); output.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: "Failed"); IEnumerable files = Directory.EnumerateFiles(this.Enlistment.DiagnosticsRoot); files.ShouldBeNonEmpty(); string zipFilePath = files.First(); zipFilePath.EndsWith(".zip").ShouldEqual(true); output.Contains(zipFilePath).ShouldEqual(true); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GVFSLockTests.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.FunctionalTests.Properties; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; using System; using System.IO; using System.Linq; using System.Runtime.InteropServices; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixture] public class GVFSLockTests : TestsWithEnlistmentPerFixture { private FileSystemRunner fileSystem; public GVFSLockTests() { this.fileSystem = new SystemIORunner(); } [Flags] private enum MoveFileFlags : uint { MoveFileReplaceExisting = 0x00000001, // MOVEFILE_REPLACE_EXISTING MoveFileCopyAllowed = 0x00000002, // MOVEFILE_COPY_ALLOWED MoveFileDelayUntilReboot = 0x00000004, // MOVEFILE_DELAY_UNTIL_REBOOT MoveFileWriteThrough = 0x00000008, // MOVEFILE_WRITE_THROUGH MoveFileCreateHardlink = 0x00000010, // MOVEFILE_CREATE_HARDLINK MoveFileFailIfNotTrackable = 0x00000020, // MOVEFILE_FAIL_IF_NOT_TRACKABLE } [TestCase] public void GitCheckoutFailsOutsideLock() { const string BackupPrefix = "BACKUP_"; string preCommand = "pre-command" + Settings.Default.BinaryFileNameExtension; string postCommand = "post-command" + Settings.Default.BinaryFileNameExtension; string hooksBase = Path.Combine(this.Enlistment.RepoRoot, ".git", "hooks"); try { // Get hooks out of the way to simulate lock not being acquired as expected this.fileSystem.MoveFile(Path.Combine(hooksBase, preCommand), Path.Combine(hooksBase, BackupPrefix + preCommand)); this.fileSystem.MoveFile(Path.Combine(hooksBase, postCommand), Path.Combine(hooksBase, BackupPrefix + postCommand)); ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20201014_minor"); result.Errors.ShouldContain("fatal: unable to write new index file"); // Ensure that branch didnt move, note however that work dir might not be clean GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "status", "On branch " + Properties.Settings.Default.Commitish); } finally { // Reset hooks for cleanup. this.fileSystem.MoveFile(Path.Combine(hooksBase, BackupPrefix + preCommand), Path.Combine(hooksBase, preCommand)); this.fileSystem.MoveFile(Path.Combine(hooksBase, BackupPrefix + postCommand), Path.Combine(hooksBase, postCommand)); } } [TestCase] public void LockPreventsRenameFromOutsideRootOnTopOfIndex() { this.OverwritingIndexShouldFail(Path.Combine(this.Enlistment.EnlistmentRoot, "LockPreventsRenameFromOutsideRootOnTopOfIndex.txt")); } [TestCase] public void LockPreventsRenameFromInsideWorkingTreeOnTopOfIndex() { this.OverwritingIndexShouldFail(this.Enlistment.GetVirtualPathTo("LockPreventsRenameFromInsideWorkingTreeOnTopOfIndex.txt")); } [TestCase] public void LockPreventsRenameOfIndexLockOnTopOfIndex() { this.OverwritingIndexShouldFail(this.Enlistment.GetVirtualPathTo(".git", "index.lock")); } [DllImport("kernel32.dll", EntryPoint = "MoveFileEx", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool WindowsMoveFileEx( string existingFileName, string newFileName, uint flags); [DllImport("libc", EntryPoint = "rename", SetLastError = true)] private static extern int POSIXRename(string oldPath, string newPath); private void OverwritingIndexShouldFail(string testFilePath) { string indexPath = this.Enlistment.GetVirtualPathTo(".git", "index"); this.Enlistment.WaitForBackgroundOperations(); byte[] indexContents = File.ReadAllBytes(indexPath); string testFileContents = "OverwriteIndexTest"; this.fileSystem.WriteAllText(testFilePath, testFileContents); this.Enlistment.WaitForBackgroundOperations(); this.RenameAndOverwrite(testFilePath, indexPath).ShouldBeFalse("GVFS should prevent renaming on top of index when GVFSLock is not held"); byte[] newIndexContents = File.ReadAllBytes(indexPath); indexContents.SequenceEqual(newIndexContents).ShouldBeTrue("Index contenst should not have changed"); this.fileSystem.DeleteFile(testFilePath); } private bool RenameAndOverwrite(string oldPath, string newPath) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return WindowsMoveFileEx( oldPath, newPath, (uint)(MoveFileFlags.MoveFileReplaceExisting | MoveFileFlags.MoveFileCopyAllowed)); } else { return POSIXRename(oldPath, newPath) == 0; } } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GVFSUpgradeReminderTests.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixture] [NonParallelizable] [Category(Categories.ExtraCoverage)] public class UpgradeReminderTests : TestsWithEnlistmentPerFixture { private const string HighestAvailableVersionFileName = "HighestAvailableVersion"; private const string UpgradeRingKey = "upgrade.ring"; private const string NugetFeedURLKey = "upgrade.feedurl"; private const string NugetFeedPackageNameKey = "upgrade.feedpackagename"; private const string AlwaysUpToDateRing = "None"; private string upgradeDownloadsDirectory; private FileSystemRunner fileSystem; public UpgradeReminderTests() { this.fileSystem = new SystemIORunner(); this.upgradeDownloadsDirectory = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles, Environment.SpecialFolderOption.Create), "GVFS", "ProgramData", "GVFS.Upgrade", "Downloads"); } [TestCase] public void NoReminderWhenUpgradeNotAvailable() { this.EmptyDownloadDirectory(); for (int count = 0; count < 50; count++) { ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo( this.Enlistment.RepoRoot, "status"); string.IsNullOrEmpty(result.Errors).ShouldBeTrue(); } } [TestCase] public void RemindWhenUpgradeAvailable() { this.CreateUpgradeAvailableMarkerFile(); this.ReminderMessagingEnabled().ShouldBeTrue(); this.EmptyDownloadDirectory(); } [TestCase] public void NoReminderForLeftOverDownloads() { this.VerifyServiceRestartStopsReminder(); // This test should not use Nuget upgrader because it will usually find an upgrade // to download. The "None" ring config doesn't stop the Nuget upgrader from checking // its feed for updates, and the VFS4G binaries installed during functional test // runs typically have a 0.X version number (meaning there will always be a newer // version of VFS4G available to download from the feed). this.ReadNugetConfig(out string feedUrl, out string feedName); this.DeleteNugetConfig(); this.VerifyUpgradeVerbStopsReminder(); this.WriteNugetConfig(feedUrl, feedName); } [TestCase] public void UpgradeTimerScheduledOnServiceStart() { this.RestartService(); bool timerScheduled = false; // Service starts upgrade checks after 60 seconds. Thread.Sleep(TimeSpan.FromSeconds(60)); for (int trialCount = 0; trialCount < 30; trialCount++) { Thread.Sleep(TimeSpan.FromSeconds(1)); if (this.ServiceLogContainsUpgradeMessaging()) { timerScheduled = true; break; } } timerScheduled.ShouldBeTrue(); } private void ReadNugetConfig(out string feedUrl, out string feedName) { GVFSProcess gvfs = new GVFSProcess(GVFSTestConfig.PathToGVFS, enlistmentRoot: null, localCacheRoot: null); // failOnError is set to false because gvfs config read can exit with // GenericError when the key-value is not available in config file. That // is normal. feedUrl = gvfs.ReadConfig(NugetFeedURLKey, failOnError: false); feedName = gvfs.ReadConfig(NugetFeedPackageNameKey, failOnError: false); } private void DeleteNugetConfig() { GVFSProcess gvfs = new GVFSProcess(GVFSTestConfig.PathToGVFS, enlistmentRoot: null, localCacheRoot: null); gvfs.DeleteConfig(NugetFeedURLKey); gvfs.DeleteConfig(NugetFeedPackageNameKey); } private void WriteNugetConfig(string feedUrl, string feedName) { GVFSProcess gvfs = new GVFSProcess(GVFSTestConfig.PathToGVFS, enlistmentRoot: null, localCacheRoot: null); if (!string.IsNullOrEmpty(feedUrl)) { gvfs.WriteConfig(NugetFeedURLKey, feedUrl); } if (!string.IsNullOrEmpty(feedName)) { gvfs.WriteConfig(NugetFeedPackageNameKey, feedName); } } private bool ServiceLogContainsUpgradeMessaging() { // This test checks for the upgrade timer start message in the Service log // file. GVFS.Service should schedule the timer as it starts. string expectedTimerMessage = "Checking for product upgrades. (Start)"; string serviceLogFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "GVFS", GVFSServiceProcess.TestServiceName, "Logs"); DirectoryInfo logsDirectory = new DirectoryInfo(serviceLogFolder); FileInfo logFile = logsDirectory.GetFiles() .OrderByDescending(f => f.LastWriteTime) .FirstOrDefault(); if (logFile != null) { using (StreamReader fileStream = new StreamReader(File.Open(logFile.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) { string nextLine = null; while ((nextLine = fileStream.ReadLine()) != null) { if (nextLine.Contains(expectedTimerMessage)) { return true; } } } } return false; } private void EmptyDownloadDirectory() { if (Directory.Exists(this.upgradeDownloadsDirectory)) { Directory.Delete(this.upgradeDownloadsDirectory, recursive: true); } Directory.CreateDirectory(this.upgradeDownloadsDirectory); Directory.Exists(this.upgradeDownloadsDirectory).ShouldBeTrue(); Directory.EnumerateFiles(this.upgradeDownloadsDirectory).Any().ShouldBeFalse(); } private void CreateUpgradeAvailableMarkerFile() { string gvfsUpgradeAvailableFilePath = Path.Combine( Path.GetDirectoryName(this.upgradeDownloadsDirectory), HighestAvailableVersionFileName); this.EmptyDownloadDirectory(); this.fileSystem.CreateEmptyFile(gvfsUpgradeAvailableFilePath); this.fileSystem.FileExists(gvfsUpgradeAvailableFilePath).ShouldBeTrue(); } private void SetUpgradeRing(string value) { this.RunGVFS($"config {UpgradeRingKey} {value}"); } private string RunUpgradeCommand() { return this.RunGVFS("upgrade"); } private string RunGVFS(string argument) { ProcessResult result = ProcessHelper.Run(GVFSTestConfig.PathToGVFS, argument); result.ExitCode.ShouldEqual(0, result.Errors); return result.Output; } private void RestartService() { GVFSServiceProcess.StopService(); GVFSServiceProcess.StartService(); } private bool ReminderMessagingEnabled() { Dictionary environmentVariables = new Dictionary(); environmentVariables["GVFS_UPGRADE_DETERMINISTIC"] = "true"; ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo( this.Enlistment.RepoRoot, "status", environmentVariables, removeWaitingMessages: true, removeUpgradeMessages: false); if (!string.IsNullOrEmpty(result.Errors) && result.Errors.Contains("A new version of VFS for Git is available.")) { return true; } return false; } private void VerifyServiceRestartStopsReminder() { this.CreateUpgradeAvailableMarkerFile(); this.ReminderMessagingEnabled().ShouldBeTrue("Upgrade marker file did not trigger reminder messaging"); this.SetUpgradeRing(AlwaysUpToDateRing); this.RestartService(); // Wait for sometime so service can detect product is up-to-date and delete left over downloads TimeSpan timeToWait = TimeSpan.FromMinutes(1); bool reminderMessagingEnabled = true; while ((reminderMessagingEnabled = this.ReminderMessagingEnabled()) && timeToWait > TimeSpan.Zero) { Thread.Sleep(TimeSpan.FromSeconds(5)); timeToWait = timeToWait.Subtract(TimeSpan.FromSeconds(5)); } reminderMessagingEnabled.ShouldBeFalse("Service restart did not stop Upgrade reminder messaging"); } private void VerifyUpgradeVerbStopsReminder() { this.SetUpgradeRing(AlwaysUpToDateRing); this.CreateUpgradeAvailableMarkerFile(); this.ReminderMessagingEnabled().ShouldBeTrue("Marker file did not trigger Upgrade reminder messaging"); this.RunUpgradeCommand(); this.ReminderMessagingEnabled().ShouldBeFalse("Upgrade verb did not stop Upgrade reminder messaging"); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitBlockCommandsTests.cs ================================================ using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixture] [Category(Categories.GitCommands)] public class GitBlockCommandsTests : TestsWithEnlistmentPerFixture { [TestCase] public void GitBlockCommands() { this.CommandBlocked("fsck"); this.CommandBlocked("gc"); this.CommandNotBlocked("gc --auto"); this.CommandBlocked("prune"); this.CommandBlocked("prune"); this.CommandBlocked("repack"); this.CommandBlocked("submodule"); this.CommandBlocked("submodule status"); this.CommandBlocked("update-index --index-version 2"); this.CommandBlocked("update-index --skip-worktree"); this.CommandBlocked("update-index --no-skip-worktree"); this.CommandBlocked("update-index --split-index"); this.CommandNotBlocked("worktree list"); } private void CommandBlocked(string command) { ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo( this.Enlistment.RepoRoot, command); result.ExitCode.ShouldNotEqual(0, $"Command {command} not blocked when it should be. Errors: {result.Errors}"); } private void CommandNotBlocked(string command) { ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo( this.Enlistment.RepoRoot, command); result.ExitCode.ShouldEqual(0, $"Command {command} blocked when it should not be. Errors: {result.Errors}"); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitCorruptObjectTests.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.FunctionalTests.Should; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; using System; using System.IO; using System.Linq; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixture] [Category(Categories.GitCommands)] public class GitCorruptObjectTests : TestsWithEnlistmentPerFixture { private FileSystemRunner fileSystem; // Set forcePerRepoObjectCache to true to avoid any of the tests inadvertently corrupting // the cache public GitCorruptObjectTests() : base(forcePerRepoObjectCache: true) { this.fileSystem = new SystemIORunner(); } [TestCase] public void GitRequestsReplacementForAllNullObject() { Action allNullObject = (string objectPath) => { FileInfo objectFileInfo = new FileInfo(objectPath); File.WriteAllBytes(objectPath, Enumerable.Repeat(0, (int)objectFileInfo.Length).ToArray()); }; this.RunGitDiffWithCorruptObject(allNullObject); this.RunGitCatFileWithCorruptObject(allNullObject); this.RunGitResetHardWithCorruptObject(allNullObject); this.RunGitCheckoutOnFileWithCorruptObject(allNullObject); } [TestCase] public void GitRequestsReplacementForTruncatedObject() { Action truncateObject = (string objectPath) => { FileInfo objectFileInfo = new FileInfo(objectPath); using (FileStream objectStream = new FileStream(objectPath, FileMode.Open)) { objectStream.SetLength(objectFileInfo.Length - 8); } }; this.RunGitDiffWithCorruptObject(truncateObject); // TODO 1114508: Update git cat-file to request object from GVFS when it finds a truncated object on disk. ////this.RunGitCatFileWithCorruptObject(truncateObject); this.RunGitResetHardWithCorruptObject(truncateObject); this.RunGitCheckoutOnFileWithCorruptObject(truncateObject); } [TestCase] public void GitRequestsReplacementForObjectCorruptedWithBadData() { Action fillObjectWithBadData = (string objectPath) => { this.fileSystem.WriteAllText(objectPath, "Not a valid git object"); }; this.RunGitDiffWithCorruptObject(fillObjectWithBadData); this.RunGitCatFileWithCorruptObject(fillObjectWithBadData); this.RunGitResetHardWithCorruptObject(fillObjectWithBadData); this.RunGitCheckoutOnFileWithCorruptObject(fillObjectWithBadData); } private void RunGitDiffWithCorruptObject(Action corruptObject) { string fileName = "Protocol.md"; string filePath = this.Enlistment.GetVirtualPathTo(fileName); string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); string newFileContents = "RunGitDiffWithCorruptObject"; this.fileSystem.WriteAllText(filePath, newFileContents); string sha; string objectPath = this.GetLooseObjectPath(fileName, out sha); corruptObject(objectPath); ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"diff {fileName}"); revParseResult.ExitCode.ShouldEqual(0); revParseResult.Output.ShouldContain("The GVFS network protocol consists of three operations"); revParseResult.Output.ShouldContain(newFileContents); } private void RunGitCatFileWithCorruptObject(Action corruptObject) { string fileName = "Readme.md"; string filePath = this.Enlistment.GetVirtualPathTo(fileName); string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); string sha; string objectPath = this.GetLooseObjectPath(fileName, out sha); corruptObject(objectPath); ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"cat-file blob {sha}"); revParseResult.ExitCode.ShouldEqual(0); revParseResult.Output.ShouldEqual(fileContents); } private void RunGitResetHardWithCorruptObject(Action corruptObject) { string fileName = "Readme.md"; string filePath = this.Enlistment.GetVirtualPathTo(fileName); string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); string newFileContents = "RunGitDiffWithCorruptObject"; this.fileSystem.WriteAllText(filePath, newFileContents); string sha; string objectPath = this.GetLooseObjectPath(fileName, out sha); corruptObject(objectPath); ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "reset --hard HEAD"); revParseResult.ExitCode.ShouldEqual(0); filePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents); } private void RunGitCheckoutOnFileWithCorruptObject(Action corruptObject) { string fileName = "Readme.md"; string filePath = this.Enlistment.GetVirtualPathTo(fileName); string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); string newFileContents = "RunGitDiffWithCorruptObject"; this.fileSystem.WriteAllText(filePath, newFileContents); string sha; string objectPath = this.GetLooseObjectPath(fileName, out sha); corruptObject(objectPath); ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"checkout -- {fileName}"); revParseResult.ExitCode.ShouldEqual(0); filePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents); } private string GetLooseObjectPath(string fileGitPath, out string sha) { ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"rev-parse :{fileGitPath}"); sha = revParseResult.Output.Trim(); if (FileSystemHelpers.CaseSensitiveFileSystem) { // Ensure SHA path is lowercase for case-sensitive filesystems sha = sha.ToLower(); } sha.Length.ShouldEqual(40); string objectPath = Path.Combine(this.Enlistment.GetObjectRoot(this.fileSystem), sha.Substring(0, 2), sha.Substring(2, 38)); return objectPath; } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.FunctionalTests.Should; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixtureSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))] public class GitFilesTests : TestsWithEnlistmentPerFixture { private FileSystemRunner fileSystem; public GitFilesTests(FileSystemRunner fileSystem) { this.fileSystem = fileSystem; } [TestCase, Order(1)] public void CreateFileTest() { string fileName = "file1.txt"; GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileName); this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(fileName), "Some content here"); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileName); this.Enlistment.GetVirtualPathTo(fileName).ShouldBeAFile(this.fileSystem).WithContents("Some content here"); string emptyFileName = "file1empty.txt"; GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, emptyFileName); this.fileSystem.CreateEmptyFile(this.Enlistment.GetVirtualPathTo(emptyFileName)); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, emptyFileName); this.Enlistment.GetVirtualPathTo(emptyFileName).ShouldBeAFile(this.fileSystem); } [TestCase, Order(2)] public void CreateHardLinkTest() { string existingFileName = "fileToLinkTo.txt"; string existingFilePath = this.Enlistment.GetVirtualPathTo(existingFileName); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, existingFileName); this.fileSystem.WriteAllText(existingFilePath, "Some content here"); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, existingFileName); existingFilePath.ShouldBeAFile(this.fileSystem).WithContents("Some content here"); string newLinkFileName = "newHardLink.txt"; string newLinkFilePath = this.Enlistment.GetVirtualPathTo(newLinkFileName); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, newLinkFileName); this.fileSystem.CreateHardLink(newLinkFilePath, existingFilePath); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, newLinkFileName); newLinkFilePath.ShouldBeAFile(this.fileSystem).WithContents("Some content here"); } [TestCase, Order(3)] public void CreateFileInFolderTest() { string folderName = "folder2"; string fileName = "file2.txt"; string filePath = Path.Combine(folderName, fileName); this.Enlistment.GetVirtualPathTo(filePath).ShouldNotExistOnDisk(this.fileSystem); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, filePath); this.fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(folderName)); this.fileSystem.CreateEmptyFile(this.Enlistment.GetVirtualPathTo(filePath)); this.Enlistment.GetVirtualPathTo(filePath).ShouldBeAFile(this.fileSystem); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, folderName + "/"); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, folderName + "/" + fileName); } [TestCase, Order(4)] public void RenameEmptyFolderTest() { string folderName = "folder3a"; string renamedFolderName = "folder3b"; string[] expectedModifiedEntries = { renamedFolderName + "/", }; this.Enlistment.GetVirtualPathTo(folderName).ShouldNotExistOnDisk(this.fileSystem); this.fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(folderName)); this.fileSystem.MoveDirectory(this.Enlistment.GetVirtualPathTo(folderName), this.Enlistment.GetVirtualPathTo(renamedFolderName)); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, expectedModifiedEntries); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, folderName + "/"); } [TestCase, Order(5)] public void RenameFolderTest() { string folderName = "folder4a"; string renamedFolderName = "folder4b"; string[] fileNames = { "a", "b", "c" }; string[] expectedModifiedEntries = { renamedFolderName + "/", }; string[] unexpectedModifiedEntries = { renamedFolderName + "/" + fileNames[0], renamedFolderName + "/" + fileNames[1], renamedFolderName + "/" + fileNames[2], folderName + "/", folderName + "/" + fileNames[0], folderName + "/" + fileNames[1], folderName + "/" + fileNames[2], }; this.Enlistment.GetVirtualPathTo(folderName).ShouldNotExistOnDisk(this.fileSystem); this.fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(folderName)); foreach (string fileName in fileNames) { string filePath = Path.Combine(folderName, fileName); this.fileSystem.CreateEmptyFile(this.Enlistment.GetVirtualPathTo(filePath)); this.Enlistment.GetVirtualPathTo(filePath).ShouldBeAFile(this.fileSystem); } this.fileSystem.MoveDirectory(this.Enlistment.GetVirtualPathTo(folderName), this.Enlistment.GetVirtualPathTo(renamedFolderName)); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, expectedModifiedEntries); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, unexpectedModifiedEntries); } [TestCase, Order(6)] public void CaseOnlyRenameOfNewFolderKeepsModifiedPathsEntries() { if (this.fileSystem is PowerShellRunner) { Assert.Ignore("Powershell does not support case only renames."); } this.fileSystem.CreateDirectory(Path.Combine(this.Enlistment.RepoRoot, "Folder")); this.fileSystem.CreateEmptyFile(Path.Combine(this.Enlistment.RepoRoot, "Folder", "testfile")); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, "Folder/"); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, "Folder/testfile"); this.fileSystem.RenameDirectory(this.Enlistment.RepoRoot, "Folder", "folder"); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, "folder/"); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, "folder/testfile"); } [TestCase, Order(7)] public void ReadingFileDoesNotUpdateIndexOrModifiedPaths() { string gitFileToCheck = "GVFS/GVFS.FunctionalTests/Category/CategoryConstants.cs"; string virtualFile = this.Enlistment.GetVirtualPathTo(gitFileToCheck); ProcessResult initialResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "ls-files --debug -svmodc " + gitFileToCheck); initialResult.ShouldNotBeNull(); initialResult.Output.ShouldNotBeNull(); initialResult.Output.StartsWith("S ").ShouldEqual(true); initialResult.Output.ShouldContain("ctime: 0:0", "mtime: 0:0", "size: 0\t"); using (FileStream fileStreamToRead = File.OpenRead(virtualFile)) { fileStreamToRead.ReadByte(); } this.Enlistment.WaitForBackgroundOperations(); ProcessResult afterUpdateResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "ls-files --debug -svmodc " + gitFileToCheck); afterUpdateResult.ShouldNotBeNull(); afterUpdateResult.Output.ShouldNotBeNull(); afterUpdateResult.Output.StartsWith("S ").ShouldEqual(true); afterUpdateResult.Output.ShouldContain("ctime: 0:0", "mtime: 0:0", "size: 0\t"); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, gitFileToCheck); } [TestCase, Order(8)] public void ModifiedFileWillGetAddedToModifiedPathsFile() { string gitFileToTest = "GVFS/GVFS.Common/RetryWrapper.cs"; string fileToCreate = this.Enlistment.GetVirtualPathTo(gitFileToTest); this.VerifyWorktreeBit(gitFileToTest, LsFilesStatus.SkipWorktree); ManualResetEventSlim resetEvent = GitHelpers.AcquireGVFSLock(this.Enlistment, out _); this.fileSystem.WriteAllText(fileToCreate, "Anything can go here"); this.fileSystem.FileExists(fileToCreate).ShouldEqual(true); resetEvent.Set(); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, gitFileToTest); this.VerifyWorktreeBit(gitFileToTest, LsFilesStatus.Cached); } [TestCase, Order(9)] public void RenamedFileAddedToModifiedPathsFile() { string fileToRenameEntry = "Test_EPF_MoveRenameFileTests/ChangeUnhydratedFileName/Program.cs"; string fileToRenameTargetEntry = "Test_EPF_MoveRenameFileTests/ChangeUnhydratedFileName/Program2.cs"; this.VerifyWorktreeBit(fileToRenameEntry, LsFilesStatus.SkipWorktree); this.fileSystem.MoveFile( this.Enlistment.GetVirtualPathTo(fileToRenameEntry), this.Enlistment.GetVirtualPathTo(fileToRenameTargetEntry)); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileToRenameEntry); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileToRenameTargetEntry); // Verify skip-worktree cleared this.VerifyWorktreeBit(fileToRenameEntry, LsFilesStatus.Cached); } [TestCase, Order(10)] public void RenamedFileAndOverwrittenTargetAddedToModifiedPathsFile() { string fileToRenameEntry = "Test_EPF_MoveRenameFileTests_2/MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite/RunUnitTests.bat"; string fileToRenameTargetEntry = "Test_EPF_MoveRenameFileTests_2/MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite/RunFunctionalTests.bat"; this.VerifyWorktreeBit(fileToRenameEntry, LsFilesStatus.SkipWorktree); this.VerifyWorktreeBit(fileToRenameTargetEntry, LsFilesStatus.SkipWorktree); this.fileSystem.ReplaceFile( this.Enlistment.GetVirtualPathTo(fileToRenameEntry), this.Enlistment.GetVirtualPathTo(fileToRenameTargetEntry)); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileToRenameEntry); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileToRenameTargetEntry); // Verify skip-worktree cleared this.VerifyWorktreeBit(fileToRenameEntry, LsFilesStatus.Cached); this.VerifyWorktreeBit(fileToRenameTargetEntry, LsFilesStatus.Cached); } [TestCase, Order(11)] public void DeletedFileAddedToModifiedPathsFile() { string fileToDeleteEntry = "GVFlt_DeleteFileTest/GVFlt_DeleteFullFileWithoutFileContext_DeleteOnClose/a.txt"; this.VerifyWorktreeBit(fileToDeleteEntry, LsFilesStatus.SkipWorktree); this.fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(fileToDeleteEntry)); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileToDeleteEntry); // Verify skip-worktree cleared this.VerifyWorktreeBit(fileToDeleteEntry, LsFilesStatus.Cached); } [TestCase, Order(12)] public void DeletedFolderAndChildrenAddedToToModifiedPathsFile() { string folderToDelete = "Scripts"; string[] filesToDelete = new string[] { "Scripts/CreateCommonAssemblyVersion.bat", "Scripts/CreateCommonCliAssemblyVersion.bat", "Scripts/CreateCommonVersionHeader.bat", "Scripts/RunFunctionalTests.bat", "Scripts/RunUnitTests.bat" }; // Verify skip-worktree initial set for all files foreach (string file in filesToDelete) { this.VerifyWorktreeBit(file, LsFilesStatus.SkipWorktree); } this.fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(folderToDelete)); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, folderToDelete + "/"); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, filesToDelete); // Verify skip-worktree cleared foreach (string file in filesToDelete) { this.VerifyWorktreeBit(file, LsFilesStatus.Cached); } } [TestCase, Order(13)] public void FileRenamedOutOfRepoAddedToModifiedPathsAndSkipWorktreeBitCleared() { string fileToRenameEntry = "GVFlt_MoveFileTest/PartialToOutside/from/lessInFrom.txt"; string fileToRenameVirtualPath = this.Enlistment.GetVirtualPathTo(fileToRenameEntry); this.VerifyWorktreeBit(fileToRenameEntry, LsFilesStatus.SkipWorktree); string fileOutsideRepoPath = Path.Combine(this.Enlistment.EnlistmentRoot, $"{nameof(this.FileRenamedOutOfRepoAddedToModifiedPathsAndSkipWorktreeBitCleared)}.txt"); this.fileSystem.MoveFile(fileToRenameVirtualPath, fileOutsideRepoPath); fileOutsideRepoPath.ShouldBeAFile(this.fileSystem).WithContents("lessData"); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileToRenameEntry); // Verify skip-worktree cleared this.VerifyWorktreeBit(fileToRenameEntry, LsFilesStatus.Cached); } [TestCase, Order(14)] public void OverwrittenFileAddedToModifiedPathsAndSkipWorktreeBitCleared() { string fileToOverwriteEntry = "Test_EPF_WorkingDirectoryTests/1/2/3/4/ReadDeepProjectedFile.cpp"; string fileToOverwriteVirtualPath = this.Enlistment.GetVirtualPathTo(fileToOverwriteEntry); this.VerifyWorktreeBit(fileToOverwriteEntry, LsFilesStatus.SkipWorktree); string testContents = $"Test contents for {nameof(this.OverwrittenFileAddedToModifiedPathsAndSkipWorktreeBitCleared)}"; this.fileSystem.WriteAllText(fileToOverwriteVirtualPath, testContents); this.Enlistment.WaitForBackgroundOperations(); fileToOverwriteVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(testContents); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileToOverwriteEntry); // Verify skip-worktree cleared this.VerifyWorktreeBit(fileToOverwriteEntry, LsFilesStatus.Cached); } [TestCase, Order(15)] public void SupersededFileAddedToModifiedPathsAndSkipWorktreeBitCleared() { string fileToSupersedeEntry = "GVFlt_FileOperationTest/WriteAndVerify.txt"; string fileToSupersedePath = this.Enlistment.GetVirtualPathTo("GVFlt_FileOperationTest\\WriteAndVerify.txt"); this.VerifyWorktreeBit(fileToSupersedeEntry, LsFilesStatus.SkipWorktree); string newContent = $"{nameof(this.SupersededFileAddedToModifiedPathsAndSkipWorktreeBitCleared)} test new contents"; SupersedeFile(fileToSupersedePath, newContent).ShouldEqual(true); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileToSupersedeEntry); // Verify skip-worktree cleared this.VerifyWorktreeBit(fileToSupersedeEntry, LsFilesStatus.Cached); // Verify new content written fileToSupersedePath.ShouldBeAFile(this.fileSystem).WithContents(newContent); } [TestCase, Order(16)] public void FileMovedFromOutsideRepoToInside() { string fileName = "OutsideRepoToInside.txt"; string fileOutsideRepo = Path.Combine(this.Enlistment.EnlistmentRoot, fileName); this.fileSystem.WriteAllText(fileOutsideRepo, "Contents for the new file"); fileOutsideRepo.ShouldBeAFile(this.fileSystem); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileName); string fileMovedInsideRepo = this.Enlistment.GetVirtualPathTo(fileName); this.fileSystem.MoveFile(fileOutsideRepo, fileMovedInsideRepo); fileMovedInsideRepo.ShouldBeAFile(this.fileSystem); fileOutsideRepo.ShouldNotExistOnDisk(this.fileSystem); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileName); } [TestCase, Order(17)] public void FileMovedFromInsideRepoToOutside() { string fileInsideRepoEntry = "GitCommandsTests/RenameFileTests/1/#test"; string fileNameFullPath = Path.Combine("GitCommandsTests", "RenameFileTests", "1", "#test"); string fileInsideRepo = this.Enlistment.GetVirtualPathTo(fileNameFullPath); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileInsideRepoEntry); string fileNameOutsideRepo = "FileNameOutSideRepo"; string fileMovedOutsideRepo = Path.Combine(this.Enlistment.EnlistmentRoot, fileNameOutsideRepo); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileNameOutsideRepo); this.fileSystem.MoveFile(fileInsideRepo, fileMovedOutsideRepo); fileInsideRepo.ShouldNotExistOnDisk(this.fileSystem); fileMovedOutsideRepo.ShouldBeAFile(this.fileSystem); this.fileSystem.ReadAllText(fileMovedOutsideRepo).ShouldContain("test"); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileInsideRepoEntry); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileNameOutsideRepo); } [TestCase, Order(18)] public void HardlinkFromOutsideRepoToInside() { string fileName = "OutsideRepoToInside_FileForHardlink.txt"; string fileOutsideRepo = Path.Combine(this.Enlistment.EnlistmentRoot, fileName); this.fileSystem.WriteAllText(fileOutsideRepo, "Contents for the new file"); fileOutsideRepo.ShouldBeAFile(this.fileSystem); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileName); string fileNameLink = "OutsideRepoToInside_RepoLink.txt"; string fileLinkInsideRepo = this.Enlistment.GetVirtualPathTo(fileNameLink); this.fileSystem.CreateHardLink(fileLinkInsideRepo, fileOutsideRepo); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileNameLink); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileName); fileLinkInsideRepo.ShouldBeAFile(this.fileSystem); } [TestCase, Order(19)] public void HardlinkFromInsideRepoToOutside() { string fileName = "Readme.md"; string fileInsideRepo = this.Enlistment.GetVirtualPathTo(fileName); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileName); string fileNameLink = "InsideRepoToOutside_RepoLink.txt"; string fileLinkOutsideRepo = Path.Combine(this.Enlistment.EnlistmentRoot, fileNameLink); this.fileSystem.CreateHardLink(fileLinkOutsideRepo, fileInsideRepo); fileLinkOutsideRepo.ShouldBeAFile(this.fileSystem); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileName); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileNameLink); } [TestCase, Order(20)] public void HardlinkInsideRepo() { string fileName = "InsideRepo_FileForHardlink.txt"; string fileInsideRepo = this.Enlistment.GetVirtualPathTo(fileName); this.fileSystem.WriteAllText(fileInsideRepo, "Contents for the new file"); fileInsideRepo.ShouldBeAFile(this.fileSystem); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileName); string fileNameLink = "InsideRepo_RepoLink.txt"; string fileLinkInsideRepo = this.Enlistment.GetVirtualPathTo(fileNameLink); this.fileSystem.CreateHardLink(fileLinkInsideRepo, fileInsideRepo); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileName); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileNameLink); fileLinkInsideRepo.ShouldBeAFile(this.fileSystem); } [TestCase, Order(21)] public void HardlinkExistingFileInRepo() { string fileName = "GVFS/GVFS.Mount/Program.cs"; string fileNameLink = "HardLinkToReadme"; GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileName); GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileNameLink); string fileInsideRepo = this.Enlistment.GetVirtualPathTo(fileName); string fileLinkInsideRepo = this.Enlistment.GetVirtualPathTo(fileNameLink); this.fileSystem.CreateHardLink(fileLinkInsideRepo, fileInsideRepo); this.Enlistment.WaitForBackgroundOperations(); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileName); GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileNameLink); fileInsideRepo.ShouldBeAFile(this.fileSystem); fileLinkInsideRepo.ShouldBeAFile(this.fileSystem); } [DllImport("GVFS.NativeTests.dll", CharSet = CharSet.Unicode)] private static extern bool SupersedeFile(string path, [MarshalAs(UnmanagedType.LPStr)]string newContent); private void VerifyWorktreeBit(string path, char expectedStatus) { ProcessResult lsfilesResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "ls-files -svomdc " + path); lsfilesResult.ShouldNotBeNull(); lsfilesResult.Output.ShouldNotBeNull(); lsfilesResult.Output.Length.ShouldBeAtLeast(2); lsfilesResult.Output[0].ShouldEqual(expectedStatus); } private static class LsFilesStatus { public const char Cached = 'H'; public const char SkipWorktree = 'S'; } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitMoveRenameTests.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.FunctionalTests.Should; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixtureSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))] [Category(Categories.GitCommands)] public class GitMoveRenameTests : TestsWithEnlistmentPerFixture { private string testFileContents = "0123456789"; private FileSystemRunner fileSystem; public GitMoveRenameTests(FileSystemRunner fileSystem) { this.fileSystem = fileSystem; } [TestCase, Order(1)] public void GitStatus() { GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "status", "On branch " + Properties.Settings.Default.Commitish, "nothing to commit, working tree clean"); } [TestCase, Order(2)] public void GitStatusAfterNewFile() { string filename = "new.cs"; string filePath = this.Enlistment.GetVirtualPathTo(filename); filePath.ShouldNotExistOnDisk(this.fileSystem); this.fileSystem.WriteAllText(filePath, this.testFileContents); filePath.ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "status", "On branch " + Properties.Settings.Default.Commitish, "Untracked files:", filename); this.fileSystem.DeleteFile(filePath); } [TestCase, Order(3)] public void GitStatusAfterFileNameCaseChange() { string oldFilename = "new.cs"; this.EnsureTestFileExists(oldFilename); string newFilename = "New.cs"; string newFilePath = this.Enlistment.GetVirtualPathTo(newFilename); this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), newFilePath); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "status", "On branch " + Properties.Settings.Default.Commitish, "Untracked files:", newFilename); this.fileSystem.DeleteFile(newFilePath); } [TestCase, Order(4)] public void GitStatusAfterFileRename() { string oldFilename = "New.cs"; this.EnsureTestFileExists(oldFilename); string newFilename = "test.cs"; string newFilePath = this.Enlistment.GetVirtualPathTo(newFilename); this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), newFilePath); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "status", "On branch " + Properties.Settings.Default.Commitish, "Untracked files:", newFilename); } [TestCase, Order(5)] public void GitStatusAndObjectAfterGitAdd() { string existingFilename = "test.cs"; this.Enlistment.GetVirtualPathTo(existingFilename).ShouldBeAFile(this.fileSystem); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "add " + existingFilename, new string[] { }); // Status should be correct GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "status", "On branch " + Properties.Settings.Default.Commitish, "Changes to be committed:", existingFilename); // Object file for the test file should have the correct contents ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo( this.Enlistment.RepoRoot, "hash-object " + existingFilename); string objectHash = result.Output.Trim(); result.Errors.ShouldBeEmpty(); this.Enlistment.GetObjectPathTo(objectHash).ShouldBeAFile(this.fileSystem); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "cat-file -p " + objectHash, this.testFileContents); } [TestCase, Order(6)] public void GitStatusAfterUnstage() { string existingFilename = "test.cs"; this.Enlistment.GetVirtualPathTo(existingFilename).ShouldBeAFile(this.fileSystem); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "reset HEAD " + existingFilename, new string[] { }); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "status", "On branch " + Properties.Settings.Default.Commitish, "Untracked files:", existingFilename); } [TestCase, Order(7)] public void GitStatusAfterFileDelete() { string existingFilename = "test.cs"; this.EnsureTestFileExists(existingFilename); this.fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(existingFilename)); this.Enlistment.GetVirtualPathTo(existingFilename).ShouldNotExistOnDisk(this.fileSystem); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "status", "On branch " + Properties.Settings.Default.Commitish, "nothing to commit, working tree clean"); } [TestCase, Order(8)] public void GitWithEnvironmentVariables() { // The trace info is an error, so we can't use CheckGitCommand(). // We just want to make sure this doesn't throw an exception. ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo( this.Enlistment.RepoRoot, "branch", new Dictionary { { "GIT_TRACE_PERFORMANCE", "1" }, { "git_trace", "1" }, }, removeWaitingMessages: false); result.Output.ShouldContain("* FunctionalTests"); result.Errors.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: "exception"); result.Errors.ShouldContain("trace.c:", "git command:"); } [TestCase, Order(9)] public void GitStatusAfterRenameFileIntoRepo() { string filename = "GitStatusAfterRenameFileIntoRepo.cs"; // Create the test file in this.Enlistment.EnlistmentRoot as it's outside of src // and is cleaned up when the functional tests run string filePath = Path.Combine(this.Enlistment.EnlistmentRoot, filename); this.fileSystem.WriteAllText(filePath, this.testFileContents); filePath.ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); string renamedFileName = Path.Combine("GVFlt_MoveFileTest", "GitStatusAfterRenameFileIntoRepo.cs"); string renamedFilePath = this.Enlistment.GetVirtualPathTo(renamedFileName); this.fileSystem.MoveFile(filePath, renamedFilePath); filePath.ShouldNotExistOnDisk(this.fileSystem); renamedFilePath.ShouldBeAFile(this.fileSystem); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "status", "On branch " + Properties.Settings.Default.Commitish, "Untracked files:", renamedFileName.Replace('\\', '/')); } [TestCase, Order(10)] public void GitStatusAfterRenameFileOutOfRepo() { string existingFilename = Path.Combine("Test_EPF_MoveRenameFileTests", "ChangeUnhydratedFileName", "Program.cs"); // Move the test file to this.Enlistment.EnlistmentRoot as it's outside of src // and is cleaned up when the functional tests run this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(existingFilename), Path.Combine(this.Enlistment.EnlistmentRoot, "Program.cs")); this.Enlistment.GetVirtualPathTo(existingFilename).ShouldNotExistOnDisk(this.fileSystem); ProcessResult result = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "status"); result.Output.ShouldContain("On branch " + Properties.Settings.Default.Commitish); result.Output.ShouldContain("Changes not staged for commit"); result.Output.ShouldContain("deleted: Test_EPF_MoveRenameFileTests/ChangeUnhydratedFileName/Program.cs"); } [TestCase, Order(11)] public void GitStatusAfterRenameFolderIntoRepo() { string folderName = "GitStatusAfterRenameFolderIntoRepo"; // Create the test folder in this.Enlistment.EnlistmentRoot as it's outside of src // and is cleaned up when the functional tests run string folderPath = Path.Combine(this.Enlistment.EnlistmentRoot, folderName); this.fileSystem.CreateDirectory(folderPath); string fileName = "GitStatusAfterRenameFolderIntoRepo_file.txt"; string filePath = Path.Combine(folderPath, fileName); this.fileSystem.WriteAllText(filePath, this.testFileContents); filePath.ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); this.fileSystem.MoveDirectory(folderPath, this.Enlistment.GetVirtualPathTo(folderName)); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "status -uall", "On branch " + Properties.Settings.Default.Commitish, "Untracked files:", folderName + "/", folderName + "/" + fileName); } private void EnsureTestFileExists(string relativePath) { string filePath = this.Enlistment.GetVirtualPathTo(relativePath); if (!this.fileSystem.FileExists(filePath)) { this.fileSystem.WriteAllText(filePath, this.testFileContents); } this.Enlistment.GetVirtualPathTo(relativePath).ShouldBeAFile(this.fileSystem); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitReadAndGitLockTests.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.FunctionalTests.Should; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; using System; using System.Diagnostics; using System.IO; using System.Threading; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixture] public class GitReadAndGitLockTests : TestsWithEnlistmentPerFixture { private const string ExpectedStatusWaitingText = @"Waiting for 'GVFS.FunctionalTests.LockHolder'"; private const int AcquireGVFSLockTimeout = 10 * 1000; private FileSystemRunner fileSystem; public GitReadAndGitLockTests() { this.fileSystem = new SystemIORunner(); } [TestCase, Order(1)] public void GitStatus() { GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "status", "On branch " + Properties.Settings.Default.Commitish, "nothing to commit, working tree clean"); } [TestCase, Order(2)] public void GitLog() { GitHelpers.CheckGitCommandAgainstGVFSRepo(this.Enlistment.RepoRoot, "log -n1", "commit", "Author:", "Date:"); } [TestCase, Order(3)] public void GitBranch() { GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, "branch -a", "* " + Properties.Settings.Default.Commitish, "remotes/origin/" + Properties.Settings.Default.Commitish); } [TestCase, Order(4)] public void GitCommandWaitsWhileAnotherIsRunning() { int pid; GitHelpers.AcquireGVFSLock(this.Enlistment, out pid, resetTimeout: 3000); ProcessResult statusWait = GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "status", removeWaitingMessages: false); statusWait.Errors.ShouldContain(ExpectedStatusWaitingText); } [TestCase, Order(5)] public void GitAliasNamedAfterKnownCommandAcquiresLock() { string alias = nameof(this.GitAliasNamedAfterKnownCommandAcquiresLock); int pid; GitHelpers.AcquireGVFSLock(this.Enlistment, out pid, resetTimeout: AcquireGVFSLockTimeout); GitHelpers.CheckGitCommandAgainstGVFSRepo(this.Enlistment.RepoRoot, "config --local alias." + alias + " status"); ProcessResult statusWait = GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, alias, removeWaitingMessages: false); statusWait.Errors.ShouldContain(ExpectedStatusWaitingText); } [TestCase, Order(6)] public void GitAliasInSubfolderNamedAfterKnownCommandAcquiresLock() { string alias = nameof(this.GitAliasInSubfolderNamedAfterKnownCommandAcquiresLock); int pid; GitHelpers.AcquireGVFSLock(this.Enlistment, out pid, resetTimeout: AcquireGVFSLockTimeout); GitHelpers.CheckGitCommandAgainstGVFSRepo(this.Enlistment.RepoRoot, "config --local alias." + alias + " rebase"); ProcessResult statusWait = GitHelpers.InvokeGitAgainstGVFSRepo( Path.Combine(this.Enlistment.RepoRoot, "GVFS"), alias + " origin/FunctionalTests/RebaseTestsSource_20170208", removeWaitingMessages: false); statusWait.Errors.ShouldContain(ExpectedStatusWaitingText); GitHelpers.CheckGitCommandAgainstGVFSRepo(this.Enlistment.RepoRoot, "rebase --abort"); } [TestCase, Order(7)] public void ExternalLockHolderReportedWhenBackgroundTasksArePending() { int pid; GitHelpers.AcquireGVFSLock(this.Enlistment, out pid, resetTimeout: 3000); // Creating a new file will queue a background task string newFilePath = this.Enlistment.GetVirtualPathTo("ExternalLockHolderReportedWhenBackgroundTasksArePending.txt"); newFilePath.ShouldNotExistOnDisk(this.fileSystem); this.fileSystem.WriteAllText(newFilePath, "New file contents"); ProcessResult statusWait = GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "status", removeWaitingMessages: false); // Validate that GVFS still reports that the git command is holding the lock statusWait.Errors.ShouldContain(ExpectedStatusWaitingText); } [TestCase, Order(8)] public void OrphanedGVFSLockIsCleanedUp() { int pid; GitHelpers.AcquireGVFSLock(this.Enlistment, out pid, resetTimeout: 1000, skipReleaseLock: true); while (true) { try { using (Process.GetProcessById(pid)) { } Thread.Sleep(1000); } catch (ArgumentException) { break; } } ProcessResult statusWait = GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "status", removeWaitingMessages: false); // There should not be any errors - in particular, there should not be // an error about "Waiting for GVFS.FunctionalTests.LockHolder" statusWait.Errors.ShouldEqual(string.Empty); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/HealthTests.cs ================================================ using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; using NUnit.Framework.Internal; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Text.RegularExpressions; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixture] public class HealthTests : TestsWithEnlistmentPerFixture { [TestCase, Order(0)] public void AfterCloningRepoIsPerfectlyHealthy() { // .gitignore is always a placeholder on creation // .gitconfig is always a modified path in functional tests since it is written at run time List topHydratedDirectories = new List { "GVFS", "GVFlt_BugRegressionTest", "GVFlt_DeleteFileTest", "GVFlt_DeleteFolderTest", "GVFlt_EnumTest" }; List directoryHydrationLevels = new List { 0, 0, 0, 0, 0 }; this.ValidateHealthOutputValues( directory: string.Empty, totalFiles: 1197, totalFilePercent: 100, fastFiles: 1, fastFilePercent: 0, slowFiles: 1, slowFilePercent: 0, totalPercent: 0, topHydratedDirectories: topHydratedDirectories, directoryHydrationLevels: directoryHydrationLevels, enlistmentHealthStatus: "OK"); } [TestCase, Order(1)] public void PlaceholdersChangeHealthScores() { // Hydrate all files in the Scripts/ directory as placeholders // This creates 6 placeholders, 5 files along with the Scripts/ directory this.HydratePlaceholder(Path.Combine(this.Enlistment.RepoRoot, "Scripts/CreateCommonAssemblyVersion.bat")); this.HydratePlaceholder(Path.Combine(this.Enlistment.RepoRoot, "Scripts/CreateCommonCliAssemblyVersion.bat")); this.HydratePlaceholder(Path.Combine(this.Enlistment.RepoRoot, "Scripts/CreateCommonVersionHeader.bat")); this.HydratePlaceholder(Path.Combine(this.Enlistment.RepoRoot, "Scripts/RunFunctionalTests.bat")); this.HydratePlaceholder(Path.Combine(this.Enlistment.RepoRoot, "Scripts/RunUnitTests.bat")); List topHydratedDirectories = new List { "Scripts", "GVFS", "GVFlt_BugRegressionTest", "GVFlt_DeleteFileTest", "GVFlt_DeleteFolderTest" }; List directoryHydrationLevels = new List { 5, 0, 0, 0, 0 }; this.ValidateHealthOutputValues( directory: string.Empty, totalFiles: 1197, totalFilePercent: 100, fastFiles: 7, fastFilePercent: 1, slowFiles: 1, slowFilePercent: 0, totalPercent:1, topHydratedDirectories: topHydratedDirectories, directoryHydrationLevels: directoryHydrationLevels, enlistmentHealthStatus: "OK"); } [TestCase, Order(2)] public void ModifiedPathsChangeHealthScores() { // Hydrate all files in GVFlt_FileOperationTest as modified paths // This creates 2 modified paths and one placeholder this.HydrateFullFile(Path.Combine(this.Enlistment.RepoRoot, "GVFlt_FileOperationTest/DeleteExistingFile.txt")); this.HydrateFullFile(Path.Combine(this.Enlistment.RepoRoot, "GVFlt_FileOperationTest/WriteAndVerify.txt")); List topHydratedDirectories = new List { "Scripts", "GVFlt_FileOperationTest", "GVFS", "GVFlt_BugRegressionTest", "GVFlt_DeleteFileTest" }; List directoryHydrationLevels = new List { 5, 2, 0, 0, 0 }; this.ValidateHealthOutputValues( directory: string.Empty, totalFiles: 1197, totalFilePercent: 100, fastFiles: 8, fastFilePercent: 1, slowFiles: 3, slowFilePercent: 0, totalPercent: 1, topHydratedDirectories: topHydratedDirectories, directoryHydrationLevels: directoryHydrationLevels, enlistmentHealthStatus: "OK"); } [TestCase, Order(3)] public void TurnPlaceholdersIntoModifiedPaths() { // Hydrate the files in Scripts/ from placeholders to modified paths this.HydrateFullFile(Path.Combine(this.Enlistment.RepoRoot, "Scripts/CreateCommonAssemblyVersion.bat")); this.HydrateFullFile(Path.Combine(this.Enlistment.RepoRoot, "Scripts/CreateCommonCliAssemblyVersion.bat")); this.HydrateFullFile(Path.Combine(this.Enlistment.RepoRoot, "Scripts/CreateCommonVersionHeader.bat")); this.HydrateFullFile(Path.Combine(this.Enlistment.RepoRoot, "Scripts/RunFunctionalTests.bat")); this.HydrateFullFile(Path.Combine(this.Enlistment.RepoRoot, "Scripts/RunUnitTests.bat")); List topHydratedDirectories = new List { "Scripts", "GVFlt_FileOperationTest", "GVFS", "GVFlt_BugRegressionTest", "GVFlt_DeleteFileTest" }; List directoryHydrationLevels = new List { 5, 2, 0, 0, 0 }; this.ValidateHealthOutputValues( directory: string.Empty, totalFiles: 1197, totalFilePercent: 100, fastFiles: 3, fastFilePercent: 0, slowFiles: 8, slowFilePercent: 1, totalPercent: 1, topHydratedDirectories: topHydratedDirectories, directoryHydrationLevels: directoryHydrationLevels, enlistmentHealthStatus: "OK"); } [TestCase, Order(4)] public void FilterIntoDirectory() { List topHydratedDirectories = new List(); List directoryHydrationLevels = new List(); this.ValidateHealthOutputValues( directory: "Scripts/", totalFiles: 5, totalFilePercent: 100, fastFiles: 0, fastFilePercent: 0, slowFiles: 5, slowFilePercent: 100, totalPercent: 100, topHydratedDirectories: topHydratedDirectories, directoryHydrationLevels: directoryHydrationLevels, enlistmentHealthStatus: "Highly Hydrated"); } private void HydratePlaceholder(string filePath) { File.ReadAllText(filePath); } private void HydrateFullFile(string filePath) { File.OpenWrite(filePath).Close(); } private void ValidateHealthOutputValues( string directory, int totalFiles, int totalFilePercent, int fastFiles, int fastFilePercent, int slowFiles, int slowFilePercent, int totalPercent, List topHydratedDirectories, List directoryHydrationLevels, string enlistmentHealthStatus) { List healthOutputLines = new List(this.Enlistment.Health(directory).Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)); int numberOfExpectedSubdirectories = topHydratedDirectories.Count; this.ValidateTargetDirectory(healthOutputLines[1], directory); this.ValidateTotalFileInfo(healthOutputLines[2], totalFiles, totalFilePercent); this.ValidateFastFileInfo(healthOutputLines[3], fastFiles, fastFilePercent); this.ValidateSlowFileInfo(healthOutputLines[4], slowFiles, slowFilePercent); this.ValidateTotalHydration(healthOutputLines[5], totalPercent); this.ValidateSubDirectoryHealth(healthOutputLines.GetRange(7, numberOfExpectedSubdirectories), topHydratedDirectories, directoryHydrationLevels); this.ValidateEnlistmentStatus(healthOutputLines[7 + numberOfExpectedSubdirectories], enlistmentHealthStatus); } private void ValidateTargetDirectory(string outputLine, string targetDirectory) { // Regex to extract the target directory // "Health of directory: " Match lineMatch = Regex.Match(outputLine, @"^Health of directory:\s*(.*)$"); string outputtedTargetDirectory = lineMatch.Groups[1].Value; outputtedTargetDirectory.ShouldEqual(targetDirectory); } private void ValidateTotalFileInfo(string outputLine, int totalFiles, int totalFilePercent) { // Regex to extract the total number of files and percentage they represent (should always be 100) // "Total files in HEAD commit: | %" Match lineMatch = Regex.Match(outputLine, @"^Total files in HEAD commit:\s*([\d,]+)\s*\|\s*(\d+)\s*%$"); int.TryParse(lineMatch.Groups[1].Value, NumberStyles.AllowThousands, CultureInfo.CurrentCulture.NumberFormat, out int outputtedTotalFiles).ShouldBeTrue(); int.TryParse(lineMatch.Groups[2].Value, out int outputtedTotalFilePercent).ShouldBeTrue(); outputtedTotalFiles.ShouldEqual(totalFiles); outputtedTotalFilePercent.ShouldEqual(totalFilePercent); } private void ValidateFastFileInfo(string outputLine, int fastFiles, int fastFilesPercent) { // Regex to extract the total number of fast files and percentage they represent // "Files managed by VFS for Git (fast): | %" Match lineMatch = Regex.Match(outputLine, @"^Files managed by VFS for Git \(fast\):\s*([\d,]+)\s*\|\s*(\d+)\s*%$"); int.TryParse(lineMatch.Groups[1].Value, NumberStyles.AllowThousands, CultureInfo.CurrentCulture.NumberFormat, out int outputtedFastFiles).ShouldBeTrue(); int.TryParse(lineMatch.Groups[2].Value, out int outputtedFastFilesPercent).ShouldBeTrue(); outputtedFastFiles.ShouldEqual(fastFiles); outputtedFastFilesPercent.ShouldEqual(fastFilesPercent); } private void ValidateSlowFileInfo(string outputLine, int slowFiles, int slowFilesPercent) { // Regex to extract the total number of slow files and percentage they represent // "Files managed by git (slow): | %" Match lineMatch = Regex.Match(outputLine, @"^Files managed by Git:\s*([\d,]+)\s*\|\s*(\d+)\s*%$"); int.TryParse(lineMatch.Groups[1].Value, NumberStyles.AllowThousands, CultureInfo.CurrentCulture.NumberFormat, out int outputtedSlowFiles).ShouldBeTrue(); int.TryParse(lineMatch.Groups[2].Value, out int outputtedSlowFilesPercent).ShouldBeTrue(); outputtedSlowFiles.ShouldEqual(slowFiles); outputtedSlowFilesPercent.ShouldEqual(slowFilesPercent); } private void ValidateTotalHydration(string outputLine, int totalHydration) { // Regex to extract the total hydration percentage of the enlistment // "Total hydration percentage: % Match lineMatch = Regex.Match(outputLine, @"^Total hydration percentage:\s*(\d+)\s*%$"); int.TryParse(lineMatch.Groups[1].Value, out int outputtedTotalHydration).ShouldBeTrue(); outputtedTotalHydration.ShouldEqual(totalHydration); } private void ValidateSubDirectoryHealth(List outputLines, List subdirectories, List healthScores) { for (int i = 0; i < outputLines.Count; i++) { // Regex to extract the most hydrated subdirectory names and their hydration percentage // " / | " listed several times for different directories Match lineMatch = Regex.Match(outputLines[i], @"^\s*([\d,]+)\s*/\s*([\d,]+)\s*\|\s*(\S.*\S)\s*$"); int.TryParse(lineMatch.Groups[1].Value, NumberStyles.AllowThousands, CultureInfo.CurrentCulture.NumberFormat, out int outputtedHealthScore).ShouldBeTrue(); string outputtedSubdirectory = lineMatch.Groups[3].Value; outputtedHealthScore.ShouldEqual(healthScores[i]); outputtedSubdirectory.ShouldEqual(subdirectories[i]); } } private void ValidateEnlistmentStatus(string outputLine, string statusMessage) { // Regex to extract the status message for the enlistment // "Repository status: " Match lineMatch = Regex.Match(outputLine, @"^Repository status:\s*(.*)$"); string outputtedStatusMessage = lineMatch.Groups[1].Value; outputtedStatusMessage.ShouldEqual(statusMessage); } } } ================================================ FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs ================================================ using GVFS.FunctionalTests.FileSystemRunners; using GVFS.FunctionalTests.Properties; using GVFS.FunctionalTests.Should; using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using Microsoft.Win32.SafeHandles; using NUnit.Framework; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { [TestFixture] [Category(Categories.ExtraCoverage)] public class MountTests : TestsWithEnlistmentPerFixture { private const int GVFSGenericError = 3; private const uint GenericRead = 2147483648; private const uint FileFlagBackupSemantics = 3355443; private readonly int fileDeletedBackgroundOperationCode; private readonly int directoryDeletedBackgroundOperationCode; private FileSystemRunner fileSystem; public MountTests() { this.fileSystem = new SystemIORunner(); this.fileDeletedBackgroundOperationCode = 3; this.directoryDeletedBackgroundOperationCode = 11; } [TestCaseSource(typeof(MountSubfolders), MountSubfolders.MountFolders)] public void SecondMountAttemptFails(string mountSubfolder) { this.MountShouldFail(0, "already mounted", this.Enlistment.GetVirtualPathTo(mountSubfolder)); } [TestCase] public void MountFailsOutsideEnlistment() { this.MountShouldFail("is not a valid GVFS enlistment", Path.GetDirectoryName(this.Enlistment.EnlistmentRoot)); } [TestCase] public void MountCopiesMissingReadObjectHook() { this.Enlistment.UnmountGVFS(); string readObjectPath = this.Enlistment.GetDotGitPath("hooks", "read-object" + Settings.Default.BinaryFileNameExtension); readObjectPath.ShouldBeAFile(this.fileSystem); this.fileSystem.DeleteFile(readObjectPath); readObjectPath.ShouldNotExistOnDisk(this.fileSystem); this.Enlistment.MountGVFS(); readObjectPath.ShouldBeAFile(this.fileSystem); } [TestCase] public void MountSetsCoreHooksPath() { try { GVFSHelpers.RegisterForOfflineIO(); this.Enlistment.UnmountGVFS(); GitProcess.Invoke(this.Enlistment.RepoBackingRoot, "config --unset core.hookspath"); string.IsNullOrWhiteSpace( GitProcess.Invoke(this.Enlistment.RepoBackingRoot, "config core.hookspath")) .ShouldBeTrue(); this.Enlistment.MountGVFS(); string expectedHooksPath = this.Enlistment.GetDotGitPath("hooks"); expectedHooksPath = GitHelpers.ConvertPathToGitFormat(expectedHooksPath); GitProcess.Invoke( this.Enlistment.RepoRoot, "config core.hookspath") .Trim('\n') .ShouldEqual(expectedHooksPath); } finally { GVFSHelpers.UnregisterForOfflineIO(); } } [TestCase] public void MountMergesLocalPrePostHooksConfig() { // Create some dummy pre/post command hooks string dummyCommandHookBin = "cmd.exe /c exit 0"; // Confirm git is not already using the dummy hooks string localGitPreCommandHooks = this.Enlistment.GetVirtualPathTo(".git", "hooks", "pre-command.hooks"); localGitPreCommandHooks.ShouldBeAFile(this.fileSystem).WithContents().Contains(dummyCommandHookBin).ShouldBeFalse(); string localGitPostCommandHooks = this.Enlistment.GetVirtualPathTo(".git", "hooks", "post-command.hooks"); localGitPreCommandHooks.ShouldBeAFile(this.fileSystem).WithContents().Contains(dummyCommandHookBin).ShouldBeFalse(); this.Enlistment.UnmountGVFS(); // Create dummy-
-command.hooks and set them in the local git config
            string dummyPreCommandHooksConfig = Path.Combine(this.Enlistment.EnlistmentRoot, "dummy-pre-command.hooks");
            this.fileSystem.WriteAllText(dummyPreCommandHooksConfig, dummyCommandHookBin);
            string dummyOostCommandHooksConfig = Path.Combine(this.Enlistment.EnlistmentRoot, "dummy-post-command.hooks");
            this.fileSystem.WriteAllText(dummyOostCommandHooksConfig, dummyCommandHookBin);

            // Configure the hooks locally
            GitProcess.Invoke(this.Enlistment.RepoRoot, $"config gvfs.clone.default-pre-command {dummyPreCommandHooksConfig}");
            GitProcess.Invoke(this.Enlistment.RepoRoot, $"config gvfs.clone.default-post-command {dummyOostCommandHooksConfig}");

            // Mount the repo
            this.Enlistment.MountGVFS();

            // .git\hooks\
-command.hooks should now contain our local dummy hook
            // The dummy pre-command hooks should appear first, and the post-command hook should appear last
            List mergedPreCommandHooksLines = localGitPreCommandHooks
                .ShouldBeAFile(this.fileSystem)
                .WithContents()
                .Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                .Where(line => !line.StartsWith("#"))
                .ToList();
            mergedPreCommandHooksLines.Count.ShouldEqual(2, $"Expected 2 lines, actual: {string.Join("\n", mergedPreCommandHooksLines)}");
            mergedPreCommandHooksLines[0].ShouldEqual(dummyCommandHookBin);

            List mergedPostCommandHooksLines = localGitPostCommandHooks
                .ShouldBeAFile(this.fileSystem)
                .WithContents()
                .Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                .Where(line => !line.StartsWith("#"))
                .ToList();
            mergedPostCommandHooksLines.Count.ShouldEqual(2, $"Expected 2 lines, actual: {string.Join("\n", mergedPostCommandHooksLines)}");
            mergedPostCommandHooksLines[1].ShouldEqual(dummyCommandHookBin);
        }

        [TestCase]
        public void MountChangesMountId()
        {
            string mountId = GitProcess.Invoke(this.Enlistment.RepoRoot, "config gvfs.mount-id")
                .Trim('\n');
            this.Enlistment.UnmountGVFS();
            this.Enlistment.MountGVFS();
            GitProcess.Invoke(this.Enlistment.RepoRoot, "config gvfs.mount-id")
                .Trim('\n')
                .ShouldNotEqual(mountId, "gvfs.mount-id should change on every mount");
        }

        [TestCase]
        public void MountFailsWhenNoOnDiskVersion()
        {
            this.Enlistment.UnmountGVFS();

            // Get the current disk layout version
            string majorVersion;
            string minorVersion;
            GVFSHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotGVFSRoot, out majorVersion, out minorVersion);

            int majorVersionNum;
            int minorVersionNum;
            int.TryParse(majorVersion.ShouldNotBeNull(), out majorVersionNum).ShouldEqual(true);
            int.TryParse(minorVersion.ShouldNotBeNull(), out minorVersionNum).ShouldEqual(true);

            // Move the RepoMetadata database to a temp file
            string versionDatabasePath = Path.Combine(this.Enlistment.DotGVFSRoot, GVFSHelpers.RepoMetadataName);
            versionDatabasePath.ShouldBeAFile(this.fileSystem);

            string tempDatabasePath = versionDatabasePath + "_MountFailsWhenNoOnDiskVersion";
            tempDatabasePath.ShouldNotExistOnDisk(this.fileSystem);

            this.fileSystem.MoveFile(versionDatabasePath, tempDatabasePath);
            versionDatabasePath.ShouldNotExistOnDisk(this.fileSystem);

            this.MountShouldFail("Failed to upgrade repo disk layout");

            // Move the RepoMetadata database back
            this.fileSystem.DeleteFile(versionDatabasePath);
            this.fileSystem.MoveFile(tempDatabasePath, versionDatabasePath);
            tempDatabasePath.ShouldNotExistOnDisk(this.fileSystem);
            versionDatabasePath.ShouldBeAFile(this.fileSystem);

            this.Enlistment.MountGVFS();
        }

        [TestCase]
        public void MountFailsWhenNoLocalCacheRootInRepoMetadata()
        {
            this.Enlistment.UnmountGVFS();

            string majorVersion;
            string minorVersion;
            GVFSHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotGVFSRoot, out majorVersion, out minorVersion);
            majorVersion.ShouldNotBeNull();
            minorVersion.ShouldNotBeNull();

            string objectsRoot = GVFSHelpers.GetPersistedGitObjectsRoot(this.Enlistment.DotGVFSRoot).ShouldNotBeNull();

            string metadataPath = Path.Combine(this.Enlistment.DotGVFSRoot, GVFSHelpers.RepoMetadataName);
            string metadataBackupPath = metadataPath + ".backup";
            this.fileSystem.MoveFile(metadataPath, metadataBackupPath);

            this.fileSystem.CreateEmptyFile(metadataPath);
            GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, majorVersion, minorVersion);
            GVFSHelpers.SaveGitObjectsRoot(this.Enlistment.DotGVFSRoot, objectsRoot);

            this.MountShouldFail("Failed to determine local cache path from repo metadata");

            this.fileSystem.DeleteFile(metadataPath);
            this.fileSystem.MoveFile(metadataBackupPath, metadataPath);

            this.Enlistment.MountGVFS();
        }

        [TestCase]
        public void MountFailsWhenNoGitObjectsRootInRepoMetadata()
        {
            this.Enlistment.UnmountGVFS();

            string majorVersion;
            string minorVersion;
            GVFSHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotGVFSRoot, out majorVersion, out minorVersion);
            majorVersion.ShouldNotBeNull();
            minorVersion.ShouldNotBeNull();

            string localCacheRoot = GVFSHelpers.GetPersistedLocalCacheRoot(this.Enlistment.DotGVFSRoot).ShouldNotBeNull();

            string metadataPath = Path.Combine(this.Enlistment.DotGVFSRoot, GVFSHelpers.RepoMetadataName);
            string metadataBackupPath = metadataPath + ".backup";
            this.fileSystem.MoveFile(metadataPath, metadataBackupPath);

            this.fileSystem.CreateEmptyFile(metadataPath);
            GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, majorVersion, minorVersion);
            GVFSHelpers.SaveLocalCacheRoot(this.Enlistment.DotGVFSRoot, localCacheRoot);

            this.MountShouldFail("Failed to determine git objects root from repo metadata");

            this.fileSystem.DeleteFile(metadataPath);
            this.fileSystem.MoveFile(metadataBackupPath, metadataPath);

            this.Enlistment.MountGVFS();
        }

        [TestCase]
        public void MountRegeneratesAlternatesFileWhenMissingGitObjectsRoot()
        {
            this.Enlistment.UnmountGVFS();

            string objectsRoot = GVFSHelpers.GetPersistedGitObjectsRoot(this.Enlistment.DotGVFSRoot).ShouldNotBeNull();

            string alternatesFilePath = this.Enlistment.GetDotGitPath("objects", "info", "alternates");
            alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot);
            this.fileSystem.WriteAllText(alternatesFilePath, "Z:\\invalidPath");

            this.Enlistment.MountGVFS();

            alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot);
        }

        [TestCase]
        public void MountRegeneratesAlternatesFileWhenMissingFromDisk()
        {
            this.Enlistment.UnmountGVFS();

            string objectsRoot = GVFSHelpers.GetPersistedGitObjectsRoot(this.Enlistment.DotGVFSRoot).ShouldNotBeNull();

            string alternatesFilePath = this.Enlistment.GetDotGitPath("objects", "info", "alternates");
            alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot);
            this.fileSystem.DeleteFile(alternatesFilePath);

            this.Enlistment.MountGVFS();

            alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot);
        }

        [TestCase]
        public void MountCanProcessSavedBackgroundQueueTasks()
        {
            string deletedFileEntry = "Test_EPF_WorkingDirectoryTests/1/2/3/4/ReadDeepProjectedFile.cpp";
            string deletedDirEntry = "Test_EPF_WorkingDirectoryTests/1/2/3/4/";
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, deletedFileEntry);
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, deletedDirEntry);
            this.Enlistment.UnmountGVFS();

            // Prime the background queue with delete messages
            string deleteFilePath = Path.Combine("Test_EPF_WorkingDirectoryTests", "1", "2", "3", "4", "ReadDeepProjectedFile.cpp");
            string deleteDirPath = Path.Combine("Test_EPF_WorkingDirectoryTests", "1", "2", "3", "4");
            string persistedDeleteFileTask = $"A 1\0{this.fileDeletedBackgroundOperationCode}\0{deleteFilePath}\0";
            string persistedDeleteDirectoryTask = $"A 2\0{this.directoryDeletedBackgroundOperationCode}\0{deleteDirPath}\0";
            this.fileSystem.WriteAllText(
                Path.Combine(this.Enlistment.EnlistmentRoot, GVFSTestConfig.DotGVFSRoot, "databases", "BackgroundGitOperations.dat"),
                $"{persistedDeleteFileTask}\r\n{persistedDeleteDirectoryTask}\r\n");

            // Background queue should process the delete messages and modifiedPaths should show the change
            this.Enlistment.MountGVFS();
            this.Enlistment.WaitForBackgroundOperations();
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, deletedFileEntry);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, deletedDirEntry);
        }

        [TestCase]
        public void MountingARepositoryThatRequiresPlaceholderUpdatesWorks()
        {
            string placeholderRelativePath = Path.Combine("EnumerateAndReadTestFiles", "a.txt");
            string placeholderPath = this.Enlistment.GetVirtualPathTo(placeholderRelativePath);

            // Ensure the placeholder is on disk and hydrated
            placeholderPath.ShouldBeAFile(this.fileSystem).WithContents();

            this.Enlistment.UnmountGVFS();

            File.Delete(placeholderPath);
            GVFSHelpers.DeletePlaceholder(
                Path.Combine(this.Enlistment.DotGVFSRoot, TestConstants.Databases.VFSForGit),
                placeholderRelativePath);
            GVFSHelpers.SetPlaceholderUpdatesRequired(this.Enlistment.DotGVFSRoot, true);

            this.Enlistment.MountGVFS();
        }

        [TestCaseSource(typeof(MountSubfolders), MountSubfolders.MountFolders)]
        public void MountFailsAfterBreakingDowngrade(string mountSubfolder)
        {
            MountSubfolders.EnsureSubfoldersOnDisk(this.Enlistment, this.fileSystem);
            this.Enlistment.UnmountGVFS();

            string majorVersion;
            string minorVersion;
            GVFSHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotGVFSRoot, out majorVersion, out minorVersion);

            int majorVersionNum;
            int minorVersionNum;
            int.TryParse(majorVersion.ShouldNotBeNull(), out majorVersionNum).ShouldEqual(true);
            int.TryParse(minorVersion.ShouldNotBeNull(), out minorVersionNum).ShouldEqual(true);

            GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, (majorVersionNum + 1).ToString(), "0");

            this.MountShouldFail("do not allow mounting after downgrade", this.Enlistment.GetVirtualPathTo(mountSubfolder));

            GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, majorVersionNum.ToString(), minorVersionNum.ToString());
            this.Enlistment.MountGVFS();
        }

        [TestCaseSource(typeof(MountSubfolders), MountSubfolders.MountFolders)]
        public void MountFailsUpgradingFromInvalidUpgradePath(string mountSubfolder)
        {
            MountSubfolders.EnsureSubfoldersOnDisk(this.Enlistment, this.fileSystem);
            string headCommitId = GitProcess.Invoke(this.Enlistment.RepoRoot, "rev-parse HEAD");

            this.Enlistment.UnmountGVFS();

            string majorVersion;
            string minorVersion;
            GVFSHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotGVFSRoot, out majorVersion, out minorVersion);

            int majorVersionNum;
            int minorVersionNum;
            int.TryParse(majorVersion.ShouldNotBeNull(), out majorVersionNum).ShouldEqual(true);
            int.TryParse(minorVersion.ShouldNotBeNull(), out minorVersionNum).ShouldEqual(true);

            // 1 will always be below the minumum support version number
            GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, "1", "0");
            this.MountShouldFail("Breaking change to GVFS disk layout has been made since cloning", this.Enlistment.GetVirtualPathTo(mountSubfolder));

            GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, majorVersionNum.ToString(), minorVersionNum.ToString());
            this.Enlistment.MountGVFS();
        }

        // Ported from ProjFS's BugRegressionTest
        [TestCase]
        public void ProjFS_CMDHangNoneActiveInstance()
        {
            this.Enlistment.UnmountGVFS();

            using (SafeFileHandle handle = NativeMethods.CreateFile(
                Path.Combine(this.Enlistment.RepoRoot, "aaa", "aaaa"),
                GenericRead,
                FileShare.Read,
                IntPtr.Zero,
                FileMode.Open,
                FileFlagBackupSemantics,
                IntPtr.Zero))
            {
                int lastError = Marshal.GetLastWin32Error();
                handle.IsInvalid.ShouldEqual(true);
                lastError.ShouldNotEqual(0); // 0 == ERROR_SUCCESS
            }

            this.Enlistment.MountGVFS();
        }

        private void MountShouldFail(int expectedExitCode, string expectedErrorMessage, string mountWorkingDirectory = null)
        {
            string enlistmentRoot = this.Enlistment.EnlistmentRoot;

            // TODO: 865304 Use app.config instead of --internal* arguments
            ProcessStartInfo processInfo = new ProcessStartInfo(GVFSTestConfig.PathToGVFS);
            processInfo.Arguments = "mount " + TestConstants.InternalUseOnlyFlag + " " + GVFSHelpers.GetInternalParameter();
            processInfo.WindowStyle = ProcessWindowStyle.Hidden;
            processInfo.WorkingDirectory = string.IsNullOrEmpty(mountWorkingDirectory) ? enlistmentRoot : mountWorkingDirectory;
            processInfo.UseShellExecute = false;
            processInfo.RedirectStandardOutput = true;

            ProcessResult result = ProcessHelper.Run(processInfo);
            result.ExitCode.ShouldEqual(expectedExitCode, $"mount exit code was not {expectedExitCode}. Output: {result.Output}");
            result.Output.ShouldContain(expectedErrorMessage);
        }

        private void MountShouldFail(string expectedErrorMessage, string mountWorkingDirectory = null)
        {
            this.MountShouldFail(GVFSGenericError, expectedErrorMessage, mountWorkingDirectory);
        }

        private class MountSubfolders
        {
            public const string MountFolders = "Folders";

            public static object[] Folders
            {
                get
                {
                    // On Linux, an unmounted repository is completely empty, so we must
                    // only try to mount from the root of the virtual path.

                    if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
                    {
                        return new object[] { new object[] { string.Empty } };
                    }
                    else
                    {
                        return new object[]
                        {
                            new object[] { string.Empty },
                            new object[] { "GVFS" },
                        };
                    }
                }
            }

            public static void EnsureSubfoldersOnDisk(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem)
            {
                // Enumerate the directory to ensure that the folder is on disk after GVFS is unmounted
                foreach (object[] folder in Folders)
                {
                    string folderPath = enlistment.GetVirtualPathTo((string)folder[0]);
                    folderPath.ShouldBeADirectory(fileSystem).WithItems();
                }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.IO;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    // TODO 452590 - Combine all of the MoveRenameTests into a single fixture, and have each use different
    // well known files
    [TestFixtureSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
    public class MoveRenameFileTests : TestsWithEnlistmentPerFixture
    {
        public const string TestFileContents =
@"using NUnitLite;
using System;
using System.Threading;

namespace GVFS.StressTests
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string[] test_args = args;

            for (int i = 0; i < Properties.Settings.Default.TestRepeatCount; i++)
            {
                Console.WriteLine(""Starting pass {0}"", i + 1);
                DateTime now = DateTime.Now;
                new AutoRun().Execute(test_args);
                Console.WriteLine(""Completed pass {0} in {1}"", i + 1, DateTime.Now - now);
                Console.WriteLine();

                Thread.Sleep(TimeSpan.FromSeconds(1));
            }

            Console.WriteLine(""All tests completed.  Press Enter to exit."");
            Console.ReadLine();
        }
    }
}";

        private FileSystemRunner fileSystem;

        public MoveRenameFileTests(FileSystemRunner fileSystem)
        {
            this.fileSystem = fileSystem;
        }

        [TestCase]
        public void ChangeUnhydratedFileName()
        {
            string oldFilename = Path.Combine("Test_EPF_MoveRenameFileTests", "ChangeUnhydratedFileName", "Program.cs");
            string newFilename = Path.Combine("Test_EPF_MoveRenameFileTests", "ChangeUnhydratedFileName", "renamed_Program.cs");

            // Don't read oldFilename or check for its existence before calling MoveFile, because doing so
            // can cause the file to hydrate
            this.Enlistment.GetVirtualPathTo(newFilename).ShouldNotExistOnDisk(this.fileSystem);

            this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), this.Enlistment.GetVirtualPathTo(newFilename));
            this.Enlistment.GetVirtualPathTo(newFilename).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents);
            this.Enlistment.GetVirtualPathTo(oldFilename).ShouldNotExistOnDisk(this.fileSystem);

            this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(newFilename), this.Enlistment.GetVirtualPathTo(oldFilename));
            this.Enlistment.GetVirtualPathTo(oldFilename).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents);
            this.Enlistment.GetVirtualPathTo(newFilename).ShouldNotExistOnDisk(this.fileSystem);
        }

        [TestCase]
        public void ChangeUnhydratedFileNameCase()
        {
            string oldName = "Readme.md";
            string newName = "readme.md";

            string oldVirtualPath = this.Enlistment.GetVirtualPathTo(oldName);
            string newVirtualPath = this.Enlistment.GetVirtualPathTo(newName);

            this.ChangeUnhydratedFileCase(oldName, oldVirtualPath, newName, newVirtualPath, knownFileContents: null);
        }

        [TestCase]
        public void ChangeNestedUnhydratedFileNameCase()
        {
            string oldName = "Program.cs";
            string newName = "program.cs";
            string folderName = Path.Combine("Test_EPF_MoveRenameFileTests", "ChangeNestedUnhydratedFileNameCase");

            string oldVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(folderName, oldName));
            string newVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(folderName, newName));

            this.ChangeUnhydratedFileCase(oldName, oldVirtualPath, newName, newVirtualPath, TestFileContents);
        }

        [TestCase]
        public void MoveUnhydratedFileToDotGitFolder()
        {
            string targetFolderName = ".git";
            this.Enlistment.GetVirtualPathTo(targetFolderName).ShouldBeADirectory(this.fileSystem);

            string testFileName = "Program.cs";
            string testFileFolder = Path.Combine("Test_EPF_MoveRenameFileTests", "MoveUnhydratedFileToDotGitFolder");
            string testFilePathSubPath = Path.Combine(testFileFolder, testFileName);

            string newTestFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(targetFolderName), testFileName);

            this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(testFilePathSubPath), newTestFileVirtualPath);
            this.Enlistment.GetVirtualPathTo(testFilePathSubPath).ShouldNotExistOnDisk(this.fileSystem);
            newTestFileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(TestFileContents);

            this.fileSystem.DeleteFile(newTestFileVirtualPath);
            newTestFileVirtualPath.ShouldNotExistOnDisk(this.fileSystem);
        }

        [TestCase]
        public void MoveVirtualNTFSFileToOverwriteUnhydratedFile()
        {
            string targetFilename = ".gitattributes";

            string sourceFilename = "SourceFile.txt";
            string sourceFileContents = "The Source";

            this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(sourceFilename), sourceFileContents);
            this.Enlistment.GetVirtualPathTo(sourceFilename).ShouldBeAFile(this.fileSystem).WithContents(sourceFileContents);

            this.fileSystem.ReplaceFile(this.Enlistment.GetVirtualPathTo(sourceFilename), this.Enlistment.GetVirtualPathTo(targetFilename));
            this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(sourceFileContents);

            this.Enlistment.GetVirtualPathTo(sourceFilename).ShouldNotExistOnDisk(this.fileSystem);
        }

        private void ChangeUnhydratedFileCase(
            string oldName,
            string oldVirtualPath,
            string newName,
            string newVirtualPath,
            string knownFileContents)
        {
            this.fileSystem.MoveFile(oldVirtualPath, newVirtualPath);
            string fileContents = newVirtualPath.ShouldBeAFile(this.fileSystem).WithCaseMatchingName(newName).WithContents();
            fileContents.ShouldBeNonEmpty();
            if (knownFileContents != null)
            {
                fileContents.ShouldEqual(knownFileContents);
            }

            this.fileSystem.MoveFile(newVirtualPath, oldVirtualPath);
            oldVirtualPath.ShouldBeAFile(this.fileSystem).WithCaseMatchingName(oldName).WithContents(fileContents);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests_2.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using NUnit.Framework;
using System.IO;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    // TODO 452590 - Combine all of the MoveRenameTests into a single fixture, and have each use different
    // well known files
    [TestFixtureSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
    public class MoveRenameFileTests_2 : TestsWithEnlistmentPerFixture
    {
        private const string TestFileFolder = "Test_EPF_MoveRenameFileTests_2";

        // Test_EPF_MoveRenameFileTests_2\RunUnitTests.bat
        private const string RunUnitTestsContents =
@"@ECHO OFF
IF ""%1""=="""" (SET ""Configuration=Debug"") ELSE (SET ""Configuration=%1"")

%~dp0\..\..\BuildOutput\GVFS.UnitTests\bin\x64\%Configuration%\GVFS.UnitTests.exe";

        // Test_EPF_MoveRenameFileTests_2\RunFunctionalTests.bat
        private const string RunFunctioanlTestsContents =
@"@ECHO OFF
IF ""%1""=="""" (SET ""Configuration=Debug"") ELSE (SET ""Configuration=%1"")

%~dp0\..\..\BuildOutput\GVFS.FunctionalTests\bin\x64\%Configuration%\GVFS.FunctionalTests.exe %2";

        private FileSystemRunner fileSystem;

        public MoveRenameFileTests_2(FileSystemRunner fileSystem)
        {
            this.fileSystem = fileSystem;
        }

        // This test needs the GVFS folder to not exist on physical disk yet, so run it first
        [TestCase, Order(1)]
        public void MoveUnhydratedFileToUnhydratedFolderAndWrite()
        {
            string testFileContents = RunUnitTestsContents;
            string testFileName = "RunUnitTests.bat";

            // Assume there will always be a GVFS folder when running tests
            string testFolderName = "GVFS";

            string oldTestFileVirtualPath = this.Enlistment.GetVirtualPathTo(TestFileFolder, testFileName);
            string newTestFileVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName, testFileName);

            this.fileSystem.MoveFile(oldTestFileVirtualPath, newTestFileVirtualPath);
            oldTestFileVirtualPath.ShouldNotExistOnDisk(this.fileSystem);
            newTestFileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(testFileContents);
            this.Enlistment.GetVirtualPathTo(testFolderName).ShouldBeADirectory(this.fileSystem);

            // Writing after the move should succeed
            string newText = "New file text for test file";
            this.fileSystem.WriteAllText(newTestFileVirtualPath, newText);
            newTestFileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(newText);
        }

        [TestCase, Order(2)]
        public void MoveUnhydratedFileToNewFolderAndWrite()
        {
            string testFolderName = "test_folder";
            this.Enlistment.GetVirtualPathTo(testFolderName).ShouldNotExistOnDisk(this.fileSystem);

            this.fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderName));
            this.Enlistment.GetVirtualPathTo(testFolderName).ShouldBeADirectory(this.fileSystem);

            string testFileName = "RunFunctionalTests.bat";
            string testFileContents = RunFunctioanlTestsContents;

            string newTestFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(testFolderName), testFolderName);

            this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(TestFileFolder, testFileName), newTestFileVirtualPath);
            this.Enlistment.GetVirtualPathTo(TestFileFolder, testFileName).ShouldNotExistOnDisk(this.fileSystem);
            newTestFileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(testFileContents);

            // Writing after the move should succeed
            string newText = "New file text for test file";
            this.fileSystem.WriteAllText(newTestFileVirtualPath, newText);
            newTestFileVirtualPath.ShouldBeAFile(this.fileSystem);
            newTestFileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(newText);

            this.fileSystem.DeleteFile(newTestFileVirtualPath);
            newTestFileVirtualPath.ShouldNotExistOnDisk(this.fileSystem);

            this.fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(testFolderName));
            this.Enlistment.GetVirtualPathTo(testFolderName).ShouldNotExistOnDisk(this.fileSystem);
        }

        [TestCase, Order(3)]
        public void MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite()
        {
            string targetFilename = Path.Combine(TestFileFolder, "MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite", "RunFunctionalTests.bat");
            string sourceFilename = Path.Combine(TestFileFolder, "MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite", "RunUnitTests.bat");
            string sourceFileContents = RunUnitTestsContents;

            // Overwriting one unhydrated file with another should create a file at the target
            this.fileSystem.ReplaceFile(this.Enlistment.GetVirtualPathTo(sourceFilename), this.Enlistment.GetVirtualPathTo(targetFilename));
            this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(sourceFileContents);

            // Source file should be gone
            this.Enlistment.GetVirtualPathTo(sourceFilename).ShouldNotExistOnDisk(this.fileSystem);

            // Writing after move should succeed
            string newText = "New file text for target file";
            this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(targetFilename), newText);
            this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(newText);
        }

        [TestCase, Order(4)]
        public void CaseOnlyRenameFileInSubfolder()
        {
            string oldFilename = "CaseOnlyRenameFileInSubfolder.txt";
            string oldVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFileFolder, oldFilename));
            oldVirtualPath.ShouldBeAFile(this.fileSystem).WithCaseMatchingName(oldFilename);

            string newFilename = "caseonlyrenamefileinsubfolder.txt";
            string newVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFileFolder, newFilename));

            // Rename file, and confirm file name case was updated
            this.fileSystem.MoveFile(oldVirtualPath, newVirtualPath);
            newVirtualPath.ShouldBeAFile(this.fileSystem).WithCaseMatchingName(newFilename);
        }

        [TestCase, Order(5)]
        public void MoveUnhydratedFileToOverwriteFullFileAndWrite()
        {
            string targetFilename = "TargetFile.txt";
            string targetFileContents = "The Target";

            string sourceFilename = Path.Combine(
                TestFileFolder,
                "MoveUnhydratedFileToOverwriteFullFileAndWrite",
                "MoveUnhydratedFileToOverwriteFullFileAndWrite.txt");

            string sourceFileContents =
@"

  
  
";

            this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(targetFilename), targetFileContents);
            this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(targetFileContents);

            // Overwriting a virtual NTFS file with an unprojected file should leave a file on disk at the
            // target location
            this.fileSystem.ReplaceFile(this.Enlistment.GetVirtualPathTo(sourceFilename), this.Enlistment.GetVirtualPathTo(targetFilename));
            this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(sourceFileContents);

            // Source file should be gone
            this.Enlistment.GetVirtualPathTo(sourceFilename).ShouldNotExistOnDisk(this.fileSystem);

            // Writes should succeed after move
            string newText = "New file text for Readme.md";
            this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(targetFilename), newText);
            this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(newText);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFolderTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.IO;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixtureSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
    public class MoveRenameFolderTests : TestsWithEnlistmentPerFixture
    {
        private const string TestFileContents =
@"// dllmain.cpp : Defines the entry point for the DLL application.
#include ""stdafx.h""

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    UNREFERENCED_PARAMETER(hModule);
    UNREFERENCED_PARAMETER(lpReserved);

    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

";
        private FileSystemRunner fileSystem;

        public MoveRenameFolderTests(FileSystemRunner fileSystem)
        {
            this.fileSystem = fileSystem;
        }

        [TestCase]
        public void RenameFolderShouldFail()
        {
            string testFileName = "RenameFolderShouldFail.cpp";
            string oldFolderName = Path.Combine("Test_EPF_MoveRenameFolderTests", "RenameFolderShouldFail", "source");
            string newFolderName = Path.Combine("Test_EPF_MoveRenameFolderTests", "RenameFolderShouldFail", "sourcerenamed");
            this.Enlistment.GetVirtualPathTo(newFolderName).ShouldNotExistOnDisk(this.fileSystem);

            this.fileSystem.MoveDirectory_RequestShouldNotBeSupported(this.Enlistment.GetVirtualPathTo(oldFolderName), this.Enlistment.GetVirtualPathTo(newFolderName));

            this.Enlistment.GetVirtualPathTo(oldFolderName).ShouldBeADirectory(this.fileSystem);
            this.Enlistment.GetVirtualPathTo(newFolderName).ShouldNotExistOnDisk(this.fileSystem);

            this.Enlistment.GetVirtualPathTo(Path.Combine(newFolderName, testFileName)).ShouldNotExistOnDisk(this.fileSystem);
            this.Enlistment.GetVirtualPathTo(Path.Combine(oldFolderName, testFileName)).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents);
        }

        [TestCase]
        public void MoveFullFolderToFullFolderInDotGitFolder()
        {
            string fileContents = "Test contents for MoveFullFolderToFullFolderInDotGitFolder";
            string testFileName = "MoveFullFolderToFullFolderInDotGitFolder.txt";
            string oldFolderPath = this.Enlistment.GetVirtualPathTo("MoveFullFolderToFullFolderInDotGitFolder");
            oldFolderPath.ShouldNotExistOnDisk(this.fileSystem);
            this.fileSystem.CreateDirectory(oldFolderPath);
            oldFolderPath.ShouldBeADirectory(this.fileSystem);

            string oldFilePath = Path.Combine(oldFolderPath, testFileName);
            this.fileSystem.WriteAllText(oldFilePath, fileContents);
            oldFilePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents);

            string newFolderName = "NewMoveFullFolderToFullFolderInDotGitFolder";
            string newFolderPath = this.Enlistment.GetVirtualPathTo(".git", newFolderName);
            newFolderPath.ShouldNotExistOnDisk(this.fileSystem);
            this.fileSystem.CreateDirectory(newFolderPath);
            newFolderPath.ShouldBeADirectory(this.fileSystem);

            string movedFolderPath = Path.Combine(newFolderPath, "Should");
            this.fileSystem.MoveDirectory(oldFolderPath, movedFolderPath);

            Path.Combine(movedFolderPath).ShouldBeADirectory(this.fileSystem);
            oldFolderPath.ShouldNotExistOnDisk(this.fileSystem);
            Path.Combine(movedFolderPath, testFileName).ShouldBeAFile(this.fileSystem).WithContents(fileContents);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MultithreadedReadWriteTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.IO;
using System.Text;
using System.Threading;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    // TODO 469238: Elaborate on these tests?
    [TestFixture]
    public class MultithreadedReadWriteTests : TestsWithEnlistmentPerFixture
    {
        [TestCase, Order(1)]
        public void CanReadVirtualFileInParallel()
        {
            // Note: This test MUST go first, or else it needs to ensure that it is reading a unique path compared to the
            // other tests in this class. That applies to every directory in the path, as well as the leaf file name.
            // Otherwise, this test loses most of its value because there will be no races occurring on creating the
            // placeholder directories, enumerating them, and then creating a placeholder file and hydrating it.

            string fileName = Path.Combine("GVFS", "GVFS.FunctionalTests", "Tests", "LongRunningEnlistment", "GitMoveRenameTests.cs");
            string virtualPath = this.Enlistment.GetVirtualPathTo(fileName);

            Exception readException = null;

            Thread[] threads = new Thread[128];
            for (int i = 0; i < threads.Length; ++i)
            {
                threads[i] = new Thread(() =>
                {
                    try
                    {
                        FileSystemRunner.DefaultRunner.ReadAllText(virtualPath).ShouldBeNonEmpty();
                    }
                    catch (Exception e)
                    {
                        readException = e;
                    }
                });

                threads[i].Start();
            }

            for (int i = 0; i < threads.Length; ++i)
            {
                threads[i].Join();
            }

            readException.ShouldBeNull("At least one of the reads failed");
        }

        [TestCase, Order(2)]
        public void CanReadHydratedPlaceholderInParallel()
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;
            string fileName = Path.Combine("GVFS", "GVFS.FunctionalTests", "Tests", "LongRunningEnlistment", "WorkingDirectoryTests.cs");
            string virtualPath = this.Enlistment.GetVirtualPathTo(fileName);
            virtualPath.ShouldBeAFile(fileSystem);

            // Not using the runner because reading specific bytes isn't common
            // Can't use ReadAllText because it will remove some bytes that the stream won't.
            byte[] actualContents = File.ReadAllBytes(virtualPath);

            Thread[] threads = new Thread[4];

            // Readers
            bool keepRunning = true;
            for (int i = 0; i < threads.Length; ++i)
            {
                int myIndex = i;
                threads[i] = new Thread(() =>
                {
                    // Create random seeks (seeded for repeatability)
                    Random randy = new Random(myIndex);

                    // Small buffer so we hit the drive a lot.
                    // Block larger than the buffer to hit the drive more
                    const int SmallBufferSize = 128;
                    const int LargerBlockSize = SmallBufferSize * 10;

                    using (Stream reader = new FileStream(virtualPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, SmallBufferSize, false))
                    {
                        while (keepRunning)
                        {
                            byte[] block = new byte[LargerBlockSize];

                            // Always try to grab a full block (easier for asserting)
                            int position = randy.Next((int)reader.Length - block.Length - 1);

                            reader.Position = position;
                            reader.Read(block, 0, block.Length).ShouldEqual(block.Length);
                            block.ShouldEqual(actualContents, position, block.Length);
                        }
                    }
                });

                threads[i].Start();
            }

            Thread.Sleep(2500);
            keepRunning = false;

            for (int i = 0; i < threads.Length; ++i)
            {
                threads[i].Join();
            }
        }

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        [Order(3)]
        public void CanReadWriteAFileInParallel(FileSystemRunner fileSystem)
        {
            string fileName = @"CanReadWriteAFileInParallel";
            string virtualPath = this.Enlistment.GetVirtualPathTo(fileName);

            // Create the file new each time.
            virtualPath.ShouldNotExistOnDisk(fileSystem);
            File.Create(virtualPath).Dispose();

            bool keepRunning = true;
            Thread[] threads = new Thread[4];
            StringBuilder[] fileContents = new StringBuilder[4];

            // Writer
            fileContents[0] = new StringBuilder();
            threads[0] = new Thread(() =>
            {
                DateTime start = DateTime.Now;
                Random r = new Random(0); // Seeded for repeatability
                while ((DateTime.Now - start).TotalSeconds < 2.5)
                {
                    string newChar = r.Next(10).ToString();
                    fileSystem.AppendAllText(virtualPath, newChar);
                    fileContents[0].Append(newChar);
                    Thread.Yield();
                }

                keepRunning = false;
            });

            // Readers
            for (int i = 1; i < threads.Length; ++i)
            {
                int myIndex = i;
                fileContents[i] = new StringBuilder();
                threads[i] = new Thread(() =>
                {
                    using (Stream readStream = File.Open(virtualPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                    using (StreamReader reader = new StreamReader(readStream, true))
                    {
                        while (keepRunning)
                        {
                            Thread.Yield();
                            fileContents[myIndex].Append(reader.ReadToEnd());
                        }

                        // Catch the last write that might have escaped us
                        fileContents[myIndex].Append(reader.ReadToEnd());
                    }
                });
            }

            foreach (Thread thread in threads)
            {
                thread.Start();
            }

            foreach (Thread thread in threads)
            {
                thread.Join();
            }

            for (int i = 1; i < threads.Length; ++i)
            {
                fileContents[i].ToString().ShouldEqual(fileContents[0].ToString());
            }

            fileSystem.DeleteFile(virtualPath);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/ParallelHydrationTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixtureSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
    public class ParallelHydrationTests : TestsWithEnlistmentPerFixture
    {
        private FileSystemRunner fileSystem;

        public ParallelHydrationTests(FileSystemRunner fileSystem)
                : base(forcePerRepoObjectCache: true)
        {
            this.fileSystem = fileSystem;
        }

        [TestCase]
        [Category(Categories.ExtraCoverage)]
        public void HydrateRepoInParallel()
        {
            GitProcess.Invoke(this.Enlistment.RepoRoot, $"checkout -f {FileConstants.CommitId}");

            ConcurrentBag collection = new ConcurrentBag();
            List threads = new List();
            foreach (string path in FileConstants.Paths)
            {
                Thread thread = new Thread(() =>
                {
                    try
                    {
                        this.fileSystem.ReadAllText(this.Enlistment.GetVirtualPathTo(path));
                        collection.Add(path);
                    }
                    catch (Exception e)
                    {
                        collection.Add($"Exception while hydrating {path}: {e.Message}");
                    }
                });
                threads.Add(thread);
                thread.Start();
            }

            foreach (Thread thread in threads)
            {
                thread.Join();
            }

            for (int i = 0; i < FileConstants.Paths.Count; i++)
            {
                collection.TryTake(out string value).ShouldBeTrue();
                FileConstants.Paths.Contains(value).ShouldBeTrue(message: value);
            }
        }

        private class FileConstants
        {
            public static readonly string CommitId = "b76df49a1e02465ef1c27d9c2a1720b337de99c8";

            /// 
            /// Generate in Git Bash using command:
            /// git ls-tree --full-tree -r --name-only HEAD | awk '{print "\""$1"\","}'
            /// 
            public static HashSet Paths = new HashSet()
            {
                ".gitattributes",
                ".gitignore",
                "AuthoringTests.md",
                "DeleteFileWithNameAheadOfDotAndSwitchCommits/(1).txt",
                "DeleteFileWithNameAheadOfDotAndSwitchCommits/1",
                "DeleteFileWithNameAheadOfDotAndSwitchCommits/test.txt",
                "EnumerateAndReadTestFiles/.B",
                "EnumerateAndReadTestFiles/._",
                "EnumerateAndReadTestFiles/._a",
                "EnumerateAndReadTestFiles/.~B",
                "EnumerateAndReadTestFiles/.~_B",
                "EnumerateAndReadTestFiles/A_100.txt",
                "EnumerateAndReadTestFiles/_C",
                "EnumerateAndReadTestFiles/_a",
                "EnumerateAndReadTestFiles/_aB",
                "EnumerateAndReadTestFiles/a",
                "EnumerateAndReadTestFiles/a.txt",
                "EnumerateAndReadTestFiles/a_1.txt",
                "EnumerateAndReadTestFiles/a_10.txt",
                "EnumerateAndReadTestFiles/a_3.txt",
                "EnumerateAndReadTestFiles/ab_",
                "EnumerateAndReadTestFiles/z_test.txt",
                "EnumerateAndReadTestFiles/zctest.txt",
                "EnumerateAndReadTestFiles/~B",
                "ErrorWhenPathTreatsFileAsFolderMatchesNTFS/full",
                "ErrorWhenPathTreatsFileAsFolderMatchesNTFS/partial",
                "ErrorWhenPathTreatsFileAsFolderMatchesNTFS/virtual",
                "GVFS.sln",
                "GVFS/FastFetch/App.config",
                "GVFS/FastFetch/CheckoutFetchHelper.cs",
                "GVFS/FastFetch/FastFetch.csproj",
                "GVFS/FastFetch/FastFetchVerb.cs",
                "GVFS/FastFetch/FetchHelper.cs",
                "GVFS/FastFetch/Git/DiffHelper.cs",
                "GVFS/FastFetch/Git/GitPackIndex.cs",
                "GVFS/FastFetch/Git/LibGit2Helpers.cs",
                "GVFS/FastFetch/Git/LibGit2Repo.cs",
                "GVFS/FastFetch/Git/LsTreeHelper.cs",
                "GVFS/FastFetch/Git/RefSpecHelpers.cs",
                "GVFS/FastFetch/Git/UpdateRefsHelper.cs",
                "GVFS/FastFetch/GitEnlistment.cs",
                "GVFS/FastFetch/Jobs/BatchObjectDownloadJob.cs",
                "GVFS/FastFetch/Jobs/CheckoutJob.cs",
                "GVFS/FastFetch/Jobs/Data/BlobDownloadRequest.cs",
                "GVFS/FastFetch/Jobs/Data/IndexPackRequest.cs",
                "GVFS/FastFetch/Jobs/Data/TreeSearchRequest.cs",
                "GVFS/FastFetch/Jobs/FindMissingBlobsJob.cs",
                "GVFS/FastFetch/Jobs/IndexPackJob.cs",
                "GVFS/FastFetch/Jobs/Job.cs",
                "GVFS/FastFetch/Program.cs",
                "GVFS/FastFetch/Properties/AssemblyInfo.cs",
                "GVFS/FastFetch/packages.config",
                "GVFS/GVFS.Common/AntiVirusExclusions.cs",
                "GVFS/GVFS.Common/BatchedLooseObjects/BatchedLooseObjectDeserializer.cs",
                "GVFS/GVFS.Common/CallbackResult.cs",
                "GVFS/GVFS.Common/ConcurrentHashSet.cs",
                "GVFS/GVFS.Common/Enlistment.cs",
                "GVFS/GVFS.Common/GVFS.Common.csproj",
                "GVFS/GVFS.Common/GVFSConstants.cs",
                "GVFS/GVFS.Common/GVFSContext.cs",
                "GVFS/GVFS.Common/GVFSEnlistment.cs",
                "GVFS/GVFS.Common/GVFSLock.cs",
                "GVFS/GVFS.Common/Git/CatFileTimeoutException.cs",
                "GVFS/GVFS.Common/Git/DiffTreeResult.cs",
                "GVFS/GVFS.Common/Git/GVFSConfigResponse.cs",
                "GVFS/GVFS.Common/Git/GitCatFileBatchCheckProcess.cs",
                "GVFS/GVFS.Common/Git/GitCatFileBatchProcess.cs",
                "GVFS/GVFS.Common/Git/GitCatFileProcess.cs",
                "GVFS/GVFS.Common/Git/GitObjects.cs",
                "GVFS/GVFS.Common/Git/GitPathConverter.cs",
                "GVFS/GVFS.Common/Git/GitProcess.cs",
                "GVFS/GVFS.Common/Git/GitRefs.cs",
                "GVFS/GVFS.Common/Git/GitTreeEntry.cs",
                "GVFS/GVFS.Common/Git/GitVersion.cs",
                "GVFS/GVFS.Common/Git/HttpGitObjects.cs",
                "GVFS/GVFS.Common/GitHelper.cs",
                "GVFS/GVFS.Common/HeartbeatThread.cs",
                "GVFS/GVFS.Common/IBackgroundOperation.cs",
                "GVFS/GVFS.Common/InvalidRepoException.cs",
                "GVFS/GVFS.Common/MountParameters.cs",
                "GVFS/GVFS.Common/NamedPipes/BrokenPipeException.cs",
                "GVFS/GVFS.Common/NamedPipes/NamedPipeClient.cs",
                "GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs",
                "GVFS/GVFS.Common/NamedPipes/NamedPipeServer.cs",
                "GVFS/GVFS.Common/NativeMethods.cs",
                "GVFS/GVFS.Common/Physical/FileSystem/DirectoryItemInfo.cs",
                "GVFS/GVFS.Common/Physical/FileSystem/FileProperties.cs",
                "GVFS/GVFS.Common/Physical/FileSystem/PhysicalFileSystem.cs",
                "GVFS/GVFS.Common/Physical/FileSystem/StreamReaderExtensions.cs",
                "GVFS/GVFS.Common/Physical/Git/BigEndianReader.cs",
                "GVFS/GVFS.Common/Physical/Git/CopyBlobContentTimeoutException.cs",
                "GVFS/GVFS.Common/Physical/Git/EndianHelper.cs",
                "GVFS/GVFS.Common/Physical/Git/GVFSGitObjects.cs",
                "GVFS/GVFS.Common/Physical/Git/GitIndex.cs",
                "GVFS/GVFS.Common/Physical/Git/GitRepo.cs",
                "GVFS/GVFS.Common/Physical/RegistryUtils.cs",
                "GVFS/GVFS.Common/Physical/RepoMetadata.cs",
                "GVFS/GVFS.Common/PrefetchPacks/PrefetchPacksDeserializer.cs",
                "GVFS/GVFS.Common/ProcessHelper.cs",
                "GVFS/GVFS.Common/ProcessPool.cs",
                "GVFS/GVFS.Common/ProcessResult.cs",
                "GVFS/GVFS.Common/Properties/AssemblyInfo.cs",
                "GVFS/GVFS.Common/ReliableBackgroundOperations.cs",
                "GVFS/GVFS.Common/RetryWrapper.cs",
                "GVFS/GVFS.Common/RetryableException.cs",
                "GVFS/GVFS.Common/ReturnCode.cs",
                "GVFS/GVFS.Common/TaskExtensions.cs",
                "GVFS/GVFS.Common/Tracing/ConsoleEventListener.cs",
                "GVFS/GVFS.Common/Tracing/EventMetadata.cs",
                "GVFS/GVFS.Common/Tracing/ITracer.cs",
                "GVFS/GVFS.Common/Tracing/InProcEventListener.cs",
                "GVFS/GVFS.Common/Tracing/JsonEtwTracer.cs",
                "GVFS/GVFS.Common/Tracing/Keywords.cs",
                "GVFS/GVFS.Common/Tracing/LogFileEventListener.cs",
                "GVFS/GVFS.Common/WindowsProcessJob.cs",
                "GVFS/GVFS.Common/packages.config",
                "GVFS/GVFS.FunctionalTests/Category/CategoryConstants.cs",
                "GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs",
                "GVFS/GVFS.FunctionalTests/FileSystemRunners/CmdRunner.cs",
                "GVFS/GVFS.FunctionalTests/FileSystemRunners/FileSystemRunner.cs",
                "GVFS/GVFS.FunctionalTests/FileSystemRunners/PowerShellRunner.cs",
                "GVFS/GVFS.FunctionalTests/FileSystemRunners/ShellRunner.cs",
                "GVFS/GVFS.FunctionalTests/FileSystemRunners/SystemIORunner.cs",
                "GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj",
                "GVFS/GVFS.FunctionalTests/Program.cs",
                "GVFS/GVFS.FunctionalTests/Properties/AssemblyInfo.cs",
                "GVFS/GVFS.FunctionalTests/Properties/Settings.Designer.cs",
                "GVFS/GVFS.FunctionalTests/Properties/Settings.settings",
                "GVFS/GVFS.FunctionalTests/Should/FileSystemShouldExtensions.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/DiagnoseTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitCommandsTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests_2.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFolderTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/TestsWithEnlistmentPerFixture.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorkingDirectoryTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/CaseOnlyFolderRenameTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PersistedSparseExcludeTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PersistedWorkingDirectoryTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PrefetchVerbTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/RebaseTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/TestsWithEnlistmentPerTestCase.cs",
                "GVFS/GVFS.FunctionalTests/Tests/FastFetchTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitMoveRenameTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitObjectManipulationTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitReadAndGitLockTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/LongRunningSetup.cs",
                "GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/MultithreadedReadWriteTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/TestsWithLongRunningEnlistment.cs",
                "GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/WorkingDirectoryTests.cs",
                "GVFS/GVFS.FunctionalTests/Tests/PrintTestCaseStats.cs",
                "GVFS/GVFS.FunctionalTests/Tools/ControlGitRepo.cs",
                "GVFS/GVFS.FunctionalTests/Tools/GVFSFunctionalTestEnlistment.cs",
                "GVFS/GVFS.FunctionalTests/Tools/GVFSProcess.cs",
                "GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs",
                "GVFS/GVFS.FunctionalTests/Tools/GitProcess.cs",
                "GVFS/GVFS.FunctionalTests/Tools/NativeMethods.cs",
                "GVFS/GVFS.FunctionalTests/Tools/ProcessHelper.cs",
                "GVFS/GVFS.FunctionalTests/Tools/ProcessResult.cs",
                "GVFS/GVFS.FunctionalTests/Tools/TestConstants.cs",
                "GVFS/GVFS.FunctionalTests/app.config",
                "GVFS/GVFS.FunctionalTests/packages.config",
                "GVFS/GVFS.Hooks/App.config",
                "GVFS/GVFS.Hooks/GVFS.Hooks.csproj",
                "GVFS/GVFS.Hooks/KnownGitCommands.cs",
                "GVFS/GVFS.Hooks/Program.cs",
                "GVFS/GVFS.Hooks/Properties/AssemblyInfo.cs",
                "GVFS/GVFS.Hooks/packages.config",
                "GVFS/GVFS.Mount/GVFS.Mount.csproj",
                "GVFS/GVFS.Mount/InProcessMount.cs",
                "GVFS/GVFS.Mount/MountAbortedException.cs",
                "GVFS/GVFS.Mount/MountVerb.cs",
                "GVFS/GVFS.Mount/Program.cs",
                "GVFS/GVFS.Mount/Properties/AssemblyInfo.cs",
                "GVFS/GVFS.Mount/packages.config",
                "GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj",
                "GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj.filters",
                "GVFS/GVFS.NativeTests/ReadMe.txt",
                "GVFS/GVFS.NativeTests/include/NtFunctions.h",
                "GVFS/GVFS.NativeTests/include/SafeHandle.h",
                "GVFS/GVFS.NativeTests/include/SafeOverlapped.h",
                "GVFS/GVFS.NativeTests/include/Should.h",
                "GVFS/GVFS.NativeTests/include/TestException.h",
                "GVFS/GVFS.NativeTests/include/TestHelpers.h",
                "GVFS/GVFS.NativeTests/include/TestVerifiers.h",
                "GVFS/GVFS.NativeTests/include/gvflt.h",
                "GVFS/GVFS.NativeTests/include/gvlib_internal.h",
                "GVFS/GVFS.NativeTests/include/stdafx.h",
                "GVFS/GVFS.NativeTests/include/targetver.h",
                "GVFS/GVFS.NativeTests/interface/GVFlt_BugRegressionTest.h",
                "GVFS/GVFS.NativeTests/interface/GVFlt_DeleteFileTest.h",
                "GVFS/GVFS.NativeTests/interface/GVFlt_DeleteFolderTest.h",
                "GVFS/GVFS.NativeTests/interface/GVFlt_DirEnumTest.h",
                "GVFS/GVFS.NativeTests/interface/GVFlt_FileAttributeTest.h",
                "GVFS/GVFS.NativeTests/interface/GVFlt_FileEATest.h",
                "GVFS/GVFS.NativeTests/interface/GVFlt_FileOperationTest.h",
                "GVFS/GVFS.NativeTests/interface/GVFlt_MoveFileTest.h",
                "GVFS/GVFS.NativeTests/interface/GVFlt_MoveFolderTest.h",
                "GVFS/GVFS.NativeTests/interface/GVFlt_MultiThreadsTest.h",
                "GVFS/GVFS.NativeTests/interface/GVFlt_SetLinkTest.h",
                "GVFS/GVFS.NativeTests/interface/NtQueryDirectoryFileTests.h",
                "GVFS/GVFS.NativeTests/interface/PlaceholderUtils.h",
                "GVFS/GVFS.NativeTests/interface/ReadAndWriteTests.h",
                "GVFS/GVFS.NativeTests/interface/TrailingSlashTests.h",
                "GVFS/GVFS.NativeTests/source/GVFlt_BugRegressionTest.cpp",
                "GVFS/GVFS.NativeTests/source/GVFlt_DeleteFileTest.cpp",
                "GVFS/GVFS.NativeTests/source/GVFlt_DeleteFolderTest.cpp",
                "GVFS/GVFS.NativeTests/source/GVFlt_DirEnumTest.cpp",
                "GVFS/GVFS.NativeTests/source/GVFlt_FileAttributeTest.cpp",
                "GVFS/GVFS.NativeTests/source/GVFlt_FileEATest.cpp",
                "GVFS/GVFS.NativeTests/source/GVFlt_FileOperationTest.cpp",
                "GVFS/GVFS.NativeTests/source/GVFlt_MoveFileTest.cpp",
                "GVFS/GVFS.NativeTests/source/GVFlt_MoveFolderTest.cpp",
                "GVFS/GVFS.NativeTests/source/GVFlt_MultiThreadTest.cpp",
                "GVFS/GVFS.NativeTests/source/GVFlt_SetLinkTest.cpp",
                "GVFS/GVFS.NativeTests/source/NtFunctions.cpp",
                "GVFS/GVFS.NativeTests/source/NtQueryDirectoryFileTests.cpp",
                "GVFS/GVFS.NativeTests/source/PlaceholderUtils.cpp",
                "GVFS/GVFS.NativeTests/source/ReadAndWriteTests.cpp",
                "GVFS/GVFS.NativeTests/source/TrailingSlashTests.cpp",
                "GVFS/GVFS.NativeTests/source/dllmain.cpp",
                "GVFS/GVFS.NativeTests/source/stdafx.cpp",
                "GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj",
                "GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj.filters",
                "GVFS/GVFS.ReadObjectHook/Version.rc",
                "GVFS/GVFS.ReadObjectHook/main.cpp",
                "GVFS/GVFS.ReadObjectHook/resource.h",
                "GVFS/GVFS.ReadObjectHook/stdafx.cpp",
                "GVFS/GVFS.ReadObjectHook/stdafx.h",
                "GVFS/GVFS.ReadObjectHook/targetver.h",
                "GVFS/GVFS.Service/GVFS.Service.csproj",
                "GVFS/GVFS.Service/GvfsService.cs",
                "GVFS/GVFS.Service/GvfsServiceInstaller.cs",
                "GVFS/GVFS.Service/Program.cs",
                "GVFS/GVFS.Service/Properties/AssemblyInfo.cs",
                "GVFS/GVFS.Service/packages.config",
                "GVFS/GVFS.Tests/GVFS.Tests.csproj",
                "GVFS/GVFS.Tests/NUnitRunner.cs",
                "GVFS/GVFS.Tests/Properties/AssemblyInfo.cs",
                "GVFS/GVFS.Tests/Should/EnumerableShouldExtensions.cs",
                "GVFS/GVFS.Tests/Should/StringExtensions.cs",
                "GVFS/GVFS.Tests/Should/StringShouldExtensions.cs",
                "GVFS/GVFS.Tests/Should/ValueShouldExtensions.cs",
                "GVFS/GVFS.Tests/packages.config",
                "GVFS/GVFS.UnitTests/App.config",
                "GVFS/GVFS.UnitTests/Category/CategoryContants.cs",
                "GVFS/GVFS.UnitTests/Common/GitHelperTests.cs",
                "GVFS/GVFS.UnitTests/Common/GitPathConverterTests.cs",
                "GVFS/GVFS.UnitTests/Common/GitVersionTests.cs",
                "GVFS/GVFS.UnitTests/Common/JsonEtwTracerTests.cs",
                "GVFS/GVFS.UnitTests/Common/ProcessHelperTests.cs",
                "GVFS/GVFS.UnitTests/Common/RetryWrapperTests.cs",
                "GVFS/GVFS.UnitTests/Common/SHA1UtilTests.cs",
                "GVFS/GVFS.UnitTests/Data/backward.txt",
                "GVFS/GVFS.UnitTests/Data/forward.txt",
                "GVFS/GVFS.UnitTests/Data/index_v2",
                "GVFS/GVFS.UnitTests/Data/index_v3",
                "GVFS/GVFS.UnitTests/Data/index_v4",
                "GVFS/GVFS.UnitTests/FastFetch/BatchObjectDownloadJobTests.cs",
                "GVFS/GVFS.UnitTests/FastFetch/DiffHelperTests.cs",
                "GVFS/GVFS.UnitTests/FastFetch/FastFetchTracingTests.cs",
                "GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj",
                "GVFS/GVFS.UnitTests/GVFlt/DotGit/ExcludeFileTests.cs",
                "GVFS/GVFS.UnitTests/GVFlt/DotGit/GitConfigFileUtilsTests.cs",
                "GVFS/GVFS.UnitTests/GVFlt/GVFltActiveEnumerationTests.cs",
                "GVFS/GVFS.UnitTests/GVFlt/GVFltCallbacksTests.cs",
                "GVFS/GVFS.UnitTests/GVFlt/PathUtilTests.cs",
                "GVFS/GVFS.UnitTests/GVFlt/Physical/FileSerializerTests.cs",
                "GVFS/GVFS.UnitTests/Mock/Common/MockEnlistment.cs",
                "GVFS/GVFS.UnitTests/Mock/Common/MockPhysicalGitObjects.cs",
                "GVFS/GVFS.UnitTests/Mock/Common/MockTracer.cs",
                "GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MassiveMockFileSystem.cs",
                "GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockDirectory.cs",
                "GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockFile.cs",
                "GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockFileSystem.cs",
                "GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockSafeHandle.cs",
                "GVFS/GVFS.UnitTests/Mock/Physical/Git/MockBatchHttpGitObjects.cs",
                "GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGVFSGitObjects.cs",
                "GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGitIndex.cs",
                "GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGitRepo.cs",
                "GVFS/GVFS.UnitTests/Mock/Physical/Git/MockHttpGitObjects.cs",
                "GVFS/GVFS.UnitTests/Mock/Physical/ReusableMemoryStream.cs",
                "GVFS/GVFS.UnitTests/Physical/Git/GitCatFileBatchProcessTests.cs",
                "GVFS/GVFS.UnitTests/Physical/Git/PhysicalGitObjectsTests.cs",
                "GVFS/GVFS.UnitTests/Prefetch/PrefetchPacksDeserializerTests.cs",
                "GVFS/GVFS.UnitTests/Program.cs",
                "GVFS/GVFS.UnitTests/Properties/AssemblyInfo.cs",
                "GVFS/GVFS.UnitTests/Should/StringShouldExtensions.cs",
                "GVFS/GVFS.UnitTests/Virtual/CommonRepoSetup.cs",
                "GVFS/GVFS.UnitTests/Virtual/DotGit/GitIndexTests.cs",
                "GVFS/GVFS.UnitTests/Virtual/TestsWithCommonRepo.cs",
                "GVFS/GVFS.UnitTests/packages.config",
                "GVFS/GVFS/App.config",
                "GVFS/GVFS/CommandLine/CloneHelper.cs",
                "GVFS/GVFS/CommandLine/CloneVerb.cs",
                "GVFS/GVFS/CommandLine/DiagnoseVerb.cs",
                "GVFS/GVFS/CommandLine/GVFSVerb.cs",
                "GVFS/GVFS/CommandLine/LogVerb.cs",
                "GVFS/GVFS/CommandLine/MountVerb.cs",
                "GVFS/GVFS/CommandLine/PrefetchHelper.cs",
                "GVFS/GVFS/CommandLine/PrefetchVerb.cs",
                "GVFS/GVFS/CommandLine/StatusVerb.cs",
                "GVFS/GVFS/CommandLine/UnmountVerb.cs",
                "GVFS/GVFS/GVFS.csproj",
                "GVFS/GVFS/GitVirtualFileSystem.ico",
                "GVFS/GVFS/Program.cs",
                "GVFS/GVFS/Properties/AssemblyInfo.cs",
                "GVFS/GVFS/Setup.iss",
                "GVFS/GVFS/packages.config",
                "GitCommandsTests/CheckoutNewBranchFromStartingPointTest/test1.txt",
                "GitCommandsTests/CheckoutNewBranchFromStartingPointTest/test2.txt",
                "GitCommandsTests/CheckoutOrhpanBranchFromStartingPointTest/test1.txt",
                "GitCommandsTests/CheckoutOrhpanBranchFromStartingPointTest/test2.txt",
                "GitCommandsTests/DeleteFileTests/1/#test",
                "GitCommandsTests/DeleteFileTests/2/$test",
                "GitCommandsTests/DeleteFileTests/3/)",
                "GitCommandsTests/DeleteFileTests/4/+.test",
                "GitCommandsTests/DeleteFileTests/5/-.test",
                "GitCommandsTests/RenameFileTests/1/#test",
                "GitCommandsTests/RenameFileTests/2/$test",
                "GitCommandsTests/RenameFileTests/3/)",
                "GitCommandsTests/RenameFileTests/4/+.test",
                "GitCommandsTests/RenameFileTests/5/-.test",
                "Protocol.md",
                "Readme.md",
                "Scripts/CreateCommonAssemblyVersion.bat",
                "Scripts/CreateCommonCliAssemblyVersion.bat",
                "Scripts/CreateCommonVersionHeader.bat",
                "Scripts/RunFunctionalTests.bat",
                "Scripts/RunUnitTests.bat",
                "Settings.StyleCop",
            };
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchVerbTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.IO;
using System.Linq;
using System.Threading;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixture]
    [NonParallelizable]
    public class PrefetchVerbTests : TestsWithEnlistmentPerFixture
    {
        private const string PrefetchCommitsAndTreesLock = "prefetch-commits-trees.lock";
        private const string LsTreeTypeInPathBranchName = "FunctionalTests/20201014_LsTreeTypeInPath";

        // on case-insensitive filesystems, test case-blind matching in
        // folder lists using "gvfs/" instead of "GVFS/"
        private static readonly string PrefetchGVFSFolder = FileSystemHelpers.CaseSensitiveFileSystem ? "GVFS" : "gvfs";
        private static readonly string PrefetchGVFSFolderPath = PrefetchGVFSFolder + "/";
        private static readonly string[] PrefetchFolderList = new string[]
        {
            "# A comment",
            " ",
            PrefetchGVFSFolderPath, // "GVFS/" or "gvfs/"
            PrefetchGVFSFolderPath + PrefetchGVFSFolder, // "GVFS/GVFS" or "gvfs/gvfs"
            PrefetchGVFSFolderPath,
        };

        private FileSystemRunner fileSystem;

        public PrefetchVerbTests()
        {
            this.fileSystem = new SystemIORunner();
        }

        [TestCase, Order(1)]
        public void PrefetchAllMustBeExplicit()
        {
            this.Enlistment.Prefetch(string.Empty, failOnError: false).ShouldContain("Did you mean to fetch all blobs?");
        }

        [TestCase, Order(2)]
        public void PrefetchSpecificFiles()
        {
            this.ExpectBlobCount(this.Enlistment.Prefetch($"--files {Path.Combine("GVFS", "GVFS", "Program.cs")}"), 1);
            this.ExpectBlobCount(this.Enlistment.Prefetch($"--files {Path.Combine("GVFS", "GVFS", "Program.cs")};{Path.Combine("GVFS", "GVFS.FunctionalTests", "GVFS.FunctionalTests.csproj")}"), 2);
        }

        [TestCase, Order(3)]
        public void PrefetchByFileExtension()
        {
            this.ExpectBlobCount(this.Enlistment.Prefetch("--files *.cs"), 199);
            this.ExpectBlobCount(this.Enlistment.Prefetch("--files *.cs;*.csproj"), 208);
        }

        [TestCase, Order(4)]
        public void PrefetchByFileExtensionWithHydrate()
        {
            int expectedCount = 3;
            string output = this.Enlistment.Prefetch("--files *.md --hydrate");
            this.ExpectBlobCount(output, expectedCount);
            output.ShouldContain("Hydrated files:   " + expectedCount);
        }

        [TestCase, Order(5)]
        public void PrefetchByFilesWithHydrateWhoseObjectsAreAlreadyDownloaded()
        {
            int expectedCount = 2;
            string output = this.Enlistment.Prefetch(
                $"--files {Path.Combine("GVFS", "GVFS", "Program.cs")};{Path.Combine("GVFS", "GVFS.FunctionalTests", "GVFS.FunctionalTests.csproj")} --hydrate");
            this.ExpectBlobCount(output, expectedCount);
            output.ShouldContain("Hydrated files:   " + expectedCount);
            output.ShouldContain("Downloaded:       0");
        }

        [TestCase, Order(6)]
        public void PrefetchFolders()
        {
            this.ExpectBlobCount(this.Enlistment.Prefetch($"--folders {Path.Combine("GVFS", "GVFS")}"), 17);
            this.ExpectBlobCount(this.Enlistment.Prefetch($"--folders {Path.Combine("GVFS", "GVFS")};{Path.Combine("GVFS", "GVFS.FunctionalTests")}"), 65);
        }

        [TestCase, Order(7)]
        public void PrefetchIsAllowedToDoNothing()
        {
            this.ExpectBlobCount(this.Enlistment.Prefetch("--files nonexistent.txt"), 0);
            this.ExpectBlobCount(this.Enlistment.Prefetch("--folders nonexistent_folder"), 0);
        }

        [TestCase, Order(8)]
        public void PrefetchFolderListFromFile()
        {
            string tempFilePath = Path.Combine(Path.GetTempPath(), "temp.file");
            File.WriteAllLines(tempFilePath, PrefetchFolderList);
            this.ExpectBlobCount(this.Enlistment.Prefetch("--folders-list \"" + tempFilePath + "\""), 279);
            File.Delete(tempFilePath);
        }

        [TestCase, Order(9)]
        public void PrefetchAll()
        {
            this.ExpectBlobCount(this.Enlistment.Prefetch("--files *"), 494);
            this.ExpectBlobCount(this.Enlistment.Prefetch($"--folders {Path.DirectorySeparatorChar}"), 494);
        }

        [TestCase, Order(10)]
        public void NoopPrefetch()
        {
            this.ExpectBlobCount(this.Enlistment.Prefetch("--files *"), 494);

            this.Enlistment.Prefetch("--files *").ShouldContain("Nothing new to prefetch.");
        }

        [TestCase, Order(11)]
        public void PrefetchCleansUpStalePrefetchLock()
        {
            this.Enlistment.Prefetch("--commits");
            this.PostFetchStepShouldComplete();
            string prefetchCommitsLockFile = Path.Combine(this.Enlistment.GetObjectRoot(this.fileSystem), "pack", PrefetchCommitsAndTreesLock);
            prefetchCommitsLockFile.ShouldNotExistOnDisk(this.fileSystem);
            this.fileSystem.WriteAllText(prefetchCommitsLockFile, this.Enlistment.EnlistmentRoot);
            prefetchCommitsLockFile.ShouldBeAFile(this.fileSystem);

            this.fileSystem
                .EnumerateDirectory(this.Enlistment.GetPackRoot(this.fileSystem))
                .Split()
                .Where(file => string.Equals(Path.GetExtension(file), ".keep", FileSystemHelpers.PathComparison))
                .Count()
                .ShouldEqual(1, "Incorrect number of .keep files in pack directory");

            this.Enlistment.Prefetch("--commits");
            this.PostFetchStepShouldComplete();
            prefetchCommitsLockFile.ShouldNotExistOnDisk(this.fileSystem);
        }

        [TestCase, Order(12)]
        public void PrefetchFilesFromFileListFile()
        {
            string tempFilePath = Path.Combine(Path.GetTempPath(), "temp.file");
            try
            {
                File.WriteAllLines(
                    tempFilePath,
                    new[]
                    {
                        Path.Combine("GVFS", "GVFS", "Program.cs"),
                        Path.Combine("GVFS", "GVFS.FunctionalTests", "GVFS.FunctionalTests.csproj")
                    });

                this.ExpectBlobCount(this.Enlistment.Prefetch($"--files-list \"{tempFilePath}\""), 2);
            }
            finally
            {
                File.Delete(tempFilePath);
            }
        }

        [TestCase, Order(13)]
        [Category(Categories.NeedsReactionInCI)]
        public void PrefetchFilesFromFileListStdIn()
        {
            // on case-insensitive filesystems, test case-blind matching
            // using "App.config" instead of "app.config"
            string input = string.Join(
                Environment.NewLine,
                new[]
                {
                    Path.Combine("GVFS", "GVFS", "packages.config"),
                    Path.Combine("GVFS", "GVFS.FunctionalTests", FileSystemHelpers.CaseSensitiveFileSystem ? "app.config" : "App.config")
                });

            this.ExpectBlobCount(this.Enlistment.Prefetch("--stdin-files-list", standardInput: input), 2);
        }

        [TestCase, Order(14)]
        [Category(Categories.NeedsReactionInCI)]
        public void PrefetchFolderListFromStdin()
        {
            string input = string.Join(Environment.NewLine, PrefetchFolderList);
            this.ExpectBlobCount(this.Enlistment.Prefetch("--stdin-folders-list", standardInput: input), 279);
        }

        public void PrefetchPathsWithLsTreeTypeInPath()
        {
            ProcessResult checkoutResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout " + LsTreeTypeInPathBranchName);

            this.ExpectBlobCount(this.Enlistment.Prefetch("--files *"), 496);
        }

        private void ExpectBlobCount(string output, int expectedCount)
        {
            output.ShouldContain("Matched blobs:    " + expectedCount);
        }

        private void PostFetchStepShouldComplete()
        {
            string objectDir = this.Enlistment.GetObjectRoot(this.fileSystem);
            string objectCacheLock = Path.Combine(objectDir, "git-maintenance-step.lock");

            // Wait first, to hopefully ensure the background thread has
            // started before we check for the lock file.
            do
            {
                Thread.Sleep(500);
            }
            while (this.fileSystem.FileExists(objectCacheLock));

            // A commit graph is not always generated, but if it is, then we want to ensure it is in a good state
            if (this.fileSystem.FileExists(Path.Combine(objectDir, "info", "commit-graphs", "commit-graph-chain")))
            {
                ProcessResult graphResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "commit-graph verify --shallow --object-dir=\"" + objectDir + "\"");
                graphResult.ExitCode.ShouldEqual(0);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchVerbWithoutSharedCacheTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.IO;
using System.Threading;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixture]
    [Category(Categories.ExtraCoverage)]
    public class PrefetchVerbWithoutSharedCacheTests : TestsWithEnlistmentPerFixture
    {
        private const string PrefetchPackPrefix = "prefetch";
        private const string TempPackFolder = "tempPacks";

        private FileSystemRunner fileSystem;

        // Set forcePerRepoObjectCache to true to avoid any of the tests inadvertently corrupting
        // the cache
        public PrefetchVerbWithoutSharedCacheTests()
            : base(forcePerRepoObjectCache: true, skipPrefetchDuringClone: true)
        {
            this.fileSystem = new SystemIORunner();
        }

        private string PackRoot
        {
            get
            {
                return this.Enlistment.GetPackRoot(this.fileSystem);
            }
        }

        private string TempPackRoot
        {
            get
            {
                return Path.Combine(this.PackRoot, TempPackFolder);
            }
        }

        [TestCase, Order(1)]
        public void PrefetchCommitsToEmptyCache()
        {
            this.Enlistment.Prefetch("--commits");
            this.PostFetchJobShouldComplete();

            // Verify prefetch pack(s) are in packs folder and have matching idx file
            string[] prefetchPacks = this.ReadPrefetchPackFileNames();
            this.AllPrefetchPacksShouldHaveIdx(prefetchPacks);

            // Verify tempPacks is empty
            this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems();
        }

        [TestCase, Order(2)]
        public void PrefetchBuildsIdxWhenMissingFromPrefetchPack()
        {
            string[] prefetchPacks = this.ReadPrefetchPackFileNames();
            prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack");

            string idxPath = Path.ChangeExtension(prefetchPacks[0], ".idx");
            idxPath.ShouldBeAFile(this.fileSystem);
            File.SetAttributes(idxPath, FileAttributes.Normal);
            this.fileSystem.DeleteFile(idxPath);
            idxPath.ShouldNotExistOnDisk(this.fileSystem);

            // Prefetch should rebuild the missing idx
            this.Enlistment.Prefetch("--commits");
            this.PostFetchJobShouldComplete();

            idxPath.ShouldBeAFile(this.fileSystem);

            // All of the original prefetch packs should still be present
            string[] newPrefetchPacks = this.ReadPrefetchPackFileNames();
            newPrefetchPacks.ShouldContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); });
            this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks);
            this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems();
        }

        [TestCase, Order(3)]
        public void PrefetchCleansUpBadPrefetchPack()
        {
            string[] prefetchPacks = this.ReadPrefetchPackFileNames();
            long mostRecentPackTimestamp = this.GetMostRecentPackTimestamp(prefetchPacks);

            // Create a bad pack that is newer than the most recent pack
            string badContents = "BADPACK";
            string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{mostRecentPackTimestamp + 1}-{Guid.NewGuid().ToString("N")}.pack");
            this.fileSystem.WriteAllText(badPackPath, badContents);
            badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents);

            // Prefetch should delete the bad pack
            this.Enlistment.Prefetch("--commits");
            this.PostFetchJobShouldComplete();

            badPackPath.ShouldNotExistOnDisk(this.fileSystem);

            // All of the original prefetch packs should still be present
            string[] newPrefetchPacks = this.ReadPrefetchPackFileNames();
            newPrefetchPacks.ShouldContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); });
            this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks);
            this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems();
        }

        [TestCase, Order(4)]
        public void PrefetchCleansUpOldPrefetchPack()
        {
            this.Enlistment.UnmountGVFS();

            string[] prefetchPacks = this.ReadPrefetchPackFileNames();
            long oldestPackTimestamp = this.GetOldestPackTimestamp(prefetchPacks);

            // Create a bad pack that is older than the oldest pack
            string badContents = "BADPACK";
            string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{oldestPackTimestamp - 1}-{Guid.NewGuid().ToString("N")}.pack");
            this.fileSystem.WriteAllText(badPackPath, badContents);
            badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents);

            // Prefetch should delete the bad pack and all packs after it
            this.Enlistment.Prefetch("--commits");
            this.PostFetchJobShouldComplete();

            badPackPath.ShouldNotExistOnDisk(this.fileSystem);
            foreach (string packPath in prefetchPacks)
            {
                string idxPath = Path.ChangeExtension(packPath, ".idx");
                badPackPath.ShouldNotExistOnDisk(this.fileSystem);
                idxPath.ShouldNotExistOnDisk(this.fileSystem);
            }

            string[] newPrefetchPacks = this.ReadPrefetchPackFileNames();
            this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks);
            this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems();
        }

        [TestCase, Order(5)]
        public void PrefetchFailsWhenItCannotRemoveABadPrefetchPack()
        {
            this.Enlistment.UnmountGVFS();

            string[] prefetchPacks = this.ReadPrefetchPackFileNames();
            long mostRecentPackTimestamp = this.GetMostRecentPackTimestamp(prefetchPacks);

            // Create a bad pack that is newer than the most recent pack
            string badContents = "BADPACK";
            string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{mostRecentPackTimestamp + 1}-{Guid.NewGuid().ToString("N")}.pack");
            this.fileSystem.WriteAllText(badPackPath, badContents);
            badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents);

            // Open a handle to the bad pack that will prevent prefetch from being able to delete it
            using (FileStream stream = new FileStream(badPackPath, FileMode.Open, FileAccess.Read, FileShare.None))
            {
                string output = this.Enlistment.Prefetch("--commits", failOnError: false);
                output.ShouldContain($"Unable to delete {badPackPath}");
            }

            // After handle is closed prefetch should succeed
            this.Enlistment.Prefetch("--commits");
            this.PostFetchJobShouldComplete();

            badPackPath.ShouldNotExistOnDisk(this.fileSystem);

            string[] newPrefetchPacks = this.ReadPrefetchPackFileNames();
            newPrefetchPacks.ShouldContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); });
            this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks);
            this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems();
        }

        [TestCase, Order(6)]
        public void PrefetchFailsWhenItCannotRemoveAPrefetchPackNewerThanBadPrefetchPack()
        {
            this.Enlistment.UnmountGVFS();

            string[] prefetchPacks = this.ReadPrefetchPackFileNames();
            long oldestPackTimestamp = this.GetOldestPackTimestamp(prefetchPacks);

            // Create a bad pack that is older than the oldest pack
            string badContents = "BADPACK";
            string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{oldestPackTimestamp - 1}-{Guid.NewGuid().ToString("N")}.pack");
            this.fileSystem.WriteAllText(badPackPath, badContents);
            badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents);

            // Open a handle to a good pack that is newer than the bad pack, which will prevent prefetch from being able to delete it
            using (FileStream stream = new FileStream(prefetchPacks[0], FileMode.Open, FileAccess.Read, FileShare.None))
            {
                string output = this.Enlistment.Prefetch("--commits", failOnError: false);
                output.ShouldContain($"Unable to delete {prefetchPacks[0]}");
            }

            // After handle is closed prefetch should succeed
            this.Enlistment.Prefetch("--commits");
            this.PostFetchJobShouldComplete();

            // The bad pack and all packs newer than it should not be on disk
            badPackPath.ShouldNotExistOnDisk(this.fileSystem);

            string[] newPrefetchPacks = this.ReadPrefetchPackFileNames();
            newPrefetchPacks.ShouldNotContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); });
            this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks);
            this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems();
        }

        [TestCase, Order(7)]
        public void PrefetchFailsWhenItCannotRemoveAPrefetchIdxNewerThanBadPrefetchPack()
        {
            this.Enlistment.UnmountGVFS();

            string[] prefetchPacks = this.ReadPrefetchPackFileNames();
            long oldestPackTimestamp = this.GetOldestPackTimestamp(prefetchPacks);

            // Create a bad pack that is older than the oldest pack
            string badContents = "BADPACK";
            string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{oldestPackTimestamp - 1}-{Guid.NewGuid().ToString("N")}.pack");
            this.fileSystem.WriteAllText(badPackPath, badContents);
            badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents);

            string newerIdxPath = Path.ChangeExtension(prefetchPacks[0], ".idx");
            newerIdxPath.ShouldBeAFile(this.fileSystem);

            // Open a handle to a good idx that is newer than the bad pack, which will prevent prefetch from being able to delete it
            using (FileStream stream = new FileStream(newerIdxPath, FileMode.Open, FileAccess.Read, FileShare.None))
            {
                string output = this.Enlistment.Prefetch("--commits", failOnError: false);
                output.ShouldContain($"Unable to delete {newerIdxPath}");
            }

            // After handle is closed prefetch should succeed
            this.Enlistment.Prefetch("--commits");
            this.PostFetchJobShouldComplete();

            // The bad pack and all packs newer than it should not be on disk
            badPackPath.ShouldNotExistOnDisk(this.fileSystem);
            newerIdxPath.ShouldNotExistOnDisk(this.fileSystem);

            string[] newPrefetchPacks = this.ReadPrefetchPackFileNames();
            newPrefetchPacks.ShouldNotContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); });
            this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks);
            this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems();
        }

        [TestCase, Order(8)]
        public void PrefetchCleansUpStaleTempPrefetchPacks()
        {
            this.Enlistment.UnmountGVFS();

            // Create stale packs and idxs  in the temp folder
            string stalePackContents = "StalePack";
            string stalePackPath = Path.Combine(this.TempPackRoot, $"{PrefetchPackPrefix}-123456-{Guid.NewGuid().ToString("N")}.pack");
            this.fileSystem.WriteAllText(stalePackPath, stalePackContents);
            stalePackPath.ShouldBeAFile(this.fileSystem).WithContents(stalePackContents);

            string staleIdxContents = "StaleIdx";
            string staleIdxPath = Path.ChangeExtension(stalePackPath, ".idx");
            this.fileSystem.WriteAllText(staleIdxPath, staleIdxContents);
            staleIdxPath.ShouldBeAFile(this.fileSystem).WithContents(staleIdxContents);

            string stalePackPath2 = Path.Combine(this.TempPackRoot, $"{PrefetchPackPrefix}-123457-{Guid.NewGuid().ToString("N")}.pack");
            this.fileSystem.WriteAllText(stalePackPath2, stalePackContents);
            stalePackPath2.ShouldBeAFile(this.fileSystem).WithContents(stalePackContents);

            string stalePack2TempIdx = Path.ChangeExtension(stalePackPath2, ".tempidx");
            this.fileSystem.WriteAllText(stalePack2TempIdx, staleIdxContents);
            stalePack2TempIdx.ShouldBeAFile(this.fileSystem).WithContents(staleIdxContents);

            // Create other unrelated file in the temp folder
            string otherFileContents = "Test file, don't delete me!";
            string otherFilePath = Path.Combine(this.TempPackRoot, "ReadmeAndDontDeleteMe.txt");
            this.fileSystem.WriteAllText(otherFilePath, otherFileContents);
            otherFilePath.ShouldBeAFile(this.fileSystem).WithContents(otherFileContents);

            this.Enlistment.Prefetch("--commits");
            this.PostFetchJobShouldComplete();

            // Validate stale prefetch packs are cleaned up
            Directory.GetFiles(this.TempPackRoot, $"{PrefetchPackPrefix}*.pack").ShouldBeEmpty("There should be no .pack files in the tempPack folder");
            Directory.GetFiles(this.TempPackRoot, $"{PrefetchPackPrefix}*.idx").ShouldBeEmpty("There should be no .idx files in the tempPack folder");
            Directory.GetFiles(this.TempPackRoot, $"{PrefetchPackPrefix}*.tempidx").ShouldBeEmpty("There should be no .tempidx files in the tempPack folder");

            // Validate other files are not impacted
            otherFilePath.ShouldBeAFile(this.fileSystem).WithContents(otherFileContents);
        }

        [TestCase, Order(9)]
        public void PrefetchCleansUpOphanedLockFiles()
        {
            // the commit-graph write happens only when the prefetch downloads at least one pack

            string graphPath = Path.Combine(this.Enlistment.GetObjectRoot(this.fileSystem), "info", "commit-graphs", "commit-graph-chain");
            string graphLockPath = graphPath + ".lock";

            this.fileSystem.CreateEmptyFile(graphLockPath);

            // Unmount so we can delete the files.
            this.Enlistment.UnmountGVFS();

            // Force deleting the prefetch packs to make the prefetch non-trivial.
            this.fileSystem.DeleteDirectory(this.PackRoot);
            this.fileSystem.CreateDirectory(this.PackRoot);

            // Re-mount so the post-fetch job runs
            this.Enlistment.MountGVFS();

            this.Enlistment.Prefetch("--commits");
            this.PostFetchJobShouldComplete();

            this.fileSystem.FileExists(graphLockPath).ShouldBeFalse(nameof(graphLockPath));
            this.fileSystem.FileExists(graphPath).ShouldBeTrue(nameof(graphPath));
        }

        private void PackShouldHaveIdxFile(string pathPath)
        {
            string idxPath = Path.ChangeExtension(pathPath, ".idx");
            idxPath.ShouldBeAFile(this.fileSystem).WithContents().Length.ShouldBeAtLeast(1, $"{idxPath} is unexepectedly empty");
        }

        private void AllPrefetchPacksShouldHaveIdx(string[] prefetchPacks)
        {
            prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack");

            foreach (string prefetchPack in prefetchPacks)
            {
                this.PackShouldHaveIdxFile(prefetchPack);
            }
        }

        private string[] ReadPrefetchPackFileNames()
        {
            return Directory.GetFiles(this.PackRoot, $"{PrefetchPackPrefix}*.pack");
        }

        private long GetTimestamp(string preFetchPackName)
        {
            string filename = Path.GetFileName(preFetchPackName);
            filename.StartsWith(PrefetchPackPrefix).ShouldBeTrue($"'{preFetchPackName}' does not start with '{PrefetchPackPrefix}'");

            string[] parts = filename.Split('-');
            long parsed;

            parts.Length.ShouldBeAtLeast(1, $"'{preFetchPackName}' has less parts ({parts.Length}) than expected (1)");
            long.TryParse(parts[1], out parsed).ShouldBeTrue($"Failed to parse long from '{parts[1]}'");
            return parsed;
        }

        private long GetMostRecentPackTimestamp(string[] prefetchPacks)
        {
            prefetchPacks.Length.ShouldBeAtLeast(1, "prefetchPacks should have at least one item");

            long mostRecentPackTimestamp = -1;
            foreach (string prefetchPack in prefetchPacks)
            {
                long timestamp = this.GetTimestamp(prefetchPack);
                if (timestamp > mostRecentPackTimestamp)
                {
                    mostRecentPackTimestamp = timestamp;
                }
            }

            mostRecentPackTimestamp.ShouldBeAtLeast(1, "Failed to find the most recent pack");
            return mostRecentPackTimestamp;
        }

        private long GetOldestPackTimestamp(string[] prefetchPacks)
        {
            prefetchPacks.Length.ShouldBeAtLeast(1, "prefetchPacks should have at least one item");

            long oldestPackTimestamp = long.MaxValue;
            foreach (string prefetchPack in prefetchPacks)
            {
                long timestamp = this.GetTimestamp(prefetchPack);
                if (timestamp < oldestPackTimestamp)
                {
                    oldestPackTimestamp = timestamp;
                }
            }

            oldestPackTimestamp.ShouldBeAtMost(long.MaxValue - 1, "Failed to find the oldest pack");
            return oldestPackTimestamp;
        }

        private void PostFetchJobShouldComplete()
        {
            string objectDir = this.Enlistment.GetObjectRoot(this.fileSystem);
            string postFetchLock = Path.Combine(objectDir, "git-maintenance-step.lock");

            while (this.fileSystem.FileExists(postFetchLock))
            {
                Thread.Sleep(500);
            }

            ProcessResult graphResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "commit-graph verify --shallow --object-dir=\"" + objectDir + "\"");
            graphResult.ExitCode.ShouldEqual(0);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/SparseTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixture]
    public class SparseTests : TestsWithEnlistmentPerFixture
    {
        private static readonly string SparseAbortedMessage = Environment.NewLine + "Sparse was aborted.";
        private static readonly string[] NoSparseFolders = new string[0];
        private FileSystemRunner fileSystem = new SystemIORunner();
        private GVFSProcess gvfsProcess;
        private string mainSparseFolder = Path.Combine("GVFS", "GVFS");
        private string[] allDirectories;
        private string[] directoriesInMainFolder;

        [OneTimeSetUp]
        public void Setup()
        {
            this.gvfsProcess = new GVFSProcess(this.Enlistment);
            this.allDirectories = Directory.GetDirectories(this.Enlistment.RepoRoot, "*", SearchOption.AllDirectories)
                .Where(x => !x.Contains(Path.DirectorySeparatorChar + ".git" + Path.DirectorySeparatorChar))
                .ToArray();
            this.directoriesInMainFolder = Directory.GetDirectories(Path.Combine(this.Enlistment.RepoRoot, this.mainSparseFolder));
        }

        [TearDown]
        public void TearDown()
        {
            GitProcess.Invoke(this.Enlistment.RepoRoot, "clean -xdf");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "reset --hard");

            this.gvfsProcess.Sparse("--disable", shouldSucceed: true);

            // Remove all sparse folders should make all folders appear again
            string[] directories = Directory.GetDirectories(this.Enlistment.RepoRoot, "*", SearchOption.AllDirectories)
                .Where(x => !x.Contains(Path.DirectorySeparatorChar + ".git" + Path.DirectorySeparatorChar))
                .ToArray();
            directories.ShouldMatchInOrder(this.allDirectories);
            this.ValidateFoldersInSparseList(NoSparseFolders);
        }

        [TestCase, Order(1)]
        public void BasicTestsAddingSparseFolder()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);
            this.CheckMainSparseFolder();

            string secondPath = Path.Combine("GVFS", "GVFS.Common", "Physical");
            this.gvfsProcess.AddSparseFolders(secondPath);
            string folder = this.Enlistment.GetVirtualPathTo(secondPath);
            folder.ShouldBeADirectory(this.fileSystem);
            string file = this.Enlistment.GetVirtualPathTo("GVFS", "GVFS.Common", "Enlistment.cs");
            file.ShouldBeAFile(this.fileSystem);
        }

        [TestCase, Order(2)]
        public void AddAndRemoveVariousPathsTests()
        {
            // Paths to validate [0] = path to pass to sparse [1] = expected path saved
            string[][] paths = new[]
            {
                // AltDirectorySeparatorChar should get converted to DirectorySeparatorChar
                new[] { string.Join(Path.AltDirectorySeparatorChar.ToString(), "GVFS", "GVFS"), this.mainSparseFolder },

                // AltDirectorySeparatorChar should get trimmed
                new[] { $"{Path.AltDirectorySeparatorChar}{string.Join(Path.AltDirectorySeparatorChar.ToString(), "GVFS", "Test")}{Path.AltDirectorySeparatorChar}", Path.Combine("GVFS", "Test") },

                // DirectorySeparatorChar should get trimmed
                new[] { $"{Path.DirectorySeparatorChar}{Path.Combine("GVFS", "More")}{Path.DirectorySeparatorChar}", Path.Combine("GVFS", "More") },

                // spaces should get trimmed
                new[] { $" {string.Join(Path.AltDirectorySeparatorChar.ToString(), "GVFS", "Other")} ", Path.Combine("GVFS", "Other") },
            };

            foreach (string[] pathToValidate in paths)
            {
                this.ValidatePathAddsAndRemoves(pathToValidate[0], pathToValidate[1]);
            }
        }

        [TestCase, Order(3)]
        public void AddingParentDirectoryShouldMakeItRecursive()
        {
            string childPath = Path.Combine(this.mainSparseFolder, "CommandLine");
            this.gvfsProcess.AddSparseFolders(childPath);
            string[] directories = Directory.GetDirectories(Path.Combine(this.Enlistment.RepoRoot, this.mainSparseFolder));
            directories.Length.ShouldEqual(1);
            directories[0].ShouldEqual(Path.Combine(this.Enlistment.RepoRoot, childPath));
            this.ValidateFoldersInSparseList(childPath);

            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            directories = Directory.GetDirectories(Path.Combine(this.Enlistment.RepoRoot, this.mainSparseFolder));
            directories.Length.ShouldBeAtLeast(2);
            directories.ShouldMatchInOrder(this.directoriesInMainFolder);
            this.ValidateFoldersInSparseList(childPath, this.mainSparseFolder);
        }

        [TestCase, Order(4)]
        public void AddingSiblingFolderShouldNotMakeParentRecursive()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            // Add and remove sibling folder to main folder
            string siblingPath = Path.Combine("GVFS", "FastFetch");
            this.gvfsProcess.AddSparseFolders(siblingPath);
            string folder = this.Enlistment.GetVirtualPathTo(siblingPath);
            folder.ShouldBeADirectory(this.fileSystem);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, siblingPath);

            this.gvfsProcess.RemoveSparseFolders(siblingPath);
            folder.ShouldNotExistOnDisk(this.fileSystem);
            folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder);
            folder.ShouldBeADirectory(this.fileSystem);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);
        }

        [TestCase, Order(5)]
        public void AddingSubfolderShouldKeepParentRecursive()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            // Add subfolder of main folder and make sure it stays recursive
            string subFolder = Path.Combine(this.mainSparseFolder, "Properties");
            this.gvfsProcess.AddSparseFolders(subFolder);
            string folder = this.Enlistment.GetVirtualPathTo(subFolder);
            folder.ShouldBeADirectory(this.fileSystem);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, subFolder);

            folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder, "CommandLine");
            folder.ShouldBeADirectory(this.fileSystem);
        }

        [TestCase, Order(6)]
        public void CreatingFolderShouldAddToSparseListAndStartProjecting()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            string newFolderPath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS.Common");
            newFolderPath.ShouldNotExistOnDisk(this.fileSystem);
            Directory.CreateDirectory(newFolderPath);
            newFolderPath.ShouldBeADirectory(this.fileSystem);
            string[] fileSystemEntries = Directory.GetFileSystemEntries(newFolderPath);
            fileSystemEntries.Length.ShouldEqual(32);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, Path.Combine("GVFS", "GVFS.Common"));

            string projectedFolder = Path.Combine(newFolderPath, "Git");
            projectedFolder.ShouldBeADirectory(this.fileSystem);
            fileSystemEntries = Directory.GetFileSystemEntries(projectedFolder);
            fileSystemEntries.Length.ShouldEqual(13);

            string projectedFile = Path.Combine(newFolderPath, "ReturnCode.cs");
            projectedFile.ShouldBeAFile(this.fileSystem);
        }

        [TestCase, Order(7)]
        public void ReadFileThenChangingSparseFoldersShouldRemoveFileAndFolder()
        {
            string fileToRead = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "RunFunctionalTests.bat");
            this.fileSystem.ReadAllText(fileToRead);

            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts");
            folderPath.ShouldNotExistOnDisk(this.fileSystem);
            fileToRead.ShouldNotExistOnDisk(this.fileSystem);
        }

        [TestCase, Order(8)]
        public void CreateNewFileWillPreventRemoveSparseFolder()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder, "Scripts");
            this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts");

            string fileToCreate = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "newfile.txt");
            this.fileSystem.WriteAllText(fileToCreate, "New Contents");

            string output = this.gvfsProcess.RemoveSparseFolders(shouldPrune: false, shouldSucceed: false, folders: "Scripts");
            output.ShouldContain(SparseAbortedMessage);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts");

            string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts");
            folderPath.ShouldBeADirectory(this.fileSystem);
            string[] fileSystemEntries = Directory.GetFileSystemEntries(folderPath);
            fileSystemEntries.Length.ShouldEqual(6);
            fileToCreate.ShouldBeAFile(this.fileSystem);

            this.fileSystem.DeleteFile(fileToCreate);
        }

        [TestCase, Order(9)]
        public void ModifiedFileShouldNotAllowSparseFolderChange()
        {
            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "RunFunctionalTests.bat");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");

            string output = this.gvfsProcess.AddSparseFolders(shouldPrune: false, shouldSucceed: false, folders: this.mainSparseFolder);
            output.ShouldContain(SparseAbortedMessage);
            this.ValidateFoldersInSparseList(NoSparseFolders);
        }

        [TestCase, Order(10)]
        public void ModifiedFileAndCommitThenChangingSparseFoldersShouldKeepFileAndFolder()
        {
            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "RunFunctionalTests.bat");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "add .");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test");

            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts");
            folderPath.ShouldBeADirectory(this.fileSystem);
            modifiedPath.ShouldBeAFile(this.fileSystem);
        }

        [TestCase, Order(11)]
        public void DeleteFileAndCommitThenChangingSparseFoldersShouldKeepFolderAndFile()
        {
            string deletePath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS.Tests", "packages.config");
            this.fileSystem.DeleteFile(deletePath);
            GitProcess.Invoke(this.Enlistment.RepoRoot, "add .");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test");

            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            // File and folder should no longer be on disk because the file was deleted and the folder deleted becase it was empty
            string folderPath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS.Tests");
            folderPath.ShouldNotExistOnDisk(this.fileSystem);
            deletePath.ShouldNotExistOnDisk(this.fileSystem);

            // Folder and file should be on disk even though they are outside the sparse scope because the file is in the modified paths
            GitProcess.Invoke(this.Enlistment.RepoRoot, "checkout HEAD~1");
            folderPath.ShouldBeADirectory(this.fileSystem);
            deletePath.ShouldBeAFile(this.fileSystem);
        }

        [TestCase, Order(12)]
        public void CreateNewFileAndCommitThenRemoveSparseFolderShouldKeepFileAndFolder()
        {
            string folderToCreateFileIn = Path.Combine("GVFS", "GVFS.Hooks");
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder, folderToCreateFileIn);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, folderToCreateFileIn);

            string fileToCreate = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn, "newfile.txt");
            this.fileSystem.WriteAllText(fileToCreate, "New Contents");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "add .");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test");

            this.gvfsProcess.RemoveSparseFolders(folderToCreateFileIn);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            string folderPath = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn);
            folderPath.ShouldBeADirectory(this.fileSystem);
            string[] fileSystemEntries = Directory.GetFileSystemEntries(folderPath);
            fileSystemEntries.Length.ShouldEqual(1);
            fileToCreate.ShouldBeAFile(this.fileSystem);
        }

        [TestCase, Order(14)]
        public void ModifiedFileAndCommitThenChangingSparseFoldersWithPrune()
        {
            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "RunFunctionalTests.bat");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "add .");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test");

            this.gvfsProcess.AddSparseFolders(shouldPrune: true, folders: this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts");
            modifiedPath.ShouldNotExistOnDisk(this.fileSystem);
            folderPath.ShouldNotExistOnDisk(this.fileSystem);
        }

        [TestCase, Order(15)]
        public void PruneWithoutAnythingToPrune()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            this.gvfsProcess.PruneSparseNoFolders();
            this.ValidateFoldersInSparseList(this.mainSparseFolder);
        }

        [TestCase, Order(16)]
        public void PruneAfterChanges()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            string folderToCreateFileIn = Path.Combine("GVFS", "GVFS.Common");
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder, folderToCreateFileIn);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, folderToCreateFileIn);

            string fileToCreate = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn, "newfile.txt");
            this.fileSystem.WriteAllText(fileToCreate, "New Contents");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "add .");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test");

            this.gvfsProcess.RemoveSparseFolders(folderToCreateFileIn);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            this.gvfsProcess.PruneSparseNoFolders();
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            string folderPath = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn);
            folderPath.ShouldNotExistOnDisk(this.fileSystem);
            fileToCreate.ShouldNotExistOnDisk(this.fileSystem);
        }

        [TestCase, Order(17)]
        public void PruneWithRemove()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            string folderToCreateFileIn = Path.Combine("GVFS", "GVFS.Common");
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder, folderToCreateFileIn);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, folderToCreateFileIn);

            string fileToCreate = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn, "newfile.txt");
            this.fileSystem.WriteAllText(fileToCreate, "New Contents");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "add .");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test");

            this.gvfsProcess.RemoveSparseFolders(shouldPrune: true, folders: folderToCreateFileIn);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            string folderPath = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn);
            folderPath.ShouldNotExistOnDisk(this.fileSystem);
            fileToCreate.ShouldNotExistOnDisk(this.fileSystem);
        }

        [TestCase, Order(18)]
        public void ModifiedFileInSparseSetShouldAllowSparseFolderAdd()
        {
            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS", "Program.cs");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");

            string output = this.gvfsProcess.AddSparseFolders(folders: this.mainSparseFolder);
            output.ShouldContain("Running git status...Succeeded");
            this.ValidateFoldersInSparseList(this.mainSparseFolder);
        }

        [TestCase, Order(19)]
        public void ModifiedFileOutsideSparseSetShouldNotAllowSparseFolderAdd()
        {
            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS", "Program.cs");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");

            string output = this.gvfsProcess.AddSparseFolders(shouldPrune: false, shouldSucceed: false, folders: "Scripts");
            output.ShouldContain("Running git status...Failed", SparseAbortedMessage);
            this.ValidateFoldersInSparseList(NoSparseFolders);
        }

        [TestCase, Order(20)]
        public void ModifiedFileInSparseSetShouldAllowSparseFolderRemove()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder, "Scripts");
            this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts");

            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS", "Program.cs");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");

            string output = this.gvfsProcess.RemoveSparseFolders(folders: "Scripts");
            output.ShouldContain("Running git status...Succeeded");
            this.ValidateFoldersInSparseList(this.mainSparseFolder);
        }

        [TestCase, Order(21)]
        public void ModifiedFileOldSparseSetShouldNotAllowSparseFolderRemove()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder, "Scripts");
            this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts");

            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS", "Program.cs");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");

            string output = this.gvfsProcess.RemoveSparseFolders(shouldPrune: false, shouldSucceed: false, folders: this.mainSparseFolder);
            output.ShouldContain("Running git status...Failed", SparseAbortedMessage);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts");
        }

        [TestCase, Order(22)]
        public void ModifiedFileInSparseSetShouldAllowPrune()
        {
            string additionalSparseFolder = Path.Combine("GVFS", "GVFS.Tests", "Should");
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder, additionalSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, additionalSparseFolder);

            // Ensure that folderToCreateFileIn is on disk so that there's something to prune
            string folderToCreateFileIn = Path.Combine("GVFS", "GVFS.Common");
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder, additionalSparseFolder, folderToCreateFileIn);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, additionalSparseFolder, folderToCreateFileIn);

            string fileToCreate = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn, "newfile.txt");
            this.fileSystem.WriteAllText(fileToCreate, "New Contents");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "add .");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test");

            // Modify a file that's in the sparse set (recursively)
            string modifiedFileContents = "New Contents";
            string modifiedPath = this.Enlistment.GetVirtualPathTo("GVFS", "GVFS", "Program.cs");
            modifiedPath.ShouldBeAFile(this.fileSystem);
            this.fileSystem.WriteAllText(modifiedPath, modifiedFileContents);

            // Modify a file that is in the sparse set (via a non-recursive parent)
            string secondModifiedPath = this.Enlistment.GetVirtualPathTo("GVFS", "GVFS.Tests", "NUnitRunner.cs");
            secondModifiedPath.ShouldBeAFile(this.fileSystem);
            this.fileSystem.WriteAllText(secondModifiedPath, modifiedFileContents);

            string expecetedStatusOutput = GitProcess.Invoke(this.Enlistment.RepoRoot, "status --porcelain -uall");

            // Remove and prune folderToCreateFileIn
            string output = this.gvfsProcess.RemoveSparseFolders(shouldPrune: true, folders: folderToCreateFileIn);
            output.ShouldContain("Running git status...Succeeded");
            this.ValidateFoldersInSparseList(this.mainSparseFolder, additionalSparseFolder);

            // Confirm the prune succeeded
            string folderPath = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn);
            folderPath.ShouldNotExistOnDisk(this.fileSystem);
            fileToCreate.ShouldNotExistOnDisk(this.fileSystem);

            // Confirm the changes to the modified file are preserved and that status does not change
            modifiedPath.ShouldBeAFile(this.fileSystem).WithContents(modifiedFileContents);
            secondModifiedPath.ShouldBeAFile(this.fileSystem).WithContents(modifiedFileContents);
            string statusOutput = GitProcess.Invoke(this.Enlistment.RepoRoot, "status --porcelain -uall");
            statusOutput.ShouldEqual(expecetedStatusOutput, "Status output should not change.");
        }

        [TestCase, Order(23)]
        public void ModifiedFileInSparseSetShouldNotBeReportedWhenDirtyFilesOutsideSetPreventPrune()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            // Create a folder and file that will prevent pruning
            string newFolderName = "FolderOutsideSparse";
            this.fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(newFolderName));

            string newFileName = "newfile.txt";
            this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(newFolderName, newFileName), "New Contents");

            // Modify a file that's in the sparse set, it should not be reported as dirty
            string modifiedFileName = "Program.cs";
            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS", "Program.cs");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");

            string output = this.gvfsProcess.SparseCommand(
                addFolders: false,
                shouldPrune: true,
                shouldSucceed: false,
                folders: Array.Empty());
            output.ShouldContain("Running git status...Failed");
            output.ShouldContain($"{newFolderName}/{newFileName}");
            output.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: modifiedFileName);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);
        }

        [TestCase, Order(24)]
        public void GitStatusShouldNotRunWhenRemovingAllSparseFolders()
        {
            this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS", "Program.cs");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");

            string output = this.gvfsProcess.RemoveSparseFolders(folders: this.mainSparseFolder);
            output.ShouldNotContain(ignoreCase: false, unexpectedSubstrings: "Running git status");
            this.ValidateFoldersInSparseList(NoSparseFolders);
        }

        [TestCase, Order(25)]
        public void GitStatusShouldRunWithFilesChangedInSparseSet()
        {
            string pathToChangeFiles = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS", "CommandLine");
            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS", "Program.cs");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");
            this.fileSystem.WriteAllText(Path.Combine(pathToChangeFiles, "NewHelper.cs"), "New Contents");
            this.fileSystem.DeleteFile(Path.Combine(pathToChangeFiles, "CloneHelper.cs"));
            this.fileSystem.MoveFile(Path.Combine(pathToChangeFiles, "PrefetchHelper.cs"), Path.Combine(pathToChangeFiles, "PrefetchHelperRenamed.cs"));
            this.fileSystem.DeleteDirectory(Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS", "Properties"));
            GitProcess.Invoke(this.Enlistment.RepoRoot, "add .");

            this.fileSystem.WriteAllText(Path.Combine(pathToChangeFiles, "NewVerb.cs"), "New Contents");
            this.fileSystem.WriteAllText(Path.Combine(pathToChangeFiles, "CloneVerb.cs"), "New Contents");
            this.fileSystem.DeleteFile(Path.Combine(pathToChangeFiles, "DiagnoseVerb.cs"));
            this.fileSystem.MoveFile(Path.Combine(pathToChangeFiles, "LogVerb.cs"), Path.Combine(pathToChangeFiles, "LogVerbRenamed.cs"));

            string expecetedStatusOutput = GitProcess.Invoke(this.Enlistment.RepoRoot, "status --porcelain -uall");

            string output = this.gvfsProcess.AddSparseFolders(this.mainSparseFolder);
            output.ShouldContain("Running git status...Succeeded");
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            output = this.gvfsProcess.AddSparseFolders(folders: "Scripts");
            output.ShouldContain("Running git status...Succeeded");
            this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts");

            output = this.gvfsProcess.RemoveSparseFolders(folders: "Scripts");
            output.ShouldContain("Running git status...Succeeded");
            this.ValidateFoldersInSparseList(this.mainSparseFolder);

            output = this.gvfsProcess.RemoveSparseFolders(folders: this.mainSparseFolder);
            output.ShouldNotContain(ignoreCase: false, unexpectedSubstrings: "Running git status");
            this.ValidateFoldersInSparseList(NoSparseFolders);

            output = this.gvfsProcess.AddSparseFolders(shouldPrune: false, shouldSucceed: false, folders: "Scripts");
            output.ShouldContain("Running git status...Failed", SparseAbortedMessage);
            this.ValidateFoldersInSparseList(NoSparseFolders);

            string statusOutput = GitProcess.Invoke(this.Enlistment.RepoRoot, "status --porcelain -uall");
            statusOutput.ShouldEqual(expecetedStatusOutput, "Status output should not change.");
        }

        [TestCase, Order(26)]
        public void SetWithOtherOptionsFails()
        {
            string output = this.gvfsProcess.Sparse($"--set test --add test1", shouldSucceed: false);
            output.ShouldContain("--set not valid with other options.");
            output = this.gvfsProcess.Sparse($"--set test --remove test1", shouldSucceed: false);
            output.ShouldContain("--set not valid with other options.");
            output = this.gvfsProcess.Sparse($"--set test --file test1", shouldSucceed: false);
            output.ShouldContain("--set not valid with other options.");
        }

        [TestCase, Order(27)]
        public void FileWithOtherOptionsFails()
        {
            string output = this.gvfsProcess.Sparse($"--file test --add test1", shouldSucceed: false);
            output.ShouldContain("--file not valid with other options.");
            output = this.gvfsProcess.Sparse($"--file test --remove test1", shouldSucceed: false);
            output.ShouldContain("--file not valid with other options.");
            output = this.gvfsProcess.Sparse($"--file test --set test1", shouldSucceed: false);
            output.ShouldContain("--set not valid with other options.");
        }

        [TestCase, Order(28)]
        public void BasicSetOption()
        {
            this.gvfsProcess.Sparse($"--set {this.mainSparseFolder}", shouldSucceed: true);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);
            this.CheckMainSparseFolder();
        }

        [TestCase, Order(29)]
        public void SetAddsAndRemovesFolders()
        {
            this.gvfsProcess.Sparse($"--set {this.mainSparseFolder};Scripts;", shouldSucceed: true);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts");
            this.gvfsProcess.Sparse($"--set Scripts;GitCommandsTests", shouldSucceed: true);
            this.ValidateFoldersInSparseList("Scripts", "GitCommandsTests");
        }

        [TestCase, Order(30)]
        public void BasicFileOption()
        {
            string sparseFile = Path.Combine(this.Enlistment.EnlistmentRoot, "sparse-folders.txt");
            this.fileSystem.WriteAllText(sparseFile, this.mainSparseFolder);

            this.gvfsProcess.Sparse($"--file {sparseFile}", shouldSucceed: true);
            this.ValidateFoldersInSparseList(this.mainSparseFolder);
            this.CheckMainSparseFolder();
        }

        [TestCase, Order(31)]
        public void FileAddsAndRemovesFolders()
        {
            string sparseFile = Path.Combine(this.Enlistment.EnlistmentRoot, "sparse-folders.txt");
            this.fileSystem.WriteAllText(sparseFile, this.mainSparseFolder + Environment.NewLine + "Scripts");

            this.gvfsProcess.Sparse($"--file {sparseFile}", shouldSucceed: true);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts");
            this.fileSystem.WriteAllText(sparseFile, "GitCommandsTests" + Environment.NewLine + "Scripts");
            this.gvfsProcess.Sparse($"--file {sparseFile}", shouldSucceed: true);
            this.ValidateFoldersInSparseList("Scripts", "GitCommandsTests");
        }

        [TestCase, Order(32)]
        public void DisableWithOtherOptionsFails()
        {
            string output = this.gvfsProcess.Sparse($"--disable --add test1", shouldSucceed: false);
            output.ShouldContain("--disable not valid with other options.");
            output = this.gvfsProcess.Sparse($"--disable --remove test1", shouldSucceed: false);
            output.ShouldContain("--disable not valid with other options.");
            output = this.gvfsProcess.Sparse($"--disable --set test1", shouldSucceed: false);
            output.ShouldContain("--disable not valid with other options.");
            output = this.gvfsProcess.Sparse($"--disable --file test1", shouldSucceed: false);
            output.ShouldContain("--disable not valid with other options.");
            output = this.gvfsProcess.Sparse($"--disable --prune", shouldSucceed: false);
            output.ShouldContain("--disable not valid with other options.");
        }

        [TestCase, Order(33)]
        public void DisableWhenNotInSparseModeShouldBeNoop()
        {
            this.ValidateFoldersInSparseList(NoSparseFolders);
            string output = this.gvfsProcess.Sparse("--disable", shouldSucceed: true);
            output.ShouldEqual(string.Empty);
            this.ValidateFoldersInSparseList(NoSparseFolders);
        }

        [TestCase, Order(34)]
        public void SetShouldFailIfModifiedFilesOutsideSparseSet()
        {
            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS", "Program.cs");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");

            string output = this.gvfsProcess.Sparse($"--set Scripts", shouldSucceed: false);
            output.ShouldContain("Running git status...Failed", SparseAbortedMessage);
        }

        [TestCase, Order(35)]
        public void SetShouldFailIfModifiedFilesOutsideChangedSparseSet()
        {
            string secondFolder = Path.Combine("GVFS", "FastFetch");
            string output = this.gvfsProcess.Sparse($"--set {this.mainSparseFolder};{secondFolder}", shouldSucceed: true);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, secondFolder);
            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, this.mainSparseFolder, "Program.cs");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");

            output = this.gvfsProcess.Sparse($"--set {secondFolder}", shouldSucceed: false);
            output.ShouldContain("Running git status...Failed", SparseAbortedMessage);
        }

        [TestCase, Order(36)]
        public void SetShouldSucceedIfModifiedFilesInChangedSparseSet()
        {
            string secondFolder = Path.Combine("GVFS", "FastFetch");
            string output = this.gvfsProcess.Sparse($"--set {this.mainSparseFolder};{secondFolder}", shouldSucceed: true);
            this.ValidateFoldersInSparseList(this.mainSparseFolder, secondFolder);
            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, this.mainSparseFolder, "Program.cs");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");

            output = this.gvfsProcess.Sparse($"--set {this.mainSparseFolder}", shouldSucceed: true);
            output.ShouldContain("Running git status...Succeeded");
        }

        [TestCase, Order(37)]
        public void PruneShouldStillRunWhenSparseSetDidNotChange()
        {
            string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS", "Program.cs");
            this.fileSystem.WriteAllText(modifiedPath, "New Contents");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "reset --hard");

            string output = this.gvfsProcess.Sparse($"--set Scripts", shouldSucceed: true);
            output.ShouldContain("Running git status...Succeeded", "Updating sparse folder set...Succeeded", "Forcing a projection change...Succeeded");
            this.ValidateFoldersInSparseList("Scripts");
            modifiedPath.ShouldBeAFile(this.fileSystem);

            output = this.gvfsProcess.Sparse($"--set Scripts --prune", shouldSucceed: true);
            output.ShouldContain("No folders to update in sparse set.", "Found 1 folders to prune.", "Cleaning up folders...Succeeded", "GVFS folder prune successful.");
            this.ValidateFoldersInSparseList("Scripts");
            modifiedPath.ShouldNotExistOnDisk(this.fileSystem);
        }

        private void CheckMainSparseFolder()
        {
            string[] directories = Directory.GetDirectories(this.Enlistment.RepoRoot);
            directories.Length.ShouldEqual(2);
            directories.ShouldContain(x => x == Path.Combine(this.Enlistment.RepoRoot, ".git"));
            directories.ShouldContain(x => x == Path.Combine(this.Enlistment.RepoRoot, "GVFS"));

            string folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder);
            folder.ShouldBeADirectory(this.fileSystem);
            folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder, "CommandLine");
            folder.ShouldBeADirectory(this.fileSystem);

            string file = this.Enlistment.GetVirtualPathTo("Readme.md");
            file.ShouldBeAFile(this.fileSystem);

            folder = this.Enlistment.GetVirtualPathTo("Scripts");
            folder.ShouldNotExistOnDisk(this.fileSystem);
            folder = this.Enlistment.GetVirtualPathTo("GVFS", "GVFS.Mount");
            folder.ShouldNotExistOnDisk(this.fileSystem);
        }

        private void ValidatePathAddsAndRemoves(string path, string expectedSparsePath)
        {
            this.gvfsProcess.AddSparseFolders(path);
            this.ValidateFoldersInSparseList(expectedSparsePath);
            this.gvfsProcess.RemoveSparseFolders(path);
            this.ValidateFoldersInSparseList(NoSparseFolders);
            this.gvfsProcess.AddSparseFolders(path);
            this.ValidateFoldersInSparseList(expectedSparsePath);
            this.gvfsProcess.RemoveSparseFolders(expectedSparsePath);
            this.ValidateFoldersInSparseList(NoSparseFolders);
        }

        private void ValidateFoldersInSparseList(params string[] folders)
        {
            StringBuilder folderErrors = new StringBuilder();
            HashSet actualSparseFolders = new HashSet(this.gvfsProcess.GetSparseFolders());

            foreach (string expectedFolder in folders)
            {
                if (!actualSparseFolders.Contains(expectedFolder))
                {
                    folderErrors.AppendLine($"{expectedFolder} not found in actual folder list");
                }

                actualSparseFolders.Remove(expectedFolder);
            }

            foreach (string extraFolder in actualSparseFolders)
            {
                folderErrors.AppendLine($"{extraFolder} unexpected in folder list");
            }

            folderErrors.Length.ShouldEqual(0, folderErrors.ToString());
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/StatusVerbTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.Collections.Generic;
using System.IO;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixture]
    public class StatusVerbTests : TestsWithEnlistmentPerFixture
    {
        [TestCase]
        public void GitTrace()
        {
            Dictionary environmentVariables = new Dictionary();

            this.Enlistment.Status(trace: "1");
            this.Enlistment.Status(trace: "2");

            string logPath = Path.Combine(this.Enlistment.RepoRoot, "log-file.txt");
            this.Enlistment.Status(trace: logPath);

            FileSystemRunner fileSystem = new SystemIORunner();
            fileSystem.FileExists(logPath).ShouldBeTrue();
            string.IsNullOrWhiteSpace(fileSystem.ReadAllText(logPath)).ShouldBeFalse();
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/SymbolicLinkTests.cs
================================================
using System.IO;
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    // Ignored until issue #297 (add SymLink support for Windows) is complete
    [Ignore("Symbolic link support not yet implemented (see issue #297)")]
    [TestFixture]
    public class SymbolicLinkTests : TestsWithEnlistmentPerFixture
    {
        private const string TestFolderName = "Test_EPF_SymbolicLinks";

        // FunctionalTests/20180925_SymLinksPart1 files
        private const string TestFileName = "TestFile.txt";
        private const string TestFileContents = "This is a real file";
        private const string TestFile2Name = "TestFile2.txt";
        private const string TestFile2Contents = "This is the second real file";
        private const string ChildFolderName = "ChildDir";
        private const string ChildLinkName = "LinkToFileInFolder";
        private const string GrandChildLinkName = "LinkToFileInParentFolder";

        // FunctionalTests/20180925_SymLinksPart2 files
        // Note: In this branch ChildLinkName has been changed to point to TestFile2Name
        private const string GrandChildFileName = "TestFile3.txt";
        private const string GrandChildFileContents = "This is the third file";
        private const string GrandChildLinkNowAFileContents = "This was a link but is now a file";

        // FunctionalTests/20180925_SymLinksPart3 files
        private const string ChildFolder2Name = "ChildDir2";

        // FunctionalTests/20180925_SymLinksPart4 files
        // Note: In this branch ChildLinkName has been changed to a directory and ChildFolder2Name has been changed to a link to ChildFolderName

        private BashRunner bashRunner;
        public SymbolicLinkTests()
        {
            this.bashRunner = new BashRunner();
        }

        [TestCase, Order(1)]
        public void CheckoutBranchWithSymLinks()
        {
            GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20201014_SymLinksPart1");
            GitHelpers.CheckGitCommandAgainstGVFSRepo(
                this.Enlistment.RepoRoot,
                "status",
                "On branch FunctionalTests/20201014_SymLinksPart1",
                "nothing to commit, working tree clean");

            string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName));
            testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents);
            this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeFalse($"{testFilePath} should not be a symlink");
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + TestFileName);

            string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name));
            testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents);
            this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink");
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + TestFile2Name);

            string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName));
            this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink");
            childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + ChildLinkName);

            string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName));
            this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeTrue($"{grandChildLinkPath} should be a symlink");
            grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + ChildFolderName + "/" + GrandChildLinkName);
        }

        [TestCase, Order(2)]
        public void CheckoutBranchWhereSymLinksChangeContentsAndTransitionToFile()
        {
            GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20201014_SymLinksPart2");
            GitHelpers.CheckGitCommandAgainstGVFSRepo(
                this.Enlistment.RepoRoot,
                "status",
                "On branch FunctionalTests/20201014_SymLinksPart2",
                "nothing to commit, working tree clean");

            // testFilePath and testFile2Path are unchanged from FunctionalTests/20180925_SymLinksPart2
            string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName));
            testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents);
            this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeFalse($"{testFilePath} should not be a symlink");
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + TestFileName);

            string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name));
            testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents);
            this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink");
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + TestFile2Name);

            // In this branch childLinkPath has been changed to point to testFile2Path
            string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName));
            this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink");
            childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + ChildLinkName);

            // grandChildLinkPath should now be a file
            string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName));
            this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeFalse($"{grandChildLinkPath} should not be a symlink");
            grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildLinkNowAFileContents);

            // There should also be a new file in the child folder
            string newGrandChildFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildFileName));
            newGrandChildFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents);
            this.bashRunner.IsSymbolicLink(newGrandChildFilePath).ShouldBeFalse($"{newGrandChildFilePath} should not be a symlink");
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + ChildFolderName + "/" + GrandChildFileName);
        }

        [TestCase, Order(3)]
        public void CheckoutBranchWhereFilesTransitionToSymLinks()
        {
            GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20201014_SymLinksPart3");
            GitHelpers.CheckGitCommandAgainstGVFSRepo(
                this.Enlistment.RepoRoot,
                "status",
                "On branch FunctionalTests/20201014_SymLinksPart3",
                "nothing to commit, working tree clean");

            // In this branch testFilePath has been changed to point to newGrandChildFilePath
            string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName));
            testFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents);
            this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink");
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + TestFileName);

            // There should be a new ChildFolder2Name directory
            string childFolder2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolder2Name));
            this.bashRunner.IsSymbolicLink(childFolder2Path).ShouldBeFalse($"{childFolder2Path} should not be a symlink");
            childFolder2Path.ShouldBeADirectory(this.bashRunner);
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + ChildFolder2Name);

            // The rest of the files are unchanged from FunctionalTests/20180925_SymLinksPart2
            string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name));
            testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents);
            this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink");

            string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName));
            this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink");
            childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + ChildLinkName);

            string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName));
            this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeFalse($"{grandChildLinkPath} should not be a symlink");
            grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildLinkNowAFileContents);

            string newGrandChildFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildFileName));
            newGrandChildFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents);
            this.bashRunner.IsSymbolicLink(newGrandChildFilePath).ShouldBeFalse($"{newGrandChildFilePath} should not be a symlink");
        }

        [TestCase, Order(4)]
        public void CheckoutBranchWhereSymLinkTransistionsToFolderAndFolderTransitionsToSymlink()
        {
            GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20201014_SymLinksPart4");
            GitHelpers.CheckGitCommandAgainstGVFSRepo(
                this.Enlistment.RepoRoot,
                "status",
                "On branch FunctionalTests/20201014_SymLinksPart4",
                "nothing to commit, working tree clean");

            // In this branch ChildLinkName has been changed to a directory and ChildFolder2Name has been changed to a link to ChildFolderName
            string linkNowADirectoryPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName));
            this.bashRunner.IsSymbolicLink(linkNowADirectoryPath).ShouldBeFalse($"{linkNowADirectoryPath} should not be a symlink");
            linkNowADirectoryPath.ShouldBeADirectory(this.bashRunner);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + ChildLinkName);

            string directoryNowALinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolder2Name));
            this.bashRunner.IsSymbolicLink(directoryNowALinkPath).ShouldBeTrue($"{directoryNowALinkPath} should be a symlink");
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.bashRunner, TestFolderName + "/" + ChildFolder2Name);
        }

        [TestCase, Order(5)]
        public void GitStatusReportsSymLinkChanges()
        {
            GitHelpers.CheckGitCommandAgainstGVFSRepo(
                this.Enlistment.RepoRoot,
                "status",
                "On branch FunctionalTests/20201014_SymLinksPart4",
                "nothing to commit, working tree clean");

            string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName));
            testFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents);
            this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink");

            string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name));
            testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents);
            this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink");

            // Update testFilePath's symlink to point to testFile2Path
            this.bashRunner.CreateSymbolicLink(testFilePath, testFile2Path);

            testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents);
            this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink");

            GitHelpers.CheckGitCommandAgainstGVFSRepo(
                this.Enlistment.RepoRoot,
                "status",
                "On branch FunctionalTests/20201014_SymLinksPart4",
                $"modified:   {TestFolderName}/{TestFileName}");
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/TestsWithEnlistmentPerFixture.cs
================================================
using GVFS.FunctionalTests.Tools;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixture]
    public abstract class TestsWithEnlistmentPerFixture
    {
        private readonly bool forcePerRepoObjectCache;
        private readonly bool skipPrefetchDuringClone;

        public TestsWithEnlistmentPerFixture(bool forcePerRepoObjectCache = false, bool skipPrefetchDuringClone = false)
        {
            this.forcePerRepoObjectCache = forcePerRepoObjectCache;
            this.skipPrefetchDuringClone = skipPrefetchDuringClone;
        }

        public GVFSFunctionalTestEnlistment Enlistment
        {
            get; private set;
        }

        [OneTimeSetUp]
        public virtual void CreateEnlistment()
        {
            if (this.forcePerRepoObjectCache)
            {
                this.Enlistment = GVFSFunctionalTestEnlistment.CloneAndMountWithPerRepoCache(GVFSTestConfig.PathToGVFS, this.skipPrefetchDuringClone);
            }
            else
            {
                this.Enlistment = GVFSFunctionalTestEnlistment.CloneAndMount(GVFSTestConfig.PathToGVFS);
            }
        }

        [OneTimeTearDown]
        public virtual void DeleteEnlistment()
        {
            if (this.Enlistment != null)
            {
                this.Enlistment.UnmountAndDeleteAll();
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/UnmountTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixture]
    [Category(Categories.ExtraCoverage)]
    public class UnmountTests : TestsWithEnlistmentPerFixture
    {
        private FileSystemRunner fileSystem;

        public UnmountTests()
        {
            this.fileSystem = new SystemIORunner();
        }

        [SetUp]
        public void SetupTest()
        {
            GVFSProcess gvfsProcess = new GVFSProcess(
                GVFSTestConfig.PathToGVFS,
                this.Enlistment.EnlistmentRoot,
                Path.Combine(this.Enlistment.EnlistmentRoot, GVFSTestConfig.DotGVFSRoot));

            if (!gvfsProcess.IsEnlistmentMounted())
            {
                gvfsProcess.Mount();
            }
        }

        [TestCase]
        public void UnmountWaitsForLock()
        {
            ManualResetEventSlim lockHolder = GitHelpers.AcquireGVFSLock(this.Enlistment, out _);

            using (Process unmountingProcess = this.StartUnmount())
            {
                unmountingProcess.WaitForExit(3000).ShouldEqual(false, "Unmount completed while lock was acquired.");

                // Release the lock.
                lockHolder.Set();

                unmountingProcess.WaitForExit(10000).ShouldEqual(true, "Unmount didn't complete as expected.");
            }
        }

        [TestCase]
        public void UnmountSkipLock()
        {
            ManualResetEventSlim lockHolder = GitHelpers.AcquireGVFSLock(this.Enlistment, out _, Timeout.Infinite, true);

            using (Process unmountingProcess = this.StartUnmount("--skip-wait-for-lock"))
            {
                unmountingProcess.WaitForExit(10000).ShouldEqual(true, "Unmount didn't complete as expected.");
            }

            // Signal process holding lock to terminate and release lock.
            lockHolder.Set();
        }

        private Process StartUnmount(string extraParams = "")
        {
            string enlistmentRoot = this.Enlistment.EnlistmentRoot;

            // TODO: 865304 Use app.config instead of --internal* arguments
            ProcessStartInfo processInfo = new ProcessStartInfo(GVFSTestConfig.PathToGVFS);
            processInfo.Arguments = "unmount " + extraParams + " " + TestConstants.InternalUseOnlyFlag + " " + GVFSHelpers.GetInternalParameter();
            processInfo.WindowStyle = ProcessWindowStyle.Hidden;
            processInfo.WorkingDirectory = enlistmentRoot;
            processInfo.UseShellExecute = false;

            Process executingProcess = new Process();
            executingProcess.StartInfo = processInfo;
            executingProcess.Start();

            return executingProcess;
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/UpdatePlaceholderTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixture]
    [Category(Categories.GitCommands)]
    public class UpdatePlaceholderTests : TestsWithEnlistmentPerFixture
    {
        private const string TestParentFolderName = "Test_EPF_UpdatePlaceholderTests";
        private const string OldCommitId = "5d7a7d4db1734fb468a4094469ec58d26301b59d";
        private const string NewFilesAndChangesCommitId = "fec239ea12de1eda6ae5329d4f345784d5b61ff9";
        private FileSystemRunner fileSystem;

        public UpdatePlaceholderTests()
        {
            this.fileSystem = new SystemIORunner();
        }

        [SetUp]
        public virtual void SetupForTest()
        {
            // Start each test at NewFilesAndChangesCommitId
            this.GitCheckoutCommitId(NewFilesAndChangesCommitId);
            this.GitStatusShouldBeClean(NewFilesAndChangesCommitId);
        }

        [TestCase, Order(1)]
        public void LockWithFullShareUpdateAndDelete()
        {
            string testFileUpdate4Contents = "Commit2LockToPreventUpdateAndDelete4 \r\n";
            string testFileDelete4Contents = "PreventDelete4 \r\n";
            string testFileUpdate4OldContents = "TestFileLockToPreventUpdateAndDelete4 \r\n";

            string testFileUpdate4Name = "test4.txt";
            string testFileDelete4Name = "test_delete4.txt";

            string testFileUpdate4Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdateAndDelete", testFileUpdate4Name));
            string testFileDelete4Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdateAndDelete", testFileDelete4Name));

            testFileUpdate4Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate4Contents);
            testFileDelete4Path.ShouldBeAFile(this.fileSystem).WithContents(testFileDelete4Contents);

            using (FileStream testFileUpdate4 = File.Open(testFileUpdate4Path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
            using (FileStream testFileDelete4 = File.Open(testFileDelete4Path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
            {
                this.GitCheckoutCommitId(OldCommitId);
                this.GitStatusShouldBeClean(OldCommitId);
            }

            testFileUpdate4Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate4OldContents);
            testFileDelete4Path.ShouldNotExistOnDisk(this.fileSystem);

            this.GitCheckoutCommitId(NewFilesAndChangesCommitId);

            this.GitStatusShouldBeClean(NewFilesAndChangesCommitId);
            testFileUpdate4Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate4Contents);
            testFileDelete4Path.ShouldBeAFile(this.fileSystem).WithContents(testFileDelete4Contents);
        }

        [TestCase, Order(2)]
        public void FileProjectedAfterPlaceholderDeleteFileAndCheckout()
        {
            string testFile1Contents = "ProjectAfterDeleteAndCheckout \r\n";
            string testFile2Contents = "ProjectAfterDeleteAndCheckout2 \r\n";
            string testFile3Contents = "ProjectAfterDeleteAndCheckout3 \r\n";

            string testFile1Name = "test.txt";
            string testFile2Name = "test2.txt";
            string testFile3Name = "test3.txt";

            string testFile1Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "FileProjectedAfterPlaceholderDeleteFileAndCheckout", testFile1Name));
            string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "FileProjectedAfterPlaceholderDeleteFileAndCheckout", testFile2Name));
            string testFile3Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "FileProjectedAfterPlaceholderDeleteFileAndCheckout", testFile3Name));

            testFile1Path.ShouldBeAFile(this.fileSystem).WithContents(testFile1Contents);
            testFile2Path.ShouldBeAFile(this.fileSystem).WithContents(testFile2Contents);
            testFile3Path.ShouldBeAFile(this.fileSystem).WithContents(testFile3Contents);

            this.GitCheckoutCommitId(OldCommitId);
            this.GitStatusShouldBeClean(OldCommitId);

            testFile1Path.ShouldNotExistOnDisk(this.fileSystem);
            testFile2Path.ShouldNotExistOnDisk(this.fileSystem);
            testFile3Path.ShouldNotExistOnDisk(this.fileSystem);

            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/FileProjectedAfterPlaceholderDeleteFileAndCheckout/" + testFile1Name);
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/FileProjectedAfterPlaceholderDeleteFileAndCheckout/" + testFile2Name);
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/FileProjectedAfterPlaceholderDeleteFileAndCheckout/" + testFile3Name);

            this.GitCheckoutCommitId(NewFilesAndChangesCommitId);
            this.GitStatusShouldBeClean(NewFilesAndChangesCommitId);

            testFile1Path.ShouldBeAFile(this.fileSystem).WithContents(testFile1Contents);
            testFile2Path.ShouldBeAFile(this.fileSystem).WithContents(testFile2Contents);
            testFile3Path.ShouldBeAFile(this.fileSystem).WithContents(testFile3Contents);
        }

        [TestCase, Order(3)]
        public void FullFilesDontAffectThePlaceholderDatabase()
        {
            string testFile = Path.Combine(this.Enlistment.RepoRoot, "FullFilesDontAffectThePlaceholderDatabase");

            string placeholderDatabase = Path.Combine(this.Enlistment.DotGVFSRoot, "databases", "VFSForGit.sqlite");
            string placeholdersBefore = GVFSHelpers.GetAllSQLitePlaceholdersAsString(placeholderDatabase);

            this.fileSystem.CreateEmptyFile(testFile);

            this.Enlistment.WaitForBackgroundOperations();
            GVFSHelpers.GetAllSQLitePlaceholdersAsString(placeholderDatabase).ShouldEqual(placeholdersBefore);

            this.fileSystem.DeleteFile(testFile);

            this.Enlistment.WaitForBackgroundOperations();
            GVFSHelpers.GetAllSQLitePlaceholdersAsString(placeholderDatabase).ShouldEqual(placeholdersBefore);
        }

        private ProcessResult InvokeGitAgainstGVFSRepo(string command)
        {
            return GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, command);
        }

        private void GitStatusShouldBeClean(string commitId)
        {
            GitHelpers.CheckGitCommandAgainstGVFSRepo(
                this.Enlistment.RepoRoot,
                "status",
                "HEAD detached at " + commitId,
                "nothing to commit, working tree clean");
        }

        private void GitCheckoutToDiscardChanges(string gitPath)
        {
            GitHelpers.CheckGitCommandAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout -- " + gitPath);
        }

        private void GitCheckoutCommitId(string commitId)
        {
            this.InvokeGitAgainstGVFSRepo("checkout " + commitId).Errors.ShouldContain("HEAD is now at " + commitId);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorkingDirectoryTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixtureSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
    public class WorkingDirectoryTests : TestsWithEnlistmentPerFixture
    {
        public const string TestFileContents =
@"// dllmain.cpp : Defines the entry point for the DLL application.
#include ""stdafx.h""

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    UNREFERENCED_PARAMETER(hModule);
    UNREFERENCED_PARAMETER(lpReserved);

    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

";
        private const int CurrentPlaceholderVersion = 1;

        private FileSystemRunner fileSystem;

        public WorkingDirectoryTests(FileSystemRunner fileSystem)
            : base(forcePerRepoObjectCache: true)
        {
            this.fileSystem = fileSystem;
        }

        [TestCase, Order(1)]
        public void ProjectedFileHasExpectedContents()
        {
            this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "ProjectedFileHasExpectedContents.cpp")
                .ShouldBeAFile(this.fileSystem)
                .WithContents(TestFileContents);
        }

        [TestCase, Order(2)]
        public void StreamAccessReadWriteMemoryMappedProjectedFile()
        {
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "StreamAccessReadWriteMemoryMappedProjectedFile.cs");
            string contents = fileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents();
            StringBuilder contentsBuilder = new StringBuilder(contents);

            using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath))
            {
                // Length of the Byte-order-mark that will be at the start of the memory mapped file.
                // See https://msdn.microsoft.com/en-us/library/windows/desktop/dd374101(v=vs.85).aspx
                int bomOffset = 3;

                // offset -> Number of bytes from the start of the file where the view starts
                int offset = 64;
                int size = contents.Length;
                string newContent = "**NEWCONTENT**";

                using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset, size - offset + bomOffset))
                {
                    streamAccessor.CanRead.ShouldEqual(true);
                    streamAccessor.CanWrite.ShouldEqual(true);

                    for (int i = offset; i < size - offset; ++i)
                    {
                        streamAccessor.ReadByte().ShouldEqual(contents[i - bomOffset]);
                    }

                    // Reset to the start of the stream (which will place the streamAccessor at offset in the memory file)
                    streamAccessor.Seek(0, SeekOrigin.Begin);
                    byte[] newContentBuffer = Encoding.ASCII.GetBytes(newContent);

                    streamAccessor.Write(newContentBuffer, 0, newContent.Length);

                    for (int i = 0; i < newContent.Length; ++i)
                    {
                        contentsBuilder[offset + i - bomOffset] = newContent[i];
                    }

                    contents = contentsBuilder.ToString();
                }

                // Verify the file has the new contents inserted into it
                using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset: 0, size: size + bomOffset))
                {
                    // Skip the BOM
                    for (int i = 0; i < bomOffset; ++i)
                    {
                        streamAccessor.ReadByte();
                    }

                    for (int i = 0; i < size; ++i)
                    {
                        streamAccessor.ReadByte().ShouldEqual(contents[i]);
                    }
                }
            }

            // Confirm the new contents was written to disk
            fileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(contents);
        }

        [TestCase, Order(3)]
        public void RandomAccessReadWriteMemoryMappedProjectedFile()
        {
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "RandomAccessReadWriteMemoryMappedProjectedFile.cs");

            string contents = fileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents();
            StringBuilder contentsBuilder = new StringBuilder(contents);

            using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath))
            {
                // Length of the Byte-order-mark that will be at the start of the memory mapped file.
                // See https://msdn.microsoft.com/en-us/library/windows/desktop/dd374101(v=vs.85).aspx
                int bomOffset = 3;

                // offset -> Number of bytes from the start of the file where the view starts
                int offset = 64;
                int size = contents.Length;
                string newContent = "**NEWCONTENT**";

                using (MemoryMappedViewAccessor randomAccessor = mmf.CreateViewAccessor(offset, size - offset + bomOffset))
                {
                    randomAccessor.CanRead.ShouldEqual(true);
                    randomAccessor.CanWrite.ShouldEqual(true);

                    for (int i = 0; i < size - offset; ++i)
                    {
                        ((char)randomAccessor.ReadByte(i)).ShouldEqual(contents[i + offset - bomOffset]);
                    }

                    for (int i = 0; i < newContent.Length; ++i)
                    {
                        // Convert to byte before writing rather than writing as char, because char version will write a 16-bit
                        // unicode char
                        randomAccessor.Write(i, Convert.ToByte(newContent[i]));
                        ((char)randomAccessor.ReadByte(i)).ShouldEqual(newContent[i]);
                    }

                    for (int i = 0; i < newContent.Length; ++i)
                    {
                        contentsBuilder[offset + i - bomOffset] = newContent[i];
                    }

                    contents = contentsBuilder.ToString();
                }

                // Verify the file has the new contents inserted into it
                using (MemoryMappedViewAccessor randomAccessor = mmf.CreateViewAccessor(offset: 0, size: size + bomOffset))
                {
                    for (int i = 0; i < size; ++i)
                    {
                        ((char)randomAccessor.ReadByte(i + bomOffset)).ShouldEqual(contents[i]);
                    }
                }
            }

            // Confirm the new contents was written to disk
            fileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(contents);
        }

        [TestCase, Order(4)]
        public void StreamAndRandomAccessReadWriteMemoryMappedProjectedFile()
        {
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "StreamAndRandomAccessReadWriteMemoryMappedProjectedFile.cs");

            StringBuilder contentsBuilder = new StringBuilder();

            // Length of the Byte-order-mark that will be at the start of the memory mapped file.
            // See https://msdn.microsoft.com/en-us/library/windows/desktop/dd374101(v=vs.85).aspx
            int bomOffset = 3;

            using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath))
            {
                // The text length of StreamAndRandomAccessReadWriteMemoryMappedProjectedFile.cs was determined
                // outside of this test so that the test would not hydrate the file before we access via MemoryMappedFile
                int fileTextLength = 13762;

                int size = bomOffset + fileTextLength;

                int streamAccessWriteOffset = 64;
                int randomAccessWriteOffset = 128;

                string newStreamAccessContent = "**NEW_STREAM_CONTENT**";
                string newRandomAccessConents = "&&NEW_RANDOM_CONTENT&&";

                // Read (and modify) contents using stream accessor
                using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset: 0, size: size))
                {
                    streamAccessor.CanRead.ShouldEqual(true);
                    streamAccessor.CanWrite.ShouldEqual(true);

                    for (int i = 0; i < size; ++i)
                    {
                        contentsBuilder.Append((char)streamAccessor.ReadByte());
                    }

                    // Reset to the start of the stream (which will place the streamAccessor at offset in the memory file)
                    streamAccessor.Seek(streamAccessWriteOffset, SeekOrigin.Begin);
                    byte[] newContentBuffer = Encoding.ASCII.GetBytes(newStreamAccessContent);

                    streamAccessor.Write(newContentBuffer, 0, newStreamAccessContent.Length);

                    for (int i = 0; i < newStreamAccessContent.Length; ++i)
                    {
                        contentsBuilder[streamAccessWriteOffset + i] = newStreamAccessContent[i];
                    }
                }

                // Read (and modify) contents using random accessor
                using (MemoryMappedViewAccessor randomAccessor = mmf.CreateViewAccessor(offset: 0, size: size))
                {
                    randomAccessor.CanRead.ShouldEqual(true);
                    randomAccessor.CanWrite.ShouldEqual(true);

                    // Confirm the random accessor reads the same content that was read (and written) by the stream
                    // accessor
                    for (int i = 0; i < size; ++i)
                    {
                        ((char)randomAccessor.ReadByte(i)).ShouldEqual(contentsBuilder[i]);
                    }

                    // Write some new content
                    for (int i = 0; i < newRandomAccessConents.Length; ++i)
                    {
                        // Convert to byte before writing rather than writing as char, because char version will write a 16-bit
                        // unicode char
                        randomAccessor.Write(i + randomAccessWriteOffset, Convert.ToByte(newRandomAccessConents[i]));
                        ((char)randomAccessor.ReadByte(i + randomAccessWriteOffset)).ShouldEqual(newRandomAccessConents[i]);
                    }

                    for (int i = 0; i < newRandomAccessConents.Length; ++i)
                    {
                        contentsBuilder[randomAccessWriteOffset + i] = newRandomAccessConents[i];
                    }
                }

                // Verify the file one more time with a stream accessor
                using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset: 0, size: size))
                {
                    for (int i = 0; i < size; ++i)
                    {
                        streamAccessor.ReadByte().ShouldEqual(contentsBuilder[i]);
                    }
                }
            }

            // Remove the BOM before comparing with the contents of the file on disk
            contentsBuilder.Remove(0, bomOffset);

            // Confirm the new contents was written to the file
            fileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(contentsBuilder.ToString());
        }

        [TestCase, Order(5)]
        public void MoveProjectedFileToInvalidFolder()
        {
            string targetFolderName = "test_folder";
            string targetFolderVirtualPath = this.Enlistment.GetVirtualPathTo(targetFolderName);
            targetFolderVirtualPath.ShouldNotExistOnDisk(this.fileSystem);

            string sourceFolderName = "Test_EPF_WorkingDirectoryTests";
            string testFileName = "MoveProjectedFileToInvalidFolder.config";
            string sourcePath = Path.Combine(sourceFolderName, testFileName);
            string sourceVirtualPath = this.Enlistment.GetVirtualPathTo(sourcePath);

            string newTestFileVirtualPath = Path.Combine(targetFolderVirtualPath, testFileName);

            this.fileSystem.MoveFileShouldFail(sourceVirtualPath, newTestFileVirtualPath);
            newTestFileVirtualPath.ShouldNotExistOnDisk(this.fileSystem);

            sourceVirtualPath.ShouldBeAFile(this.fileSystem);

            targetFolderVirtualPath.ShouldNotExistOnDisk(this.fileSystem);
        }

        [TestCase, Order(6)]
        public void EnumerateAndReadDoesNotChangeEnumerationOrder()
        {
            string folderVirtualPath = this.Enlistment.GetVirtualPathTo("EnumerateAndReadTestFiles");
            this.EnumerateAndReadShouldNotChangeEnumerationOrder(folderVirtualPath);
            folderVirtualPath.ShouldBeADirectory(this.fileSystem);
            folderVirtualPath.ShouldBeADirectory(this.fileSystem).WithItems();
        }

        [TestCase, Order(7)]
        public void HydratingFileUsesNameCaseFromRepo()
        {
            string fileName = "Readme.md";
            string parentFolderPath = this.Enlistment.GetVirtualPathTo(Path.GetDirectoryName(fileName));
            parentFolderPath.ShouldBeADirectory(this.fileSystem).WithItems().ShouldContainSingle(info => info.Name.Equals(fileName, StringComparison.Ordinal));

            // Hydrate file with a request using different file name case except on case-sensitive filesystems
            string testFileName = FileSystemHelpers.CaseSensitiveFileSystem ? fileName : fileName.ToUpper();
            string testFilePath = this.Enlistment.GetVirtualPathTo(testFileName);
            string fileContents = testFilePath.ShouldBeAFile(this.fileSystem).WithContents();

            // File on disk should have original case projected from repo
            parentFolderPath.ShouldBeADirectory(this.fileSystem).WithItems().ShouldContainSingle(info => info.Name.Equals(fileName, StringComparison.Ordinal));
        }

        [TestCase, Order(8)]
        public void HydratingNestedFileUsesNameCaseFromRepo()
        {
            string filePath = Path.Combine("GVFS", "FastFetch", "Properties", "AssemblyInfo.cs");
            string testFilePath = FileSystemHelpers.CaseSensitiveFileSystem ? filePath : filePath.ToUpper();
            string testParentFolderVirtualPath = this.Enlistment.GetVirtualPathTo(Path.GetDirectoryName(testFilePath));
            testParentFolderVirtualPath.ShouldBeADirectory(this.fileSystem).WithItems().ShouldContainSingle(info => info.Name.Equals(Path.GetFileName(filePath), StringComparison.Ordinal));

            // Hydrate file with a request using different file name case except on case-sensitive filesystems
            testFilePath = this.Enlistment.GetVirtualPathTo(testFilePath);
            string fileContents = testFilePath.ShouldBeAFile(this.fileSystem).WithContents();

            // File on disk should have original case projected from repo
            string parentFolderVirtualPath = this.Enlistment.GetVirtualPathTo(Path.GetDirectoryName(filePath));
            parentFolderVirtualPath.ShouldBeADirectory(this.fileSystem).WithItems().ShouldContainSingle(info => info.Name.Equals(Path.GetFileName(filePath), StringComparison.Ordinal));

            // Confirm all folders up to root have the correct case
            string parentFolderPath = Path.GetDirectoryName(filePath);
            while (!string.IsNullOrWhiteSpace(parentFolderPath))
            {
                string folderName = Path.GetFileName(parentFolderPath);
                parentFolderPath = Path.GetDirectoryName(parentFolderPath);
                this.Enlistment.GetVirtualPathTo(parentFolderPath).ShouldBeADirectory(this.fileSystem).WithItems().ShouldContainSingle(info => info.Name.Equals(folderName, StringComparison.Ordinal));
            }
        }

        [TestCase, Order(9)]
        public void AppendToHydratedFileAfterRemount()
        {
            string fileToAppendEntry = "Test_EPF_WorkingDirectoryTests/WriteToHydratedFileAfterRemount.cpp";
            string virtualFilePath = this.Enlistment.GetVirtualPathTo(fileToAppendEntry);
            string fileContents = virtualFilePath.ShouldBeAFile(this.fileSystem).WithContents();
            this.Enlistment.WaitForBackgroundOperations();
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, fileToAppendEntry);

            // Remount
            this.Enlistment.UnmountGVFS();
            this.Enlistment.MountGVFS();

            string appendedText = "Text to append";
            this.fileSystem.AppendAllText(virtualFilePath, appendedText);
            this.Enlistment.WaitForBackgroundOperations();
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, fileToAppendEntry);
            virtualFilePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents + appendedText);
        }

        [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);
        }

        [TestCase, Order(11)]
        public void FilePlaceHolderHasVersionInfo()
        {
            string sha = "BB1C8B9ADA90D6B8F6C88F12C6DDB07C186155BD";
            string virtualFilePath = this.Enlistment.GetVirtualPathTo("GVFlt_BugRegressionTest", "GVFlt_ModifyFileInScratchAndDir", "ModifyFileInScratchAndDir.txt");
            virtualFilePath.ShouldBeAFile(this.fileSystem).WithContents();

            ProcessResult revParseHeadResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "rev-parse HEAD");
            string commitID = revParseHeadResult.Output.Trim();

            this.PlaceholderHasVersionInfo(virtualFilePath, CurrentPlaceholderVersion, sha).ShouldEqual(true);
        }

        [TestCase, Order(12), Ignore("Results in an access violation in the functional test on the build server")]
        public void FolderPlaceHolderHasVersionInfo()
        {
            string virtualFilePath = this.Enlistment.GetVirtualPathTo("GVFlt_BugRegressionTest", "GVFlt_ModifyFileInScratchAndDir");

            ProcessResult revParseHeadResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "rev-parse HEAD");
            string commitID = revParseHeadResult.Output.Trim();

            this.PlaceholderHasVersionInfo(virtualFilePath, CurrentPlaceholderVersion, string.Empty).ShouldEqual(true);
        }

        [TestCase, Order(13)]
        [Category(Categories.GitCommands)]
        public void FolderContentsProjectedAfterFolderCreateAndCheckout()
        {
            string folderName = "GVFlt_MultiThreadTest";

            // 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.Enlistment, this.fileSystem, folderName);

            string virtualFolderPath = this.Enlistment.GetVirtualPathTo(folderName);
            virtualFolderPath.ShouldNotExistOnDisk(this.fileSystem);
            this.fileSystem.CreateDirectory(virtualFolderPath);

            // 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; on case-sensitive filesystems, case sensitivity is validated instead
            GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout b3ddcf43b997cba3fbf9d2341b297e22bf48601a");

            string testFolderName = FileSystemHelpers.CaseSensitiveFileSystem ? "GVFLT_MultiThreadTest" : folderName;
            this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderName, "OpenForReadsSameTime", "test")).ShouldBeAFile(this.fileSystem).WithContents("123 \r\n");
            this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderName, "OpenForWritesSameTime", "test")).ShouldBeAFile(this.fileSystem).WithContents("123 \r\n");
        }

        [TestCase, Order(14)]
        [Category(Categories.GitCommands)]
        public void FolderContentsCorrectAfterCreateNewFolderRenameAndCheckoutCommitWithSameFolder()
        {
            // 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";
            string folder = this.Enlistment.GetVirtualPathTo(folderName);
            folder.ShouldNotExistOnDisk(this.fileSystem);
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, folderName);

            // Confirm modified paths picks up renamed folder
            string newFolder = this.Enlistment.GetVirtualPathTo("newFolder");
            this.fileSystem.CreateDirectory(newFolder);
            this.fileSystem.MoveDirectory(newFolder, folder);

            this.Enlistment.WaitForBackgroundOperations();
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, folderName + "/");

            // Switch back to this.ControlGitRepo.Commitish and confirm that folder contents are correct
            GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout " + Properties.Settings.Default.Commitish);

            folder.ShouldBeADirectory(this.fileSystem);
            Path.Combine(folder, "ChangeNestedUnhydratedFileNameCase", "Program.cs").ShouldBeAFile(this.fileSystem).WithContents(MoveRenameFileTests.TestFileContents);
            Path.Combine(folder, "ChangeUnhydratedFileName", "Program.cs").ShouldBeAFile(this.fileSystem).WithContents(MoveRenameFileTests.TestFileContents);
            Path.Combine(folder, "MoveUnhydratedFileToDotGitFolder", "Program.cs").ShouldBeAFile(this.fileSystem).WithContents(MoveRenameFileTests.TestFileContents);
        }

        [TestCase, Order(15)]
        public void FilterNonUTF8FileName()
        {
            string encodingFilename = "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt";
            string folderVirtualPath = this.Enlistment.GetVirtualPathTo("FilenameEncoding");

            this.FolderEnumerationShouldHaveSingleEntry(folderVirtualPath, encodingFilename, null);
            this.FolderEnumerationShouldHaveSingleEntry(folderVirtualPath, encodingFilename, "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
            this.FolderEnumerationShouldHaveSingleEntry(folderVirtualPath, encodingFilename, "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك*");
            string testEntryExt = FileSystemHelpers.CaseSensitiveFileSystem ? "txt" : "TXT";
            string testEntryName = "ريلٌأكتوبر*." + testEntryExt;
            this.FolderEnumerationShouldHaveSingleEntry(folderVirtualPath, encodingFilename, testEntryName);

            folderVirtualPath.ShouldBeADirectory(this.fileSystem).WithNoItems("test*");
            folderVirtualPath.ShouldBeADirectory(this.fileSystem).WithNoItems("ريلٌأكتوب.TXT");
        }

        [TestCase, Order(16)]
        public void AllNullObjectRedownloaded()
        {
            GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout " + this.Enlistment.Commitish);
            ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "rev-parse :Test_EPF_WorkingDirectoryTests/AllNullObjectRedownloaded.txt");
            string sha = revParseResult.Output.Trim();
            sha.Length.ShouldEqual(40);

            // Ensure SHA path is lowercase for case-sensitive filesystems
            string objectPathSha = FileSystemHelpers.CaseSensitiveFileSystem ? sha.ToLower() : sha;
            string objectPath = Path.Combine(this.Enlistment.GetObjectRoot(this.fileSystem), objectPathSha.Substring(0, 2), objectPathSha.Substring(2, 38));
            objectPath.ShouldNotExistOnDisk(this.fileSystem);

            // At this point there should be no corrupt objects
            string corruptObjectFolderPath = Path.Combine(this.Enlistment.DotGVFSRoot, "CorruptObjects");
            corruptObjectFolderPath.ShouldNotExistOnDisk(this.fileSystem);

            // Read a copy of AllNullObjectRedownloaded.txt to force the object to be downloaded
            GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "rev-parse :Test_EPF_WorkingDirectoryTests/AllNullObjectRedownloaded_copy.txt").Output.Trim().ShouldEqual(sha);
            string testFileContents = this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "AllNullObjectRedownloaded_copy.txt").ShouldBeAFile(this.fileSystem).WithContents();
            objectPath.ShouldBeAFile(this.fileSystem);

            // Set the contents of objectPath to all NULL
            FileInfo objectFileInfo = new FileInfo(objectPath);
            File.WriteAllBytes(objectPath, Enumerable.Repeat(0, (int)objectFileInfo.Length).ToArray());

            // Read the original path and verify its contents are correct
            this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "AllNullObjectRedownloaded.txt").ShouldBeAFile(this.fileSystem).WithContents(testFileContents);

            // Confirm there's a new item in the corrupt objects folder
            corruptObjectFolderPath.ShouldBeADirectory(this.fileSystem);
            FileSystemInfo badObject = corruptObjectFolderPath.ShouldBeADirectory(this.fileSystem).WithOneItem();
            (badObject as FileInfo).ShouldNotBeNull().Length.ShouldEqual(objectFileInfo.Length);
        }

        [TestCase, Order(17)]
        public void TruncatedObjectRedownloaded()
        {
            GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout " + this.Enlistment.Commitish);
            ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "rev-parse :Test_EPF_WorkingDirectoryTests/TruncatedObjectRedownloaded.txt");
            string sha = revParseResult.Output.Trim();
            sha.Length.ShouldEqual(40);
            string objectPath = Path.Combine(this.Enlistment.GetObjectRoot(this.fileSystem), sha.Substring(0, 2), sha.Substring(2, 38));
            objectPath.ShouldNotExistOnDisk(this.fileSystem);

            string corruptObjectFolderPath = Path.Combine(this.Enlistment.DotGVFSRoot, "CorruptObjects");
            int initialCorruptObjectCount = 0;
            if (this.fileSystem.DirectoryExists(corruptObjectFolderPath))
            {
                initialCorruptObjectCount = new DirectoryInfo(corruptObjectFolderPath).EnumerateFileSystemInfos().Count();
            }

            // Read a copy of TruncatedObjectRedownloaded.txt to force the object to be downloaded
            GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "rev-parse :Test_EPF_WorkingDirectoryTests/TruncatedObjectRedownloaded_copy.txt").Output.Trim().ShouldEqual(sha);
            string testFileContents = this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "TruncatedObjectRedownloaded_copy.txt").ShouldBeAFile(this.fileSystem).WithContents();
            objectPath.ShouldBeAFile(this.fileSystem);
            string modifedFile = "Test_EPF_WorkingDirectoryTests/TruncatedObjectRedownloaded.txt";
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, modifedFile);

            // Truncate the contents of objectPath
            string tempTruncatedObjectPath = objectPath + "truncated";
            FileInfo objectFileInfo = new FileInfo(objectPath);
            long objectLength = objectFileInfo.Length;
            using (FileStream objectStream = new FileStream(objectPath, FileMode.Open))
            using (FileStream truncatedObjectStream = new FileStream(tempTruncatedObjectPath, FileMode.CreateNew))
            {
                for (int i = 0; i < (objectStream.Length - 16); ++i)
                {
                    truncatedObjectStream.WriteByte((byte)objectStream.ReadByte());
                }
            }

            this.fileSystem.DeleteFile(objectPath);
            this.fileSystem.MoveFile(tempTruncatedObjectPath, objectPath);
            tempTruncatedObjectPath.ShouldNotExistOnDisk(this.fileSystem);
            objectPath.ShouldBeAFile(this.fileSystem);
            new FileInfo(objectPath).Length.ShouldEqual(objectLength - 16);

            // Windows should correct a corrupt obect
            // Read the original path and verify its contents are correct
            this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "TruncatedObjectRedownloaded.txt").ShouldBeAFile(this.fileSystem).WithContents(testFileContents);

            // Confirm there's a new item in the corrupt objects folder
            corruptObjectFolderPath.ShouldBeADirectory(this.fileSystem).WithItems().Count().ShouldEqual(initialCorruptObjectCount + 1);

            // File should not be in ModifiedPaths.dat
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.fileSystem, "Test_EPF_WorkingDirectoryTests/TruncatedObjectRedownloaded.txt");
        }

        [TestCase, Order(18)]
        public void CreateFileAfterTryOpenNonExistentFile()
        {
            string filePath = this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "CreateFileAfterTryOpenNonExistentFile_NotProjected.txt");
            string fileContents = "CreateFileAfterTryOpenNonExistentFile file contents";
            filePath.ShouldNotExistOnDisk(this.fileSystem);
            this.fileSystem.WriteAllText(filePath, fileContents);
            filePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents);
        }

        [TestCase, Order(19)]
        public void RenameFileAfterTryOpenNonExistentFile()
        {
            string filePath = this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "RenameFileAfterTryOpenNonExistentFile_NotProjected.txt");
            string fileContents = "CreateFileAfterTryOpenNonExistentFile file contents";
            filePath.ShouldNotExistOnDisk(this.fileSystem);

            string newFilePath = this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "RenameFileAfterTryOpenNonExistentFile_NewFile.txt");
            this.fileSystem.WriteAllText(newFilePath, fileContents);
            newFilePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents);

            this.fileSystem.MoveFile(newFilePath, filePath);
            filePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents);
        }

        [TestCase, Order(20)]
        public void VerifyFileSize()
        {
            string filePath = this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests", "ProjectedFileHasExpectedContents.cpp");
            long fileSize = this.fileSystem.FileSize(filePath);
            fileSize.ShouldEqual(536);
        }

        private void FolderEnumerationShouldHaveSingleEntry(string folderVirtualPath, string expectedEntryName, string searchPatten)
        {
            IEnumerable folderEntries;
            if (string.IsNullOrEmpty(searchPatten))
            {
                folderEntries = folderVirtualPath.ShouldBeADirectory(this.fileSystem).WithItems();
            }
            else
            {
                folderEntries = folderVirtualPath.ShouldBeADirectory(this.fileSystem).WithItems(searchPatten);
            }

            folderEntries.Count().ShouldEqual(1);
            FileSystemInfo singleEntry = folderEntries.First();
            singleEntry.Name.ShouldEqual(expectedEntryName, $"Actual name: {singleEntry.Name} does not equal expected name {expectedEntryName}");
        }

        private void EnumerateAndReadShouldNotChangeEnumerationOrder(string folderRelativePath)
        {
            NativeTests.EnumerateAndReadDoesNotChangeEnumerationOrder(folderRelativePath).ShouldEqual(true);
        }

        private bool PlaceholderHasVersionInfo(string relativePath, int version, string sha)
        {
            return NativeTests.PlaceHolderHasVersionInfo(relativePath, version, sha);
        }

        private class NativeTests
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool EnumerateAndReadDoesNotChangeEnumerationOrder(string folderVirtualPath);

            [DllImport("GVFS.NativeTests.dll", CharSet = CharSet.Ansi)]
            public static extern bool PlaceHolderHasVersionInfo(
                string virtualPath,
                int version,
                [MarshalAs(UnmanagedType.LPWStr)]string sha);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorktreeTests.cs
================================================
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.IO;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixture]
    [Category(Categories.GitCommands)]
    public class WorktreeTests : TestsWithEnlistmentPerFixture
    {
        private const string WorktreeBranchA = "worktree-test-branch-a";
        private const string WorktreeBranchB = "worktree-test-branch-b";

        [TestCase]
        public void ConcurrentWorktreeAddCommitRemove()
        {
            string worktreePathA = Path.Combine(this.Enlistment.EnlistmentRoot, "test-wt-a-" + Guid.NewGuid().ToString("N").Substring(0, 8));
            string worktreePathB = Path.Combine(this.Enlistment.EnlistmentRoot, "test-wt-b-" + Guid.NewGuid().ToString("N").Substring(0, 8));

            try
            {
                // 1. Create both worktrees in parallel
                ProcessResult addResultA = null;
                ProcessResult addResultB = null;
                System.Threading.Tasks.Parallel.Invoke(
                    () => addResultA = GitHelpers.InvokeGitAgainstGVFSRepo(
                        this.Enlistment.RepoRoot,
                        $"worktree add -b {WorktreeBranchA} \"{worktreePathA}\""),
                    () => addResultB = GitHelpers.InvokeGitAgainstGVFSRepo(
                        this.Enlistment.RepoRoot,
                        $"worktree add -b {WorktreeBranchB} \"{worktreePathB}\""));

                addResultA.ExitCode.ShouldEqual(0, $"worktree add A failed: {addResultA.Errors}");
                addResultB.ExitCode.ShouldEqual(0, $"worktree add B failed: {addResultB.Errors}");

                // 2. Verify both have projected files
                Directory.Exists(worktreePathA).ShouldBeTrue("Worktree A directory should exist");
                Directory.Exists(worktreePathB).ShouldBeTrue("Worktree B directory should exist");
                File.Exists(Path.Combine(worktreePathA, "Readme.md")).ShouldBeTrue("Readme.md should be projected in A");
                File.Exists(Path.Combine(worktreePathB, "Readme.md")).ShouldBeTrue("Readme.md should be projected in B");

                // 3. Verify git status is clean in both
                ProcessResult statusA = GitHelpers.InvokeGitAgainstGVFSRepo(worktreePathA, "status --porcelain");
                ProcessResult statusB = GitHelpers.InvokeGitAgainstGVFSRepo(worktreePathB, "status --porcelain");
                statusA.ExitCode.ShouldEqual(0, $"git status A failed: {statusA.Errors}");
                statusB.ExitCode.ShouldEqual(0, $"git status B failed: {statusB.Errors}");
                statusA.Output.Trim().ShouldBeEmpty("Worktree A should have clean status");
                statusB.Output.Trim().ShouldBeEmpty("Worktree B should have clean status");

                // 4. Verify worktree list shows all three
                ProcessResult listResult = GitHelpers.InvokeGitAgainstGVFSRepo(
                    this.Enlistment.RepoRoot, "worktree list");
                listResult.ExitCode.ShouldEqual(0, $"worktree list failed: {listResult.Errors}");
                string listOutput = listResult.Output;
                Assert.IsTrue(listOutput.Contains(worktreePathA.Replace('\\', '/')),
                    $"worktree list should contain A. Output: {listOutput}");
                Assert.IsTrue(listOutput.Contains(worktreePathB.Replace('\\', '/')),
                    $"worktree list should contain B. Output: {listOutput}");

                // 5. Make commits in both worktrees
                File.WriteAllText(Path.Combine(worktreePathA, "from-a.txt"), "created in worktree A");
                GitHelpers.InvokeGitAgainstGVFSRepo(worktreePathA, "add from-a.txt")
                    .ExitCode.ShouldEqual(0);
                GitHelpers.InvokeGitAgainstGVFSRepo(worktreePathA, "commit -m \"commit from A\"")
                    .ExitCode.ShouldEqual(0);

                File.WriteAllText(Path.Combine(worktreePathB, "from-b.txt"), "created in worktree B");
                GitHelpers.InvokeGitAgainstGVFSRepo(worktreePathB, "add from-b.txt")
                    .ExitCode.ShouldEqual(0);
                GitHelpers.InvokeGitAgainstGVFSRepo(worktreePathB, "commit -m \"commit from B\"")
                    .ExitCode.ShouldEqual(0);

                // 6. Verify commits are visible from all worktrees (shared objects)
                GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, $"log -1 --format=%s {WorktreeBranchA}")
                    .Output.ShouldContain(expectedSubstrings: new[] { "commit from A" });
                GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, $"log -1 --format=%s {WorktreeBranchB}")
                    .Output.ShouldContain(expectedSubstrings: new[] { "commit from B" });

                // A can see B's commit and vice versa
                GitHelpers.InvokeGitAgainstGVFSRepo(worktreePathA, $"log -1 --format=%s {WorktreeBranchB}")
                    .Output.ShouldContain(expectedSubstrings: new[] { "commit from B" });
                GitHelpers.InvokeGitAgainstGVFSRepo(worktreePathB, $"log -1 --format=%s {WorktreeBranchA}")
                    .Output.ShouldContain(expectedSubstrings: new[] { "commit from A" });

                // 7. Remove both in parallel
                ProcessResult removeA = null;
                ProcessResult removeB = null;
                System.Threading.Tasks.Parallel.Invoke(
                    () => removeA = GitHelpers.InvokeGitAgainstGVFSRepo(
                        this.Enlistment.RepoRoot,
                        $"worktree remove --force \"{worktreePathA}\""),
                    () => removeB = GitHelpers.InvokeGitAgainstGVFSRepo(
                        this.Enlistment.RepoRoot,
                        $"worktree remove --force \"{worktreePathB}\""));

                removeA.ExitCode.ShouldEqual(0, $"worktree remove A failed: {removeA.Errors}");
                removeB.ExitCode.ShouldEqual(0, $"worktree remove B failed: {removeB.Errors}");

                // 8. Verify cleanup
                Directory.Exists(worktreePathA).ShouldBeFalse("Worktree A directory should be deleted");
                Directory.Exists(worktreePathB).ShouldBeFalse("Worktree B directory should be deleted");
            }
            finally
            {
                this.ForceCleanupWorktree(worktreePathA, WorktreeBranchA);
                this.ForceCleanupWorktree(worktreePathB, WorktreeBranchB);
            }
        }

        private void ForceCleanupWorktree(string worktreePath, string branchName)
        {
            // Best-effort cleanup for test failure cases
            try
            {
                GitHelpers.InvokeGitAgainstGVFSRepo(
                    this.Enlistment.RepoRoot,
                    $"worktree remove --force \"{worktreePath}\"");
            }
            catch
            {
            }

            if (Directory.Exists(worktreePath))
            {
                try
                {
                    // Unmount any running GVFS mount for this worktree
                    Process unmount = Process.Start("gvfs", $"unmount \"{worktreePath}\"");
                    unmount?.WaitForExit(30000);
                }
                catch
                {
                }

                try
                {
                    Directory.Delete(worktreePath, recursive: true);
                }
                catch
                {
                }
            }

            // Clean up branch
            try
            {
                GitHelpers.InvokeGitAgainstGVFSRepo(
                    this.Enlistment.RepoRoot,
                    $"branch -D {branchName}");
            }
            catch
            {
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/DiskLayoutUpgradeTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using System;
using System.IO;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase
{
    public abstract class DiskLayoutUpgradeTests : TestsWithEnlistmentPerTestCase
    {
        protected static readonly string PlaceholderListDatabaseContent = $@"A .gitignore{GVFSHelpers.PlaceholderFieldDelimiter}E9630E4CF715315FC90D4AEC98E16A7398F8BF64
A Readme.md{GVFSHelpers.PlaceholderFieldDelimiter}583F1A56DB7CC884D54534C5D9C56B93A1E00A2B
A Scripts{GVFSHelpers.PlaceholderFieldDelimiter}{TestConstants.PartialFolderPlaceholderDatabaseValue}
A Scripts{Path.DirectorySeparatorChar}RunUnitTests.bat{GVFSHelpers.PlaceholderFieldDelimiter}0112E0DD6FC64BF57C4735F4D7D6E018C0F34B6D
A GVFS{GVFSHelpers.PlaceholderFieldDelimiter}{TestConstants.PartialFolderPlaceholderDatabaseValue}
A GVFS{Path.DirectorySeparatorChar}GVFS.Common{GVFSHelpers.PlaceholderFieldDelimiter}{TestConstants.PartialFolderPlaceholderDatabaseValue}
A GVFS{Path.DirectorySeparatorChar}GVFS.Common{Path.DirectorySeparatorChar}Git{GVFSHelpers.PlaceholderFieldDelimiter}{TestConstants.PartialFolderPlaceholderDatabaseValue}
A GVFS{Path.DirectorySeparatorChar}GVFS.Common{Path.DirectorySeparatorChar}Git{Path.DirectorySeparatorChar}GitRefs.cs{GVFSHelpers.PlaceholderFieldDelimiter}37595A9C6C7E00A8AFDE306765896770F2508927
A GVFS{Path.DirectorySeparatorChar}GVFS.Tests{GVFSHelpers.PlaceholderFieldDelimiter}{TestConstants.PartialFolderPlaceholderDatabaseValue}
A GVFS{Path.DirectorySeparatorChar}GVFS.Tests{Path.DirectorySeparatorChar}Properties{GVFSHelpers.PlaceholderFieldDelimiter}{TestConstants.PartialFolderPlaceholderDatabaseValue}
A GVFS{Path.DirectorySeparatorChar}GVFS.Tests{Path.DirectorySeparatorChar}Properties{Path.DirectorySeparatorChar}AssemblyInfo.cs{GVFSHelpers.PlaceholderFieldDelimiter}5911485CFE87E880F64B300BA5A289498622DBC1
D GVFS{Path.DirectorySeparatorChar}GVFS.Tests{Path.DirectorySeparatorChar}Properties{Path.DirectorySeparatorChar}AssemblyInfo.cs
";

        protected FileSystemRunner fileSystem = new SystemIORunner();

        private const string PlaceholderTableFilePathType = "0";
        private const string PlaceholderTablePartialFolderPathType = "1";

        public abstract int GetCurrentDiskLayoutMajorVersion();
        public abstract int GetCurrentDiskLayoutMinorVersion();

        protected void PlaceholderDatabaseShouldIncludeCommonLines(string[] placeholderLines)
        {
            placeholderLines.ShouldContain(x => x.Contains(this.FilePlaceholderString("Readme.md")));
            placeholderLines.ShouldContain(x => x.Contains(this.FilePlaceholderString("Scripts", "RunUnitTests.bat")));
            placeholderLines.ShouldContain(x => x.Contains(this.FilePlaceholderString("GVFS", "GVFS.Common", "Git", "GitRefs.cs")));
            placeholderLines.ShouldContain(x => x.Contains(this.FilePlaceholderString(".gitignore")));
            placeholderLines.ShouldContain(x => x == this.PartialFolderPlaceholderString("Scripts"));
            placeholderLines.ShouldContain(x => x == this.PartialFolderPlaceholderString("GVFS"));
            placeholderLines.ShouldContain(x => x == this.PartialFolderPlaceholderString("GVFS", "GVFS.Common"));
            placeholderLines.ShouldContain(x => x == this.PartialFolderPlaceholderString("GVFS", "GVFS.Common", "Git"));
            placeholderLines.ShouldContain(x => x == this.PartialFolderPlaceholderString("GVFS", "GVFS.Tests"));
        }

        protected void WriteOldPlaceholderListDatabase()
        {
            this.fileSystem.WriteAllText(Path.Combine(this.Enlistment.DotGVFSRoot, GVFSHelpers.PlaceholderListFile), PlaceholderListDatabaseContent);
        }

        protected void PerformIOBeforePlaceholderDatabaseUpgradeTest()
        {
            // Create some placeholder data
            this.fileSystem.ReadAllText(Path.Combine(this.Enlistment.RepoRoot, "Readme.md"));
            this.fileSystem.ReadAllText(Path.Combine(this.Enlistment.RepoRoot, "Scripts", "RunUnitTests.bat"));
            this.fileSystem.ReadAllText(Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS.Common", "Git", "GitRefs.cs"));

            // Create a full folder
            this.fileSystem.CreateDirectory(Path.Combine(this.Enlistment.RepoRoot, "GVFS", "FullFolder"));
            this.fileSystem.WriteAllText(Path.Combine(this.Enlistment.RepoRoot, "GVFS", "FullFolder", "test.txt"), "Test contents");

            // Create a tombstone
            this.fileSystem.DeleteDirectory(Path.Combine(this.Enlistment.RepoRoot, "GVFS", "GVFS.Tests", "Properties"));

            string junctionTarget = Path.Combine(this.Enlistment.EnlistmentRoot, "DirJunction");
            string symLinkTarget = Path.Combine(this.Enlistment.EnlistmentRoot, "DirSymLink");
            Directory.CreateDirectory(junctionTarget);
            Directory.CreateDirectory(symLinkTarget);

            string junctionLink = Path.Combine(this.Enlistment.RepoRoot, "DirJunction");
            string symLink = Path.Combine(this.Enlistment.RepoRoot, "DirLink");
            ProcessHelper.Run("CMD.exe", "/C mklink /J " + junctionLink + " " + junctionTarget);
            ProcessHelper.Run("CMD.exe", "/C mklink /D " + symLink + " " + symLinkTarget);

            string target = Path.Combine(this.Enlistment.EnlistmentRoot, "GVFS", "GVFS", "GVFS.UnitTests");
            string link = Path.Combine(this.Enlistment.RepoRoot, "UnitTests");
            ProcessHelper.Run("CMD.exe", "/C mklink /J " + link + " " + target);
            target = Path.Combine(this.Enlistment.EnlistmentRoot, "GVFS", "GVFS", "GVFS.Installer");
            link = Path.Combine(this.Enlistment.RepoRoot, "Installer");
            ProcessHelper.Run("CMD.exe", "/C mklink /D " + link + " " + target);
        }

        protected string FilePlaceholderString(params string[] pathParts)
        {
            return $"{Path.Combine(pathParts)}{GVFSHelpers.PlaceholderFieldDelimiter}{PlaceholderTableFilePathType}{GVFSHelpers.PlaceholderFieldDelimiter}";
        }

        protected string PartialFolderPlaceholderString(params string[] pathParts)
        {
            return $"{Path.Combine(pathParts)}{GVFSHelpers.PlaceholderFieldDelimiter}{PlaceholderTablePartialFolderPathType}{GVFSHelpers.PlaceholderFieldDelimiter}{TestConstants.PartialFolderPlaceholderDatabaseValue}{GVFSHelpers.PlaceholderFieldDelimiter}";
        }

        protected void ValidatePersistedVersionMatchesCurrentVersion()
        {
            string majorVersion;
            string minorVersion;
            GVFSHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotGVFSRoot, out majorVersion, out minorVersion);

            majorVersion
                .ShouldBeAnInt("Disk layout version should always be an int")
                .ShouldEqual(this.GetCurrentDiskLayoutMajorVersion(), "Disk layout version should be upgraded to the latest");

            minorVersion
                .ShouldBeAnInt("Disk layout version should always be an int")
                .ShouldEqual(this.GetCurrentDiskLayoutMinorVersion(), "Disk layout version should be upgraded to the latest");
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/LooseObjectStepTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase
{
    [TestFixture]
    public class LooseObjectStepTests : TestsWithEnlistmentPerTestCase
    {
        private const string TempPackFolder = "tempPacks";
        private FileSystemRunner fileSystem;

        // Set forcePerRepoObjectCache to true to avoid any of the tests inadvertently corrupting
        // the cache
        public LooseObjectStepTests()
            : base(forcePerRepoObjectCache: true)
        {
            this.fileSystem = new SystemIORunner();
        }

        private string GitObjectRoot => this.Enlistment.GetObjectRoot(this.fileSystem);
        private string PackRoot => this.Enlistment.GetPackRoot(this.fileSystem);
        private string TempPackRoot => Path.Combine(this.PackRoot, TempPackFolder);

        [TestCase]
        [Category(Categories.NeedsReactionInCI)]
        public void RemoveLooseObjectsInPackFiles()
        {
            this.ClearAllObjects();

            // Copy and expand one pack
            this.ExpandOneTempPack(copyPackBackToPackDirectory: true);
            this.GetLooseObjectFiles().Count.ShouldBeAtLeast(1);
            this.CountPackFiles().ShouldEqual(1);

            // Cleanup should delete all loose objects, since they are in the packfile
            this.Enlistment.LooseObjectStep();

            this.GetLooseObjectFiles().Count.ShouldEqual(0);
            this.CountPackFiles().ShouldEqual(1);
            this.GetLooseObjectFiles().Count.ShouldEqual(0);
            this.CountPackFiles().ShouldEqual(1);
        }

        [TestCase]
        [Category(Categories.NeedsReactionInCI)]
        public void PutLooseObjectsInPackFiles()
        {
            this.ClearAllObjects();

            // Expand one pack, and verify we have loose objects
            this.ExpandOneTempPack(copyPackBackToPackDirectory: false);
            int looseObjectCount = this.GetLooseObjectFiles().Count();
            looseObjectCount.ShouldBeAtLeast(1);

            // This step should put the loose objects into a packfile
            this.Enlistment.LooseObjectStep();

            this.GetLooseObjectFiles().Count.ShouldEqual(looseObjectCount);
            this.CountPackFiles().ShouldEqual(1);

            // Running the step a second time should remove the loose obects and keep the pack file
            this.Enlistment.LooseObjectStep();

            this.GetLooseObjectFiles().Count.ShouldEqual(0);
            this.CountPackFiles().ShouldEqual(1);
        }

        [TestCase]
        public void NoLooseObjectsDoesNothing()
        {
            this.DeleteFiles(this.GetLooseObjectFiles());
            this.GetLooseObjectFiles().Count.ShouldEqual(0);
            int startingPackFileCount = this.CountPackFiles();

            this.Enlistment.LooseObjectStep();

            this.GetLooseObjectFiles().Count.ShouldEqual(0);
            this.CountPackFiles().ShouldEqual(startingPackFileCount);
        }

        [TestCase]
        [Category(Categories.NeedsReactionInCI)]
        public void CorruptLooseObjectIsDeleted()
        {
            this.ClearAllObjects();

            // Expand one pack, and verify we have loose objects
            this.ExpandOneTempPack(copyPackBackToPackDirectory: false);
            int looseObjectCount = this.GetLooseObjectFiles().Count();
            looseObjectCount.ShouldBeAtLeast(1, "Too few loose objects");

            // Create an invalid loose object
            string fakeBlobFolder = Path.Combine(this.GitObjectRoot, "00");
            string fakeBlob = Path.Combine(
                        fakeBlobFolder,
                        "01234567890123456789012345678901234567");
            this.fileSystem.CreateDirectory(fakeBlobFolder);
            this.fileSystem.CreateEmptyFile(fakeBlob);

            // This step should fail to place the objects, but
            // succeed in deleting the given file.
            this.Enlistment.LooseObjectStep();

            this.fileSystem.FileExists(fakeBlob).ShouldBeFalse(
                   "Step failed to delete corrupt blob");
            this.CountPackFiles().ShouldEqual(0, "Incorrect number of packs after first loose object step");
            this.GetLooseObjectFiles().Count.ShouldEqual(
                looseObjectCount,
                "unexpected number of loose objects after step");

            // This step should create a pack.
            this.Enlistment.LooseObjectStep();

            this.CountPackFiles().ShouldEqual(1, "Incorrect number of packs after second loose object step");
            this.GetLooseObjectFiles().Count.ShouldEqual(looseObjectCount);

            // This step should delete the loose objects
            this.Enlistment.LooseObjectStep();

            this.GetLooseObjectFiles().Count.ShouldEqual(0, "Incorrect number of loose objects after third loose object step");
        }

        private void ClearAllObjects()
        {
            this.Enlistment.UnmountGVFS();

            // Delete/Move any starting loose objects and packfiles
            this.DeleteFiles(this.GetLooseObjectFiles());
            this.MovePackFilesToTemp();
            this.GetLooseObjectFiles().Count.ShouldEqual(0, "incorrect number of loose objects after setup");
            this.CountPackFiles().ShouldEqual(0, "incorrect number of packs after setup");
        }

        private List GetLooseObjectFiles()
        {
            List looseObjectFiles = new List();
            foreach (string directory in Directory.GetDirectories(this.GitObjectRoot))
            {
                // Check if the directory is 2 letter HEX
                if (Regex.IsMatch(directory, @"[/\\][0-9a-fA-F]{2}$"))
                {
                    string[] files = Directory.GetFiles(directory);
                    looseObjectFiles.AddRange(files);
                }
            }

            return looseObjectFiles;
        }

        private void DeleteFiles(List filePaths)
        {
            foreach (string filePath in filePaths)
            {
                File.Delete(filePath);
            }
        }

        private int CountPackFiles()
        {
            return Directory.GetFiles(this.PackRoot, "*.pack").Length;
        }

        private void MovePackFilesToTemp()
        {
            string[] files = Directory.GetFiles(this.PackRoot);
            foreach (string file in files)
            {
                string path2 = Path.Combine(this.TempPackRoot, Path.GetFileName(file));

                File.Move(file, path2);
            }
        }

        private void ExpandOneTempPack(bool copyPackBackToPackDirectory)
        {
            // Find all pack files
            string[] packFiles = Directory.GetFiles(this.TempPackRoot, "pack-*.pack");
            Assert.Greater(packFiles.Length, 0);

            // Pick the first one found
            string packFile = packFiles[0];

            // Send the contents of the packfile to unpack-objects to example the loose objects
            // Note this won't work if the object exists in a pack file which is why we had to move them
            using (FileStream packFileStream = File.OpenRead(packFile))
            {
                string output = GitProcess.InvokeProcess(
                    this.Enlistment.RepoBackingRoot,
                    "unpack-objects",
                    new Dictionary() { { "GIT_OBJECT_DIRECTORY", this.GitObjectRoot } },
                    inputStream: packFileStream).Output;
            }

            if (copyPackBackToPackDirectory)
            {
                // Copy the pack file back to packs
                string packFileName = Path.GetFileName(packFile);
                File.Copy(packFile, Path.Combine(this.PackRoot, packFileName));

                // Replace the '.pack' with '.idx' to copy the index file
                string packFileIndexName = packFileName.Replace(".pack", ".idx");
                File.Copy(Path.Combine(this.TempPackRoot, packFileIndexName), Path.Combine(this.PackRoot, packFileIndexName));
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/ModifiedPathsTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.IO;
using System.Linq;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase
{
    [TestFixture]
    public class ModifiedPathsTests : TestsWithEnlistmentPerTestCase
    {
        private static readonly string FileToAdd = Path.Combine("GVFS", "TestAddFile.txt");
        private static readonly string FileToUpdate = Path.Combine("GVFS", "GVFS", "Program.cs");
        private static readonly string FileToDelete = "Readme.md";
        private static readonly string FileToRename = Path.Combine("GVFS", "GVFS.Mount", "MountVerb.cs");
        private static readonly string RenameFileTarget = Path.Combine("GVFS", "GVFS.Mount", "MountVerb2.cs");
        private static readonly string FolderToCreate = $"{nameof(ModifiedPathsTests)}_NewFolder";
        private static readonly string FolderToRename = $"{nameof(ModifiedPathsTests)}_NewFolderForRename";
        private static readonly string RenameFolderTarget = $"{nameof(ModifiedPathsTests)}_NewFolderForRename2";
        private static readonly string DotGitFileToCreate = Path.Combine(".git", "TestFileFromDotGit.txt");
        private static readonly string RenameNewDotGitFileTarget = "TestFileFromDotGit.txt";
        private static readonly string FileToCreateOutsideRepo = $"{nameof(ModifiedPathsTests)}_outsideRepo.txt";
        private static readonly string FolderToCreateOutsideRepo = $"{nameof(ModifiedPathsTests)}_outsideFolder";
        private static readonly string FolderToDelete = "Scripts";

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void DeletedTempFileIsRemovedFromModifiedFiles(FileSystemRunner fileSystem)
        {
            string tempFile = this.CreateFile(fileSystem, "temp.txt");
            fileSystem.DeleteFile(tempFile);
            tempFile.ShouldNotExistOnDisk(fileSystem);

            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, fileSystem, "temp.txt");
        }

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void DeletedTempFolderIsRemovedFromModifiedFiles(FileSystemRunner fileSystem)
        {
            string tempFolder = this.CreateDirectory(fileSystem, "Temp");
            fileSystem.DeleteDirectory(tempFolder);
            tempFolder.ShouldNotExistOnDisk(fileSystem);

            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, fileSystem, "Temp/");
        }

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void DeletedTempFolderDeletesFilesFromModifiedFiles(FileSystemRunner fileSystem)
        {
            string tempFolder = this.CreateDirectory(fileSystem, "Temp");
            string tempFile1 = this.CreateFile(fileSystem, Path.Combine("Temp", "temp1.txt"));
            string tempFile2 = this.CreateFile(fileSystem, Path.Combine("Temp", "temp2.txt"));
            fileSystem.DeleteDirectory(tempFolder);
            tempFolder.ShouldNotExistOnDisk(fileSystem);
            tempFile1.ShouldNotExistOnDisk(fileSystem);
            tempFile2.ShouldNotExistOnDisk(fileSystem);

            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, fileSystem, "Temp/", "Temp/temp1.txt", "Temp/temp2.txt");
        }

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void ModifiedPathsFromChangesInsideRepoSavedAfterRemount(FileSystemRunner fileSystem)
        {
            string[] expectedModifiedFilesContentsAfterRemount =
                {
                    @"A .gitattributes",
                    $"A {GVFSHelpers.ConvertPathToGitFormat(FileToAdd)}",
                    $"A {GVFSHelpers.ConvertPathToGitFormat(FileToUpdate)}",
                    $"A {FileToDelete}",
                    $"A {GVFSHelpers.ConvertPathToGitFormat(FileToRename)}",
                    $"A {GVFSHelpers.ConvertPathToGitFormat(RenameFileTarget)}",
                    $"A {FolderToCreate}/",
                    $"A {RenameNewDotGitFileTarget}",
                    $"A {FolderToDelete}/",
                };

            string fileToAdd = this.Enlistment.GetVirtualPathTo(FileToAdd);
            fileSystem.WriteAllText(fileToAdd, "Contents for the new file");

            string fileToUpdate = this.Enlistment.GetVirtualPathTo(FileToUpdate);
            fileSystem.AppendAllText(fileToUpdate, "// Testing");

            string fileToDelete = this.Enlistment.GetVirtualPathTo(FileToDelete);
            fileSystem.DeleteFile(fileToDelete);
            fileToDelete.ShouldNotExistOnDisk(fileSystem);

            string fileToRename = this.Enlistment.GetVirtualPathTo(FileToRename);
            fileSystem.MoveFile(fileToRename, this.Enlistment.GetVirtualPathTo(RenameFileTarget));

            string folderToCreate = this.Enlistment.GetVirtualPathTo(FolderToCreate);
            fileSystem.CreateDirectory(folderToCreate);

            string folderToRename = this.Enlistment.GetVirtualPathTo(FolderToRename);
            fileSystem.CreateDirectory(folderToRename);
            string folderToRenameTarget = this.Enlistment.GetVirtualPathTo(RenameFolderTarget);
            fileSystem.MoveDirectory(folderToRename, folderToRenameTarget);

            // Deleting the new folder will remove it from the modified paths file
            fileSystem.DeleteDirectory(folderToRenameTarget);
            folderToRenameTarget.ShouldNotExistOnDisk(fileSystem);

            // Moving a file from the .git folder to the working directory should add the file to the modified paths
            string dotGitfileToAdd = this.Enlistment.GetVirtualPathTo(DotGitFileToCreate);
            fileSystem.WriteAllText(dotGitfileToAdd, "Contents for the new file in dot git");
            fileSystem.MoveFile(dotGitfileToAdd, this.Enlistment.GetVirtualPathTo(RenameNewDotGitFileTarget));

            string folderToDeleteFullPath = this.Enlistment.GetVirtualPathTo(FolderToDelete);
            fileSystem.WriteAllText(Path.Combine(folderToDeleteFullPath, "NewFile.txt"), "Contents for new file");
            string newFileToDelete = Path.Combine(folderToDeleteFullPath, "NewFileToDelete.txt");
            fileSystem.WriteAllText(newFileToDelete, "Contents for new file");
            fileSystem.DeleteFile(newFileToDelete);
            fileSystem.WriteAllText(Path.Combine(folderToDeleteFullPath, "CreateCommonVersionHeader.bat"), "Changing the file contents");
            fileSystem.DeleteFile(Path.Combine(folderToDeleteFullPath, "RunUnitTests.bat"));

            fileSystem.DeleteDirectory(folderToDeleteFullPath);
            folderToDeleteFullPath.ShouldNotExistOnDisk(fileSystem);

            // Remount
            this.Enlistment.UnmountGVFS();
            this.Enlistment.MountGVFS();

            this.Enlistment.WaitForBackgroundOperations();

            string modifiedPathsDatabase = Path.Combine(this.Enlistment.DotGVFSRoot, TestConstants.Databases.ModifiedPaths);
            modifiedPathsDatabase.ShouldBeAFile(fileSystem);
            using (StreamReader reader = new StreamReader(File.Open(modifiedPathsDatabase, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
            {
                reader.ReadToEnd().Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).OrderBy(x => x)
                    .ShouldMatchInOrder(expectedModifiedFilesContentsAfterRemount.OrderBy(x => x));
            }
        }

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void ModifiedPathsFromRenamingOutsideRepoSavedAfterRemount(FileSystemRunner fileSystem)
        {
            string[] expectedModifiedFilesContentsAfterRemount =
                {
                    @"A .gitattributes",
                    $"A {FileToCreateOutsideRepo}",
                    $"A {FolderToCreateOutsideRepo}/",
                };

            string folderToRename = this.Enlistment.GetVirtualPathTo(FolderToRename);
            fileSystem.CreateDirectory(folderToRename);
            string folderToRenameTarget = this.Enlistment.GetVirtualPathTo(RenameFolderTarget);
            fileSystem.MoveDirectory(folderToRename, folderToRenameTarget);

            // Moving the new folder out of the repo will remove it from the modified paths file
            string folderTargetOutsideSrc = Path.Combine(this.Enlistment.EnlistmentRoot, RenameFolderTarget);
            folderTargetOutsideSrc.ShouldNotExistOnDisk(fileSystem);
            fileSystem.MoveDirectory(folderToRenameTarget, folderTargetOutsideSrc);
            folderTargetOutsideSrc.ShouldBeADirectory(fileSystem);
            folderToRenameTarget.ShouldNotExistOnDisk(fileSystem);

            // Move a file from outside of src into src
            string fileToCreateOutsideRepoPath = Path.Combine(this.Enlistment.EnlistmentRoot, FileToCreateOutsideRepo);
            fileSystem.WriteAllText(fileToCreateOutsideRepoPath, "Contents for the new file outside of repo");
            string fileToCreateOutsideRepoTargetPath = this.Enlistment.GetVirtualPathTo(FileToCreateOutsideRepo);
            fileToCreateOutsideRepoTargetPath.ShouldNotExistOnDisk(fileSystem);
            fileSystem.MoveFile(fileToCreateOutsideRepoPath, fileToCreateOutsideRepoTargetPath);
            fileToCreateOutsideRepoTargetPath.ShouldBeAFile(fileSystem);
            fileToCreateOutsideRepoPath.ShouldNotExistOnDisk(fileSystem);

            // Move a folder from outside of src into src
            string folderToCreateOutsideRepoPath = Path.Combine(this.Enlistment.EnlistmentRoot, FolderToCreateOutsideRepo);
            fileSystem.CreateDirectory(folderToCreateOutsideRepoPath);
            folderToCreateOutsideRepoPath.ShouldBeADirectory(fileSystem);
            string folderToCreateOutsideRepoTargetPath = this.Enlistment.GetVirtualPathTo(FolderToCreateOutsideRepo);
            folderToCreateOutsideRepoTargetPath.ShouldNotExistOnDisk(fileSystem);
            fileSystem.MoveDirectory(folderToCreateOutsideRepoPath, folderToCreateOutsideRepoTargetPath);
            folderToCreateOutsideRepoTargetPath.ShouldBeADirectory(fileSystem);
            folderToCreateOutsideRepoPath.ShouldNotExistOnDisk(fileSystem);

            // Remount
            this.Enlistment.UnmountGVFS();
            this.Enlistment.MountGVFS();

            this.Enlistment.WaitForBackgroundOperations();

            string modifiedPathsDatabase = Path.Combine(this.Enlistment.DotGVFSRoot, TestConstants.Databases.ModifiedPaths);
            modifiedPathsDatabase.ShouldBeAFile(fileSystem);
            using (StreamReader reader = new StreamReader(File.Open(modifiedPathsDatabase, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
            {
                reader.ReadToEnd().Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).OrderBy(x => x)
                    .ShouldMatchInOrder(expectedModifiedFilesContentsAfterRemount.OrderBy(x => x));
            }
        }

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void ModifiedPathsCorrectAfterHardLinkingInsideRepo(FileSystemRunner fileSystem)
        {
            string[] expectedModifiedFilesContentsAfterHardlinks =
                {
                    "A .gitattributes",
                    "A LinkToReadme.md",
                    "A Readme.md",
                };

            // Create a link from src\LinkToReadme.md to src\Readme.md
            string existingFileInRepoPath = this.Enlistment.GetVirtualPathTo("Readme.md");
            string contents = existingFileInRepoPath.ShouldBeAFile(fileSystem).WithContents();
            string hardLinkToFileInRepoPath = this.Enlistment.GetVirtualPathTo("LinkToReadme.md");
            hardLinkToFileInRepoPath.ShouldNotExistOnDisk(fileSystem);
            fileSystem.CreateHardLink(hardLinkToFileInRepoPath, existingFileInRepoPath);
            hardLinkToFileInRepoPath.ShouldBeAFile(fileSystem).WithContents(contents);

            this.Enlistment.WaitForBackgroundOperations();

            string modifiedPathsDatabase = Path.Combine(this.Enlistment.DotGVFSRoot, TestConstants.Databases.ModifiedPaths);
            modifiedPathsDatabase.ShouldBeAFile(fileSystem);
            using (StreamReader reader = new StreamReader(File.Open(modifiedPathsDatabase, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
            {
                reader.ReadToEnd().Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).OrderBy(x => x)
                    .ShouldMatchInOrder(expectedModifiedFilesContentsAfterHardlinks.OrderBy(x => x));
            }
        }

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void ModifiedPathsCorrectAfterHardLinkingOutsideRepo(FileSystemRunner fileSystem)
        {
            string[] expectedModifiedFilesContentsAfterHardlinks =
                {
                    "A .gitattributes",
                    "A LinkToFileOutsideSrc.txt",
                    "A GVFS/GVFS/Program.cs",
                };

            // Create a link from src\LinkToFileOutsideSrc.txt to FileOutsideRepo.txt
            string fileOutsideOfRepoPath = Path.Combine(this.Enlistment.EnlistmentRoot, "FileOutsideRepo.txt");
            string fileOutsideOfRepoContents = "File outside of repo";
            fileOutsideOfRepoPath.ShouldNotExistOnDisk(fileSystem);
            fileSystem.WriteAllText(fileOutsideOfRepoPath, fileOutsideOfRepoContents);
            string hardLinkToFileOutsideRepoPath = this.Enlistment.GetVirtualPathTo("LinkToFileOutsideSrc.txt");
            hardLinkToFileOutsideRepoPath.ShouldNotExistOnDisk(fileSystem);
            fileSystem.CreateHardLink(hardLinkToFileOutsideRepoPath, fileOutsideOfRepoPath);
            hardLinkToFileOutsideRepoPath.ShouldBeAFile(fileSystem).WithContents(fileOutsideOfRepoContents);

            // Create a link from LinkOutsideSrcToInsideSrc.cs to src\GVFS\GVFS\Program.cs
            string secondFileInRepoPath = this.Enlistment.GetVirtualPathTo("GVFS", "GVFS", "Program.cs");
            string contents = secondFileInRepoPath.ShouldBeAFile(fileSystem).WithContents();
            string hardLinkOutsideRepoToFileInRepoPath = Path.Combine(this.Enlistment.EnlistmentRoot, "LinkOutsideSrcToInsideSrc.cs");
            hardLinkOutsideRepoToFileInRepoPath.ShouldNotExistOnDisk(fileSystem);
            fileSystem.CreateHardLink(hardLinkOutsideRepoToFileInRepoPath, secondFileInRepoPath);
            hardLinkOutsideRepoToFileInRepoPath.ShouldBeAFile(fileSystem).WithContents(contents);

            this.Enlistment.WaitForBackgroundOperations();

            string modifiedPathsDatabase = Path.Combine(this.Enlistment.DotGVFSRoot, TestConstants.Databases.ModifiedPaths);
            modifiedPathsDatabase.ShouldBeAFile(fileSystem);
            using (StreamReader reader = new StreamReader(File.Open(modifiedPathsDatabase, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
            {
                reader.ReadToEnd().Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).OrderBy(x => x)
                    .ShouldMatchInOrder(expectedModifiedFilesContentsAfterHardlinks.OrderBy(x => x));
            }
        }

        private string CreateDirectory(FileSystemRunner fileSystem, string relativePath)
        {
            string tempFolder = this.Enlistment.GetVirtualPathTo(relativePath);
            fileSystem.CreateDirectory(tempFolder);
            tempFolder.ShouldBeADirectory(fileSystem);
            return tempFolder;
        }

        private string CreateFile(FileSystemRunner fileSystem, string relativePath)
        {
            string tempFile = this.Enlistment.GetVirtualPathTo(relativePath);
            fileSystem.WriteAllText(tempFile, $"Contents for the {relativePath} file");
            tempFile.ShouldBeAFile(fileSystem);
            return tempFile;
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PersistedWorkingDirectoryTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.Collections.Generic;
using System.IO;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase
{
    [TestFixture]
    [Category(Categories.ExtraCoverage)]
    public class PersistedWorkingDirectoryTests : TestsWithEnlistmentPerTestCase
    {
        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void PersistedDirectoryLazyLoad(FileSystemRunner fileSystem)
        {
            string enumerateDirectoryName = Path.Combine("GVFS", "GVFS");

            string[] subFolders = new string[]
            {
                Path.Combine(enumerateDirectoryName, "Properties"),
                Path.Combine(enumerateDirectoryName, "CommandLine")
            };

            string[] subFiles = new string[]
            {
                Path.Combine(enumerateDirectoryName, "App.config"),
                Path.Combine(enumerateDirectoryName, "GitVirtualFileSystem.ico"),
                Path.Combine(enumerateDirectoryName, "GVFS.csproj"),
                Path.Combine(enumerateDirectoryName, "packages.config"),
                Path.Combine(enumerateDirectoryName, "Program.cs"),
                Path.Combine(enumerateDirectoryName, "Setup.iss")
            };

            string enumerateDirectoryPath = this.Enlistment.GetVirtualPathTo(enumerateDirectoryName);
            fileSystem.DirectoryExists(enumerateDirectoryPath).ShouldEqual(true);

            foreach (string folder in subFolders)
            {
                string directoryPath = this.Enlistment.GetVirtualPathTo(folder);
                fileSystem.DirectoryExists(directoryPath).ShouldEqual(true);
            }

            foreach (string file in subFiles)
            {
                string filePath = this.Enlistment.GetVirtualPathTo(file);
                fileSystem.FileExists(filePath).ShouldEqual(true);
            }

            this.Enlistment.UnmountGVFS();
            this.Enlistment.MountGVFS();

            foreach (string folder in subFolders)
            {
                string directoryPath = this.Enlistment.GetVirtualPathTo(folder);
                fileSystem.DirectoryExists(directoryPath).ShouldEqual(true);
            }

            foreach (string file in subFiles)
            {
                string filePath = this.Enlistment.GetVirtualPathTo(file);
                fileSystem.FileExists(filePath).ShouldEqual(true);
            }
        }

        /// 
        /// This test is intentionally one monolithic test. Because we have to mount/remount to
        /// test persistence, we want to save as much time in tests runs as possible by only
        /// remounting once.
        /// 
        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void PersistedDirectoryTests(FileSystemRunner fileSystem)
        {
            // Delete File Setup
            string deleteFileName = ".gitattributes";
            string deleteFilepath = this.Enlistment.GetVirtualPathTo(deleteFileName);
            fileSystem.DeleteFile(deleteFilepath);

            // Delete Folder Setup
            string deleteFolderName = Path.Combine("GVFS", "GVFS");
            string deleteFolderPath = this.Enlistment.GetVirtualPathTo(deleteFolderName);
            fileSystem.DeleteDirectory(deleteFolderPath);

            // Add File Setup
            string fileToAdd = "NewFile.txt";
            string fileToAddContent = "This is new file text.";
            string fileToAddPath = this.Enlistment.GetVirtualPathTo(fileToAdd);
            fileSystem.WriteAllText(fileToAddPath, fileToAddContent);

            // Add Folder Setup
            string directoryToAdd = "NewDirectory";
            string directoryToAddPath = this.Enlistment.GetVirtualPathTo(directoryToAdd);
            fileSystem.CreateDirectory(directoryToAddPath);

            // Move File Setup
            string fileToMove = this.Enlistment.GetVirtualPathTo("FileToMove.txt");
            string fileToMoveNewPath = this.Enlistment.GetVirtualPathTo("MovedFile.txt");
            string fileToMoveContent = "This is new file text.";
            fileSystem.WriteAllText(fileToMove, fileToMoveContent);
            fileSystem.MoveFile(fileToMove, fileToMoveNewPath);

            // Replace File Setup
            string fileToReplace = this.Enlistment.GetVirtualPathTo("FileToReplace.txt");
            string fileToReplaceNewPath = this.Enlistment.GetVirtualPathTo("ReplacedFile.txt");
            string fileToReplaceContent = "This is new file text.";
            string fileToReplaceOldContent = "This is very different file text.";
            fileSystem.WriteAllText(fileToReplace, fileToReplaceContent);
            fileSystem.WriteAllText(fileToReplaceNewPath, fileToReplaceOldContent);
            fileSystem.ReplaceFile(fileToReplace, fileToReplaceNewPath);

            // MoveFolderPersistsOnRemount Setup
            string directoryToMove = this.Enlistment.GetVirtualPathTo("MoveDirectory");
            string directoryMoveTarget = this.Enlistment.GetVirtualPathTo("MoveDirectoryTarget");
            string newDirectory = Path.Combine(directoryMoveTarget, "MoveDirectory_renamed");
            string childFile = Path.Combine(directoryToMove, "MoveFile.txt");
            string movedChildFile = Path.Combine(newDirectory, "MoveFile.txt");
            string moveFileContents = "This text file is getting moved";
            fileSystem.CreateDirectory(directoryToMove);
            fileSystem.CreateDirectory(directoryMoveTarget);
            fileSystem.WriteAllText(childFile, moveFileContents);
            fileSystem.MoveDirectory(directoryToMove, newDirectory);

            // NestedLoadAndWriteAfterMount Setup
            // Write a file to GVFS to ensure it has a physical folder
            string childFileToAdd = Path.Combine("GVFS", "ChildFileToAdd.txt");
            string childFileToAddContent = "This is new child file in the GVFS folder.";
            string childFileToAddPath = this.Enlistment.GetVirtualPathTo(childFileToAdd);
            fileSystem.WriteAllText(childFileToAddPath, childFileToAddContent);

            // Remount
            this.Enlistment.UnmountGVFS();
            this.Enlistment.MountGVFS();

            // Delete File Validation
            deleteFilepath.ShouldNotExistOnDisk(fileSystem);

            // Delete Folder Validation
            deleteFolderPath.ShouldNotExistOnDisk(fileSystem);

            // Add File Validation
            fileToAddPath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(fileToAddContent);

            // Add Folder Validation
            directoryToAddPath.ShouldBeADirectory(fileSystem);

            // Move File Validation
            fileToMove.ShouldNotExistOnDisk(fileSystem);
            fileToMoveNewPath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(fileToMoveContent);

            // Replace File Validation
            fileToReplace.ShouldNotExistOnDisk(fileSystem);
            fileToReplaceNewPath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(fileToReplaceContent);

            // MoveFolderPersistsOnRemount Validation
            directoryToMove.ShouldNotExistOnDisk(fileSystem);

            directoryMoveTarget.ShouldBeADirectory(fileSystem);
            newDirectory.ShouldBeADirectory(fileSystem);
            movedChildFile.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(moveFileContents);

            // NestedLoadAndWriteAfterMount Validation
            childFileToAddPath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(childFileToAddContent);
            string childFolder = Path.Combine("GVFS", "GVFS.FunctionalTests");
            string childFolderPath = this.Enlistment.GetVirtualPathTo(childFolder);
            childFolderPath.ShouldBeADirectory(fileSystem);
            string postMountChildFile = "PostMountChildFile.txt";
            string postMountChildFileContent = "This is new child file added after the mount";
            string postMountChildFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(childFolder, postMountChildFile));
            fileSystem.WriteAllText(postMountChildFilePath, postMountChildFileContent); // Verify we can create files in subfolders of GVFS
            postMountChildFilePath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(postMountChildFileContent);

            // 663045 - Ensure that folder can be deleted after a new file is added and GVFS is remounted
            fileSystem.DeleteDirectory(childFolderPath);
            childFolderPath.ShouldNotExistOnDisk(fileSystem);
        }
    }
}

================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/RepairTests.cs
================================================
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase
{
    [TestFixture]
    [Category(Categories.ExtraCoverage)]
    public class RepairTests : TestsWithEnlistmentPerTestCase
    {
        [OneTimeSetUp]
        public void TurnOfflineIOOn()
        {
            GVFSHelpers.RegisterForOfflineIO();
        }

        [OneTimeTearDown]
        public void TurnOfflineIOOff()
        {
            GVFSHelpers.UnregisterForOfflineIO();
        }

        [TestCase]
        public void NoFixesNeeded()
        {
            this.Enlistment.UnmountGVFS();
            this.Enlistment.Repair(confirm: false);
            this.Enlistment.Repair(confirm: true);
        }

        [TestCase]
        public void FixesCorruptHeadSha()
        {
            this.Enlistment.UnmountGVFS();

            string headFilePath = Path.Combine(this.Enlistment.RepoBackingRoot, ".git", "HEAD");
            File.WriteAllText(headFilePath, "0000");
            this.Enlistment.TryMountGVFS().ShouldEqual(false, "GVFS shouldn't mount when HEAD is corrupt");

            this.RepairWithoutConfirmShouldNotFix();

            this.RepairWithConfirmShouldFix();
        }

        [TestCase]
        public void FixesCorruptHeadSymRef()
        {
            this.Enlistment.UnmountGVFS();

            string headFilePath = Path.Combine(this.Enlistment.RepoBackingRoot, ".git", "HEAD");
            File.WriteAllText(headFilePath, "ref: refs");
            this.Enlistment.TryMountGVFS().ShouldEqual(false, "GVFS shouldn't mount when HEAD is corrupt");

            this.RepairWithoutConfirmShouldNotFix();

            this.RepairWithConfirmShouldFix();
        }

        [TestCase]
        public void FixesMissingGitIndex()
        {
            this.Enlistment.UnmountGVFS();

            string gitIndexPath = Path.Combine(this.Enlistment.RepoBackingRoot, ".git", "index");
            File.Delete(gitIndexPath);
            this.Enlistment.TryMountGVFS().ShouldEqual(false, "GVFS shouldn't mount when git index is missing");

            this.RepairWithoutConfirmShouldNotFix();

            this.RepairWithConfirmShouldFix();
        }

        [TestCase]
        public void FixesGitIndexCorruptedWithBadData()
        {
            this.Enlistment.UnmountGVFS();

            string gitIndexPath = Path.Combine(this.Enlistment.RepoBackingRoot, ".git", "index");
            this.CreateCorruptIndexAndRename(
                gitIndexPath,
                (current, temp) =>
                {
                    byte[] badData = Encoding.ASCII.GetBytes("BAD_INDEX");
                    temp.Write(badData, 0, badData.Length);
                });

            string output;
            this.Enlistment.TryMountGVFS(out output).ShouldEqual(false, "GVFS shouldn't mount when index is corrupt");
            output.ShouldContain("Index validation failed");

            this.RepairWithoutConfirmShouldNotFix();

            this.RepairWithConfirmShouldFix();
        }

        [TestCase]
        public void FixesGitIndexContainingAllNulls()
        {
            this.Enlistment.UnmountGVFS();

            string gitIndexPath = Path.Combine(this.Enlistment.RepoBackingRoot, ".git", "index");

            // Set the contents of the index file to gitIndexPath NULL
            this.CreateCorruptIndexAndRename(
                gitIndexPath,
                (current, temp) =>
                {
                    temp.Write(Enumerable.Repeat(0, (int)current.Length).ToArray(), 0, (int)current.Length);
                });

            string output;
            this.Enlistment.TryMountGVFS(out output).ShouldEqual(false, "GVFS shouldn't mount when index is corrupt");
            output.ShouldContain("Index validation failed");

            this.RepairWithoutConfirmShouldNotFix();

            this.RepairWithConfirmShouldFix();
        }

        [TestCase]
        public void FixesGitIndexCorruptedByTruncation()
        {
            this.Enlistment.UnmountGVFS();

            string gitIndexPath = Path.Combine(this.Enlistment.RepoBackingRoot, ".git", "index");

            // Truncate the contents of the index
            this.CreateCorruptIndexAndRename(
                gitIndexPath,
                (current, temp) =>
                {
                    // 20 will truncate the file in the middle of the first entry in the index
                    byte[] currentStartOfIndex = new byte[20];
                    current.Read(currentStartOfIndex, 0, currentStartOfIndex.Length);
                    temp.Write(currentStartOfIndex, 0, currentStartOfIndex.Length);
                });

            string output;
            this.Enlistment.TryMountGVFS(out output).ShouldEqual(false, "GVFS shouldn't mount when index is corrupt");
            output.ShouldContain("Index validation failed");

            this.RepairWithoutConfirmShouldNotFix();

            this.RepairWithConfirmShouldFix();
        }

        [TestCase]
        public void FixesCorruptGitConfig()
        {
            this.Enlistment.UnmountGVFS();

            string gitIndexPath = Path.Combine(this.Enlistment.RepoBackingRoot, ".git", "config");
            File.WriteAllText(gitIndexPath, "[cor");

            this.Enlistment.TryMountGVFS().ShouldEqual(false, "GVFS shouldn't mount when git config is missing");

            this.RepairWithoutConfirmShouldNotFix();

            this.Enlistment.Repair(confirm: true);
            ProcessResult result = GitProcess.InvokeProcess(this.Enlistment.RepoBackingRoot, "remote add origin " + this.Enlistment.RepoUrl);
            result.ExitCode.ShouldEqual(0, result.Errors);
            this.Enlistment.MountGVFS();
        }

        private void CreateCorruptIndexAndRename(string indexPath, Action corruptionAction)
        {
            string tempIndexPath = indexPath + ".lock";
            using (FileStream currentIndexStream = new FileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (FileStream tempIndexStream = new FileStream(tempIndexPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite))
            {
                corruptionAction(currentIndexStream, tempIndexStream);
            }

            File.Delete(indexPath);
            File.Move(tempIndexPath, indexPath);
        }

        private void RepairWithConfirmShouldFix()
        {
            this.Enlistment.Repair(confirm: true);
            this.Enlistment.MountGVFS();
        }

        private void RepairWithoutConfirmShouldNotFix()
        {
            this.Enlistment.Repair(confirm: false);
            this.Enlistment.TryMountGVFS().ShouldEqual(false, "Repair without confirm should not fix the enlistment");
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/TestsWithEnlistmentPerTestCase.cs
================================================
using GVFS.FunctionalTests.Tools;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase
{
    [TestFixture]
    public abstract class TestsWithEnlistmentPerTestCase
    {
        private readonly bool forcePerRepoObjectCache;

        public TestsWithEnlistmentPerTestCase(bool forcePerRepoObjectCache = false)
        {
            this.forcePerRepoObjectCache = forcePerRepoObjectCache;
        }

        public GVFSFunctionalTestEnlistment Enlistment
        {
            get; private set;
        }

        [SetUp]
        public virtual void CreateEnlistment()
        {
            if (this.forcePerRepoObjectCache)
            {
                this.Enlistment = GVFSFunctionalTestEnlistment.CloneAndMountWithPerRepoCache(GVFSTestConfig.PathToGVFS, skipPrefetch: false);
            }
            else
            {
                this.Enlistment = GVFSFunctionalTestEnlistment.CloneAndMount(GVFSTestConfig.PathToGVFS);
            }
        }

        [TearDown]
        public virtual void DeleteEnlistment()
        {
            if (this.Enlistment != null)
            {
                this.Enlistment.UnmountAndDeleteAll();
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/FastFetchTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespace GVFS.FunctionalTests.Tests
{
    [TestFixture]
    [Category(Categories.FastFetch)]
    [Category(Categories.ExtraCoverage)]
    public class FastFetchTests
    {
        private const string LsTreeTypeInPathBranchName = "FunctionalTests/20181105_LsTreeTypeInPath";

        private readonly string fastFetchRepoRoot = Settings.Default.FastFetchRoot;
        private readonly string fastFetchControlRoot = Settings.Default.FastFetchControl;
        private readonly string fastFetchBaseRoot = Settings.Default.FastFetchBaseRoot;

        [OneTimeSetUp]
        public void InitControlRepo()
        {
            Directory.CreateDirectory(this.fastFetchControlRoot);
            GitProcess.Invoke(this.fastFetchBaseRoot, "clone -b " + Settings.Default.Commitish + " " + GVFSTestConfig.RepoToClone + " " + this.fastFetchControlRoot);
        }

        [SetUp]
        public void InitRepo()
        {
            // Just in case Teardown did not run.  Say when debugging...
            if (Directory.Exists(this.fastFetchRepoRoot))
            {
                this.TearDownTests();
            }

            Directory.CreateDirectory(this.fastFetchRepoRoot);
            GitProcess.Invoke(this.fastFetchRepoRoot, "init");
            GitProcess.Invoke(this.fastFetchRepoRoot, "remote add origin " + GVFSTestConfig.RepoToClone);
        }

        [TearDown]
        public void TearDownTests()
        {
            RepositoryHelpers.DeleteTestDirectory(this.fastFetchRepoRoot);
        }

        [OneTimeTearDown]
        public void DeleteControlRepo()
        {
            RepositoryHelpers.DeleteTestDirectory(this.fastFetchControlRoot);
        }

        [TestCase]
        public void CanFetchIntoEmptyGitRepoAndCheckoutWithGit()
        {
            this.RunFastFetch("-b " + Settings.Default.Commitish);

            this.GetRefTreeSha("remotes/origin/" + Settings.Default.Commitish).ShouldNotBeNull();

            ProcessResult checkoutResult = GitProcess.InvokeProcess(this.fastFetchRepoRoot, "checkout " + Settings.Default.Commitish);
            checkoutResult.Errors.ShouldEqual("Switched to a new branch '" + Settings.Default.Commitish + "'\r\n");
            checkoutResult.Output.ShouldEqual("Branch '" + Settings.Default.Commitish + "' set up to track remote branch '" + Settings.Default.Commitish + "' from 'origin'.\n");

            // When checking out with git, must manually update shallow.
            ProcessResult updateRefResult = GitProcess.InvokeProcess(this.fastFetchRepoRoot, "update-ref shallow " + Settings.Default.Commitish);
            updateRefResult.ExitCode.ShouldEqual(0);
            updateRefResult.Errors.ShouldBeEmpty();
            updateRefResult.Output.ShouldBeEmpty();

            this.CurrentBranchShouldEqual(Settings.Default.Commitish);

            this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner)
                .WithDeepStructure(FileSystemRunner.DefaultRunner, this.fastFetchControlRoot);
        }

        [TestCase]
        public void CanFetchAndCheckoutASingleFolderIntoEmptyGitRepo()
        {
            this.RunFastFetch("--checkout --folders \"/GVFS\" -b " + Settings.Default.Commitish);

            this.CurrentBranchShouldEqual(Settings.Default.Commitish);

            this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner);
            List dirs = Directory.EnumerateFileSystemEntries(this.fastFetchRepoRoot).ToList();
            dirs.SequenceEqual(new[]
            {
                Path.Combine(this.fastFetchRepoRoot, ".git"),
                Path.Combine(this.fastFetchRepoRoot, "GVFS"),
                Path.Combine(this.fastFetchRepoRoot, "GVFS.sln")
            });

            Directory.EnumerateFileSystemEntries(Path.Combine(this.fastFetchRepoRoot, "GVFS"), "*", SearchOption.AllDirectories)
                .Count()
                .ShouldEqual(345);

            this.AllFetchedFilePathsShouldPassCheck(path => path.StartsWith("GVFS", FileSystemHelpers.PathComparison));
        }

        [TestCase]
        public void CanFetchAndCheckoutMultipleTimesUsingForceCheckoutFlag()
        {
            this.RunFastFetch($"--checkout --folders \"/GVFS\" -b {Settings.Default.Commitish}");

            this.CurrentBranchShouldEqual(Settings.Default.Commitish);

            this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner);
            List dirs = Directory.EnumerateFileSystemEntries(this.fastFetchRepoRoot).ToList();
            dirs.SequenceEqual(new[]
            {
                Path.Combine(this.fastFetchRepoRoot, ".git"),
                Path.Combine(this.fastFetchRepoRoot, "GVFS"),
                Path.Combine(this.fastFetchRepoRoot, "GVFS.sln")
            });

            Directory.EnumerateFileSystemEntries(Path.Combine(this.fastFetchRepoRoot, "GVFS"), "*", SearchOption.AllDirectories)
                .Count()
                .ShouldEqual(345);
            this.AllFetchedFilePathsShouldPassCheck(path => path.StartsWith("GVFS", FileSystemHelpers.PathComparison));

            // Run a second time in the same repo on the same branch with more folders.
            this.RunFastFetch($"--checkout --folders \"/GVFS;/Scripts\" -b {Settings.Default.Commitish} --force-checkout");
            dirs = Directory.EnumerateFileSystemEntries(this.fastFetchRepoRoot).ToList();
            dirs.SequenceEqual(new[]
            {
                Path.Combine(this.fastFetchRepoRoot, ".git"),
                Path.Combine(this.fastFetchRepoRoot, "GVFS"),
                Path.Combine(this.fastFetchRepoRoot, "Scripts"),
                Path.Combine(this.fastFetchRepoRoot, "GVFS.sln")
            });
            Directory.EnumerateFileSystemEntries(Path.Combine(this.fastFetchRepoRoot, "Scripts"), "*", SearchOption.AllDirectories)
                .Count()
                .ShouldEqual(5);
        }

        [TestCase]
        public void ForceCheckoutRequiresCheckout()
        {
            this.RunFastFetch($"--checkout --folders \"/Scripts\" -b {Settings.Default.Commitish}");

            // Run a second time in the same repo on the same branch with more folders but expect an error.
            ProcessResult result = this.RunFastFetch($"--force-checkout --folders \"/GVFS;/Scripts\" -b {Settings.Default.Commitish}");

            string[] expectedResults = new string[] { "Cannot use --force-checkout option without --checkout option." };
            result.Output.ShouldContain(expectedResults);
        }

        [TestCase]
        public void FastFetchFolderWithOnlyOneFile()
        {
            string folderPath = Path.Combine("GVFS", "GVFS", "Properties");
            this.RunFastFetch("--checkout --folders " + folderPath + " -b " + Settings.Default.Commitish);

            this.CurrentBranchShouldEqual(Settings.Default.Commitish);

            this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner);
            List dirs = Directory.EnumerateFileSystemEntries(this.fastFetchRepoRoot).ToList();
            dirs.SequenceEqual(new[]
            {
                Path.Combine(this.fastFetchRepoRoot, ".git"),
                Path.Combine(this.fastFetchRepoRoot, "GVFS"),
                Path.Combine(this.fastFetchRepoRoot, "GVFS.sln")
            });

            dirs = Directory.EnumerateFileSystemEntries(Path.Combine(this.fastFetchRepoRoot, "GVFS"), "*", SearchOption.AllDirectories).ToList();
            dirs.SequenceEqual(new[]
            {
                Path.Combine(this.fastFetchRepoRoot, "GVFS", "GVFS"),
                Path.Combine(this.fastFetchRepoRoot, "GVFS", "GVFS", "Properties"),
                Path.Combine(this.fastFetchRepoRoot, "GVFS", "GVFS", "Properties", "AssemblyInfo.cs"),
            });

            this.AllFetchedFilePathsShouldPassCheck(path => path.StartsWith("GVFS", FileSystemHelpers.PathComparison));
        }

        [TestCase]
        public void CanFetchAndCheckoutBranchIntoEmptyGitRepo()
        {
            this.RunFastFetch("--checkout -b " + Settings.Default.Commitish);

            this.CurrentBranchShouldEqual(Settings.Default.Commitish);

            this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner)
                .WithDeepStructure(FileSystemRunner.DefaultRunner, this.fastFetchControlRoot);
        }

        [TestCase]
        public void CanUpdateIndex()
        {
            // Testing index versions 2, 3 and 4.  Not bothering to test version 1; it's not in use anymore.
            this.CanUpdateIndex(2, indexSigningOff: true);
            this.CanUpdateIndex(3, indexSigningOff: true);
            this.CanUpdateIndex(4, indexSigningOff: true);

            this.CanUpdateIndex(2, indexSigningOff: false);
            this.CanUpdateIndex(3, indexSigningOff: false);
            this.CanUpdateIndex(4, indexSigningOff: false);
        }

        [TestCase]
        public void CanFetchAndCheckoutAfterDeletingIndex()
        {
            this.RunFastFetch("--checkout -b " + Settings.Default.Commitish);

            File.Delete(Path.Combine(this.fastFetchRepoRoot, ".git", "index"));
            this.RunFastFetch("--checkout -b " + Settings.Default.Commitish);

            this.CurrentBranchShouldEqual(Settings.Default.Commitish);

            this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner)
                .WithDeepStructure(FileSystemRunner.DefaultRunner, this.fastFetchControlRoot);
        }

        public void CanUpdateIndex(int indexVersion, bool indexSigningOff)
        {
            // Initialize the repo
            GitProcess.Invoke(this.fastFetchRepoRoot, "config --local --add core.gvfs " + (indexSigningOff ? 1 : 0));
            this.CanFetchAndCheckoutBranchIntoEmptyGitRepo();
            string lsfilesAfterFirstFetch = GitProcess.Invoke(this.fastFetchRepoRoot, "ls-files --debug");
            lsfilesAfterFirstFetch.ShouldBeNonEmpty();

            // Reset the index and use 'git status' to get baseline.
            GitProcess.Invoke(this.fastFetchRepoRoot, $"-c index.version={indexVersion} read-tree HEAD");
            string lsfilesBeforeStatus = GitProcess.Invoke(this.fastFetchRepoRoot, "ls-files --debug");
            lsfilesBeforeStatus.ShouldBeNonEmpty();

            GitProcess.Invoke(this.fastFetchRepoRoot, "status");
            string lsfilesAfterStatus = GitProcess.Invoke(this.fastFetchRepoRoot, "ls-files --debug");
            lsfilesAfterStatus.ShouldBeNonEmpty();
            lsfilesAfterStatus.ShouldNotBeSameAs(lsfilesBeforeStatus, "Ensure 'git status' updates index");

            // Reset the index and use fastfetch to update the index. Compare against 'git status' baseline.
            GitProcess.Invoke(this.fastFetchRepoRoot, $"-c index.version= {indexVersion} read-tree HEAD");
            ProcessResult fastFetchResult = this.RunFastFetch("--checkout --Allow-index-metadata-update-from-working-tree");
            string lsfilesAfterUpdate = GitProcess.Invoke(this.fastFetchRepoRoot, "ls-files --debug");
            lsfilesAfterUpdate.ShouldEqual(lsfilesAfterStatus, "git status and fastfetch didn't result in the same index");

            // Don't reset the index and use 'git status' to update again.  Should be same results.
            this.RunFastFetch("--checkout --Allow-index-metadata-update-from-working-tree");
            string lsfilesAfterUpdate2 = GitProcess.Invoke(this.fastFetchRepoRoot, "ls-files --debug");
            lsfilesAfterUpdate2.ShouldEqual(lsfilesAfterUpdate, "Incremental update should not change index");

            // Verify that the final results are the same as the intial fetch results
            lsfilesAfterUpdate2.ShouldEqual(lsfilesAfterFirstFetch, "Incremental update should not change index");
        }

        [TestCase]
        public void IncrementalChangesLeaveGoodStatus()
        {
            // Specific commits taken from branch  FunctionalTests/20170206_Conflict_Source
            // These commits have adds, edits and removals
            const string BaseCommit = "db95d631e379d366d26d899523f8136a77441914";
            const string UpdateCommit = "51d15f7584e81d59d44c1511ce17d7c493903390";

            GitProcess.Invoke(this.fastFetchRepoRoot, "config --local --add core.gvfs 1");

            this.RunFastFetch($"--checkout -c {BaseCommit}");
            string status = GitProcess.Invoke(this.fastFetchRepoRoot, "status --porcelain");
            status.ShouldBeEmpty("Status shows unexpected files changed");

            this.RunFastFetch($"--checkout -c {UpdateCommit}");
            status = GitProcess.Invoke(this.fastFetchRepoRoot, "status --porcelain");
            status.ShouldBeEmpty("Status shows unexpected files changed");

            // Now that we have the content, verify that these commits meet our needs...
            string changes = GitProcess.Invoke(this.fastFetchRepoRoot, $"diff-tree -r --name-status {BaseCommit}..{UpdateCommit}");

            // There must be modified files in these commits.  Modified files must
            // be updated with valid metadata (times, sizes) or 'git status' will
            // show them as modified when they were not actually modified.
            Regex.IsMatch(changes, @"^M\s", RegexOptions.Multiline).ShouldEqual(true, "Data does not meet requirements");
        }

        [TestCase]
        public void CanFetchAndCheckoutBetweenTwoBranchesIntoEmptyGitRepo()
        {
            this.RunFastFetch("--checkout -b " + Settings.Default.Commitish);
            this.CurrentBranchShouldEqual(Settings.Default.Commitish);

            // Switch to another branch
            this.RunFastFetch("--checkout -b FunctionalTests/20170602");
            this.CurrentBranchShouldEqual("FunctionalTests/20170602");

            // And back
            this.RunFastFetch("--checkout -b " + Settings.Default.Commitish);
            this.CurrentBranchShouldEqual(Settings.Default.Commitish);

            this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner)
                .WithDeepStructure(FileSystemRunner.DefaultRunner, this.fastFetchControlRoot);
        }

        [TestCase]
        public void CanDetectAlreadyUpToDate()
        {
            this.RunFastFetch("--checkout -b " + Settings.Default.Commitish);
            this.CurrentBranchShouldEqual(Settings.Default.Commitish);

            this.RunFastFetch(" -b " + Settings.Default.Commitish).Output.ShouldContain("\"TotalMissingObjects\":0");
            this.RunFastFetch("--checkout -b " + Settings.Default.Commitish).Output.ShouldContain("\"RequiredBlobsCount\":0");

            this.CurrentBranchShouldEqual(Settings.Default.Commitish);
            this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner)
                .WithDeepStructure(FileSystemRunner.DefaultRunner, this.fastFetchControlRoot);
        }

        [TestCase]
        public void SuccessfullyChecksOutCaseChanges()
        {
            // The delta between these two is the same as the UnitTest "caseChange.txt" data file.
            this.RunFastFetch("--checkout -c b3ddcf43b997cba3fbf9d2341b297e22bf48601a");
            this.RunFastFetch("--checkout -c e637c874f6a914ae83cd5668bcdd07293fef961d");

            GitProcess.Invoke(this.fastFetchControlRoot, "checkout e637c874f6a914ae83cd5668bcdd07293fef961d");

            try
            {
                // Ignore case differences on case-insensitive filesystems
                this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner)
                    .WithDeepStructure(FileSystemRunner.DefaultRunner, this.fastFetchControlRoot, ignoreCase: !FileSystemHelpers.CaseSensitiveFileSystem);
            }
            finally
            {
                GitProcess.Invoke(this.fastFetchControlRoot, "checkout " + Settings.Default.Commitish);
            }
        }

        [TestCase]
        public void SuccessfullyChecksOutDirectoryToFileToDirectory()
        {
            // This test switches between two branches and verifies specific transitions occured
            this.RunFastFetch("--checkout -b FunctionalTests/20171103_DirectoryFileTransitionsPart1");

            // Delta of interest - Check initial state
            // renamed:    foo.cpp\foo.cpp -> foo.cpp
            //   where the top level "foo.cpp" is a folder with a file, then becomes just a file
            //   note that folder\file names picked illustrate a real example
            Path.Combine(this.fastFetchRepoRoot, "foo.cpp", "foo.cpp")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner);

            // Delta of interest - Check initial state
            // renamed:    a\a <-> b && b <-> a
            //   where a\a contains "file contents one"
            //   and b contains "file contents two"
            //   This tests two types of renames crossing into each other
            Path.Combine(this.fastFetchRepoRoot, "a", "a")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents one");
            Path.Combine(this.fastFetchRepoRoot, "b")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents two");

            // Delta of interest - Check initial state
            // renamed:    c\c <-> d\c && d\d <-> c\d
            //   where c\c contains "file contents c"
            //   and d\d contains "file contents d"
            //   This tests two types of renames crossing into each other
            Path.Combine(this.fastFetchRepoRoot, "c", "c")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents c");
            Path.Combine(this.fastFetchRepoRoot, "d", "d")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents d");

            // Now switch to second branch, part2 and verify transitions
            this.RunFastFetch("--checkout -b FunctionalTests/20171103_DirectoryFileTransitionsPart2");

            // Delta of interest - Verify change
            // renamed:    foo.cpp\foo.cpp -> foo.cpp
            Path.Combine(this.fastFetchRepoRoot, "foo.cpp")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner);

            // Delta of interest - Verify change
            // renamed:    a\a <-> b && b <-> a
            Path.Combine(this.fastFetchRepoRoot, "a")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents two");
            Path.Combine(this.fastFetchRepoRoot, "b")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents one");

            // Delta of interest - Verify change
            // renamed:    c\c <-> d\c && d\d <-> c\d
            Path.Combine(this.fastFetchRepoRoot, "c", "d")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents d");
            Path.Combine(this.fastFetchRepoRoot, "d", "c")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents c");
            Path.Combine(this.fastFetchRepoRoot, "c", "c")
                .ShouldNotExistOnDisk(FileSystemRunner.DefaultRunner);
            Path.Combine(this.fastFetchRepoRoot, "d", "d")
                .ShouldNotExistOnDisk(FileSystemRunner.DefaultRunner);

            // And back again
            this.RunFastFetch("--checkout -b FunctionalTests/20171103_DirectoryFileTransitionsPart1");

            // Delta of interest - Final validation
            // renamed:    foo.cpp\foo.cpp -> foo.cpp
            Path.Combine(this.fastFetchRepoRoot, "foo.cpp", "foo.cpp")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner);

            // Delta of interest - Final validation
            // renamed:    a\a <-> b && b <-> a
            Path.Combine(this.fastFetchRepoRoot, "a", "a")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents one");
            Path.Combine(this.fastFetchRepoRoot, "b")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents two");

            // Delta of interest - Final validation
            // renamed:    c\c <-> d\c && d\d <-> c\d
            Path.Combine(this.fastFetchRepoRoot, "c", "c")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents c");
            Path.Combine(this.fastFetchRepoRoot, "d", "d")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("file contents d");
            Path.Combine(this.fastFetchRepoRoot, "c", "d")
                .ShouldNotExistOnDisk(FileSystemRunner.DefaultRunner);
            Path.Combine(this.fastFetchRepoRoot, "d", "c")
                .ShouldNotExistOnDisk(FileSystemRunner.DefaultRunner);
        }

        [TestCase]
        public void CanFetchPathsWithLsTreeTypes()
        {
            this.RunFastFetch("--checkout -b " + LsTreeTypeInPathBranchName);
            Path.Combine(this.fastFetchRepoRoot, "Test_LsTree_Issues", "file with tree in name.txt")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("File with \" tree \" in name caused issues with ls tree diff logic.");
            Path.Combine(this.fastFetchRepoRoot, "Test_LsTree_Issues", "directory with blob in path")
                .ShouldBeADirectory(FileSystemRunner.DefaultRunner);
            Path.Combine(this.fastFetchRepoRoot, "Test_LsTree_Issues", "directory with blob in path", "file with tree in name.txt")
                .ShouldBeAFile(FileSystemRunner.DefaultRunner).WithContents("File with \" tree \" in name caused issues with ls tree diff logic. This is another example.");
        }

        private void AllFetchedFilePathsShouldPassCheck(Func checkPath)
        {
            // Form a cache map of sha => path
            string[] allObjects = GitProcess.Invoke(this.fastFetchRepoRoot, "cat-file --batch-check --batch-all-objects").Split('\n');
            string[] gitlsLines = GitProcess.Invoke(this.fastFetchRepoRoot, "ls-tree -r HEAD").Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
            Dictionary> allPaths = new Dictionary>();
            foreach (string line in gitlsLines)
            {
                string sha = this.GetShaFromLsLine(line);
                string path = this.GetPathFromLsLine(line);

                if (!allPaths.ContainsKey(sha))
                {
                    allPaths.Add(sha, new List());
                }

                allPaths[sha].Add(path);
            }

            foreach (string sha in allObjects.Where(line => line.Contains(" blob ")).Select(line => line.Substring(0, 40)))
            {
                allPaths.ContainsKey(sha).ShouldEqual(true, "Found a blob that wasn't in the tree: " + sha);

                // A single blob should map to multiple files, so if any pass for a single sha, we have to give a pass.
                allPaths[sha].Any(path => checkPath(path))
                    .ShouldEqual(true, "Downloaded extra paths:\r\n" + string.Join("\r\n", allPaths[sha]));
            }
        }

        private void CurrentBranchShouldEqual(string commitish)
        {
            // Ensure remote branch has been created
            this.GetRefTreeSha("remotes/origin/" + commitish).ShouldNotBeNull();

            // And head has been updated to local branch, which are both updated
            this.GetRefTreeSha("HEAD")
                .ShouldNotBeNull()
                .ShouldEqual(this.GetRefTreeSha(commitish));

            // Ensure no errors are thrown with git log
            GitHelpers.CheckGitCommand(this.fastFetchRepoRoot, "log");
        }

        private string GetRefTreeSha(string refName)
        {
            string headInfo = GitProcess.Invoke(this.fastFetchRepoRoot, "cat-file -p " + refName);
            if (string.IsNullOrEmpty(headInfo) || headInfo.EndsWith("missing"))
            {
                return null;
            }

            string[] headInfoLines = headInfo.Split('\n');
            headInfoLines[0].StartsWith("tree").ShouldEqual(true);
            int firstSpace = headInfoLines[0].IndexOf(' ');
            string headTreeSha = headInfoLines[0].Substring(firstSpace + 1);
            headTreeSha.Length.ShouldEqual(40);
            return headTreeSha;
        }

        private ProcessResult RunFastFetch(string args)
        {
            args = args + " --verbose";

            string fastfetch = Path.Combine(Settings.Default.CurrentDirectory, "FastFetch.exe");

            File.Exists(fastfetch).ShouldBeTrue($"{fastfetch} did not exist.");
            Console.WriteLine($"Using {fastfetch}");

            ProcessStartInfo processInfo = new ProcessStartInfo(fastfetch);
            processInfo.Arguments = args;
            processInfo.WorkingDirectory = this.fastFetchRepoRoot;
            processInfo.UseShellExecute = false;
            processInfo.RedirectStandardOutput = true;
            processInfo.RedirectStandardError = true;

            ProcessResult result = ProcessHelper.Run(processInfo);

            return result;
        }

        private string GetShaFromLsLine(string line)
        {
            string output = line.Substring(line.LastIndexOf('\t') - 40, 40);
            return output;
        }

        private string GetPathFromLsLine(string line)
        {
            int idx = line.LastIndexOf('\t') + 1;
            string output = line.Substring(idx, line.Length - idx);
            return output;
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GVFSVerbTests.cs
================================================
using GVFS.Tests.Should;
using NUnit.Framework;
using System.Diagnostics;
using System.IO;

namespace GVFS.FunctionalTests.Tests
{
    [TestFixture]
    public class GVFSVerbTests
    {
        public GVFSVerbTests()
        {
        }

        private enum ExpectedReturnCode
        {
            Success = 0,
            ParsingError = 1,
        }

        [TestCase]
        public void UnknownVerb()
        {
            this.CallGVFS("help", ExpectedReturnCode.Success);
            this.CallGVFS("unknownverb", ExpectedReturnCode.ParsingError);
        }

        [TestCase]
        public void UnknownArgs()
        {
            this.CallGVFS("log --help", ExpectedReturnCode.Success);
            this.CallGVFS("log --unknown-arg", ExpectedReturnCode.ParsingError);
        }

        private void CallGVFS(string args, ExpectedReturnCode expectedErrorCode)
        {
            ProcessStartInfo processInfo = new ProcessStartInfo(GVFSTestConfig.PathToGVFS);
            processInfo.Arguments = args;
            processInfo.WindowStyle = ProcessWindowStyle.Hidden;
            processInfo.UseShellExecute = false;
            processInfo.RedirectStandardOutput = true;
            processInfo.RedirectStandardError = true;

            using (Process process = Process.Start(processInfo))
            {
                string result = process.StandardOutput.ReadToEnd();
                process.WaitForExit();

                process.ExitCode.ShouldEqual((int)expectedErrorCode, result);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/AddStageTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Tools;
using NUnit.Framework;
using System.IO;
using System.Threading;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class AddStageTests : GitRepoTests
    {
        public AddStageTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: false, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase, Order(1)]
        public void AddBasicTest()
        {
            this.EditFile("Some new content.", "Readme.md");
            this.ValidateGitCommand("add Readme.md");
            this.RunGitCommand("commit -m \"Changing the Readme.md\"");
        }

        [TestCase, Order(2)]
        public void StageBasicTest()
        {
            this.EditFile("Some new content.", "AuthoringTests.md");
            this.ValidateGitCommand("stage AuthoringTests.md");
            this.RunGitCommand("commit -m \"Changing the AuthoringTests.md\"");
        }

        [TestCase, Order(3)]
        public void AddAndStageHardLinksTest()
        {
            this.CreateHardLink("ReadmeLink.md", "Readme.md");
            this.ValidateGitCommand("add ReadmeLink.md");
            this.RunGitCommand("commit -m \"Created ReadmeLink.md\"");

            this.CreateHardLink("AuthoringTestsLink.md", "AuthoringTests.md");
            this.ValidateGitCommand("stage AuthoringTestsLink.md");
            this.RunGitCommand("commit -m \"Created AuthoringTestsLink.md\"");
        }

        [TestCase, Order(4)]
        public void AddAllowsPlaceholderCreation()
        {
            this.CommandAllowsPlaceholderCreation("add", "GVFS", "GVFS", "Program.cs");
        }

        [TestCase, Order(5)]
        public void StageAllowsPlaceholderCreation()
        {
            this.CommandAllowsPlaceholderCreation("stage", "GVFS", "GVFS", "App.config");
        }

        private void CommandAllowsPlaceholderCreation(string command, params string[] fileToReadPathParts)
        {
            string fileToRead = Path.Combine(fileToReadPathParts);
            this.EditFile($"Some new content for {command}.", "Protocol.md");
            ManualResetEventSlim resetEvent = GitHelpers.RunGitCommandWithWaitAndStdIn(this.Enlistment, resetTimeout: 3000, command: $"{command} -p", stdinToQuit: "q", processId: out _);
            this.FileContentsShouldMatch(fileToRead);
            this.ValidateGitCommand("--no-optional-locks status");
            resetEvent.Wait();
            this.RunGitCommand("reset --hard");
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/CheckoutTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using Microsoft.Win32.SafeHandles;
using NUnit.Framework;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class CheckoutTests : GitRepoTests
    {
        private const string BranchWithoutFiles = "FunctionalTests/20201014_CheckoutTests";
        private const string BranchWithFiles = "FunctionalTests/20201014_CheckoutTests2";
        private const string BranchWithFiles2 = "FunctionalTests/20201014_CheckoutTests3";

        public CheckoutTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        private enum NativeFileAttributes : uint
        {
            FILE_ATTRIBUTE_READONLY = 1,
            FILE_ATTRIBUTE_HIDDEN = 2,
            FILE_ATTRIBUTE_SYSTEM = 4,
            FILE_ATTRIBUTE_DIRECTORY = 16,
            FILE_ATTRIBUTE_ARCHIVE = 32,
            FILE_ATTRIBUTE_DEVICE = 64,
            FILE_ATTRIBUTE_NORMAL = 128,
            FILE_ATTRIBUTE_TEMPORARY = 256,
            FILE_ATTRIBUTE_SPARSEFILE = 512,
            FILE_ATTRIBUTE_REPARSEPOINT = 1024,
            FILE_ATTRIBUTE_COMPRESSED = 2048,
            FILE_ATTRIBUTE_OFFLINE = 4096,
            FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 8192,
            FILE_ATTRIBUTE_ENCRYPTED = 16384,
            FILE_FLAG_FIRST_PIPE_INSTANCE = 524288,
            FILE_FLAG_OPEN_NO_RECALL = 1048576,
            FILE_FLAG_OPEN_REPARSE_POINT = 2097152,
            FILE_FLAG_POSIX_SEMANTICS = 16777216,
            FILE_FLAG_BACKUP_SEMANTICS = 33554432,
            FILE_FLAG_DELETE_ON_CLOSE = 67108864,
            FILE_FLAG_SEQUENTIAL_SCAN = 134217728,
            FILE_FLAG_RANDOM_ACCESS = 268435456,
            FILE_FLAG_NO_BUFFERING = 536870912,
            FILE_FLAG_OVERLAPPED = 1073741824,
            FILE_FLAG_WRITE_THROUGH = 2147483648
        }

        private enum NativeFileAccess : uint
        {
            FILE_READ_DATA = 1,
            FILE_LIST_DIRECTORY = 1,
            FILE_WRITE_DATA = 2,
            FILE_ADD_FILE = 2,
            FILE_APPEND_DATA = 4,
            FILE_ADD_SUBDIRECTORY = 4,
            FILE_CREATE_PIPE_INSTANCE = 4,
            FILE_READ_EA = 8,
            FILE_WRITE_EA = 16,
            FILE_EXECUTE = 32,
            FILE_TRAVERSE = 32,
            FILE_DELETE_CHILD = 64,
            FILE_READ_ATTRIBUTES = 128,
            FILE_WRITE_ATTRIBUTES = 256,
            SPECIFIC_RIGHTS_ALL = 65535,
            DELETE = 65536,
            READ_CONTROL = 131072,
            STANDARD_RIGHTS_READ = 131072,
            STANDARD_RIGHTS_WRITE = 131072,
            STANDARD_RIGHTS_EXECUTE = 131072,
            WRITE_DAC = 262144,
            WRITE_OWNER = 524288,
            STANDARD_RIGHTS_REQUIRED = 983040,
            SYNCHRONIZE = 1048576,
            FILE_GENERIC_READ = 1179785,
            FILE_GENERIC_EXECUTE = 1179808,
            FILE_GENERIC_WRITE = 1179926,
            STANDARD_RIGHTS_ALL = 2031616,
            FILE_ALL_ACCESS = 2032127,
            ACCESS_SYSTEM_SECURITY = 16777216,
            MAXIMUM_ALLOWED = 33554432,
            GENERIC_ALL = 268435456,
            GENERIC_EXECUTE = 536870912,
            GENERIC_WRITE = 1073741824,
            GENERIC_READ = 2147483648
        }

        [TestCase]
        public void ReadDeepFilesAfterCheckout()
        {
            this.ControlGitRepo.Fetch(BranchWithFiles);
            this.ControlGitRepo.Fetch(BranchWithoutFiles);

            // In commit 8df701986dea0a5e78b742d2eaf9348825b14d35 the CheckoutNewBranchFromStartingPointTest files were not present
            this.ValidateGitCommand($"checkout {BranchWithoutFiles}");

            // In commit cd5c55fea4d58252bb38058dd3818da75aff6685 the CheckoutNewBranchFromStartingPointTest files were present
            this.ValidateGitCommand($"checkout {BranchWithFiles}");

            this.FileShouldHaveContents("TestFile1 \r\n", "GitCommandsTests", "CheckoutNewBranchFromStartingPointTest", "test1.txt");
            this.FileShouldHaveContents("TestFile2 \r\n", "GitCommandsTests", "CheckoutNewBranchFromStartingPointTest", "test2.txt");

            this.ValidateGitCommand("status");
        }

        [TestCase]
        [Ignore("This doesn't work right now. Tracking if this is a ProjFS problem. See #1696 for tracking.")]
        public void CheckoutNewBranchFromStartingPointTest()
        {
            this.ControlGitRepo.Fetch(BranchWithFiles);
            this.ControlGitRepo.Fetch(BranchWithoutFiles);

            // In commit 8df701986dea0a5e78b742d2eaf9348825b14d35 the CheckoutNewBranchFromStartingPointTest files were not present
            this.ValidateGitCommand($"checkout {BranchWithoutFiles}");
            this.ShouldNotExistOnDisk("GitCommandsTests", "CheckoutNewBranchFromStartingPointTest", "test1.txt");
            this.ShouldNotExistOnDisk("GitCommandsTests", "CheckoutNewBranchFromStartingPointTest", "test2.txt");

            // In commit cd5c55fea4d58252bb38058dd3818da75aff6685 the CheckoutNewBranchFromStartingPointTest files were present
            this.ValidateGitCommand($"checkout -b tests/functional/CheckoutNewBranchFromStartingPointTest {BranchWithFiles}");
            this.FileShouldHaveContents("TestFile1 \r\n", "GitCommandsTests", "CheckoutNewBranchFromStartingPointTest", "test1.txt");
            this.FileShouldHaveContents("TestFile2 \r\n", "GitCommandsTests", "CheckoutNewBranchFromStartingPointTest", "test2.txt");

            this.ValidateGitCommand("status");
        }

        [TestCase]
        [Ignore("This doesn't work right now. Tracking if this is a ProjFS problem. See #1696 for tracking.")]
        public void CheckoutOrhpanBranchFromStartingPointTest()
        {
            this.ControlGitRepo.Fetch(BranchWithoutFiles);
            this.ControlGitRepo.Fetch(BranchWithFiles2);

            // In commit 8df701986dea0a5e78b742d2eaf9348825b14d35 the CheckoutOrhpanBranchFromStartingPointTest files were not present
            this.ValidateGitCommand($"checkout {BranchWithoutFiles}");
            this.ShouldNotExistOnDisk("GitCommandsTests", "CheckoutOrhpanBranchFromStartingPointTest", "test1.txt");
            this.ShouldNotExistOnDisk("GitCommandsTests", "CheckoutOrhpanBranchFromStartingPointTest", "test2.txt");

            // In commit 15a9676c9192448820bd243807f6dab1bac66680 the CheckoutOrhpanBranchFromStartingPointTest files were present
            this.ValidateGitCommand($"checkout --orphan tests/functional/CheckoutOrhpanBranchFromStartingPointTest {BranchWithFiles2}");
            this.FileShouldHaveContents("TestFile1 \r\n", "GitCommandsTests", "CheckoutOrhpanBranchFromStartingPointTest", "test1.txt");
            this.FileShouldHaveContents("TestFile2 \r\n", "GitCommandsTests", "CheckoutOrhpanBranchFromStartingPointTest", "test2.txt");

            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void MoveFileFromDotGitFolderToWorkingDirectoryAndAddAndCheckout()
        {
            string testFileContents = "Test file contents for MoveFileFromDotGitFolderToWorkingDirectoryAndAddAndCheckout";
            string filename = "AddedBySource.txt";
            string dotGitFilePath = Path.Combine(".git", filename);
            string targetPath = Path.Combine("Test_ConflictTests", "AddedFiles", filename);

            // In commit db95d631e379d366d26d899523f8136a77441914 Test_ConflictTests\AddedFiles\AddedBySource.txt does not exist
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_CheckoutTests4");
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_CheckoutTests5");

            this.ValidateGitCommand("checkout FunctionalTests/20201014_CheckoutTests4");

            string newBranchName = "tests/functional/MoveFileFromDotGitFolderToWorkingDirectoryAndAddAndCheckout";
            this.ValidateGitCommand("checkout -b " + newBranchName);

            this.ShouldNotExistOnDisk(targetPath);
            this.CreateFile(testFileContents, dotGitFilePath);
            this.FileShouldHaveContents(testFileContents, dotGitFilePath);

            // Move file to working directory
            this.MoveFile(dotGitFilePath, targetPath);
            this.FileContentsShouldMatch(targetPath);

            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for MoveFileFromDotGitFolderToWorkingDirectoryAndAddAndCheckout\"");

            // In commit 51d15f7584e81d59d44c1511ce17d7c493903390 Test_ConflictTests\AddedFiles\AddedBySource.txt was added
            this.ValidateGitCommand("checkout FunctionalTests/20201014_CheckoutTests5");
            this.FileContentsShouldMatch(targetPath);
        }

        [TestCase]
        public void CheckoutBranchNoCrashOnStatus()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_git_crash");
            this.ValidateGitCommand("checkout FunctionalTests/20201014_git_crash");
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void CheckoutCommitWhereFileContentsChangeAfterRead()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_CheckoutTests4");

            string fileName = "SameChange.txt";

            // In commit db95d631e379d366d26d899523f8136a77441914 the initial files for the FunctionalTests/20201014_Conflict_Source_2 branch were created
            this.ValidateGitCommand("checkout FunctionalTests/20201014_CheckoutTests4");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", fileName);

            // A read should not add the file to the modified paths
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.FileSystem, fileName);

            this.ValidateGitCommand($"checkout {GitRepoTests.ConflictSourceBranch}");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", fileName);
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.FileSystem, fileName);
        }

        [TestCase]
        public void CheckoutCommitWhereFileDeletedAfterRead()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_CheckoutTests4");

            string fileName = "DeleteInSource.txt";
            string filePath = Path.Combine("Test_ConflictTests", "DeletedFiles", fileName);

            // In commit db95d631e379d366d26d899523f8136a77441914 the initial files for the FunctionalTests/20201014_Conflict_Source_2 branch were created
            this.ValidateGitCommand("checkout FunctionalTests/20201014_CheckoutTests4");
            this.FileContentsShouldMatch(filePath);

            // A read should not add the file to the modified paths
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.FileSystem, fileName);

            this.ValidateGitCommand($"checkout {GitRepoTests.ConflictSourceBranch}");
            this.ShouldNotExistOnDisk(filePath);
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.FileSystem, fileName);
        }

        [TestCase]
        public void CheckoutBranchAfterReadingFileAndVerifyContentsCorrect()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.FilesShouldMatchCheckoutOfTargetBranch();

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchCheckoutOfSourceBranch();

            // Verify modified paths contents
            GVFSHelpers.ModifiedPathsContentsShouldEqual(this.Enlistment, this.FileSystem, "A .gitattributes" + GVFSHelpers.ModifiedPathsNewLine);
        }

        [TestCase]
        public void CheckoutBranchAfterReadingAllFilesAndVerifyContentsCorrect()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, compareContent: true, withinPrefixes: this.pathPrefixes);

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);
            this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, compareContent: true, withinPrefixes: this.pathPrefixes);

            // Verify modified paths contents
            GVFSHelpers.ModifiedPathsContentsShouldEqual(this.Enlistment, this.FileSystem, "A .gitattributes" + GVFSHelpers.ModifiedPathsNewLine);
        }

        [TestCase]
        public void CheckoutBranchThatHasFolderShouldGetDeleted()
        {
            // this.ControlGitRepo.Commitish should not have the folder Test_ConflictTests\AddedFiles
            string testFolder = Path.Combine("Test_ConflictTests", "AddedFiles");
            this.ShouldNotExistOnDisk(testFolder);

            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);
            string testFile = Path.Combine(testFolder, "AddedByBothDifferentContent.txt");
            this.FileContentsShouldMatch(testFile);

            // Move back to this.ControlGitRepo.Commitish where testFolder and testFile are not in the repo
            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.ShouldNotExistOnDisk(testFile);

            string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, testFolder);
            string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, testFolder);
            controlFolder.ShouldNotExistOnDisk(this.FileSystem);
            virtualFolder.ShouldNotExistOnDisk(this.FileSystem);

            // Move back to GitRepoTests.ConflictSourceBranch where testFolder and testFile are present
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);
            this.FileContentsShouldMatch(testFile);
        }

        [TestCase]
        public void CheckoutBranchThatDoesNotHaveFolderShouldNotHaveFolder()
        {
            // this.ControlGitRepo.Commitish should not have the folder Test_ConflictTests\AddedFiles
            string testFolder = Path.Combine("Test_ConflictTests", "AddedFiles");
            this.ShouldNotExistOnDisk(testFolder);

            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);

            string testFile = Path.Combine(testFolder, "AddedByBothDifferentContent.txt");
            this.FileContentsShouldMatch(testFile);
            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.ShouldNotExistOnDisk(testFile);

            this.ValidateGitCommand("checkout -b tests/functional/DeleteEmptyFolderPlaceholderAndCheckoutBranchThatDoesNotHaveFolder" + this.ControlGitRepo.Commitish);

            string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, testFolder);
            string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, testFolder);
            controlFolder.ShouldNotExistOnDisk(this.FileSystem);
            virtualFolder.ShouldNotExistOnDisk(this.FileSystem);

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
        }

        [TestCase]
        public void EditFileReadFileAndCheckoutConflict()
        {
            // editFilePath was changed on ConflictTargetBranch
            string editFilePath = Path.Combine("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt");

            // readFilePath has different contents on ConflictSourceBranch and ConflictTargetBranch
            string readFilePath = Path.Combine("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt");

            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);

            this.EditFile("New content", editFilePath);
            this.FileContentsShouldMatch(readFilePath);
            string originalReadFileContents = this.Enlistment.GetVirtualPathTo(readFilePath).ShouldBeAFile(this.FileSystem).WithContents();

            // This checkout will hit a conflict due to the changes in editFilePath
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.FileContentsShouldMatch(readFilePath);
            this.FileContentsShouldMatch(editFilePath);

            // The contents of originalReadFileContents should not have changed
            this.Enlistment.GetVirtualPathTo(readFilePath).ShouldBeAFile(this.FileSystem).WithContents(originalReadFileContents);

            this.ValidateGitCommand("checkout -- " + editFilePath.Replace('\\', '/'));
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.FileContentsShouldMatch(readFilePath);
            this.FileContentsShouldMatch(editFilePath);
            this.Enlistment.GetVirtualPathTo(readFilePath).ShouldBeAFile(this.FileSystem).WithContents().ShouldNotEqual(originalReadFileContents);

            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.FileSystem, Path.GetFileName(readFilePath));
        }

        [TestCase]
        public void MarkFileAsReadOnlyAndCheckoutCommitWhereFileIsDifferent()
        {
            string filePath = Path.Combine("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt");

            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);

            this.SetFileAsReadOnly(filePath);

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.FileContentsShouldMatch(filePath);
        }

        [TestCase]
        public void MarkFileAsReadOnlyAndCheckoutCommitWhereFileIsDeleted()
        {
            string filePath = Path.Combine("Test_ConflictTests", "AddedFiles", "AddedBySource.txt");

            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);

            this.SetFileAsReadOnly(filePath);

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ShouldNotExistOnDisk(filePath);
        }

        [TestCase]
        public void ModifyAndCheckoutFirstOfSeveralFilesWhoseNamesAppearBeforeDot()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_CheckoutTests6");

            // 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 FunctionalTests/20201014_CheckoutTests6");
            this.EditFile(newContent, "DeleteFileWithNameAheadOfDotAndSwitchCommits", "(a).txt");
            this.FileShouldHaveContents(originalContent + newContent, "DeleteFileWithNameAheadOfDotAndSwitchCommits", "(a).txt");
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("checkout -- DeleteFileWithNameAheadOfDotAndSwitchCommits/(a).txt");
            this.ValidateGitCommand("status");
            this.FileShouldHaveContents(originalContent, "DeleteFileWithNameAheadOfDotAndSwitchCommits", "(a).txt");
        }

        [TestCase]
        public void ResetMixedToCommitWithNewFileThenCheckoutNewBranchAndCheckoutCommitWithNewFile()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_CheckoutTests4");
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_CheckoutTests5");

            // 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 FunctionalTests/20201014_CheckoutTests4");
            this.ValidateGitCommand("reset --mixed FunctionalTests/20201014_CheckoutTests5");

            // 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 FunctionalTests/20201014_CheckoutTests5");
        }

        // ReadFileAfterTryingToReadFileAtCommitWhereFileDoesNotExist is meant to exercise the NegativePathCache and its
        // behavior when projections change
        [TestCase]
        public void ReadFileAfterTryingToReadFileAtCommitWhereFileDoesNotExist()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_CheckoutTests4");
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_CheckoutTests5");

            // 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 FunctionalTests/20201014_CheckoutTests4");

            // Files should not exist
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt");
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt");
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedBySource.txt");

            // Check a second time to exercise the ProjFS negative cache
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt");
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt");
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedBySource.txt");

            // Switch to commit where files should exist
            this.ValidateGitCommand("checkout FunctionalTests/20201014_CheckoutTests5");

            // Confirm files exist
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedBySource.txt");

            // Switch to commit where files should not exist
            this.ValidateGitCommand("checkout FunctionalTests/20201014_CheckoutTests4");

            // Verify files do not not exist
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt");
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt");
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedBySource.txt");

            // Check a second time to exercise the ProjFS negative cache
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt");
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt");
            this.ShouldNotExistOnDisk("Test_ConflictTests", "AddedFiles", "AddedBySource.txt");
        }

        [TestCase]
        public void CheckoutBranchWithOpenHandleBlockingRepoMetdataUpdate()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);

            ManualResetEventSlim testReady = new ManualResetEventSlim(initialState: false);
            ManualResetEventSlim fileLocked = new ManualResetEventSlim(initialState: false);
            Task task = Task.Run(() =>
            {
                int attempts = 0;
                while (attempts < 100)
                {
                    try
                    {
                        using (FileStream stream = new FileStream(Path.Combine(this.Enlistment.DotGVFSRoot, "databases", "RepoMetadata.dat"), FileMode.Open, FileAccess.Read, FileShare.None))
                        {
                            fileLocked.Set();
                            testReady.Set();
                            Thread.Sleep(15000);
                            return;
                        }
                    }
                    catch (Exception)
                    {
                        ++attempts;
                        Thread.Sleep(50);
                    }
                }

                testReady.Set();
            });

            // Wait for task to acquire the handle
            testReady.Wait();
            fileLocked.IsSet.ShouldBeTrue("Failed to obtain exclusive file handle.  Exclusive handle required to validate behavior");

            try
            {
                this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            }
            catch (Exception)
            {
                // If the test fails, we should wait for the Task to complete so that it does not keep a handle open
                task.Wait();
                throw;
            }
        }

        [TestCase]
        public void CheckoutBranchWithOpenHandleBlockingProjectionDeleteAndRepoMetdataUpdate()
        {
            try
            {
                GVFSHelpers.RegisterForOfflineIO();

                this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
                this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);

                this.Enlistment.UnmountGVFS();
                string gitIndexPath = Path.Combine(this.Enlistment.RepoBackingRoot, ".git", "index");
                CopyIndexAndRename(gitIndexPath);
                this.Enlistment.MountGVFS();

                ManualResetEventSlim testReady = new ManualResetEventSlim(initialState: false);
                ManualResetEventSlim fileLocked = new ManualResetEventSlim(initialState: false);
                Task task = Task.Run(() =>
                {
                    int attempts = 0;
                    while (attempts < 100)
                    {
                        try
                        {
                            using (FileStream projectionStream = new FileStream(Path.Combine(this.Enlistment.DotGVFSRoot, "GVFS_projection"), FileMode.Open, FileAccess.Read, FileShare.None))
                            using (FileStream metadataStream = new FileStream(Path.Combine(this.Enlistment.DotGVFSRoot, "databases", "RepoMetadata.dat"), FileMode.Open, FileAccess.Read, FileShare.None))
                            {
                                fileLocked.Set();
                                testReady.Set();
                                Thread.Sleep(15000);
                                return;
                            }
                        }
                        catch (Exception)
                        {
                            ++attempts;
                            Thread.Sleep(50);
                        }
                    }

                    testReady.Set();
                });

                // Wait for task to acquire the handle
                testReady.Wait();
                fileLocked.IsSet.ShouldBeTrue("Failed to obtain exclusive file handle.  Exclusive handle required to validate behavior");

                try
                {
                    this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
                }
                catch (Exception)
                {
                    // If the test fails, we should wait for the Task to complete so that it does not keep a handle open
                    task.Wait();
                    throw;
                }
            }
            finally
            {
                GVFSHelpers.UnregisterForOfflineIO();
            }
        }

        [TestCase]
        public void CheckoutBranchWithStaleRepoMetadataTmpFileOnDisk()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);

            this.FileSystem.WriteAllText(Path.Combine(this.Enlistment.DotGVFSRoot, "databases", "RepoMetadata.dat.tmp"), "Stale RepoMetadata.dat.tmp file");
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
        }

        [TestCase]
        public void CheckoutBranchWhileOutsideToolDoesNotAllowDeleteOfOpenRepoMetadata()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);

            ManualResetEventSlim testReady = new ManualResetEventSlim(initialState: false);
            ManualResetEventSlim fileLocked = new ManualResetEventSlim(initialState: false);
            Task task = Task.Run(() =>
            {
                int attempts = 0;
                while (attempts < 100)
                {
                    try
                    {
                        using (FileStream stream = new FileStream(Path.Combine(this.Enlistment.DotGVFSRoot, "databases", "RepoMetadata.dat"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                        {
                            fileLocked.Set();
                            testReady.Set();
                            Thread.Sleep(15000);
                            return;
                        }
                    }
                    catch (Exception)
                    {
                        ++attempts;
                        Thread.Sleep(50);
                    }
                }

                testReady.Set();
            });

            // Wait for task to acquire the handle
            testReady.Wait();
            fileLocked.IsSet.ShouldBeTrue("Failed to obtain file handle.  Handle required to validate behavior");

            try
            {
                this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            }
            catch (Exception)
            {
                // If the test fails, we should wait for the Task to complete so that it does not keep a handle open
                task.Wait();
                throw;
            }
        }

        [TestCase]
        public void CheckoutBranchWhileOutsideToolHasExclusiveReadHandleOnDatabasesFolder()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);

            ManualResetEventSlim testReady = new ManualResetEventSlim(initialState: false);
            ManualResetEventSlim folderLocked = new ManualResetEventSlim(initialState: false);
            Task task = Task.Run(() =>
            {
                int attempts = 0;
                string databasesPath = Path.Combine(this.Enlistment.DotGVFSRoot, "databases");
                while (attempts < 100)
                {
                    using (SafeFileHandle result = CreateFile(
                        databasesPath,
                        NativeFileAccess.GENERIC_READ,
                        FileShare.Read,
                        IntPtr.Zero,
                        FileMode.Open,
                        NativeFileAttributes.FILE_FLAG_BACKUP_SEMANTICS | NativeFileAttributes.FILE_FLAG_OPEN_REPARSE_POINT,
                        IntPtr.Zero))
                    {
                        if (result.IsInvalid)
                        {
                            ++attempts;
                            Thread.Sleep(50);
                        }
                        else
                        {
                            folderLocked.Set();
                            testReady.Set();
                            Thread.Sleep(15000);
                            return;
                        }
                    }
                }

                testReady.Set();
            });

            // Wait for task to acquire the handle
            testReady.Wait();
            folderLocked.IsSet.ShouldBeTrue("Failed to obtain exclusive file handle.  Handle required to validate behavior");

            try
            {
                this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            }
            catch (Exception)
            {
                // If the test fails, we should wait for the Task to complete so that it does not keep a handle open
                task.Wait();
                throw;
            }
        }

        [TestCase]
        public void ResetMixedTwiceThenCheckoutWithChanges()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_MultipleFileEdits");
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_ResetMixedTwice_A");
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_ResetMixedTwice_B");
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_ResetMixedTwice_C");

            this.ValidateGitCommand("checkout FunctionalTests/20201014_ResetMixedTwice_A");
            this.ValidateGitCommand("checkout -b tests/functional/ResetMixedTwice");

            // 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 FunctionalTests/20201014_ResetMixedTwice_B");
            this.ValidateGitCommand("reset --mixed FunctionalTests/20201014_ResetMixedTwice_C");

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
        }

        [TestCase]
        public void ResetMixedTwiceThenCheckoutWithRemovedFiles()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_MultipleFileDeletes");
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_ResetMixedTwice_D");
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_ResetMixedTwice_E");
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_ResetMixedTwice_F");

            this.ValidateGitCommand("checkout FunctionalTests/20201014_ResetMixedTwice_D");
            this.ValidateGitCommand("checkout -b tests/functional/ResetMixedTwice");

            // 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 FunctionalTests/20201014_ResetMixedTwice_E");
            this.ValidateGitCommand("reset --mixed FunctionalTests/20201014_ResetMixedTwice_F");

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
        }

        [TestCase]
        public void DeleteFolderAndChangeBranchToFolderWithDifferentCase()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_Checkout9");

            // 692765 - Recursive modified paths entries for folders should be case insensitive when
            // changing branches

            string folderName = "GVFlt_MultiThreadTest";

            // Confirm that no other test has caused "GVFlt_MultiThreadTest" to be added to the modified paths database
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.FileSystem, folderName);

            this.FolderShouldHaveCaseMatchingName(folderName);
            this.DeleteFolder(folderName);

            // 4141dc6023b853740795db41a06b278ebdee0192 is the commit prior to deleting GVFLT_MultiThreadTest
            // and re-adding it as as GVFlt_MultiThreadTest
            this.ValidateGitCommand("checkout FunctionalTests/20201014_Checkout9");
            this.FolderShouldHaveCaseMatchingName("GVFLT_MultiThreadTest");
        }

        [TestCase]
        public void SuccessfullyChecksOutDirectoryToFileToDirectory()
        {
            // This test switches between two branches and verifies specific transitions occured
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_DirectoryFileTransitionsPart1");
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_DirectoryFileTransitionsPart2");
            this.ValidateGitCommand("checkout FunctionalTests/20201014_DirectoryFileTransitionsPart1");

            // Delta of interest - Check initial state
            // renamed:    foo.cpp\foo.cpp -> foo.cpp
            //   where the top level "foo.cpp" is a folder with a file, then becomes just a file
            //   note that folder\file names picked illustrate a real example
            this.FolderShouldExistAndHaveFile("foo.cpp", "foo.cpp");

            // Delta of interest - Check initial state
            // renamed:    a\a <-> b && b <-> a
            //   where a\a contains "file contents one"
            //   and b contains "file contents two"
            //   This tests two types of renames crossing into each other
            this.FileShouldHaveContents("file contents one", "a", "a");
            this.FileShouldHaveContents("file contents two", "b");

            // Delta of interest - Check initial state
            // renamed:    c\c <-> d\c && d\d <-> c\d
            //   where c\c contains "file contents c"
            //   and d\d contains "file contents d"
            //   This tests two types of renames crossing into each other
            this.FileShouldHaveContents("file contents c", "c", "c");
            this.FileShouldHaveContents("file contents d", "d", "d");

            // Now switch to second branch, part2 and verify transitions
            this.ValidateGitCommand("checkout FunctionalTests/20201014_DirectoryFileTransitionsPart2");

            // Delta of interest - Verify change
            // renamed:    foo.cpp\foo.cpp -> foo.cpp
            this.FolderShouldExistAndHaveFile(string.Empty, "foo.cpp");

            // Delta of interest - Verify change
            // renamed:    a\a <-> b && b <-> a
            this.FileShouldHaveContents("file contents two", "a");
            this.FileShouldHaveContents("file contents one", "b");

            // Delta of interest - Verify change
            // renamed:    c\c <-> d\c && d\d <-> c\d
            this.FileShouldHaveContents("file contents d", "c", "d");
            this.FileShouldHaveContents("file contents c", "d", "c");
            this.ShouldNotExistOnDisk("c", "c");
            this.ShouldNotExistOnDisk("d", "d");

            // And back again
            this.ValidateGitCommand("checkout FunctionalTests/20201014_DirectoryFileTransitionsPart1");

            // Delta of interest - Final validation
            // renamed:    foo.cpp\foo.cpp -> foo.cpp
            this.FolderShouldExistAndHaveFile("foo.cpp", "foo.cpp");

            // Delta of interest - Final validation
            // renamed:    a\a <-> b && b <-> a
            this.FileShouldHaveContents("file contents one", "a", "a");
            this.FileShouldHaveContents("file contents two", "b");

            // Delta of interest - Final validation
            // renamed:    c\c <-> d\c && d\d <-> c\d
            this.FileShouldHaveContents("file contents c", "c", "c");
            this.FileShouldHaveContents("file contents d", "d", "d");
            this.ShouldNotExistOnDisk("c", "d");
            this.ShouldNotExistOnDisk("d", "c");
        }

        [TestCase]
        public void DeleteFileThenCheckout()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_CheckoutTests6");

            this.FolderShouldExistAndHaveFile("GitCommandsTests", "DeleteFileTests", "1", "#test");
            this.DeleteFile("GitCommandsTests", "DeleteFileTests", "1", "#test");
            this.FolderShouldExistAndBeEmpty("GitCommandsTests", "DeleteFileTests", "1");

            // Commit cb2d05febf64e3b0df50bd8d3fe8f05c0e2caa47 is before
            // the files in GitCommandsTests\DeleteFileTests were added
            this.ValidateGitCommand("checkout FunctionalTests/20201014_CheckoutTests6");

            this.ShouldNotExistOnDisk("GitCommandsTests", "DeleteFileTests", "1");
            this.ShouldNotExistOnDisk("GitCommandsTests", "DeleteFileTests");
        }

        [TestCase]
        public void CheckoutEditCheckoutWithoutFolderThenCheckoutWithMultipleFiles()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_Checkout8");

            // Edit the file to get the entry in the modified paths database
            this.EditFile("Changing the content of one file", "DeleteFileWithNameAheadOfDotAndSwitchCommits", "1");
            this.RunGitCommand("reset --hard -q HEAD");

            // This commit should remove the DeleteFileWithNameAheadOfDotAndSwitchCommits folder
            this.ValidateGitCommand("checkout FunctionalTests/20201014_Checkout8");

            this.ShouldNotExistOnDisk("DeleteFileWithNameAheadOfDotAndSwitchCommits");
        }

        [TestCase]
        public void CreateAFolderThenCheckoutBranchWithFolder()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_Checkout8");

            this.FolderShouldExistAndHaveFile("DeleteFileWithNameAheadOfDotAndSwitchCommits", "1");

            // This commit should remove the DeleteFileWithNameAheadOfDotAndSwitchCommits folder
            this.ValidateGitCommand("checkout FunctionalTests/20201014_Checkout8");
            this.ShouldNotExistOnDisk("DeleteFileWithNameAheadOfDotAndSwitchCommits");
            this.CreateFolder("DeleteFileWithNameAheadOfDotAndSwitchCommits");
            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.FolderShouldExistAndHaveFile("DeleteFileWithNameAheadOfDotAndSwitchCommits", "1");
        }

        [TestCase]
        public void CheckoutBranchWithDirectoryNameSameAsFile()
        {
            this.SetupForFileDirectoryTest();
            this.ValidateFileDirectoryTest("checkout");
        }

        [TestCase]
        public void CheckoutBranchWithDirectoryNameSameAsFileEnumerate()
        {
            this.RunFileDirectoryEnumerateTest("checkout");
        }

        [TestCase]
        public void CheckoutBranchWithDirectoryNameSameAsFileWithRead()
        {
            this.RunFileDirectoryReadTest("checkout");
        }

        [TestCase]
        public void CheckoutBranchWithDirectoryNameSameAsFileWithWrite()
        {
            this.RunFileDirectoryWriteTest("checkout");
        }

        [TestCase]
        public void CheckoutBranchDirectoryWithOneFile()
        {
            this.SetupForFileDirectoryTest(commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
            this.ValidateFileDirectoryTest("checkout", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }

        [TestCase]
        public void CheckoutBranchDirectoryWithOneFileEnumerate()
        {
            this.RunFileDirectoryEnumerateTest("checkout", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }

        [TestCase]
        public void CheckoutBranchDirectoryWithOneFileRead()
        {
            this.RunFileDirectoryReadTest("checkout", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }

        [TestCase]
        public void CheckoutBranchDirectoryWithOneFileWrite()
        {
            this.RunFileDirectoryWriteTest("checkout", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }

        [TestCase]
        public void CheckoutBranchDirectoryWithOneDeepFileWrite()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.DeepDirectoryWithOneFile);
            this.ControlGitRepo.Fetch(GitRepoTests.DeepDirectoryWithOneDifferentFile);
            this.ValidateGitCommand($"checkout {GitRepoTests.DeepDirectoryWithOneFile}");
            this.FileShouldHaveContents(
                "TestFile1\n",
                "GitCommandsTests",
                "CheckoutBranchDirectoryWithOneDeepFile",
                "FolderDepth1",
                "FolderDepth2",
                "FolderDepth3",
                "File1.txt");

            // Edit the file and commit the change so that git will
            // delete the file (and its parent directories) when
            // changing branches
            this.EditFile(
                "Change file",
                "GitCommandsTests",
                "CheckoutBranchDirectoryWithOneDeepFile",
                "FolderDepth1",
                "FolderDepth2",
                "FolderDepth3",
                "File1.txt");
            this.ValidateGitCommand("add --all");
            this.RunGitCommand("commit -m \"Some change\"");

            this.ValidateGitCommand($"checkout {GitRepoTests.DeepDirectoryWithOneDifferentFile}");
            this.FileShouldHaveContents(
                "TestFile2\n",
                "GitCommandsTests",
                "CheckoutBranchDirectoryWithOneDeepFile",
                "FolderDepth1",
                "FolderDepth2",
                "FolderDepth3",
                "File2.txt");
            this.ShouldNotExistOnDisk(
                "GitCommandsTests",
                "CheckoutBranchDirectoryWithOneDeepFile",
                "FolderDepth1",
                "FolderDepth2",
                "FolderDepth3",
                "File1.txt");
        }

        private static void CopyIndexAndRename(string indexPath)
        {
            string tempIndexPath = indexPath + ".lock";
            using (FileStream currentIndexStream = new FileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (FileStream tempIndexStream = new FileStream(tempIndexPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite))
            {
                currentIndexStream.CopyTo(tempIndexStream);
            }

            File.Delete(indexPath);
            File.Move(tempIndexPath, indexPath);
        }

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern SafeFileHandle CreateFile(
            [In] string fileName,
            [MarshalAs(UnmanagedType.U4)] NativeFileAccess desiredAccess,
            FileShare shareMode,
            [In] IntPtr securityAttributes,
            [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
            [MarshalAs(UnmanagedType.U4)] NativeFileAttributes flagsAndAttributes,
            [In] IntPtr templateFile);
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/CherryPickConflictTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class CherryPickConflictTests : GitRepoTests
    {
        public CherryPickConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        public void CherryPickConflict()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void CherryPickConflictWithFileReads()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ReadConflictTargetFiles();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void CherryPickConflictWithFileReads2()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ReadConflictTargetFiles();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchAfterConflict();
            this.ValidateGitCommand("cherry-pick --abort");
            this.FilesShouldMatchCheckoutOfTargetBranch();
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);
        }

        [TestCase]
        public void CherryPickConflict_ThenAbort()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("cherry-pick --abort");
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void CherryPickConflict_ThenSkip()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("cherry-pick --skip");
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void CherryPickConflict_UsingOurs()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("cherry-pick -Xours " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void CherryPickConflict_UsingTheirs()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("cherry-pick -Xtheirs " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void CherryPickNoCommit()
        {
            this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0");
            this.ValidateGitCommand("cherry-pick --no-commit " + GitRepoTests.ConflictTargetBranch);
        }

        [TestCase]
        public void CherryPickNoCommitReset()
        {
            this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0");
            this.ValidateGitCommand("cherry-pick --no-commit " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("reset");
        }

        protected override void CreateEnlistment()
        {
            base.CreateEnlistment();
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/CorruptionReproTests.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GVFS.Common;
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Tests.EnlistmentPerTestCase;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    /// 
    /// This class is used to reproduce corruption scenarios in the GVFS virtual projection.
    /// 
    [Category(Categories.GitCommands)]
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    public class CorruptionReproTests : GitRepoTests
    {
        public CorruptionReproTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        public void ReproCherryPickRestoreCorruption()
        {
            // Reproduces a corruption scenario where git commands (like cherry-pick -n)
            // stage changes directly, bypassing the filesystem. In VFS mode, these staged
            // files have skip-worktree set and are not in the ModifiedPaths database.
            // Without the fix, a subsequent "restore --staged" would fail to properly
            // unstage them, leaving the index and projection in an inconsistent state.
            //
            // See https://github.com/microsoft/VFSForGit/issues/1855

            // Based on FunctionalTests/20170206_Conflict_Source
            const string CherryPickCommit = "51d15f7584e81d59d44c1511ce17d7c493903390";
            const string StartingCommit = "db95d631e379d366d26d899523f8136a77441914";

            this.ControlGitRepo.Fetch(StartingCommit);
            this.ControlGitRepo.Fetch(CherryPickCommit);

            this.ValidateGitCommand($"checkout -b FunctionalTests/CherryPickRestoreCorruptionRepro {StartingCommit}");

            // Cherry-pick stages adds, deletes, and modifications without committing.
            // In VFS mode, these changes are made directly by git in the index — they
            // are not in ModifiedPaths, so all affected files still have skip-worktree set.
            this.ValidateGitCommand($"cherry-pick -n {CherryPickCommit}");

            // Restore --staged for a single file first. This verifies that only the
            // targeted file is added to ModifiedPaths, not all staged files (important
            // for performance when there are many staged files, e.g. during merge
            // conflict resolution).
            //
            // Before the fix: added files with skip-worktree would be skipped by
            // restore --staged, remaining stuck as staged in the index.
            this.ValidateGitCommand("restore --staged Test_ConflictTests/AddedFiles/AddedBySource.txt");

            // Restore --staged for everything remaining. Before the fix:
            // - Modified files: restored in the index but invisible to git status
            //   because skip-worktree was set and the file wasn't in ModifiedPaths,
            //   so git never checked the working tree against the index.
            // - Deleted files: same issue — deletions became invisible.
            // - Added files: remained stuck as staged because restore --staged
            //   skipped them (skip-worktree set), and their ProjFS placeholders
            //   would later vanish when the projection reverted to HEAD.
            this.ValidateGitCommand("restore --staged .");

            // Restore the working directory. Before the fix, this step would
            // silently succeed but leave corrupted state: modified/deleted files
            // had stale projected content that didn't match HEAD, and added files
            // (as ProjFS placeholders) would vanish entirely since they're not in
            // HEAD's tree.
            this.ValidateGitCommand("restore -- .");
            this.FilesShouldMatchCheckoutOfSourceBranch();
        }

        /// 
        /// Reproduction of a reported issue:
        /// Restoring a file after its parent directory was deleted fails with
        /// "fatal: could not unlink 'path\to\': Directory not empty"
        ///
        /// See https://github.com/microsoft/VFSForGit/issues/1901
        /// 
        [TestCase]
        public void RestoreAfterDeleteNesteredDirectory()
        {
            // Delete a directory with nested subdirectories and files.
            this.ValidateNonGitCommand("cmd.exe", "/c \"rmdir /s /q GVFlt_DeleteFileTest\"");

            // Restore the working directory.
            this.ValidateGitCommand("restore .");

            this.FilesShouldMatchCheckoutOfSourceBranch();
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/CreatePlaceholderTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using NUnit.Framework;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class CreatePlaceholderTests : GitRepoTests
    {
        private static readonly string FileToRead = Path.Combine("GVFS", "GVFS", "Program.cs");

        public CreatePlaceholderTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase("check-attr --stdin --all")]
        [TestCase("check-ignore --stdin")]
        [TestCase("check-mailmap --stdin")]
        [TestCase("diff-tree --stdin")]
        [TestCase("hash-object --stdin")]
        [TestCase("index-pack --stdin")]
        [TestCase("name-rev --stdin")]
        [TestCase("rev-list --stdin --quiet --all")]
        [TestCase("update-ref --stdin")]
        public void AllowsPlaceholderCreationWhileGitCommandIsRunning(string commandToRun)
        {
            this.CheckPlaceholderCreation(commandToRun, shouldAllow: true);
        }

        [TestCase("checkout-index --stdin")]
        [TestCase("fetch-pack --stdin URL")]
        [TestCase("notes copy --stdin")]
        [TestCase("reset --stdin")]
        [TestCase("send-pack --stdin URL")]
        [TestCase("update-index --stdin")]
        public void BlocksPlaceholderCreationWhileGitCommandIsRunning(string commandToRun)
        {
            this.CheckPlaceholderCreation(commandToRun, shouldAllow: false);
        }

        private void CheckPlaceholderCreation(string command, bool shouldAllow)
        {
            string eofCharacter = "\x04";
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                eofCharacter = "\x1A";
            }

            this.EditFile($"Some new content for {command}.", "Protocol.md");
            ManualResetEventSlim resetEvent = GitHelpers.RunGitCommandWithWaitAndStdIn(this.Enlistment, resetTimeout: 3000, command: $"{command}", stdinToQuit: eofCharacter, processId: out _);

            if (shouldAllow)
            {
                this.FileContentsShouldMatch(FileToRead);
            }
            else
            {
                string virtualPath = Path.Combine(this.Enlistment.RepoRoot, FileToRead);
                string controlPath = Path.Combine(this.ControlGitRepo.RootPath, FileToRead);
                virtualPath.ShouldNotExistOnDisk(this.FileSystem);
                controlPath.ShouldBeAFile(this.FileSystem);
            }

            this.ValidateGitCommand("--no-optional-locks status");
            resetEvent.Wait();
            this.RunGitCommand("reset --hard");
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/DeleteEmptyFolderTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class DeleteEmptyFolderTests : GitRepoTests
    {
        public DeleteEmptyFolderTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        [Ignore("This doesn't work right now. Tracking if this is a ProjFS problem. See #1696 for tracking.")]
        public void VerifyResetHardDeletesEmptyFolders()
        {
            this.SetupFolderDeleteTest();

            this.RunGitCommand("reset --hard HEAD");
            this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes);
        }

        [TestCase]
        public void VerifyCleanDeletesEmptyFolders()
        {
            this.SetupFolderDeleteTest();

            this.RunGitCommand("clean -fd");
            this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes);
        }

        private void SetupFolderDeleteTest()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_RenameTestMergeTarget");
            this.ValidateGitCommand("checkout FunctionalTests/20201014_RenameTestMergeTarget");
            this.DeleteFile("Test_EPF_GitCommandsTestOnlyFileFolder", "file.txt");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m\"Delete only file.\"");
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/EnumerationMergeTest.cs
================================================
using GVFS.FunctionalTests.Properties;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class EnumerationMergeTest : GitRepoTests
    {
        // Commit that found GvFlt Bug 12258777: Entries are sometimes skipped during
        // enumeration when they don't fit in a user's buffer
        private const string EnumerationReproCommitish = "FunctionalTests/20201014_EnumerationRepro";

        public EnumerationMergeTest(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        public void ConfirmEnumerationMatches()
        {
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);

            // Failure for ProjFS Bug 12258777 occurs during teardown, the calls above are to set up
            // the conditions to reproduce the bug
        }

        protected override void CreateEnlistment()
        {
            this.CreateEnlistment(EnumerationReproCommitish);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.IO;
using System.Runtime.CompilerServices;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class GitCommandsTests : GitRepoTests
    {
        public const string TopLevelFolderToCreate = "level1";
        private const string EncodingFileFolder = "FilenameEncoding";
        private const string EncodingFilename = "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt";
        private const string ContentWhenEditingFile = "// Adding a comment to the file";
        private const string UnknownTestName = "Unknown";
        private const string SubFolderToCreate = "level2";

        private static readonly string EditFilePath = Path.Combine("GVFS", "GVFS.Common", "GVFSContext.cs");
        private static readonly string DeleteFilePath = Path.Combine("GVFS", "GVFS", "Program.cs");
        private static readonly string RenameFilePathFrom = Path.Combine("GVFS", "GVFS.Common", "Physical", "FileSystem", "FileProperties.cs");
        private static readonly string RenameFilePathTo = Path.Combine("GVFS", "GVFS.Common", "Physical", "FileSystem", "FileProperties2.cs");
        private static readonly string RenameFolderPathFrom = Path.Combine("GVFS", "GVFS.Common", "PrefetchPacks");
        private static readonly string RenameFolderPathTo = Path.Combine("GVFS", "GVFS.Common", "PrefetchPacksRenamed");

        public GitCommandsTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: false, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        public void VerifyTestFilesExist()
        {
            // Sanity checks to ensure that the test files we expect to be in our test repo are present
            Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath).ShouldBeAFile(this.FileSystem);
            Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath).ShouldBeAFile(this.FileSystem);
            Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.DeleteFilePath).ShouldBeAFile(this.FileSystem);
            Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathFrom).ShouldBeAFile(this.FileSystem);
            Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFolderPathFrom).ShouldBeADirectory(this.FileSystem);
        }

        [TestCase]
        public void StatusTest()
        {
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void StatusShortTest()
        {
            this.ValidateGitCommand("status -s");
        }

        [TestCase]
        public void BranchTest()
        {
            this.ValidateGitCommand("branch");
        }

        [TestCase]
        public void NewBranchTest()
        {
            this.ValidateGitCommand("branch tests/functional/NewBranchTest");
            this.ValidateGitCommand("branch");
        }

        [TestCase]
        public void DeleteBranchTest()
        {
            this.ValidateGitCommand("branch tests/functional/DeleteBranchTest");
            this.ValidateGitCommand("branch");
            this.ValidateGitCommand("branch -d tests/functional/DeleteBranchTest");
            this.ValidateGitCommand("branch");
        }

        [TestCase]
        public void RenameCurrentBranchTest()
        {
            this.ValidateGitCommand("checkout -b tests/functional/RenameBranchTest");
            this.ValidateGitCommand("branch -m tests/functional/RenameBranchTest2");
            this.ValidateGitCommand("branch");
        }

        [TestCase]
        public void UntrackedFileTest()
        {
            this.BasicCommit(this.CreateFile, addCommand: "add .");
        }

        [TestCase]
        public void UntrackedEmptyFileTest()
        {
            this.BasicCommit(this.CreateEmptyFile, addCommand: "add .");
        }

        [TestCase]
        public void UntrackedFileAddAllTest()
        {
            this.BasicCommit(this.CreateFile, addCommand: "add --all");
        }

        [TestCase]
        public void UntrackedEmptyFileAddAllTest()
        {
            this.BasicCommit(this.CreateEmptyFile, addCommand: "add --all");
        }

        [TestCase]
        public void StageUntrackedFileTest()
        {
            this.BasicCommit(this.CreateFile, addCommand: "stage .");
        }

        [TestCase]
        public void StageUntrackedEmptyFileTest()
        {
            this.BasicCommit(this.CreateEmptyFile, addCommand: "stage .");
        }

        [TestCase]
        public void StageUntrackedFileAddAllTest()
        {
            this.BasicCommit(this.CreateFile, addCommand: "stage --all");
        }

        [TestCase]
        public void StageUntrackedEmptyFileAddAllTest()
        {
            this.BasicCommit(this.CreateEmptyFile, addCommand: "stage --all");
        }

        [TestCase]
        public void CheckoutNewBranchTest()
        {
            this.ValidateGitCommand("checkout -b tests/functional/CheckoutNewBranchTest");
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void CheckoutOrphanBranchTest()
        {
            this.ValidateGitCommand("checkout --orphan tests/functional/CheckoutOrphanBranchTest");
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void CreateFileSwitchBranchTest()
        {
            this.SwitchBranch(fileSystemAction: this.CreateFile);
        }

        [TestCase]
        public void CreateFileStageChangesSwitchBranchTest()
        {
            this.StageChangesSwitchBranch(fileSystemAction: this.CreateFile);
        }

        [TestCase]
        public void CreateFileCommitChangesSwitchBranchTest()
        {
            this.CommitChangesSwitchBranch(fileSystemAction: this.CreateFile);
        }

        [TestCase]
        public void CreateFileCommitChangesSwitchBranchSwitchBranchBackTest()
        {
            this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.CreateFile);
        }

        [TestCase]
        public void DeleteFileSwitchBranchTest()
        {
            this.SwitchBranch(fileSystemAction: this.DeleteFile);
        }

        [TestCase]
        public void DeleteFileStageChangesSwitchBranchTest()
        {
            this.StageChangesSwitchBranch(fileSystemAction: this.DeleteFile);
        }

        [TestCase]
        public void DeleteFileCommitChangesSwitchBranchTest()
        {
            this.CommitChangesSwitchBranch(fileSystemAction: this.DeleteFile);
        }

        [TestCase]
        public void DeleteFileCommitChangesSwitchBranchSwitchBackTest()
        {
            this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.DeleteFile);
        }

        [TestCase]
        public void DeleteFileCommitChangesSwitchBranchSwitchBackDeleteFolderTest()
        {
            // 663045 - Confirm that folder can be deleted after deleting file then changing
            // branches
            string deleteFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_DeleteOnClose", "NonEmptyFolder");
            string deleteFilePath = Path.Combine(deleteFolderPath, "bar.txt");

            this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: () => this.DeleteFile(deleteFilePath));
            this.DeleteFolder(deleteFolderPath);
        }

        [TestCase]
        public void DeleteFolderSwitchBranchTest()
        {
            this.SwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteLocalEmptyFolder_DeleteOnClose"));
        }

        [TestCase]
        public void DeleteFolderStageChangesSwitchBranchTest()
        {
            this.StageChangesSwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteLocalEmptyFolder_SetDisposition"));
        }

        [TestCase]
        public void DeleteFolderCommitChangesSwitchBranchTest()
        {
            this.CommitChangesSwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteNonRootVirtualFolder_DeleteOnClose"));
        }

        [TestCase]
        public void DeleteFolderCommitChangesSwitchBranchSwitchBackTest()
        {
            this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteNonRootVirtualFolder_SetDisposition"));
        }

        [TestCase]
        public void DeleteFilesWithNameAheadOfDot()
        {
            string folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "1");
            this.FolderShouldExistAndHaveFile(folder, "#test");
            this.DeleteFile(folder, "#test");
            this.FolderShouldExistAndBeEmpty(folder);

            folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "2");
            this.FolderShouldExistAndHaveFile(folder, "$test");
            this.DeleteFile(folder, "$test");
            this.FolderShouldExistAndBeEmpty(folder);

            folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "3");
            this.FolderShouldExistAndHaveFile(folder, ")");
            this.DeleteFile(folder, ")");
            this.FolderShouldExistAndBeEmpty(folder);

            folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "4");
            this.FolderShouldExistAndHaveFile(folder, "+.test");
            this.DeleteFile(folder, "+.test");
            this.FolderShouldExistAndBeEmpty(folder);

            folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "5");
            this.FolderShouldExistAndHaveFile(folder, "-.test");
            this.DeleteFile(folder, "-.test");
            this.FolderShouldExistAndBeEmpty(folder);

            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void RenameFilesWithNameAheadOfDot()
        {
            this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "1", "#test");
            this.MoveFile(
                Path.Combine("GitCommandsTests", "RenameFileTests", "1", "#test"),
                Path.Combine("GitCommandsTests", "RenameFileTests", "1", "#testRenamed"));

            this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "2", "$test");
            this.MoveFile(
                Path.Combine("GitCommandsTests", "RenameFileTests", "2", "$test"),
                Path.Combine("GitCommandsTests", "RenameFileTests", "2", "$testRenamed"));

            this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "3", ")");
            this.MoveFile(
                Path.Combine("GitCommandsTests", "RenameFileTests", "3", ")"),
                Path.Combine("GitCommandsTests", "RenameFileTests", "3", ")Renamed"));

            this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "4", "+.test");
            this.MoveFile(
                Path.Combine("GitCommandsTests", "RenameFileTests", "4", "+.test"),
                Path.Combine("GitCommandsTests", "RenameFileTests", "4", "+.testRenamed"));

            this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "5", "-.test");
            this.MoveFile(
                Path.Combine("GitCommandsTests", "RenameFileTests", "5", "-.test"),
                Path.Combine("GitCommandsTests", "RenameFileTests", "5", "-.testRenamed"));

            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void DeleteFileWithNameAheadOfDotAndSwitchCommits()
        {
            string fileRelativePath = Path.Combine("DeleteFileWithNameAheadOfDotAndSwitchCommits", "(1).txt");
            this.DeleteFile(fileRelativePath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("checkout -- DeleteFileWithNameAheadOfDotAndSwitchCommits/(1).txt");
            this.DeleteFile(fileRelativePath);
            this.ValidateGitCommand("status");

            // 14cf226119766146b1fa5c5aa4cd0896d05f6b63 is the commit prior to creating (1).txt, it has two different files with
            // names that start with '(':
            // (a).txt
            // (z).txt
            this.ValidateGitCommand("checkout 14cf226119766146b1fa5c5aa4cd0896d05f6b63");
            this.DeleteFile("DeleteFileWithNameAheadOfDotAndSwitchCommits", "(a).txt");
            this.ValidateGitCommand("checkout -- DeleteFileWithNameAheadOfDotAndSwitchCommits/(a).txt");
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack()
        {
            // 663045 - Confirm that folder can be deleted after adding a file then changing branches
            string newFileParentFolderPath = Path.Combine("GVFS", "GVFS", "CommandLine");
            string newFilePath = Path.Combine(newFileParentFolderPath, "testfile.txt");
            string newFileContents = "test contents";

            this.CommitChangesSwitchBranch(
                fileSystemAction: () => this.CreateFile(newFileContents, newFilePath),
                test: "AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack");

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.DeleteFolder(newFileParentFolderPath);

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.ValidateGitCommand("checkout tests/functional/AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack");

            this.FolderShouldExist(newFileParentFolderPath);
            this.FileShouldHaveContents(newFileContents, newFilePath);
        }

        [TestCase]
        public void OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack()
        {
            string overwrittenFileParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition");

            // GVFlt_DeleteFolderTest\GVFlt_DeletePlaceholderNonEmptyFolder_SetDispositiontestfile.txt already exists in the repo as TestFile.txt
            string fileToOverwritePath = Path.Combine(overwrittenFileParentFolderPath, "testfile.txt");
            string newFileContents = "test contents";

            this.CommitChangesSwitchBranch(
                fileSystemAction: () => this.CreateFile(newFileContents, fileToOverwritePath),
                test: "OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack");

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.DeleteFolder(overwrittenFileParentFolderPath);

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.ValidateGitCommand("checkout tests/functional/OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack");

            string subFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition", "NonEmptyFolder");
            this.ShouldNotExistOnDisk(subFolderPath);
            this.FolderShouldExist(overwrittenFileParentFolderPath);
            this.FileShouldHaveContents(newFileContents, fileToOverwritePath);
        }

        [TestCase]
        public void AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack()
        {
            // 663045 - Confirm that grandparent folder can be deleted after adding a (granchild) file
            // then changing branches
            string newFileParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose", "NonEmptyFolder");
            string newFileGrandParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose");
            string newFilePath = Path.Combine(newFileParentFolderPath, "testfile.txt");
            string newFileContents = "test contents";

            this.CommitChangesSwitchBranch(
                fileSystemAction: () => this.CreateFile(newFileContents, newFilePath),
                test: "AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack");

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.DeleteFolder(newFileGrandParentFolderPath);

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.ValidateGitCommand("checkout tests/functional/AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack");

            this.FolderShouldExist(newFileParentFolderPath);
            this.FolderShouldExist(newFileGrandParentFolderPath);
            this.FileShouldHaveContents(newFileContents, newFilePath);
        }

        [TestCase]
        public void CommitWithNewlinesInMessage()
        {
            this.ValidateGitCommand("checkout -b tests/functional/commit_with_uncommon_arguments");
            this.CreateFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Message that contains \na\nnew\nline\"");
        }

        [TestCase]
        public void CaseOnlyRenameFileAndChangeBranches()
        {
            // 693190 - Confirm that file does not disappear after case-only rename and branch
            // changes
            string newBranchName = "tests/functional/CaseOnlyRenameFileAndChangeBranches";
            string oldFileName = "Readme.md";
            string newFileName = "README.md";

            this.ValidateGitCommand("checkout -b " + newBranchName);
            this.ValidateGitCommand("mv {0} {1}", oldFileName, newFileName);
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for CaseOnlyRenameFileAndChangeBranches\"");
            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.FileShouldHaveCaseMatchingName(oldFileName);

            this.ValidateGitCommand("checkout " + newBranchName);
            this.FileShouldHaveCaseMatchingName(newFileName);
        }

        [TestCase]
        public void MoveFileFromOutsideRepoToInsideRepoAndAdd()
        {
            string testFileContents = "0123456789";
            string filename = "MoveFileFromOutsideRepoToInsideRepo.cs";

            // Create the test files in this.Enlistment.EnlistmentRoot as it's outside of src and the control
            // repo and is cleaned up when the functional tests run
            string oldFilePath = Path.Combine(this.Enlistment.EnlistmentRoot, filename);
            string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filename);
            string gvfsFilePath = Path.Combine(this.Enlistment.RepoRoot, filename);

            string newBranchName = "tests/functional/MoveFileFromOutsideRepoToInsideRepoAndAdd";
            this.ValidateGitCommand("checkout -b " + newBranchName);

            // Move file to control repo
            this.FileSystem.WriteAllText(oldFilePath, testFileContents);
            this.FileSystem.MoveFile(oldFilePath, controlFilePath);
            oldFilePath.ShouldNotExistOnDisk(this.FileSystem);
            controlFilePath.ShouldBeAFile(this.FileSystem).WithContents(testFileContents);

            // Move file to GVFS repo
            this.FileSystem.WriteAllText(oldFilePath, testFileContents);
            this.FileSystem.MoveFile(oldFilePath, gvfsFilePath);
            oldFilePath.ShouldNotExistOnDisk(this.FileSystem);
            gvfsFilePath.ShouldBeAFile(this.FileSystem).WithContents(testFileContents);

            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for MoveFileFromOutsideRepoToInsideRepoAndAdd\"");
            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
        }

        [TestCase]
        public void MoveFolderFromOutsideRepoToInsideRepoAndAdd()
        {
            string testFileContents = "0123456789";
            string filename = "MoveFolderFromOutsideRepoToInsideRepoAndAdd.cs";
            string folderName = "GitCommand_MoveFolderFromOutsideRepoToInsideRepoAndAdd";

            // Create the test folders in this.Enlistment.EnlistmentRoot as it's outside of src and the control
            // repo and is cleaned up when the functional tests run
            string oldFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, folderName);
            string oldFilePath = Path.Combine(this.Enlistment.EnlistmentRoot, folderName, filename);
            string controlFolderPath = Path.Combine(this.ControlGitRepo.RootPath, folderName);
            string gvfsFolderPath = Path.Combine(this.Enlistment.RepoRoot, folderName);

            string newBranchName = "tests/functional/MoveFolderFromOutsideRepoToInsideRepoAndAdd";
            this.ValidateGitCommand("checkout -b " + newBranchName);

            // Move folder to control repo
            this.FileSystem.CreateDirectory(oldFolderPath);
            this.FileSystem.WriteAllText(oldFilePath, testFileContents);
            this.FileSystem.MoveDirectory(oldFolderPath, controlFolderPath);
            oldFolderPath.ShouldNotExistOnDisk(this.FileSystem);
            Path.Combine(controlFolderPath, filename).ShouldBeAFile(this.FileSystem).WithContents(testFileContents);

            // Move folder to GVFS repo
            this.FileSystem.CreateDirectory(oldFolderPath);
            this.FileSystem.WriteAllText(oldFilePath, testFileContents);
            this.FileSystem.MoveDirectory(oldFolderPath, gvfsFolderPath);
            oldFolderPath.ShouldNotExistOnDisk(this.FileSystem);
            Path.Combine(gvfsFolderPath, filename).ShouldBeAFile(this.FileSystem).WithContents(testFileContents);

            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for MoveFolderFromOutsideRepoToInsideRepoAndAdd\"");
            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
        }

        [TestCase]
        public void MoveFileFromInsideRepoToOutsideRepoAndCommit()
        {
            string newBranchName = "tests/functional/MoveFileFromInsideRepoToOutsideRepoAndCommit";
            this.ValidateGitCommand("checkout -b " + newBranchName);

            // Confirm that no other test has caused "Protocol.md" to be added to the modified paths
            string fileName = "Protocol.md";
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.FileSystem, fileName);

            string controlTargetFolder = "MoveFileFromInsideRepoToOutsideRepoAndCommit_ControlTarget";
            string gvfsTargetFolder = "MoveFileFromInsideRepoToOutsideRepoAndCommit_GVFSTarget";

            // Create the target folders in this.Enlistment.EnlistmentRoot as it's outside of src and the control repo
            // and is cleaned up when the functional tests run
            string controlTargetFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, controlTargetFolder);
            string gvfsTargetFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, gvfsTargetFolder);
            string controlTargetFilePath = Path.Combine(controlTargetFolderPath, fileName);
            string gvfsTargetFilePath = Path.Combine(gvfsTargetFolderPath, fileName);

            // Move control repo file
            this.FileSystem.CreateDirectory(controlTargetFolderPath);
            this.FileSystem.MoveFile(Path.Combine(this.ControlGitRepo.RootPath, fileName), controlTargetFilePath);
            controlTargetFilePath.ShouldBeAFile(this.FileSystem);

            // Move GVFS repo file
            this.FileSystem.CreateDirectory(gvfsTargetFolderPath);
            this.FileSystem.MoveFile(Path.Combine(this.Enlistment.RepoRoot, fileName), gvfsTargetFilePath);
            gvfsTargetFilePath.ShouldBeAFile(this.FileSystem);

            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for MoveFileFromInsideRepoToOutsideRepoAndCommit\"");
            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
        }

        [TestCase]
        public void EditFileSwitchBranchTest()
        {
            this.SwitchBranch(fileSystemAction: this.EditFile);
        }

        [TestCase]
        public void EditFileStageChangesSwitchBranchTest()
        {
            this.StageChangesSwitchBranch(fileSystemAction: this.EditFile);
        }

        [TestCase]
        public void EditFileCommitChangesSwitchBranchTest()
        {
            this.CommitChangesSwitchBranch(fileSystemAction: this.EditFile);
        }

        [TestCase]
        public void EditFileCommitChangesSwitchBranchSwitchBackTest()
        {
            this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.EditFile);
        }

        [TestCase]
        public void RenameFileCommitChangesSwitchBranchSwitchBackTest()
        {
            this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.RenameFile);
        }

        [TestCase]
        public void AddFileCommitThenDeleteAndCommit()
        {
            this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndCommit_before");
            this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndCommit_after");
            string filePath = Path.Combine("GVFS", "testfile.txt");
            this.CreateFile("Some new content for the file", filePath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\"");
            this.DeleteFile(filePath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Delete file for AddFileCommitThenDeleteAndCommit\"");
            this.ValidateGitCommand("checkout tests/functional/AddFileCommitThenDeleteAndCommit_before");
            this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
               .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes);
            this.ValidateGitCommand("checkout tests/functional/AddFileCommitThenDeleteAndCommit_after");
        }

        [TestCase]
        public void AddFileCommitThenDeleteAndResetSoft()
        {
            this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft");
            string filePath = Path.Combine("GVFS", "testfile.txt");
            this.CreateFile("Some new content for the file", filePath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\"");
            this.DeleteFile(filePath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("reset --soft HEAD~1");
        }

        [TestCase]
        public void AddFileCommitThenDeleteAndResetMixed()
        {
            this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft");
            string filePath = Path.Combine("GVFS", "testfile.txt");
            this.CreateFile("Some new content for the file", filePath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\"");
            this.DeleteFile(filePath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("reset --soft HEAD~1");
        }

        [TestCase]
        public void AddFolderAndFileCommitThenDeleteAndResetSoft()
        {
            this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft");
            string folderPath = "test_folder";
            this.CreateFolder(folderPath);
            string filePath = Path.Combine(folderPath, "testfile.txt");
            this.CreateFile("Some new content for the file", filePath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\"");
            this.DeleteFile(filePath);
            this.DeleteFolder(folderPath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("reset --soft HEAD~1");
        }

        [TestCase]
        public void AddFolderAndFileCommitThenDeleteAndResetMixed()
        {
            this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft");
            string folderPath = "test_folder";
            this.CreateFolder(folderPath);
            string filePath = Path.Combine(folderPath, "testfile.txt");
            this.CreateFile("Some new content for the file", filePath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\"");
            this.DeleteFile(filePath);
            this.DeleteFolder(folderPath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("reset --mixed HEAD~1");
        }

        [TestCase]
        public void AddFolderAndFileCommitThenResetSoftAndResetHard()
        {
            this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft");
            string folderPath = "test_folder";
            this.CreateFolder(folderPath);
            string filePath = Path.Combine(folderPath, "testfile.txt");
            this.CreateFile("Some new content for the file", filePath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\"");
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("reset --soft HEAD~1");
            this.ValidateGitCommand("reset --hard HEAD");
        }

        [TestCase]
        public void AddFolderAndFileCommitThenResetSoftAndResetMixed()
        {
            this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft");
            string folderPath = "test_folder";
            this.CreateFolder(folderPath);
            string filePath = Path.Combine(folderPath, "testfile.txt");
            this.CreateFile("Some new content for the file", filePath);
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\"");
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("reset --soft HEAD~1");
            this.ValidateGitCommand("reset --mixed HEAD");
        }

        [TestCase]
        public void AddFoldersAndFilesAndRenameFolder()
        {
            this.ValidateGitCommand("checkout -b tests/functional/AddFoldersAndFilesAndRenameFolder");

            string topMostNewFolder = "AddFoldersAndFilesAndRenameFolder_Test";
            this.CreateFolder(topMostNewFolder);
            this.CreateFile("test contents", topMostNewFolder, "top_level_test_file.txt");

            string testFolderLevel1 = Path.Combine(topMostNewFolder, "TestFolderLevel1");
            this.CreateFolder(testFolderLevel1);
            this.CreateFile("test contents", testFolderLevel1, "level_1_test_file.txt");

            string testFolderLevel2 = Path.Combine(testFolderLevel1, "TestFolderLevel2");
            this.CreateFolder(testFolderLevel2);
            this.CreateFile("test contents", testFolderLevel2, "level_2_test_file.txt");

            string testFolderLevel3 = Path.Combine(testFolderLevel2, "TestFolderLevel3");
            this.CreateFolder(testFolderLevel3);
            this.CreateFile("test contents", testFolderLevel3, "level_3_test_file.txt");
            this.ValidateGitCommand("status");

            this.MoveFolder(testFolderLevel3, Path.Combine(testFolderLevel2, "TestFolderLevel3Renamed"));
            this.ValidateGitCommand("status");

            this.MoveFolder(testFolderLevel2, Path.Combine(testFolderLevel1, "TestFolderLevel2Renamed"));
            this.ValidateGitCommand("status");

            this.MoveFolder(testFolderLevel1, Path.Combine(topMostNewFolder, "TestFolderLevel1Renamed"));
            this.ValidateGitCommand("status");

            this.MoveFolder(topMostNewFolder, "AddFoldersAndFilesAndRenameFolder_TestRenamed");
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void AddFileAfterFolderRename()
        {
            this.ValidateGitCommand("checkout -b tests/functional/AddFileAfterFolderRename");

            string folder = "AddFileAfterFolderRename_Test";
            string renamedFolder = "AddFileAfterFolderRename_TestRenamed";
            this.CreateFolder(folder);
            this.MoveFolder(folder, renamedFolder);
            this.CreateFile("test contents", renamedFolder, "test_file.txt");
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void ResetSoft()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetSoft");
            this.ValidateGitCommand("reset --soft HEAD~1");
        }

        [TestCase]
        public void ResetMixed()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetMixed");
            this.ValidateGitCommand("reset --mixed HEAD~1");
        }

        [TestCase]
        public void ResetMixed2()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetMixed2");
            this.ValidateGitCommand("reset HEAD~1");
        }

        [TestCase]
        public void ManuallyModifyHead()
        {
            this.ValidateGitCommand("status");
            this.ReplaceText("f1bce402a7a980a8320f3f235cf8c8fdade4b17a", TestConstants.DotGit.Head);
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void ResetSoftTwice()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetSoftTwice");

            // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and
            // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2
            this.ValidateGitCommand("reset --soft 60d19c87328120d11618ad563c396044a50985b2");
            this.ValidateGitCommand("reset --soft 99fc72275f950b0052c8548bbcf83a851f2b4467");
        }

        [TestCase]
        public void ResetMixedTwice()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetMixedTwice");

            // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and
            // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2
            this.ValidateGitCommand("reset --mixed 60d19c87328120d11618ad563c396044a50985b2");
            this.ValidateGitCommand("reset --mixed 99fc72275f950b0052c8548bbcf83a851f2b4467");
        }

        [TestCase]
        public void ResetMixed2Twice()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetMixed2Twice");

            // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and
            // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2
            this.ValidateGitCommand("reset 60d19c87328120d11618ad563c396044a50985b2");
            this.ValidateGitCommand("reset 99fc72275f950b0052c8548bbcf83a851f2b4467");
        }

        [TestCase]
        public void ResetHardAfterCreate()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterCreate");
            this.CreateFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("reset --hard HEAD");
        }

        [TestCase]
        public void ResetHardAfterEdit()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterEdit");
            this.EditFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("reset --hard HEAD");
        }

        [TestCase]
        public void ResetHardAfterDelete()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterDelete");
            this.DeleteFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("reset --hard HEAD");
        }

        [TestCase]
        public void ResetHardAfterCreateAndAdd()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterCreateAndAdd");
            this.CreateFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.ValidateGitCommand("reset --hard HEAD");
        }

        [TestCase]
        public void ResetHardAfterEditAndAdd()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterEditAndAdd");
            this.EditFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.ValidateGitCommand("reset --hard HEAD");
        }

        [TestCase]
        public void ResetHardAfterDeleteAndAdd()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterDeleteAndAdd");
            this.DeleteFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.ValidateGitCommand("reset --hard HEAD");
        }

        [TestCase]
        public void ChangeTwoBranchesAndMerge()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ChangeTwoBranchesAndMerge_1");
            this.EditFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for ChangeTwoBranchesAndMerge first branch\"");

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.ValidateGitCommand("checkout -b tests/functional/ChangeTwoBranchesAndMerge_2");
            this.DeleteFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for ChangeTwoBranchesAndMerge second branch\"");
            this.ValidateGitCommand("merge tests/functional/ChangeTwoBranchesAndMerge_1");
        }

        [TestCase]
        public void ChangeBranchAndCherryPickIntoAnotherBranch()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchesAndCherryPickIntoAnotherBranch_1");
            this.CreateFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Create for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\"");
            this.DeleteFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Delete for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\"");
            this.ValidateGitCommand("tag DeleteForCherryPick");
            this.EditFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Edit for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\"");

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchesAndCherryPickIntoAnotherBranch_2");
            this.RunGitCommand("cherry-pick -q DeleteForCherryPick");
        }

        [TestCase]
        public void ChangeBranchAndMergeRebaseOnAnotherBranch()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_1");
            this.CreateFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Create for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\"");
            this.DeleteFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Delete for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\"");

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_2");
            this.EditFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Edit for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\"");

            this.RunGitCommand("rebase --merge tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_1", ignoreErrors: true);
            this.ValidateGitCommand("rev-parse HEAD^{{tree}}");
        }

        [TestCase]
        public void ChangeBranchAndRebaseOnAnotherBranch()
        {
            this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndRebaseOnAnotherBranch_1");
            this.CreateFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Create for ChangeBranchAndRebaseOnAnotherBranch first branch\"");
            this.DeleteFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Delete for ChangeBranchAndRebaseOnAnotherBranch first branch\"");

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndRebaseOnAnotherBranch_2");
            this.EditFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Edit for ChangeBranchAndRebaseOnAnotherBranch first branch\"");

            this.RunGitCommand("rebase tests/functional/ChangeBranchAndRebaseOnAnotherBranch_1", ignoreErrors: true);
            this.ValidateGitCommand("rev-parse HEAD^{{tree}}");
        }

        [TestCase]
        public void StashChanges()
        {
            this.ValidateGitCommand("checkout -b tests/functional/StashChanges");
            this.EditFile();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.ValidateGitCommand("stash");

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.ValidateGitCommand("checkout -b tests/functional/StashChanges_2");
            this.RunGitCommand("stash pop");
        }

        [TestCase]
        public void OpenFileThenCheckout()
        {
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.EditFilePath);

            // Open files with ReadWrite sharing because depending on the state of the index (and the mtimes), git might need to read the file
            // as part of status (while we have the handle open).
            using (FileStream virtualFS = File.Open(virtualFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite))
            using (StreamWriter virtualWriter = new StreamWriter(virtualFS))
            using (FileStream controlFS = File.Open(controlFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite))
            using (StreamWriter controlWriter = new StreamWriter(controlFS))
            {
                this.ValidateGitCommand("checkout -b tests/functional/OpenFileThenCheckout");
                virtualWriter.WriteLine("// Adding a line for testing purposes");
                controlWriter.WriteLine("// Adding a line for testing purposes");
                this.ValidateGitCommand("status");
            }

            // NOTE: Due to optimizations in checkout -b, the modified files will not be included as part of the
            // success message.  Validate that the succcess messages match, and the call to validate "status" below
            // will ensure that GVFS is still reporting the edited file as modified.

            string controlRepoRoot = this.ControlGitRepo.RootPath;
            string gvfsRepoRoot = this.Enlistment.RepoRoot;
            string command = "checkout -b tests/functional/OpenFileThenCheckout_2";
            ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command);
            ProcessResult actualResult = GitHelpers.InvokeGitAgainstGVFSRepo(gvfsRepoRoot, command);
            GitHelpers.ErrorsShouldMatch(command, expectedResult, actualResult);
            actualResult.Errors.ShouldContain("Switched to a new branch");

            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void EditFileNeedingUtf8Encoding()
        {
            this.ValidateGitCommand("checkout -b tests/functional/EditFileNeedingUtf8Encoding");
            this.ValidateGitCommand("status");

            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, EncodingFileFolder, EncodingFilename);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, EncodingFileFolder, EncodingFilename);
            string relativeGitPath = EncodingFileFolder + "/" + EncodingFilename;

            string contents = virtualFile.ShouldBeAFile(this.FileSystem).WithContents();
            string expectedContents = controlFile.ShouldBeAFile(this.FileSystem).WithContents();
            contents.ShouldEqual(expectedContents);

            // Confirm that the entry is not in the the modified paths database
            GVFSHelpers.ModifiedPathsShouldNotContain(this.Enlistment, this.FileSystem, relativeGitPath);
            this.ValidateGitCommand("status");

            this.AppendAllText(ContentWhenEditingFile, virtualFile);
            this.AppendAllText(ContentWhenEditingFile, controlFile);

            this.ValidateGitCommand("status");

            // Confirm that the entry was added to the modified paths database
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.FileSystem, relativeGitPath);
        }

        [TestCase]
        public void ChangeTimestampAndDiff()
        {
            // User scenario -
            // 1. Enlistment's "diff.autoRefreshIndex" config is set to false
            // 2. A checked out file got into a state where it differs from the git copy
            // only in its LastWriteTime metadata (no change in file contents.)
            // Repro steps - This happens when user edits a file, saves it and later decides
            // to undo the edit and save the file again.
            // Once in this state, the unchanged file (only its timestamp has changed) shows
            // up in `git difftool` creating noise. It also shows up in `git diff --raw` command,
            // (but not in `git status` or `git diff`.)

            // Change the timestamp - The lastwrite time can be close to the time this test method gets
            // run. Changing (Subtracting) it to the past so there will always be a difference.
            this.AdjustLastWriteTime(GitCommandsTests.EditFilePath, TimeSpan.FromDays(-10));
            this.ValidateGitCommand("diff --raw");
            this.ValidateGitCommand($"checkout {GitCommandsTests.EditFilePath}");
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void UseAlias()
        {
            this.ValidateGitCommand("config --local alias.potato status");
            this.ValidateGitCommand("potato");
        }

        [TestCase]
        public void RenameOnlyFileInFolder()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_RenameTestMergeTarget");
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_RenameTestMergeSource");

            this.ValidateGitCommand("checkout FunctionalTests/20201014_RenameTestMergeTarget");
            this.FileSystem.ReadAllText(this.Enlistment.GetVirtualPathTo("Test_EPF_GitCommandsTestOnlyFileFolder", "file.txt"));
            this.ValidateGitCommand("merge origin/FunctionalTests/20201014_RenameTestMergeSource");
        }

        [TestCase]
        public void BlameTest()
        {
            this.ValidateGitCommand("blame Readme.md");
        }

        private void BasicCommit(Action fileSystemAction, string addCommand, [CallerMemberName]string test = GitCommandsTests.UnknownTestName)
        {
            this.ValidateGitCommand($"checkout -b tests/functional/{test}");
            fileSystemAction();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand(addCommand);
            this.RunGitCommand($"commit -m \"BasicCommit for {test}\"");
        }

        private void SwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName)
        {
            this.ValidateGitCommand("checkout -b tests/functional/{0}", test);
            fileSystemAction();
            this.ValidateGitCommand("status");
        }

        private void StageChangesSwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName)
        {
            this.ValidateGitCommand("checkout -b tests/functional/{0}", test);
            fileSystemAction();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
        }

        private void CommitChangesSwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName)
        {
            this.ValidateGitCommand("checkout -b tests/functional/{0}", test);
            fileSystemAction();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for {0}\"", test);
        }

        private void CommitChangesSwitchBranchSwitchBack(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName)
        {
            string branch = string.Format("tests/functional/{0}", test);
            this.ValidateGitCommand("checkout -b {0}", branch);
            fileSystemAction();
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("add .");
            this.RunGitCommand("commit -m \"Change for {0}\"", branch);
            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);
            this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes);

            this.ValidateGitCommand("checkout {0}", branch);
        }

        private void CreateFile()
        {
            this.CreateFile("Some content here", Path.GetRandomFileName() + "tempFile.txt");
            this.CreateFolder(TopLevelFolderToCreate);
            this.CreateFolder(Path.Combine(TopLevelFolderToCreate, SubFolderToCreate));
            this.CreateFile("File in new folder", Path.Combine(TopLevelFolderToCreate, SubFolderToCreate, Path.GetRandomFileName() + "folderFile.txt"));
        }

        private void EditFile()
        {
            this.AppendAllText(ContentWhenEditingFile, GitCommandsTests.EditFilePath);
        }

        private void DeleteFile()
        {
            this.DeleteFile(GitCommandsTests.DeleteFilePath);
        }

        private void RenameFile()
        {
            string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathFrom);
            string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathTo);
            string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.RenameFilePathFrom);
            string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.RenameFilePathTo);
            this.FileSystem.MoveFile(virtualFileFrom, virtualFileTo);
            this.FileSystem.MoveFile(controlFileFrom, controlFileTo);
            virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem);
            controlFileFrom.ShouldNotExistOnDisk(this.FileSystem);
        }

        private void MoveFolder()
        {
            this.MoveFolder(GitCommandsTests.RenameFolderPathFrom, GitCommandsTests.RenameFolderPathTo);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitRepoTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.IO;
using System.Linq;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixture]
    public abstract class GitRepoTests
    {
        protected const string ConflictSourceBranch = "FunctionalTests/20201014_Conflict_Source";
        protected const string ConflictTargetBranch = "FunctionalTests/20201014_Conflict_Target";
        protected const string NoConflictSourceBranch = "FunctionalTests/20201014_NoConflict_Source";
        protected const string DirectoryWithFileBeforeBranch = "FunctionalTests/20201014_DirectoryWithFileBefore";
        protected const string DirectoryWithFileAfterBranch = "FunctionalTests/20201014_DirectoryWithFileAfter";
        protected const string DirectoryWithDifferentFileAfterBranch = "FunctionalTests/20201014_DirectoryWithDifferentFile";
        protected const string DeepDirectoryWithOneFile = "FunctionalTests/20201014_DeepFolderOneFile";
        protected const string DeepDirectoryWithOneDifferentFile = "FunctionalTests/20201014_DeepFolderOneDifferentFile";

        protected string[] pathPrefixes;

        // These are the folders for the sparse mode that are needed for the functional tests
        // because they are the folders that the tests rely on to be there.
        private static readonly string[] SparseModeFolders = new string[]
        {
            "a",
            "AddFileAfterFolderRename_Test",
            "AddFileAfterFolderRename_TestRenamed",
            "AddFoldersAndFilesAndRenameFolder_Test",
            "AddFoldersAndFilesAndRenameFolder_TestRenamed",
            "c",
            "CheckoutNewBranchFromStartingPointTest",
            "CheckoutOrhpanBranchFromStartingPointTest",
            "d",
            "DeleteFileWithNameAheadOfDotAndSwitchCommits",
            "EnumerateAndReadTestFiles",
            "ErrorWhenPathTreatsFileAsFolderMatchesNTFS",
            "file.txt", // Changes to a folder in one test
            "foo.cpp", // Changes to a folder in one test
            "FilenameEncoding",
            "GitCommandsTests",
            "GVFLT_MultiThreadTest", // Required by DeleteFolderAndChangeBranchToFolderWithDifferentCase test in sparse mode
            "GVFlt_BugRegressionTest",
            "GVFlt_DeleteFileTest",
            "GVFlt_DeleteFolderTest",
            "GVFlt_EnumTest",
            "GVFlt_FileAttributeTest",
            "GVFlt_FileEATest",
            "GVFlt_FileOperationTest",
            "GVFlt_MoveFileTest",
            "GVFlt_MoveFolderTest",
            "GVFlt_MultiThreadTest",
            "GVFlt_SetLinkTest",
            Path.Combine("GVFS", "GVFS"),
            Path.Combine("GVFS", "GVFS.Common"),
            GitCommandsTests.TopLevelFolderToCreate,
            "ResetTwice_OnlyDeletes_Test",
            "ResetTwice_OnlyEdits_Test",
            "Test_ConflictTests",
            "Test_EPF_GitCommandsTestOnlyFileFolder",
            "Test_EPF_MoveRenameFileTests",
            "Test_EPF_MoveRenameFileTests_2",
            "Test_EPF_MoveRenameFolderTests",
            "Test_EPF_UpdatePlaceholderTests",
            "Test_EPF_WorkingDirectoryTests",
            "test_folder",
            "TrailingSlashTests",
        };

        // Add directory separator for matching paths since they should be directories
        private static readonly string[] PathPrefixesForSparseMode = SparseModeFolders.Select(x => x + Path.DirectorySeparatorChar).ToArray();

        private bool enlistmentPerTest;
        private Settings.ValidateWorkingTreeMode validateWorkingTree;

        public GitRepoTests(bool enlistmentPerTest, Settings.ValidateWorkingTreeMode validateWorkingTree)
        {
            this.enlistmentPerTest = enlistmentPerTest;
            this.validateWorkingTree = validateWorkingTree;
            this.FileSystem = new SystemIORunner();
        }

        public static object[] ValidateWorkingTree
        {
            get
            {
                return GVFSTestConfig.GitRepoTestsValidateWorkTree;
            }
        }

        public ControlGitRepo ControlGitRepo
        {
            get; private set;
        }

        protected FileSystemRunner FileSystem
        {
            get; private set;
        }

        protected GVFSFunctionalTestEnlistment Enlistment
        {
            get; private set;
        }

        [OneTimeSetUp]
        public virtual void SetupForFixture()
        {
            if (!this.enlistmentPerTest)
            {
                this.CreateEnlistment();
            }
        }

        [OneTimeTearDown]
        public virtual void TearDownForFixture()
        {
            if (!this.enlistmentPerTest)
            {
                this.DeleteEnlistment();
            }
        }

        [SetUp]
        public virtual void SetupForTest()
        {
            if (this.enlistmentPerTest)
            {
                this.CreateEnlistment();
            }

            if (this.validateWorkingTree == Settings.ValidateWorkingTreeMode.SparseMode)
            {
                new GVFSProcess(this.Enlistment).AddSparseFolders(SparseModeFolders);
                this.pathPrefixes = PathPrefixesForSparseMode;
            }

            this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);

            this.CheckHeadCommitTree();

            if (this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None)
            {
                this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                    .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes);
            }

            this.ValidateGitCommand("status");
        }

        [TearDown]
        public virtual void TearDownForTest()
        {
            this.TestValidationAndCleanup();
        }

        protected void TestValidationAndCleanup(bool ignoreCase = false)
        {
            try
            {
                this.CheckHeadCommitTree();

                if (this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None)
                {
                    this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                        .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, ignoreCase: ignoreCase, withinPrefixes: this.pathPrefixes);
                }

                this.RunGitCommand("reset --hard -q HEAD");
                this.RunGitCommand("clean -d -f -x");
                this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish);

                this.CheckHeadCommitTree();

                // If enlistmentPerTest is true we can always validate the working tree because
                // this is the last place we'll use it
                if ((this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None) || this.enlistmentPerTest)
                {
                    this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                        .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, ignoreCase: ignoreCase, withinPrefixes: this.pathPrefixes);
                }
            }
            finally
            {
                if (this.enlistmentPerTest)
                {
                    this.DeleteEnlistment();
                }
            }
        }

        protected virtual void CreateEnlistment()
        {
            this.CreateEnlistment(null);
        }

        protected void CreateEnlistment(string commitish = null)
        {
            this.Enlistment = GVFSFunctionalTestEnlistment.CloneAndMount(GVFSTestConfig.PathToGVFS, commitish: commitish);
            GitProcess.Invoke(this.Enlistment.RepoRoot, "config advice.statusUoption false");
            GitProcess.Invoke(this.Enlistment.RepoRoot, "config core.editor true");
            this.ControlGitRepo = ControlGitRepo.Create(commitish);
            this.ControlGitRepo.Initialize();
        }

        protected virtual void DeleteEnlistment()
        {
            if (this.Enlistment != null)
            {
                this.Enlistment.UnmountAndDeleteAll();
            }

            if (this.ControlGitRepo != null)
            {
                RepositoryHelpers.DeleteTestDirectory(this.ControlGitRepo.RootPath);
            }
        }

        protected void CheckHeadCommitTree()
        {
            this.ValidateGitCommand("ls-tree HEAD");
        }

        protected void RunGitCommand(string command, params object[] args)
        {
            this.RunGitCommand(string.Format(command, args));
        }

        /* We are using the following method for these scenarios
         * 1. Some commands compute a new commit sha, which is dependent on time and therefore
         *    won't match what is in the control repo.  For those commands, we just ensure that
         *    the errors match what we expect, but we skip comparing the output
         * 2. Using the sparse-checkout feature git will error out before checking the untracked files
         *    so the control repo will show the untracked files as being overwritten while the GVFS
         *    repo which is using the sparse-checkout will not.
         * 3. GVFS is returning not found for files that are outside the sparse-checkout and there
         *    are cases when git will delete these files during a merge outputting that it removed them
         *    which the GVFS repo did not have to remove so the message is missing that output.
         */
        protected void RunGitCommand(string command, bool ignoreErrors = false, bool checkStatus = true)
        {
            string controlRepoRoot = this.ControlGitRepo.RootPath;
            string gvfsRepoRoot = this.Enlistment.RepoRoot;

            ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command);
            ProcessResult actualResult = GitHelpers.InvokeGitAgainstGVFSRepo(gvfsRepoRoot, command);
            if (!ignoreErrors)
            {
                GitHelpers.ErrorsShouldMatch(command, expectedResult, actualResult);
            }

            if (command != "status" && checkStatus)
            {
                this.ValidateGitCommand("status");
            }
        }

        protected void ValidateGitCommand(string command, params object[] args)
        {
            GitHelpers.ValidateGitCommand(
                this.Enlistment,
                this.ControlGitRepo,
                command,
                args);
        }

        protected void ValidateNonGitCommand(string command, string args = "", bool ignoreErrors = false, bool checkStatus = true)
        {
            string controlRepoRoot = this.ControlGitRepo.RootPath;
            string gvfsRepoRoot = this.Enlistment.RepoRoot;

            ProcessResult expectedResult = ProcessHelper.Run(command, args, controlRepoRoot);
            ProcessResult actualResult = ProcessHelper.Run(command, args, gvfsRepoRoot);
            if (!ignoreErrors)
            {
                GitHelpers.ErrorsShouldMatch(command, expectedResult, actualResult);
            }
            if (checkStatus)
            {
                this.ValidateGitCommand("status");
            }
        }

        protected void ChangeMode(string filePath, ushort mode)
        {
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath);
            this.FileSystem.ChangeMode(virtualFile, mode);
            this.FileSystem.ChangeMode(controlFile, mode);
        }

        protected void CreateEmptyFile()
        {
            string filePath = Path.GetRandomFileName() + "emptyFile.txt";
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath);
            this.FileSystem.CreateEmptyFile(virtualFile);
            this.FileSystem.CreateEmptyFile(controlFile);
        }

        protected void CreateFile(string content, params string[] filePathPaths)
        {
            string filePath = Path.Combine(filePathPaths);
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath);
            this.FileSystem.WriteAllText(virtualFile, content);
            this.FileSystem.WriteAllText(controlFile, content);
        }

        protected void CreateFileWithoutClose(string path)
        {
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, path);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, path);
            this.FileSystem.CreateFileWithoutClose(virtualFile);
            this.FileSystem.CreateFileWithoutClose(controlFile);
        }

        protected void ReadFileAndWriteWithoutClose(string path, string contents)
        {
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, path);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, path);
            this.FileSystem.ReadAllText(virtualFile);
            this.FileSystem.ReadAllText(controlFile);
            this.FileSystem.OpenFileAndWriteWithoutClose(virtualFile, contents);
            this.FileSystem.OpenFileAndWriteWithoutClose(controlFile, contents);
        }

        protected void CreateFolder(string folderPath)
        {
            string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath);
            string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath);
            this.FileSystem.CreateDirectory(virtualFolder);
            this.FileSystem.CreateDirectory(controlFolder);
        }

        protected void EditFile(string content, params string[] filePathParts)
        {
            string filePath = Path.Combine(filePathParts);
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath);
            this.FileSystem.AppendAllText(virtualFile, content);
            this.FileSystem.AppendAllText(controlFile, content);
        }

        protected void CreateHardLink(string newLinkFileName, string existingFileName)
        {
            string virtualExistingFile = Path.Combine(this.Enlistment.RepoRoot, existingFileName);
            string controlExistingFile = Path.Combine(this.ControlGitRepo.RootPath, existingFileName);
            string virtualNewLinkFile = Path.Combine(this.Enlistment.RepoRoot, newLinkFileName);
            string controlNewLinkFile = Path.Combine(this.ControlGitRepo.RootPath, newLinkFileName);

            this.FileSystem.CreateHardLink(virtualNewLinkFile, virtualExistingFile);
            this.FileSystem.CreateHardLink(controlNewLinkFile, controlExistingFile);
        }

        protected void SetFileAsReadOnly(string filePath)
        {
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath);

            File.SetAttributes(virtualFile, File.GetAttributes(virtualFile) | FileAttributes.ReadOnly);
            File.SetAttributes(virtualFile, File.GetAttributes(controlFile) | FileAttributes.ReadOnly);
        }

        protected void AdjustLastWriteTime(string filePath, TimeSpan timestamp)
        {
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath);

            File.SetLastWriteTime(virtualFile, File.GetLastWriteTime(virtualFile).Add(timestamp));
            File.SetLastWriteTime(controlFile, File.GetLastWriteTime(controlFile).Add(timestamp));
        }

        protected void MoveFile(string pathFrom, string pathTo)
        {
            string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, pathFrom);
            string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, pathTo);
            string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, pathFrom);
            string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, pathTo);
            this.FileSystem.MoveFile(virtualFileFrom, virtualFileTo);
            this.FileSystem.MoveFile(controlFileFrom, controlFileTo);
            virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem);
            controlFileFrom.ShouldNotExistOnDisk(this.FileSystem);
            virtualFileTo.ShouldBeAFile(this.FileSystem);
            controlFileTo.ShouldBeAFile(this.FileSystem);
        }

        protected void DeleteFile(params string[] filePathParts)
        {
            string filePath = Path.Combine(filePathParts);
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath);
            this.FileSystem.DeleteFile(virtualFile);
            this.FileSystem.DeleteFile(controlFile);
            virtualFile.ShouldNotExistOnDisk(this.FileSystem);
            controlFile.ShouldNotExistOnDisk(this.FileSystem);
        }

        protected void DeleteFolder(params string[] folderPathParts)
        {
            string folderPath = Path.Combine(folderPathParts);
            string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath);
            string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath);
            this.FileSystem.DeleteDirectory(virtualFolder);
            this.FileSystem.DeleteDirectory(controlFolder);
            virtualFolder.ShouldNotExistOnDisk(this.FileSystem);
            controlFolder.ShouldNotExistOnDisk(this.FileSystem);
        }

        protected void MoveFolder(string pathFrom, string pathTo)
        {
            string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, pathFrom);
            string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, pathTo);
            string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, pathFrom);
            string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, pathTo);
            this.FileSystem.MoveDirectory(virtualFileFrom, virtualFileTo);
            this.FileSystem.MoveDirectory(controlFileFrom, controlFileTo);
            virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem);
            controlFileFrom.ShouldNotExistOnDisk(this.FileSystem);
        }

        protected void FolderShouldExist(params string[] folderPathParts)
        {
            string folderPath = Path.Combine(folderPathParts);
            string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath);
            string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath);
            virtualFolder.ShouldBeADirectory(this.FileSystem);
            controlFolder.ShouldBeADirectory(this.FileSystem);
        }

        protected void FolderShouldExistAndHaveFile(params string[] filePathParts)
        {
            string filePath = Path.Combine(filePathParts);
            string folderPath = Path.GetDirectoryName(filePath);
            string fileName = Path.GetFileName(filePath);

            string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath);
            string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath);
            virtualFolder.ShouldBeADirectory(this.FileSystem).WithItems(fileName).Count().ShouldEqual(1);
            controlFolder.ShouldBeADirectory(this.FileSystem).WithItems(fileName).Count().ShouldEqual(1);
        }

        protected void FolderShouldExistAndBeEmpty(params string[] folderPathParts)
        {
            string folderPath = Path.Combine(folderPathParts);
            string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath);
            string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath);
            virtualFolder.ShouldBeADirectory(this.FileSystem).WithNoItems();
            controlFolder.ShouldBeADirectory(this.FileSystem).WithNoItems();
        }

        protected void ShouldNotExistOnDisk(params string[] pathParts)
        {
            string path = Path.Combine(pathParts);
            string virtualPath = Path.Combine(this.Enlistment.RepoRoot, path);
            string controlPath = Path.Combine(this.ControlGitRepo.RootPath, path);
            virtualPath.ShouldNotExistOnDisk(this.FileSystem);
            controlPath.ShouldNotExistOnDisk(this.FileSystem);
        }

        protected void FileShouldHaveContents(string contents, params string[] filePathParts)
        {
            string filePath = Path.Combine(filePathParts);
            string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filePath);
            virtualFilePath.ShouldBeAFile(this.FileSystem).WithContents(contents);
            controlFilePath.ShouldBeAFile(this.FileSystem).WithContents(contents);
        }

        protected void FileContentsShouldMatch(params string[] filePathPaths)
        {
            string filePath = Path.Combine(filePathPaths);
            string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filePath);
            bool virtualExists = File.Exists(virtualFilePath);
            bool controlExists = File.Exists(controlFilePath);

            if (virtualExists)
            {
                if (controlExists)
                {
                    virtualFilePath.ShouldBeAFile(this.FileSystem)
                                   .WithContents(controlFilePath.ShouldBeAFile(this.FileSystem)
                                                                .WithContents());
                }
                else
                {
                    virtualExists.ShouldEqual(controlExists, $"{virtualExists} exists, but {controlExists} does not");
                }
            }
            else if (controlExists)
            {
                virtualExists.ShouldEqual(controlExists, $"{virtualExists} does not exist, but {controlExists} does");
            }
        }

        protected void FileShouldHaveCaseMatchingName(string caseSensitiveFilePath)
        {
            string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, caseSensitiveFilePath);
            string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, caseSensitiveFilePath);
            string caseSensitiveName = Path.GetFileName(caseSensitiveFilePath);
            virtualFilePath.ShouldBeAFile(this.FileSystem).WithCaseMatchingName(caseSensitiveName);
            controlFilePath.ShouldBeAFile(this.FileSystem).WithCaseMatchingName(caseSensitiveName);
        }

        protected void FolderShouldHaveCaseMatchingName(string caseSensitiveFolderPath)
        {
            string virtualFolderPath = Path.Combine(this.Enlistment.RepoRoot, caseSensitiveFolderPath);
            string controlFolderPath = Path.Combine(this.ControlGitRepo.RootPath, caseSensitiveFolderPath);
            string caseSensitiveName = Path.GetFileName(caseSensitiveFolderPath);
            virtualFolderPath.ShouldBeADirectory(this.FileSystem).WithCaseMatchingName(caseSensitiveName);
            controlFolderPath.ShouldBeADirectory(this.FileSystem).WithCaseMatchingName(caseSensitiveName);
        }

        protected void AppendAllText(string content, params string[] filePathParts)
        {
            string filePath = Path.Combine(filePathParts);
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath);
            this.FileSystem.AppendAllText(virtualFile, content);
            this.FileSystem.AppendAllText(controlFile, content);
        }

        protected void ReplaceText(string newContent, params string[] filePathParts)
        {
            string filePath = Path.Combine(filePathParts);
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath);
            this.FileSystem.WriteAllText(virtualFile, newContent);
            this.FileSystem.WriteAllText(controlFile, newContent);
        }

        protected void SetupForFileDirectoryTest(string commandBranch = DirectoryWithFileAfterBranch)
        {
            this.ControlGitRepo.Fetch(DirectoryWithFileBeforeBranch);
            this.ControlGitRepo.Fetch(commandBranch);
            this.ValidateGitCommand($"checkout {DirectoryWithFileBeforeBranch}");
        }

        protected void ValidateFileDirectoryTest(string command, string commandBranch = DirectoryWithFileAfterBranch)
        {
            this.EditFile("Change file", "Readme.md");
            this.ValidateGitCommand("add --all");
            this.RunGitCommand("commit -m \"Some change\"");
            this.ValidateGitCommand($"{command} {commandBranch}");
        }

        protected void RunFileDirectoryEnumerateTest(string command, string commandBranch = DirectoryWithFileAfterBranch)
        {
            this.SetupForFileDirectoryTest(commandBranch);

            // file.txt is a folder with a file named file.txt to test checking out branches
            // that have folders with the same name as files
            this.FileSystem.EnumerateDirectory(this.Enlistment.GetVirtualPathTo("file.txt"));
            this.ValidateFileDirectoryTest(command, commandBranch);
        }

        protected void RunFileDirectoryReadTest(string command, string commandBranch = DirectoryWithFileAfterBranch)
        {
            this.SetupForFileDirectoryTest(commandBranch);
            this.FileContentsShouldMatch("file.txt", "file.txt");
            this.ValidateFileDirectoryTest(command, commandBranch);
        }

        protected void RunFileDirectoryWriteTest(string command, string commandBranch = DirectoryWithFileAfterBranch)
        {
            this.SetupForFileDirectoryTest(commandBranch);
            this.EditFile("Change file", "file.txt", "file.txt");
            this.ValidateFileDirectoryTest(command, commandBranch);
        }

        protected void ReadConflictTargetFiles()
        {
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInSource.txt");
        }

        protected void FilesShouldMatchCheckoutOfTargetBranch()
        {
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt");

            this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInSource.txt");

            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt");
        }

        protected void FilesShouldMatchCheckoutOfSourceBranch()
        {
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedBySource.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt");

            this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInTarget.txt");

            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSourceDeleteInTarget.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt");
        }

        protected void FilesShouldMatchAfterNoConflict()
        {
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt");

            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt");
        }

        protected void FilesShouldMatchAfterConflict()
        {
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedBySource.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt");

            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSourceDeleteInTarget.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt");
            this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt");
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/HashObjectTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using NUnit.Framework;
using System.IO;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixture]
    [Category(Categories.GitCommands)]
    public class HashObjectTests : GitRepoTests
    {
        public HashObjectTests()
            : base(enlistmentPerTest: false, validateWorkingTree: Settings.ValidateWorkingTreeMode.None)
        {
        }

        [TestCase]
        public void CanReadFileAfterHashObject()
        {
            this.ValidateGitCommand("status");

            // Validate that Scripts\RunUnitTests.bad is not on disk at all
            string filePath = Path.Combine("Scripts", "RunUnitTests.bat");

            this.Enlistment.UnmountGVFS();
            this.Enlistment.GetVirtualPathTo(filePath).ShouldNotExistOnDisk(this.FileSystem);
            this.Enlistment.MountGVFS();

            // TODO 1087312: Fix 'git hash-oject' so that it works for files that aren't on disk yet
            GitHelpers.InvokeGitAgainstGVFSRepo(
                this.Enlistment.RepoRoot,
                "hash-object " + GitHelpers.ConvertPathToGitFormat(filePath));

            this.FileContentsShouldMatch(filePath);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/MergeConflictTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class MergeConflictTests : GitRepoTests
    {
        public MergeConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        public void MergeConflict()
        {
            // No need to tear down this config since these tests are for enlistment per test.
            this.SetupRenameDetectionAvoidanceInConfig();

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void MergeConflictWithFileReads()
        {
            // No need to tear down this config since these tests are for enlistment per test.
            this.SetupRenameDetectionAvoidanceInConfig();

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ReadConflictTargetFiles();
            this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void MergeConflict_ThenAbort()
        {
            // No need to tear down this config since these tests are for enlistment per test.
            this.SetupRenameDetectionAvoidanceInConfig();

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("merge --abort");
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void MergeConflict_UsingOurs()
        {
            // No need to tear down this config since these tests are for enlistment per test.
            this.SetupRenameDetectionAvoidanceInConfig();

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.RunGitCommand($"merge -s ours {GitRepoTests.ConflictSourceBranch}");
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void MergeConflict_UsingStrategyTheirs()
        {
            // No need to tear down this config since these tests are for enlistment per test.
            this.SetupRenameDetectionAvoidanceInConfig();

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.RunGitCommand($"merge -s recursive -Xtheirs {GitRepoTests.ConflictSourceBranch}");
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void MergeConflict_UsingStrategyOurs()
        {
            // No need to tear down this config since these tests are for enlistment per test.
            this.SetupRenameDetectionAvoidanceInConfig();

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.RunGitCommand($"merge -s recursive -Xours {GitRepoTests.ConflictSourceBranch}");
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void MergeConflictEnsureStatusFailsDueToConfig()
        {
            // This is compared against the message emitted by GVFS.Hooks\Program.cs
            string expectedErrorMessagePart = "--no-renames";

            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch, checkStatus: false);

            ProcessResult result1 = GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "status");
            result1.Errors.Contains(expectedErrorMessagePart);

            ProcessResult result2 = GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "status --no-renames");
            result2.Errors.Contains(expectedErrorMessagePart);

            // Complete setup to ensure teardown succeeds
            GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "config --local test.renames false");
        }

        protected override void CreateEnlistment()
        {
            base.CreateEnlistment();
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
        }

        private void SetupRenameDetectionAvoidanceInConfig()
        {
            // Tell the pre-command hook that it shouldn't check for "--no-renames" when runing "git status"
            // as the control repo won't do that.  When the pre-command hook has been updated to properly
            // check for "status.renames" we can set that value here instead.
            this.ValidateGitCommand("config --local test.renames false");
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/RebaseConflictTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class RebaseConflictTests : GitRepoTests
    {
        public RebaseConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        public void RebaseConflict()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void RebaseConflictWithPrefetch()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.Enlistment.Prefetch("--files * --hydrate");
            this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void RebaseConflictWithFileReads()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ReadConflictTargetFiles();
            this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void RebaseConflict_ThenAbort()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("rebase --abort");
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void RebaseConflict_ThenSkip()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("rebase --skip");
            this.FilesShouldMatchCheckoutOfSourceBranch();
        }

        [TestCase]
        public void RebaseConflict_RemoveDeletedTheirsFile()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("rm Test_ConflictTests/ModifiedFiles/ChangeInSourceDeleteInTarget.txt");
        }

        [TestCase]
        public void RebaseConflict_AddThenContinue()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("add .");
            this.ValidateGitCommand("rebase --continue");
            this.FilesShouldMatchAfterConflict();
        }

        [TestCase]
        public void RebaseMultipleCommits()
        {
            string sourceCommit = "FunctionalTests/20201014_rebase_multiple_source";
            string targetCommit = "FunctionalTests/20201014_rebase_multiple_onto";

            this.ControlGitRepo.Fetch(sourceCommit);
            this.ControlGitRepo.Fetch(targetCommit);

            this.ValidateGitCommand("checkout " + sourceCommit);
            this.RunGitCommand("rebase origin/" + targetCommit);
            this.ValidateGitCommand("rebase --abort");
        }

        protected override void CreateEnlistment()
        {
            base.CreateEnlistment();
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/RebaseTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class RebaseTests : GitRepoTests
    {
        public RebaseTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        [Ignore("This is producing different output because git is not checking out files in the rebase.  The virtual file system changes should address this issue.")]
        public void RebaseSmallNoConflicts()
        {
            // 5d299512450f4029d7a1fe8d67e833b84247d393 is the tip of FunctionalTests/RebaseTestsSource_20170130
            string sourceCommit = "5d299512450f4029d7a1fe8d67e833b84247d393";

            // Target commit 47fabb534c35af40156db6e8365165cb04f9dd75 is part of the history of
            // FunctionalTests/20170130
            string targetCommit = "47fabb534c35af40156db6e8365165cb04f9dd75";

            this.ControlGitRepo.Fetch(sourceCommit);
            this.ControlGitRepo.Fetch(targetCommit);

            this.ValidateGitCommand("checkout {0}", sourceCommit);
            this.ValidateGitCommand("rebase {0}", targetCommit);
        }

        [TestCase]
        public void RebaseSmallOneFileConflict()
        {
            // 5d299512450f4029d7a1fe8d67e833b84247d393 is the tip of FunctionalTests/RebaseTestsSource_20170130
            string sourceCommit = "5d299512450f4029d7a1fe8d67e833b84247d393";

            // Target commit 99fc72275f950b0052c8548bbcf83a851f2b4467 is part of the history of
            // FunctionalTests/20170130
            string targetCommit = "99fc72275f950b0052c8548bbcf83a851f2b4467";

            this.ControlGitRepo.Fetch(sourceCommit);
            this.ControlGitRepo.Fetch(targetCommit);

            this.ValidateGitCommand("checkout {0}", sourceCommit);
            this.ValidateGitCommand("rebase {0}", targetCommit);
        }

        [TestCase]
        [Ignore("This is producing different output because git is not checking out files in the rebase.  The virtual file system changes should address this issue.")]
        public void RebaseEditThenDelete()
        {
            // 23a238b04497da2449fd730966c06f84b6326c3a is the tip of FunctionalTests/RebaseTestsSource_20170208
            string sourceCommit = "23a238b04497da2449fd730966c06f84b6326c3a";

            // Target commit 47fabb534c35af40156db6e8365165cb04f9dd75 is part of the history of
            // FunctionalTests/20170208
            string targetCommit = "47fabb534c35af40156db6e8365165cb04f9dd75";

            this.ControlGitRepo.Fetch(sourceCommit);
            this.ControlGitRepo.Fetch(targetCommit);

            this.ValidateGitCommand("checkout {0}", sourceCommit);
            this.ValidateGitCommand("rebase {0}", targetCommit);
        }

        [TestCase]
        public void RebaseWithDirectoryNameSameAsFile()
        {
            this.SetupForFileDirectoryTest();
            this.ValidateFileDirectoryTest("rebase");
        }

        [TestCase]
        public void RebaseWithDirectoryNameSameAsFileEnumerate()
        {
            this.RunFileDirectoryEnumerateTest("rebase");
        }

        [TestCase]
        public void RebaseWithDirectoryNameSameAsFileWithRead()
        {
            this.RunFileDirectoryReadTest("rebase");
        }

        [TestCase]
        public void RebaseWithDirectoryNameSameAsFileWithWrite()
        {
            this.RunFileDirectoryWriteTest("rebase");
        }

        [TestCase]
        public void RebaseDirectoryWithOneFile()
        {
            this.SetupForFileDirectoryTest(commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
            this.ValidateFileDirectoryTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }

        [TestCase]
        public void RebaseDirectoryWithOneFileEnumerate()
        {
            this.RunFileDirectoryEnumerateTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }

        [TestCase]
        public void RebaseDirectoryWithOneFileRead()
        {
            this.RunFileDirectoryReadTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }

        [TestCase]
        public void RebaseDirectoryWithOneFileWrite()
        {
            this.RunFileDirectoryWriteTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/ResetHardTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class ResetHardTests : GitRepoTests
    {
        private const string ResetHardCommand = "reset --hard";

        public ResetHardTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        [Ignore("This doesn't work right now. Tracking if this is a ProjFS problem. See #1696 for tracking.")]
        public void VerifyResetHardDeletesEmptyFolders()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_RenameTestMergeTarget");
            this.ValidateGitCommand("checkout FunctionalTests/20201014_RenameTestMergeTarget");
            this.ValidateGitCommand("reset --hard HEAD~1");
            this.ShouldNotExistOnDisk("Test_EPF_GitCommandsTestOnlyFileFolder");
            this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes);
        }

        [TestCase]
        public void ResetHardWithDirectoryNameSameAsFile()
        {
            this.SetupForFileDirectoryTest();
            this.ValidateFileDirectoryTest(ResetHardCommand);
        }

        [TestCase]
        public void ResetHardWithDirectoryNameSameAsFileEnumerate()
        {
            this.RunFileDirectoryEnumerateTest(ResetHardCommand);
        }

        [TestCase]
        public void ResetHardWithDirectoryNameSameAsFileWithRead()
        {
            this.RunFileDirectoryReadTest(ResetHardCommand);
        }

        [TestCase]
        public void ResetHardWithDirectoryNameSameAsFileWithWrite()
        {
            this.RunFileDirectoryWriteTest(ResetHardCommand);
        }

        [TestCase]
        public void ResetHardDirectoryWithOneFile()
        {
            this.SetupForFileDirectoryTest(commandBranch: GitRepoTests.DirectoryWithFileAfterBranch);
            this.ValidateFileDirectoryTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }

        [TestCase]
        public void ResetHardDirectoryWithOneFileEnumerate()
        {
            this.RunFileDirectoryEnumerateTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }

        [TestCase]
        public void ResetHardDirectoryWithOneFileRead()
        {
            this.RunFileDirectoryReadTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }

        [TestCase]
        public void ResetHardDirectoryWithOneFileWrite()
        {
            this.RunFileDirectoryWriteTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/ResetMixedTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class ResetMixedTests : GitRepoTests
    {
        public ResetMixedTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        public void ResetMixed()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("reset --mixed HEAD~1");
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void ResetMixedAfterPrefetch()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.Enlistment.Prefetch("--files * --hydrate");
            this.ValidateGitCommand("reset --mixed HEAD~1");
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void ResetMixedAndCheckoutNewBranch()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("reset --mixed HEAD~1");

            // 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/ResetMixedAndCheckoutNewBranch");
            this.FilesShouldMatchCheckoutOfTargetBranch();
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void ResetMixedAndCheckoutOrphanBranch()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("reset --mixed HEAD~1");
            this.ValidateGitCommand("checkout --orphan tests/functional/ResetMixedAndCheckoutOrphanBranch");
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void ResetMixedAndRemount()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("reset --mixed HEAD~1");
            this.FilesShouldMatchCheckoutOfTargetBranch();

            this.Enlistment.UnmountGVFS();
            this.Enlistment.MountGVFS();
            this.ValidateGitCommand("status");
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void ResetMixedThenCheckoutWithConflicts()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("reset --mixed HEAD~1");

            // Because git while using the sparse-checkout feature
            // will check for index merge conflicts and error out before it checks
            // for untracked files that will be overwritten we just run the command
            this.RunGitCommand("checkout " + GitRepoTests.ConflictSourceBranch, ignoreErrors: true);
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void ResetMixedAndCheckoutFile()
        {
            this.ControlGitRepo.Fetch("FunctionalTests/20201014_ResetMixedAndCheckoutFile");

            // We start with a branch that deleted two files that were present in its parent commit
            this.ValidateGitCommand("checkout FunctionalTests/20201014_ResetMixedAndCheckoutFile");

            // Then reset --mixed to the parent commit, and validate that the deleted files did not come back into the projection
            this.ValidateGitCommand("reset --mixed HEAD~1");
            this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes);

            // And checkout a file (without changing branches) and ensure that that doesn't update the projection either
            this.ValidateGitCommand("checkout HEAD~2 .gitattributes");
            this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes);

            // And now if we checkout the original commit, the deleted files should stay deleted
            this.ValidateGitCommand("checkout FunctionalTests/20201014_ResetMixedAndCheckoutFile");
            this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem)
                .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes);
        }

        protected override void CreateEnlistment()
        {
            base.CreateEnlistment();
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/ResetSoftTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class ResetSoftTests : GitRepoTests
    {
        public ResetSoftTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        public void ResetSoft()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("reset --soft HEAD~1");
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void ResetSoftThenRemount()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("reset --soft HEAD~1");
            this.FilesShouldMatchCheckoutOfTargetBranch();

            this.Enlistment.UnmountGVFS();
            this.Enlistment.MountGVFS();
            this.ValidateGitCommand("status");
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void ResetSoftThenCheckoutWithConflicts()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("reset --soft HEAD~1");
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch);
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void ResetSoftThenCheckoutNoConflicts()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("reset --soft HEAD~1");
            this.ValidateGitCommand("checkout " + GitRepoTests.NoConflictSourceBranch);
            this.FilesShouldMatchAfterNoConflict();
        }

        [TestCase]
        public void ResetSoftThenResetHeadThenCheckoutNoConflicts()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("reset --soft HEAD~1");
            this.ValidateGitCommand("reset HEAD Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt");
            this.ValidateGitCommand("checkout " + GitRepoTests.NoConflictSourceBranch);
            this.FilesShouldMatchAfterNoConflict();
        }

        protected override void CreateEnlistment()
        {
            base.CreateEnlistment();
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.NoConflictSourceBranch);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/RmTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using NUnit.Framework;
using System.IO;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixture]
    public class RmTests : GitRepoTests
    {
        public RmTests()
            : base(enlistmentPerTest: false, validateWorkingTree: Settings.ValidateWorkingTreeMode.None)
        {
        }

        [TestCase]
        public void CanReadFileAfterGitRmDryRun()
        {
            this.ValidateGitCommand("status");

            // Validate that Scripts\RunUnitTests.bad is not on disk at all
            string filePath = Path.Combine("Scripts", "RunUnitTests.bat");

            this.Enlistment.UnmountGVFS();
            this.Enlistment.GetVirtualPathTo(filePath).ShouldNotExistOnDisk(this.FileSystem);
            this.Enlistment.MountGVFS();

            this.ValidateGitCommand("rm --dry-run " + GitHelpers.ConvertPathToGitFormat(filePath));
            this.FileContentsShouldMatch(filePath);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/StatusTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.IO;
using System.Threading;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class StatusTests : GitRepoTests
    {
        public StatusTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        public void MoveFileIntoDotGitDirectory()
        {
            string srcPath = @"Readme.md";
            string dstPath = Path.Combine(".git", "destination.txt");

            this.MoveFile(srcPath, dstPath);
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void DeleteThenCreateThenDeleteFile()
        {
            string srcPath = @"Readme.md";

            this.DeleteFile(srcPath);
            this.ValidateGitCommand("status");
            this.CreateFile("Testing", srcPath);
            this.ValidateGitCommand("status");
            this.DeleteFile(srcPath);
            this.ValidateGitCommand("status");
        }

        [TestCase]
        public void CreateFileWithoutClose()
        {
            string srcPath = @"CreateFileWithoutClose.md";
            this.CreateFileWithoutClose(srcPath);
            this.ValidGitStatusWithRetry(srcPath);
        }

        [TestCase]
        public void WriteWithoutClose()
        {
            string srcPath = @"Readme.md";
            this.ReadFileAndWriteWithoutClose(srcPath, "More Stuff");
            this.ValidGitStatusWithRetry(srcPath);
        }

         [TestCase]
         public void AppendFileUsingBash()
         {
            // Bash will perform the append using '>>' which will cause KAUTH_VNODE_APPEND_DATA to be sent without hydration
            // Other Runners may cause hydration before append
            BashRunner bash = new BashRunner();
            string filePath = Path.Combine("Test_EPF_UpdatePlaceholderTests", "LockToPreventUpdate", "test.txt");
            string content = "Apended Data";
            string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath);
            string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath);
            bash.AppendAllText(virtualFile, content);
            bash.AppendAllText(controlFile, content);

            this.ValidateGitCommand("status");

            // We check the contents after status, to ensure this check didn't cause the hydration
            string appendedContent = string.Concat("Commit2LockToPreventUpdate \r\n", content);
            virtualFile.ShouldBeAFile(this.FileSystem).WithContents(appendedContent);
            controlFile.ShouldBeAFile(this.FileSystem).WithContents(appendedContent);
        }

        [TestCase]
        public void ModifyingAndDeletingRepositoryExcludeFileInvalidatesCache()
        {
            string repositoryExcludeFile = Path.Combine(".git", "info", "exclude");

            this.RepositoryIgnoreTestSetup();

            // Add ignore pattern to existing exclude file
            this.EditFile("*.ign", repositoryExcludeFile);

            // 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("*.ign", repositoryExcludeFileRelativePath);

            // 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()
        {
            this.WaitForUpToDateStatusCache();

            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("file to be ignored", "test.ign");

            this.WaitForStatusCacheToBeGenerated();

            // Verify that status from the status cache includes the "test.ign" entry
            this.ValidateGitCommand("status");
        }

        /// 
        /// Wait for an up-to-date status cache file to exist on disk.
        /// 
        private void WaitForUpToDateStatusCache()
        {
            // Run "git status" for the side effect that it will delete any stale status cache file.
            this.ValidateGitCommand("status");

            // Wait for a new status cache to be generated.
            this.WaitForStatusCacheToBeGenerated(waitForNewFile: false);
        }

        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.");
        }

        private void ValidGitStatusWithRetry(string srcPath)
        {
            this.Enlistment.WaitForBackgroundOperations();
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.FileSystem, srcPath);
            try
            {
                this.ValidateGitCommand("status");
            }
            catch (Exception ex)
            {
                Thread.Sleep(1000);
                this.ValidateGitCommand("status");
                Assert.Fail("{0} was succesful on the second try, but failed on first: {1}", nameof(this.ValidateGitCommand), ex.Message);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/UpdateIndexTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Tools;
using NUnit.Framework;
using System.IO;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class UpdateIndexTests : GitRepoTests
    {
        public UpdateIndexTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        [Ignore("TODO 940287: git update-index --remove does not check if the file is on disk if the skip-worktree bit is set")]
        public void UpdateIndexRemoveFileOnDisk()
        {
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);
            this.ValidateGitCommand("update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt");
            this.FilesShouldMatchCheckoutOfTargetBranch();
        }

        [TestCase]
        public void UpdateIndexRemoveFileOnDiskDontCheckStatus()
        {
            // TODO 940287: Remove this test and re-enable UpdateIndexRemoveFileOnDisk
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);

            // git-status will not match because update-index --remove does not check what is on disk if the skip-worktree bit is set,
            // meaning it will always remove the file from the index
            GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt");
            GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt");
            this.FilesShouldMatchCheckoutOfTargetBranch();

            // Add the files back to the index so the git-status that is run during teardown matches
            GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt");
            GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt");
        }

        [TestCase]
        public void UpdateIndexRemoveAddFileOpenForWrite()
        {
            // TODO 940287: Remove this test and re-enable UpdateIndexRemoveFileOnDisk
            this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch);

            // git-status will not match because update-index --remove does not check what is on disk if the skip-worktree bit is set,
            // meaning it will always remove the file from the index
            GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt");
            GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt");
            this.FilesShouldMatchCheckoutOfTargetBranch();

            // Add the files back to the index so the git-status that is run during teardown matches
            GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt");
            GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt");
        }

        [TestCase]
        public void UpdateIndexWithCacheInfo()
        {
            // Update Protocol.md with the contents from blob 583f1...
            string command = $"update-index --cacheinfo 100644 \"583f1a56db7cc884d54534c5d9c56b93a1e00a2b\n\" Protocol.md";

            this.ValidateGitCommand(command);
        }

        protected override void CreateEnlistment()
        {
            base.CreateEnlistment();
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch);
            this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/GitCommands/UpdateRefTests.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Tools;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.GitCommands
{
    [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))]
    [Category(Categories.GitCommands)]
    public class UpdateRefTests : GitRepoTests
    {
        public UpdateRefTests(Settings.ValidateWorkingTreeMode validateWorkingTree)
            : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree)
        {
        }

        [TestCase]
        public void UpdateRefModifiesHead()
        {
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("update-ref HEAD f1bce402a7a980a8320f3f235cf8c8fdade4b17a");
        }

        [TestCase]
        public void UpdateRefModifiesHeadThenResets()
        {
            this.ValidateGitCommand("status");
            this.ValidateGitCommand("update-ref HEAD f1bce402a7a980a8320f3f235cf8c8fdade4b17a");
            this.ValidateGitCommand("reset HEAD");
        }

        public override void TearDownForTest()
        {
            if (FileSystemHelpers.CaseSensitiveFileSystem)
            {
                this.TestValidationAndCleanup();
            }
            else
            {
                // On case-insensitive filesystems, we
                // need to ignore case changes in this test because the update-ref will have
                // folder names that only changed the case and when checking the folder structure
                // it will create partial folders with that case and will not get updated to the
                // previous case when the reset --hard running in the tear down step
                this.TestValidationAndCleanup(ignoreCase: true);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/MultiEnlistmentTests/ConfigVerbTests.cs
================================================
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;

namespace GVFS.FunctionalTests.Tests.MultiEnlistmentTests
{
    [TestFixture]
    [Category(Categories.ExtraCoverage)]
    public class ConfigVerbTests : TestsWithMultiEnlistment
    {
        private const string IntegerSettingKey = "functionalTest_Integer";
        private const string FloatSettingKey = "functionalTest_Float";
        private const string RegularStringSettingKey = "functionalTest_RegularString";
        private const string SpacedStringSettingKey = "functionalTest_SpacedString";
        private const string SpacesOnlyStringSettingKey = "functionalTest_SpacesOnlyString";
        private const string EmptyStringSettingKey = "functionalTest_EmptyString";
        private const string NonExistentSettingKey = "functionalTest_NonExistentSetting";

        private const int GenericErrorExitCode = 3;

        private readonly Dictionary initialSettings = new Dictionary()
        {
            { IntegerSettingKey, "213" },
            { FloatSettingKey, "213.15" },
            { RegularStringSettingKey, "foobar" },
            { SpacedStringSettingKey, "quick brown fox" }
        };

        private readonly Dictionary updateSettings = new Dictionary()
        {
            { IntegerSettingKey, "32123" },
            { FloatSettingKey, "3.14159" },
            { RegularStringSettingKey, "helloWorld!" },
            { SpacedStringSettingKey, "jumped over lazy dog" }
        };

        [OneTimeSetUp]
        public void ResetTestConfig()
        {
            this.DeleteSettings(this.initialSettings);
            this.DeleteSettings(this.updateSettings);
        }

        [TestCase, Order(1)]
        public void CreateSettings()
        {
            this.ApplySettings(this.initialSettings);
            this.ConfigShouldContainSettings(this.initialSettings);
        }

        [TestCase, Order(2)]
        public void UpdateSettings()
        {
            this.ApplySettings(this.updateSettings);
            this.ConfigShouldContainSettings(this.updateSettings);
        }

        [TestCase, Order(3)]
        public void ListSettings()
        {
            this.ConfigShouldContainSettings(this.updateSettings);
        }

        [TestCase, Order(4)]
        public void ReadSingleSetting()
        {
            foreach (KeyValuePair setting in this.updateSettings)
            {
                string value = this.RunConfigCommand($"{setting.Key}");
                value.TrimEnd(Environment.NewLine.ToCharArray()).ShouldEqual($"{setting.Value}");
            }
        }

        [TestCase, Order(5)]
        public void AddSpaceValueSetting()
        {
            string writeSpacesValue = "     ";
            this.WriteSetting(SpacesOnlyStringSettingKey, writeSpacesValue);

            string readSpacesValue = this.ReadSetting($"{SpacesOnlyStringSettingKey}");
            readSpacesValue.TrimEnd(Environment.NewLine.ToCharArray()).ShouldEqual(writeSpacesValue);
        }

        [TestCase, Order(6)]
        public void AddNullValueSetting()
        {
            string writeEmptyValue = string.Empty;
            this.WriteSetting(EmptyStringSettingKey, writeEmptyValue, GenericErrorExitCode);

            string readEmptyValue = this.ReadSetting(EmptyStringSettingKey, GenericErrorExitCode);
            readEmptyValue.ShouldBeEmpty();
        }

        [TestCase, Order(7)]
        public void ReadNonExistentSetting()
        {
            string nonExistentValue = this.ReadSetting(NonExistentSettingKey, GenericErrorExitCode);
            nonExistentValue.ShouldBeEmpty();
        }

        [TestCase, Order(8)]
        public void DeleteSettings()
        {
            this.DeleteSettings(this.updateSettings);

            List deletedLines = new List();
            foreach (KeyValuePair setting in this.updateSettings)
            {
                deletedLines.Add(this.GetSettingLineInConfigFileFormat(setting));
            }

            string allSettings = this.RunConfigCommand("--list");
            allSettings.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: deletedLines.ToArray());
        }

        private void DeleteSettings(Dictionary settings)
        {
            List deletedLines = new List();
            foreach (KeyValuePair setting in settings)
            {
                this.RunConfigCommand($"--delete {setting.Key}");
            }
        }

        private void ConfigShouldContainSettings(Dictionary expectedSettings)
        {
            List expectedLines = new List();
            foreach (KeyValuePair setting in expectedSettings)
            {
                expectedLines.Add(this.GetSettingLineInConfigFileFormat(setting));
            }

            string allSettings = this.RunConfigCommand("--list");
            allSettings.ShouldContain(expectedLines.ToArray());
        }

        private string GetSettingLineInConfigFileFormat(KeyValuePair setting)
        {
            return $"{setting.Key}={setting.Value}";
        }

        private void ApplySettings(Dictionary settings)
        {
            foreach (KeyValuePair setting in settings)
            {
                this.WriteSetting(setting.Key, setting.Value);
            }
        }

        private void WriteSetting(string key, string value, int expectedExitCode = 0)
        {
            this.RunConfigCommand($"{key} \"{value}\"", expectedExitCode);
        }

        private string ReadSetting(string key, int expectedExitCode = 0)
        {
            return this.RunConfigCommand($"{key}", expectedExitCode);
        }

        private string RunConfigCommand(string argument, int expectedExitCode = 0)
        {
            ProcessResult result = ProcessHelper.Run(GVFSTestConfig.PathToGVFS, $"config {argument}");
            result.ExitCode.ShouldEqual(expectedExitCode, result.Errors);

            return result.Output;
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/MultiEnlistmentTests/ServiceVerbTests.cs
================================================
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.FunctionalTests.Tests.MultiEnlistmentTests
{
    [TestFixture]
    [NonParallelizable]
    [Category(Categories.ExtraCoverage)]
    public class ServiceVerbTests : TestsWithMultiEnlistment
    {
        private static readonly string[] EmptyRepoList = new string[] { };

        [TestCase]
        public void ServiceCommandsWithNoRepos()
        {
            this.RunServiceCommandAndCheckOutput("--unmount-all", EmptyRepoList);
            this.RunServiceCommandAndCheckOutput("--mount-all", EmptyRepoList);
            this.RunServiceCommandAndCheckOutput("--list-mounted", EmptyRepoList);
        }

        [TestCase]
        public void ServiceCommandsWithMultipleRepos()
        {
            GVFSFunctionalTestEnlistment enlistment1 = this.CreateNewEnlistment();
            GVFSFunctionalTestEnlistment enlistment2 = this.CreateNewEnlistment();

            string[] repoRootList = new string[] { enlistment1.EnlistmentRoot, enlistment2.EnlistmentRoot };

            GVFSProcess gvfsProcess1 = new GVFSProcess(
                GVFSTestConfig.PathToGVFS,
                enlistment1.EnlistmentRoot,
                enlistment1.LocalCacheRoot);

            GVFSProcess gvfsProcess2 = new GVFSProcess(
                GVFSTestConfig.PathToGVFS,
                enlistment2.EnlistmentRoot,
                enlistment2.LocalCacheRoot);

            this.RunServiceCommandAndCheckOutput("--list-mounted", expectedRepoRoots: repoRootList);
            this.RunServiceCommandAndCheckOutput("--unmount-all", expectedRepoRoots: repoRootList);

            // Check both are unmounted
            gvfsProcess1.IsEnlistmentMounted().ShouldEqual(false);
            gvfsProcess2.IsEnlistmentMounted().ShouldEqual(false);

            this.RunServiceCommandAndCheckOutput("--list-mounted", EmptyRepoList);
            this.RunServiceCommandAndCheckOutput("--unmount-all", EmptyRepoList);
            this.RunServiceCommandAndCheckOutput("--mount-all", expectedRepoRoots: repoRootList);

            // Check both are mounted
            gvfsProcess1.IsEnlistmentMounted().ShouldEqual(true);
            gvfsProcess2.IsEnlistmentMounted().ShouldEqual(true);

            this.RunServiceCommandAndCheckOutput("--list-mounted", expectedRepoRoots: repoRootList);
        }

        [TestCase]
        public void ServiceCommandsWithMountAndUnmount()
        {
            GVFSFunctionalTestEnlistment enlistment1 = this.CreateNewEnlistment();

            string[] repoRootList = new string[] { enlistment1.EnlistmentRoot };

            GVFSProcess gvfsProcess1 = new GVFSProcess(
                GVFSTestConfig.PathToGVFS,
                enlistment1.EnlistmentRoot,
                enlistment1.LocalCacheRoot);

            this.RunServiceCommandAndCheckOutput("--list-mounted", expectedRepoRoots: repoRootList);

            gvfsProcess1.Unmount();

            this.RunServiceCommandAndCheckOutput("--list-mounted", EmptyRepoList, unexpectedRepoRoots: repoRootList);
            this.RunServiceCommandAndCheckOutput("--unmount-all", EmptyRepoList, unexpectedRepoRoots: repoRootList);
            this.RunServiceCommandAndCheckOutput("--mount-all", EmptyRepoList, unexpectedRepoRoots: repoRootList);

            // Check that it is still unmounted
            gvfsProcess1.IsEnlistmentMounted().ShouldEqual(false);

            gvfsProcess1.Mount();

            this.RunServiceCommandAndCheckOutput("--unmount-all", expectedRepoRoots: repoRootList);
            this.RunServiceCommandAndCheckOutput("--mount-all", expectedRepoRoots: repoRootList);
        }

        private void RunServiceCommandAndCheckOutput(string argument, string[] expectedRepoRoots, string[] unexpectedRepoRoots = null)
        {
            GVFSProcess gvfsProcess = new GVFSProcess(
                GVFSTestConfig.PathToGVFS,
                enlistmentRoot: null,
                localCacheRoot: null);

            string result = gvfsProcess.RunServiceVerb(argument);
            result.ShouldContain(expectedRepoRoots);

            if (unexpectedRepoRoots != null)
            {
                result.ShouldNotContain(false, unexpectedRepoRoots);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/MultiEnlistmentTests/SharedCacheTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace GVFS.FunctionalTests.Tests.MultiEnlistmentTests
{
    [TestFixture]
    [Category(Categories.ExtraCoverage)]
    public class SharedCacheTests : TestsWithMultiEnlistment
    {
        private const string WellKnownFile = "Readme.md";

        // This branch and commit sha should point to the same place.
        private const string WellKnownBranch = "FunctionalTests/20201014_ResetMixedAndCheckoutFile";
        private const string WellKnownCommitSha = "42eb6632beffae26893a3d6e1a9f48d652327c6f";

        private string localCachePath;
        private string localCacheParentPath;

        private FileSystemRunner fileSystem;

        public SharedCacheTests()
        {
            this.fileSystem = new SystemIORunner();
        }

        [SetUp]
        public void SetCacheLocation()
        {
            this.localCacheParentPath = Path.Combine(Properties.Settings.Default.EnlistmentRoot, "..", Guid.NewGuid().ToString("N"));
            this.localCachePath = Path.Combine(this.localCacheParentPath, ".customGVFSCache");
        }

        [TestCase]
        public void SecondCloneDoesNotDownloadAdditionalObjects()
        {
            GVFSFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment();
            File.ReadAllText(Path.Combine(enlistment1.RepoRoot, WellKnownFile));

            this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1);

            string[] allObjects = Directory.EnumerateFiles(enlistment1.LocalCacheRoot, "*", SearchOption.AllDirectories).ToArray();

            GVFSFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment();
            File.ReadAllText(Path.Combine(enlistment2.RepoRoot, WellKnownFile));

            this.AlternatesFileShouldHaveGitObjectsRoot(enlistment2);

            enlistment2.LocalCacheRoot.ShouldEqual(enlistment1.LocalCacheRoot, "Sanity: Local cache roots are expected to match.");
            Directory.EnumerateFiles(enlistment2.LocalCacheRoot, "*", SearchOption.AllDirectories)
                .ShouldMatchInOrder(allObjects);
        }

        [TestCase]
        public void RepairFixesCorruptBlobSizesDatabase()
        {
            GVFSFunctionalTestEnlistment enlistment = this.CloneAndMountEnlistment();
            enlistment.UnmountGVFS();

            // Repair on a healthy enlistment should succeed
            enlistment.Repair(confirm: true);

            string blobSizesRoot = GVFSHelpers.GetPersistedBlobSizesRoot(enlistment.DotGVFSRoot).ShouldNotBeNull();
            string blobSizesDbPath = Path.Combine(blobSizesRoot, "BlobSizes.sql");
            blobSizesDbPath.ShouldBeAFile(this.fileSystem);
            this.fileSystem.WriteAllText(blobSizesDbPath, "0000");

            enlistment.TryMountGVFS().ShouldEqual(false, "GVFS shouldn't mount when blob size db is corrupt");
            enlistment.Repair(confirm: true);
            enlistment.MountGVFS();
        }

        [TestCase]
        public void CloneCleansUpStaleMetadataLock()
        {
            GVFSFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment();
            string metadataLockPath = Path.Combine(this.localCachePath, "mapping.dat.lock");
            metadataLockPath.ShouldNotExistOnDisk(this.fileSystem);
            this.fileSystem.WriteAllText(metadataLockPath, enlistment1.EnlistmentRoot);
            metadataLockPath.ShouldBeAFile(this.fileSystem);

            GVFSFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment();
            metadataLockPath.ShouldNotExistOnDisk(this.fileSystem);

            enlistment1.Status().ShouldContain("Mount status: Ready");
            enlistment2.Status().ShouldContain("Mount status: Ready");
        }

        [TestCase]
        public void ParallelReadsInASharedCache()
        {
            GVFSFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment();
            GVFSFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment();
            GVFSFunctionalTestEnlistment enlistment3 = null;

            Task task1 = Task.Run(() => this.HydrateEntireRepo(enlistment1));
            Task task2 = Task.Run(() => this.HydrateEntireRepo(enlistment2));
            Task task3 = Task.Run(() => enlistment3 = this.CloneAndMountEnlistment());

            task1.Wait();
            task2.Wait();
            task3.Wait();

            task1.Exception.ShouldBeNull();
            task2.Exception.ShouldBeNull();
            task3.Exception.ShouldBeNull();

            enlistment1.Status().ShouldContain("Mount status: Ready");
            enlistment2.Status().ShouldContain("Mount status: Ready");
            enlistment3.Status().ShouldContain("Mount status: Ready");

            this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1);
            this.AlternatesFileShouldHaveGitObjectsRoot(enlistment2);
            this.AlternatesFileShouldHaveGitObjectsRoot(enlistment3);
        }

        [TestCase]
        public void DeleteObjectsCacheAndCacheMappingBeforeMount()
        {
            GVFSFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment();
            GVFSFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment();

            enlistment1.UnmountGVFS();

            string objectsRoot = GVFSHelpers.GetPersistedGitObjectsRoot(enlistment1.DotGVFSRoot).ShouldNotBeNull();
            objectsRoot.ShouldBeADirectory(this.fileSystem);
            RepositoryHelpers.DeleteTestDirectory(objectsRoot);

            string metadataPath = Path.Combine(this.localCachePath, "mapping.dat");
            metadataPath.ShouldBeAFile(this.fileSystem);
            this.fileSystem.DeleteFile(metadataPath);

            enlistment1.MountGVFS();

            Task task1 = Task.Run(() => this.HydrateRootFolder(enlistment1));
            Task task2 = Task.Run(() => this.HydrateRootFolder(enlistment2));
            task1.Wait();
            task2.Wait();
            task1.Exception.ShouldBeNull();
            task2.Exception.ShouldBeNull();

            enlistment1.Status().ShouldContain("Mount status: Ready");
            enlistment2.Status().ShouldContain("Mount status: Ready");

            this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1);
            this.AlternatesFileShouldHaveGitObjectsRoot(enlistment2);
        }

        [TestCase]
        public void DeleteCacheDuringHydrations()
        {
            GVFSFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment();

            string objectsRoot = GVFSHelpers.GetPersistedGitObjectsRoot(enlistment1.DotGVFSRoot).ShouldNotBeNull();
            objectsRoot.ShouldBeADirectory(this.fileSystem);

            Task task1 = Task.Run(() =>
            {
                this.HydrateEntireRepo(enlistment1);
            });

            while (!task1.IsCompleted)
            {
                try
                {
                    // Delete objectsRoot rather than this.localCachePath as the blob sizes database cannot be deleted while GVFS is mounted
                    RepositoryHelpers.DeleteTestDirectory(objectsRoot);
                    Thread.Sleep(100);
                }
                catch (IOException)
                {
                    // Hydration may have handles into the cache, so failing this delete is expected.
                }
            }

            task1.Exception.ShouldBeNull();

            enlistment1.Status().ShouldContain("Mount status: Ready");
        }

        [TestCase]
        public void DownloadingACommitWithoutTreesDoesntBreakNextClone()
        {
            GVFSFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment();
            GitProcess.Invoke(enlistment1.RepoRoot, "cat-file -s " + WellKnownCommitSha).ShouldEqual("293\n");

            GVFSFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment(WellKnownBranch);
            enlistment2.Status().ShouldContain("Mount status: Ready");
        }

        [TestCase]
        public void MountReusesLocalCacheKeyWhenGitObjectsRootDeleted()
        {
            GVFSFunctionalTestEnlistment enlistment = this.CloneAndMountEnlistment();

            enlistment.UnmountGVFS();

            // Find the current git objects root and ensure it's on disk
            string objectsRoot = GVFSHelpers.GetPersistedGitObjectsRoot(enlistment.DotGVFSRoot).ShouldNotBeNull();
            objectsRoot.ShouldBeADirectory(this.fileSystem);

            string mappingFilePath = Path.Combine(enlistment.LocalCacheRoot, "mapping.dat");
            string mappingFileContents = this.fileSystem.ReadAllText(mappingFilePath);
            mappingFileContents.Length.ShouldNotEqual(0, "mapping.dat should not be empty");

            // Delete the git objects root folder, mount should re-create it and the mapping.dat file should not change
            RepositoryHelpers.DeleteTestDirectory(objectsRoot);

            enlistment.MountGVFS();

            GVFSHelpers.GetPersistedGitObjectsRoot(enlistment.DotGVFSRoot).ShouldEqual(objectsRoot);
            objectsRoot.ShouldBeADirectory(this.fileSystem);
            mappingFilePath.ShouldBeAFile(this.fileSystem).WithContents(mappingFileContents);

            this.AlternatesFileShouldHaveGitObjectsRoot(enlistment);
        }

        [TestCase]
        public void MountUsesNewLocalCacheKeyWhenLocalCacheDeleted()
        {
            GVFSFunctionalTestEnlistment enlistment = this.CloneAndMountEnlistment();

            enlistment.UnmountGVFS();

            // Find the current git objects root and ensure it's on disk
            string objectsRoot = GVFSHelpers.GetPersistedGitObjectsRoot(enlistment.DotGVFSRoot).ShouldNotBeNull();
            objectsRoot.ShouldBeADirectory(this.fileSystem);

            string mappingFilePath = Path.Combine(enlistment.LocalCacheRoot, "mapping.dat");
            string mappingFileContents = this.fileSystem.ReadAllText(mappingFilePath);
            mappingFileContents.Length.ShouldNotEqual(0, "mapping.dat should not be empty");

            // Delete the local cache folder, mount should re-create it and generate a new mapping file and local cache key
            RepositoryHelpers.DeleteTestDirectory(enlistment.LocalCacheRoot);

            enlistment.MountGVFS();

            // Mount should recreate the local cache root
            enlistment.LocalCacheRoot.ShouldBeADirectory(this.fileSystem);

            // Determine the new local cache key
            string newMappingFileContents = mappingFilePath.ShouldBeAFile(this.fileSystem).WithContents();
            const int GuidStringLength = 32;
            string mappingFileKey = "A {\"Key\":\"https://gvfs.visualstudio.com/ci/_git/fortests\",\"Value\":\"";
            int localKeyIndex = newMappingFileContents.IndexOf(mappingFileKey);
            string newCacheKey = newMappingFileContents.Substring(localKeyIndex + mappingFileKey.Length, GuidStringLength);

            // Validate the new objects root is on disk and uses the new key
            objectsRoot.ShouldNotExistOnDisk(this.fileSystem);
            string newObjectsRoot = GVFSHelpers.GetPersistedGitObjectsRoot(enlistment.DotGVFSRoot);
            newObjectsRoot.ShouldNotEqual(objectsRoot);
            newObjectsRoot.ShouldContain(newCacheKey);
            newObjectsRoot.ShouldBeADirectory(this.fileSystem);

            this.AlternatesFileShouldHaveGitObjectsRoot(enlistment);
        }

        [TestCase]
        public void SecondCloneSucceedsWithMissingTrees()
        {
            string newCachePath = Path.Combine(this.localCacheParentPath, ".customGvfsCache2");
            GVFSFunctionalTestEnlistment enlistment1 = this.CreateNewEnlistment(localCacheRoot: newCachePath, skipPrefetch: true);
            File.ReadAllText(Path.Combine(enlistment1.RepoRoot, WellKnownFile));
            this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1);

            // This Git command loads the commit and root tree for WellKnownCommitSha,
            // but does not download any more reachable objects.
            string command = "cat-file -p origin/" + WellKnownBranch + "^{tree}";
            ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo(enlistment1.RepoRoot, command);
            result.ExitCode.ShouldEqual(0, $"git {command} failed with error: " + result.Errors);

            // If we did not properly check the failed checkout at this step, then clone will fail during checkout.
            GVFSFunctionalTestEnlistment enlistment2 = this.CreateNewEnlistment(localCacheRoot: newCachePath, branch: WellKnownBranch, skipPrefetch: true);
            File.ReadAllText(Path.Combine(enlistment2.RepoRoot, WellKnownFile));
        }

        // Override OnTearDownEnlistmentsDeleted rathern than using [TearDown] as the enlistments need to be unmounted before
        // localCacheParentPath can be deleted (as the SQLite blob sizes database cannot be deleted while GVFS is mounted)
        protected override void OnTearDownEnlistmentsDeleted()
        {
            RepositoryHelpers.DeleteTestDirectory(this.localCacheParentPath);
        }

        private GVFSFunctionalTestEnlistment CloneAndMountEnlistment(string branch = null)
        {
            return this.CreateNewEnlistment(this.localCachePath, branch);
        }

        private void AlternatesFileShouldHaveGitObjectsRoot(GVFSFunctionalTestEnlistment enlistment)
        {
            string objectsRoot = GVFSHelpers.GetPersistedGitObjectsRoot(enlistment.DotGVFSRoot);
            string alternatesFileContents = Path.Combine(enlistment.RepoRoot, ".git", "objects", "info", "alternates").ShouldBeAFile(this.fileSystem).WithContents();
            alternatesFileContents.ShouldEqual(objectsRoot);
        }

        private void HydrateRootFolder(GVFSFunctionalTestEnlistment enlistment)
        {
            List allFiles = Directory.EnumerateFiles(enlistment.RepoRoot, "*", SearchOption.TopDirectoryOnly).ToList();
            for (int i = 0; i < allFiles.Count; ++i)
            {
                File.ReadAllText(allFiles[i]);
            }
        }

        private void HydrateEntireRepo(GVFSFunctionalTestEnlistment enlistment)
        {
            List allFiles = Directory.EnumerateFiles(enlistment.RepoRoot, "*", SearchOption.AllDirectories).ToList();
            string dotGitRoot = Path.Combine(enlistment.RepoRoot, ".git") + Path.DirectorySeparatorChar;
            for (int i = 0; i < allFiles.Count; ++i)
            {
                if (!allFiles[i].StartsWith(dotGitRoot, FileSystemHelpers.PathComparison))
                {
                    File.ReadAllText(allFiles[i]);
                }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/MultiEnlistmentTests/TestsWithMultiEnlistment.cs
================================================
using GVFS.FunctionalTests.Tools;
using NUnit.Framework;
using System.Collections.Generic;

namespace GVFS.FunctionalTests.Tests.MultiEnlistmentTests
{
    public class TestsWithMultiEnlistment
    {
        private List enlistmentsToDelete = new List();

        [TearDown]
        public void DeleteEnlistments()
        {
            foreach (GVFSFunctionalTestEnlistment enlistment in this.enlistmentsToDelete)
            {
                enlistment.UnmountAndDeleteAll();
            }

            this.OnTearDownEnlistmentsDeleted();

            this.enlistmentsToDelete.Clear();
        }

        /// 
        /// Can be overridden for custom [TearDown] steps that occur after the test enlistements have been unmounted and deleted
        /// 
        protected virtual void OnTearDownEnlistmentsDeleted()
        {
        }

        protected GVFSFunctionalTestEnlistment CreateNewEnlistment(
            string localCacheRoot = null,
            string branch = null,
            bool skipPrefetch = false)
        {
            GVFSFunctionalTestEnlistment output = GVFSFunctionalTestEnlistment.CloneAndMount(
                GVFSTestConfig.PathToGVFS,
                branch,
                localCacheRoot,
                skipPrefetch);
            this.enlistmentsToDelete.Add(output);
            return output;
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/PrintTestCaseStats.cs
================================================
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

[assembly: GVFS.FunctionalTests.Tests.PrintTestCaseStats]

namespace GVFS.FunctionalTests.Tests
{
    public class PrintTestCaseStats : TestActionAttribute
    {
        private const string StartTimeKey = "StartTime";

        private static ConcurrentDictionary fixtureRunTimes = new ConcurrentDictionary();
        private static ConcurrentDictionary testRunTimes = new ConcurrentDictionary();

        public override ActionTargets Targets
        {
            get { return ActionTargets.Test; }
        }

        public static void PrintRunTimeStats()
        {
            Console.WriteLine();
            Console.WriteLine("Fixture run times:");
            foreach (KeyValuePair fixture in fixtureRunTimes.OrderByDescending(kvp => kvp.Value))
            {
                Console.WriteLine("    {0}\t{1}", fixture.Value, fixture.Key);
            }

            Console.WriteLine();
            Console.WriteLine("Test case run times:");
            foreach (KeyValuePair testcase in testRunTimes.OrderByDescending(kvp => kvp.Value))
            {
                Console.WriteLine("    {0}\t{1}", testcase.Value, testcase.Key);
            }
        }

        public override void BeforeTest(ITest test)
        {
            test.Properties.Add(StartTimeKey, DateTime.Now);
        }

        public override void AfterTest(ITest test)
        {
            DateTime startTime = (DateTime)test.Properties.Get(StartTimeKey);
            DateTime endTime = DateTime.Now;
            TimeSpan duration = endTime - startTime;
            string message = TestContext.CurrentContext.Result.Message;
            TestStatus status = TestContext.CurrentContext.Result.Outcome.Status;

            Console.WriteLine("Test " + test.FullName);
            Console.WriteLine($"{status} at {endTime.ToLongTimeString()} taking {duration}");
            if (status != TestStatus.Passed)
            {
                Console.WriteLine(message);
            }

            Console.WriteLine();

            fixtureRunTimes.AddOrUpdate(
                test.ClassName,
                duration,
                (key, existingValue) => existingValue + duration);
            testRunTimes.TryAdd(test.FullName, duration);
        }
    }
}

================================================
FILE: GVFS/GVFS.FunctionalTests/Tests/TestResultsHelper.cs
================================================
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace GVFS.FunctionalTests.Tests
{
    public static class TestResultsHelper
    {
        public static void OutputGVFSLogs(GVFSFunctionalTestEnlistment enlistment)
        {
            if (enlistment == null)
            {
                return;
            }

            Console.WriteLine("GVFS logs output attached below.\n\n");

            foreach (string filename in GetAllFilesInDirectory(enlistment.GVFSLogsRoot))
            {
                if (filename.Contains("mount_process"))
                {
                    // Validate that all mount processes started by the functional tests were started
                    // by verbs, and that "StartedByVerb" was set to true when the mount process was launched
                    OutputFileContents(
                        filename,
                        contents => contents.ShouldContain("\"StartedByVerb\":true"));
                }
                else
                {
                    OutputFileContents(filename);
                }
            }
        }

        public static void OutputFileContents(string filename, Action contentsValidator = null)
        {
            try
            {
                using (StreamReader reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
                {
                    Console.WriteLine("----- {0} -----", filename);

                    string contents = reader.ReadToEnd();

                    if (contentsValidator != null)
                    {
                        contentsValidator(contents);
                    }

                    Console.WriteLine(contents + "\n\n");
                }
            }
            catch (IOException ex)
            {
                Console.WriteLine("Unable to read logfile at {0}: {1}", filename, ex.ToString());
            }
        }

        public static IEnumerable GetAllFilesInDirectory(string folderName)
        {
            DirectoryInfo directory = new DirectoryInfo(folderName);
            if (!directory.Exists)
            {
                return Enumerable.Empty();
            }

            return directory.GetFiles().Select(file => file.FullName);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/ControlGitRepo.cs
================================================
using System;
using System.IO;

namespace GVFS.FunctionalTests.Tools
{
    public class ControlGitRepo
    {
        static ControlGitRepo()
        {
            if (!Directory.Exists(CachePath))
            {
                GitProcess.Invoke(Environment.SystemDirectory, "clone " + GVFSTestConfig.RepoToClone + " " + CachePath + " --bare");
            }
            else
            {
                GitProcess.Invoke(CachePath, "fetch origin +refs/*:refs/*");
            }
        }

        private ControlGitRepo(string repoUrl, string rootPath, string commitish)
        {
            this.RootPath = rootPath;
            this.RepoUrl = repoUrl;
            this.Commitish = commitish;
        }

        public string RootPath { get; private set; }
        public string RepoUrl { get; private set; }
        public string Commitish { get; private set; }

        private static string CachePath
        {
            get { return Path.Combine(Properties.Settings.Default.ControlGitRepoRoot, "cache"); }
        }

        public static ControlGitRepo Create(string commitish = null)
        {
            string clonePath = Path.Combine(Properties.Settings.Default.ControlGitRepoRoot, Guid.NewGuid().ToString("N"));
            return new ControlGitRepo(
                GVFSTestConfig.RepoToClone,
                clonePath,
                commitish == null ? Properties.Settings.Default.Commitish : commitish);
        }

        //
        // IMPORTANT! These must parallel the settings in GVFSVerb:TrySetRequiredGitConfigSettings
        //
        public void Initialize()
        {
            Directory.CreateDirectory(this.RootPath);
            GitProcess.Invoke(this.RootPath, "init");
            GitProcess.Invoke(this.RootPath, "config core.autocrlf false");
            GitProcess.Invoke(this.RootPath, "config core.editor true");
            GitProcess.Invoke(this.RootPath, "config merge.stat false");
            GitProcess.Invoke(this.RootPath, "config merge.renames false");
            GitProcess.Invoke(this.RootPath, "config advice.statusUoption false");
            GitProcess.Invoke(this.RootPath, "config core.abbrev 40");
            GitProcess.Invoke(this.RootPath, "config checkout.workers 0");
            GitProcess.Invoke(this.RootPath, "config core.useBuiltinFSMonitor false");
            GitProcess.Invoke(this.RootPath, "config pack.useSparse true");
            GitProcess.Invoke(this.RootPath, "config reset.quiet true");
            GitProcess.Invoke(this.RootPath, "config status.aheadbehind false");
            GitProcess.Invoke(this.RootPath, "config user.name \"Functional Test User\"");
            GitProcess.Invoke(this.RootPath, "config user.email \"functional@test.com\"");
            GitProcess.Invoke(this.RootPath, "remote add origin " + CachePath);
            this.Fetch(this.Commitish);
            GitProcess.Invoke(this.RootPath, "branch --set-upstream " + this.Commitish + " origin/" + this.Commitish);
            GitProcess.Invoke(this.RootPath, "checkout " + this.Commitish);
            GitProcess.Invoke(this.RootPath, "branch --unset-upstream");

            // Enable the ORT merge strategy
            GitProcess.Invoke(this.RootPath, "config pull.twohead ort");
        }

        public void Fetch(string commitish)
        {
            GitProcess.Invoke(this.RootPath, "fetch origin " + commitish);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/FileSystemHelpers.cs
================================================
using System;
using System.Runtime.InteropServices;

namespace GVFS.FunctionalTests.Tools
{
    public static class FileSystemHelpers
    {
        public static StringComparison PathComparison
        {
            get
            {
                return CaseSensitiveFileSystem ?
                    StringComparison.Ordinal :
                    StringComparison.OrdinalIgnoreCase;
            }
        }

        public static StringComparer PathComparer
        {
            get
            {
                return CaseSensitiveFileSystem ?
                    StringComparer.Ordinal :
                    StringComparer.OrdinalIgnoreCase;
            }
        }

        public static bool CaseSensitiveFileSystem
        {
            get
            {
                return RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/GVFSFunctionalTestEnlistment.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tests;
using GVFS.Tests.Should;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;

namespace GVFS.FunctionalTests.Tools
{
    public class GVFSFunctionalTestEnlistment
    {
        private const string LockHeldByGit = "GVFS Lock: Held by {0}";
        private const int SleepMSWaitingForStatusCheck = 100;
        private const int DefaultMaxWaitMSForStatusCheck = 5000;
        private static readonly string ZeroBackgroundOperations = "Background operations: 0" + Environment.NewLine;

        private GVFSProcess gvfsProcess;

        private GVFSFunctionalTestEnlistment(string pathToGVFS, string enlistmentRoot, string repoUrl, string commitish, string localCacheRoot = null)
        {
            this.EnlistmentRoot = enlistmentRoot;
            this.RepoUrl = repoUrl;
            this.Commitish = commitish;

            if (localCacheRoot == null)
            {
                if (GVFSTestConfig.NoSharedCache)
                {
                    // eg C:\Repos\GVFSFunctionalTests\enlistment\7942ca69d7454acbb45ea39ef5be1d15\.gvfs\.gvfsCache
                    localCacheRoot = GetRepoSpecificLocalCacheRoot(enlistmentRoot);
                }
                else
                {
                    // eg C:\Repos\GVFSFunctionalTests\.gvfsCache
                    // Ensures the general cache is not cleaned up between test runs
                    localCacheRoot = Path.Combine(Properties.Settings.Default.EnlistmentRoot, "..", ".gvfsCache");
                }
            }

            this.LocalCacheRoot = localCacheRoot;
            this.gvfsProcess = new GVFSProcess(pathToGVFS, this.EnlistmentRoot, this.LocalCacheRoot);
        }

        public string EnlistmentRoot
        {
            get; private set;
        }

        public string RepoUrl
        {
            get; private set;
        }

        public string LocalCacheRoot { get; }

        public string RepoBackingRoot
        {
            get
            {
                if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
                {
                    return Path.Combine(this.EnlistmentRoot, ".vfsforgit/lower");
                }
                else
                {
                    return this.RepoRoot;
                }
            }
        }

        public string RepoRoot
        {
            get { return Path.Combine(this.EnlistmentRoot, "src"); }
        }

        public string DotGVFSRoot
        {
            get { return Path.Combine(this.EnlistmentRoot, GVFSTestConfig.DotGVFSRoot); }
        }

        public string GVFSLogsRoot
        {
            get { return Path.Combine(this.DotGVFSRoot, "logs"); }
        }

        public string DiagnosticsRoot
        {
            get { return Path.Combine(this.DotGVFSRoot, "diagnostics"); }
        }

        public string Commitish
        {
            get; private set;
        }

        public static GVFSFunctionalTestEnlistment CloneAndMountWithPerRepoCache(string pathToGvfs, bool skipPrefetch)
        {
            string enlistmentRoot = GVFSFunctionalTestEnlistment.GetUniqueEnlistmentRoot();
            string localCache = GVFSFunctionalTestEnlistment.GetRepoSpecificLocalCacheRoot(enlistmentRoot);
            return CloneAndMount(pathToGvfs, enlistmentRoot, null, localCache, skipPrefetch);
        }

        public static GVFSFunctionalTestEnlistment CloneAndMount(
            string pathToGvfs,
            string commitish = null,
            string localCacheRoot = null,
            bool skipPrefetch = false)
        {
            string enlistmentRoot = GVFSFunctionalTestEnlistment.GetUniqueEnlistmentRoot();
            return CloneAndMount(pathToGvfs, enlistmentRoot, commitish, localCacheRoot, skipPrefetch);
        }

        public static GVFSFunctionalTestEnlistment CloneAndMountEnlistmentWithSpacesInPath(string pathToGvfs, string commitish = null)
        {
            string enlistmentRoot = GVFSFunctionalTestEnlistment.GetUniqueEnlistmentRootWithSpaces();
            string localCache = GVFSFunctionalTestEnlistment.GetRepoSpecificLocalCacheRoot(enlistmentRoot);
            return CloneAndMount(pathToGvfs, enlistmentRoot, commitish, localCache);
        }

        public static string GetUniqueEnlistmentRoot()
        {
            return Path.Combine(Properties.Settings.Default.EnlistmentRoot, Guid.NewGuid().ToString("N").Substring(0, 20));
        }

        public static string GetUniqueEnlistmentRootWithSpaces()
        {
            return Path.Combine(Properties.Settings.Default.EnlistmentRoot, "test " + Guid.NewGuid().ToString("N").Substring(0, 15));
        }

        public string GetObjectRoot(FileSystemRunner fileSystem)
        {
            string mappingFile = Path.Combine(this.LocalCacheRoot, "mapping.dat");
            mappingFile.ShouldBeAFile(fileSystem);

            HashSet allowedFileNames = new HashSet(FileSystemHelpers.PathComparer)
            {
                "mapping.dat",
                "mapping.dat.lock" // mapping.dat.lock can be present, but doesn't have to be present
            };

            this.LocalCacheRoot.ShouldBeADirectory(fileSystem).WithFiles().ShouldNotContain(f => !allowedFileNames.Contains(f.Name));

            string mappingFileContents = File.ReadAllText(mappingFile);
            mappingFileContents.ShouldNotBeNull();
            string[] objectRootEntries = mappingFileContents.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                                                            .Where(x => x.IndexOf(this.RepoUrl, StringComparison.OrdinalIgnoreCase) >= 0)
                                                            .ToArray();
            objectRootEntries.Length.ShouldEqual(1, $"Should be only one entry for repo url: {this.RepoUrl} mapping file content: {mappingFileContents}");
            objectRootEntries[0].Substring(0, 2).ShouldEqual("A ", $"Invalid mapping entry for repo: {objectRootEntries[0]}");
            JObject rootEntryJson = JObject.Parse(objectRootEntries[0].Substring(2));
            string objectRootFolder = rootEntryJson.GetValue("Value").ToString();
            objectRootFolder.ShouldNotBeNull();
            objectRootFolder.Length.ShouldBeAtLeast(1, $"Invalid object root folder: {objectRootFolder} for {this.RepoUrl} mapping file content: {mappingFileContents}");

            return Path.Combine(this.LocalCacheRoot, objectRootFolder, "gitObjects");
        }

        public string GetPackRoot(FileSystemRunner fileSystem)
        {
            return Path.Combine(this.GetObjectRoot(fileSystem), "pack");
        }

        public void DeleteEnlistment()
        {
            TestResultsHelper.OutputGVFSLogs(this);
            RepositoryHelpers.DeleteTestDirectory(this.EnlistmentRoot);
        }

        public void CloneAndMount(bool skipPrefetch)
        {
            this.gvfsProcess.Clone(this.RepoUrl, this.Commitish, skipPrefetch);

            GitProcess.Invoke(this.RepoRoot, "checkout " + this.Commitish);
            GitProcess.Invoke(this.RepoRoot, "branch --unset-upstream");
            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 bool IsMounted()
        {
            return this.gvfsProcess.IsEnlistmentMounted();
        }

        public void MountGVFS()
        {
            this.gvfsProcess.Mount();
        }

        public bool TryMountGVFS()
        {
            string output;
            return this.TryMountGVFS(out output);
        }

        public bool TryMountGVFS(out string output)
        {
            return this.gvfsProcess.TryMount(out output);
        }

        public string Prefetch(string args, bool failOnError = true, string standardInput = null)
        {
            return this.gvfsProcess.Prefetch(args, failOnError, standardInput);
        }

        public void Repair(bool confirm)
        {
            this.gvfsProcess.Repair(confirm);
        }

        public string Diagnose()
        {
            return this.gvfsProcess.Diagnose();
        }

        public string LooseObjectStep()
        {
            return this.gvfsProcess.LooseObjectStep();
        }

        public string PackfileMaintenanceStep(long? batchSize = null)
        {
            return this.gvfsProcess.PackfileMaintenanceStep(batchSize);
        }

        public string PostFetchStep()
        {
            return this.gvfsProcess.PostFetchStep();
        }

        public string Status(string trace = null)
        {
            return this.gvfsProcess.Status(trace);
        }

        public string Health(string directory = null)
        {
            return this.gvfsProcess.Health(directory);
        }

        public bool WaitForBackgroundOperations(int maxWaitMilliseconds = DefaultMaxWaitMSForStatusCheck)
        {
            return this.WaitForStatus(maxWaitMilliseconds, ZeroBackgroundOperations).ShouldBeTrue("Background operations failed to complete.");
        }

        public bool WaitForLock(string lockCommand, int maxWaitMilliseconds = DefaultMaxWaitMSForStatusCheck)
        {
            return this.WaitForStatus(maxWaitMilliseconds, string.Format(LockHeldByGit, lockCommand));
        }

        public void WriteConfig(string key, string value)
        {
            this.gvfsProcess.WriteConfig(key, value);
        }

        public void UnmountGVFS()
        {
            this.gvfsProcess.Unmount();
        }

        public string GetCacheServer()
        {
            return this.gvfsProcess.CacheServer("--get");
        }

        public string SetCacheServer(string arg)
        {
            return this.gvfsProcess.CacheServer("--set " + arg);
        }

        public void UnmountAndDeleteAll()
        {
            this.UnmountGVFS();
            this.DeleteEnlistment();
        }

        public string GetVirtualPathTo(string path)
        {
            // Replace '/' with Path.DirectorySeparatorChar to ensure that any
            // Git paths are converted to system paths
            return Path.Combine(this.RepoRoot, path.Replace(TestConstants.GitPathSeparator, Path.DirectorySeparatorChar));
        }

        public string GetVirtualPathTo(params string[] pathParts)
        {
            return Path.Combine(this.RepoRoot, Path.Combine(pathParts));
        }

        public string GetBackingPathTo(string path)
        {
            // Replace '/' with Path.DirectorySeparatorChar to ensure that any
            // Git paths are converted to system paths
            return Path.Combine(this.RepoBackingRoot, path.Replace(TestConstants.GitPathSeparator, Path.DirectorySeparatorChar));
        }

        public string GetBackingPathTo(params string[] pathParts)
        {
            return Path.Combine(this.RepoBackingRoot, Path.Combine(pathParts));
        }

        public string GetDotGitPath(params string[] pathParts)
        {
            return this.GetBackingPathTo(TestConstants.DotGit.Root, Path.Combine(pathParts));
        }

        public string GetObjectPathTo(string objectHash)
        {
            return Path.Combine(
                this.RepoBackingRoot,
                TestConstants.DotGit.Objects.Root,
                objectHash.Substring(0, 2),
                objectHash.Substring(2));
        }

        private static GVFSFunctionalTestEnlistment CloneAndMount(string pathToGvfs, string enlistmentRoot, string commitish, string localCacheRoot, bool skipPrefetch = false)
        {
            GVFSFunctionalTestEnlistment enlistment = new GVFSFunctionalTestEnlistment(
                pathToGvfs,
                enlistmentRoot ?? GetUniqueEnlistmentRoot(),
                GVFSTestConfig.RepoToClone,
                commitish ?? Properties.Settings.Default.Commitish,
                localCacheRoot ?? GVFSTestConfig.LocalCacheRoot);

            try
            {
                enlistment.CloneAndMount(skipPrefetch);
            }
            catch (Exception e)
            {
                Console.WriteLine("Unhandled exception in CloneAndMount: " + e.ToString());
                TestResultsHelper.OutputGVFSLogs(enlistment);
                throw;
            }

            return enlistment;
        }

        private static string GetRepoSpecificLocalCacheRoot(string enlistmentRoot)
        {
            return Path.Combine(enlistmentRoot, GVFSTestConfig.DotGVFSRoot, ".gvfsCache");
        }

        private bool WaitForStatus(int maxWaitMilliseconds, string statusShouldContain)
        {
            string status = null;
            int totalWaitMilliseconds = 0;
            while (totalWaitMilliseconds <= maxWaitMilliseconds && (status == null || !status.Contains(statusShouldContain)))
            {
                Thread.Sleep(SleepMSWaitingForStatusCheck);
                status = this.Status();
                totalWaitMilliseconds += SleepMSWaitingForStatusCheck;
            }

            return totalWaitMilliseconds <= maxWaitMilliseconds;
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.Tests.Should;
using Microsoft.Data.Sqlite;
using Newtonsoft.Json;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace GVFS.FunctionalTests.Tools
{
    public static class GVFSHelpers
    {
        public const string ModifiedPathsNewLine = "\r\n";
        public const string PlaceholderFieldDelimiter = "\0";

        public static readonly string BackgroundOpsFile = Path.Combine("databases", "BackgroundGitOperations.dat");
        public static readonly string PlaceholderListFile = Path.Combine("databases", "PlaceholderList.dat");
        public static readonly string RepoMetadataName = Path.Combine("databases", "RepoMetadata.dat");

        private const string ModifedPathsLineAddPrefix = "A ";
        private const string ModifedPathsLineDeletePrefix = "D ";

        private const string DiskLayoutMajorVersionKey = "DiskLayoutVersion";
        private const string DiskLayoutMinorVersionKey = "DiskLayoutMinorVersion";
        private const string LocalCacheRootKey = "LocalCacheRoot";
        private const string GitObjectsRootKey = "GitObjectsRoot";
        private const string PlaceholdersNeedUpdate = "PlaceholdersNeedUpdate";
        private const string BlobSizesRootKey = "BlobSizesRoot";

        private const string PrjFSLibPath = "libPrjFSLib.dylib";
        private const int PrjFSResultSuccess = 1;

        private const int WindowsCurrentDiskLayoutMajorVersion = 19;
        private const int MacCurrentDiskLayoutMajorVersion = 19;
        private const int WindowsCurrentDiskLayoutMinimumMajorVersion = 14;
        private const int MacCurrentDiskLayoutMinimumMajorVersion = 18;

        public static string ConvertPathToGitFormat(string path)
        {
            return path.Replace(Path.DirectorySeparatorChar, TestConstants.GitPathSeparator);
        }

        public static void SaveDiskLayoutVersion(string dotGVFSRoot, string majorVersion, string minorVersion)
        {
            SavePersistedValue(dotGVFSRoot, DiskLayoutMajorVersionKey, majorVersion);
            SavePersistedValue(dotGVFSRoot, DiskLayoutMinorVersionKey, minorVersion);
        }

        public static void GetPersistedDiskLayoutVersion(string dotGVFSRoot, out string majorVersion, out string minorVersion)
        {
            majorVersion = GetPersistedValue(dotGVFSRoot, DiskLayoutMajorVersionKey);
            minorVersion = GetPersistedValue(dotGVFSRoot, DiskLayoutMinorVersionKey);
        }

        public static void SaveLocalCacheRoot(string dotGVFSRoot, string value)
        {
            SavePersistedValue(dotGVFSRoot, LocalCacheRootKey, value);
        }

        public static string GetPersistedLocalCacheRoot(string dotGVFSRoot)
        {
            return GetPersistedValue(dotGVFSRoot, LocalCacheRootKey);
        }

        public static void SaveGitObjectsRoot(string dotGVFSRoot, string value)
        {
            SavePersistedValue(dotGVFSRoot, GitObjectsRootKey, value);
        }

        public static void SetPlaceholderUpdatesRequired(string dotGVFSRoot, bool isUpdateRequired)
        {
            SavePersistedValue(dotGVFSRoot, PlaceholdersNeedUpdate, isUpdateRequired.ToString());
        }

        public static string GetPersistedGitObjectsRoot(string dotGVFSRoot)
        {
            return GetPersistedValue(dotGVFSRoot, GitObjectsRootKey);
        }

        public static string GetPersistedBlobSizesRoot(string dotGVFSRoot)
        {
            return GetPersistedValue(dotGVFSRoot, BlobSizesRootKey);
        }

        public static void SQLiteBlobSizesDatabaseHasEntry(string blobSizesDbPath, string blobSha, long blobSize)
        {
            RunSqliteCommand(blobSizesDbPath, command =>
            {
                SqliteParameter shaParam = command.CreateParameter();
                shaParam.ParameterName = "@sha";
                command.CommandText = "SELECT size FROM BlobSizes WHERE sha = (@sha)";
                command.Parameters.Add(shaParam);
                shaParam.Value = StringToShaBytes(blobSha);

                using (SqliteDataReader reader = command.ExecuteReader())
                {
                    reader.Read().ShouldBeTrue();
                    reader.GetInt64(0).ShouldEqual(blobSize);
                }

                return true;
            });
        }

        public static string GetAllSQLitePlaceholdersAsString(string placeholdersDbPath)
        {
            return RunSqliteCommand(placeholdersDbPath, command =>
                {
                    command.CommandText = "SELECT path, pathType, sha FROM Placeholder";
                    using (SqliteDataReader reader = command.ExecuteReader())
                    {
                        StringBuilder sb = new StringBuilder();
                        while (reader.Read())
                        {
                            sb.Append(reader.GetString(0));
                            sb.Append(PlaceholderFieldDelimiter);
                            sb.Append(reader.GetByte(1));
                            sb.Append(PlaceholderFieldDelimiter);
                            if (!reader.IsDBNull(2))
                            {
                                sb.Append(reader.GetString(2));
                                sb.Append(PlaceholderFieldDelimiter);
                            }

                            sb.AppendLine();
                        }

                        return sb.ToString();
                    }
                });
        }

        public static void AddPlaceholderFolder(string placeholdersDbPath, string path, int pathType)
        {
            RunSqliteCommand(placeholdersDbPath, command =>
            {
                command.CommandText = "INSERT OR REPLACE INTO Placeholder (path, pathType, sha) VALUES (@path, @pathType, NULL)";
                command.Parameters.AddWithValue("@path", path);
                command.Parameters.AddWithValue("@pathType", pathType);
                return command.ExecuteNonQuery();
            });
        }

        public static void DeletePlaceholder(string placeholdersDbPath, string path)
        {
            RunSqliteCommand(placeholdersDbPath, command =>
            {
                command.CommandText = "DELETE FROM Placeholder WHERE path = @path";
                command.Parameters.AddWithValue("@path", path);
                return command.ExecuteNonQuery();
            });
        }

        public static string ReadAllTextFromWriteLockedFile(string filename)
        {
            // File.ReadAllText and others attempt to open for read and FileShare.None, which always fail on
            // the placeholder db and other files that open for write and only share read access
            using (StreamReader reader = new StreamReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
            {
                return reader.ReadToEnd();
            }
        }

        public static void ModifiedPathsContentsShouldEqual(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem, string contents)
        {
            string modifedPathsContents = GetModifiedPathsContents(enlistment, fileSystem);
            modifedPathsContents.ShouldEqual(contents);
        }

        public static void ModifiedPathsShouldContain(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem, params string[] gitPaths)
        {
            string modifedPathsContents = GetModifiedPathsContents(enlistment, fileSystem);
            string[] modifedPathLines = modifedPathsContents.Split(new[] { ModifiedPathsNewLine }, StringSplitOptions.None);
            foreach (string gitPath in gitPaths)
            {
                modifedPathLines.ShouldContain(path => path.Equals(ModifedPathsLineAddPrefix + gitPath, FileSystemHelpers.PathComparison));
            }
        }

        public static void ModifiedPathsShouldNotContain(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem, params string[] gitPaths)
        {
            string modifedPathsContents = GetModifiedPathsContents(enlistment, fileSystem);
            string[] modifedPathLines = modifedPathsContents.Split(new[] { ModifiedPathsNewLine }, StringSplitOptions.None);
            foreach (string gitPath in gitPaths)
            {
                modifedPathLines.ShouldNotContain(
                    path =>
                    {
                        return path.Equals(ModifedPathsLineAddPrefix + gitPath, FileSystemHelpers.PathComparison) ||
                               path.Equals(ModifedPathsLineDeletePrefix + gitPath, FileSystemHelpers.PathComparison);
                    });
            }
        }

        public static string GetInternalParameter(string maintenanceJob = "null", string packfileMaintenanceBatchSize = "null")
        {
            return $"\"{{\\\"ServiceName\\\":\\\"{GVFSServiceProcess.TestServiceName}\\\"," +
                    "\\\"StartedByService\\\":false," +
                    $"\\\"MaintenanceJob\\\":{maintenanceJob}," +
                    $"\\\"PackfileMaintenanceBatchSize\\\":{packfileMaintenanceBatchSize}}}\"";
        }

        public static void RegisterForOfflineIO()
        {
        }

        public static void UnregisterForOfflineIO()
        {
        }

        public static int GetCurrentDiskLayoutMajorVersion()
        {
            return WindowsCurrentDiskLayoutMajorVersion;
        }

        public static int GetCurrentDiskLayoutMinimumMajorVersion()
        {
            return WindowsCurrentDiskLayoutMinimumMajorVersion;
        }

        private static string GetModifiedPathsContents(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem)
        {
            enlistment.WaitForBackgroundOperations();
            string modifiedPathsDatabase = Path.Combine(enlistment.DotGVFSRoot, TestConstants.Databases.ModifiedPaths);
            modifiedPathsDatabase.ShouldBeAFile(fileSystem);
            return GVFSHelpers.ReadAllTextFromWriteLockedFile(modifiedPathsDatabase);
        }

        private static T RunSqliteCommand(string sqliteDbPath, Func runCommand)
        {
            string connectionString = $"data source={sqliteDbPath}";
            using (SqliteConnection connection = new SqliteConnection(connectionString))
            {
                connection.Open();
                using (SqliteCommand command = connection.CreateCommand())
                {
                    return runCommand(command);
                }
            }
        }

        private static byte[] StringToShaBytes(string sha)
        {
            byte[] shaBytes = new byte[20];

            string upperCaseSha = sha.ToUpper();
            int stringIndex = 0;
            for (int i = 0; i < 20; ++i)
            {
                stringIndex = i * 2;
                char firstChar = sha[stringIndex];
                char secondChar = sha[stringIndex + 1];
                shaBytes[i] = (byte)(CharToByte(firstChar) << 4 | CharToByte(secondChar));
            }

            return shaBytes;
        }

        private static byte CharToByte(char c)
        {
            if (c >= '0' && c <= '9')
            {
                return (byte)(c - '0');
            }

            if (c >= 'A' && c <= 'F')
            {
                return (byte)(10 + (c - 'A'));
            }

            Assert.Fail($"Invalid character c: {c}");

            return 0;
        }

        private static string GetPersistedValue(string dotGVFSRoot, string key)
        {
            string metadataPath = Path.Combine(dotGVFSRoot, RepoMetadataName);
            string json;
            using (FileStream fs = new FileStream(metadataPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
            using (StreamReader reader = new StreamReader(fs))
            {
                while (!reader.EndOfStream)
                {
                    json = reader.ReadLine();
                    json.Substring(0, 2).ShouldEqual("A ");

                    KeyValuePair kvp = JsonConvert.DeserializeObject>(json.Substring(2));
                    if (kvp.Key == key)
                    {
                        return kvp.Value;
                    }
                }
            }

            return null;
        }

        private static void SavePersistedValue(string dotGVFSRoot, string key, string value)
        {
            string metadataPath = Path.Combine(dotGVFSRoot, RepoMetadataName);

            Dictionary repoMetadata = new Dictionary();
            string json;
            using (FileStream fs = new FileStream(metadataPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
            using (StreamReader reader = new StreamReader(fs))
            {
                while (!reader.EndOfStream)
                {
                    json = reader.ReadLine();
                    json.Substring(0, 2).ShouldEqual("A ");

                    KeyValuePair kvp = JsonConvert.DeserializeObject>(json.Substring(2));
                    repoMetadata.Add(kvp.Key, kvp.Value);
                }
            }

            repoMetadata[key] = value;

            string newRepoMetadataContents = string.Empty;

            foreach (KeyValuePair kvp in repoMetadata)
            {
                newRepoMetadataContents += "A " + JsonConvert.SerializeObject(kvp).Trim() + "\r\n";
            }

            File.WriteAllText(metadataPath, newRepoMetadataContents);
        }

        [DllImport(PrjFSLibPath, EntryPoint = "PrjFS_RegisterForOfflineIO")]
        private static extern uint PrjFSRegisterForOfflineIO();

        [DllImport(PrjFSLibPath, EntryPoint = "PrjFS_UnregisterForOfflineIO")]
        private static extern uint PrjFSUnregisterForOfflineIO();
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/GVFSProcess.cs
================================================
using GVFS.Tests.Should;
using System;
using System.Diagnostics;
using System.IO;

namespace GVFS.FunctionalTests.Tools
{
    public class GVFSProcess
    {
        private const int SuccessExitCode = 0;
        private const int ExitCodeShouldNotBeZero = -1;
        private const int DoNotCheckExitCode = -2;

        private readonly string pathToGVFS;
        private readonly string enlistmentRoot;
        private readonly string localCacheRoot;

        public GVFSProcess(GVFSFunctionalTestEnlistment enlistment)
            : this(GVFSTestConfig.PathToGVFS, enlistment.EnlistmentRoot, Path.Combine(enlistment.EnlistmentRoot, GVFSTestConfig.DotGVFSRoot))
        {
        }

        public GVFSProcess(string pathToGVFS, string enlistmentRoot, string localCacheRoot)
        {
            this.pathToGVFS = pathToGVFS;
            this.enlistmentRoot = enlistmentRoot;
            this.localCacheRoot = localCacheRoot;
        }

        public void Clone(string repositorySource, string branchToCheckout, bool skipPrefetch)
        {
            string args = string.Format(
                "clone \"{0}\" \"{1}\" --branch \"{2}\" --local-cache-path \"{3}\" {4}",
                repositorySource,
                this.enlistmentRoot,
                branchToCheckout,
                this.localCacheRoot,
                skipPrefetch ? "--no-prefetch" : string.Empty);
            this.CallGVFS(args, expectedExitCode: SuccessExitCode);
        }

        public void Mount()
        {
            string output;
            this.TryMount(out output).ShouldEqual(true, "GVFS did not mount: " + output);
            output.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: "warning");
        }

        public bool TryMount(out string output)
        {
            this.IsEnlistmentMounted().ShouldEqual(false, "GVFS is already mounted");
            output = this.CallGVFS("mount \"" + this.enlistmentRoot + "\"");
            return this.IsEnlistmentMounted();
        }

        public string PruneSparseNoFolders()
        {
            return this.SparseCommand(addFolders: true, shouldPrune: true, shouldSucceed: true, folders: new string[0]);
        }

        public string AddSparseFolders(params string[] folders)
        {
            return this.SparseCommand(addFolders: true, shouldPrune: false, shouldSucceed: true, folders: folders);
        }

        public string AddSparseFolders(bool shouldPrune, params string[] folders)
        {
            return this.SparseCommand(addFolders: true, shouldPrune: shouldPrune, shouldSucceed: true, folders: folders);
        }

        public string AddSparseFolders(bool shouldPrune, bool shouldSucceed, params string[] folders)
        {
            return this.SparseCommand(addFolders: true, shouldPrune: shouldPrune, shouldSucceed: shouldSucceed, folders: folders);
        }

        public string RemoveSparseFolders(params string[] folders)
        {
            return this.SparseCommand(addFolders: false, shouldPrune: false, shouldSucceed: true, folders: folders);
        }

        public string RemoveSparseFolders(bool shouldPrune, params string[] folders)
        {
            return this.SparseCommand(addFolders: false, shouldPrune: shouldPrune, shouldSucceed: true, folders: folders);
        }

        public string RemoveSparseFolders(bool shouldPrune, bool shouldSucceed, params string[] folders)
        {
            return this.SparseCommand(addFolders: false, shouldPrune: shouldPrune, shouldSucceed: shouldSucceed, folders: folders);
        }

        public string SparseCommand(bool addFolders, bool shouldPrune, bool shouldSucceed, params string[] folders)
        {
            string action = string.Empty;
            string folderList = string.Empty;
            string pruneArg = shouldPrune ? "--prune" : string.Empty;

            if (folders.Length > 0)
            {
                action = addFolders ? "-a" : "-r";
                folderList = string.Join(";", folders);
                if (folderList.Contains(" "))
                {
                    folderList = $"\"{folderList}\"";
                }
            }

            return this.Sparse($"{pruneArg} {action} {folderList}", shouldSucceed);
        }

        public string Sparse(string arguments, bool shouldSucceed)
        {
            return this.CallGVFS($"sparse {this.enlistmentRoot} {arguments}", expectedExitCode: shouldSucceed ? SuccessExitCode : ExitCodeShouldNotBeZero);
        }

        public string[] GetSparseFolders()
        {
            string output = this.CallGVFS($"sparse {this.enlistmentRoot} -l");
            if (output.StartsWith("No folders in sparse list."))
            {
                return new string[0];
            }

            return output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
        }

        public string Prefetch(string args, bool failOnError, string standardInput = null)
        {
            return this.CallGVFS("prefetch \"" + this.enlistmentRoot + "\" " + args, failOnError ? SuccessExitCode : DoNotCheckExitCode, standardInput: standardInput);
        }

        public void Repair(bool confirm)
        {
            string confirmArg = confirm ? "--confirm " : string.Empty;
            this.CallGVFS(
                "repair " + confirmArg + "\"" + this.enlistmentRoot + "\"",
                expectedExitCode: SuccessExitCode);
        }

        public string LooseObjectStep()
        {
            return this.CallGVFS(
                "dehydrate \"" + this.enlistmentRoot + "\"",
                expectedExitCode: SuccessExitCode,
                internalParameter: GVFSHelpers.GetInternalParameter("\\\"LooseObjects\\\""));
        }

        public string PackfileMaintenanceStep(long? batchSize)
        {
            string sizeString = batchSize.HasValue ? $"\\\"{batchSize.Value}\\\"" : "null";
            string internalParameter = GVFSHelpers.GetInternalParameter("\\\"PackfileMaintenance\\\"", sizeString);
            return this.CallGVFS(
                "dehydrate \"" + this.enlistmentRoot + "\"",
                expectedExitCode: SuccessExitCode,
                internalParameter: internalParameter);
        }

        public string PostFetchStep()
        {
            string internalParameter = GVFSHelpers.GetInternalParameter("\\\"PostFetch\\\"");
            return this.CallGVFS(
                "dehydrate \"" + this.enlistmentRoot + "\"",
                expectedExitCode: SuccessExitCode,
                internalParameter: internalParameter);
        }

        public string Diagnose()
        {
            return this.CallGVFS("diagnose \"" + this.enlistmentRoot + "\"");
        }

        public string Status(string trace = null)
        {
            return this.CallGVFS("status " + this.enlistmentRoot, trace: trace);
        }

        public string Health(string directory = null)
        {
            if (string.IsNullOrEmpty(directory))
            {
                return this.CallGVFS("health \"" + this.enlistmentRoot + '"');
            }
            else
            {
                return this.CallGVFS("health -d \"" + directory + "\" \"" + this.enlistmentRoot + '"');
            }
        }

        public string CacheServer(string args)
        {
            return this.CallGVFS("cache-server " + args + " \"" + this.enlistmentRoot + "\"");
        }

        public void Unmount()
        {
            if (this.IsEnlistmentMounted())
            {
                string result = this.CallGVFS("unmount \"" + this.enlistmentRoot + "\"", expectedExitCode: SuccessExitCode);
                this.IsEnlistmentMounted().ShouldEqual(false, "GVFS did not unmount: " + result);
            }
        }

        public bool IsEnlistmentMounted()
        {
            string statusResult = this.CallGVFS("status \"" + this.enlistmentRoot + "\"");
            return statusResult.Contains("Mount status: Ready");
        }

        public string RunServiceVerb(string argument)
        {
            return this.CallGVFS("service " + argument, expectedExitCode: SuccessExitCode);
        }

        public string ReadConfig(string key, bool failOnError)
        {
            return this.CallGVFS($"config {key}", failOnError ? SuccessExitCode : DoNotCheckExitCode).TrimEnd('\r', '\n');
        }

        public void WriteConfig(string key, string value)
        {
            this.CallGVFS($"config {key} {value}", expectedExitCode: SuccessExitCode);
        }

        public void DeleteConfig(string key)
        {
            this.CallGVFS($"config --delete {key}", expectedExitCode: SuccessExitCode);
        }

        /// 
        /// Invokes a call to gvfs using the arguments specified
        /// 
        /// The arguments to use when invoking gvfs
        /// 
        /// What the expected exit code should be.
        /// >= than 0 to check the exit code explicitly
        /// -1 = Fail if the exit code is 0
        /// -2 = Do not check the exit code (Default)
        /// 
        /// What to set the GIT_TRACE environment variable to
        /// What to write to the standard input stream
        /// The internal parameter to set in the arguments
        /// 
        private string CallGVFS(string args, int expectedExitCode = DoNotCheckExitCode, string trace = null, string standardInput = null, string internalParameter = null)
        {
            ProcessStartInfo processInfo = null;
            processInfo = new ProcessStartInfo(this.pathToGVFS);

            if (internalParameter == null)
            {
                internalParameter = GVFSHelpers.GetInternalParameter();
            }

            processInfo.Arguments = args + " " + TestConstants.InternalUseOnlyFlag + " " + internalParameter;

            processInfo.WindowStyle = ProcessWindowStyle.Hidden;
            processInfo.UseShellExecute = false;
            processInfo.RedirectStandardOutput = true;
            if (standardInput != null)
            {
                processInfo.RedirectStandardInput = true;
            }

            if (trace != null)
            {
                processInfo.EnvironmentVariables["GIT_TRACE"] = trace;
            }

            using (Process process = Process.Start(processInfo))
            {
                if (standardInput != null)
                {
                    process.StandardInput.Write(standardInput);
                    process.StandardInput.Close();
                }

                string result = process.StandardOutput.ReadToEnd();
                process.WaitForExit();

                if (expectedExitCode >= SuccessExitCode)
                {
                    process.ExitCode.ShouldEqual(expectedExitCode, result);
                }
                else if (expectedExitCode == ExitCodeShouldNotBeZero)
                {
                    process.ExitCode.ShouldNotEqual(SuccessExitCode, "Exit code should not be zero");
                }

                return result;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/GVFSServiceProcess.cs
================================================
using GVFS.Tests.Should;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.ServiceProcess;
using System.Threading;

namespace GVFS.FunctionalTests.Tools
{
    public static class GVFSServiceProcess
    {
        private static readonly string ServiceNameArgument = "--servicename=" + TestServiceName;
        private static Process consoleServiceProcess;

        public static string TestServiceName
        {
            get
            {
                string name = Environment.GetEnvironmentVariable("GVFS_TEST_SERVICE_NAME");
                return string.IsNullOrWhiteSpace(name) ? "Test.GVFS.Service" : name;
            }
        }

        public static void InstallService()
        {
            if (GVFSTestConfig.IsDevMode)
            {
                StartServiceAsConsoleProcess();
            }
            else
            {
                InstallWindowsService();
            }
        }

        public static void UninstallService()
        {
            if (GVFSTestConfig.IsDevMode)
            {
                StopConsoleServiceProcess();
                CleanupServiceData();
            }
            else
            {
                UninstallWindowsService();
            }
        }

        public static void StartService()
        {
            if (GVFSTestConfig.IsDevMode)
            {
                StartServiceAsConsoleProcess();
            }
            else
            {
                StartWindowsService();
            }
        }

        public static void StopService()
        {
            if (GVFSTestConfig.IsDevMode)
            {
                StopConsoleServiceProcess();
            }
            else
            {
                StopWindowsService();
            }
        }

        private static void StartServiceAsConsoleProcess()
        {
            StopConsoleServiceProcess();

            string pathToService = GetPathToService();
            Console.WriteLine("Starting test service in console mode: " + pathToService);

            ProcessStartInfo startInfo = new ProcessStartInfo(pathToService);
            startInfo.Arguments = $"--console {ServiceNameArgument}";
            startInfo.UseShellExecute = false;
            startInfo.CreateNoWindow = true;
            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;

            consoleServiceProcess = Process.Start(startInfo);
            consoleServiceProcess.ShouldNotBeNull("Failed to start test service process");

            // Consume output asynchronously to prevent buffer deadlock
            consoleServiceProcess.BeginOutputReadLine();
            consoleServiceProcess.BeginErrorReadLine();

            // Wait for the service to start listening on its named pipe
            string pipeName = TestServiceName + ".pipe";
            int retries = 50;
            while (retries-- > 0)
            {
                if (consoleServiceProcess.HasExited)
                {
                    throw new InvalidOperationException(
                        $"Test service process exited with code {consoleServiceProcess.ExitCode} before becoming ready");
                }

                if (File.Exists(@"\\.\pipe\" + pipeName))
                {
                    Console.WriteLine("Test service is ready (pipe: " + pipeName + ")");
                    return;
                }

                Thread.Sleep(200);
            }

            throw new System.TimeoutException("Timed out waiting for test service pipe: " + pipeName);
        }

        private static void StopConsoleServiceProcess()
        {
            if (consoleServiceProcess != null && !consoleServiceProcess.HasExited)
            {
                try
                {
                    Console.WriteLine("Stopping test service console process (PID: " + consoleServiceProcess.Id + ")");
                    consoleServiceProcess.Kill();
                    consoleServiceProcess.WaitForExit(5000);
                }
                catch (InvalidOperationException)
                {
                    // Process already exited
                }

                consoleServiceProcess = null;
            }
        }

        private static void CleanupServiceData()
        {
            string commonAppDataRoot = Environment.GetEnvironmentVariable("GVFS_COMMON_APPDATA_ROOT");
            string serviceData;
            if (!string.IsNullOrEmpty(commonAppDataRoot))
            {
                serviceData = Path.Combine(commonAppDataRoot, TestServiceName);
            }
            else
            {
                serviceData = Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
                    "GVFS",
                    TestServiceName);
            }

            DirectoryInfo serviceDataDir = new DirectoryInfo(serviceData);
            if (serviceDataDir.Exists)
            {
                serviceDataDir.Delete(true);
            }
        }

        private static void InstallWindowsService()
        {
            Console.WriteLine("Installing " + TestServiceName);

            UninstallWindowsService();

            // Wait for delete to complete. If the services control panel is open, this will never complete.
            while (RunScCommand("query", TestServiceName).ExitCode == 0)
            {
                Thread.Sleep(1000);
            }

            // Install service
            string pathToService = GetPathToService();
            Console.WriteLine("Using service executable: " + pathToService);

            File.Exists(pathToService).ShouldBeTrue($"{pathToService} does not exist");

            string createServiceArguments = string.Format(
                "{0} binPath= \"{1}\"",
                TestServiceName,
                pathToService);

            ProcessResult result = RunScCommand("create", createServiceArguments);
            result.ExitCode.ShouldEqual(0, "Failure while running sc create " + createServiceArguments + "\r\n" + result.Output);

            StartWindowsService();
        }

        private static void UninstallWindowsService()
        {
            StopWindowsService();

            RunScCommand("delete", TestServiceName);

            // Make sure to delete any test service data state
            string serviceData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "GVFS", TestServiceName);
            DirectoryInfo serviceDataDir = new DirectoryInfo(serviceData);
            if (serviceDataDir.Exists)
            {
                serviceDataDir.Delete(true);
            }
        }

        private static void StartWindowsService()
        {
            ServiceController testService = ServiceController.GetServices().SingleOrDefault(service => service.ServiceName == TestServiceName);
            testService.ShouldNotBeNull($"{TestServiceName} does not exist as a service");

            using (ServiceController controller = new ServiceController(TestServiceName))
            {
                controller.Start(new[] { ServiceNameArgument });
                controller.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10));
                controller.Status.ShouldEqual(ServiceControllerStatus.Running);
            }
        }

        private static void StopWindowsService()
        {
            try
            {
                ServiceController testService = ServiceController.GetServices().SingleOrDefault(service => service.ServiceName == TestServiceName);
                if (testService != null)
                {
                    if (testService.Status == ServiceControllerStatus.Running)
                    {
                        testService.Stop();
                    }

                    testService.WaitForStatus(ServiceControllerStatus.Stopped);
                }
            }
            catch (InvalidOperationException)
            {
                return;
            }
        }

        private static ProcessResult RunScCommand(string command, string parameters)
        {
            ProcessStartInfo processInfo = new ProcessStartInfo("sc");
            processInfo.WindowStyle = ProcessWindowStyle.Hidden;
            processInfo.UseShellExecute = false;
            processInfo.RedirectStandardOutput = true;

            processInfo.Arguments = command + " " + parameters;

            return ProcessHelper.Run(processInfo);
        }

        private static string GetPathToService()
        {
            File.Exists(Properties.Settings.Default.PathToGVFSService).ShouldBeTrue("Failed to locate GVFS.Service.exe");
            return Properties.Settings.Default.PathToGVFSService;
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs
================================================
using GVFS.FunctionalTests.Properties;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace GVFS.FunctionalTests.Tools
{
    public static class GitHelpers
    {
        /// 
        /// This string must match the command name provided in the
        /// GVFS.FunctionalTests.LockHolder program.
        /// 
        private const string LockHolderCommandName = @"GVFS.FunctionalTests.LockHolder";
        private const string LockHolderCommand = @"GVFS.FunctionalTests.LockHolder.exe";

        private const string WindowsPathSeparator = "\\";
        private const string GitPathSeparator = "/";

        private static string LockHolderCommandPath
        {
            get
            {
                // LockHolder is a .NET Framework application and can be found inside
                // GVFS.FunctionalTest Output directory.
                return Path.Combine(Settings.Default.CurrentDirectory, LockHolderCommand);
            }
        }

        public static string ConvertPathToGitFormat(string relativePath)
        {
            return relativePath.Replace(WindowsPathSeparator, GitPathSeparator);
        }

        public static void CheckGitCommand(string virtualRepoRoot, string command, params string[] expectedLinesInResult)
        {
            ProcessResult result = GitProcess.InvokeProcess(virtualRepoRoot, command);
            result.Errors.ShouldBeEmpty();
            foreach (string line in expectedLinesInResult)
            {
                result.Output.ShouldContain(line);
            }
        }

        public static void CheckGitCommandAgainstGVFSRepo(string virtualRepoRoot, string command, params string[] expectedLinesInResult)
        {
            ProcessResult result = InvokeGitAgainstGVFSRepo(virtualRepoRoot, command);
            result.Errors.ShouldBeEmpty();
            foreach (string line in expectedLinesInResult)
            {
                result.Output.ShouldContain(line);
            }
        }

        public static ProcessResult InvokeGitAgainstGVFSRepo(
            string gvfsRepoRoot,
            string command,
            Dictionary environmentVariables = null,
            bool removeWaitingMessages = true,
            bool removeUpgradeMessages = true,
            bool removePartialHydrationMessages = true,
            bool removeFSMonitorMessages = true,
            bool removeGvfsHealthMessages = true)
        {
            ProcessResult result = GitProcess.InvokeProcess(gvfsRepoRoot, command, environmentVariables);
            string output = FilterMessages(result.Output, false, false, false, removePartialHydrationMessages, removeFSMonitorMessages, removeGvfsHealthMessages);
            string errors = FilterMessages(result.Errors, true, removeWaitingMessages, removeUpgradeMessages, removePartialHydrationMessages, removeFSMonitorMessages, removeGvfsHealthMessages);

            return new ProcessResult(
                output,
                errors,
                result.ExitCode);
        }

        private static IEnumerable SplitLinesKeepingNewlines(string input)
        {
            for (int start = 0;  start < input.Length; )
            {
                int nextLine = input.IndexOf('\n', start) + 1;

                if (nextLine == 0)
                {
                    // No more newlines, yield the rest
                    nextLine = input.Length;
                }

                yield return input.Substring(start, nextLine - start);
                start = nextLine;
            }
        }

        private static string FilterMessages(
            string input,
            bool removeEmptyLines,
            bool removeWaitingMessages,
            bool removeUpgradeMessages,
            bool removePartialHydrationMessages,
            bool removeFSMonitorMessages,
            bool removeGvfsHealthMessages)
        {
            if (!string.IsNullOrEmpty(input) && (removeWaitingMessages || removeUpgradeMessages || removePartialHydrationMessages || removeFSMonitorMessages))
            {
                IEnumerable lines = SplitLinesKeepingNewlines(input);
                IEnumerable filteredLines = lines.Where(line =>
                {
                    if ((removeEmptyLines && string.IsNullOrWhiteSpace(line)) ||
                        (removeUpgradeMessages && line.StartsWith("A new version of VFS for Git is available.")) ||
                        (removeWaitingMessages && line.StartsWith("Waiting for ")) ||
                        (removePartialHydrationMessages && line.StartsWith("You are in a partially-hydrated checkout with ")) ||
                        (removeGvfsHealthMessages && line.TrimEnd().EndsWith("Run 'gvfs health' for details.")) ||
                        (removeFSMonitorMessages && line.TrimEnd().EndsWith(" is incompatible with fsmonitor")))
                    {
                        return false;
                    }
                    else
                    {
                        return true;
                    }
                });

                return filteredLines.Any() ? string.Join("", filteredLines) : string.Empty;
            }

            return input;
        }

        public static void ValidateGitCommand(
            GVFSFunctionalTestEnlistment enlistment,
            ControlGitRepo controlGitRepo,
            string command,
            params object[] args)
        {
            command = string.Format(command, args);
            string controlRepoRoot = controlGitRepo.RootPath;
            string gvfsRepoRoot = enlistment.RepoRoot;

            Dictionary environmentVariables = new Dictionary();
            environmentVariables["GIT_QUIET"] = "true";
            environmentVariables["GIT_COMMITTER_DATE"] = "Thu Feb 16 10:07:35 2017 -0700";

            ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command, environmentVariables);
            ProcessResult actualResult = GitHelpers.InvokeGitAgainstGVFSRepo(gvfsRepoRoot, command, environmentVariables);

            ErrorsShouldMatch(command, expectedResult, actualResult);
            actualResult.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                .ShouldMatchInOrder(expectedResult.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), LinesAreEqual, command + " Output Lines");

            if (command != "status")
            {
                ValidateGitCommand(enlistment, controlGitRepo, "status");
            }
        }

        /// 
        /// Acquire the GVFSLock. This method will return once the GVFSLock has been acquired.
        /// 
        /// The ID of the process that acquired the lock.
        ///  that can be signaled to exit the lock acquisition program.
        public static ManualResetEventSlim AcquireGVFSLock(
            GVFSFunctionalTestEnlistment enlistment,
            out int processId,
            int resetTimeout = Timeout.Infinite,
            bool skipReleaseLock = false)
        {
            string args = null;
            if (skipReleaseLock)
            {
                args = "--skip-release-lock";
            }

            return RunCommandWithWaitAndStdIn(
                enlistment,
                resetTimeout,
                LockHolderCommandPath,
                args,
                GitHelpers.LockHolderCommandName,
                "done",
                out processId);
        }

        /// 
        /// Run the specified Git command. This method will return once the GVFSLock has been acquired.
        /// 
        /// The ID of the process that acquired the lock.
        ///  that can be signaled to exit the lock acquisition program.
        public static ManualResetEventSlim RunGitCommandWithWaitAndStdIn(
            GVFSFunctionalTestEnlistment enlistment,
            int resetTimeout,
            string command,
            string stdinToQuit,
            out int processId)
        {
            return
                RunCommandWithWaitAndStdIn(
                    enlistment,
                    resetTimeout,
                    Properties.Settings.Default.PathToGit,
                    command,
                    "git " + command,
                    stdinToQuit,
                    out processId);
        }

        public static void ErrorsShouldMatch(string command, ProcessResult expectedResult, ProcessResult actualResult)
        {
            actualResult.Errors.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                .ShouldMatchInOrder(expectedResult.Errors.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), LinesAreEqual, command + " Errors Lines");
        }

        /// 
        /// Run the specified command as an external program. This method will return once the GVFSLock has been acquired.
        /// 
        /// The ID of the process that acquired the lock.
        ///  that can be signaled to exit the lock acquisition program.
        private static ManualResetEventSlim RunCommandWithWaitAndStdIn(
            GVFSFunctionalTestEnlistment enlistment,
            int resetTimeout,
            string pathToCommand,
            string args,
            string lockingProcessCommandName,
            string stdinToQuit,
            out int processId)
        {
            ManualResetEventSlim resetEvent = new ManualResetEventSlim(initialState: false);

            ProcessStartInfo processInfo = new ProcessStartInfo(pathToCommand);
            processInfo.WorkingDirectory = enlistment.RepoRoot;
            processInfo.UseShellExecute = false;
            processInfo.RedirectStandardOutput = true;
            processInfo.RedirectStandardError = true;
            processInfo.RedirectStandardInput = true;
            processInfo.Arguments = args;

            Process holdingProcess = Process.Start(processInfo);
            StreamWriter stdin = holdingProcess.StandardInput;
            processId = holdingProcess.Id;

            enlistment.WaitForLock(lockingProcessCommandName);

            Task.Run(
                () =>
                {
                    resetEvent.Wait(resetTimeout);

                    try
                    {
                        // Make sure to let the holding process end.
                        if (stdin != null)
                        {
                            stdin.WriteLine(stdinToQuit);
                            stdin.Close();
                        }

                        if (holdingProcess != null)
                        {
                            bool holdingProcessHasExited = holdingProcess.WaitForExit(10000);

                            if (!holdingProcess.HasExited)
                            {
                                holdingProcess.Kill();
                            }

                            holdingProcess.Dispose();

                            holdingProcessHasExited.ShouldBeTrue("Locking process did not exit in time.");
                        }
                    }
                    catch (Exception ex)
                    {
                        Assert.Fail($"{nameof(RunCommandWithWaitAndStdIn)} exception closing stdin {ex.ToString()}");
                    }
                    finally
                    {
                        resetEvent.Set();
                    }
                });

            return resetEvent;
        }

        private static bool LinesAreEqual(string actualLine, string expectedLine)
        {
            return actualLine.Equals(expectedLine);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/GitProcess.cs
================================================
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

namespace GVFS.FunctionalTests.Tools
{
    public static class GitProcess
    {
        public static string Invoke(string executionWorkingDirectory, string command)
        {
            return InvokeProcess(executionWorkingDirectory, command).Output;
        }

        public static ProcessResult InvokeProcess(string executionWorkingDirectory, string command, Dictionary environmentVariables = null, Stream inputStream = null)
        {
            ProcessStartInfo processInfo = new ProcessStartInfo(Properties.Settings.Default.PathToGit);
            processInfo.WorkingDirectory = executionWorkingDirectory;
            processInfo.UseShellExecute = false;
            processInfo.RedirectStandardOutput = true;
            processInfo.RedirectStandardError = true;
            processInfo.Arguments = command;

            if (inputStream != null)
            {
                processInfo.RedirectStandardInput = true;
            }

            processInfo.EnvironmentVariables["GIT_TERMINAL_PROMPT"] = "0";

            if (environmentVariables != null)
            {
                foreach (string key in environmentVariables.Keys)
                {
                    processInfo.EnvironmentVariables[key] = environmentVariables[key];
                }
            }

            return ProcessHelper.Run(processInfo, inputStream: inputStream);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/NativeMethods.cs
================================================
using Microsoft.Win32.SafeHandles;
using System;
using System.IO;
using System.Runtime.InteropServices;

namespace GVFS.FunctionalTests.Tools
{
    public class NativeMethods
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool MoveFile(string lpExistingFileName, string lpNewFileName);

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern SafeFileHandle CreateFile(
            [In] string lpFileName,
            uint dwDesiredAccess,
            FileShare dwShareMode,
            [In] IntPtr lpSecurityAttributes,
            [MarshalAs(UnmanagedType.U4)]FileMode dwCreationDisposition,
            uint dwFlagsAndAttributes,
            [In] IntPtr hTemplateFile);
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/ProcessHelper.cs
================================================
using System.Diagnostics;
using System.IO;

namespace GVFS.FunctionalTests.Tools
{
    public static class ProcessHelper
    {
        public static ProcessResult Run(string fileName, string arguments)
        {
            return Run(fileName, arguments, null);
        }

        public static ProcessResult Run(string fileName, string arguments, string workingDirectory)
        {
            ProcessStartInfo startInfo = new ProcessStartInfo();
            startInfo.UseShellExecute = false;
            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;
            startInfo.CreateNoWindow = true;
            startInfo.FileName = fileName;
            startInfo.Arguments = arguments;
            if (!string.IsNullOrEmpty(workingDirectory))
            {
                startInfo.WorkingDirectory = workingDirectory;
            }

            return Run(startInfo);
        }

        public static ProcessResult Run(ProcessStartInfo processInfo, string errorMsgDelimeter = "\r\n", object executionLock = null, Stream inputStream = null)
        {
            using (Process executingProcess = new Process())
            {
                string output = string.Empty;
                string errors = string.Empty;

                // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx
                // To avoid deadlocks, use asynchronous read operations on at least one of the streams.
                // Do not perform a synchronous read to the end of both redirected streams.
                executingProcess.StartInfo = processInfo;
                executingProcess.ErrorDataReceived += (sender, args) =>
                {
                    if (args.Data != null)
                    {
                        errors = errors + args.Data + errorMsgDelimeter;
                    }
                };

                if (executionLock != null)
                {
                    lock (executionLock)
                    {
                        output = StartProcess(executingProcess, inputStream);
                    }
                }
                else
                {
                    output = StartProcess(executingProcess, inputStream);
                }

                return new ProcessResult(output.ToString(), errors.ToString(), executingProcess.ExitCode);
            }
        }

        private static string StartProcess(Process executingProcess, Stream inputStream = null)
        {
            executingProcess.Start();

            if (inputStream != null)
            {
                inputStream.CopyTo(executingProcess.StandardInput.BaseStream);
            }

            if (executingProcess.StartInfo.RedirectStandardError)
            {
                executingProcess.BeginErrorReadLine();
            }

            string output = string.Empty;
            if (executingProcess.StartInfo.RedirectStandardOutput)
            {
                output = executingProcess.StandardOutput.ReadToEnd();
            }

            executingProcess.WaitForExit();

            return output;
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/ProcessResult.cs
================================================
namespace GVFS.FunctionalTests.Tools
{
    public class ProcessResult
    {
        public ProcessResult(string output, string errors, int exitCode)
        {
            this.Output = output;
            this.Errors = errors;
            this.ExitCode = exitCode;
        }

        public string Output { get; }
        public string Errors { get; }
        public int ExitCode { get; }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/ProjFSFilterInstaller.cs
================================================
using GVFS.Tests.Should;
using System;
using System.IO;

namespace GVFS.FunctionalTests.Tools
{
    public class ProjFSFilterInstaller
    {
        private const string GVFSServiceName = "GVFS.Service";
        private const string ProjFSServiceName = "prjflt";
        private const string OptionalFeatureName = "Client-ProjFS";
        private const string GVFSInstallPath = @"C:\Program Files\VFS for Git";
        private const string NativeProjFSLibInstallLocation = GVFSInstallPath + @"\ProjFS\ProjectedFSLib.dll";

        private const string PrjfltInfName = "prjflt.inf";
        private const string PrjfltInfInstallFolder = GVFSInstallPath + @"\Filter";

        private const string PrjfltSysName = "prjflt.sys";
        private const string System32DriversPath = @"C:\Windows\System32\drivers";

        public static void ReplaceInboxProjFS()
        {
            if (IsInboxProjFSEnabled())
            {
                StopService(GVFSServiceName);
                StopService(ProjFSServiceName);
                DisableAndRemoveInboxProjFS();
                InstallProjFSViaINF();
                ValidateProjFSInstalled();
                StartService(ProjFSServiceName);
                StartService(GVFSServiceName);
            }
            else
            {
                ValidateProjFSInstalled();
            }
        }

        private static ProcessResult CallPowershellCommand(string command)
        {
            return ProcessHelper.Run("powershell.exe", "-NonInteractive -NoProfile -Command \"& { " + command + " }\"");
        }

        private static bool IsInboxProjFSEnabled()
        {
            const int ProjFSNotAnOptionalFeature = 2;
            const int ProjFSEnabled = 3;
            const int ProjFSDisabled = 4;

            ProcessResult getOptionalFeatureResult = CallPowershellCommand(
                "$var=(Get-WindowsOptionalFeature -Online -FeatureName " + OptionalFeatureName + ");  if($var -eq $null){exit " +
                ProjFSNotAnOptionalFeature + "}else{if($var.State -eq 'Enabled'){exit " + ProjFSEnabled + "}else{exit " + ProjFSDisabled + "}}");

            return getOptionalFeatureResult.ExitCode == ProjFSEnabled;
        }

        private static void StartService(string serviceName)
        {
            ProcessResult result = ProcessHelper.Run("sc.exe", $"start {serviceName}");
            Console.WriteLine($"sc start {serviceName} Output: {result.Output}");
            Console.WriteLine($"sc start {serviceName} Errors: {result.Errors}");
            result.ExitCode.ShouldEqual(0, $"Failed to start {serviceName}");
        }

        private static void StopService(string serviceName)
        {
            ProcessResult result = ProcessHelper.Run("sc.exe", $"stop {serviceName}");

            // 1060 -> The specified service does not exist as an installed service
            // 1062 -> The service has not been started
            bool stopSucceeded = result.ExitCode == 0 || result.ExitCode == 1060 || result.ExitCode == 1062;
            Console.WriteLine($"sc stop {serviceName} Output: {result.Output}");
            Console.WriteLine($"sc stop {serviceName} Errors: {result.Errors}");
            stopSucceeded.ShouldBeTrue($"Failed to stop {serviceName}");
        }

        private static void DisableAndRemoveInboxProjFS()
        {
            ProcessResult disableFeatureResult = CallPowershellCommand("Disable-WindowsOptionalFeature -Online -FeatureName " + OptionalFeatureName + " -Remove");
            Console.WriteLine($"Disable ProjfS Output: {disableFeatureResult.Output}");
            Console.WriteLine($"Disable ProjfS Errors: {disableFeatureResult.Errors}");
            disableFeatureResult.ExitCode.ShouldEqual(0, "Error when disabling ProjFS");
        }

        private static void InstallProjFSViaINF()
        {
            File.Exists(NativeProjFSLibInstallLocation).ShouldBeTrue($"{NativeProjFSLibInstallLocation} missing");
            File.Copy(NativeProjFSLibInstallLocation, GVFSInstallPath + @"\ProjectedFSLib.dll", overwrite: true);

            string prjfltInfInstallLocation = Path.Combine(PrjfltInfInstallFolder, PrjfltInfName);
            File.Exists(prjfltInfInstallLocation).ShouldBeTrue($"{prjfltInfInstallLocation} missing");
            ProcessResult result = ProcessHelper.Run("RUNDLL32.EXE", $"SETUPAPI.DLL,InstallHinfSection DefaultInstall 128 {prjfltInfInstallLocation}");
            result.ExitCode.ShouldEqual(0, "Failed to install ProjFS via INF");
        }

        private static void ValidateProjFSInstalled()
        {
            string installPathPrjflt = Path.Combine(PrjfltInfInstallFolder, PrjfltSysName);
            string system32Prjflt = Path.Combine(System32DriversPath, PrjfltSysName);
            ProcessResult result = ProcessHelper.Run("fc.exe", $"/b \"{installPathPrjflt}\" \"{system32Prjflt}\"");
            result.ExitCode.ShouldEqual(0, $"fc failed to validate prjflt.sys");
            result.Output.ShouldContain("no differences encountered");
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/RepositoryHelpers.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using System.Runtime.InteropServices;

namespace GVFS.FunctionalTests.Tools
{
    public static class RepositoryHelpers
    {
        public static void DeleteTestDirectory(string repoPath)
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                // Use cmd.exe to delete the enlistment as it properly handles tombstones and reparse points
                CmdRunner.DeleteDirectoryWithUnlimitedRetries(repoPath);
            }
            else
            {
                BashRunner.DeleteDirectoryWithUnlimitedRetries(repoPath);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Tools/TestConstants.cs
================================================
using System.IO;

namespace GVFS.FunctionalTests.Tools
{
    public static class TestConstants
    {
        public const string AllZeroSha = "0000000000000000000000000000000000000000";
        public const string PartialFolderPlaceholderDatabaseValue = "                          PARTIAL FOLDER";
        public const char GitPathSeparator = '/';
        public const string InternalUseOnlyFlag = "--internal_use_only";

        public static class DotGit
        {
            public const string Root = ".git";
            public static readonly string Head = Path.Combine(DotGit.Root, "HEAD");

            public static class Objects
            {
                public static readonly string Root = Path.Combine(DotGit.Root, "objects");
            }

            public static class Info
            {
                public const string Name = "info";
                public const string AlwaysExcludeName = "always_exclude";
                public const string SparseCheckoutName = "sparse-checkout";

                public static readonly string Root = Path.Combine(DotGit.Root, Info.Name);
                public static readonly string SparseCheckoutPath = Path.Combine(Info.Root, Info.SparseCheckoutName);
                public static readonly string AlwaysExcludePath = Path.Combine(Info.Root, AlwaysExcludeName);
            }
        }

        public static class Databases
        {
            public const string Root = "databases";
            public static readonly string BackgroundOpsFile = Path.Combine(Root, "BackgroundGitOperations.dat");
            public static readonly string ModifiedPaths = Path.Combine(Root, "ModifiedPaths.dat");
            public static readonly string VFSForGit = Path.Combine(Root, "VFSForGit.sqlite");
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Windows/Tests/JunctionAndSubstTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tests.EnlistmentPerFixture;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace GVFS.FunctionalTests.Windows.Tests
{
    [TestFixture]
    [Category(Categories.ExtraCoverage)]
    public class JunctionAndSubstTests : TestsWithEnlistmentPerFixture
    {
        private const string SubstDrive = "Q:";
        private const string SubstDrivePath = @"Q:\";
        private const string JunctionAndSubstTestsName = nameof(JunctionAndSubstTests);
        private const string ExpectedStatusWaitingText = @"Waiting for 'GVFS.FunctionalTests.LockHolder'";

        private string junctionsRoot;
        private FileSystemRunner fileSystem;

        public JunctionAndSubstTests()
        {
            this.fileSystem = new SystemIORunner();
        }

        [SetUp]
        public void SetupJunctionRoot()
        {
            // Create junctionsRoot in Properties.Settings.Default.EnlistmentRoot (the parent folder of the GVFS enlistment root `this.Enlistment.EnlistmentRoot`)
            // junctionsRoot is created here (outside of the GVFS enlistment root) to ensure that git hooks and GVFS commands will not find a .gvfs folder
            // walking up the tree from their current (non-normalized) path.
            this.junctionsRoot = Path.Combine(Properties.Settings.Default.EnlistmentRoot, JunctionAndSubstTestsName, Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(this.junctionsRoot);
        }

        [TearDown]
        public void TearDownJunctionRoot()
        {
            DirectoryInfo junctionsRootInfo = new DirectoryInfo(this.junctionsRoot);
            if (junctionsRootInfo.Exists)
            {
                foreach (DirectoryInfo junction in junctionsRootInfo.GetDirectories())
                {
                    junction.Delete();
                }

                junctionsRootInfo.Delete();
            }
        }

        [TestCase]
        public void GVFSStatusWorksFromSubstDrive()
        {
            this.CreateSubstDrive(this.Enlistment.EnlistmentRoot);
            this.RepoStatusShouldBeMounted(workingDirectory: SubstDrivePath);

            this.CreateSubstDrive(this.Enlistment.RepoRoot);
            this.RepoStatusShouldBeMounted(workingDirectory: SubstDrivePath);

            string subFolderPath = this.Enlistment.GetVirtualPathTo("GVFS");
            this.CreateSubstDrive(subFolderPath);
            this.RepoStatusShouldBeMounted(workingDirectory: SubstDrivePath);

            this.RepoStatusShouldBeMounted(workingDirectory: string.Empty, enlistmentPath: SubstDrive);
        }

        [TestCase]
        public void GVFSStatusWorksFromJunction()
        {
            string enlistmentRootjunctionLink = Path.Combine(this.junctionsRoot, $"{nameof(this.GVFSStatusWorksFromJunction)}_ToEnlistmentRoot");
            this.CreateJunction(enlistmentRootjunctionLink, this.Enlistment.EnlistmentRoot);
            this.RepoStatusShouldBeMounted(workingDirectory: enlistmentRootjunctionLink);

            string junctionLink = Path.Combine(this.junctionsRoot, $"{nameof(this.GVFSStatusWorksFromJunction)}_ToRepoRoot");
            this.CreateJunction(junctionLink, this.Enlistment.RepoRoot);
            this.RepoStatusShouldBeMounted(workingDirectory: junctionLink);

            junctionLink = Path.Combine(this.junctionsRoot, $"{nameof(this.GVFSStatusWorksFromJunction)}_ToSubFolder");
            string subFolderPath = this.Enlistment.GetVirtualPathTo("GVFS");
            this.CreateJunction(junctionLink, subFolderPath);
            this.RepoStatusShouldBeMounted(workingDirectory: junctionLink);

            this.RepoStatusShouldBeMounted(workingDirectory: string.Empty, enlistmentPath: enlistmentRootjunctionLink);
        }

        [TestCase]
        public void GVFSMountWorksFromSubstDrive()
        {
            this.CreateSubstDrive(this.Enlistment.EnlistmentRoot);
            this.Enlistment.UnmountGVFS();
            this.MountGVFS(workingDirectory: SubstDrivePath);

            this.CreateSubstDrive(this.Enlistment.RepoRoot);
            this.Enlistment.UnmountGVFS();
            this.MountGVFS(workingDirectory: SubstDrivePath);

            string subFolderPath = this.Enlistment.GetVirtualPathTo("GVFS");
            subFolderPath.ShouldBeADirectory(this.fileSystem);
            this.CreateSubstDrive(subFolderPath);
            this.Enlistment.UnmountGVFS();
            this.MountGVFS(workingDirectory: SubstDrivePath);

            this.Enlistment.UnmountGVFS();
            this.MountGVFS(workingDirectory: null, enlistmentPath: SubstDrive);
        }

        [TestCase]
        public void GVFSMountWorksFromJunction()
        {
            string enlistmentRootjunctionLink = Path.Combine(this.junctionsRoot, $"{nameof(this.GVFSMountWorksFromJunction)}_ToEnlistmentRoot");
            this.CreateJunction(enlistmentRootjunctionLink, this.Enlistment.EnlistmentRoot);
            this.Enlistment.UnmountGVFS();
            this.MountGVFS(workingDirectory: enlistmentRootjunctionLink);

            string junctionLink = Path.Combine(this.junctionsRoot, $"{nameof(this.GVFSMountWorksFromJunction)}_ToRepoRoot");
            this.CreateJunction(junctionLink, this.Enlistment.RepoRoot);
            this.Enlistment.UnmountGVFS();
            this.MountGVFS(workingDirectory: junctionLink);

            string subFolderPath = this.Enlistment.GetVirtualPathTo("GVFS");
            subFolderPath.ShouldBeADirectory(this.fileSystem);
            junctionLink = Path.Combine(this.junctionsRoot, $"{nameof(this.GVFSMountWorksFromJunction)}_ToSubFolder");
            this.CreateJunction(junctionLink, subFolderPath);
            this.Enlistment.UnmountGVFS();
            this.MountGVFS(workingDirectory: junctionLink);

            this.Enlistment.UnmountGVFS();
            this.MountGVFS(workingDirectory: null, enlistmentPath: enlistmentRootjunctionLink);
        }

        [TestCase]
        public void GitCommandInSubstToSubfolderWaitsWhileAnotherIsRunning()
        {
            this.CreateSubstDrive(this.Enlistment.EnlistmentRoot);
            this.GitCommandWaitsForLock(Path.Combine(SubstDrivePath, "src"));

            this.CreateSubstDrive(this.Enlistment.RepoRoot);
            this.GitCommandWaitsForLock(SubstDrivePath);
        }

        [TestCase]
        public void GitCommandInJunctionToSubfolderWaitsWhileAnotherIsRunning()
        {
            string junctionLink = Path.Combine(this.junctionsRoot, $"{nameof(this.GitCommandInJunctionToSubfolderWaitsWhileAnotherIsRunning)}_ToEnlistmentRoot");
            this.CreateJunction(junctionLink, this.Enlistment.EnlistmentRoot);
            this.GitCommandWaitsForLock(Path.Combine(junctionLink, "src"));

            junctionLink = Path.Combine(this.junctionsRoot, $"{nameof(this.GitCommandInJunctionToSubfolderWaitsWhileAnotherIsRunning)}_ToRepoRoot");
            this.CreateJunction(junctionLink, this.Enlistment.RepoRoot);
            this.GitCommandWaitsForLock(junctionLink);
        }

        private void GitCommandWaitsForLock(string gitWorkingDirectory)
        {
            ManualResetEventSlim resetEvent = GitHelpers.AcquireGVFSLock(this.Enlistment, out _, resetTimeout: 3000);
            ProcessResult statusWait = GitHelpers.InvokeGitAgainstGVFSRepo(gitWorkingDirectory, "status", removeWaitingMessages: false);
            statusWait.Errors.ShouldContain(ExpectedStatusWaitingText);
            resetEvent.Set();
            this.Enlistment.WaitForBackgroundOperations();
        }

        private void CreateSubstDrive(string path)
        {
            this.RemoveSubstDrive();
            this.fileSystem.DirectoryExists(path).ShouldBeTrue($"{path} needs to exist to be able to map it to a drive letter");
            string substResult = this.RunSubst($"{SubstDrive} {path}");
            this.fileSystem.DirectoryExists(SubstDrivePath).ShouldBeTrue($"{SubstDrivePath} should exist after creating mapping with subst. subst result: {substResult}");
        }

        private void RemoveSubstDrive()
        {
            string substResult = this.RunSubst($"{SubstDrive} /D");
            this.fileSystem.DirectoryExists(SubstDrivePath).ShouldBeFalse($"{SubstDrivePath} should not exist after being removed with subst /D. subst result: {substResult}");
        }

        private string RunSubst(string substArguments)
        {
            string cmdArguments = $"/C subst {substArguments}";
            ProcessResult result = ProcessHelper.Run("CMD.exe", cmdArguments);
            return !string.IsNullOrEmpty(result.Output) ? result.Output : result.Errors;
        }

        private void CreateJunction(string junctionLink, string junctionTarget)
        {
            junctionLink.ShouldNotExistOnDisk(this.fileSystem);
            junctionTarget.ShouldBeADirectory(this.fileSystem);
            ProcessHelper.Run("CMD.exe", "/C mklink /J " + junctionLink + " " + junctionTarget);
            junctionLink.ShouldBeADirectory(this.fileSystem);
        }

        private void RepoStatusShouldBeMounted(string workingDirectory, string enlistmentPath = null)
        {
            ProcessStartInfo startInfo = new ProcessStartInfo(GVFSTestConfig.PathToGVFS);
            startInfo.Arguments = "status" + (enlistmentPath != null ? $" {enlistmentPath}" : string.Empty);
            startInfo.UseShellExecute = false;
            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;
            startInfo.CreateNoWindow = true;
            startInfo.WorkingDirectory = workingDirectory;

            ProcessResult result = ProcessHelper.Run(startInfo);
            result.ExitCode.ShouldEqual(0, result.Errors);
            result.Output.ShouldContain("Mount status: Ready");
        }

        private void MountGVFS(string workingDirectory, string enlistmentPath = null)
        {
            string mountCommand;
            if (enlistmentPath != null)
            {
                mountCommand = $"mount \"{enlistmentPath}\" {TestConstants.InternalUseOnlyFlag} {GVFSHelpers.GetInternalParameter()}";
            }
            else
            {
                mountCommand = $"mount {TestConstants.InternalUseOnlyFlag} {GVFSHelpers.GetInternalParameter()}";
            }

            ProcessStartInfo startInfo = new ProcessStartInfo(GVFSTestConfig.PathToGVFS);
            startInfo.Arguments = mountCommand;
            startInfo.UseShellExecute = false;
            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;
            startInfo.CreateNoWindow = true;
            startInfo.WorkingDirectory = workingDirectory;

            ProcessResult result = ProcessHelper.Run(startInfo);
            result.ExitCode.ShouldEqual(0, result.Errors);

            this.RepoStatusShouldBeMounted(workingDirectory, enlistmentPath);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Windows/Tests/ServiceTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Tests.EnlistmentPerFixture;
using GVFS.FunctionalTests.Tools;
using GVFS.FunctionalTests.Windows.Tools;
using GVFS.Tests.Should;
using Microsoft.Win32;
using NUnit.Framework;
using System;
using System.Runtime.InteropServices;
using System.ServiceProcess;

namespace GVFS.FunctionalTests.Windows.Tests
{
    [TestFixture]
    [NonParallelizable]
    [Category(Categories.ExtraCoverage)]
    public class ServiceTests : TestsWithEnlistmentPerFixture
    {
        private const string NativeLibPath = @"C:\Program Files\VFS for Git\ProjectedFSLib.dll";
        private const string PrjFltAutoLoggerKey = "SYSTEM\\CurrentControlSet\\Control\\WMI\\Autologger\\Microsoft-Windows-ProjFS-Filter-Log";
        private const string PrjFltAutoLoggerStartValue = "Start";

        private FileSystemRunner fileSystem;

        public ServiceTests()
        {
            this.fileSystem = new SystemIORunner();
        }

        [TestCase]
        public void MountAsksServiceToEnsurePrjFltServiceIsHealthy()
        {
            this.Enlistment.UnmountGVFS();
            StopPrjFlt();

            // Disable the ProjFS autologger
            RegistryHelper.GetValueFromRegistry(RegistryHive.LocalMachine, PrjFltAutoLoggerKey, PrjFltAutoLoggerStartValue).ShouldNotBeNull();
            RegistryHelper.TrySetDWordInRegistry(RegistryHive.LocalMachine, PrjFltAutoLoggerKey, PrjFltAutoLoggerStartValue, 0).ShouldBeTrue();

            this.Enlistment.MountGVFS();
            IsPrjFltRunning().ShouldBeTrue();

            // The service should have re-enabled the autologger
            Convert.ToInt32(RegistryHelper.GetValueFromRegistry(RegistryHive.LocalMachine, PrjFltAutoLoggerKey, PrjFltAutoLoggerStartValue)).ShouldEqual(1);
        }

        [TestCase]
        public void ServiceStartsPrjFltService()
        {
            this.Enlistment.UnmountGVFS();
            StopPrjFlt();
            GVFSServiceProcess.StopService();
            GVFSServiceProcess.StartService();

            ServiceController controller = new ServiceController("prjflt");
            controller.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10));
            controller.Status.ShouldEqual(ServiceControllerStatus.Running);

            this.Enlistment.MountGVFS();
        }

        private static bool IsPrjFltRunning()
        {
            ServiceController controller = new ServiceController("prjflt");
            return controller.Status.Equals(ServiceControllerStatus.Running);
        }

        private static void StopPrjFlt()
        {
            IsPrjFltRunning().ShouldBeTrue();

            ServiceController controller = new ServiceController("prjflt");
            controller.Stop();
            controller.WaitForStatus(ServiceControllerStatus.Stopped);
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
        private static extern bool GetVersionEx([In, Out] ref OSVersionInfo versionInfo);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        private struct OSVersionInfo
        {
            public uint OSVersionInfoSize;
            public uint MajorVersion;
            public uint MinorVersion;
            public uint BuildNumber;
            public uint PlatformId;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
            public string CSDVersion;
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Windows/Tests/SharedCacheUpgradeTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tests.MultiEnlistmentTests;
using GVFS.FunctionalTests.Tools;
using GVFS.FunctionalTests.Windows.Tests;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.IO;

namespace GVFS.FunctionalTests.Windows.Windows.Tests
{
    [TestFixture]
    [Category(Categories.ExtraCoverage)]
    public class SharedCacheUpgradeTests : TestsWithMultiEnlistment
    {
        private string localCachePath;
        private string localCacheParentPath;

        private FileSystemRunner fileSystem;

        public SharedCacheUpgradeTests()
        {
            this.fileSystem = new SystemIORunner();
        }

        [SetUp]
        public void SetCacheLocation()
        {
            this.localCacheParentPath = Path.Combine(Properties.Settings.Default.EnlistmentRoot, "..", Guid.NewGuid().ToString("N"));
            this.localCachePath = Path.Combine(this.localCacheParentPath, ".customGVFSCache");
        }

        private GVFSFunctionalTestEnlistment CloneAndMountEnlistment(string branch = null)
        {
            return this.CreateNewEnlistment(this.localCachePath, branch);
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Windows/Tests/WindowsDiskLayoutUpgradeTests.cs
================================================
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tests.EnlistmentPerTestCase;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace GVFS.FunctionalTests.Windows.Tests
{
    [TestFixture]
    [Category(Categories.ExtraCoverage)]
    public class WindowsDiskLayoutUpgradeTests : DiskLayoutUpgradeTests
    {
        public const int CurrentDiskLayoutMajorVersion = 19;
        public const int CurrentDiskLayoutMinorVersion = 0;

        public const string BlobSizesCacheName = "blobSizes";
        public const string BlobSizesDBFileName = "BlobSizes.sql";

        private const string DatabasesFolderName = "databases";

        public override int GetCurrentDiskLayoutMajorVersion() => CurrentDiskLayoutMajorVersion;
        public override int GetCurrentDiskLayoutMinorVersion() => CurrentDiskLayoutMinorVersion;

        [SetUp]
        public override void CreateEnlistment()
        {
            base.CreateEnlistment();

            // Since there isn't a sparse-checkout file that is used anymore one needs to be added
            // in order to test the old upgrades that might have needed it
            string sparseCheckoutPath = Path.Combine(this.Enlistment.RepoRoot, TestConstants.DotGit.Info.SparseCheckoutPath);
            this.fileSystem.WriteAllText(sparseCheckoutPath, "/.gitattributes\r\n");
        }

        [TestCase]
        public void MountUpgradesFromMinimumSupportedVersion()
        {
            this.Enlistment.UnmountGVFS();

            GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, "14", "0");

            this.Enlistment.MountGVFS();

            this.ValidatePersistedVersionMatchesCurrentVersion();
        }

        [TestCase]
        public void MountWritesFolderPlaceholdersToPlaceholderDatabase()
        {
            this.PerformIOBeforePlaceholderDatabaseUpgradeTest();

            this.Enlistment.UnmountGVFS();

            this.fileSystem.DeleteFile(Path.Combine(this.Enlistment.DotGVFSRoot, TestConstants.Databases.VFSForGit));
            this.WriteOldPlaceholderListDatabase();

            // Get the existing folder placeholder data
            string placeholderDatabasePath = Path.Combine(this.Enlistment.DotGVFSRoot, GVFSHelpers.PlaceholderListFile);
            string[] lines = this.GetPlaceholderDatabaseLinesBeforeUpgrade(placeholderDatabasePath);

            // Placeholder database file should only have file placeholders
            this.fileSystem.WriteAllText(
                placeholderDatabasePath,
                string.Join(Environment.NewLine, lines.Where(x => !x.EndsWith(TestConstants.PartialFolderPlaceholderDatabaseValue))) + Environment.NewLine);

            GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, "15", "0");

            this.Enlistment.MountGVFS();
            this.Enlistment.UnmountGVFS();

            // Validate the folder placeholders are in the placeholder database now
            this.GetPlaceholderDatabaseLinesAfterUpgradeFrom12_1(Path.Combine(this.Enlistment.DotGVFSRoot, TestConstants.Databases.VFSForGit));

            this.ValidatePersistedVersionMatchesCurrentVersion();
        }

        [TestCase]
        public void MountUpdatesAllZeroShaFolderPlaceholderEntriesToPartialFolderSpecialValue()
        {
            this.PerformIOBeforePlaceholderDatabaseUpgradeTest();

            this.Enlistment.UnmountGVFS();
            this.WriteOldPlaceholderListDatabase();

            // Get the existing folder placeholder data
            string placeholderDatabasePath = Path.Combine(this.Enlistment.DotGVFSRoot, GVFSHelpers.PlaceholderListFile);
            string[] lines = this.GetPlaceholderDatabaseLinesBeforeUpgrade(placeholderDatabasePath);

            // Update the placeholder file so that folders have an all zero SHA
            this.fileSystem.WriteAllText(
                placeholderDatabasePath,
                string.Join(
                    Environment.NewLine,
                    lines.Select(x => x.Replace(TestConstants.PartialFolderPlaceholderDatabaseValue, TestConstants.AllZeroSha))) + Environment.NewLine);

            GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, "16", "0");

            this.Enlistment.MountGVFS();
            this.Enlistment.UnmountGVFS();

            // Validate the folder placeholders in the database have PartialFolderPlaceholderDatabaseValue values
            this.GetPlaceholderDatabaseLinesAfterUpgradeFrom16(Path.Combine(this.Enlistment.DotGVFSRoot, TestConstants.Databases.VFSForGit));

            this.ValidatePersistedVersionMatchesCurrentVersion();
        }

        [TestCase]
        public void MountCreatesModifiedPathsDatabase()
        {
            this.Enlistment.UnmountGVFS();
            GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, "15", "0");

            // Delete the existing modified paths database to make sure mount creates it.
            string modifiedPathsDatabasePath = Path.Combine(this.Enlistment.DotGVFSRoot, TestConstants.Databases.ModifiedPaths);
            this.fileSystem.DeleteFile(modifiedPathsDatabasePath);

            // Overwrite the sparse-checkout with entries to test
            string sparseCheckoutPath = Path.Combine(this.Enlistment.RepoRoot, TestConstants.DotGit.Info.SparseCheckoutPath);
            string sparseCheckoutContent = @"/.gitattributes
/developer/me/
/developer/me/JLANGE9._prerazzle
/developer/me/StateSwitch.Save
/tools/x86/remote.exe
/tools/x86/runelevated.exe
/tools/amd64/remote.exe
/tools/amd64/runelevated.exe
/tools/perllib/MS/TraceLogging.dll
/tools/managed/v2.0/midldd.CheckedInExe
/tools/managed/v4.0/sdapi.dll
/tools/managed/v2.0/midlpars.dll
/tools/managed/v2.0/RPCDataSupport.dll
";
            this.fileSystem.WriteAllText(sparseCheckoutPath, sparseCheckoutContent);

            // Overwrite the always_exclude file with entries to test
            string alwaysExcludePath = Path.Combine(this.Enlistment.RepoRoot, TestConstants.DotGit.Info.AlwaysExcludePath);
            string alwaysExcludeContent = @"*
!/developer
!/developer/*
!/developer/me
!/developer/me/*
!/tools
!/tools/x86
!/tools/x86/*
!/tools/amd64
!/tools/amd64/*
!/tools/perllib/
!/tools/perllib/MS/
!/tools/perllib/MS/Somefile.txt
!/tools/managed/
!/tools/managed/v2.0/
!/tools/managed/v2.0/MidlStaticAnalysis.dll
!/tools/managed/v2.0/RPCDataSupport.dll
";
            this.fileSystem.WriteAllText(alwaysExcludePath, alwaysExcludeContent);

            this.Enlistment.MountGVFS();
            this.Enlistment.UnmountGVFS();

            string[] expectedModifiedPaths =
                {
                    "A .gitattributes",
                    "A developer/me/",
                    "A tools/x86/remote.exe",
                    "A tools/x86/runelevated.exe",
                    "A tools/amd64/remote.exe",
                    "A tools/amd64/runelevated.exe",
                    "A tools/perllib/MS/TraceLogging.dll",
                    "A tools/managed/v2.0/midldd.CheckedInExe",
                    "A tools/managed/v4.0/sdapi.dll",
                    "A tools/managed/v2.0/midlpars.dll",
                    "A tools/managed/v2.0/RPCDataSupport.dll",
                    "A tools/managed/v2.0/MidlStaticAnalysis.dll",
                    "A tools/perllib/MS/Somefile.txt",
                };

            modifiedPathsDatabasePath.ShouldBeAFile(this.fileSystem);
            this.fileSystem.ReadAllText(modifiedPathsDatabasePath)
                .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                .OrderBy(x => x)
                .ShouldMatchInOrder(expectedModifiedPaths.OrderBy(x => x));

            this.ValidatePersistedVersionMatchesCurrentVersion();
        }

        private void PlaceholderDatabaseShouldIncludeCommonLinesForUpgradeTestIO(string[] placeholderLines)
        {
            placeholderLines.ShouldContain(x => x.Contains("A Readme.md"));
            placeholderLines.ShouldContain(x => x.Contains("A Scripts\\RunUnitTests.bat"));
            placeholderLines.ShouldContain(x => x.Contains("A GVFS\\GVFS.Common\\Git\\GitRefs.cs"));
            placeholderLines.ShouldContain(x => x.Contains("A .gitignore"));
            placeholderLines.ShouldContain(x => x == "A Scripts\0" + TestConstants.PartialFolderPlaceholderDatabaseValue);
            placeholderLines.ShouldContain(x => x == "A GVFS\0" + TestConstants.PartialFolderPlaceholderDatabaseValue);
            placeholderLines.ShouldContain(x => x == "A GVFS\\GVFS.Common\0" + TestConstants.PartialFolderPlaceholderDatabaseValue);
            placeholderLines.ShouldContain(x => x == "A GVFS\\GVFS.Common\\Git\0" + TestConstants.PartialFolderPlaceholderDatabaseValue);
            placeholderLines.ShouldContain(x => x == "A GVFS\\GVFS.Tests\0" + TestConstants.PartialFolderPlaceholderDatabaseValue);
        }

        private string[] GetPlaceholderDatabaseLinesBeforeUpgrade(string placeholderDatabasePath)
        {
            placeholderDatabasePath.ShouldBeAFile(this.fileSystem);
            string[] lines = this.fileSystem.ReadAllText(placeholderDatabasePath).Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
            lines.Length.ShouldEqual(12);
            this.PlaceholderDatabaseShouldIncludeCommonLinesForUpgradeTestIO(lines);
            lines.ShouldContain(x => x.Contains("A GVFS\\GVFS.Tests\\Properties\\AssemblyInfo.cs"));
            lines.ShouldContain(x => x == "D GVFS\\GVFS.Tests\\Properties\\AssemblyInfo.cs");
            lines.ShouldContain(x => x == "A GVFS\\GVFS.Tests\\Properties\0" + TestConstants.PartialFolderPlaceholderDatabaseValue);
            return lines;
        }

        private string[] GetPlaceholderDatabaseLinesAfterUpgradeFrom12_1(string placeholderDatabasePath)
        {
            placeholderDatabasePath.ShouldBeAFile(this.fileSystem);
            string[] lines = GVFSHelpers.GetAllSQLitePlaceholdersAsString(placeholderDatabasePath).Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
            lines.Length.ShouldEqual(9);
            this.PlaceholderDatabaseShouldIncludeCommonLines(lines);
            return lines;
        }

        private string[] GetPlaceholderDatabaseLinesAfterUpgradeFrom16(string placeholderDatabasePath)
        {
            placeholderDatabasePath.ShouldBeAFile(this.fileSystem);
            string[] lines = GVFSHelpers.GetAllSQLitePlaceholdersAsString(placeholderDatabasePath).Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
            lines.Length.ShouldEqual(10);
            this.PlaceholderDatabaseShouldIncludeCommonLines(lines);
            lines.ShouldContain(x => x == this.PartialFolderPlaceholderString("GVFS", "GVFS.Tests", "Properties"));
            return lines;
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Windows/Tests/WindowsFileSystemTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tests.EnlistmentPerFixture;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace GVFS.FunctionalTests.Windows.Windows.Tests
{
    [TestFixture]
    public class WindowsFileSystemTests : TestsWithEnlistmentPerFixture
    {
        private enum CreationDisposition
        {
            CreateNew = 1,        // CREATE_NEW
            CreateAlways = 2,     // CREATE_ALWAYS
            OpenExisting = 3,     // OPEN_EXISTING
            OpenAlways = 4,       // OPEN_ALWAYS
            TruncateExisting = 5  // TRUNCATE_EXISTING
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Runners))]
        public void CaseOnlyRenameEmptyVirtualNTFSFolder(FileSystemRunner fileSystem, string parentFolder)
        {
            string testFolderName = Path.Combine(parentFolder, "test_folder");
            string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName);
            testFolderVirtualPath.ShouldNotExistOnDisk(fileSystem);

            fileSystem.CreateDirectory(testFolderVirtualPath);
            testFolderVirtualPath.ShouldBeADirectory(fileSystem);

            string newFolderName = Path.Combine(parentFolder, "test_FOLDER");
            string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(newFolderName);

            // Use NativeMethods.MoveFile instead of the runner because it supports case only rename
            NativeMethods.MoveFile(testFolderVirtualPath, newFolderVirtualPath);

            newFolderVirtualPath.ShouldBeADirectory(fileSystem).WithCaseMatchingName(Path.GetFileName(newFolderName));

            fileSystem.DeleteDirectory(newFolderVirtualPath);
            newFolderVirtualPath.ShouldNotExistOnDisk(fileSystem);
        }

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void CaseOnlyRenameToAllCapsEmptyVirtualNTFSFolder(FileSystemRunner fileSystem)
        {
            string testFolderName = Path.Combine("test_folder");
            string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName);
            testFolderVirtualPath.ShouldNotExistOnDisk(fileSystem);

            fileSystem.CreateDirectory(testFolderVirtualPath);
            testFolderVirtualPath.ShouldBeADirectory(fileSystem);

            string newFolderName = Path.Combine("TEST_FOLDER");
            string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(newFolderName);

            // Use NativeMethods.MoveFile instead of the runner because it supports case only rename
            NativeMethods.MoveFile(testFolderVirtualPath, newFolderVirtualPath);

            newFolderVirtualPath.ShouldBeADirectory(fileSystem).WithCaseMatchingName(Path.GetFileName(newFolderName));

            fileSystem.DeleteDirectory(newFolderVirtualPath);
            newFolderVirtualPath.ShouldNotExistOnDisk(fileSystem);
        }

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void CaseOnlyRenameTopOfVirtualNTFSFolderTree(FileSystemRunner fileSystem)
        {
            string testFolderParent = "test_folder_parent";
            string testFolderChild = "test_folder_child";
            string testFolderGrandChild = "test_folder_grandchild";
            string testFile = "test.txt";
            this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem);

            // Create the folder tree
            fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent));
            this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem);

            string realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild);
            fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath));
            this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem);

            string realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild);
            fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath));
            this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem);

            string relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile);
            string testFileContents = "This is the contents of a test file";
            fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(relativeTestFilePath), testFileContents);
            this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents);

            string newFolderParentName = "test_FOLDER_PARENT";

            // Use NativeMethods.MoveFile instead of the runner because it supports case only rename
            NativeMethods.MoveFile(this.Enlistment.GetVirtualPathTo(testFolderParent), this.Enlistment.GetVirtualPathTo(newFolderParentName));

            this.Enlistment.GetVirtualPathTo(newFolderParentName).ShouldBeADirectory(fileSystem).WithCaseMatchingName(newFolderParentName);
            this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem);
            this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem);
            this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents);

            // Cleanup
            fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent));

            this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem);
            this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldNotExistOnDisk(fileSystem);
            this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldNotExistOnDisk(fileSystem);
            this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldNotExistOnDisk(fileSystem);
        }

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void CaseOnlyRenameFullDotGitFolder(FileSystemRunner fileSystem)
        {
            string testFolderName = ".git\\test_folder";
            string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName);
            testFolderVirtualPath.ShouldNotExistOnDisk(fileSystem);

            fileSystem.CreateDirectory(testFolderVirtualPath);
            testFolderVirtualPath.ShouldBeADirectory(fileSystem);

            string newFolderName = "test_FOLDER";
            string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(".git", newFolderName));

            // Use NativeMethods.MoveFile instead of the runner because it supports case only rename
            NativeMethods.MoveFile(testFolderVirtualPath, newFolderVirtualPath);

            newFolderVirtualPath.ShouldBeADirectory(fileSystem).WithCaseMatchingName(newFolderName);

            fileSystem.DeleteDirectory(newFolderVirtualPath);
            newFolderVirtualPath.ShouldNotExistOnDisk(fileSystem);
        }

        [TestCaseSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))]
        public void CaseOnlyRenameTopOfDotGitFullFolderTree(FileSystemRunner fileSystem)
        {
            string testFolderParent = ".git\\test_folder_parent";
            string testFolderChild = "test_folder_child";
            string testFolderGrandChild = "test_folder_grandchild";
            string testFile = "test.txt";
            this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem);

            // Create the folder tree
            fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent));
            this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem);

            string realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild);
            fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath));
            this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem);

            string realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild);
            fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath));
            this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem);

            string relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile);
            string testFileContents = "This is the contents of a test file";
            fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(relativeTestFilePath), testFileContents);
            this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents);

            string newFolderParentName = "test_FOLDER_PARENT";

            // Use NativeMethods.MoveFile instead of the runner because it supports case only rename
            NativeMethods.MoveFile(this.Enlistment.GetVirtualPathTo(testFolderParent), this.Enlistment.GetVirtualPathTo(Path.Combine(".git", newFolderParentName)));

            this.Enlistment.GetVirtualPathTo(Path.Combine(".git", newFolderParentName)).ShouldBeADirectory(fileSystem).WithCaseMatchingName(newFolderParentName);
            this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem);
            this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem);
            this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents);

            // Cleanup
            fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(Path.Combine(".git", newFolderParentName)));

            this.Enlistment.GetVirtualPathTo(Path.Combine(".git", newFolderParentName)).ShouldNotExistOnDisk(fileSystem);
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))]
        public void StreamAccessReadFromMemoryMappedVirtualNTFSFile(string parentFolder)
        {
            // Use SystemIORunner as the text we are writing is too long to pass to the command line
            FileSystemRunner fileSystem = new SystemIORunner();

            string filename = Path.Combine(parentFolder, "StreamAccessReadFromMemoryMappedVirtualNTFSFile");
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename);
            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            StringBuilder contentsBuilder = new StringBuilder();
            while (contentsBuilder.Length < 4096 * 2)
            {
                contentsBuilder.Append(Guid.NewGuid().ToString());
            }

            string contents = contentsBuilder.ToString();

            fileSystem.WriteAllText(fileVirtualPath, contents);
            fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents);

            using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath))
            {
                int offset = 0;
                int size = contents.Length;
                using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset, size))
                {
                    streamAccessor.CanRead.ShouldEqual(true);

                    for (int i = 0; i < size; ++i)
                    {
                        streamAccessor.ReadByte().ShouldEqual(contents[i]);
                    }
                }
            }

            fileSystem.DeleteFile(fileVirtualPath);
            FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder);
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))]
        public void RandomAccessReadFromMemoryMappedVirtualNTFSFile(string parentFolder)
        {
            // Use SystemIORunner as the text we are writing is too long to pass to the command line
            FileSystemRunner fileSystem = new SystemIORunner();

            string filename = Path.Combine(parentFolder, "RandomAccessReadFromMemoryMappedVirtualNTFSFile");
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename);
            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            StringBuilder contentsBuilder = new StringBuilder();
            while (contentsBuilder.Length < 4096 * 2)
            {
                contentsBuilder.Append(Guid.NewGuid().ToString());
            }

            string contents = contentsBuilder.ToString();

            fileSystem.WriteAllText(fileVirtualPath, contents);
            fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents);

            using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath))
            {
                int offset = 0;
                int size = contents.Length;
                using (MemoryMappedViewAccessor randAccessor = mmf.CreateViewAccessor(offset, size))
                {
                    randAccessor.CanRead.ShouldEqual(true);

                    for (int i = 0; i < size; ++i)
                    {
                        ((char)randAccessor.ReadByte(i)).ShouldEqual(contents[i]);
                    }

                    for (int i = size - 1; i >= 0; --i)
                    {
                        ((char)randAccessor.ReadByte(i)).ShouldEqual(contents[i]);
                    }
                }
            }

            fileSystem.DeleteFile(fileVirtualPath);
            FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder);
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))]
        public void StreamAccessReadWriteFromMemoryMappedVirtualNTFSFile(string parentFolder)
        {
            // Use SystemIORunner as the text we are writing is too long to pass to the command line
            FileSystemRunner fileSystem = new SystemIORunner();

            string filename = Path.Combine(parentFolder, "StreamAccessReadWriteFromMemoryMappedVirtualNTFSFile");
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename);
            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            StringBuilder contentsBuilder = new StringBuilder();
            while (contentsBuilder.Length < 4096 * 2)
            {
                contentsBuilder.Append(Guid.NewGuid().ToString());
            }

            string contents = contentsBuilder.ToString();

            fileSystem.WriteAllText(fileVirtualPath, contents);
            fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents);

            using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath))
            {
                int offset = 64;
                int size = contents.Length;
                string newContent = "**NEWCONTENT**";

                using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset, size - offset))
                {
                    streamAccessor.CanRead.ShouldEqual(true);
                    streamAccessor.CanWrite.ShouldEqual(true);

                    for (int i = offset; i < size - offset; ++i)
                    {
                        streamAccessor.ReadByte().ShouldEqual(contents[i]);
                    }

                    // Reset to the start of the stream (which will place the streamAccessor at offset in the memory file)
                    streamAccessor.Seek(0, SeekOrigin.Begin);
                    byte[] newContentBuffer = Encoding.ASCII.GetBytes(newContent);

                    streamAccessor.Write(newContentBuffer, 0, newContent.Length);

                    for (int i = 0; i < newContent.Length; ++i)
                    {
                        contentsBuilder[offset + i] = newContent[i];
                    }

                    contents = contentsBuilder.ToString();
                }

                // Verify the file has the new contents inserted into it
                using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset: 0, size: size))
                {
                    for (int i = 0; i < size; ++i)
                    {
                        streamAccessor.ReadByte().ShouldEqual(contents[i]);
                    }
                }
            }

            // Confirm the new contents was written to disk
            fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents);

            fileSystem.DeleteFile(fileVirtualPath);
            FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder);
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))]
        public void RandomAccessReadWriteFromMemoryMappedVirtualNTFSFile(string parentFolder)
        {
            // Use SystemIORunner as the text we are writing is too long to pass to the command line
            FileSystemRunner fileSystem = new SystemIORunner();

            string filename = Path.Combine(parentFolder, "RandomAccessReadWriteFromMemoryMappedVirtualNTFSFile");
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename);
            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            StringBuilder contentsBuilder = new StringBuilder();
            while (contentsBuilder.Length < 4096 * 2)
            {
                contentsBuilder.Append(Guid.NewGuid().ToString());
            }

            string contents = contentsBuilder.ToString();

            fileSystem.WriteAllText(fileVirtualPath, contents);
            fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents);

            using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath))
            {
                int offset = 64;
                int size = contents.Length;
                string newContent = "**NEWCONTENT**";

                using (MemoryMappedViewAccessor randomAccessor = mmf.CreateViewAccessor(offset, size - offset))
                {
                    randomAccessor.CanRead.ShouldEqual(true);
                    randomAccessor.CanWrite.ShouldEqual(true);

                    for (int i = 0; i < size - offset; ++i)
                    {
                        ((char)randomAccessor.ReadByte(i)).ShouldEqual(contents[i + offset]);
                    }

                    for (int i = 0; i < newContent.Length; ++i)
                    {
                        // Convert to byte before writing rather than writing as char, because char version will write a 16-bit
                        // unicode char
                        randomAccessor.Write(i, Convert.ToByte(newContent[i]));
                        ((char)randomAccessor.ReadByte(i)).ShouldEqual(newContent[i]);
                    }

                    for (int i = 0; i < newContent.Length; ++i)
                    {
                        contentsBuilder[offset + i] = newContent[i];
                    }

                    contents = contentsBuilder.ToString();
                }

                // Verify the file has the new contents inserted into it
                using (MemoryMappedViewAccessor randomAccessor = mmf.CreateViewAccessor(offset: 0, size: size))
                {
                    for (int i = 0; i < size; ++i)
                    {
                        ((char)randomAccessor.ReadByte(i)).ShouldEqual(contents[i]);
                    }
                }
            }

            // Confirm the new contents was written to disk
            fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents);

            fileSystem.DeleteFile(fileVirtualPath);
            FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder);
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))]
        public void StreamAccessToExistingMemoryMappedFile(string parentFolder)
        {
            // Use SystemIORunner as the text we are writing is too long to pass to the command line
            FileSystemRunner fileSystem = new SystemIORunner();

            string filename = Path.Combine(parentFolder, "StreamAccessToExistingMemoryMappedFile");
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename);
            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            StringBuilder contentsBuilder = new StringBuilder();
            while (contentsBuilder.Length < 4096 * 2)
            {
                contentsBuilder.Append(Guid.NewGuid().ToString());
            }

            string contents = contentsBuilder.ToString();
            int size = contents.Length;

            fileSystem.WriteAllText(fileVirtualPath, contents);
            fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents);

            string memoryMapFileName = "StreamAccessFile";
            using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath, FileMode.Open, memoryMapFileName))
            {
                Thread[] threads = new Thread[4];
                bool keepRunning = true;
                for (int i = 0; i < threads.Length; ++i)
                {
                    int myIndex = i;
                    threads[i] = new Thread(() =>
                    {
                        // Create random seeks (seeded for repeatability)
                        Random randNum = new Random(myIndex);

                        using (MemoryMappedFile threadFile = MemoryMappedFile.OpenExisting(memoryMapFileName))
                        {
                            while (keepRunning)
                            {
                                // Pick an offset somewhere in the first half of the file
                                int offset = randNum.Next(size / 2);

                                using (MemoryMappedViewStream streamAccessor = threadFile.CreateViewStream(offset, size - offset))
                                {
                                    for (int j = 0; j < size - offset; ++j)
                                    {
                                        streamAccessor.ReadByte().ShouldEqual(contents[j + offset]);
                                    }
                                }
                            }
                        }
                    });

                    threads[i].Start();
                }

                Thread.Sleep(500);
                keepRunning = false;

                for (int i = 0; i < threads.Length; ++i)
                {
                    threads[i].Join();
                }
            }

            fileSystem.DeleteFile(fileVirtualPath);
            FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder);
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))]
        public void RandomAccessToExistingMemoryMappedFile(string parentFolder)
        {
            // Use SystemIORunner as the text we are writing is too long to pass to the command line
            FileSystemRunner fileSystem = new SystemIORunner();

            string filename = Path.Combine(parentFolder, "RandomAccessToExistingMemoryMappedFile");
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename);
            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            StringBuilder contentsBuilder = new StringBuilder();
            while (contentsBuilder.Length < 4096 * 2)
            {
                contentsBuilder.Append(Guid.NewGuid().ToString());
            }

            string contents = contentsBuilder.ToString();
            int size = contents.Length;

            fileSystem.WriteAllText(fileVirtualPath, contents);
            fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents);

            string memoryMapFileName = "RandomAccessFile";
            using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath, FileMode.Open, memoryMapFileName))
            {
                Thread[] threads = new Thread[4];
                bool keepRunning = true;
                for (int i = 0; i < threads.Length; ++i)
                {
                    int myIndex = i;
                    threads[i] = new Thread(() =>
                    {
                        // Create random seeks (seeded for repeatability)
                        Random randNum = new Random(myIndex);

                        using (MemoryMappedFile threadFile = MemoryMappedFile.OpenExisting(memoryMapFileName))
                        {
                            while (keepRunning)
                            {
                                // Pick an offset somewhere in the first half of the file
                                int offset = randNum.Next(size / 2);

                                using (MemoryMappedViewAccessor randomAccessor = threadFile.CreateViewAccessor(offset, size - offset))
                                {
                                    for (int j = 0; j < size - offset; ++j)
                                    {
                                        ((char)randomAccessor.ReadByte(j)).ShouldEqual(contents[j + offset]);
                                    }
                                }
                            }
                        }
                    });

                    threads[i].Start();
                }

                Thread.Sleep(500);
                keepRunning = false;

                for (int i = 0; i < threads.Length; ++i)
                {
                    threads[i].Join();
                }
            }

            fileSystem.DeleteFile(fileVirtualPath);
            FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder);
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))]
        public void NativeReadAndWriteSeparateHandles(string parentFolder)
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;

            string filename = Path.Combine(parentFolder, "NativeReadAndWriteSeparateHandles");
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename);
            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            NativeTests.ReadAndWriteSeparateHandles(fileVirtualPath).ShouldEqual(true);

            FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder);
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))]
        public void NativeReadAndWriteSameHandle(string parentFolder)
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;

            string filename = Path.Combine(parentFolder, "NativeReadAndWriteSameHandle");
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename);
            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            NativeTests.ReadAndWriteSameHandle(fileVirtualPath, synchronousIO: false).ShouldEqual(true);

            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            NativeTests.ReadAndWriteSameHandle(fileVirtualPath, synchronousIO: true).ShouldEqual(true);

            FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder);
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))]
        public void NativeReadAndWriteRepeatedly(string parentFolder)
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;

            string filename = Path.Combine(parentFolder, "NativeReadAndWriteRepeatedly");
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename);
            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            NativeTests.ReadAndWriteRepeatedly(fileVirtualPath, synchronousIO: false).ShouldEqual(true);

            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            NativeTests.ReadAndWriteRepeatedly(fileVirtualPath, synchronousIO: true).ShouldEqual(true);

            FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder);
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))]
        public void NativeRemoveReadOnlyAttribute(string parentFolder)
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;

            string filename = Path.Combine(parentFolder, "NativeRemoveReadOnlyAttribute");
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename);
            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            NativeTests.RemoveReadOnlyAttribute(fileVirtualPath).ShouldEqual(true);

            FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder);
        }

        [TestCaseSource(typeof(FileRunnersAndFolders), nameof(FileRunnersAndFolders.Folders))]
        public void NativeCannotWriteToReadOnlyFile(string parentFolder)
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;

            string filename = Path.Combine(parentFolder, "NativeCannotWriteToReadOnlyFile");
            string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename);
            fileVirtualPath.ShouldNotExistOnDisk(fileSystem);

            NativeTests.CannotWriteToReadOnlyFile(fileVirtualPath).ShouldEqual(true);

            FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder);
        }

        [TestCase]
        public void NativeEnumerationErrorsMatchNTFS()
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;
            string nonExistentVirtualPath = this.Enlistment.GetVirtualPathTo("this_does_not_exist");
            nonExistentVirtualPath.ShouldNotExistOnDisk(fileSystem);
            string nonExistentPhysicalPath = Path.Combine(this.Enlistment.DotGVFSRoot, "this_does_not_exist");
            nonExistentPhysicalPath.ShouldNotExistOnDisk(fileSystem);

            NativeTests.EnumerationErrorsMatchNTFSForNonExistentFolder(nonExistentVirtualPath, nonExistentPhysicalPath).ShouldEqual(true);
        }

        [TestCase]
        public void NativeEnumerationErrorsMatchNTFSForNestedFolder()
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;

            this.Enlistment.GetVirtualPathTo("GVFS").ShouldBeADirectory(fileSystem);
            string nonExistentVirtualPath = this.Enlistment.GetVirtualPathTo("GVFS\\this_does_not_exist");
            nonExistentVirtualPath.ShouldNotExistOnDisk(fileSystem);

            this.Enlistment.DotGVFSRoot.ShouldBeADirectory(fileSystem);
            string nonExistentPhysicalPath = Path.Combine(this.Enlistment.DotGVFSRoot, "this_does_not_exist");
            nonExistentPhysicalPath.ShouldNotExistOnDisk(fileSystem);

            NativeTests.EnumerationErrorsMatchNTFSForNonExistentFolder(nonExistentVirtualPath, nonExistentPhysicalPath).ShouldEqual(true);
        }

        [TestCase]
        public void NativeEnumerationDotGitFolderErrorsMatchNTFS()
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;
            string nonExistentVirtualPath = this.Enlistment.GetVirtualPathTo(".git\\this_does_not_exist");
            nonExistentVirtualPath.ShouldNotExistOnDisk(fileSystem);
            string nonExistentPhysicalPath = Path.Combine(this.Enlistment.DotGVFSRoot, "this_does_not_exist");
            nonExistentPhysicalPath.ShouldNotExistOnDisk(fileSystem);

            NativeTests.EnumerationErrorsMatchNTFSForNonExistentFolder(nonExistentVirtualPath, nonExistentPhysicalPath).ShouldEqual(true);
        }

        [TestCase]
        public void NativeEnumerationErrorsMatchNTFSForEmptyNewFolder()
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;
            string newVirtualFolderPath = this.Enlistment.GetVirtualPathTo("new_folder");
            newVirtualFolderPath.ShouldNotExistOnDisk(fileSystem);
            fileSystem.CreateDirectory(newVirtualFolderPath);
            newVirtualFolderPath.ShouldBeADirectory(fileSystem);

            string newPhysicalFolderPath = Path.Combine(this.Enlistment.DotGVFSRoot, "new_folder");
            newPhysicalFolderPath.ShouldNotExistOnDisk(fileSystem);
            fileSystem.CreateDirectory(newPhysicalFolderPath);
            newPhysicalFolderPath.ShouldBeADirectory(fileSystem);

            NativeTests.EnumerationErrorsMatchNTFSForEmptyFolder(newVirtualFolderPath, newPhysicalFolderPath).ShouldEqual(true);

            fileSystem.DeleteDirectory(newVirtualFolderPath);
            newVirtualFolderPath.ShouldNotExistOnDisk(fileSystem);
            fileSystem.DeleteDirectory(newPhysicalFolderPath);
            newPhysicalFolderPath.ShouldNotExistOnDisk(fileSystem);
        }

        [TestCase]
        public void NativeDeleteEmptyFolderWithFileDispositionOnClose()
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;
            string newVirtualFolderPath = this.Enlistment.GetVirtualPathTo("new_folder");
            newVirtualFolderPath.ShouldNotExistOnDisk(fileSystem);
            fileSystem.CreateDirectory(newVirtualFolderPath);
            newVirtualFolderPath.ShouldBeADirectory(fileSystem);

            NativeTests.CanDeleteEmptyFolderWithFileDispositionOnClose(newVirtualFolderPath).ShouldEqual(true);

            newVirtualFolderPath.ShouldNotExistOnDisk(fileSystem);
        }

        [TestCase]
        public void NativeQueryDirectoryFileRestartScanResetsFilter()
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;
            string folderPath = this.Enlistment.GetVirtualPathTo("EnumerateAndReadTestFiles");
            folderPath.ShouldBeADirectory(fileSystem);

            NativeTests.QueryDirectoryFileRestartScanResetsFilter(folderPath).ShouldEqual(true);
        }

        [TestCase]
        public void ErrorWhenPathTreatsFileAsFolderMatchesNTFS_VirtualProjFSPath()
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;
            string existingFileVirtualPath = this.Enlistment.GetVirtualPathTo("ErrorWhenPathTreatsFileAsFolderMatchesNTFS\\virtual");
            string existingFilePhysicalPath = this.CreateFileInPhysicalPath(fileSystem);

            foreach (CreationDisposition creationDispostion in Enum.GetValues(typeof(CreationDisposition)))
            {
                NativeTests.ErrorWhenPathTreatsFileAsFolderMatchesNTFS(existingFileVirtualPath, existingFilePhysicalPath, (int)creationDispostion).ShouldEqual(true);
            }
        }

        [TestCase]
        public void ErrorWhenPathTreatsFileAsFolderMatchesNTFS_PartialProjFSPath()
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;

            string existingFileVirtualPath = this.Enlistment.GetVirtualPathTo("ErrorWhenPathTreatsFileAsFolderMatchesNTFS\\partial");
            existingFileVirtualPath.ShouldBeAFile(fileSystem);
            fileSystem.ReadAllText(existingFileVirtualPath);
            string existingFilePhysicalPath = this.CreateFileInPhysicalPath(fileSystem);

            foreach (CreationDisposition creationDispostion in Enum.GetValues(typeof(CreationDisposition)))
            {
                NativeTests.ErrorWhenPathTreatsFileAsFolderMatchesNTFS(existingFileVirtualPath, existingFilePhysicalPath, (int)creationDispostion).ShouldEqual(true);
            }
        }

        [TestCase]
        public void ErrorWhenPathTreatsFileAsFolderMatchesNTFS_FullProjFSPath()
        {
            FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;

            string existingFileVirtualPath = this.Enlistment.GetVirtualPathTo("ErrorWhenPathTreatsFileAsFolderMatchesNTFS\\full");
            existingFileVirtualPath.ShouldBeAFile(fileSystem);
            fileSystem.AppendAllText(existingFileVirtualPath, "extra text");
            string existingFilePhysicalPath = this.CreateFileInPhysicalPath(fileSystem);

            foreach (CreationDisposition creationDispostion in Enum.GetValues(typeof(CreationDisposition)))
            {
                NativeTests.ErrorWhenPathTreatsFileAsFolderMatchesNTFS(existingFileVirtualPath, existingFilePhysicalPath, (int)creationDispostion).ShouldEqual(true);
            }
        }

        [TestCase]
        public void EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete()
        {
            NativeTrailingSlashTests.EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_ModifyFileInScratchAndDir()
        {
            ProjFS_BugRegressionTest.ProjFS_ModifyFileInScratchAndDir(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_RMDIRTest1()
        {
            ProjFS_BugRegressionTest.ProjFS_RMDIRTest1(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_RMDIRTest2()
        {
            ProjFS_BugRegressionTest.ProjFS_RMDIRTest2(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_RMDIRTest3()
        {
            ProjFS_BugRegressionTest.ProjFS_RMDIRTest3(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_RMDIRTest4()
        {
            ProjFS_BugRegressionTest.ProjFS_RMDIRTest4(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_RMDIRTest5()
        {
            ProjFS_BugRegressionTest.ProjFS_RMDIRTest5(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeepNonExistFileUnderPartial()
        {
            ProjFS_BugRegressionTest.ProjFS_DeepNonExistFileUnderPartial(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_SupersededReparsePoint()
        {
            ProjFS_BugRegressionTest.ProjFS_SupersededReparsePoint(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteVirtualFile_SetDisposition()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteVirtualFile_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteVirtualFile_DeleteOnClose()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteVirtualFile_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeletePlaceholder_SetDisposition()
        {
            ProjFS_DeleteFileTest.ProjFS_DeletePlaceholder_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeletePlaceholder_DeleteOnClose()
        {
            ProjFS_DeleteFileTest.ProjFS_DeletePlaceholder_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteFullFile_SetDisposition()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteFullFile_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteFullFile_DeleteOnClose()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteFullFile_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteLocalFile_SetDisposition()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteLocalFile_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteLocalFile_DeleteOnClose()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteLocalFile_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteNotExistFile_SetDisposition()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteNotExistFile_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteNotExistFile_DeleteOnClose()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteNotExistFile_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteNonRootVirtualFile_SetDisposition()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteNonRootVirtualFile_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteNonRootVirtualFile_DeleteOnClose()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteNonRootVirtualFile_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteFileOutsideVRoot_SetDisposition()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteFileOutsideVRoot_SetDisposition(Path.GetDirectoryName(this.Enlistment.RepoRoot)).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteFileOutsideVRoot_DeleteOnClose()
        {
            ProjFS_DeleteFileTest.ProjFS_DeleteFileOutsideVRoot_DeleteOnClose(Path.GetDirectoryName(this.Enlistment.RepoRoot)).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteVirtualNonEmptyFolder_SetDisposition()
        {
            ProjFS_DeleteFolderTest.ProjFS_DeleteVirtualNonEmptyFolder_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteVirtualNonEmptyFolder_DeleteOnClose()
        {
            ProjFS_DeleteFolderTest.ProjFS_DeleteVirtualNonEmptyFolder_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeletePlaceholderNonEmptyFolder_SetDisposition()
        {
            ProjFS_DeleteFolderTest.ProjFS_DeletePlaceholderNonEmptyFolder_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeletePlaceholderNonEmptyFolder_DeleteOnClose()
        {
            ProjFS_DeleteFolderTest.ProjFS_DeletePlaceholderNonEmptyFolder_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteLocalEmptyFolder_SetDisposition()
        {
            ProjFS_DeleteFolderTest.ProjFS_DeleteLocalEmptyFolder_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteLocalEmptyFolder_DeleteOnClose()
        {
            ProjFS_DeleteFolderTest.ProjFS_DeleteLocalEmptyFolder_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteNonRootVirtualFolder_SetDisposition()
        {
            ProjFS_DeleteFolderTest.ProjFS_DeleteNonRootVirtualFolder_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteNonRootVirtualFolder_DeleteOnClose()
        {
            ProjFS_DeleteFolderTest.ProjFS_DeleteNonRootVirtualFolder_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_EnumEmptyFolder()
        {
            ProjFS_DirEnumTest.ProjFS_EnumEmptyFolder(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_EnumFolderWithOneFileInRepo()
        {
            ProjFS_DirEnumTest.ProjFS_EnumFolderWithOneFileInPackage(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_EnumFolderWithOneFileInRepoBeforeScratchFile()
        {
            ProjFS_DirEnumTest.ProjFS_EnumFolderWithOneFileInBoth(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_EnumFolderWithOneFileInRepoAfterScratchFile()
        {
            ProjFS_DirEnumTest.ProjFS_EnumFolderWithOneFileInBoth1(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_EnumFolderDeleteExistingFile()
        {
            ProjFS_DirEnumTest.ProjFS_EnumFolderDeleteExistingFile(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_EnumFolderSmallBuffer()
        {
            ProjFS_DirEnumTest.ProjFS_EnumFolderSmallBuffer(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_EnumTestNoMoreNoSuchReturnCodes()
        {
            ProjFS_DirEnumTest.ProjFS_EnumTestNoMoreNoSuchReturnCodes(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_EnumTestQueryDirectoryFileRestartScanProjectedFile()
        {
            ProjFS_DirEnumTest.ProjFS_EnumTestQueryDirectoryFileRestartScanProjectedFile(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_ModifyFileInScratchAndCheckLastWriteTime()
        {
            ProjFS_FileAttributeTest.ProjFS_ModifyFileInScratchAndCheckLastWriteTime(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_FileSize()
        {
            ProjFS_FileAttributeTest.ProjFS_FileSize(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_ModifyFileInScratchAndCheckFileSize()
        {
            ProjFS_FileAttributeTest.ProjFS_ModifyFileInScratchAndCheckFileSize(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_FileAttributes()
        {
            ProjFS_FileAttributeTest.ProjFS_FileAttributes(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_OneEAAttributeWillPass()
        {
            ProjFS_FileEATest.ProjFS_OneEAAttributeWillPass(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_OpenRootFolder()
        {
            ProjFS_FileOperationTest.ProjFS_OpenRootFolder(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_WriteAndVerify()
        {
            ProjFS_FileOperationTest.ProjFS_WriteAndVerify(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_DeleteExistingFile()
        {
            ProjFS_FileOperationTest.ProjFS_DeleteExistingFile(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_OpenNonExistingFile()
        {
            ProjFS_FileOperationTest.ProjFS_OpenNonExistingFile(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_NoneToNone()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_NoneToNone(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_VirtualToNone()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_VirtualToNone(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_PartialToNone()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_PartialToNone(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_FullToNone()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_FullToNone(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_LocalToNone()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_LocalToNone(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_VirtualToVirtual()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_VirtualToVirtual(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_VirtualToVirtualFileNameChanged()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_VirtualToVirtualFileNameChanged(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_VirtualToPartial()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_VirtualToPartial(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_PartialToPartial()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_PartialToPartial(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_LocalToVirtual()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_LocalToVirtual(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_VirtualToVirtualIntermidiateDirNotExist()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_VirtualToVirtualIntermidiateDirNotExist(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_VirtualToNoneIntermidiateDirNotExist()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_VirtualToNoneIntermidiateDirNotExist(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_OutsideToNone()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_OutsideToNone(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_OutsideToVirtual()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_OutsideToVirtual(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_OutsideToPartial()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_OutsideToPartial(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_NoneToOutside()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_NoneToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_VirtualToOutside()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_VirtualToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [Ignore("Disable this test until we can surface native test errors, see #454")]
        [TestCase]
        public void Native_ProjFS_MoveFile_PartialToOutside()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_PartialToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFile_OutsideToOutside()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_OutsideToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        [Ignore("Disabled while ProjFS fixes a regression")]
        public void Native_ProjFS_MoveFile_LongFileName()
        {
            ProjFS_MoveFileTest.ProjFS_MoveFile_LongFileName(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFolder_NoneToNone()
        {
            ProjFS_MoveFolderTest.ProjFS_MoveFolder_NoneToNone(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFolder_VirtualToNone()
        {
            ProjFS_MoveFolderTest.ProjFS_MoveFolder_VirtualToNone(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFolder_PartialToNone()
        {
            ProjFS_MoveFolderTest.ProjFS_MoveFolder_PartialToNone(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFolder_VirtualToVirtual()
        {
            ProjFS_MoveFolderTest.ProjFS_MoveFolder_VirtualToVirtual(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFolder_VirtualToPartial()
        {
            ProjFS_MoveFolderTest.ProjFS_MoveFolder_VirtualToPartial(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFolder_OutsideToNone()
        {
            ProjFS_MoveFolderTest.ProjFS_MoveFolder_OutsideToNone(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFolder_OutsideToVirtual()
        {
            ProjFS_MoveFolderTest.ProjFS_MoveFolder_OutsideToVirtual(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFolder_NoneToOutside()
        {
            ProjFS_MoveFolderTest.ProjFS_MoveFolder_NoneToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFolder_VirtualToOutside()
        {
            ProjFS_MoveFolderTest.ProjFS_MoveFolder_VirtualToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_MoveFolder_OutsideToOutside()
        {
            ProjFS_MoveFolderTest.ProjFS_MoveFolder_OutsideToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_OpenForReadsSameTime()
        {
            ProjFS_MultiThreadTest.ProjFS_OpenForReadsSameTime(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_OpenMultipleFilesForReadsSameTime()
        {
            ProjFS_MultiThreadTest.ProjFS_OpenMultipleFilesForReadsSameTime(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_OpenForWritesSameTime()
        {
            ProjFS_MultiThreadTest.ProjFS_OpenForWritesSameTime(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_SetLink_ToVirtualFile()
        {
            ProjFS_SetLinkTest.ProjFS_SetLink_ToVirtualFile(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_SetLink_ToPlaceHolder()
        {
            ProjFS_SetLinkTest.ProjFS_SetLink_ToPlaceHolder(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_SetLink_ToFullFile()
        {
            ProjFS_SetLinkTest.ProjFS_SetLink_ToFullFile(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_SetLink_ToNonExistFileWillFail()
        {
            ProjFS_SetLinkTest.ProjFS_SetLink_ToNonExistFileWillFail(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_SetLink_NameAlreadyExistWillFail()
        {
            ProjFS_SetLinkTest.ProjFS_SetLink_NameAlreadyExistWillFail(this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_SetLink_FromOutside()
        {
            ProjFS_SetLinkTest.ProjFS_SetLink_FromOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        [TestCase]
        public void Native_ProjFS_SetLink_ToOutside()
        {
            ProjFS_SetLinkTest.ProjFS_SetLink_ToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true);
        }

        private string CreateFileInPhysicalPath(FileSystemRunner fileSystem)
        {
            string existingFilePhysicalPath = Path.Combine(this.Enlistment.DotGVFSRoot, "existingFileTest.txt");
            fileSystem.WriteAllText(existingFilePhysicalPath, "File for testing");
            existingFilePhysicalPath.ShouldBeAFile(fileSystem);
            return existingFilePhysicalPath;
        }

        private class NativeTests
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ReadAndWriteSeparateHandles(string fileVirtualPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ReadAndWriteSameHandle(string fileVirtualPath, bool synchronousIO);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ReadAndWriteRepeatedly(string fileVirtualPath, bool synchronousIO);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool RemoveReadOnlyAttribute(string fileVirtualPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool CannotWriteToReadOnlyFile(string fileVirtualPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool EnumerationErrorsMatchNTFSForNonExistentFolder(string nonExistentVirtualPath, string nonExistentPhysicalPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool EnumerationErrorsMatchNTFSForEmptyFolder(string emptyFolderVirtualPath, string emptyFolderPhysicalPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool CanDeleteEmptyFolderWithFileDispositionOnClose(string emptyFolderPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool QueryDirectoryFileRestartScanResetsFilter(string folderPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ErrorWhenPathTreatsFileAsFolderMatchesNTFS(string filePath, string fileNTFSPath, int creationDisposition);
        }

        private class NativeTrailingSlashTests
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete(string virtualRootPath);
        }

        private class ProjFS_BugRegressionTest
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_ModifyFileInScratchAndDir(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_RMDIRTest1(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_RMDIRTest2(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_RMDIRTest3(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_RMDIRTest4(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_RMDIRTest5(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeepNonExistFileUnderPartial(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_SupersededReparsePoint(string virtualRootPath);
        }

        private class ProjFS_DeleteFileTest
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteVirtualFile_SetDisposition(string enumFolderSmallBufferPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteVirtualFile_DeleteOnClose(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeletePlaceholder_SetDisposition(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeletePlaceholder_DeleteOnClose(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteFullFile_SetDisposition(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteFullFile_DeleteOnClose(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteLocalFile_SetDisposition(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteLocalFile_DeleteOnClose(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteNotExistFile_SetDisposition(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteNotExistFile_DeleteOnClose(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteNonRootVirtualFile_SetDisposition(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteNonRootVirtualFile_DeleteOnClose(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteFileOutsideVRoot_SetDisposition(string pathOutsideRepo);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteFileOutsideVRoot_DeleteOnClose(string pathOutsideRepo);
        }

        private class ProjFS_DeleteFolderTest
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteVirtualNonEmptyFolder_SetDisposition(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteVirtualNonEmptyFolder_DeleteOnClose(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeletePlaceholderNonEmptyFolder_SetDisposition(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeletePlaceholderNonEmptyFolder_DeleteOnClose(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteLocalEmptyFolder_SetDisposition(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteLocalEmptyFolder_DeleteOnClose(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteNonRootVirtualFolder_SetDisposition(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteNonRootVirtualFolder_DeleteOnClose(string virtualRootPath);
        }

        private class ProjFS_DirEnumTest
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_EnumEmptyFolder(string emptyFolderPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_EnumFolderWithOneFileInPackage(string enumFolderWithOneFileInRepoPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_EnumFolderWithOneFileInBoth(string enumFolderWithOneFileInRepoBeforeScratchPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_EnumFolderWithOneFileInBoth1(string enumFolderWithOneFileInRepoAfterScratchPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_EnumFolderDeleteExistingFile(string enumFolderDeleteExistingFilePath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_EnumFolderSmallBuffer(string enumFolderSmallBufferPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_EnumTestNoMoreNoSuchReturnCodes(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_EnumTestQueryDirectoryFileRestartScanProjectedFile(string virtualRootPath);
        }

        private class ProjFS_FileAttributeTest
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_ModifyFileInScratchAndCheckLastWriteTime(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_FileSize(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_ModifyFileInScratchAndCheckFileSize(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_FileAttributes(string virtualRootPath);
        }

        private class ProjFS_FileEATest
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_OneEAAttributeWillPass(string virtualRootPath);
        }

        private class ProjFS_FileOperationTest
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_OpenRootFolder(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_WriteAndVerify(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_DeleteExistingFile(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_OpenNonExistingFile(string virtualRootPath);
        }

        private class ProjFS_MoveFileTest
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_NoneToNone(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_VirtualToNone(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_PartialToNone(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_FullToNone(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_LocalToNone(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_VirtualToVirtual(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_VirtualToVirtualFileNameChanged(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_VirtualToPartial(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_PartialToPartial(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_LocalToVirtual(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_VirtualToVirtualIntermidiateDirNotExist(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_VirtualToNoneIntermidiateDirNotExist(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_OutsideToNone(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_OutsideToVirtual(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_OutsideToPartial(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_NoneToOutside(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_VirtualToOutside(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_PartialToOutside(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_OutsideToOutside(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFile_LongFileName(string virtualRootPath);
        }

        private class ProjFS_MoveFolderTest
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFolder_NoneToNone(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFolder_VirtualToNone(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFolder_PartialToNone(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFolder_VirtualToVirtual(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFolder_VirtualToPartial(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFolder_OutsideToNone(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFolder_OutsideToVirtual(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFolder_NoneToOutside(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFolder_VirtualToOutside(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_MoveFolder_OutsideToOutside(string pathOutsideRepo, string virtualRootPath);
        }

        private class ProjFS_MultiThreadTest
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_OpenForReadsSameTime(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_OpenForWritesSameTime(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_OpenMultipleFilesForReadsSameTime(string virtualRootPath);
        }

        private class ProjFS_SetLinkTest
        {
            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_SetLink_ToVirtualFile(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_SetLink_ToPlaceHolder(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_SetLink_ToFullFile(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_SetLink_ToNonExistFileWillFail(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_SetLink_NameAlreadyExistWillFail(string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_SetLink_FromOutside(string pathOutsideRepo, string virtualRootPath);

            [DllImport("GVFS.NativeTests.dll")]
            public static extern bool ProjFS_SetLink_ToOutside(string pathOutsideRepo, string virtualRootPath);
        }

        private class FileRunnersAndFolders
        {
            public const string TestFolders = "Folders";
            public const string TestRunners = "Runners";
            public const string DotGitFolder = ".git";

            private static object[] allFolders =
            {
                new object[] { string.Empty },
                new object[] { DotGitFolder },
            };

            public static object[] Runners
            {
                get
                {
                    List runnersAndParentFolders = new List();
                    foreach (object[] runner in FileSystemRunner.Runners.ToList())
                    {
                        runnersAndParentFolders.Add(new object[] { runner.ToList().First(), string.Empty });
                        runnersAndParentFolders.Add(new object[] { runner.ToList().First(), DotGitFolder });
                    }

                    return runnersAndParentFolders.ToArray();
                }
            }

            public static object[] Folders
            {
                get
                {
                    return allFolders;
                }
            }

            public static void ShouldNotExistOnDisk(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem, string filename, string parentFolder)
            {
                enlistment.GetVirtualPathTo(filename).ShouldNotExistOnDisk(fileSystem);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Windows/Tests/WindowsFolderUsnUpdate.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Tests.EnlistmentPerFixture;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.IO;
using System.Text.RegularExpressions;

namespace GVFS.FunctionalTests.Windows.Windows.Tests
{
    [TestFixture]
    [Category(Categories.GitCommands)]
    [Ignore("fsutil requires WSL be enabled.  Need to find a way to enable for builds or a different way to get the USN for the folder.")]
    public class WindowsFolderUsnUpdate : TestsWithEnlistmentPerFixture
    {
        private const string StartingCommit = "ad87b3877c8fa6bebbe62330354f5c535875c4dd";
        private const string CommitWithChanges = "e6d047cf65f4a384568b7a451530e18410bc8a12";
        private FileSystemRunner fileSystem;

        public WindowsFolderUsnUpdate()
        {
            this.fileSystem = new SystemIORunner();
        }

        [TestCase]
        public void CheckoutUpdatesFolderUsnJournal()
        {
            // Update config then remount.
            this.Enlistment.WriteConfig("usn.updateDirectories", "true");
            this.Enlistment.UnmountGVFS();
            this.Enlistment.MountGVFS();

            this.GitCheckoutCommitId(StartingCommit);
            this.GitStatusShouldBeClean(StartingCommit);

            FolderPathUsn[] pathsToCheck = new FolderPathUsn[]
            {
                new FolderPathUsn(Path.Combine(this.Enlistment.RepoRoot, "Test_ConflictTests", "AddedFiles"), this.fileSystem),
                new FolderPathUsn(Path.Combine(this.Enlistment.RepoRoot, "Test_ConflictTests", "DeletedFiles"), this.fileSystem),
                new FolderPathUsn(Path.Combine(this.Enlistment.RepoRoot, "Test_ConflictTests", "ModifiedFiles"), this.fileSystem),
            };

            this.GitCheckoutCommitId(CommitWithChanges);
            this.GitStatusShouldBeClean(CommitWithChanges);

            foreach (FolderPathUsn folderPath in pathsToCheck)
            {
                folderPath.ValidateUsnChange();
            }

            this.Enlistment.WriteConfig("usn.updateDirectories", "false");
        }

        private static string UsnFolderId(string path)
        {
            ProcessResult result = ProcessHelper.Run("fsutil", $"usn readdata \"{path}\"");
            Match match = Regex.Match(result.Output, @"^Usn\s+:\s(\w+)", RegexOptions.Multiline);
            if (match.Success)
            {
                return match.Value;
            }

            return string.Empty;
        }

        private void GitStatusShouldBeClean(string commitId)
        {
            GitHelpers.CheckGitCommandAgainstGVFSRepo(
                this.Enlistment.RepoRoot,
                "status",
                "HEAD detached at " + commitId,
                "nothing to commit, working tree clean");
        }

        private void GitCheckoutCommitId(string commitId)
        {
            GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout " + commitId).Errors.ShouldContain("HEAD is now at " + commitId);
        }

        private class FolderPathUsn
        {
            private readonly string path;
            private readonly string originalUsn;

            public FolderPathUsn(string path, FileSystemRunner fileSystem)
            {
                this.path = path;
                fileSystem.EnumerateDirectory(path);
                this.originalUsn = UsnFolderId(path);
            }

            public void ValidateUsnChange()
            {
                string usnAfter = UsnFolderId(this.path);
                usnAfter.ShouldNotEqual(this.originalUsn);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Windows/Tests/WindowsTombstoneTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.IO;
using System.Linq;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    [TestFixture]
    [Category(Categories.GitCommands)]
    public class WindowsTombstoneTests : TestsWithEnlistmentPerFixture
    {
        private const string Delimiter = "\r\n";
        private const int TombstoneFolderPlaceholderType = 3;
        private FileSystemRunner fileSystem;

        public WindowsTombstoneTests()
        {
            this.fileSystem = new SystemIORunner();
        }

        [TestCase]
        public void CheckoutCleansUpTombstones()
        {
            const string folderToDelete = "Scripts";

            // Delete directory to create the tombstone
            string directoryToDelete = this.Enlistment.GetVirtualPathTo(folderToDelete);
            this.fileSystem.DeleteDirectory(directoryToDelete);
            this.Enlistment.UnmountGVFS();

            // Remove the directory entry from modified paths so git will not keep the folder up to date
            string modifiedPathsFile = Path.Combine(this.Enlistment.DotGVFSRoot, TestConstants.Databases.ModifiedPaths);
            string modifiedPathsContent = this.fileSystem.ReadAllText(modifiedPathsFile);
            modifiedPathsContent = string.Join(Delimiter, modifiedPathsContent.Split(new[] { Delimiter }, StringSplitOptions.RemoveEmptyEntries).Where(x => !x.StartsWith($"A {folderToDelete}/")));
            this.fileSystem.WriteAllText(modifiedPathsFile, modifiedPathsContent + Delimiter);

            // Add tombstone folder entry to the placeholder database so the checkout will remove the tombstone
            // and start projecting the folder again
            string placeholderDatabasePath = Path.Combine(this.Enlistment.DotGVFSRoot, TestConstants.Databases.VFSForGit);
            GVFSHelpers.AddPlaceholderFolder(placeholderDatabasePath, folderToDelete, TombstoneFolderPlaceholderType);

            this.Enlistment.MountGVFS();
            directoryToDelete.ShouldNotExistOnDisk(this.fileSystem);

            // checkout branch to remove tombstones and project the folder again
            GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout -f HEAD");
            directoryToDelete.ShouldBeADirectory(this.fileSystem);

            this.Enlistment.UnmountGVFS();

            string placholders = GVFSHelpers.GetAllSQLitePlaceholdersAsString(placeholderDatabasePath);
            placholders.ShouldNotContain(ignoreCase: false, unexpectedSubstrings: $"{folderToDelete}{GVFSHelpers.PlaceholderFieldDelimiter}{TombstoneFolderPlaceholderType}{GVFSHelpers.PlaceholderFieldDelimiter}");
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests/Windows/Tests/WindowsUpdatePlaceholderTests.cs
================================================
using GVFS.FunctionalTests.FileSystemRunners;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using Microsoft.Win32.SafeHandles;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
    // WindowsOnly because tests in this class depend on Windows specific file sharing behavior
    [TestFixture]
    [Category(Categories.GitCommands)]
    public class WindowsUpdatePlaceholderTests : TestsWithEnlistmentPerFixture
    {
        private const string TestParentFolderName = "Test_EPF_UpdatePlaceholderTests";
        private const string OldCommitId = "5d7a7d4db1734fb468a4094469ec58d26301b59d";
        private const string NewFilesAndChangesCommitId = "fec239ea12de1eda6ae5329d4f345784d5b61ff9";
        private FileSystemRunner fileSystem;

        public WindowsUpdatePlaceholderTests()
        {
            this.fileSystem = new SystemIORunner();
        }

        [SetUp]
        public virtual void SetupForTest()
        {
            // Start each test at NewFilesAndChangesCommitId
            this.GitCheckoutCommitId(NewFilesAndChangesCommitId);
            this.GitStatusShouldBeClean(NewFilesAndChangesCommitId);
        }

        [TestCase, Order(1)]
        public void LockToPreventDelete_SingleFile()
        {
            string testFile1Contents = "TestContentsLockToPreventDelete \r\n";
            string testFile1Name = "test.txt";
            string testFile1Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventDelete", testFile1Name));

            testFile1Path.ShouldBeAFile(this.fileSystem).WithContents(testFile1Contents);
            using (SafeFileHandle testFile1Handle = this.CreateFile(testFile1Path, FileShare.Read))
            {
                testFile1Handle.IsInvalid.ShouldEqual(false);

                ProcessResult result = this.InvokeGitAgainstGVFSRepo("checkout " + OldCommitId);
                result.Errors.ShouldContain(
                    "GVFS was unable to delete the following files. To recover, close all handles to the files and run these commands:",
                    "git clean -f " + TestParentFolderName + "/LockToPreventDelete/" + testFile1Name);

                GitHelpers.CheckGitCommandAgainstGVFSRepo(
                    this.Enlistment.RepoRoot,
                    "status -u",
                    "HEAD detached at " + OldCommitId,
                    "Untracked files:",
                    TestParentFolderName + "/LockToPreventDelete/" + testFile1Name);

                testFile1Path.ShouldBeAFile(this.fileSystem).WithContents(testFile1Contents);
            }

            this.GitCleanFile(TestParentFolderName + "/LockToPreventDelete/" + testFile1Name);
            this.GitStatusShouldBeClean(OldCommitId);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventDelete/" + testFile1Name);
            testFile1Path.ShouldNotExistOnDisk(this.fileSystem);

            this.GitCheckoutCommitId(NewFilesAndChangesCommitId);

            this.GitStatusShouldBeClean(NewFilesAndChangesCommitId);
            testFile1Path.ShouldBeAFile(this.fileSystem).WithContents(testFile1Contents);
        }

        [TestCase, Order(2)]
        public void LockToPreventDelete_MultipleFiles()
        {
            string testFile2Contents = "TestContentsLockToPreventDelete2 \r\n";
            string testFile3Contents = "TestContentsLockToPreventDelete3 \r\n";
            string testFile4Contents = "TestContentsLockToPreventDelete4 \r\n";

            string testFile2Name = "test2.txt";
            string testFile3Name = "test3.txt";
            string testFile4Name = "test4.txt";

            string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventDelete", testFile2Name));
            string testFile3Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventDelete", testFile3Name));
            string testFile4Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventDelete", testFile4Name));

            testFile2Path.ShouldBeAFile(this.fileSystem).WithContents(testFile2Contents);
            testFile3Path.ShouldBeAFile(this.fileSystem).WithContents(testFile3Contents);
            testFile4Path.ShouldBeAFile(this.fileSystem).WithContents(testFile4Contents);

            using (SafeFileHandle testFile2Handle = this.CreateFile(testFile2Path, FileShare.Read))
            using (SafeFileHandle testFile3Handle = this.CreateFile(testFile3Path, FileShare.Read))
            using (SafeFileHandle testFile4Handle = this.CreateFile(testFile4Path, FileShare.Read))
            {
                testFile2Handle.IsInvalid.ShouldEqual(false);
                testFile3Handle.IsInvalid.ShouldEqual(false);
                testFile4Handle.IsInvalid.ShouldEqual(false);

                ProcessResult result = this.InvokeGitAgainstGVFSRepo("checkout " + OldCommitId);
                result.Errors.ShouldContain(
                    "GVFS was unable to delete the following files. To recover, close all handles to the files and run these commands:",
                    "git clean -f " + TestParentFolderName + "/LockToPreventDelete/" + testFile2Name,
                    "git clean -f " + TestParentFolderName + "/LockToPreventDelete/" + testFile3Name,
                    "git clean -f " + TestParentFolderName + "/LockToPreventDelete/" + testFile4Name);

                GitHelpers.CheckGitCommandAgainstGVFSRepo(
                    this.Enlistment.RepoRoot,
                    "status -u",
                    "HEAD detached at " + OldCommitId,
                    "Untracked files:",
                    TestParentFolderName + "/LockToPreventDelete/" + testFile2Name,
                    TestParentFolderName + "/LockToPreventDelete/" + testFile3Name,
                    TestParentFolderName + "/LockToPreventDelete/" + testFile4Name);
            }

            this.GitCleanFile(TestParentFolderName + "/LockToPreventDelete/" + testFile2Name);
            this.GitCleanFile(TestParentFolderName + "/LockToPreventDelete/" + testFile3Name);
            this.GitCleanFile(TestParentFolderName + "/LockToPreventDelete/" + testFile4Name);

            this.GitStatusShouldBeClean(OldCommitId);

            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventDelete/" + testFile2Name);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventDelete/" + testFile3Name);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventDelete/" + testFile4Name);

            testFile2Path.ShouldNotExistOnDisk(this.fileSystem);
            testFile3Path.ShouldNotExistOnDisk(this.fileSystem);
            testFile4Path.ShouldNotExistOnDisk(this.fileSystem);

            this.GitCheckoutCommitId(NewFilesAndChangesCommitId);

            this.GitStatusShouldBeClean(NewFilesAndChangesCommitId);
            testFile2Path.ShouldBeAFile(this.fileSystem).WithContents(testFile2Contents);
            testFile3Path.ShouldBeAFile(this.fileSystem).WithContents(testFile3Contents);
            testFile4Path.ShouldBeAFile(this.fileSystem).WithContents(testFile4Contents);
        }

        [TestCase, Order(3)]
        public void LockToPreventUpdate_SingleFile()
        {
            string testFile1Contents = "Commit2LockToPreventUpdate \r\n";
            string testFile1OldContents = "TestFileLockToPreventUpdate \r\n";
            string testFile1Name = "test.txt";
            string testFile1Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdate", testFile1Name));

            testFile1Path.ShouldBeAFile(this.fileSystem).WithContents(testFile1Contents);
            using (SafeFileHandle testFile1Handle = this.CreateFile(testFile1Path, FileShare.Read))
            {
                testFile1Handle.IsInvalid.ShouldEqual(false);

                ProcessResult result = this.InvokeGitAgainstGVFSRepo("checkout " + OldCommitId);
                result.Errors.ShouldContain(
                    "GVFS was unable to update the following files. To recover, close all handles to the files and run these commands:",
                    "git checkout -- " + TestParentFolderName + "/LockToPreventUpdate/" + testFile1Name);

                GitHelpers.CheckGitCommandAgainstGVFSRepo(
                    this.Enlistment.RepoRoot,
                    "status",
                    "HEAD detached at " + OldCommitId,
                    "Changes not staged for commit:",
                    TestParentFolderName + "/LockToPreventUpdate/" + testFile1Name);
            }

            this.GitCheckoutToDiscardChanges(TestParentFolderName + "/LockToPreventUpdate/" + testFile1Name);
            this.GitStatusShouldBeClean(OldCommitId);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventUpdate/" + testFile1Name);
            testFile1Path.ShouldBeAFile(this.fileSystem).WithContents(testFile1OldContents);

            this.GitCheckoutCommitId(NewFilesAndChangesCommitId);

            this.GitStatusShouldBeClean(NewFilesAndChangesCommitId);
            testFile1Path.ShouldBeAFile(this.fileSystem).WithContents(testFile1Contents);
        }

        [TestCase, Order(4)]
        public void LockToPreventUpdate_MultipleFiles()
        {
            string testFile2Contents = "Commit2LockToPreventUpdate2 \r\n";
            string testFile3Contents = "Commit2LockToPreventUpdate3 \r\n";
            string testFile4Contents = "Commit2LockToPreventUpdate4 \r\n";

            string testFile2OldContents = "TestFileLockToPreventUpdate2 \r\n";
            string testFile3OldContents = "TestFileLockToPreventUpdate3 \r\n";
            string testFile4OldContents = "TestFileLockToPreventUpdate4 \r\n";

            string testFile2Name = "test2.txt";
            string testFile3Name = "test3.txt";
            string testFile4Name = "test4.txt";

            string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdate", testFile2Name));
            string testFile3Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdate", testFile3Name));
            string testFile4Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdate", testFile4Name));

            testFile2Path.ShouldBeAFile(this.fileSystem).WithContents(testFile2Contents);
            testFile3Path.ShouldBeAFile(this.fileSystem).WithContents(testFile3Contents);
            testFile4Path.ShouldBeAFile(this.fileSystem).WithContents(testFile4Contents);

            using (SafeFileHandle testFile2Handle = this.CreateFile(testFile2Path, FileShare.Read))
            using (SafeFileHandle testFile3Handle = this.CreateFile(testFile3Path, FileShare.Read))
            using (SafeFileHandle testFile4Handle = this.CreateFile(testFile4Path, FileShare.Read))
            {
                testFile2Handle.IsInvalid.ShouldEqual(false);
                testFile3Handle.IsInvalid.ShouldEqual(false);
                testFile4Handle.IsInvalid.ShouldEqual(false);

                ProcessResult result = this.InvokeGitAgainstGVFSRepo("checkout " + OldCommitId);
                result.Errors.ShouldContain(
                    "GVFS was unable to update the following files. To recover, close all handles to the files and run these commands:",
                    "git checkout -- " + TestParentFolderName + "/LockToPreventUpdate/" + testFile2Name,
                    "git checkout -- " + TestParentFolderName + "/LockToPreventUpdate/" + testFile3Name,
                    "git checkout -- " + TestParentFolderName + "/LockToPreventUpdate/" + testFile4Name);

                GitHelpers.CheckGitCommandAgainstGVFSRepo(
                    this.Enlistment.RepoRoot,
                    "status",
                    "HEAD detached at " + OldCommitId,
                    "Changes not staged for commit:",
                    TestParentFolderName + "/LockToPreventUpdate/" + testFile2Name,
                    TestParentFolderName + "/LockToPreventUpdate/" + testFile3Name,
                    TestParentFolderName + "/LockToPreventUpdate/" + testFile4Name);
            }

            this.GitCheckoutToDiscardChanges(TestParentFolderName + "/LockToPreventUpdate/" + testFile2Name);
            this.GitCheckoutToDiscardChanges(TestParentFolderName + "/LockToPreventUpdate/" + testFile3Name);
            this.GitCheckoutToDiscardChanges(TestParentFolderName + "/LockToPreventUpdate/" + testFile4Name);

            this.GitStatusShouldBeClean(OldCommitId);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventUpdate/" + testFile2Name);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventUpdate/" + testFile3Name);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventUpdate/" + testFile4Name);
            testFile2Path.ShouldBeAFile(this.fileSystem).WithContents(testFile2OldContents);
            testFile3Path.ShouldBeAFile(this.fileSystem).WithContents(testFile3OldContents);
            testFile4Path.ShouldBeAFile(this.fileSystem).WithContents(testFile4OldContents);

            this.GitCheckoutCommitId(NewFilesAndChangesCommitId);

            this.GitStatusShouldBeClean(NewFilesAndChangesCommitId);
            testFile2Path.ShouldBeAFile(this.fileSystem).WithContents(testFile2Contents);
            testFile3Path.ShouldBeAFile(this.fileSystem).WithContents(testFile3Contents);
            testFile4Path.ShouldBeAFile(this.fileSystem).WithContents(testFile4Contents);
        }

        [TestCase, Order(5)]
        public void LockToPreventUpdateAndDelete()
        {
            string testFileUpdate1Contents = "Commit2LockToPreventUpdateAndDelete \r\n";
            string testFileUpdate2Contents = "Commit2LockToPreventUpdateAndDelete2 \r\n";
            string testFileUpdate3Contents = "Commit2LockToPreventUpdateAndDelete3 \r\n";
            string testFileDelete1Contents = "PreventDelete \r\n";
            string testFileDelete2Contents = "PreventDelete2 \r\n";
            string testFileDelete3Contents = "PreventDelete3 \r\n";

            string testFileUpdate1OldContents = "TestFileLockToPreventUpdateAndDelete \r\n";
            string testFileUpdate2OldContents = "TestFileLockToPreventUpdateAndDelete2 \r\n";
            string testFileUpdate3OldContents = "TestFileLockToPreventUpdateAndDelete3 \r\n";

            string testFileUpdate1Name = "test.txt";
            string testFileUpdate2Name = "test2.txt";
            string testFileUpdate3Name = "test3.txt";
            string testFileDelete1Name = "test_delete.txt";
            string testFileDelete2Name = "test_delete2.txt";
            string testFileDelete3Name = "test_delete3.txt";

            string testFileUpdate1Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdateAndDelete", testFileUpdate1Name));
            string testFileUpdate2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdateAndDelete", testFileUpdate2Name));
            string testFileUpdate3Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdateAndDelete", testFileUpdate3Name));
            string testFileDelete1Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdateAndDelete", testFileDelete1Name));
            string testFileDelete2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdateAndDelete", testFileDelete2Name));
            string testFileDelete3Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "LockToPreventUpdateAndDelete", testFileDelete3Name));

            testFileUpdate1Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate1Contents);
            testFileUpdate2Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate2Contents);
            testFileUpdate3Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate3Contents);
            testFileDelete1Path.ShouldBeAFile(this.fileSystem).WithContents(testFileDelete1Contents);
            testFileDelete2Path.ShouldBeAFile(this.fileSystem).WithContents(testFileDelete2Contents);
            testFileDelete3Path.ShouldBeAFile(this.fileSystem).WithContents(testFileDelete3Contents);

            using (SafeFileHandle testFileUpdate1Handle = this.CreateFile(testFileUpdate1Path, FileShare.Read))
            using (SafeFileHandle testFileUpdate2Handle = this.CreateFile(testFileUpdate2Path, FileShare.Read))
            using (SafeFileHandle testFileUpdate3Handle = this.CreateFile(testFileUpdate3Path, FileShare.Read))
            using (SafeFileHandle testFileDelete1Handle = this.CreateFile(testFileDelete1Path, FileShare.Read))
            using (SafeFileHandle testFileDelete2Handle = this.CreateFile(testFileDelete2Path, FileShare.Read))
            using (SafeFileHandle testFileDelete3Handle = this.CreateFile(testFileDelete3Path, FileShare.Read))
            {
                testFileUpdate1Handle.IsInvalid.ShouldEqual(false);
                testFileUpdate2Handle.IsInvalid.ShouldEqual(false);
                testFileUpdate3Handle.IsInvalid.ShouldEqual(false);
                testFileDelete1Handle.IsInvalid.ShouldEqual(false);
                testFileDelete2Handle.IsInvalid.ShouldEqual(false);
                testFileDelete3Handle.IsInvalid.ShouldEqual(false);

                ProcessResult checkoutResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout " + OldCommitId);
                checkoutResult.Errors.ShouldContain(
                    "HEAD is now at " + OldCommitId,
                    "GVFS was unable to delete the following files. To recover, close all handles to the files and run these commands:",
                    "git clean -f " + TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileDelete1Name,
                    "git clean -f " + TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileDelete2Name,
                    "git clean -f " + TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileDelete3Name,
                    "GVFS was unable to update the following files. To recover, close all handles to the files and run these commands:",
                    "git checkout -- " + TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileUpdate1Name,
                    "git checkout -- " + TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileUpdate2Name,
                    "git checkout -- " + TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileUpdate3Name);

                GitHelpers.CheckGitCommandAgainstGVFSRepo(
                    this.Enlistment.RepoRoot,
                    "status",
                    "HEAD detached at " + OldCommitId,
                    "modified:   Test_EPF_UpdatePlaceholderTests/LockToPreventUpdateAndDelete/test.txt",
                    "modified:   Test_EPF_UpdatePlaceholderTests/LockToPreventUpdateAndDelete/test2.txt",
                    "modified:   Test_EPF_UpdatePlaceholderTests/LockToPreventUpdateAndDelete/test3.txt",
                    "Untracked files:\n  (use \"git add ...\" to include in what will be committed)\n\tTest_EPF_UpdatePlaceholderTests/LockToPreventUpdateAndDelete/test_delete.txt\n\tTest_EPF_UpdatePlaceholderTests/LockToPreventUpdateAndDelete/test_delete2.txt\n\tTest_EPF_UpdatePlaceholderTests/LockToPreventUpdateAndDelete/test_delete3.txt",
                    "no changes added to commit (use \"git add\" and/or \"git commit -a\")\n");
            }

            this.GitCheckoutToDiscardChanges(TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileUpdate1Name);
            this.GitCheckoutToDiscardChanges(TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileUpdate2Name);
            this.GitCheckoutToDiscardChanges(TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileUpdate3Name);
            this.GitCleanFile(TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileDelete1Name);
            this.GitCleanFile(TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileDelete2Name);
            this.GitCleanFile(TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileDelete3Name);

            this.GitStatusShouldBeClean(OldCommitId);

            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileUpdate1Name);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileUpdate2Name);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileUpdate3Name);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileDelete1Name);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileDelete2Name);
            GVFSHelpers.ModifiedPathsShouldContain(this.Enlistment, this.fileSystem, TestParentFolderName + "/LockToPreventUpdateAndDelete/" + testFileDelete3Name);

            testFileUpdate1Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate1OldContents);
            testFileUpdate2Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate2OldContents);
            testFileUpdate3Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate3OldContents);
            testFileDelete1Path.ShouldNotExistOnDisk(this.fileSystem);
            testFileDelete2Path.ShouldNotExistOnDisk(this.fileSystem);
            testFileDelete3Path.ShouldNotExistOnDisk(this.fileSystem);

            this.GitCheckoutCommitId(NewFilesAndChangesCommitId);

            this.GitStatusShouldBeClean(NewFilesAndChangesCommitId);
            testFileUpdate1Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate1Contents);
            testFileUpdate2Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate2Contents);
            testFileUpdate3Path.ShouldBeAFile(this.fileSystem).WithContents(testFileUpdate3Contents);
            testFileDelete1Path.ShouldBeAFile(this.fileSystem).WithContents(testFileDelete1Contents);
            testFileDelete2Path.ShouldBeAFile(this.fileSystem).WithContents(testFileDelete2Contents);
            testFileDelete3Path.ShouldBeAFile(this.fileSystem).WithContents(testFileDelete3Contents);
        }

        [TestCase, Order(6)]
        public void LockMoreThanMaxReportedFileNames()
        {
            string updateFilesFolder = "FilesToUpdate";
            string deleteFilesFolder = "FilesToDelete";

            for (int i = 1; i <= 51; ++i)
            {
                this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "MaxFileListCount", updateFilesFolder, i.ToString() + ".txt")).ShouldBeAFile(this.fileSystem);
                this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "MaxFileListCount", deleteFilesFolder, i.ToString() + ".txt")).ShouldBeAFile(this.fileSystem);
            }

            List openHandles = new List();
            try
            {
                for (int i = 1; i <= 51; ++i)
                {
                    SafeFileHandle handle = this.CreateFile(
                        this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "MaxFileListCount", updateFilesFolder, i.ToString() + ".txt")),
                        FileShare.Read);
                    openHandles.Add(handle);
                    handle.IsInvalid.ShouldEqual(false);

                    handle = this.CreateFile(
                        this.Enlistment.GetVirtualPathTo(Path.Combine(TestParentFolderName, "MaxFileListCount", deleteFilesFolder, i.ToString() + ".txt")),
                        FileShare.Read);
                    openHandles.Add(handle);
                    handle.IsInvalid.ShouldEqual(false);
                }

                ProcessResult result = this.InvokeGitAgainstGVFSRepo("checkout " + OldCommitId);
                result.Errors.ShouldContain(
                    "GVFS failed to update 102 files, run 'git status' to check the status of files in the repo");

                List expectedOutputStrings = new List()
                    {
                        "HEAD detached at " + OldCommitId,
                        "no changes added to commit (use \"git add\" and/or \"git commit -a\")\n"
                    };

                for (int expectedFilePrefix = 1; expectedFilePrefix <= 51; ++expectedFilePrefix)
                {
                    expectedOutputStrings.Add("modified:   Test_EPF_UpdatePlaceholderTests/MaxFileListCount/" + updateFilesFolder + "/" + expectedFilePrefix.ToString() + ".txt");
                    expectedOutputStrings.Add("Test_EPF_UpdatePlaceholderTests/MaxFileListCount/" + deleteFilesFolder + "/" + expectedFilePrefix.ToString() + ".txt");
                }

                GitHelpers.CheckGitCommandAgainstGVFSRepo(this.Enlistment.RepoRoot, "status -u", expectedOutputStrings.ToArray());
            }
            finally
            {
                foreach (SafeFileHandle handle in openHandles)
                {
                    handle.Dispose();
                }
            }

            for (int i = 1; i <= 51; ++i)
            {
                this.GitCheckoutToDiscardChanges(TestParentFolderName + "/MaxFileListCount/" + updateFilesFolder + "/" + i.ToString() + ".txt");
                this.GitCleanFile(TestParentFolderName + "/MaxFileListCount/" + deleteFilesFolder + "/" + i.ToString() + ".txt");
            }

            this.GitStatusShouldBeClean(OldCommitId);
            this.GitCheckoutCommitId(NewFilesAndChangesCommitId);
            this.GitStatusShouldBeClean(NewFilesAndChangesCommitId);
        }

        private ProcessResult InvokeGitAgainstGVFSRepo(string command)
        {
            return GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, command);
        }

        private void GitStatusShouldBeClean(string commitId)
        {
            GitHelpers.CheckGitCommandAgainstGVFSRepo(
                this.Enlistment.RepoRoot,
                "status",
                "HEAD detached at " + commitId,
                "nothing to commit, working tree clean");
        }

        private void GitCleanFile(string gitPath)
        {
            GitHelpers.CheckGitCommandAgainstGVFSRepo(
                    this.Enlistment.RepoRoot,
                    "clean -f " + gitPath,
                    "Removing " + gitPath);
        }

        private void GitCheckoutToDiscardChanges(string gitPath)
        {
            GitHelpers.CheckGitCommandAgainstGVFSRepo(this.Enlistment.RepoRoot, "checkout -- " + gitPath);
        }

        private void GitCheckoutCommitId(string commitId)
        {
            this.InvokeGitAgainstGVFSRepo("checkout " + commitId).Errors.ShouldContain("HEAD is now at " + commitId);
        }

        private SafeFileHandle CreateFile(string path, FileShare shareMode)
        {
            return NativeMethods.CreateFile(
                path,
                (uint)FileAccess.Read,
                shareMode,
                IntPtr.Zero,
                FileMode.Open,
                (uint)FileAttributes.Normal,
                IntPtr.Zero);
        }

        private bool CanUpdateAndDeletePlaceholdersWithOpenHandles()
        {
            // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724429(v=vs.85).aspx
            FileVersionInfo kernel32Info = FileVersionInfo.GetVersionInfo(Path.Combine(Environment.SystemDirectory, "kernel32.dll"));

            // 16248 is first build with support - see 12658248 for details
            if (kernel32Info.FileBuildPart >= 16248)
            {
                return true;
            }

            return false;
        }
    }
}

================================================
FILE: GVFS/GVFS.FunctionalTests/Windows/Tools/RegistryHelper.cs
================================================
using Microsoft.Win32;

namespace GVFS.FunctionalTests.Windows.Tools
{
    public class RegistryHelper
    {
        public static object GetValueFromRegistry(RegistryHive registryHive, string key, string valueName, RegistryView view)
        {
            RegistryKey localKey = RegistryKey.OpenBaseKey(registryHive, view);
            RegistryKey localKeySub = localKey.OpenSubKey(key);

            object value = localKeySub == null ? null : localKeySub.GetValue(valueName);
            return value;
        }

        public static object GetValueFromRegistry(RegistryHive registryHive, string key, string valueName)
        {
            object value = GetValueFromRegistry(registryHive, key, valueName, RegistryView.Registry64);
            if (value == null)
            {
                value = GetValueFromRegistry(registryHive, key, valueName, RegistryView.Registry32);
            }

            return value;
        }

        public static bool TrySetDWordInRegistry(RegistryHive registryHive, string key, string valueName, uint value)
        {
            RegistryKey localKey = RegistryKey.OpenBaseKey(registryHive, RegistryView.Registry64);
            RegistryKey localKeySub = localKey.OpenSubKey(key, writable: true);

            if (localKeySub == null)
            {
                localKey = RegistryKey.OpenBaseKey(registryHive, RegistryView.Registry32);
                localKeySub = localKey.OpenSubKey(key, writable: true);
            }

            if (localKeySub == null)
            {
                return false;
            }

            localKeySub.SetValue(valueName, value, RegistryValueKind.DWord);
            return true;
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests.LockHolder/AcquireGVFSLock.cs
================================================
using CommandLine;
using GVFS.Common;
using GVFS.Common.NamedPipes;
using GVFS.Platform.Windows;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace GVFS.FunctionalTests.LockHolder
{
    public class AcquireGVFSLockVerb
    {
        private static string fullCommand = "GVFS.FunctionalTests.LockHolder";

        [Option(
            "skip-release-lock",
            Default = false,
            Required = false,
            HelpText = "Skip releasing the GVFS lock when exiting the program.")]
        public bool NoReleaseLock { get; set; }

        public void Execute()
        {
            string errorMessage;
            string enlistmentRoot;
            if (!TryGetGVFSEnlistmentRootImplementation(Environment.CurrentDirectory, out enlistmentRoot, out errorMessage))
            {
                throw new Exception("Unable to get GVFS Enlistment root: " + errorMessage);
            }

            string enlistmentPipename = GetNamedPipeNameImplementation(enlistmentRoot);

            AcquireLock(enlistmentPipename);

            Console.ReadLine();

            if (!this.NoReleaseLock)
            {
                ReleaseLock(enlistmentPipename, enlistmentRoot);
            }
        }

        private static bool TryGetGVFSEnlistmentRootImplementation(string directory, out string enlistmentRoot, out string errorMessage)
        {
            // Not able to use WindowsPlatform here - because of its dependency on WindowsIdentity (and also kernel32.dll).
            enlistmentRoot = null;

            string finalDirectory;
            if (!WindowsFileSystem.TryGetNormalizedPathImplementation(directory, out finalDirectory, out errorMessage))
            {
                return false;
            }

            const string dotGVFSRoot = ".gvfs";
            enlistmentRoot = Paths.GetRoot(finalDirectory, dotGVFSRoot);
            if (enlistmentRoot == null)
            {
                errorMessage = $"Failed to find the root directory for {dotGVFSRoot} in {finalDirectory}";
                return false;
            }

            return true;
        }

        private static string GetNamedPipeNameImplementation(string enlistmentRoot)
        {
            // Not able to use WindowsPlatform here - because of its dependency on WindowsIdentity (and also kernel32.dll).
            return "GVFS_" + enlistmentRoot.ToUpper().Replace(':', '_');
        }

        private static void AcquireLock(string enlistmentPipename)
        {
            using (NamedPipeClient pipeClient = new NamedPipeClient(enlistmentPipename))
            {
                if (!pipeClient.Connect())
                {
                    throw new Exception("The repo does not appear to be mounted. Use 'gvfs status' to check.");
                }

                int pid = Process.GetCurrentProcess().Id;

                string result;
                if (!GVFSLock.TryAcquireGVFSLockForProcess(
                    unattended: false,
                    pipeClient: pipeClient,
                    fullCommand: AcquireGVFSLockVerb.fullCommand,
                    pid: pid,
                    isElevated: false,
                    isConsoleOutputRedirectedToFile: false,
                    checkAvailabilityOnly: false,
                    gvfsEnlistmentRoot: null,
                    gitCommandSessionId: string.Empty,
                    result: out result))
                {
                    throw new Exception(result);
                }
            }
        }

        private static void ReleaseLock(string enlistmentPipename, string enlistmentRoot)
        {
            using (NamedPipeClient pipeClient = new NamedPipeClient(enlistmentPipename))
            {
                if (!pipeClient.Connect())
                {
                    throw new Exception("The repo does not appear to be mounted. Use 'gvfs status' to check.");
                }

                int pid = Process.GetCurrentProcess().Id;

                NamedPipeMessages.LockRequest request = new NamedPipeMessages.LockRequest(pid: pid, isElevated: false, checkAvailabilityOnly: false, parsedCommand: AcquireGVFSLockVerb.fullCommand, gitCommandSessionId: string.Empty);
                NamedPipeMessages.Message requestMessage = request.CreateMessage(NamedPipeMessages.ReleaseLock.Request);

                pipeClient.SendRequest(requestMessage);
                NamedPipeMessages.ReleaseLock.Response response = response = new NamedPipeMessages.ReleaseLock.Response(pipeClient.ReadResponse());
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.FunctionalTests.LockHolder/GVFS.FunctionalTests.LockHolder.csproj
================================================


  
    net471
    Exe
  

  
    
  

  
    
    
    
  




================================================
FILE: GVFS/GVFS.FunctionalTests.LockHolder/Program.cs
================================================
using CommandLine;

namespace GVFS.FunctionalTests.LockHolder
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Parser.Default.ParseArguments(args)
                    .WithParsed(acquireGVFSLock => acquireGVFSLock.Execute());
        }
    }
}


================================================
FILE: GVFS/GVFS.Hooks/GVFS.Hooks.csproj
================================================


  
    Exe
    net471
    true
  

  
    
  

  
    
    
      Common\ConsoleHelper.cs
    
    
      Common\Git\GitOid.cs
    
    
      Common\Git\GitVersion.cs
    
    
      Common\Git\LibGit2Exception.cs
    
    
      Common\Git\LibGit2Repo.cs
    
    
      Common\Git\LibGit2RepoInvoker.cs
    
    
      Common\GVFSConstants.cs
    
    
      Common\GVFSEnlistment.Shared.cs
    
    
      Common\GVFSLock.Shared.cs
    
    
      Common\NamedPipes\BrokenPipeException.cs
    
    
      Common\NamedPipes\LockNamedPipeMessages.cs
    
    
      Common\NamedPipes\UnstageNamedPipeMessages.cs
    
    
      Common\NamedPipes\HydrationStatusNamedPipeMessages.cs
    
    
      Common\NamedPipes\NamedPipeClient.cs
    
    
      Common\NamedPipes\NamedPipeStreamReader.cs
    
    
      Common\NamedPipes\NamedPipeStreamWriter.cs
    
    
      Common\NativeMethods.Shared.cs
    
    
      Common\Paths.Shared.cs
    
    
      Common\ProcessHelper.cs
    
    
      Common\ProcessResult.cs
    
    
      Common\WorktreeCommandParser.cs
    
    
      Common\SHA1Util.cs
    
    
      Common\Tracing\EventLevel.cs
    
    
      Common\Tracing\EventMetadata.cs
    
    
      Common\Tracing\EventOpcode.cs
    
    
      Common\Tracing\ITracer.cs
    
    
      Common\Tracing\ITracer.cs
    
    
      Common\Tracing\Keywords.cs
    
    
      Windows\WindowsFileSystem.Shared.cs
    
    
      Windows\WindowsPlatform.Shared.cs
    
  

  
    
    
    
    
      
      
    
  





================================================
FILE: GVFS/GVFS.Hooks/HooksPlatform/GVFSHooksPlatform.cs
================================================
using GVFS.Platform.Windows;

namespace GVFS.Hooks.HooksPlatform
{
    public static class GVFSHooksPlatform
    {
        public static bool IsElevated()
        {
            return WindowsPlatform.IsElevatedImplementation();
        }

        public static bool IsProcessActive(int processId)
        {
            // Since the hooks are children of the running git process, they will have permissions
            // to OpenProcess and don't need to try the expessive GetProcessById method to determine
            // if the process is still active.
            return WindowsPlatform.IsProcessActiveImplementation(processId, tryGetProcessById: false);
        }

        public static string GetNamedPipeName(string enlistmentRoot)
        {
            return WindowsPlatform.GetNamedPipeNameImplementation(enlistmentRoot);
        }

        public static bool IsConsoleOutputRedirectedToFile()
        {
            return WindowsPlatform.IsConsoleOutputRedirectedToFileImplementation();
        }

        public static bool TryGetGVFSEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage)
        {
            return WindowsPlatform.TryGetGVFSEnlistmentRootImplementation(directory, out enlistmentRoot, out errorMessage);
        }

        public static bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage)
        {
            return WindowsFileSystem.TryGetNormalizedPathImplementation(path, out normalizedPath, out errorMessage);
        }

        public static string GetGitGuiBlockedMessage()
        {
            return "To access the 'git gui' in a GVFS repo, please invoke 'git-gui.exe' instead.";
        }
    }
}


================================================
FILE: GVFS/GVFS.Hooks/KnownGitCommands.cs
================================================
using System.Collections.Generic;

namespace GVFS.Hooks
{
    internal static class KnownGitCommands
    {
        private static HashSet knownCommands = new HashSet()
        {
            "add",
            "am",
            "annotate",
            "apply",
            "archive",
            "bisect--helper",
            "blame",
            "branch",
            "bundle",
            "cat-file",
            "check-attr",
            "check-ignore",
            "check-mailmap",
            "check-ref-format",
            "checkout",
            "checkout-index",
            "cherry",
            "cherry-pick",
            "clean",
            "clone",
            "column",
            "commit",
            "commit-tree",
            "config",
            "count-objects",
            "credential",
            "describe",
            "diff",
            "diff-files",
            "diff-index",
            "diff-tree",
            "fast-export",
            "fetch",
            "fetch-pack",
            "fmt-merge-msg",
            "for-each-ref",
            "format-patch",
            "fsck",
            "fsck-objects",
            "gc",
            "get-tar-commit-id",
            "grep",
            "hash-object",
            "help",
            "index-pack",
            "init",
            "init-db",
            "interpret-trailers",
            "log",
            "ls-files",
            "ls-remote",
            "ls-tree",
            "mailinfo",
            "mailsplit",
            "merge",
            "merge-base",
            "merge-file",
            "merge-index",
            "merge-ours",
            "merge-recursive",
            "merge-recursive-ours",
            "merge-recursive-theirs",
            "merge-subtree",
            "merge-tree",
            "mktag",
            "mktree",
            "mv",
            "name-rev",
            "notes",
            "pack-objects",
            "pack-redundant",
            "pack-refs",
            "patch-id",
            "pickaxe",
            "prune",
            "prune-packed",
            "pull",
            "push",
            "read-tree",
            "rebase",
            "rebase--helper",
            "receive-pack",
            "reflog",
            "remote",
            "remote-ext",
            "remote-fd",
            "repack",
            "replace",
            "rerere",
            "reset",
            "restore",
            "rev-list",
            "rev-parse",
            "revert",
            "rm",
            "send-pack",
            "shortlog",
            "show",
            "show-branch",
            "show-ref",
            "stage",
            "status",
            "stripspace",
            "switch",
            "symbolic-ref",
            "tag",
            "unpack-file",
            "unpack-objects",
            "update-index",
            "update-ref",
            "update-server-info",
            "upload-archive",
            "upload-archive--writer",
            "var",
            "verify-commit",
            "verify-pack",
            "verify-tag",
            "version",
            "whatchanged",
            "worktree",
            "write-tree",

            // Externals
            "bisect",
            "filter-branch",
            "gui",
            "merge-octopus",
            "merge-one-file",
            "merge-resolve",
            "mergetool",
            "parse-remote",
            "quiltimport",
            "rebase",
            "submodule",
        };

        public static bool Contains(string gitCommand)
        {
            return knownCommands.Contains(gitCommand);
        }
    }
}


================================================
FILE: GVFS/GVFS.Hooks/Program.Unstage.cs
================================================
using GVFS.Common.NamedPipes;
using System;

namespace GVFS.Hooks
{
    /// 
    /// Partial class for unstage-related pre-command handling.
    /// Detects "restore --staged" and "checkout HEAD --" operations and sends
    /// a PrepareForUnstage message to the GVFS mount process so it can add
    /// staged files to ModifiedPaths before git clears skip-worktree.
    /// 
    public partial class Program
    {
        /// 
        /// Sends a PrepareForUnstage message to the GVFS mount process, which will
        /// add staged files matching the pathspec to ModifiedPaths so that git will
        /// clear skip-worktree and process them.
        /// 
        private static void SendPrepareForUnstageMessage(string command, string[] args)
        {
            UnstageCommandParser.PathspecResult pathspecResult = UnstageCommandParser.GetRestorePathspec(command, args);

            if (pathspecResult.Failed)
            {
                ExitWithError(
                    "VFS for Git was unable to determine the pathspecs for this unstage operation.",
                    "This can happen when --pathspec-from-file=- (stdin) is used.",
                    "",
                    "Instead, pass the paths directly on the command line:",
                    "  git restore --staged   ...");
                return;
            }

            // Build the message body. Format:
            //   null/empty          → all staged files (no pathspec)
            //   "path1\0path2"      → inline pathspecs (null-separated)
            //   "\nF\n"   → --pathspec-from-file (mount forwards to git)
            //   "\nFZ\n"  → --pathspec-from-file with --pathspec-file-nul
            // The leading \n distinguishes file-reference bodies from inline pathspecs.
            string body;
            if (pathspecResult.PathspecFromFile != null)
            {
                string prefix = pathspecResult.PathspecFileNul ? "\nFZ\n" : "\nF\n";
                body = prefix + pathspecResult.PathspecFromFile;

                // If there are also inline pathspecs, append them after another \n
                if (!string.IsNullOrEmpty(pathspecResult.InlinePathspecs))
                {
                    body += "\n" + pathspecResult.InlinePathspecs;
                }
            }
            else
            {
                body = pathspecResult.InlinePathspecs;
            }

            string message = string.IsNullOrEmpty(body)
                ? NamedPipeMessages.PrepareForUnstage.Request
                : NamedPipeMessages.PrepareForUnstage.Request + "|" + body;

            bool succeeded = false;
            string failureMessage = null;

            try
            {
                using (NamedPipeClient pipeClient = new NamedPipeClient(enlistmentPipename))
                {
                    if (pipeClient.Connect())
                    {
                        pipeClient.SendRequest(message);
                        string rawResponse = pipeClient.ReadRawResponse();
                        if (rawResponse != null && rawResponse.StartsWith(NamedPipeMessages.PrepareForUnstage.SuccessResult))
                        {
                            succeeded = true;
                        }
                        else
                        {
                            failureMessage = "GVFS mount process returned failure for PrepareForUnstage.";
                        }
                    }
                    else
                    {
                        failureMessage = "Unable to connect to GVFS mount process.";
                    }
                }
            }
            catch (Exception e)
            {
                failureMessage = "Exception communicating with GVFS: " + e.Message;
            }

            if (!succeeded && failureMessage != null)
            {
                ExitWithError(
                    failureMessage,
                    "The unstage operation cannot safely proceed because GVFS was unable to",
                    "prepare the staged files. This could lead to index corruption.",
                    "",
                    "To resolve:",
                    "  1. Run 'gvfs unmount' and 'gvfs mount' to reset the GVFS state",
                    "  2. Retry the restore --staged command",
                    "If the problem persists, run 'gvfs repair' or re-clone the enlistment.");
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Hooks/Program.Worktree.cs
================================================
using GVFS.Common;
using GVFS.Common.NamedPipes;
using GVFS.Hooks.HooksPlatform;
using System;
using System.IO;
using System.Linq;

namespace GVFS.Hooks
{
    public partial class Program
    {
        private static string GetWorktreeSubcommand(string[] args)
        {
            return WorktreeCommandParser.GetSubcommand(args);
        }

        /// 
        /// Gets a positional argument from git worktree subcommand args.
        /// For 'add': git worktree add [options] <path> [<commit-ish>]
        /// For 'remove': git worktree remove [options] <worktree>
        /// For 'move': git worktree move [options] <worktree> <new-path>
        /// 
        private static string GetWorktreePositionalArg(string[] args, int positionalIndex)
        {
            return WorktreeCommandParser.GetPositionalArg(args, positionalIndex);
        }

        private static string GetWorktreePathArg(string[] args)
        {
            return WorktreeCommandParser.GetPathArg(args);
        }

        private static void RunWorktreePreCommand(string[] args)
        {
            string subcommand = GetWorktreeSubcommand(args);
            switch (subcommand)
            {
                case "add":
                    BlockNestedWorktreeAdd(args);
                    break;
                case "remove":
                    HandleWorktreeRemove(args);
                    break;
                case "move":
                    // Unmount at old location before git moves the directory
                    UnmountWorktreeByArg(args);
                    break;
            }
        }

        private static void RunWorktreePostCommand(string[] args)
        {
            string subcommand = GetWorktreeSubcommand(args);
            switch (subcommand)
            {
                case "add":
                    MountNewWorktree(args);
                    break;
                case "remove":
                    RemountWorktreeIfRemoveFailed(args);
                    CleanupSkipCleanCheckMarker(args);
                    break;
                case "move":
                    // Mount at the new location after git moved the directory
                    MountMovedWorktree(args);
                    break;
            }
        }

        private static void UnmountWorktreeByArg(string[] args)
        {
            string worktreePath = GetWorktreePathArg(args);
            if (string.IsNullOrEmpty(worktreePath))
            {
                return;
            }

            string fullPath = ResolvePath(worktreePath);
            if (!UnmountWorktree(fullPath))
            {
                Console.Error.WriteLine(
                    $"error: failed to unmount worktree '{fullPath}'. Cannot proceed with move.");
                Environment.Exit(1);
            }
        }

        /// 
        /// If the worktree directory and its .git file both still exist after
        /// git worktree remove, the removal failed completely. Remount ProjFS
        /// so the worktree remains usable. If the remove partially succeeded
        /// (e.g., .git file or gitdir removed), don't attempt recovery.
        /// 
        private static void RemountWorktreeIfRemoveFailed(string[] args)
        {
            string worktreePath = GetWorktreePathArg(args);
            if (string.IsNullOrEmpty(worktreePath))
            {
                return;
            }

            string fullPath = ResolvePath(worktreePath);
            string dotGitFile = Path.Combine(fullPath, ".git");
            if (Directory.Exists(fullPath) && File.Exists(dotGitFile))
            {
                ProcessHelper.Run("gvfs", $"mount \"{fullPath}\"", redirectOutput: false);
            }
        }

        /// 
        /// Remove the skip-clean-check marker if it still exists after
        /// worktree remove completes (e.g., if the remove failed and the
        /// worktree gitdir was not deleted).
        /// 
        private static void CleanupSkipCleanCheckMarker(string[] args)
        {
            string worktreePath = GetWorktreePathArg(args);
            if (string.IsNullOrEmpty(worktreePath))
            {
                return;
            }

            string fullPath = ResolvePath(worktreePath);
            GVFSEnlistment.WorktreeInfo wtInfo = GVFSEnlistment.TryGetWorktreeInfo(fullPath);
            if (wtInfo != null)
            {
                string markerPath = Path.Combine(wtInfo.WorktreeGitDir, GVFSConstants.DotGit.SkipCleanCheckName);
                if (File.Exists(markerPath))
                {
                    File.Delete(markerPath);
                }
            }
        }

        /// 
        /// Block creating a worktree inside the primary VFS working directory
        /// or inside any other existing worktree.
        /// ProjFS cannot handle nested virtualization roots.
        /// 
        private static void BlockNestedWorktreeAdd(string[] args)
        {
            string worktreePath = GetWorktreePathArg(args);
            if (string.IsNullOrEmpty(worktreePath))
            {
                return;
            }

            string fullPath = ResolvePath(worktreePath);
            string primaryWorkingDir = Path.Combine(enlistmentRoot, GVFSConstants.WorkingDirectoryRootName);

            if (GVFSEnlistment.IsPathInsideDirectory(fullPath, primaryWorkingDir))
            {
                Console.Error.WriteLine(
                    $"error: cannot create worktree inside the VFS working directory.\n" +
                    $"Create the worktree outside of '{primaryWorkingDir}'.");
                Environment.Exit(1);
            }

            string gitDir = Path.Combine(primaryWorkingDir, ".git");
            foreach (string existingWorktreePath in GVFSEnlistment.GetKnownWorktreePaths(gitDir))
            {
                if (GVFSEnlistment.IsPathInsideDirectory(fullPath, existingWorktreePath))
                {
                    Console.Error.WriteLine(
                        $"error: cannot create worktree inside an existing worktree.\n" +
                        $"'{fullPath}' is inside worktree '{existingWorktreePath}'.");
                    Environment.Exit(1);
                }
            }
        }

        private static void HandleWorktreeRemove(string[] args)
        {
            string worktreePath = GetWorktreePathArg(args);
            if (string.IsNullOrEmpty(worktreePath))
            {
                return;
            }

            string fullPath = ResolvePath(worktreePath);
            GVFSEnlistment.WorktreeInfo wtInfo = GVFSEnlistment.TryGetWorktreeInfo(fullPath);

            bool hasForce = args.Any(a =>
                a.Equals("--force", StringComparison.OrdinalIgnoreCase) ||
                a.Equals("-f", StringComparison.OrdinalIgnoreCase));

            // Check if the worktree's GVFS mount is running by probing the pipe.
            bool isMounted = false;
            if (wtInfo != null)
            {
                string pipeName = GVFSHooksPlatform.GetNamedPipeName(enlistmentRoot) + wtInfo.PipeSuffix;
                using (NamedPipeClient pipeClient = new NamedPipeClient(pipeName))
                {
                    isMounted = pipeClient.Connect(500);
                }
            }

            if (!hasForce)
            {
                if (!isMounted)
                {
                    Console.Error.WriteLine(
                        $"error: worktree '{fullPath}' is not mounted.\n" +
                        $"Mount it with 'gvfs mount \"{fullPath}\"' or use 'git worktree remove --force'.");
                    Environment.Exit(1);
                }

                // Check for uncommitted changes while ProjFS is still mounted.
                ProcessResult statusResult = ProcessHelper.Run(
                    "git",
                    $"-C \"{fullPath}\" status --porcelain",
                    redirectOutput: true);

                if (!string.IsNullOrWhiteSpace(statusResult.Output))
                {
                    Console.Error.WriteLine(
                        $"error: worktree '{fullPath}' has uncommitted changes.\n" +
                        $"Use 'git worktree remove --force' to remove it anyway.");
                    Environment.Exit(1);
                }
            }
            else if (!isMounted)
            {
                // Force remove of unmounted worktree — nothing to unmount.
                return;
            }

            // Write a marker in the worktree gitdir that tells git.exe
            // to skip the cleanliness check during worktree remove.
            // We already did our own check above while ProjFS was alive.
            string skipCleanCheck = Path.Combine(wtInfo.WorktreeGitDir, GVFSConstants.DotGit.SkipCleanCheckName);
            File.WriteAllText(skipCleanCheck, string.Empty);

            // Unmount ProjFS before git deletes the worktree directory.
            if (!UnmountWorktree(fullPath, wtInfo) && !hasForce)
            {
                Console.Error.WriteLine(
                    $"error: failed to unmount worktree '{fullPath}'.\n" +
                    $"Use 'git worktree remove --force' to attempt removal anyway.");
                Environment.Exit(1);
            }
        }

        private static bool UnmountWorktree(string fullPath)
        {
            GVFSEnlistment.WorktreeInfo wtInfo = GVFSEnlistment.TryGetWorktreeInfo(fullPath);
            if (wtInfo == null)
            {
                return false;
            }

            return UnmountWorktree(fullPath, wtInfo);
        }

        private static bool UnmountWorktree(string fullPath, GVFSEnlistment.WorktreeInfo wtInfo)
        {
            ProcessResult result = ProcessHelper.Run("gvfs", $"unmount \"{fullPath}\"", redirectOutput: false);

            // After gvfs unmount exits, ProjFS handles may still be closing.
            // Wait briefly to allow the OS to release all handles before git
            // attempts to delete the worktree directory.
            System.Threading.Thread.Sleep(200);

            return result.ExitCode == 0;
        }

        private static void MountNewWorktree(string[] args)
        {
            string worktreePath = GetWorktreePathArg(args);
            if (string.IsNullOrEmpty(worktreePath))
            {
                return;
            }

            string fullPath = ResolvePath(worktreePath);

            // Verify worktree was created (check for .git file)
            string dotGitFile = Path.Combine(fullPath, ".git");
            if (File.Exists(dotGitFile))
            {
                string worktreeError;
                GVFSEnlistment.WorktreeInfo wtInfo = GVFSEnlistment.TryGetWorktreeInfo(fullPath, out worktreeError);
                if (worktreeError != null)
                {
                    Console.Error.WriteLine($"warning: failed to read worktree info for '{fullPath}': {worktreeError}");
                }

                // Store the primary enlistment root so mount/unmount can find
                // it without deriving from path structure assumptions.
                if (wtInfo?.WorktreeGitDir != null)
                {
                    string markerPath = Path.Combine(
                        wtInfo.WorktreeGitDir,
                        GVFSEnlistment.WorktreeInfo.EnlistmentRootFileName);
                    File.WriteAllText(markerPath, enlistmentRoot);
                }

                // Copy the primary's index to the worktree before checkout.
                // The primary index has all entries with correct skip-worktree
                // bits. If the worktree targets the same commit, checkout is
                // a no-op. If a different commit, git does an incremental
                // update — much faster than building 2.5M entries from scratch.
                if (wtInfo?.SharedGitDir != null)
                {
                    string primaryIndex = Path.Combine(wtInfo.SharedGitDir, "index");
                    string worktreeIndex = Path.Combine(wtInfo.WorktreeGitDir, "index");
                    if (File.Exists(primaryIndex) && !File.Exists(worktreeIndex))
                    {
                        // Copy to a temp file first, then rename atomically.
                        // The primary index may be updated concurrently by the
                        // running mount; a direct copy risks a torn read on
                        // large indexes (200MB+ in some large repos).
                        // Note: mirrors PhysicalFileSystem.TryCopyToTempFileAndRename
                        // but that method requires GVFSPlatform which is not
                        // available in the hooks process.
                        string tempIndex = worktreeIndex + ".tmp";
                        try
                        {
                            File.Copy(primaryIndex, tempIndex, overwrite: true);
                            File.Move(tempIndex, worktreeIndex);
                        }
                        catch
                        {
                            try { File.Delete(tempIndex); } catch { }
                            throw;
                        }
                    }
                }

                // Run checkout to reconcile the index with the worktree's HEAD.
                // With a pre-populated index this is fast (incremental diff).
                // Override core.virtualfilesystem with an empty script that
                // returns .gitattributes so it gets materialized while all
                // other entries keep skip-worktree set.
                //
                // Disable hooks via core.hookspath — the worktree's GVFS mount
                // doesn't exist yet, so post-index-change would fail trying
                // to connect to a pipe that hasn't been created.
                string emptyVfsHook = Path.Combine(fullPath, ".vfs-empty-hook");
                try
                {
                    File.WriteAllText(emptyVfsHook, "#!/bin/sh\nprintf \".gitattributes\\n\"\n");
                    string emptyVfsHookGitPath = emptyVfsHook.Replace('\\', '/');

                    ProcessHelper.Run(
                        "git",
                        $"-C \"{fullPath}\" -c core.virtualfilesystem=\"'{emptyVfsHookGitPath}'\" -c core.hookspath= checkout -f HEAD",
                        redirectOutput: false);
                }
                finally
                {
                    File.Delete(emptyVfsHook);
                }

                // Hydrate .gitattributes — copy from the primary enlistment.
                if (wtInfo?.SharedGitDir != null)
                {
                    string primarySrc = Path.GetDirectoryName(wtInfo.SharedGitDir);
                    string primaryGitattributes = Path.Combine(primarySrc, ".gitattributes");
                    string worktreeGitattributes = Path.Combine(fullPath, ".gitattributes");
                    if (File.Exists(primaryGitattributes) && !File.Exists(worktreeGitattributes))
                    {
                        File.Copy(primaryGitattributes, worktreeGitattributes);
                    }
                }

                // Now mount GVFS — the index exists for GitIndexProjection
                ProcessHelper.Run("gvfs", $"mount \"{fullPath}\"", redirectOutput: false);
            }
        }

        private static void MountMovedWorktree(string[] args)
        {
            // git worktree move  
            // After move, the worktree is at 
            string newPath = GetWorktreePositionalArg(args, 1);
            if (string.IsNullOrEmpty(newPath))
            {
                return;
            }

            string fullPath = ResolvePath(newPath);

            string dotGitFile = Path.Combine(fullPath, ".git");
            if (File.Exists(dotGitFile))
            {
                ProcessHelper.Run("gvfs", $"mount \"{fullPath}\"", redirectOutput: false);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Hooks/Program.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Hooks.HooksPlatform;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace GVFS.Hooks
{
    public partial class Program
    {
        private const string PreCommandHook = "pre-command";
        private const string PostCommandHook = "post-command";

        private const string GitPidArg = "--git-pid=";
        private const int InvalidProcessId = -1;

        private const int PostCommandSpinnerDelayMs = 500;

        private static string enlistmentRoot;
        private static string enlistmentPipename;
        private static string normalizedCurrentDirectory;
        private static Random random = new Random();

        private delegate void LockRequestDelegate(bool unattended, string[] args, int pid, NamedPipeClient pipeClient);

        public static void Main(string[] args)
        {
            try
            {
                if (args.Length < 2)
                {
                    ExitWithError("Usage: gvfs.hooks.exe --git-pid=   []");
                }

                bool unattended = GVFSEnlistment.IsUnattended(tracer: null);

                string errorMessage;
                if (!GVFSHooksPlatform.TryGetNormalizedPath(Environment.CurrentDirectory, out normalizedCurrentDirectory, out errorMessage))
                {
                    ExitWithError($"Failed to determine final path for current directory {Environment.CurrentDirectory}. Error: {errorMessage}");
                }

                if (!GVFSHooksPlatform.TryGetGVFSEnlistmentRoot(Environment.CurrentDirectory, out enlistmentRoot, out errorMessage))
                {
                    // Nothing to hook when being run outside of a GVFS repo.
                    // This is also the path when run with --git-dir outside of a GVFS directory, see Story #949665
                    Environment.Exit(0);
                }

                enlistmentPipename = GVFSHooksPlatform.GetNamedPipeName(enlistmentRoot);

                // If running inside a worktree, append a worktree-specific
                // suffix to the pipe name so hooks communicate with the
                // correct GVFS mount instance.
                string worktreeSuffix = GVFSEnlistment.GetWorktreePipeSuffix(normalizedCurrentDirectory);
                if (worktreeSuffix != null)
                {
                    enlistmentPipename += worktreeSuffix;
                }

                switch (GetHookType(args))
                {
                    case PreCommandHook:
                        CheckForLegalCommands(args);
                        RunLockRequest(args, unattended, AcquireGVFSLockForProcess);
                        RunPreCommands(args);
                        break;

                    case PostCommandHook:
                        // Do not release the lock if this request was only run to see if it could acquire the GVFSLock,
                        // but did not actually acquire it.
                        if (!CheckGVFSLockAvailabilityOnly(args))
                        {
                            RunLockRequest(args, unattended, ReleaseGVFSLock);
                        }

                        RunPostCommands(args);
                        break;

                    default:
                        ExitWithError("Unrecognized hook: " + string.Join(" ", args));
                        break;
                }
            }
            catch (Exception ex)
            {
                ExitWithError("Unexpected exception: " + ex.ToString());
            }
        }

        private static void RunPreCommands(string[] args)
        {
            string command = GetGitCommand(args);
            switch (command)
            {
                case "fetch":
                case "pull":
                    ProcessHelper.Run("gvfs", "prefetch --commits", redirectOutput: false);
                    break;
                case "status":
                    /* If status is being run to serialize for caching, or if --porcelain is specified, skip the health display */
                    if (!ArgsBlockHydrationStatus(args)
                        && ConfigurationAllowsHydrationStatus())
                    {
                        TryDisplayCachedHydrationStatus();
                    }
                    break;
                case "restore":
                case "checkout":
                    if (UnstageCommandParser.IsUnstageOperation(command, args))
                    {
                        SendPrepareForUnstageMessage(command, args);
                    }
                    break;
                case "worktree":
                    RunWorktreePreCommand(args);
                    break;
            }
        }

        private static bool ArgsBlockHydrationStatus(string[] args)
        {
            return args.Any(arg =>
                arg.StartsWith("--serialize", StringComparison.OrdinalIgnoreCase)
                || arg.StartsWith("--porcelain", StringComparison.OrdinalIgnoreCase)
                || arg.Equals("--short", StringComparison.OrdinalIgnoreCase)
                || HasShortFlag(arg, "s"));
        }

        private static void RunPostCommands(string[] args)
        {
            string command = GetGitCommand(args);
            switch (command)
            {
                case "worktree":
                    RunWorktreePostCommand(args);
                    break;
            }
        }

        private static string ResolvePath(string path)
        {
            return Path.GetFullPath(
                Path.IsPathRooted(path)
                    ? path
                    : Path.Combine(normalizedCurrentDirectory, path));
        }

        private static bool HasShortFlag(string arg, string flag)
        {
            return arg.StartsWith("-") && !arg.StartsWith("--") && arg.Substring(1).Contains(flag);
        }

        private static bool ConfigurationAllowsHydrationStatus()
        {
            using (LibGit2RepoInvoker repo = new LibGit2RepoInvoker(NullTracer.Instance, normalizedCurrentDirectory))
            {
                return repo.GetConfigBoolOrDefault(GVFSConstants.GitConfig.ShowHydrationStatus, GVFSConstants.GitConfig.ShowHydrationStatusDefault);
            }
        }

        /// 
        /// Query the mount process for the cached hydration summary via named pipe.
        /// The entire operation (connect + send + receive + parse) is bounded to
        /// 100ms via Task.Wait. Exits silently on any failure — this must never block git status.
        /// 
        private static void TryDisplayCachedHydrationStatus()
        {
            const int HydrationStatusTimeoutMs = 100;
            const int ConnectTimeoutMs = 50;

            try
            {
                Task task = Task.Run(() =>
                {
                    using (NamedPipeClient pipeClient = new NamedPipeClient(enlistmentPipename))
                    {
                        if (!pipeClient.Connect(timeoutMilliseconds: ConnectTimeoutMs))
                        {
                            return null;
                        }

                        pipeClient.SendRequest(new NamedPipeMessages.Message(NamedPipeMessages.HydrationStatus.Request, null));
                        NamedPipeMessages.Message response = pipeClient.ReadResponse();

                        if (response.Header == NamedPipeMessages.HydrationStatus.SuccessResult
                            && NamedPipeMessages.HydrationStatus.Response.TryParse(response.Body, out NamedPipeMessages.HydrationStatus.Response status))
                        {
                            return status.ToDisplayMessage();
                        }

                        return null;
                    }
                });

                // Hard outer timeout — if the task hasn't completed (e.g., ReadResponse
                // blocked on a stalled mount process), we abandon it. The orphaned thread
                // is cleaned up when the hook process exits immediately after.
                if (task.Wait(HydrationStatusTimeoutMs) && task.Status == TaskStatus.RanToCompletion && task.Result != null)
                {
                    Console.WriteLine(task.Result);
                }
            }
            catch (Exception)
            {
                // Silently ignore — never block git status for hydration display
            }
        }

        private static void ExitWithError(params string[] messages)
        {
            foreach (string message in messages)
            {
                Console.WriteLine(message);
            }

            Environment.Exit(1);
        }

        private static void CheckForLegalCommands(string[] args)
        {
            string command = GetGitCommand(args);
            switch (command)
            {
                case "gui":
                    ExitWithError(GVFSHooksPlatform.GetGitGuiBlockedMessage());
                    break;
            }
        }

        private static void RunLockRequest(string[] args, bool unattended, LockRequestDelegate requestToRun)
        {
            try
            {
                if (ShouldLock(args))
                {
                    using (NamedPipeClient pipeClient = new NamedPipeClient(enlistmentPipename))
                    {
                        if (!pipeClient.Connect())
                        {
                            ExitWithError("The repo does not appear to be mounted. Use 'gvfs status' to check.");
                        }

                        int pid = GetParentPid(args);
                        if (pid == Program.InvalidProcessId ||
                            !GVFSHooksPlatform.IsProcessActive(pid))
                        {
                            ExitWithError("GVFS.Hooks: Unable to find parent git.exe process " + "(PID: " + pid + ").");
                        }

                        requestToRun(unattended, args, pid, pipeClient);
                    }
                }
            }
            catch (Exception exc)
            {
                ExitWithError(
                    "Unable to initialize Git command.",
                    "Ensure that GVFS is running.",
                    exc.ToString());
            }
        }

        private static string GenerateFullCommand(string[] args)
        {
            return "git " + string.Join(" ", args.Skip(1).Where(arg => !arg.StartsWith(GitPidArg)));
        }

        private static int GetParentPid(string[] args)
        {
            string pidArg = args.SingleOrDefault(x => x.StartsWith(GitPidArg));
            if (!string.IsNullOrEmpty(pidArg))
            {
                pidArg = pidArg.Remove(0, GitPidArg.Length);
                int pid;
                if (int.TryParse(pidArg, out pid))
                {
                    return pid;
                }
            }

            ExitWithError(
                "Git did not supply the process Id.",
                "Ensure you are using the correct version of the git client.");

            return Program.InvalidProcessId;
        }

        private static void AcquireGVFSLockForProcess(bool unattended, string[] args, int pid, NamedPipeClient pipeClient)
        {
            string result;
            bool checkGvfsLockAvailabilityOnly = CheckGVFSLockAvailabilityOnly(args);
            string fullCommand = GenerateFullCommand(args);
            string gitCommandSessionId = GetGitCommandSessionId();

            if (!GVFSLock.TryAcquireGVFSLockForProcess(
                    unattended,
                    pipeClient,
                    fullCommand,
                    pid,
                    GVFSHooksPlatform.IsElevated(),
                    isConsoleOutputRedirectedToFile: GVFSHooksPlatform.IsConsoleOutputRedirectedToFile(),
                    checkAvailabilityOnly: checkGvfsLockAvailabilityOnly,
                    gvfsEnlistmentRoot: null,
                    gitCommandSessionId: gitCommandSessionId,
                    result: out result))
            {
                ExitWithError(result);
            }
        }

        private static void ReleaseGVFSLock(bool unattended, string[] args, int pid, NamedPipeClient pipeClient)
        {
            string fullCommand = GenerateFullCommand(args);

            GVFSLock.ReleaseGVFSLock(
                unattended,
                pipeClient,
                fullCommand,
                pid,
                GVFSHooksPlatform.IsElevated(),
                GVFSHooksPlatform.IsConsoleOutputRedirectedToFile(),
                response =>
                {
                    if (response == null || response.ResponseData == null)
                    {
                        Console.WriteLine("\nError communicating with GVFS: Run 'gvfs status' to check the status of your repo");
                    }
                    else if (response.ResponseData.HasFailures)
                    {
                        if (response.ResponseData.FailureCountExceedsMaxFileNames)
                        {
                            Console.WriteLine(
                                "\nGVFS failed to update {0} files, run 'git status' to check the status of files in the repo",
                                response.ResponseData.FailedToDeleteCount + response.ResponseData.FailedToUpdateCount);
                        }
                        else
                        {
                            string deleteFailuresMessage = BuildUpdatePlaceholderFailureMessage(response.ResponseData.FailedToDeleteFileList, "delete", "git clean -f ");
                            if (deleteFailuresMessage.Length > 0)
                            {
                                Console.WriteLine(deleteFailuresMessage);
                            }

                            string updateFailuresMessage = BuildUpdatePlaceholderFailureMessage(response.ResponseData.FailedToUpdateFileList, "update", "git checkout -- ");
                            if (updateFailuresMessage.Length > 0)
                            {
                                Console.WriteLine(updateFailuresMessage);
                            }
                        }
                    }
                },
                gvfsEnlistmentRoot: null,
                waitingMessage: "Waiting for GVFS to parse index and update placeholder files",
                spinnerDelay: PostCommandSpinnerDelayMs);
        }

        private static bool CheckGVFSLockAvailabilityOnly(string[] args)
        {
            try
            {
                // Don't acquire the GVFS lock if the git command is not acquiring locks.
                // This enables tools to run status commands without to the index and
                // blocking other commands from running. The git argument
                // "--no-optional-locks" results in a 'negative'
                // value GIT_OPTIONAL_LOCKS environment variable.
                return GetGitCommand(args).Equals("status", StringComparison.OrdinalIgnoreCase) &&
                    (args.Any(arg => arg.Equals("--no-lock-index", StringComparison.OrdinalIgnoreCase)) ||
                    IsGitEnvVarDisabled("GIT_OPTIONAL_LOCKS"));
            }
            catch (Exception e)
            {
                ExitWithError("Failed to determine if GVFS should aquire GVFS lock: " + e.ToString());
            }

            return false;
        }

        private static string BuildUpdatePlaceholderFailureMessage(List fileList, string failedOperation, string recoveryCommand)
        {
            if (fileList == null || fileList.Count == 0)
            {
                return string.Empty;
            }

            fileList.Sort(StringComparer.OrdinalIgnoreCase);
            string message = "\nGVFS was unable to " + failedOperation + " the following files. To recover, close all handles to the files and run these commands:";
            message += string.Concat(fileList.Select(x => "\n    " + recoveryCommand + x));
            return message;
        }

        private static bool IsGitEnvVarDisabled(string envVar)
        {
            string envVarValue = Environment.GetEnvironmentVariable(envVar);
            if (!string.IsNullOrEmpty(envVarValue))
            {
                if (string.Equals(envVarValue, "false", StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(envVarValue, "no", StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(envVarValue, "off", StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(envVarValue, "0", StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
            }

            return false;
        }

        private static bool ShouldLock(string[] args)
        {
            string gitCommand = GetGitCommand(args);

            switch (gitCommand)
            {
                // Keep these alphabetically sorted
                case "blame":
                case "branch":
                case "cat-file":
                case "check-attr":
                case "check-ignore":
                case "check-mailmap":
                case "commit-graph":
                case "config":
                case "credential":
                case "diff":
                case "diff-files":
                case "diff-index":
                case "diff-tree":
                case "difftool":
                case "fetch":
                case "for-each-ref":
                case "help":
                case "hash-object":
                case "index-pack":
                case "log":
                case "ls-files":
                case "ls-tree":
                case "merge-base":
                case "multi-pack-index":
                case "name-rev":
                case "pack-objects":
                case "push":
                case "remote":
                case "rev-list":
                case "rev-parse":
                case "show":
                case "show-ref":
                case "symbolic-ref":
                case "tag":
                case "unpack-objects":
                case "update-ref":
                case "version":
                case "web--browse":
                    return false;

                /*
                 * There are several git commands that are "unsupported" in virtualized (VFS4G)
                 * enlistments that are blocked by git. Usually, these are blocked before they acquire
                 * a GVFSLock, but the submodule command is different, and is blocked after acquiring the
                 * GVFS lock. This can cause issues if another action is attempting to create placeholders.
                 * As we know the submodule command is a no-op, allow it to proceed without acquiring the
                 * GVFSLock. I have filed issue #1164 to track having git block all unsupported commands
                 * before calling the pre-command hook.
                 */
                case "submodule":
                    return false;
            }

            if (gitCommand == "reset" && args.Contains("--soft"))
            {
                return false;
            }

            if (!KnownGitCommands.Contains(gitCommand) &&
                IsAlias(gitCommand))
            {
                return false;
            }

            return true;
        }

        private static string GetHookType(string[] args)
        {
            return args[0].ToLowerInvariant();
        }

        private static string GetGitCommand(string[] args)
        {
            string command = args[1].ToLowerInvariant();
            if (command.StartsWith("git-"))
            {
                command = command.Substring(4);
            }

            return command;
        }

        private static bool IsAlias(string command)
        {
            ProcessResult result = ProcessHelper.Run("git", "config --get alias." + command);

            return !string.IsNullOrEmpty(result.Output);
        }

        private static string GetGitCommandSessionId()
        {
            try
            {
                return Environment.GetEnvironmentVariable("GIT_TR2_PARENT_SID", EnvironmentVariableTarget.Process) ?? string.Empty;
            }
            catch (Exception)
            {
                return string.Empty;
            }
        }

        private static bool IsUpgradeMessageDeterministic()
        {
            try
            {
                return Environment.GetEnvironmentVariable("GVFS_UPGRADE_DETERMINISTIC", EnvironmentVariableTarget.Process) != null;
            }
            catch (Exception)
            {
                return false;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Hooks/UnstageCommandParser.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;

namespace GVFS.Hooks
{
    /// 
    /// Pure parsing logic for detecting and extracting pathspecs from
    /// git unstage commands. Separated from Program.Unstage.cs so it
    /// can be linked into the unit test project without pulling in the
    /// rest of the Hooks assembly.
    /// 
    public static class UnstageCommandParser
    {
        /// 
        /// Result of parsing pathspec arguments from a git unstage command.
        /// 
        public class PathspecResult
        {
            /// Null-separated inline pathspecs, or empty for all staged files.
            public string InlinePathspecs { get; set; }

            /// Path to a --pathspec-from-file, or null if not specified.
            public string PathspecFromFile { get; set; }

            /// Whether --pathspec-file-nul was specified.
            public bool PathspecFileNul { get; set; }

            /// True if parsing failed and the command should be blocked.
            public bool Failed { get; set; }
        }

        /// 
        /// Detects whether the git command is an unstage operation that may need
        /// special handling for VFS projections.
        /// Matches: "restore --staged", "restore -S", "checkout HEAD --"
        /// 
        public static bool IsUnstageOperation(string command, string[] args)
        {
            if (command == "restore")
            {
                return args.Any(arg =>
                    arg.Equals("--staged", StringComparison.OrdinalIgnoreCase) ||
                    // -S is --staged; char overload of IndexOf is case-sensitive,
                    // which is required because lowercase -s means --source
                    (arg.StartsWith("-") && !arg.StartsWith("--") && arg.IndexOf('S') >= 0));
            }

            if (command == "checkout")
            {
                // "checkout HEAD -- " is an unstage+restore operation.
                // TODO: investigate whether "checkout  -- " also
                // needs PrepareForUnstage protection. It re-stages files (sets index to
                // a different tree-ish) and could hit the same skip-worktree interference
                // if the target files were staged by cherry-pick -n / merge and aren't in
                // ModifiedPaths. Currently scoped to HEAD only as the common unstage case.
                bool hasHead = args.Any(arg => arg.Equals("HEAD", StringComparison.OrdinalIgnoreCase));
                bool hasDashDash = args.Any(arg => arg == "--");
                return hasHead && hasDashDash;
            }

            return false;
        }

        /// 
        /// Extracts pathspec arguments from a restore/checkout unstage command.
        /// Returns a  containing either inline pathspecs,
        /// a --pathspec-from-file reference, or a failure indicator.
        ///
        /// When --pathspec-from-file is specified, the file path is returned so the
        /// caller can forward it through IPC to the mount process, which passes it
        /// to git diff --cached --pathspec-from-file.
        /// 
        public static PathspecResult GetRestorePathspec(string command, string[] args)
        {
            // args[0] = hook type, args[1] = git command, rest are arguments
            List paths = new List();
            bool pastDashDash = false;
            bool skipNext = false;
            bool isCheckout = command == "checkout";

            // For checkout, the first non-option arg before -- is the tree-ish (e.g. HEAD),
            // not a pathspec. Track whether we've consumed it.
            bool treeishConsumed = false;

            // --pathspec-from-file support: collect the file path and nul flag
            string pathspecFromFile = null;
            bool pathspecFileNul = false;
            bool captureNextAsPathspecFile = false;

            for (int i = 2; i < args.Length; i++)
            {
                string arg = args[i];

                if (captureNextAsPathspecFile)
                {
                    pathspecFromFile = arg;
                    captureNextAsPathspecFile = false;
                    continue;
                }

                if (skipNext)
                {
                    skipNext = false;
                    continue;
                }

                if (arg.StartsWith("--git-pid="))
                    continue;

                // Capture --pathspec-from-file value
                if (arg.StartsWith("--pathspec-from-file="))
                {
                    pathspecFromFile = arg.Substring("--pathspec-from-file=".Length);
                    continue;
                }

                if (arg == "--pathspec-from-file")
                {
                    captureNextAsPathspecFile = true;
                    continue;
                }

                if (arg == "--pathspec-file-nul")
                {
                    pathspecFileNul = true;
                    continue;
                }

                if (arg == "--")
                {
                    pastDashDash = true;
                    continue;
                }

                if (!pastDashDash && arg.StartsWith("-"))
                {
                    // For restore: --source and -s take a following argument
                    if (!isCheckout &&
                        (arg == "--source" || arg == "-s"))
                    {
                        skipNext = true;
                    }

                    continue;
                }

                // For checkout, the first positional arg before -- is the tree-ish
                if (isCheckout && !pastDashDash && !treeishConsumed)
                {
                    treeishConsumed = true;
                    continue;
                }

                paths.Add(arg);
            }

            // stdin ("-") is not supported in hook context — the hook's stdin
            // is not connected to the user's terminal
            if (pathspecFromFile == "-")
            {
                return new PathspecResult { Failed = true };
            }

            return new PathspecResult
            {
                InlinePathspecs = paths.Count > 0 ? string.Join("\0", paths) : "",
                PathspecFromFile = pathspecFromFile,
                PathspecFileNul = pathspecFileNul,
            };
        }
    }
}


================================================
FILE: GVFS/GVFS.Installers/GVFS.Installers.csproj
================================================


  
    net471
    false
    $(RepoOutPath)GVFS.Payload\bin\$(Configuration)\win-x64\
  

  
    
    
  

  
    
    
  

  
    
  

  
    
    
    
  

  
    
      Microsoft400
      false
    
  

  
    
  

  
    
      
    
    
  




================================================
FILE: GVFS/GVFS.Installers/Setup.iss
================================================
; This script requires Inno Setup Compiler 5.5.9 or later to compile
; The Inno Setup Compiler (and IDE) can be found at http://www.jrsoftware.org/isinfo.php

; General documentation on how to use InnoSetup scripts: http://www.jrsoftware.org/ishelp/index.php

#define MyAppName "VFS for Git"
#define MyAppInstallerVersion GetFileVersion(LayoutDir + "\GVFS.exe")
#define MyAppPublisher "Microsoft"
#define MyAppPublisherURL "http://www.microsoft.com"
#define MyAppURL "https://github.com/microsoft/VFSForGit"
#define MyAppExeName "GVFS.exe"
#define EnvironmentKey "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
#define FileSystemKey "SYSTEM\CurrentControlSet\Control\FileSystem"
#define GvFltAutologgerKey "SYSTEM\CurrentControlSet\Control\WMI\Autologger\Microsoft-Windows-Git-Filter-Log"
#define GVFSConfigFileName "gvfs.config"
#define GVFSStatuscacheTokenFileName "EnableGitStatusCacheToken.dat"
#define ServiceName "GVFS.Service"

[Setup]
AppId={{489CA581-F131-4C28-BE04-4FB178933E6D}
AppName={#MyAppName}
AppVersion={#MyAppInstallerVersion}
VersionInfoVersion={#MyAppInstallerVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppPublisherURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
AppCopyright=Copyright (c) Microsoft 2021
BackColor=clWhite
BackSolid=yes
DefaultDirName={pf}\{#MyAppName}
OutputBaseFilename=SetupGVFS.{#GVFSVersion}
OutputDir=Setup
Compression=lzma2
InternalCompressLevel=ultra64
SolidCompression=yes
MinVersion=10.0.14374
DisableDirPage=yes
DisableReadyPage=yes
SetupIconFile="{#LayoutDir}\GitVirtualFileSystem.ico"
ArchitecturesInstallIn64BitMode=x64compatible
ArchitecturesAllowed=x64compatible
WizardImageStretch=no
WindowResizable=no
CloseApplications=yes
ChangesEnvironment=yes
RestartIfNeededByRun=yes

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl";

[Types]
Name: "full"; Description: "Full installation"; Flags: iscustom;

[Components]

[InstallDelete]
; Delete old dependencies from VS 2015 VC redistributables
Type: files; Name: "{app}\ucrtbase.dll"

[Files]
DestDir: "{app}"; Flags: ignoreversion recursesubdirs; Source:"{#LayoutDir}\*"
DestDir: "{app}"; Flags: ignoreversion; Source:"{#LayoutDir}\GVFS.Service.exe"; AfterInstall: InstallGVFSService

[Dirs]
Name: "{app}\ProgramData\{#ServiceName}"; Permissions: users-readexec

[UninstallDelete]
; Deletes the entire installation directory, including files and subdirectories
Type: filesandordirs; Name: "{app}";
Type: filesandordirs; Name: "{commonappdata}\GVFS\GVFS.Upgrade";

[Registry]
Root: HKLM; Subkey: "{#EnvironmentKey}"; \
    ValueType: expandsz; ValueName: "PATH"; ValueData: "{olddata};{app}"; \
    Check: NeedsAddPath(ExpandConstant('{app}'))

Root: HKLM; Subkey: "{#FileSystemKey}"; \
    ValueType: dword; ValueName: "NtfsEnableDetailedCleanupResults"; ValueData: "1"; \
    Check: IsWindows10VersionPriorToCreatorsUpdate

Root: HKLM; SubKey: "{#GvFltAutologgerKey}"; Flags: deletekey

[Code]
var
  ExitCode: Integer;

function NeedsAddPath(Param: string): boolean;
var
  OrigPath: string;
begin
  if not RegQueryStringValue(HKEY_LOCAL_MACHINE,
    '{#EnvironmentKey}',
    'PATH', OrigPath)
  then begin
    Result := True;
    exit;
  end;
  // look for the path with leading and trailing semicolon
  // Pos() returns 0 if not found
  Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0;
end;

function IsWindows10VersionPriorToCreatorsUpdate(): Boolean;
var
  Version: TWindowsVersion;
begin
  GetWindowsVersionEx(Version);
  Result := (Version.Major = 10) and (Version.Minor = 0) and (Version.Build < 15063);
end;

procedure RemovePath(Path: string);
var
  Paths: string;
  PathMatchIndex: Integer;
begin
  if not RegQueryStringValue(HKEY_LOCAL_MACHINE, '{#EnvironmentKey}', 'Path', Paths) then
    begin
      Log('PATH not found');
    end
  else
    begin
      Log(Format('PATH is [%s]', [Paths]));

      PathMatchIndex := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';');
      if PathMatchIndex = 0 then
        begin
          Log(Format('Path [%s] not found in PATH', [Path]));
        end
      else
        begin
          Delete(Paths, PathMatchIndex - 1, Length(Path) + 1);
          Log(Format('Path [%s] removed from PATH => [%s]', [Path, Paths]));

          if RegWriteStringValue(HKEY_LOCAL_MACHINE, '{#EnvironmentKey}', 'Path', Paths) then
            begin
              Log('PATH written');
            end
          else
            begin
              Log('Error writing PATH');
            end;
        end;
    end;
end;

procedure StopService(ServiceName: string);
var
  ResultCode: integer;
begin
  Log('StopService: stopping: ' + ServiceName);
  // ErrorCode 1060 means service not installed, 1062 means service not started
  if not Exec(ExpandConstant('{sys}\SC.EXE'), 'stop ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode <> 1060) and (ResultCode <> 1062) then
    begin
      RaiseException('Fatal: Could not stop service: ' + ServiceName);
    end;
end;

procedure UninstallService(ServiceName: string; ShowProgress: boolean);
var
  ResultCode: integer;
begin
  if Exec(ExpandConstant('{sys}\SC.EXE'), 'query ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode <> 1060) then
    begin
      Log('UninstallService: uninstalling service: ' + ServiceName);
      if (ShowProgress) then
        begin
          WizardForm.StatusLabel.Caption := 'Uninstalling service: ' + ServiceName;
          WizardForm.ProgressGauge.Style := npbstMarquee;
        end;

      try
        StopService(ServiceName);

        if not Exec(ExpandConstant('{sys}\SC.EXE'), 'delete ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) or (ResultCode <> 0) then
          begin
            Log('UninstallService: Could not uninstall service: ' + ServiceName);
            RaiseException('Fatal: Could not uninstall service: ' + ServiceName);
          end;

        if (ShowProgress) then
          begin
            WizardForm.StatusLabel.Caption := 'Waiting for pending ' + ServiceName + ' deletion to complete. This may take a while.';
          end;

      finally
        if (ShowProgress) then
          begin
            WizardForm.ProgressGauge.Style := npbstNormal;
          end;
      end;

    end;
end;

procedure WriteOnDiskVersion16CapableFile();
var
  FilePath: string;
begin
  FilePath := ExpandConstant('{app}\OnDiskVersion16CapableInstallation.dat');
  if not FileExists(FilePath) then
    begin
      Log('WriteOnDiskVersion16CapableFile: Writing file ' + FilePath);
      SaveStringToFile(FilePath, '', False);
    end
end;

procedure InstallGVFSService();
var
  ResultCode: integer;
  StatusText: string;
  InstallSuccessful: Boolean;
begin
  InstallSuccessful := False;

  StatusText := WizardForm.StatusLabel.Caption;
  WizardForm.StatusLabel.Caption := 'Installing GVFS.Service.';
  WizardForm.ProgressGauge.Style := npbstMarquee;

  // Spaces after the equal signs are REQUIRED.
  // https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/sc-create#remarks
  try
    // We must add additional quotes to the binPath to ensure that they survive argument parsing.
    // Without quotes, sc.exe will try to start a file located at C:\Program if it exists.
    if Exec(ExpandConstant('{sys}\SC.EXE'), ExpandConstant('create GVFS.Service binPath= "\"{app}\GVFS.Service.exe\"" start= auto'), '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0) then
      begin
        if Exec(ExpandConstant('{sys}\SC.EXE'), 'failure GVFS.Service reset= 30 actions= restart/10/restart/5000//1', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
          begin
            if Exec(ExpandConstant('{sys}\SC.EXE'), 'start GVFS.Service', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
              begin
                InstallSuccessful := True;
              end;
          end;
      end;

    WriteOnDiskVersion16CapableFile();
  finally
    WizardForm.StatusLabel.Caption := StatusText;
    WizardForm.ProgressGauge.Style := npbstNormal;
  end;

  if InstallSuccessful = False then
    begin
      RaiseException('Fatal: An error occured while installing GVFS.Service.');
    end;
end;

function DeleteFileIfItExists(FilePath: string) : Boolean;
begin
  Result := False;
  if FileExists(FilePath) then
    begin
      Log('DeleteFileIfItExists: Removing ' + FilePath);
      if DeleteFile(FilePath) then
        begin
          if not FileExists(FilePath) then
            begin
              Result := True;
            end
          else
            begin
              Log('DeleteFileIfItExists: File still exists after deleting: ' + FilePath);
            end;
        end
      else
        begin
          Log('DeleteFileIfItExists: Failed to delete ' + FilePath);
        end;
    end
  else
    begin
      Log('DeleteFileIfItExists: File does not exist: ' + FilePath);
      Result := True;
    end;
end;

procedure UninstallGvFlt();
var
  StatusText: string;
  UninstallSuccessful: Boolean;
begin
  if (FileExists(ExpandConstant('{app}\Filter\GvFlt.inf'))) then
  begin
    UninstallSuccessful := False;

    StatusText := WizardForm.StatusLabel.Caption;
    WizardForm.StatusLabel.Caption := 'Uninstalling GvFlt Driver.';
    WizardForm.ProgressGauge.Style := npbstMarquee;

    try
      UninstallService('gvflt', False);
      if DeleteFileIfItExists(ExpandConstant('{sys}\drivers\gvflt.sys')) then
        begin
           UninstallSuccessful := True;
        end;
    finally
      WizardForm.StatusLabel.Caption := StatusText;
      WizardForm.ProgressGauge.Style := npbstNormal;
    end;

    if UninstallSuccessful = True then
      begin
        if not DeleteFile(ExpandConstant('{app}\Filter\GvFlt.inf')) then
          begin
            Log('UninstallGvFlt: Failed to delete GvFlt.inf');
          end;
      end
    else
      begin
          RaiseException('Fatal: An error occured while uninstalling GvFlt drivers.');
      end;
  end;
end;

function UninstallNonInboxProjFS(): Boolean;
var
  StatusText: string;
begin
  Result := False;
  StatusText := WizardForm.StatusLabel.Caption;
  WizardForm.StatusLabel.Caption := 'Uninstalling PrjFlt Driver.';
  WizardForm.ProgressGauge.Style := npbstMarquee;

  Log('UninstallNonInboxProjFS: Uninstalling ProjFS');
  try
    UninstallService('prjflt', False);
    if DeleteFileIfItExists(ExpandConstant('{app}\ProjectedFSLib.dll')) then
      begin
        if DeleteFileIfItExists(ExpandConstant('{sys}\drivers\prjflt.sys')) then
          begin
            Result := True;
          end;
      end;
  finally
    WizardForm.StatusLabel.Caption := StatusText;
    WizardForm.ProgressGauge.Style := npbstNormal;
  end;
end;

procedure UninstallProjFSIfNecessary();
var
  ProjFSFeatureEnabledResultCode: integer;
  UninstallSuccessful: Boolean;
begin
  if FileExists(ExpandConstant('{app}\Filter\PrjFlt.inf')) and FileExists(ExpandConstant('{sys}\drivers\prjflt.sys')) then
    begin
      UninstallSuccessful := False;

      if Exec('powershell.exe', '-NoProfile "$var=(Get-WindowsOptionalFeature -Online -FeatureName Client-ProjFS);  if($var -eq $null){exit 2}else{if($var.State -eq ''Enabled''){exit 3}else{exit 4}}"', '', SW_HIDE, ewWaitUntilTerminated, ProjFSFeatureEnabledResultCode) then
        begin
          if ProjFSFeatureEnabledResultCode = 2 then
            begin
              // Client-ProjFS is not an optional feature
              Log('UninstallProjFSIfNecessary: Could not locate Windows Projected File System optional feature, uninstalling ProjFS');
              if UninstallNonInboxProjFS() then
                begin
                  UninstallSuccessful := True;
                end;
            end;
          if ProjFSFeatureEnabledResultCode = 3 then
            begin
              // Client-ProjFS is already enabled. If the native ProjFS library is in the apps folder it must
              // be deleted to ensure GVFS uses the inbox library (in System32)
              Log('UninstallProjFSIfNecessary: Client-ProjFS already enabled');
              if DeleteFileIfItExists(ExpandConstant('{app}\ProjectedFSLib.dll')) then
                begin
                  UninstallSuccessful := True;
                end;
            end;
          if ProjFSFeatureEnabledResultCode = 4 then
            begin
              // Client-ProjFS is currently disabled but prjflt.sys is present and should be removed
              Log('UninstallProjFSIfNecessary: Client-ProjFS is disabled, uninstalling ProjFS');
              if UninstallNonInboxProjFS() then
                begin
                  UninstallSuccessful := True;
                end;
            end;
        end;

      if UninstallSuccessful = False then
      begin
        RaiseException('Fatal: An error occured while uninstalling ProjFS.');
      end;
    end;
end;

function IsGVFSRunning(): Boolean;
var
  ResultCode: integer;
begin
  if Exec('powershell.exe', '-NoProfile "Get-Process gvfs,gvfs.mount | foreach {exit 10}"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
    begin
      if ResultCode = 10 then
        begin
          Result := True;
        end;
      if ResultCode = 1 then
        begin
          Result := False;
        end;
    end;
end;

function ExecWithResult(Filename, Params, WorkingDir: String; ShowCmd: Integer;
  Wait: TExecWait; var ResultCode: Integer; var ResultString: ansiString): Boolean;
var
  TempFilename: string;
  Command: string;
begin
  TempFilename := ExpandConstant('{tmp}\~execwithresult.txt');
  { Exec via cmd and redirect output to file. Must use special string-behavior to work. }
  Command := Format('"%s" /S /C ""%s" %s > "%s""', [ExpandConstant('{cmd}'), Filename, Params, TempFilename]);
  Result := Exec(ExpandConstant('{cmd}'), Command, WorkingDir, ShowCmd, Wait, ResultCode);
  if Result then
    begin
      LoadStringFromFile(TempFilename, ResultString);
    end;
  DeleteFile(TempFilename);
end;

procedure UnmountRepos();
var
  ResultCode: integer;
begin
  Exec('gvfs.exe', 'service --unmount-all', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;

procedure MountRepos();
var
  StatusText: string;
  MountOutput: ansiString;
  ResultCode: integer;
  MsgBoxText: string;
begin
  StatusText := WizardForm.StatusLabel.Caption;
  WizardForm.StatusLabel.Caption := 'Mounting Repos.';
  WizardForm.ProgressGauge.Style := npbstMarquee;

  ExecWithResult(ExpandConstant('{app}') + '\gvfs.exe', 'service --mount-all', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, MountOutput);
  WizardForm.StatusLabel.Caption := StatusText;
  WizardForm.ProgressGauge.Style := npbstNormal;

  // 4 = ReturnCode.FilterError
  if (ResultCode = 4) then
    begin
      RaiseException('Fatal: Could not configure and start Windows Projected File System.');
    end
  else if (ResultCode <> 0) then
    begin
      MsgBoxText := 'Mounting one or more repos failed:' + #13#10 + MountOutput;
      SuppressibleMsgBox(MsgBoxText, mbConfirmation, MB_OK, IDOK);
      ExitCode := 17;
    end;
end;

procedure MigrateFile(OldPath, NewPath : string);
begin
  Log('MigrateFile(' + OldPath + ', ' + NewPath + ')');
  if (FileExists(OldPath)) then
    begin
      if (not FileExists(NewPath)) then
        begin
          if (not RenameFile(OldPath, NewPath)) then
            Log('Could not move ' + OldPath + ' continuing anyway')
          else
            Log('Moved ' + OldPath + ' to ' + NewPath);
        end
      else
        Log('Migration cancelled. Newer file exists at path ' + NewPath);
    end
  else
    Log('Migration cancelled. ' + OldPath + ' does not exist');
end;

procedure MigrateConfigAndStatusCacheFiles();
var
  CommonAppDataDir: string;
  SecureAppDataDir: string;
begin
  CommonAppDataDir := ExpandConstant('{commonappdata}\GVFS');
  SecureAppDataDir := ExpandConstant('{app}\ProgramData');

  MigrateFile(CommonAppDataDir + '\{#GVFSConfigFileName}', SecureAppDataDir + '\{#GVFSConfigFileName}');
  MigrateFile(CommonAppDataDir + '\{#ServiceName}\{#GVFSStatuscacheTokenFileName}', SecureAppDataDir + '\{#ServiceName}\{#GVFSStatuscacheTokenFileName}');
end;

function ConfirmUnmountAll(): Boolean;
var
  MsgBoxResult: integer;
  Repos: ansiString;
  ResultCode: integer;
  MsgBoxText: string;
begin
  Result := False;
  if ExecWithResult('gvfs.exe', 'service --list-mounted', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, Repos) then
    begin
      if Repos = '' then
        begin
          Result := False;
        end
      else
        begin
          if ResultCode = 0 then
            begin
              MsgBoxText := 'The following repos are currently mounted:' + #13#10 + Repos + #13#10 + 'Setup needs to unmount all repos before it can proceed, and those repos will be unavailable while setup is running. Do you want to continue?';
              MsgBoxResult := SuppressibleMsgBox(MsgBoxText, mbConfirmation, MB_OKCANCEL, IDOK);
              if (MsgBoxResult = IDOK) then
                begin
                  Result := True;
                end
              else
                begin
                  Abort();
                end;
            end;
        end;
    end;
end;

function EnsureGvfsNotRunning(): Boolean;
var
  MsgBoxResult: integer;
begin
  MsgBoxResult := IDRETRY;
  while (IsGVFSRunning()) Do
    begin
      if(MsgBoxResult = IDRETRY) then
        begin
          MsgBoxResult := SuppressibleMsgBox('GVFS is currently running. Please close all instances of GVFS before continuing the installation.', mbError, MB_RETRYCANCEL, IDCANCEL);
        end;
      if(MsgBoxResult = IDCANCEL) then
        begin
          Result := False;
          Abort();
        end;
    end;

  Result := True;
end;

type
  UpgradeRing = (urUnconfigured, urNone, urFast, urSlow);

function GetConfiguredUpgradeRing(): UpgradeRing;
var
  ResultCode: integer;
  ResultString: ansiString;
begin
  Result := urUnconfigured;
  if ExecWithResult('gvfs.exe', 'config upgrade.ring', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, ResultString) then begin
    if ResultCode = 0 then begin
      ResultString := AnsiLowercase(Trim(ResultString));
      Log('GetConfiguredUpgradeRing: upgrade.ring is ' + ResultString);
      if CompareText(ResultString, 'none') = 0 then begin
        Result := urNone;
      end else if CompareText(ResultString, 'fast') = 0 then begin
        Result := urFast;
      end else if CompareText(ResultString, 'slow') = 0 then begin
        Result := urSlow;
      end else begin
        Log('GetConfiguredUpgradeRing: Unknown upgrade ring: ' + ResultString);
      end;
    end else begin
      Log('GetConfiguredUpgradeRing: Call to gvfs config upgrade.ring failed with ' + SysErrorMessage(ResultCode));
    end;
  end else begin
    Log('GetConfiguredUpgradeRing: Call to gvfs config upgrade.ring failed with ' + SysErrorMessage(ResultCode));
  end;
end;

function IsConfigured(ConfigKey: String): Boolean;
var
  ResultCode: integer;
  ResultString: ansiString;
begin
  Result := False
  if ExecWithResult('gvfs.exe', Format('config %s', [ConfigKey]), '', SW_HIDE, ewWaitUntilTerminated, ResultCode, ResultString) then begin
    ResultString := AnsiLowercase(Trim(ResultString));
    Log(Format('IsConfigured(%s): value is %s', [ConfigKey, ResultString]));
    Result := Length(ResultString) > 1
  end
end;

procedure SetIfNotConfigured(ConfigKey: String; ConfigValue: String);
var
  ResultCode: integer;
  ResultString: ansiString;
begin
  if IsConfigured(ConfigKey) = False then begin
    if ExecWithResult('gvfs.exe', Format('config %s %s', [ConfigKey, ConfigValue]), '', SW_HIDE, ewWaitUntilTerminated, ResultCode, ResultString) then begin
      Log(Format('SetIfNotConfigured: Set %s to %s', [ConfigKey, ConfigValue]));
    end else begin
      Log(Format('SetIfNotConfigured: Failed to set %s with %s', [ConfigKey, SysErrorMessage(ResultCode)]));
    end;
  end else begin
    Log(Format('SetIfNotConfigured: %s is configured, not overwriting', [ConfigKey]));
  end;
end;

procedure SetNuGetFeedIfNecessary();
var
  ConfiguredRing: UpgradeRing;
  RingName: String;
  TargetFeed: String;
  FeedPackageName: String;
begin
  ConfiguredRing := GetConfiguredUpgradeRing();
  if ConfiguredRing = urFast then begin
    RingName := 'Fast';
  end else if (ConfiguredRing = urSlow) or (ConfiguredRing = urNone) then begin
    RingName := 'Slow';
  end else begin
    Log('SetNuGetFeedIfNecessary: No upgrade ring configured. Not configuring NuGet feed.')
    exit;
  end;

  TargetFeed := Format('https://pkgs.dev.azure.com/microsoft/_packaging/VFSForGit-%s/nuget/v3/index.json', [RingName]);
  FeedPackageName := 'Microsoft.VfsForGitEnvironment';

  SetIfNotConfigured('upgrade.feedurl', TargetFeed);
  SetIfNotConfigured('upgrade.feedpackagename', FeedPackageName);
end;

// Below are EVENT FUNCTIONS -> The main entry points of InnoSetup into the code region
// Documentation : http://www.jrsoftware.org/ishelp/index.php?topic=scriptevents

function InitializeUninstall(): Boolean;
begin
  UnmountRepos();
  Result := EnsureGvfsNotRunning();
end;

// Called just after "install" phase, before "post install"
function NeedRestart(): Boolean;
begin
  Result := False;
end;

function UninstallNeedRestart(): Boolean;
begin
  Result := False;
end;

procedure CurStepChanged(CurStep: TSetupStep);
begin
  case CurStep of
    ssInstall:
      begin
        UninstallService('GVFS.Service', True);
      end;
    ssPostInstall:
      begin
        MigrateConfigAndStatusCacheFiles();
        if ExpandConstant('{param:REMOUNTREPOS|true}') = 'true' then
          begin
            MountRepos();
          end
      end;
    end;
end;

function GetCustomSetupExitCode: Integer;
begin
  Result := ExitCode;
end;

procedure CurUninstallStepChanged(CurStep: TUninstallStep);
begin
  case CurStep of
    usUninstall:
      begin
        UninstallService('GVFS.Service', False);
        RemovePath(ExpandConstant('{app}'));
      end;
    end;
end;

function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
  NeedsRestart := False;
  Result := '';
  SetNuGetFeedIfNecessary();
  if ConfirmUnmountAll() then
    begin
      if ExpandConstant('{param:REMOUNTREPOS|true}') = 'true' then
        begin
          UnmountRepos();
        end
    end;
  if not EnsureGvfsNotRunning() then
    begin
      Abort();
    end;
  StopService('GVFS.Service');
  UninstallGvFlt();
  UninstallProjFSIfNecessary();
end;


================================================
FILE: GVFS/GVFS.Installers/info.bat
================================================
@ECHO OFF
SETLOCAL

SET SYS_PRJFLT=C:\Windows\System32\drivers\prjflt.sys
SET SYS_PROJFSLIB=C:\Windows\System32\ProjectedFSLib.dll
SET VFS_PROJFSLIB=C:\Program Files\VFS for Git\ProjectedFSLib.dll
SET VFS_BUND_PRJFLT=C:\Program Files\VFS for Git\Filter\PrjFlt.sys
SET VFS_BUND_PROJFSLIB=C:\Program Files\VFS for Git\ProjFS\ProjectedFSLib.dll
SET VFS_EXEC=C:\Program Files\VFS for Git\GVFS.exe
SET GIT_EXEC=C:\Program Files\Git\cmd\git.exe

REM Lookup the current Windows version
FOR /F "tokens=*" %%i IN ('powershell -Command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"') DO SET OS_VER=%%i

ECHO Print system information...
ECHO OS version: %OS_VER%
ECHO CPU architecture: %PROCESSOR_ARCHITECTURE%

ECHO.
ECHO Checking ProjFS Windows feature...
powershell -Command "Get-WindowsOptionalFeature -Online -FeatureName Client-ProjFS"

ECHO.
ECHO Checking ProjFS and GVFS services...
ECHO GVFS.Service:
sc query GVFS.Service

ECHO.
ECHO Test.GVFS.Service:
sc query Test.GVFS.Service

ECHO.
ECHO prjflt:
sc query prjflt

ECHO.
ECHO Checking ProjFS files...
IF EXIST "%SYS_PRJFLT%" (
    ECHO [ FOUND ] %SYS_PRJFLT%
) ELSE (
    ECHO [MISSING] %SYS_PRJFLT%
)

IF EXIST "%SYS_PROJFSLIB%" (
    ECHO [ FOUND ] %SYS_PROJFSLIB%
) ELSE (
    ECHO [MISSING] %SYS_PROJFSLIB%
)

IF EXIST "%VFS_PROJFSLIB%" (
    ECHO [ FOUND ] %VFS_PROJFSLIB%
) ELSE (
    ECHO [MISSING] %VFS_PROJFSLIB%
)

IF EXIST "%VFS_BUND_PRJFLT%" (
    ECHO [ FOUND ] %VFS_BUND_PRJFLT%
) ELSE (
    ECHO [MISSING] %VFS_BUND_PRJFLT%
)

IF EXIST "%VFS_BUND_PROJFSLIB%" (
    ECHO [ FOUND ] %VFS_BUND_PROJFSLIB%
) ELSE (
    ECHO [MISSING] %VFS_BUND_PROJFSLIB%
)

ECHO.
ECHO Print product versions...
IF EXIST "%VFS_EXEC%" (
    "%VFS_EXEC%" version
) ELSE (
    ECHO GVFS not installed at %VFS_EXEC%
)

IF EXIST "%GIT_EXEC%" (
    "%GIT_EXEC%" version
) ELSE (
    ECHO Git not installed at %GIT_EXEC%
)


================================================
FILE: GVFS/GVFS.Installers/install.bat
================================================
@ECHO OFF
SETLOCAL

REM Lookup full path to VFS for Git installer
FOR /F "tokens=* USEBACKQ" %%F IN ( `where /R %~dp0 SetupGVFS*.exe` ) DO SET GVFS_INSTALLER=%%F

REM Create new empty directory for logs
SET LOGDIR=%~dp0\logs
IF EXIST %LOGDIR% (
    rmdir /S /Q %LOGDIR%
)
mkdir %LOGDIR%

ECHO Installing VFS for Git...
%GVFS_INSTALLER% /LOG="%LOGDIR%\gvfs.log" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /DIR="C:\Program Files\VFS for Git"


================================================
FILE: GVFS/GVFS.MSBuild/CompileTemplatedFile.cs
================================================
using System;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace GVFS.MSBuild
{
    public class CompileTemplatedFile : Task
    {
        [Required]
        public ITaskItem Template { get; set; }

        [Required]
        public string OutputFile { get; set; }

        [Output]
        public ITaskItem CompiledTemplate { get; set; }

        public override bool Execute()
        {
            string templateFilePath = this.Template.ItemSpec;
            IDictionary properties = ParseProperties(this.Template.GetMetadata("Properties"));

            string outputFileDirectory = Path.GetDirectoryName(this.OutputFile);

            if (!File.Exists(templateFilePath))
            {
                this.Log.LogError("Failed to find template file '{0}'.", templateFilePath);
                return false;
            }

            // Copy the template to the destination to keep the same file mode bits/ACLs as the template
            File.Copy(templateFilePath, this.OutputFile, true);

            this.Log.LogMessage(MessageImportance.Low, "Reading template contents");
            string template = File.ReadAllText(this.OutputFile);

            this.Log.LogMessage(MessageImportance.Normal, "Compiling template '{0}'", templateFilePath);
            string compiled = Compile(template, properties);

            if (!Directory.Exists(outputFileDirectory))
            {
                this.Log.LogMessage(MessageImportance.Low, "Creating output directory '{0}'", outputFileDirectory);
                Directory.CreateDirectory(outputFileDirectory);
            }

            this.Log.LogMessage(MessageImportance.Normal, "Writing compiled template to '{0}'", this.OutputFile);
            File.WriteAllText(this.OutputFile, compiled);

            this.CompiledTemplate = new TaskItem(this.OutputFile, this.Template.CloneCustomMetadata());

            return true;
        }

        private IDictionary ParseProperties(string propertiesStr)
        {
            string[] properties = propertiesStr?.Split(';') ?? new string[0];
            var dict = new Dictionary();

            foreach (string propertyStr in properties)
            {
                string[] kvp = propertyStr.Split(new[] {'='}, count: 2);
                if (kvp.Length > 1)
                {
                    string key = kvp[0].Trim();
                    dict[key]  = kvp[1].Trim();
                }
            }

            return dict;
        }

        private string Compile(string template, IDictionary properties)
        {
            var sb = new StringBuilder(template);

            foreach (var kvp in properties)
            {
                this.Log.LogMessage(MessageImportance.Low, "Replacing \"{0}\" -> \"{1}\"", kvp.Key, kvp.Value);
                sb.Replace(kvp.Key, kvp.Value);
            }

            return sb.ToString();
        }
    }
}


================================================
FILE: GVFS/GVFS.MSBuild/GVFS.MSBuild.csproj
================================================


  
    netstandard2.0
    false
  

  
    
    
  




================================================
FILE: GVFS/GVFS.MSBuild/GVFS.targets
================================================

  
  

  
  
    $(IntermediateOutputPath)app.manifest
  

  
  
    
    
      
    
  

  
  
    
    
      
    
  



================================================
FILE: GVFS/GVFS.MSBuild/GVFS.tasks
================================================

    
        <_TaskAssembly>$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll
        <_TaskFactory>CodeTaskFactory
    
    
        <_TaskAssembly>$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll
        <_TaskFactory>RoslynCodeTaskFactory
    

    
        
            
        
    
    
        
            
        
    
    
        
            
        
    
    
        
            
        
    



================================================
FILE: GVFS/GVFS.MSBuild/GenerateGVFSConstants.cs
================================================
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.IO;
using System.Text.RegularExpressions;

namespace GVFS.MSBuild
{
    public class GenerateGVFSConstants : Task
    {
        [Required]
        public string MinimumGitVersion { get; set; }

        [Required]
        public string LibGit2FileName { get; set; }

        [Required]
        public string OutputFile { get; set; }

        public override bool Execute()
        {
            this.Log.LogMessage(MessageImportance.Normal,
                "Creating GVFS constants file with minimum Git version '{0}' at '{1}'...",
                this.MinimumGitVersion, this.OutputFile);

            if (!TryParseVersion(this.MinimumGitVersion, out var version))
            {
                this.Log.LogError("Failed to parse Git version '{0}'.", this.MinimumGitVersion);
                return false;
            }

            string outputDirectory = Path.GetDirectoryName(this.OutputFile);
            if (!Directory.Exists(outputDirectory))
            {
                Directory.CreateDirectory(outputDirectory);
            }

            string template =
@"//
// This file is auto-generated by Scalar.Build.GenerateScalarConstants.
// 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({0}, {1}, {2}, ""{3}"", {4}, {5});
        public const string LibGit2LibraryName = ""{6}"";
    }}
}}";

            File.WriteAllText(
                this.OutputFile,
                string.Format(
                    template,
                    version.Major,
                    version.Minor,
                    version.Build,
                    version.Platform,
                    version.Revision,
                    version.MinorRevision,
                    this.LibGit2FileName));

            return true;
        }

        private static bool TryParseVersion(string versionString, out GitVersion version)
        {
            const string pattern = @"(\d+)\.(\d+)\.(\d+)\.([A-Z]+)\.(\d+)\.(\d+)";

            Match match = Regex.Match(versionString, pattern, RegexOptions.IgnoreCase);
            if (match.Success)
            {
                version = new GitVersion
                {
                    Major = int.Parse(match.Groups[1].Value),
                    Minor = int.Parse(match.Groups[2].Value),
                    Build = int.Parse(match.Groups[3].Value),
                    Platform = match.Groups[4].Value,
                    Revision = int.Parse(match.Groups[5].Value),
                    MinorRevision = int.Parse(match.Groups[6].Value)
                };

                return true;
            }

            version = default(GitVersion);
            return false;
        }

        private struct GitVersion
        {
            public int Major { get; set; }
            public int Minor { get; set; }
            public int Build { get; set; }
            public string Platform { get; set; }
            public int Revision { get; set; }
            public int MinorRevision { get; set; }
        }
    }
}

================================================
FILE: GVFS/GVFS.MSBuild/GenerateGVFSVersionHeader.cs
================================================
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.IO;

namespace GVFS.MSBuild
{
    public class GenerateGVFSVersionHeader : Task
    {
        [Required]
        public string Version { get; set; }

        [Required]
        public string OutputFile { get; set; }

        public override bool Execute()
        {
            this.Log.LogMessage(MessageImportance.Normal,
                "Creating GVFS version header file with version '{0}' at '{1}'...",
                this.Version, this.OutputFile);

            string outputDirectory = Path.GetDirectoryName(this.OutputFile);
            if (!Directory.Exists(outputDirectory))
            {
                Directory.CreateDirectory(outputDirectory);
            }

            string template =
@"/*
 * This file is auto-generated by GVFS.MSBuild.GenerateGVFSVersionHeader.
 * Any changes made directly in this file will be lost.
 */
#define GVFS_FILE_VERSION {1}
#define GVFS_FILE_VERSION_STRING ""{0}""
#define GVFS_PRODUCT_VERSION {1}
#define GVFS_PRODUCT_VERSION_STRING ""{0}""
";

            File.WriteAllText(
                this.OutputFile,
                string.Format(
                    template,
                    this.Version,
                    this.Version?.Replace('.',',')));

            return true;
        }
    }
}

================================================
FILE: GVFS/GVFS.MSBuild/GenerateWindowsAppManifest.cs
================================================
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.IO;

namespace GVFS.MSBuild
{
    public class GenerateWindowsAppManifest : Task
    {
        [Required]
        public string Version { get; set; }

        [Required]
        public string ApplicationName { get; set; }

        [Required]
        public string OutputFile { get; set; }

        public override bool Execute()
        {
            this.Log.LogMessage(MessageImportance.Normal, "Creating application manifest file for '{0}'...", this.ApplicationName);

            string manifestDirectory = Path.GetDirectoryName(this.OutputFile);
            if (!Directory.Exists(manifestDirectory))
            {
                Directory.CreateDirectory(manifestDirectory);
            }

            // Any application that calls GetVersionEx must have an application manifest in order to get an accurate response.
            // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms724451(v=vs.85).aspx for details
            File.WriteAllText(
                this.OutputFile,
                string.Format(
                    @"

  
  
    
      
      
    
  

",
                    this.Version,
                    this.ApplicationName));

            return true;
        }
    }
}

================================================
FILE: GVFS/GVFS.Mount/GVFS.Mount.csproj
================================================


  
    Exe
    net471
  

  
    
      false
      Content
      PreserveNewest
      Build;DebugSymbolsProjectOutputGroup
    
    
  

  
    
  




================================================
FILE: GVFS/GVFS.Mount/InProcessMount.cs
================================================
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Http;
using GVFS.Common.Maintenance;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.PlatformLoader;
using GVFS.Virtualization;
using GVFS.Virtualization.FileSystem;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static GVFS.Common.Git.LibGit2Repo;

namespace GVFS.Mount
{
    public class InProcessMount
    {
        // Tests show that 250 is the max supported pipe name length
        private const int MaxPipeNameLength = 250;
        private const int MutexMaxWaitTimeMS = 500;

        // This is value chosen based on tested scenarios to limit the required download time for
        // all the trees. This is approximately the amount of trees that can be downloaded in 1 second.
        // Downloading an entire commit pack also takes around 1 second, so this should limit downloading
        // all the trees in a commit to ~2-3 seconds.
        private const int MissingTreeThresholdForDownloadingCommitPack = 200;

        // Number of unique missing trees to track with LRU eviction. Eviction is commit-based:
        // when capacity is reached, the LRU commit and all its unique trees are dropped to make room.
        // Set to 20x the threshold so that enough trees can accumulate for the heuristic to
        // reliably trigger a commit pack download.
        private const int TrackedTreeCapacity = MissingTreeThresholdForDownloadingCommitPack * 20;

        private readonly bool showDebugWindow;

        private FileSystemCallbacks fileSystemCallbacks;
        private GVFSDatabase gvfsDatabase;
        private GVFSEnlistment enlistment;
        private ITracer tracer;
        private GitMaintenanceScheduler maintenanceScheduler;

        private CacheServerInfo cacheServer;
        private RetryConfig retryConfig;
        private GitStatusCacheConfig gitStatusCacheConfig;

        private GVFSContext context;
        private GVFSGitObjects gitObjects;

        private MountState currentState;
        private HeartbeatThread heartbeat;
        private ManualResetEvent unmountEvent;

        private readonly MissingTreeTracker missingTreeTracker;

        // True if InProcessMount is calling git reset as part of processing
        // a folder dehydrate request
        private volatile bool resetForDehydrateInProgress;

        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;
            this.unmountEvent = new ManualResetEvent(false);
            this.missingTreeTracker = new MissingTreeTracker(tracer, TrackedTreeCapacity);
        }

        private enum MountState
        {
            Invalid = 0,

            Mounting,
            Ready,
            Unmounting,
            MountFailed
        }

        public void Mount(EventLevel verbosity, Keywords keywords)
        {
            this.currentState = MountState.Mounting;

            // For worktree mounts, create the .gvfs metadata directory and
            // bootstrap it with cache paths from the primary enlistment
            if (this.enlistment.IsWorktree)
            {
                this.InitializeWorktreeMetadata();
            }

            string mountLockPath = Path.Combine(this.enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.MountLock);
            using (FileBasedLock mountLock = GVFSPlatform.Instance.CreateFileBasedLock(
                new PhysicalFileSystem(),
                this.tracer,
                mountLockPath))
            {
                if (!mountLock.TryAcquireLock(out Exception lockException))
                {
                    if (lockException is IOException)
                    {
                        this.FailMountAndExit(ReturnCode.MountAlreadyRunning, "Mount: Another mount process is already running.");
                    }

                    this.FailMountAndExit("Mount: Failed to acquire mount lock: {0}", lockException.Message);
                }

                this.MountWithLockAcquired(verbosity, keywords);
            }
        }

        private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords)
        {
            // Start auth + config query immediately — these are network-bound and don't
            // depend on repo metadata or cache paths. Every millisecond of network latency
            // we can overlap with local I/O is a win.
            // TryInitializeAndQueryGVFSConfig combines the anonymous probe, credential fetch,
            // and config query into at most 2 HTTP requests (1 for anonymous repos), reusing
            // the same HttpClient/TCP connection.
            Stopwatch parallelTimer = Stopwatch.StartNew();

            var networkTask = Task.Run(() =>
            {
                Stopwatch sw = Stopwatch.StartNew();
                ServerGVFSConfig config;
                string authConfigError;

                if (!this.enlistment.Authentication.TryInitializeAndQueryGVFSConfig(
                    this.tracer, this.enlistment, this.retryConfig,
                    out config, out authConfigError))
                {
                    if (this.cacheServer != null && !string.IsNullOrWhiteSpace(this.cacheServer.Url))
                    {
                        this.tracer.RelatedWarning("Mount will proceed with fallback cache server: " + authConfigError);
                        config = null;
                    }
                    else
                    {
                        this.FailMountAndExit("Unable to query /gvfs/config" + Environment.NewLine + authConfigError);
                    }
                }

                this.ValidateGVFSVersion(config);
                this.tracer.RelatedInfo("ParallelMount: Auth + config completed in {0}ms", sw.ElapsedMilliseconds);
                return config;
            });
            // We must initialize repo metadata before starting the pipe server so it
            // can immediately handle status requests
            string error;
            if (!RepoMetadata.TryInitialize(this.tracer, this.enlistment.DotGVFSRoot, out error))
            {
                this.FailMountAndExit("Failed to load repo metadata: " + error);
            }

            string gitObjectsRoot;
            if (!RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error))
            {
                this.FailMountAndExit("Failed to determine git objects root from repo metadata: " + error);
            }

            string localCacheRoot;
            if (!RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error))
            {
                this.FailMountAndExit("Failed to determine local cache path from repo metadata: " + error);
            }

            string blobSizesRoot;
            if (!RepoMetadata.Instance.TryGetBlobSizesRoot(out blobSizesRoot, out error))
            {
                this.FailMountAndExit("Failed to determine blob sizes root from repo metadata: " + error);
            }

            this.tracer.RelatedEvent(
                EventLevel.Informational,
                "CachePathsLoaded",
                new EventMetadata
                {
                    { "gitObjectsRoot", gitObjectsRoot },
                    { "localCacheRoot", localCacheRoot },
                    { "blobSizesRoot", blobSizesRoot },
                });

            this.enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot);

            // Local validations and git config run while we wait for the network
            var localTask = Task.Run(() =>
            {
                Stopwatch sw = Stopwatch.StartNew();

                this.ValidateGitVersion();
                this.tracer.RelatedInfo("ParallelMount: ValidateGitVersion completed in {0}ms", sw.ElapsedMilliseconds);

                this.ValidateHooksVersion();
                this.ValidateFileSystemSupportsRequiredFeatures();

                GitProcess git = new GitProcess(this.enlistment);
                if (!git.IsValidRepo())
                {
                    this.FailMountAndExit("The .git folder is missing or has invalid contents");
                }

                if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(this.enlistment.EnlistmentRoot, out string fsError))
                {
                    this.FailMountAndExit("FileSystem unsupported: " + fsError);
                }

                this.tracer.RelatedInfo("ParallelMount: Local validations completed in {0}ms", sw.ElapsedMilliseconds);

                if (!this.TrySetRequiredGitConfigSettings())
                {
                    this.FailMountAndExit("Unable to configure git repo");
                }

                this.LogEnlistmentInfoAndSetConfigValues();
                this.tracer.RelatedInfo("ParallelMount: Local validations + git config completed in {0}ms", sw.ElapsedMilliseconds);
            });

            try
            {
                Task.WaitAll(networkTask, localTask);
            }
            catch (AggregateException ae)
            {
                this.FailMountAndExit(ae.Flatten().InnerExceptions[0].Message);
            }

            parallelTimer.Stop();
            this.tracer.RelatedInfo("ParallelMount: All parallel tasks completed in {0}ms", parallelTimer.ElapsedMilliseconds);

            ServerGVFSConfig serverGVFSConfig = networkTask.Result;

            CacheServerResolver cacheServerResolver = new CacheServerResolver(this.tracer, this.enlistment);
            this.cacheServer = cacheServerResolver.ResolveNameFromRemote(this.cacheServer.Url, serverGVFSConfig);

            this.EnsureLocalCacheIsHealthy(serverGVFSConfig);

            using (NamedPipeServer pipeServer = this.StartNamedPipe())
            {
                this.tracer.RelatedEvent(
                    EventLevel.Informational,
                    $"{nameof(this.Mount)}_StartedNamedPipe",
                    new EventMetadata { { "NamedPipeName", this.enlistment.NamedPipeName } });

                this.context = this.CreateContext();

                if (this.context.Unattended)
                {
                    this.tracer.RelatedEvent(EventLevel.Critical, GVFSConstants.UnattendedEnvironmentVariable, null);
                }

                this.ValidateMountPoints();

                string errorMessage;

                // Worktrees share hooks with the primary enlistment via core.hookspath,
                // so skip installation to avoid locking conflicts with the running mount.
                if (!this.enlistment.IsWorktree && !HooksInstaller.TryUpdateHooks(this.context, out errorMessage))
                {
                    this.FailMountAndExit(errorMessage);
                }

                GVFSPlatform.Instance.ConfigureVisualStudio(this.enlistment.GitBinPath, this.tracer);

                this.MountAndStartWorkingDirectoryCallbacks(this.cacheServer);

                Console.Title = "GVFS " + ProcessHelper.GetCurrentProcessVersion() + " - " + this.enlistment.EnlistmentRoot;

                this.tracer.RelatedEvent(
                    EventLevel.Informational,
                    "Mount",
                    new EventMetadata
                    {
                        // Use TracingConstants.MessageKey.InfoMessage rather than TracingConstants.MessageKey.CriticalMessage
                        // as this message should not appear as an error
                        { TracingConstants.MessageKey.InfoMessage, "Virtual repo is ready" },
                    },
                    Keywords.Telemetry);

                this.currentState = MountState.Ready;

                this.unmountEvent.WaitOne();
            }
        }

        private GVFSContext CreateContext()
        {
            PhysicalFileSystem fileSystem = new PhysicalFileSystem();
            GitRepo gitRepo = this.CreateOrReportAndExit(
                () => new GitRepo(
                    this.tracer,
                    this.enlistment,
                    fileSystem),
                "Failed to read git repo");
            return new GVFSContext(this.tracer, fileSystem, gitRepo, this.enlistment);
        }

        private void ValidateMountPoints()
        {
            DirectoryInfo workingDirectoryRootInfo = new DirectoryInfo(this.enlistment.WorkingDirectoryBackingRoot);
            if (!workingDirectoryRootInfo.Exists)
            {
                this.FailMountAndExit("Failed to initialize file system callbacks. Directory \"{0}\" must exist.", this.enlistment.WorkingDirectoryBackingRoot);
            }

            if (this.enlistment.IsWorktree)
            {
                // Worktrees have a .git file (not directory) pointing to the shared git dir
                string dotGitFile = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Root);
                if (!File.Exists(dotGitFile))
                {
                    this.FailMountAndExit("Failed to mount worktree. File \"{0}\" must exist.", dotGitFile);
                }
            }
            else
            {
                string dotGitPath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Root);
                DirectoryInfo dotGitPathInfo = new DirectoryInfo(dotGitPath);
                if (!dotGitPathInfo.Exists)
                {
                    this.FailMountAndExit("Failed to mount. Directory \"{0}\" must exist.", dotGitPathInfo);
                }
            }
        }

        /// 
        /// For worktree mounts, create the .gvfs metadata directory and
        /// bootstrap RepoMetadata with cache paths from the primary enlistment.
        /// 
        private void InitializeWorktreeMetadata()
        {
            string dotGVFSRoot = this.enlistment.DotGVFSRoot;
            if (!Directory.Exists(dotGVFSRoot))
            {
                try
                {
                    Directory.CreateDirectory(dotGVFSRoot);
                    this.tracer.RelatedInfo($"Created worktree metadata directory: {dotGVFSRoot}");
                }
                catch (Exception e)
                {
                    this.FailMountAndExit("Failed to create worktree metadata directory '{0}': {1}", dotGVFSRoot, e.Message);
                }
            }

            // Bootstrap RepoMetadata from the primary enlistment's metadata.
            // Use try/finally to guarantee Shutdown() even if an unexpected
            // exception occurs — the singleton must not be left pointing at
            // the primary's metadata directory.
            string primaryDotGVFS = Path.Combine(this.enlistment.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot);
            string error;
            string gitObjectsRoot;
            string localCacheRoot;
            string blobSizesRoot;

            if (!RepoMetadata.TryInitialize(this.tracer, primaryDotGVFS, out error))
            {
                this.FailMountAndExit("Failed to read primary enlistment metadata: " + error);
            }

            try
            {
                if (!RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error))
                {
                    this.FailMountAndExit("Failed to read git objects root from primary metadata: " + error);
                }

                if (!RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error))
                {
                    this.FailMountAndExit("Failed to read local cache root from primary metadata: " + error);
                }

                if (!RepoMetadata.Instance.TryGetBlobSizesRoot(out blobSizesRoot, out error))
                {
                    this.FailMountAndExit("Failed to read blob sizes root from primary metadata: " + error);
                }
            }
            finally
            {
                RepoMetadata.Shutdown();
            }

            // Initialize cache paths on the enlistment so SaveCloneMetadata
            // can persist them into the worktree's metadata
            this.enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot);

            // Initialize the worktree's own metadata with cache paths,
            // disk layout version, and a new enlistment ID
            if (!RepoMetadata.TryInitialize(this.tracer, dotGVFSRoot, out error))
            {
                this.FailMountAndExit("Failed to initialize worktree metadata: " + error);
            }

            try
            {
                RepoMetadata.Instance.SaveCloneMetadata(this.tracer, this.enlistment);
            }
            finally
            {
                RepoMetadata.Shutdown();
            }
        }

        private NamedPipeServer StartNamedPipe()
        {
            try
            {
                return NamedPipeServer.StartNewServer(this.enlistment.NamedPipeName, this.tracer, this.HandleRequest);
            }
            catch (PipeNameLengthException)
            {
                this.FailMountAndExit("Failed to create mount point. Mount path exceeds the maximum number of allowed characters");
                return null;
            }
        }

        private void FailMountAndExit(string error, params object[] args)
        {
            this.FailMountAndExit(ReturnCode.GenericError, error, args);
        }

        private void FailMountAndExit(ReturnCode returnCode, string error, params object[] args)
        {
            this.currentState = MountState.MountFailed;

            this.tracer.RelatedError(error, args);
            if (this.showDebugWindow)
            {
                Console.WriteLine("\nPress Enter to Exit");
                Console.ReadLine();
            }

            if (this.fileSystemCallbacks != null)
            {
                this.fileSystemCallbacks.Dispose();
                this.fileSystemCallbacks = null;
            }

            Environment.Exit((int)returnCode);
        }

        private T CreateOrReportAndExit(Func factory, string reportMessage)
        {
            try
            {
                return factory();
            }
            catch (Exception e)
            {
                this.FailMountAndExit(reportMessage + " " + e.ToString());
                throw;
            }
        }

        private void HandleRequest(ITracer tracer, string request, NamedPipeServer.Connection connection)
        {
            NamedPipeMessages.Message message = NamedPipeMessages.Message.FromString(request);

            switch (message.Header)
            {
                case NamedPipeMessages.GetStatus.Request:
                    this.HandleGetStatusRequest(connection);
                    break;

                case NamedPipeMessages.Unmount.Request:
                    this.HandleUnmountRequest(connection);
                    break;

                case NamedPipeMessages.AcquireLock.AcquireRequest:
                    this.HandleLockRequest(message.Body, connection);
                    break;

                case NamedPipeMessages.ReleaseLock.Request:
                    this.HandleReleaseLockRequest(message.Body, connection);
                    break;

                case NamedPipeMessages.DownloadObject.DownloadRequest:
                    this.HandleDownloadObjectRequest(message, connection);
                    break;

                case NamedPipeMessages.ModifiedPaths.ListRequest:
                    this.HandleModifiedPathsListRequest(message, connection);
                    break;

                case NamedPipeMessages.PostIndexChanged.NotificationRequest:
                    this.HandlePostIndexChangedRequest(message, connection);
                    break;

                case NamedPipeMessages.PrepareForUnstage.Request:
                    this.HandlePrepareForUnstageRequest(message, connection);
                    break;

                case NamedPipeMessages.RunPostFetchJob.PostFetchJob:
                    this.HandlePostFetchJobRequest(message, connection);
                    break;

                case NamedPipeMessages.DehydrateFolders.Dehydrate:
                    this.HandleDehydrateFolders(message, connection);
                    break;

                case NamedPipeMessages.HydrationStatus.Request:
                    this.HandleGetHydrationStatusRequest(connection);
                    break;

                default:
                    EventMetadata metadata = new EventMetadata();
                    metadata.Add("Area", "Mount");
                    metadata.Add("Header", message.Header);
                    this.tracer.RelatedError(metadata, "HandleRequest: Unknown request");

                    connection.TrySendResponse(NamedPipeMessages.UnknownRequest);
                    break;
            }
        }

        private void HandleGetHydrationStatusRequest(NamedPipeServer.Connection connection)
        {
            EnlistmentHydrationSummary summary = this.fileSystemCallbacks?.GetCachedHydrationSummary();
            if (summary == null || !summary.IsValid)
            {
                this.tracer.RelatedInfo(
                    $"{nameof(this.HandleGetHydrationStatusRequest)}: " +
                    (summary == null ? "No cached hydration summary available yet" : "Cached hydration summary is invalid"));

                connection.TrySendResponse(
                    new NamedPipeMessages.Message(NamedPipeMessages.HydrationStatus.NotAvailableResult, null));
                return;
            }

            NamedPipeMessages.HydrationStatus.Response response = new NamedPipeMessages.HydrationStatus.Response
            {
                PlaceholderFileCount = summary.PlaceholderFileCount,
                PlaceholderFolderCount = summary.PlaceholderFolderCount,
                ModifiedFileCount = summary.ModifiedFileCount,
                ModifiedFolderCount = summary.ModifiedFolderCount,
                TotalFileCount = summary.TotalFileCount,
                TotalFolderCount = summary.TotalFolderCount,
            };

            connection.TrySendResponse(
                new NamedPipeMessages.Message(NamedPipeMessages.HydrationStatus.SuccessResult, response.ToBody()));
        }

        private void HandleDehydrateFolders(NamedPipeMessages.Message message, NamedPipeServer.Connection connection)
        {
            NamedPipeMessages.DehydrateFolders.Request request = NamedPipeMessages.DehydrateFolders.Request.FromMessage(message);

            EventMetadata metadata = new EventMetadata();
            metadata.Add(nameof(request.Folders), request.Folders);
            metadata.Add(TracingConstants.MessageKey.InfoMessage, "Received dehydrate folders request");
            this.tracer.RelatedEvent(EventLevel.Informational, nameof(this.HandleDehydrateFolders), metadata);

            NamedPipeMessages.DehydrateFolders.Response response;
            if (this.currentState == MountState.Ready)
            {
                response = new NamedPipeMessages.DehydrateFolders.Response(NamedPipeMessages.DehydrateFolders.DehydratedResult);
                string[] folders = request.Folders.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                StringBuilder resetFolderPaths = new StringBuilder();
                List movedFolders = BackupFoldersWhileUnmounted(request, response, folders);

                foreach (string folder in movedFolders)
                {
                    if (this.fileSystemCallbacks.TryDehydrateFolder(folder, out string errorMessage))
                    {
                        response.SuccessfulFolders.Add(folder);
                    }
                    else
                    {
                        response.FailedFolders.Add($"{folder}\0{errorMessage}");
                    }

                    resetFolderPaths.Append($"\"{folder.Replace(Path.DirectorySeparatorChar, GVFSConstants.GitPathSeparator)}\" ");
                }

                // Since modified paths could have changed with the dehydrate, the paths that were dehydrated need to be reset in the index
                string resetPaths = resetFolderPaths.ToString();
                GitProcess gitProcess = new GitProcess(this.enlistment);

                EventMetadata resetIndexMetadata = new EventMetadata();
                resetIndexMetadata.Add(nameof(resetPaths), resetPaths);

                GitProcess.Result refreshIndexResult;
                this.resetForDehydrateInProgress = true;
                try
                {
                    // Because we've set resetForDehydrateInProgress to true, this call to 'git reset' will also force
                    // the projection to be updated (required because 'git reset' will adjust the skip worktree bits in
                    // the index).
                    refreshIndexResult = gitProcess.Reset(GVFSConstants.DotGit.HeadName, resetPaths);
                }
                finally
                {
                    this.resetForDehydrateInProgress = false;
                }

                resetIndexMetadata.Add(nameof(refreshIndexResult.ExitCode), refreshIndexResult.ExitCode);
                resetIndexMetadata.Add(nameof(refreshIndexResult.Output), refreshIndexResult.Output);
                resetIndexMetadata.Add(nameof(refreshIndexResult.Errors), refreshIndexResult.Errors);
                resetIndexMetadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.HandleDehydrateFolders)}: Reset git index");
                this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.HandleDehydrateFolders)}_ResetIndex", resetIndexMetadata);
            }
            else
            {
                response = new NamedPipeMessages.DehydrateFolders.Response(NamedPipeMessages.DehydrateFolders.MountNotReadyResult);
            }

            connection.TrySendResponse(response.CreateMessage());
        }

        private List BackupFoldersWhileUnmounted(NamedPipeMessages.DehydrateFolders.Request request, NamedPipeMessages.DehydrateFolders.Response response, string[] folders)
        {
            /* We can't move folders while the virtual file system is mounted, so unmount it first.
             * After moving the folders, remount the virtual file system.
             */

            var movedFolders = new List();
            try
            {
                /* Set to "Mounting" instead of "Unmounting" so that incoming requests
                 * that are rejected will know they can try again soon.
                 */
                this.currentState = MountState.Mounting;
                this.UnmountAndStopWorkingDirectoryCallbacks(willRemountInSameProcess: true);
                foreach (string folder in folders)
                {
                    try
                    {
                        var source = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, folder);
                        var destination = Path.Combine(request.BackupFolderPath, folder);
                        var destinationParent = Path.GetDirectoryName(destination);
                        this.context.FileSystem.CreateDirectory(destinationParent);
                        if (this.context.FileSystem.DirectoryExists(source))
                        {
                            this.context.FileSystem.MoveDirectory(source, destination);
                        }
                        movedFolders.Add(folder);
                    }
                    catch (Exception ex)
                    {
                        response.FailedFolders.Add($"{folder}\0{ex.Message}");
                        continue;
                    }
                }
            }
            finally
            {
                this.MountAndStartWorkingDirectoryCallbacks(this.cacheServer, alreadyInitialized: true);
                this.currentState = MountState.Ready;
            }

            return movedFolders;
        }

        private void HandleLockRequest(string messageBody, NamedPipeServer.Connection connection)
        {
            NamedPipeMessages.AcquireLock.Response response;

            NamedPipeMessages.LockRequest request = new NamedPipeMessages.LockRequest(messageBody);
            NamedPipeMessages.LockData requester = request.RequestData;
            if (this.currentState == MountState.Unmounting)
            {
                response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.UnmountInProgressResult);

                EventMetadata metadata = new EventMetadata();
                metadata.Add("LockRequest", requester.ToString());
                metadata.Add(TracingConstants.MessageKey.InfoMessage, "Request denied, unmount in progress");
                this.tracer.RelatedEvent(EventLevel.Informational, "HandleLockRequest_UnmountInProgress", metadata);
            }
            else if (this.currentState != MountState.Ready)
            {
                response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.MountNotReadyResult);
            }
            else
            {
                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);

                if (!requester.CheckAvailabilityOnly && isReadyForExternalLockRequests)
                {
                    lockAcquired = this.context.Repository.GVFSLock.TryAcquireLockForExternalRequestor(requester, out existingExternalHolder);
                }

                if (requester.CheckAvailabilityOnly && lockAvailable && isReadyForExternalLockRequests)
                {
                    response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.AvailableResult);
                }
                else if (lockAcquired)
                {
                    response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.AcceptResult);
                    this.tracer.SetGitCommandSessionId(requester.GitCommandSessionId);
                }
                else if (existingExternalHolder == null)
                {
                    response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.DenyGVFSResult, responseData: null, denyGVFSMessage: denyGVFSMessage);
                }
                else
                {
                    response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.DenyGitResult, existingExternalHolder);
                }
            }

            connection.TrySendResponse(response.CreateMessage());
        }

        private void HandleReleaseLockRequest(string messageBody, NamedPipeServer.Connection connection)
        {
            NamedPipeMessages.LockRequest request = new NamedPipeMessages.LockRequest(messageBody);

            if (request.RequestData == null)
            {
                this.tracer.RelatedError($"{nameof(this.HandleReleaseLockRequest)} received invalid lock request with body '{messageBody}'");
                this.UnmountAndStopWorkingDirectoryCallbacks();
                Environment.Exit((int)ReturnCode.NullRequestData);
            }

            NamedPipeMessages.ReleaseLock.Response response = this.fileSystemCallbacks.TryReleaseExternalLock(request.RequestData.PID);
            if (response.Result == NamedPipeMessages.ReleaseLock.SuccessResult)
            {
                this.tracer.SetGitCommandSessionId(string.Empty);
            }

            connection.TrySendResponse(response.CreateMessage());
        }

        private void HandlePostIndexChangedRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection)
        {
            NamedPipeMessages.PostIndexChanged.Response response;
            NamedPipeMessages.PostIndexChanged.Request request = new NamedPipeMessages.PostIndexChanged.Request(message);
            if (request == null)
            {
                response = new NamedPipeMessages.PostIndexChanged.Response(NamedPipeMessages.UnknownRequest);
            }
            else if (this.currentState != MountState.Ready)
            {
                response = new NamedPipeMessages.PostIndexChanged.Response(NamedPipeMessages.MountNotReadyResult);
            }
            else
            {
                if (this.resetForDehydrateInProgress)
                {
                    // To avoid having to parse the index twice when dehydrating folders, repurpose the PostIndexChangedRequest
                    // for git reset to rebuild the projection.  Additionally, if we were to call ForceIndexProjectionUpdate
                    // directly in HandleDehydrateFolders we'd have a race condition where the OnIndexWriteRequiringModifiedPathsValidation
                    // background task would be trying to parse the index at the same time as HandleDehydrateFolders

                    this.fileSystemCallbacks.ForceIndexProjectionUpdate(invalidateProjection: true, invalidateModifiedPaths: false);
                }
                else
                {
                    this.fileSystemCallbacks.ForceIndexProjectionUpdate(request.UpdatedWorkingDirectory, request.UpdatedSkipWorktreeBits);
                }

                response = new NamedPipeMessages.PostIndexChanged.Response(NamedPipeMessages.PostIndexChanged.SuccessResult);
            }

            connection.TrySendResponse(response.CreateMessage());
        }

        /// 
        /// Handles a request to prepare for an unstage operation (e.g., restore --staged).
        /// Finds index entries that are staged (not in HEAD) with skip-worktree set and adds
        /// them to ModifiedPaths so that git will clear skip-worktree and process them.
        /// Also forces a projection update to fix stale placeholders for modified/deleted files.
        /// 
        private void HandlePrepareForUnstageRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection)
        {
            NamedPipeMessages.PrepareForUnstage.Response response;

            if (this.currentState != MountState.Ready)
            {
                response = new NamedPipeMessages.PrepareForUnstage.Response(NamedPipeMessages.MountNotReadyResult);
            }
            else
            {
                try
                {
                    string pathspec = message.Body;
                    bool success = this.fileSystemCallbacks.AddStagedFilesToModifiedPaths(pathspec, out int addedCount);

                    EventMetadata metadata = new EventMetadata();
                    metadata.Add("addedToModifiedPaths", addedCount);
                    metadata.Add("pathspec", pathspec ?? "(all)");
                    metadata.Add("success", success);
                    this.tracer.RelatedEvent(
                        EventLevel.Informational,
                        nameof(this.HandlePrepareForUnstageRequest),
                        metadata);

                    response = new NamedPipeMessages.PrepareForUnstage.Response(
                        success
                            ? NamedPipeMessages.PrepareForUnstage.SuccessResult
                            : NamedPipeMessages.PrepareForUnstage.FailureResult);
                }
                catch (Exception e)
                {
                    EventMetadata metadata = new EventMetadata();
                    metadata.Add("Exception", e.ToString());
                    this.tracer.RelatedError(metadata, nameof(this.HandlePrepareForUnstageRequest) + " failed");
                    response = new NamedPipeMessages.PrepareForUnstage.Response(NamedPipeMessages.PrepareForUnstage.FailureResult);
                }
            }

            connection.TrySendResponse(response.CreateMessage());
        }

        private void HandleModifiedPathsListRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection)
        {
            NamedPipeMessages.ModifiedPaths.Response response;
            NamedPipeMessages.ModifiedPaths.Request request = new NamedPipeMessages.ModifiedPaths.Request(message);
            if (request == null)
            {
                response = new NamedPipeMessages.ModifiedPaths.Response(NamedPipeMessages.UnknownRequest);
            }
            else if (this.currentState != MountState.Ready)
            {
                response = new NamedPipeMessages.ModifiedPaths.Response(NamedPipeMessages.MountNotReadyResult);
            }
            else
            {
                if (request.Version != NamedPipeMessages.ModifiedPaths.CurrentVersion)
                {
                    response = new NamedPipeMessages.ModifiedPaths.Response(NamedPipeMessages.ModifiedPaths.InvalidVersion);
                }
                else
                {
                    string data = string.Join("\0", this.fileSystemCallbacks.GetAllModifiedPaths()) + "\0";
                    response = new NamedPipeMessages.ModifiedPaths.Response(NamedPipeMessages.ModifiedPaths.SuccessResult, data);
                }
            }

            connection.TrySendResponse(response.CreateMessage());
        }

        private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection)
        {
            NamedPipeMessages.DownloadObject.Response response;

            NamedPipeMessages.DownloadObject.Request request = new NamedPipeMessages.DownloadObject.Request(message);
            string objectSha = request.RequestSha;
            if (this.currentState != MountState.Ready)
            {
                response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.MountNotReadyResult);
            }
            else
            {
                if (!SHA1Util.IsValidShaFormat(objectSha))
                {
                    response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.InvalidSHAResult);
                }
                else
                {
                    Stopwatch downloadTime = Stopwatch.StartNew();

                    /* If this is the root tree for a commit that was was just downloaded, assume that more
                     * trees will be needed soon and download them as well by using the download commit API.
                     *
                     * Otherwise, or as a fallback if the commit download fails, download the object directly.
                     */
                    if (this.ShouldDownloadCommitPack(objectSha, out string commitSha)
                        && this.gitObjects.TryDownloadCommit(commitSha))
                    {
                        this.DownloadedCommitPack(commitSha);
                        response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult);
                        // FUTURE: Should the stats be updated to reflect all the trees in the pack?
                        // FUTURE: Should we try to clean up duplicate trees or increase depth of the commit download?
                    }
                    else if (this.gitObjects.TryDownloadAndSaveObject(objectSha, GVFSGitObjects.RequestSource.NamedPipeMessage) == GitObjects.DownloadAndSaveObjectResult.Success)
                    {
                        this.UpdateTreesForDownloadedCommits(objectSha);
                        response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult);
                    }
                    else
                    {
                        response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.DownloadFailed);
                    }


                    Native.ObjectTypes? objectType;
                    this.context.Repository.TryGetObjectType(objectSha, out objectType);
                    this.context.Repository.GVFSLock.Stats.RecordObjectDownload(objectType == Native.ObjectTypes.Blob, downloadTime.ElapsedMilliseconds);

                    if (objectType == Native.ObjectTypes.Commit
                        && !this.context.Repository.CommitAndRootTreeExists(objectSha, out var treeSha)
                        && !string.IsNullOrEmpty(treeSha))
                    {
                        /* If a commit is downloaded, it wasn't prefetched.
                         * The trees for the commit may be needed soon depending on the context.
                         * e.g. git log (without a pathspec) doesn't need trees, but git checkout does.
                         *
                         * If any prefetch has been done there is probably a similar commit/tree in the graph,
                         * but in case there isn't (such as if the cache server repack maintenance job is failing)
                         * we should still try to avoid downloading an excessive number of loose trees for a commit.
                         *
                         * Save the tree/commit so if more trees are requested we can download all the trees for the commit in a batch.
                         */
                        this.missingTreeTracker.AddMissingRootTree(treeSha: treeSha, commitSha: objectSha);
                    }
                }
            }

            connection.TrySendResponse(response.CreateMessage());
        }

        private bool ShouldDownloadCommitPack(string objectSha, out string commitSha)
        {
            if (!this.missingTreeTracker.TryGetCommits(objectSha, out string[] commitShas))
            {
                commitSha = null;
                return false;
            }

            /* This is a heuristic to prevent downloading multiple packs related to git history commands.
             * Closely related commits are likely to have similar trees, so we'll find fewer missing trees in them.
             * Conversely, if we know (from previously downloaded missing trees) that a commit has a lot of missing
             * trees left, we'll probably need to download many more trees for the commit so we should download the pack.
             */
            int missingTreeCount = this.missingTreeTracker.GetHighestMissingTreeCount(commitShas, out commitSha);

            return missingTreeCount > MissingTreeThresholdForDownloadingCommitPack;
        }

        private void UpdateTreesForDownloadedCommits(string objectSha)
        {
            /* If we are downloading missing trees, we probably are missing more trees for the commit.
             * Update our list of trees associated with the commit so we can use the # of missing trees
             * as a heuristic to decide whether to batch download all the trees for the commit the
             * next time a missing one is requested.
             */
            if (!this.missingTreeTracker.TryGetCommits(objectSha, out _))
            {
                return;
            }

            if (!this.context.Repository.TryGetObjectType(objectSha, out var objectType)
                || objectType != Native.ObjectTypes.Tree)
            {
                return;
            }

            if (this.context.Repository.TryGetMissingSubTrees(objectSha, out var missingSubTrees))
            {
                this.missingTreeTracker.AddMissingSubTrees(objectSha, missingSubTrees);
            }
        }

        private void DownloadedCommitPack(string commitSha)
        {
            this.missingTreeTracker.MarkCommitComplete(commitSha);
        }

        private void HandlePostFetchJobRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection)
        {
            NamedPipeMessages.RunPostFetchJob.Request request = new NamedPipeMessages.RunPostFetchJob.Request(message);

            this.tracer.RelatedInfo("Received post-fetch job request with body {0}", message.Body);

            NamedPipeMessages.RunPostFetchJob.Response response;
            if (this.currentState == MountState.Ready)
            {
                List packIndexes = JsonConvert.DeserializeObject>(message.Body);
                this.maintenanceScheduler.EnqueueOneTimeStep(new PostFetchStep(this.context, packIndexes));

                response = new NamedPipeMessages.RunPostFetchJob.Response(NamedPipeMessages.RunPostFetchJob.QueuedResult);
            }
            else
            {
                response = new NamedPipeMessages.RunPostFetchJob.Response(NamedPipeMessages.RunPostFetchJob.MountNotReadyResult);
            }

            connection.TrySendResponse(response.CreateMessage());
        }

        private void HandleGetStatusRequest(NamedPipeServer.Connection connection)
        {
            NamedPipeMessages.GetStatus.Response response = new NamedPipeMessages.GetStatus.Response();
            response.EnlistmentRoot = this.enlistment.EnlistmentRoot;
            response.LocalCacheRoot = !string.IsNullOrWhiteSpace(this.enlistment.LocalCacheRoot) ? this.enlistment.LocalCacheRoot : this.enlistment.GitObjectsRoot;
            response.RepoUrl = this.enlistment.RepoUrl;
            response.CacheServer = this.cacheServer.ToString();
            response.LockStatus = this.context?.Repository.GVFSLock != null ? this.context.Repository.GVFSLock.GetStatus() : "Unavailable";
            response.DiskLayoutVersion = $"{GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion}.{GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion}";

            switch (this.currentState)
            {
                case MountState.Mounting:
                    response.MountStatus = NamedPipeMessages.GetStatus.Mounting;
                    break;

                case MountState.Ready:
                    response.MountStatus = NamedPipeMessages.GetStatus.Ready;
                    response.BackgroundOperationCount = this.fileSystemCallbacks.BackgroundOperationCount;
                    break;

                case MountState.Unmounting:
                    response.MountStatus = NamedPipeMessages.GetStatus.Unmounting;
                    break;

                case MountState.MountFailed:
                    response.MountStatus = NamedPipeMessages.GetStatus.MountFailed;
                    break;

                default:
                    response.MountStatus = NamedPipeMessages.UnknownGVFSState;
                    break;
            }

            connection.TrySendResponse(response.ToJson());
        }

        private void HandleUnmountRequest(NamedPipeServer.Connection connection)
        {
            switch (this.currentState)
            {
                case MountState.Mounting:
                    connection.TrySendResponse(NamedPipeMessages.Unmount.NotMounted);
                    break;

                // Even if the previous mount failed, attempt to unmount anyway.  Otherwise the user has no
                // recourse but to kill the process.
                case MountState.MountFailed:
                    goto case MountState.Ready;

                case MountState.Ready:
                    this.currentState = MountState.Unmounting;

                    connection.TrySendResponse(NamedPipeMessages.Unmount.Acknowledged);
                    this.UnmountAndStopWorkingDirectoryCallbacks();
                    connection.TrySendResponse(NamedPipeMessages.Unmount.Completed);

                    this.unmountEvent.Set();
                    Environment.Exit((int)ReturnCode.Success);
                    break;

                case MountState.Unmounting:
                    connection.TrySendResponse(NamedPipeMessages.Unmount.AlreadyUnmounting);
                    break;

                default:
                    connection.TrySendResponse(NamedPipeMessages.UnknownGVFSState);
                    break;
            }
        }

        private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache, bool alreadyInitialized = false)
        {
            string error;

            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");

            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);
            }
            else
            {
                this.tracer.RelatedInfo("Git status cache is not enabled");
            }

            this.gvfsDatabase = this.CreateOrReportAndExit(() => new GVFSDatabase(this.context.FileSystem, this.context.Enlistment.EnlistmentRoot, new SqliteDatabase()), "Failed to create database connection");
            this.fileSystemCallbacks = this.CreateOrReportAndExit(
                () =>
                {
                    return new FileSystemCallbacks(
                        this.context,
                        this.gitObjects,
                        RepoMetadata.Instance,
                        blobSizes: null,
                        gitIndexProjection: null,
                        backgroundFileSystemTaskRunner: null,
                        fileSystemVirtualizer: virtualizer,
                        placeholderDatabase: new PlaceholderTable(this.gvfsDatabase),
                        sparseCollection: new SparseTable(this.gvfsDatabase),
                        gitStatusCache: gitStatusCache);
                }, "Failed to create src folder callback listener");
            this.maintenanceScheduler = this.CreateOrReportAndExit(() => new GitMaintenanceScheduler(this.context, this.gitObjects), "Failed to start maintenance scheduler");

            if (!alreadyInitialized)
            {
                int majorVersion;
                int minorVersion;
                if (!RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error))
                {
                    this.FailMountAndExit("Error: {0}", error);
                }

                if (majorVersion != GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion)
                {
                    this.FailMountAndExit(
                        "Error: On disk version ({0}) does not match current version ({1})",
                        majorVersion,
                        GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion);
                }
            }

            try
            {
                if (!this.fileSystemCallbacks.TryStart(out error))
                {
                    this.FailMountAndExit("Error: {0}. \r\nPlease confirm that gvfs clone completed without error.", error);
                }
            }
            catch (Exception e)
            {
                this.FailMountAndExit("Failed to initialize src folder callbacks. {0}", e.ToString());
            }

            this.heartbeat = new HeartbeatThread(this.tracer, this.fileSystemCallbacks);
            this.heartbeat.Start();
        }

        private void ValidateGitVersion()
        {
            GitVersion gitVersion = null;
            if (string.IsNullOrEmpty(this.enlistment.GitBinPath) || !GitProcess.TryGetVersion(this.enlistment.GitBinPath, out gitVersion, out string _))
            {
                this.FailMountAndExit("Error: Unable to retrieve the Git version");
            }

            this.enlistment.SetGitVersion(gitVersion.ToString());

            if (gitVersion.Platform != GVFSConstants.SupportedGitVersion.Platform)
            {
                this.FailMountAndExit("Error: Invalid version of Git {0}. Must use vfs version.", gitVersion);
            }

            if (gitVersion.IsLessThan(GVFSConstants.SupportedGitVersion))
            {
                this.FailMountAndExit(
                    "Error: Installed Git version {0} is less than the minimum supported version of {1}.",
                    gitVersion,
                    GVFSConstants.SupportedGitVersion);
            }
            else if (gitVersion.Revision != GVFSConstants.SupportedGitVersion.Revision)
            {
                this.FailMountAndExit(
                    "Error: Installed Git version {0} has revision number {1} instead of {2}."
                    + " This Git version is too new, so either downgrade Git or upgrade VFS for Git."
                    + " The minimum supported version of Git is {3}.",
                    gitVersion,
                    gitVersion.Revision,
                    GVFSConstants.SupportedGitVersion.Revision,
                    GVFSConstants.SupportedGitVersion);
            }
        }

        private void ValidateHooksVersion()
        {
            string hooksVersion;
            string error;
            if (!GVFSPlatform.Instance.TryGetGVFSHooksVersion(out hooksVersion, out error))
            {
                this.FailMountAndExit(error);
            }

            string gvfsVersion = ProcessHelper.GetCurrentProcessVersion();
            if (hooksVersion != gvfsVersion)
            {
                this.FailMountAndExit("GVFS.Hooks version ({0}) does not match GVFS version ({1}).", hooksVersion, gvfsVersion);
            }

            this.enlistment.SetGVFSHooksVersion(hooksVersion);
        }

        private void ValidateFileSystemSupportsRequiredFeatures()
        {
            try
            {
                string warning;
                string error;
                if (!GVFSPlatform.Instance.KernelDriver.IsSupported(this.enlistment.EnlistmentRoot, out warning, out error))
                {
                    this.FailMountAndExit("Error: {0}", error);
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = new EventMetadata();
                metadata.Add("Exception", e.ToString());
                this.tracer.RelatedError(metadata, "Failed to determine if file system supports features required by GVFS");
                this.FailMountAndExit("Error: Failed to determine if file system supports features required by GVFS.");
            }
        }

        private ServerGVFSConfig QueryAndValidateGVFSConfig()
        {
            ServerGVFSConfig serverGVFSConfig = null;
            string errorMessage = null;

            using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(this.tracer, this.enlistment, this.retryConfig))
            {
                const bool LogErrors = true;
                if (!configRequestor.TryQueryGVFSConfig(LogErrors, out serverGVFSConfig, out _, out errorMessage))
                {
                    // If we have a valid cache server, continue without config (matches verb fallback behavior)
                    if (this.cacheServer != null && !string.IsNullOrWhiteSpace(this.cacheServer.Url))
                    {
                        this.tracer.RelatedWarning("Unable to query /gvfs/config: " + errorMessage);
                        serverGVFSConfig = null;
                    }
                    else
                    {
                        this.FailMountAndExit("Unable to query /gvfs/config" + Environment.NewLine + errorMessage);
                    }
                }
            }

            this.ValidateGVFSVersion(serverGVFSConfig);

            return serverGVFSConfig;
        }

        private void ValidateGVFSVersion(ServerGVFSConfig config)
        {
            using (ITracer activity = this.tracer.StartActivity("ValidateGVFSVersion", EventLevel.Informational))
            {
                if (ProcessHelper.IsDevelopmentVersion())
                {
                    return;
                }

                string recordedVersion = ProcessHelper.GetCurrentProcessVersion();
                int plus = recordedVersion.IndexOf('+');
                Version currentVersion = new Version(plus < 0 ? recordedVersion : recordedVersion.Substring(0, plus));
                IEnumerable allowedGvfsClientVersions =
                    config != null
                    ? config.AllowedGVFSClientVersions
                    : null;

                if (allowedGvfsClientVersions == null || !allowedGvfsClientVersions.Any())
                {
                    string warningMessage = "WARNING: Unable to validate your GVFS version" + Environment.NewLine;
                    if (config == null)
                    {
                        warningMessage += "Could not query valid GVFS versions from: " + Uri.EscapeUriString(this.enlistment.RepoUrl);
                    }
                    else
                    {
                        warningMessage += "Server not configured to provide supported GVFS versions";
                    }

                    this.tracer.RelatedWarning(warningMessage);
                    return;
                }

                foreach (ServerGVFSConfig.VersionRange versionRange in config.AllowedGVFSClientVersions)
                {
                    if (currentVersion >= versionRange.Min &&
                        (versionRange.Max == null || currentVersion <= versionRange.Max))
                    {
                        activity.RelatedEvent(
                            EventLevel.Informational,
                            "GVFSVersionValidated",
                            new EventMetadata
                            {
                                { "SupportedVersionRange", versionRange },
                            });

                        this.enlistment.SetGVFSVersion(currentVersion.ToString());
                        return;
                    }
                }

                activity.RelatedError("GVFS version {0} is not supported", currentVersion);
                this.FailMountAndExit("ERROR: Your GVFS version is no longer supported. Install the latest and try again.");
            }
        }

        private void EnsureLocalCacheIsHealthy(ServerGVFSConfig serverGVFSConfig)
        {
            if (!Directory.Exists(this.enlistment.LocalCacheRoot))
            {
                try
                {
                    this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Local cache root: {this.enlistment.LocalCacheRoot} missing, recreating it");
                    Directory.CreateDirectory(this.enlistment.LocalCacheRoot);
                }
                catch (Exception e)
                {
                    EventMetadata metadata = new EventMetadata();
                    metadata.Add("Exception", e.ToString());
                    metadata.Add("enlistment.LocalCacheRoot", this.enlistment.LocalCacheRoot);
                    this.tracer.RelatedError(metadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create local cache root");
                    this.FailMountAndExit("Failed to create local cache: " + this.enlistment.LocalCacheRoot);
                }
            }

            PhysicalFileSystem fileSystem = new PhysicalFileSystem();
            if (Directory.Exists(this.enlistment.GitObjectsRoot))
            {
                bool gitObjectsRootInAlternates = false;
                string alternatesFilePath = Path.Combine(this.enlistment.DotGitRoot, GVFSConstants.DotGit.Objects.Info.AlternatesRelativePath);
                if (File.Exists(alternatesFilePath))
                {
                    try
                    {
                        using (Stream stream = fileSystem.OpenFileStream(
                            alternatesFilePath,
                            FileMode.Open,
                            FileAccess.Read,
                            FileShare.ReadWrite,
                            callFlushFileBuffers: false))
                        {
                            using (StreamReader reader = new StreamReader(stream))
                            {
                                while (!reader.EndOfStream)
                                {
                                    string alternatesLine = reader.ReadLine();
                                    if (string.Equals(alternatesLine, this.enlistment.GitObjectsRoot, GVFSPlatform.Instance.Constants.PathComparison))
                                    {
                                        gitObjectsRootInAlternates = true;
                                    }
                                }
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        EventMetadata exceptionMetadata = new EventMetadata();
                        exceptionMetadata.Add("Exception", e.ToString());
                        this.tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to validate alternates file");
                        this.FailMountAndExit($"Failed to validate that alternates file includes git objects root: {e.Message}");
                    }
                }
                else
                {
                    this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Alternates file not found");
                }

                if (!gitObjectsRootInAlternates)
                {
                    this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({this.enlistment.GitObjectsRoot}) missing from alternates files, recreating alternates");
                    string error;
                    if (!this.TryCreateAlternatesFile(fileSystem, out error))
                    {
                        this.FailMountAndExit($"Failed to update alternates file to include git objects root: {error}");
                    }
                }
            }
            else
            {
                this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({this.enlistment.GitObjectsRoot}) missing, determining new root");

                if (serverGVFSConfig == null)
                {
                    using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(this.tracer, this.enlistment, this.retryConfig))
                    {
                        string configError;
                        if (!configRequestor.TryQueryGVFSConfig(true, out serverGVFSConfig, out _, out configError))
                        {
                            this.FailMountAndExit("Unable to query /gvfs/config" + Environment.NewLine + configError);
                        }
                    }
                }

                string localCacheKey;
                string error;
                LocalCacheResolver localCacheResolver = new LocalCacheResolver(this.enlistment);
                if (!localCacheResolver.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers(
                    this.tracer,
                    serverGVFSConfig,
                    this.cacheServer,
                    this.enlistment.LocalCacheRoot,
                    localCacheKey: out localCacheKey,
                    errorMessage: out error))
                {
                    this.FailMountAndExit($"Previous git objects root ({this.enlistment.GitObjectsRoot}) not found, and failed to determine new local cache key: {error}");
                }

                EventMetadata keyMetadata = new EventMetadata();
                keyMetadata.Add("localCacheRoot", this.enlistment.LocalCacheRoot);
                keyMetadata.Add("localCacheKey", localCacheKey);
                keyMetadata.Add(TracingConstants.MessageKey.InfoMessage, "Initializing and persisting updated paths");
                this.tracer.RelatedEvent(EventLevel.Informational, "EnsureLocalCacheIsHealthy_InitializePathsFromKey", keyMetadata);
                this.enlistment.InitializeCachePathsFromKey(this.enlistment.LocalCacheRoot, localCacheKey);

                this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating GitObjectsRoot ({this.enlistment.GitObjectsRoot}), GitPackRoot ({this.enlistment.GitPackRoot}), and BlobSizesRoot ({this.enlistment.BlobSizesRoot})");
                try
                {
                    Directory.CreateDirectory(this.enlistment.GitObjectsRoot);
                    Directory.CreateDirectory(this.enlistment.GitPackRoot);
                }
                catch (Exception e)
                {
                    EventMetadata exceptionMetadata = new EventMetadata();
                    exceptionMetadata.Add("Exception", e.ToString());
                    exceptionMetadata.Add("enlistment.GitObjectsRoot", this.enlistment.GitObjectsRoot);
                    exceptionMetadata.Add("enlistment.GitPackRoot", this.enlistment.GitPackRoot);
                    this.tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create objects and pack folders");
                    this.FailMountAndExit("Failed to create objects and pack folders");
                }

                this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating new alternates file");
                if (!this.TryCreateAlternatesFile(fileSystem, out error))
                {
                    this.FailMountAndExit($"Failed to update alternates file with new objects path: {error}");
                }

                this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving git objects root ({this.enlistment.GitObjectsRoot}) in repo metadata");
                RepoMetadata.Instance.SetGitObjectsRoot(this.enlistment.GitObjectsRoot);

                this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving blob sizes root ({this.enlistment.BlobSizesRoot}) in repo metadata");
                RepoMetadata.Instance.SetBlobSizesRoot(this.enlistment.BlobSizesRoot);
            }

            if (!Directory.Exists(this.enlistment.BlobSizesRoot))
            {
                this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: BlobSizesRoot ({this.enlistment.BlobSizesRoot}) not found, re-creating");
                try
                {
                    Directory.CreateDirectory(this.enlistment.BlobSizesRoot);
                }
                catch (Exception e)
                {
                    EventMetadata exceptionMetadata = new EventMetadata();
                    exceptionMetadata.Add("Exception", e.ToString());
                    exceptionMetadata.Add("enlistment.BlobSizesRoot", this.enlistment.BlobSizesRoot);
                    this.tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create blob sizes folder");
                    this.FailMountAndExit("Failed to create blob sizes folder");
                }
            }
        }

        private bool TryCreateAlternatesFile(PhysicalFileSystem fileSystem, out string errorMessage)
        {
            try
            {
                string alternatesFilePath = Path.Combine(this.enlistment.DotGitRoot, GVFSConstants.DotGit.Objects.Info.AlternatesRelativePath);
                string tempFilePath= alternatesFilePath + ".tmp";
                fileSystem.WriteAllText(tempFilePath, this.enlistment.GitObjectsRoot);
                fileSystem.MoveAndOverwriteFile(tempFilePath, alternatesFilePath);
            }
            catch (SecurityException e) { errorMessage = e.Message; return false; }
            catch (IOException e) { errorMessage = e.Message; return false; }

            errorMessage = null;
            return true;
        }

        private bool TrySetRequiredGitConfigSettings()
        {
            Dictionary requiredSettings = RequiredGitConfig.GetRequiredSettings(this.enlistment);

            GitProcess git = new GitProcess(this.enlistment);

            Dictionary existingConfigSettings;
            if (!git.TryGetAllConfig(localOnly: true, configSettings: out existingConfigSettings))
            {
                return false;
            }

            foreach (KeyValuePair setting in requiredSettings)
            {
                GitConfigSetting existingSetting;
                if (setting.Value != null)
                {
                    if (!existingConfigSettings.TryGetValue(setting.Key, out existingSetting) ||
                        !existingSetting.HasValue(setting.Value))
                    {
                        GitProcess.Result setConfigResult = git.SetInLocalConfig(setting.Key, setting.Value);
                        if (setConfigResult.ExitCodeIsFailure)
                        {
                            return false;
                        }
                    }
                }
                else
                {
                    if (existingConfigSettings.TryGetValue(setting.Key, out existingSetting))
                    {
                        git.DeleteFromLocalConfig(setting.Key);
                    }
                }
            }

            return true;
        }

        private void LogEnlistmentInfoAndSetConfigValues()
        {
            string mountId = Guid.NewGuid().ToString("N");
            EventMetadata metadata = new EventMetadata();
            metadata.Add(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId);
            metadata.Add(nameof(mountId), mountId);
            metadata.Add("Enlistment", this.enlistment);
            metadata.Add("PhysicalDiskInfo", GVFSPlatform.Instance.GetPhysicalDiskInfo(this.enlistment.WorkingDirectoryRoot, sizeStatsOnly: false));
            this.tracer.RelatedEvent(EventLevel.Informational, "EnlistmentInfo", metadata, Keywords.Telemetry);

            GitProcess git = new GitProcess(this.enlistment);
            GitProcess.Result configResult = git.SetInLocalConfig(GVFSConstants.GitConfig.EnlistmentId, RepoMetadata.Instance.EnlistmentId, replaceAll: true);
            if (configResult.ExitCodeIsFailure)
            {
                string error = "Could not update config with enlistment id, error: " + configResult.Errors;
                this.tracer.RelatedWarning(error);
            }

            configResult = git.SetInLocalConfig(GVFSConstants.GitConfig.MountId, mountId, replaceAll: true);
            if (configResult.ExitCodeIsFailure)
            {
                string error = "Could not update config with mount id, error: " + configResult.Errors;
                this.tracer.RelatedWarning(error);
            }
        }

        private void UnmountAndStopWorkingDirectoryCallbacks(bool willRemountInSameProcess = false)
        {
            if (this.maintenanceScheduler != null)
            {
                this.maintenanceScheduler.Dispose();
                this.maintenanceScheduler = null;
            }

            if (this.heartbeat != null)
            {
                this.heartbeat.Stop();
                this.heartbeat = null;
            }

            if (this.fileSystemCallbacks != null)
            {
                this.fileSystemCallbacks.Stop();
                this.fileSystemCallbacks.Dispose();
                this.fileSystemCallbacks = null;
            }

            this.gvfsDatabase?.Dispose();
            this.gvfsDatabase = null;

            if (!willRemountInSameProcess)
            {
                this.context?.Dispose();
                this.context = null;
            }
        }
    }
}

================================================
FILE: GVFS/GVFS.Mount/InProcessMountVerb.cs
================================================
using CommandLine;
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Http;
using GVFS.Common.Tracing;
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;

namespace GVFS.Mount
{
    [Verb("mount", HelpText = "Starts the background mount process")]
    public class InProcessMountVerb
    {
        private TextWriter output;

        public InProcessMountVerb()
        {
            this.output = Console.Out;
            this.ReturnCode = ReturnCode.Success;

            this.InitializeDefaultParameterValues();
        }

        public ReturnCode ReturnCode { get; private set; }

        [Option(
            'v',
            GVFSConstants.VerbParameters.Mount.Verbosity,
            Default = GVFSConstants.VerbParameters.Mount.DefaultVerbosity,
            Required = false,
            HelpText = "Sets the verbosity of console logging. Accepts: Verbose, Informational, Warning, Error")]
        public string Verbosity { get; set; }

        [Option(
            'k',
            GVFSConstants.VerbParameters.Mount.Keywords,
            Default = GVFSConstants.VerbParameters.Mount.DefaultKeywords,
            Required = false,
            HelpText = "A CSV list of logging filter keywords. Accepts: Any, Network")]
        public string KeywordsCsv { get; set; }

        [Option(
            'd',
            GVFSConstants.VerbParameters.Mount.DebugWindow,
            Default = false,
            Required = false,
            HelpText = "Show the debug window.  By default, all output is written to a log file and no debug window is shown.")]
        public bool ShowDebugWindow { get; set; }

        [Option(
            's',
            GVFSConstants.VerbParameters.Mount.StartedByService,
            Default = "false",
            Required = false,
            HelpText = "Service initiated mount.")]
        public string StartedByService { get; set; }

        [Option(
            'b',
            GVFSConstants.VerbParameters.Mount.StartedByVerb,
            Default = false,
            Required = false,
            HelpText = "Verb initiated mount.")]
        public bool StartedByVerb { get; set; }

        [Value(
                0,
                Required = true,
                MetaName = "Enlistment Root Path",
                HelpText = "Full or relative path to the GVFS enlistment root")]
        public string EnlistmentRootPathParameter { get; set; }

        public void InitializeDefaultParameterValues()
        {
            this.Verbosity = GVFSConstants.VerbParameters.Mount.DefaultVerbosity;
            this.KeywordsCsv = GVFSConstants.VerbParameters.Mount.DefaultKeywords;
        }

        public void Execute()
        {
            if (this.StartedByVerb)
            {
                // If this process was started by a verb it means that StartBackgroundVFS4GProcess was used
                // and we should be running in the background.  PrepareProcessToRunInBackground will perform
                // any platform specific preparation required to run as a background process.
                GVFSPlatform.Instance.PrepareProcessToRunInBackground();
            }

            GVFSEnlistment enlistment = this.CreateEnlistment(this.EnlistmentRootPathParameter);

            EventLevel verbosity;
            Keywords keywords;
            this.ParseEnumArgs(out verbosity, out keywords);

            JsonTracer tracer = this.CreateTracer(enlistment, verbosity, keywords);

            CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment);

            tracer.WriteStartEvent(
                enlistment.EnlistmentRoot,
                enlistment.RepoUrl,
                cacheServer.Url,
                new EventMetadata
                {
                    { "IsElevated", GVFSPlatform.Instance.IsElevated() },
                    { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter },
                    { nameof(this.StartedByService), this.StartedByService },
                    { nameof(this.StartedByVerb), this.StartedByVerb },
                });

            AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) =>
            {
                this.UnhandledGVFSExceptionHandler(tracer, sender, e);
            };

            string error;
            RetryConfig retryConfig;
            if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error))
            {
                this.ReportErrorAndExit(tracer, "Failed to determine GVFS timeout and max retries: " + error);
            }

            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
            {
                mountHelper.Mount(verbosity, keywords);
            }
            catch (Exception ex)
            {
                this.ReportErrorAndExit(tracer, "Failed to mount: {0}", ex.Message);
            }
        }

        private void UnhandledGVFSExceptionHandler(ITracer tracer, object sender, UnhandledExceptionEventArgs e)
        {
            Exception exception = e.ExceptionObject as Exception;

            EventMetadata metadata = new EventMetadata();
            metadata.Add("Exception", exception.ToString());
            metadata.Add("IsTerminating", e.IsTerminating);
            tracer.RelatedError(metadata, "UnhandledGVFSExceptionHandler caught unhandled exception");
        }

        private JsonTracer CreateTracer(GVFSEnlistment enlistment, EventLevel verbosity, Keywords keywords)
        {
            JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "GVFSMount", enlistment.GetEnlistmentId(), enlistment.GetMountId());
            tracer.AddLogFileEventListener(
                GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.MountProcess),
                verbosity,
                keywords);
            if (this.ShowDebugWindow)
            {
                tracer.AddDiagnosticConsoleEventListener(verbosity, keywords);
            }

            return tracer;
        }

        private void ParseEnumArgs(out EventLevel verbosity, out Keywords keywords)
        {
            if (!Enum.TryParse(this.KeywordsCsv, out keywords))
            {
                this.ReportErrorAndExit("Error: Invalid logging filter keywords: " + this.KeywordsCsv);
            }

            if (!Enum.TryParse(this.Verbosity, out verbosity))
            {
                this.ReportErrorAndExit("Error: Invalid logging verbosity: " + this.Verbosity);
            }
        }

        private GVFSEnlistment CreateEnlistment(string enlistmentRootPath)
        {
            string gitBinPath = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath();
            if (string.IsNullOrWhiteSpace(gitBinPath))
            {
                this.ReportErrorAndExit("Error: " + GVFSConstants.GitIsNotInstalledError);
            }

            GVFSEnlistment enlistment = null;
            try
            {
                enlistment = GVFSEnlistment.CreateFromDirectory(enlistmentRootPath, gitBinPath, authentication: null);
            }
            catch (InvalidRepoException e)
            {
                this.ReportErrorAndExit(
                    "Error: '{0}' is not a valid GVFS enlistment. {1}",
                    enlistmentRootPath,
                    e.Message);
            }

            return enlistment;
        }

        private void ReportErrorAndExit(string error, params object[] args)
        {
            this.ReportErrorAndExit(null, error, args);
        }

        private void ReportErrorAndExit(ITracer tracer, string error, params object[] args)
        {
            if (tracer != null)
            {
                tracer.RelatedError(error, args);
            }

            if (error != null)
            {
                this.output.WriteLine(error, args);
            }

            if (this.ShowDebugWindow)
            {
                Console.WriteLine("\nPress Enter to Exit");
                Console.ReadLine();
            }

            this.ReturnCode = ReturnCode.GenericError;
            throw new MountAbortedException(this);
        }
    }
}


================================================
FILE: GVFS/GVFS.Mount/MountAbortedException.cs
================================================
using System;

namespace GVFS.Mount
{
    public class MountAbortedException : Exception
    {
        public MountAbortedException(InProcessMountVerb verb)
        {
            this.Verb = verb;
        }

        public InProcessMountVerb Verb { get; }
    }
}


================================================
FILE: GVFS/GVFS.Mount/Program.cs
================================================
using CommandLine;
using GVFS.PlatformLoader;
using System;

namespace GVFS.Mount
{
    public class Program
    {
        public static void Main(string[] args)
        {
            GVFSPlatformLoader.Initialize();
            try
            {
                Parser.Default.ParseArguments(args)
                    .WithParsed(mount => mount.Execute());
            }
            catch (MountAbortedException e)
            {
                // Calling Environment.Exit() is required, to force all background threads to exit as well
                Environment.Exit((int)e.Verb.ReturnCode);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.NativeHooks.Common/common.h
================================================
#pragma once

#include 
#include 

#ifdef __APPLE__
typedef std::string PATH_STRING;
typedef int PIPE_HANDLE;
#define PRINTF_FMT(X, Y) __attribute__((__format__ (printf, X, Y)))
#elif _WIN32
typedef std::wstring PATH_STRING;
typedef HANDLE PIPE_HANDLE;
#define PRINTF_FMT(X, Y)
#else
#error Unsupported platform
#endif

#if __cplusplus <  201103L
  #error The hooks require at least C++11 support
#endif

enum ReturnCode
{
	Success = 0,
	InvalidArgCount = 1,
	GetCurrentDirectoryFailure = 2,
	NotInGVFSEnlistment = 3,
	PipeConnectError = 4,
	PipeConnectTimeout = 5,
	InvalidSHA = 6,
	PipeWriteFailed = 7,
	PipeReadFailed = 8,
	FailureToDownload = 9,
	PathNameError = 10,

	LastError = PathNameError,	
};

void die(int err, const char *fmt, ...) PRINTF_FMT(2,3);
inline void die(int err, const char *fmt, ...)
{
	va_list params;
	va_start(params, fmt);
	vfprintf(stderr, fmt, params);
	va_end(params);
	exit(err);
}

PATH_STRING GetFinalPathName(const PATH_STRING& path);
PATH_STRING GetGVFSPipeName(const char *appName);
PIPE_HANDLE CreatePipeToGVFS(const PATH_STRING& pipeName);
void DisableCRLFTranslationOnStdPipes();

bool WriteToPipe(
    PIPE_HANDLE pipe, 
    const char* message, 
    unsigned long messageLength, 
    /* out */ unsigned long* bytesWritten, 
    /* out */ int* error);

bool ReadFromPipe(
    PIPE_HANDLE pipe, 
    char* buffer, 
    unsigned long bufferLength, 
    /* out */ unsigned long* bytesRead, 
    /* out */ int* error);


================================================
FILE: GVFS/GVFS.NativeHooks.Common/common.windows.cpp
================================================
#pragma once
#include "stdafx.h"
#include 
#include 
#include 
#include "common.h"

PATH_STRING GetFinalPathName(const PATH_STRING& path)
{
    HANDLE fileHandle;

    // Using FILE_FLAG_BACKUP_SEMANTICS as it works with file as well as folder path
    // According to MSDN, https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx,
    // we must set this flag to obtain a handle to a directory
    fileHandle = CreateFileW(
        path.c_str(),
        FILE_READ_ATTRIBUTES,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS,
        NULL);

    if (fileHandle == INVALID_HANDLE_VALUE)
    {
        die(ReturnCode::PathNameError, "Could not open oppen handle to %ls to determine final path name, Error: %d\n", path.c_str(), GetLastError());
    }

    wchar_t finalPathByHandle[MAX_PATH] = { 0 };
    DWORD finalPathSize = GetFinalPathNameByHandleW(fileHandle, finalPathByHandle, MAX_PATH, FILE_NAME_NORMALIZED);
    if (finalPathSize == 0)
    {
        die(ReturnCode::PathNameError, "Could not get final path name by handle for %ls, Error: %d\n", path.c_str(), GetLastError());
    }

    std::wstring finalPath(finalPathByHandle);

    // The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\" or "\\?\UNC\"
    // More information the prefixes is here http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx
    std::wstring PathPrefix(L"\\\\?\\");
    std::wstring UncPrefix(L"\\\\?\\UNC\\");

    if (finalPath.compare(0, UncPrefix.length(), UncPrefix) == 0)
    {
        finalPath = L"\\\\" + finalPath.substr(UncPrefix.length());
    }
    else if (finalPath.compare(0, PathPrefix.length(), PathPrefix) == 0)
    {
        finalPath = finalPath.substr(PathPrefix.length());
    }

    return finalPath;
}

// Checks if the given directory is a git worktree by looking for a
// ".git" file (not directory). If found, reads it to extract the
// worktree name and returns a pipe name suffix like "_WT_NAME".
// Returns an empty string if not in a worktree.
PATH_STRING GetWorktreePipeSuffix(const wchar_t* directory)
{
    PATH_STRING dotGitPath(directory);
    if (!dotGitPath.empty() && dotGitPath.back() != L'\\')
        dotGitPath += L'\\';
    dotGitPath += L".git";

    DWORD dotGitAttrs = GetFileAttributesW(dotGitPath.c_str());
    if (dotGitAttrs == INVALID_FILE_ATTRIBUTES ||
        (dotGitAttrs & FILE_ATTRIBUTE_DIRECTORY))
    {
        return PATH_STRING();
    }

    // .git is a file — this is a worktree. Read it to find the
    // worktree git directory (format: "gitdir: ")
    FILE* gitFile = NULL;
    errno_t fopenResult = _wfopen_s(&gitFile, dotGitPath.c_str(), L"r");
    if (fopenResult != 0 || gitFile == NULL)
        return PATH_STRING();

    char gitdirLine[4096];
    if (fgets(gitdirLine, sizeof(gitdirLine), gitFile) == NULL)
    {
        fclose(gitFile);
        return PATH_STRING();
    }
    fclose(gitFile);

    char* gitdirPath = gitdirLine;
    if (strncmp(gitdirPath, "gitdir: ", 8) == 0)
        gitdirPath += 8;

    // Trim trailing whitespace
    size_t lineLen = strlen(gitdirPath);
    while (lineLen > 0 && (gitdirPath[lineLen - 1] == '\n' ||
           gitdirPath[lineLen - 1] == '\r' ||
           gitdirPath[lineLen - 1] == ' '))
        gitdirPath[--lineLen] = '\0';

    // Extract worktree name — last path component
    // e.g., from ".git/worktrees/my-worktree" extract "my-worktree"
    char* lastSep = strrchr(gitdirPath, '/');
    if (!lastSep)
        lastSep = strrchr(gitdirPath, '\\');

    if (lastSep == NULL)
        return PATH_STRING();

    std::string nameUtf8(lastSep + 1);
    int wideLen = MultiByteToWideChar(CP_UTF8, 0, nameUtf8.c_str(), -1, NULL, 0);
    if (wideLen <= 0)
        return PATH_STRING();

    std::wstring wtName(wideLen, L'\0');
    MultiByteToWideChar(CP_UTF8, 0, nameUtf8.c_str(), -1, &wtName[0], wideLen);
    wtName.resize(wideLen - 1); // remove null terminator from string

    PATH_STRING suffix = L"_WT_";
    suffix += wtName;
    return suffix;
}

PATH_STRING GetGVFSPipeName(const char *appName)
{
    // The pipe name is built using the path of the GVFS enlistment root.
    // Start in the current directory and walk up the directory tree
    // until we find a folder that contains the ".gvfs" folder.
    // For worktrees, a suffix is appended to target the worktree's mount.

    const size_t dotGVFSRelativePathLength = sizeof(L"\\.gvfs") / sizeof(wchar_t);

    // TODO 640838: Support paths longer than MAX_PATH
    wchar_t enlistmentRoot[MAX_PATH];
    DWORD currentDirResult = GetCurrentDirectoryW(MAX_PATH - dotGVFSRelativePathLength, enlistmentRoot);
    if (currentDirResult == 0 || currentDirResult > MAX_PATH - dotGVFSRelativePathLength)
    {
        die(ReturnCode::GetCurrentDirectoryFailure, "GetCurrentDirectory failed (%d)\n", GetLastError());
    }

    PATH_STRING finalRootPath(GetFinalPathName(enlistmentRoot));
    errno_t copyResult = wcscpy_s(enlistmentRoot, finalRootPath.c_str());
    if (copyResult != 0)
    {
        die(ReturnCode::PipeConnectError, "Could not copy finalRootPath: %ls. Error: %d\n", finalRootPath.c_str(), copyResult);
    }

    size_t enlistmentRootLength = wcslen(enlistmentRoot);
    if ('\\' != enlistmentRoot[enlistmentRootLength - 1])
    {
        wcscat_s(enlistmentRoot, L"\\");
        enlistmentRootLength++;
    }

    // Walk up enlistmentRoot looking for a folder named .gvfs
    wchar_t* lastslash = enlistmentRoot + enlistmentRootLength - 1;
    WIN32_FIND_DATAW findFileData;
    HANDLE dotGVFSHandle;
    while (1)
    {
        wcscat_s(lastslash, MAX_PATH - (lastslash - enlistmentRoot), L".gvfs");
        dotGVFSHandle = FindFirstFileW(enlistmentRoot, &findFileData);
        if (dotGVFSHandle != INVALID_HANDLE_VALUE)
        {
            FindClose(dotGVFSHandle);
            if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
            {
                break;
            }
        }

        lastslash--;
        while ((enlistmentRoot != lastslash) && (*lastslash != '\\'))
        {
            lastslash--;
        }

        if (enlistmentRoot == lastslash)
        {
            die(ReturnCode::NotInGVFSEnlistment, "%s must be run from inside a GVFS enlistment\n", appName);
        }

        *(lastslash + 1) = 0;
    };

    *(lastslash) = 0;

    PATH_STRING namedPipe(CharUpperW(enlistmentRoot));
    std::replace(namedPipe.begin(), namedPipe.end(), L':', L'_');
    PATH_STRING pipeName = L"\\\\.\\pipe\\GVFS_" + namedPipe;

    // Append worktree suffix if running in a worktree
    PATH_STRING worktreeSuffix = GetWorktreePipeSuffix(finalRootPath.c_str());
    if (!worktreeSuffix.empty())
    {
        std::transform(worktreeSuffix.begin(), worktreeSuffix.end(),
                       worktreeSuffix.begin(), ::towupper);
        pipeName += worktreeSuffix;
    }

    return pipeName;
}

PIPE_HANDLE CreatePipeToGVFS(const PATH_STRING& pipeName)
{
    PIPE_HANDLE pipeHandle;
    while (1)
    {
        pipeHandle = CreateFileW(
            pipeName.c_str(), // pipe name 
            GENERIC_READ |     // read and write access 
            GENERIC_WRITE,
            0,                 // no sharing 
            NULL,              // default security attributes
            OPEN_EXISTING,     // opens existing pipe 
            0,                 // default attributes 
            NULL);             // no template file 

        if (pipeHandle != INVALID_HANDLE_VALUE)
        {
            break;
        }

        if (GetLastError() != ERROR_PIPE_BUSY)
        {
            die(ReturnCode::PipeConnectError, "Could not open pipe: %ls, Error: %d\n", pipeName.c_str(), GetLastError());
        }

        if (!WaitNamedPipeW(pipeName.c_str(), 3000))
        {
            die(ReturnCode::PipeConnectTimeout, "Could not open pipe: %ls, Timed out.", pipeName.c_str());
        }
    }

    return pipeHandle;
}

void DisableCRLFTranslationOnStdPipes()
{
    // set the mode to binary so we don't get CRLF translation
    _setmode(_fileno(stdin), _O_BINARY);
    _setmode(_fileno(stdout), _O_BINARY);
}

bool WriteToPipe(PIPE_HANDLE pipe, const char* message, unsigned long messageLength, /* out */ unsigned long* bytesWritten, /* out */ int* error)
{
    BOOL success = WriteFile(
        pipe,                   // pipe handle 
        message,                // message 
        messageLength,          // message length 
        bytesWritten,           // bytes written 
        NULL);                  // not overlapped
    
    *error = success ? 0 : GetLastError();

    return success != FALSE;
}

bool ReadFromPipe(PIPE_HANDLE pipe, char* buffer, unsigned long bufferLength, /* out */ unsigned long* bytesRead, /* out */ int* error)
{
    *error = 0;
    *bytesRead = 0;
    BOOL success = ReadFile(
        pipe,		    	// pipe handle 
        buffer,			    // buffer to receive reply 
        bufferLength,	    // size of buffer 
        bytesRead,         // number of bytes read 
        NULL);              // not overlapped 

    if (!success)
    {
        *error = GetLastError();
    }

    return success || (*error == ERROR_MORE_DATA);
}

================================================
FILE: GVFS/GVFS.NativeTests/FileUtils.cpp
================================================
#include "stdafx.h"
#include "FileUtils.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "TestVerifiers.h"
#include "Should.h"

using namespace TestHelpers;

bool SupersedeFile(const wchar_t* path, const char* newContent)
{
    try
    {
        HANDLE handle;
        OBJECT_ATTRIBUTES attributes;
        UNICODE_STRING fullPath;
        IO_STATUS_BLOCK statusBlock;

        std::wstring pathBuilder(L"\\??\\");
        pathBuilder += path;

        RtlInitUnicodeString(&fullPath, pathBuilder.c_str());
        InitializeObjectAttributes(&attributes, &fullPath, 0, NULL, NULL);

        NTSTATUS result = NtCreateFile(
            &handle,
            DELETE | FILE_GENERIC_WRITE | FILE_GENERIC_READ,
            &attributes,
            &statusBlock,
            NULL,
            FILE_ATTRIBUTE_NORMAL,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            FILE_SUPERSEDE,
            FILE_SYNCHRONOUS_IO_NONALERT,
            NULL,
            0);
        SHOULD_EQUAL(result, STATUS_SUCCESS);

        std::string writeContent(newContent);
        result = NtWriteFile(
            handle,
            NULL,
            NULL,
            NULL,
            &statusBlock,
            (PVOID)writeContent.c_str(),
            static_cast(writeContent.length()),
            NULL,
            NULL);

        SHOULD_EQUAL(result, STATUS_SUCCESS);
        SHOULD_EQUAL(statusBlock.Information, writeContent.length());

        NtClose(handle);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

================================================
FILE: GVFS/GVFS.NativeTests/FileUtils.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool SupersedeFile(const wchar_t* path, const char* newContent);
}

================================================
FILE: GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj
================================================


  
    GVFS.ProjFS.2019.411.1
  
  
    
      Debug
      x64
    
    
      Release
      x64
    
  
  
    {3771C555-B5C1-45E2-B8B7-2CEF1619CDC5}
    Win32Proj
    GVFSNativeTests
    10.0
  
  
  
    DynamicLibrary
    true
    v143
    NotSet
  
  
    DynamicLibrary
    false
    v143
    true
    NotSet
  
  
  
  
  
  
  
    
  
  
    
  
  
  
    true
  
  
    false
  
  
    
      Use
      Level4
      Disabled
      _DEBUG;_WINDOWS;_USRDLL;GVFSNATIVETESTS_EXPORTS;%(PreprocessorDefinitions)
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0\ucrt;$(MSBuildProjectDirectory)\include;$(MSBuildProjectDirectory)\interface;%(AdditionalIncludeDirectories)
    
    
      Windows
      true
      ProjectedFSLib.lib;fltlib.lib;Shlwapi.lib;%(AdditionalDependencies)
      C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64;$(BuildPackagesPath)$(ProjFSNativePackage)\lib
    
  
  
    
      Level4
      Use
      MaxSpeed
      true
      true
      NDEBUG;_WINDOWS;_USRDLL;GVFSNATIVETESTS_EXPORTS;%(PreprocessorDefinitions)
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0\ucrt;$(MSBuildProjectDirectory)\include;$(MSBuildProjectDirectory)\interface;%(AdditionalIncludeDirectories)
    
    
      Windows
      true
      true
      true
      ProjectedFSLib.lib;fltlib.lib;Shlwapi.lib;%(AdditionalDependencies)
      C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64;$(BuildPackagesPath)$(ProjFSNativePackage)\lib
    
  
  
    
  
  
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
  
  
    
    
      false
      
      
      false
      
      
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
      Create
      Create
    
  
  
    
  
  


================================================
FILE: GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj.filters
================================================


  
    
      {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
      rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
    
    
      {58670690-8e03-4f0a-bd9d-b0f82f02ff5f}
    
    
      {93586a9a-4ffd-42b8-966e-28d0fa780751}
    
    
      {fa2786cf-49d1-44fc-b4b0-1a77e860d7b8}
    
  
  
    
  
  
    
      include
    
    
      include
    
    
      include
    
    
      include
    
    
      interface
    
    
      include
    
    
      include
    
    
      interface
    
    
      include
    
    
      include
    
    
      interface
    
    
      interface
    
    
      include
    
    
      interface
    
    
      include
    
    
      interface
    
    
      interface
    
    
      interface
    
    
      interface
    
    
      interface
    
    
      interface
    
    
      interface
    
    
      interface
    
    
      interface
    
    
      interface
    
    
      interface
    
    
      include
    
  
  
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
    
      source
    
  
  
    
  


================================================
FILE: GVFS/GVFS.NativeTests/ReadMe.txt
================================================
========================================================================
GVFS.NativeTests
========================================================================

Summary:

GVFS.NativeTests is a library used by GVFS.FunctionalTests to run GVFS
functional tests using the native WinAPI.

The GVFS.NativeTests dll is output into the appropriate GVFS.FunctionalTests
directory so that the dll can be found when it is DllImported by GVFS.NativeTests.


Folder Structure:

interface -> Header files that are consumable by projects outside of GVFS.NativeTests
include   -> Header files that are internal to GVFS.NativeTests
source    -> GVFS.NativeTests source code


Debugging:

To step through tests in GVFS.NativeTests and to set breakpoints, ensure that the 
"Enable native code debugging" setting is checked in the GVFS.FunctionalTests 
project properites (Debug tab)

================================================
FILE: GVFS/GVFS.NativeTests/include/NtFunctions.h
================================================
#pragma once

#define InitializeObjectAttributes( p, n, a, r, s ) {   \
    (p)->Length = sizeof( OBJECT_ATTRIBUTES );          \
    (p)->RootDirectory = r;                             \
    (p)->Attributes = a;                                \
    (p)->ObjectName = n;                                \
    (p)->SecurityDescriptor = s;                        \
    (p)->SecurityQualityOfService = NULL;               \
    }

NTSTATUS NtQueryDirectoryFile(
    _In_     HANDLE                 FileHandle,
    _In_opt_ HANDLE                 Event,
    _In_opt_ PIO_APC_ROUTINE        ApcRoutine,
    _In_opt_ PVOID                  ApcContext,
    _Out_    PIO_STATUS_BLOCK       IoStatusBlock,
    _Out_    PVOID                  FileInformation,
    _In_     ULONG                  Length,
    _In_     FILE_INFORMATION_CLASS FileInformationClass,
    _In_     BOOLEAN                ReturnSingleEntry,
    _In_opt_ PUNICODE_STRING        FileName,
    _In_     BOOLEAN                RestartScan
);

NTSTATUS NtCreateFile(
    _Out_    PHANDLE            FileHandle,
    _In_     ACCESS_MASK        DesiredAccess,
    _In_     POBJECT_ATTRIBUTES ObjectAttributes,
    _Out_    PIO_STATUS_BLOCK   IoStatusBlock,
    _In_opt_ PLARGE_INTEGER     AllocationSize,
    _In_     ULONG              FileAttributes,
    _In_     ULONG              ShareAccess,
    _In_     ULONG              CreateDisposition,
    _In_     ULONG              CreateOptions,
    _In_     PVOID              EaBuffer,
    _In_     ULONG              EaLength
);

NTSTATUS NtClose(
    _In_ HANDLE Handle
);

NTSTATUS NtWriteFile(
    _In_     HANDLE           FileHandle,
    _In_opt_ HANDLE           Event,
    _In_opt_ PIO_APC_ROUTINE  ApcRoutine,
    _In_opt_ PVOID            ApcContext,
    _Out_    PIO_STATUS_BLOCK IoStatusBlock,
    _In_     PVOID            Buffer,
    _In_     ULONG            Length,
    _In_opt_ PLARGE_INTEGER   ByteOffset,
    _In_opt_ PULONG           Key
);

VOID WINAPI RtlInitUnicodeString(
    _Inout_  PUNICODE_STRING DestinationString,
    _In_opt_ PCWSTR          SourceString
);


================================================
FILE: GVFS/GVFS.NativeTests/include/SafeHandle.h
================================================
#pragma once

// Wrapper for HANDLE that calls CloseHandle when destroyed
class SafeHandle
{
public:
    SafeHandle(HANDLE handle);
    ~SafeHandle();

    HANDLE GetHandle();
    void CloseHandle();

private:
    HANDLE handle;
};

inline SafeHandle::SafeHandle(HANDLE handle)
{
    this->handle = handle;
}

inline SafeHandle::~SafeHandle()
{
    if (this->handle != NULL)
    {
        this->CloseHandle();
    }
}

inline HANDLE SafeHandle::GetHandle()
{
    return this->handle;
}

inline void SafeHandle::CloseHandle()
{
    if (this->handle != NULL && this->handle != INVALID_HANDLE_VALUE)
    {
        ::CloseHandle(this->handle);
        this->handle = NULL;
    }
}

================================================
FILE: GVFS/GVFS.NativeTests/include/SafeOverlapped.h
================================================
#pragma once

// Wrapper for OVERLAPPED that calls CloseHandle on the OVERLAPPED's hEvent when destroyed
struct SafeOverlapped
{
    SafeOverlapped();
    ~SafeOverlapped();

    OVERLAPPED overlapped;
};

inline SafeOverlapped::SafeOverlapped()
{
    memset(&this->overlapped, 0, sizeof(OVERLAPPED));
}

inline SafeOverlapped::~SafeOverlapped()
{
    if (this->overlapped.hEvent != NULL)
    {
        CloseHandle(this->overlapped.hEvent);
    }
}

================================================
FILE: GVFS/GVFS.NativeTests/include/Should.h
================================================
#pragma once

#include "TestException.h"

#define STRINGIFY(X) #X
#define EXPAND_AND_STRINGIFY(X) STRINGIFY(X)

#define SHOULD_BE_TRUE(expr)                                                                                  \
do {                                                                                                          \
    if(!(expr))                                                                                               \
    {                                                                                                         \
        if(IsDebuggerPresent())                                                                               \
        {                                                                                                     \
            assert(expr);                                                                                     \
        }                                                                                                     \
        throw TestException("Failure on line:" EXPAND_AND_STRINGIFY(__LINE__) ", in function:" __FUNCTION__); \
    }                                                                                                         \
} while (0)

#define SHOULD_EQUAL(P1, P2) SHOULD_BE_TRUE((P1) == (P2))
#define SHOULD_NOT_EQUAL(P1, P2) SHOULD_BE_TRUE((P1) != (P2))

#define FAIL_TEST(msg)                                  \
do {                                                    \
    if (IsDebuggerPresent())                            \
    {                                                   \
        assert(false);                                  \
    }                                                   \
    throw TestException(msg);                           \
} while (0)


================================================
FILE: GVFS/GVFS.NativeTests/include/TestException.h
================================================
#pragma once

class TestException : public std::exception
{

public:
    TestException(const std::string& message);
    virtual ~TestException();
    virtual const char* what() const override;

private:
    std::string message;
};

inline TestException::TestException(const std::string& message)
    : message(message)
{
}

inline TestException::~TestException()
{
}

inline const char* TestException::what() const
{
    return this->message.c_str();
}


================================================
FILE: GVFS/GVFS.NativeTests/include/TestHelpers.h
================================================
#pragma once

#include "Should.h"
#include "prjlib_internal.h"

// Map ProjFS testing macros to GVFS testing macros
#define VERIFY_ARE_EQUAL SHOULD_EQUAL
#define VERIFY_ARE_NOT_EQUAL SHOULD_NOT_EQUAL
#define VERIFY_FAIL FAIL_TEST

static const DWORD MAX_BUF_SIZE = 256;

struct FileInfo
{
    std::string Name;
    bool IsFile = true;
    DWORD FileSize = 0;
};

namespace TestHelpers
{

inline std::shared_ptr OpenForRead(const std::string& path)
{
    std::shared_ptr handle(
        CreateFile(path.c_str(),
            GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS,
            NULL),
        CloseHandle);

    if (INVALID_HANDLE_VALUE == handle.get()) {
        VERIFY_FAIL("failed to open file for read");
    }

    VERIFY_ARE_NOT_EQUAL(INVALID_HANDLE_VALUE, handle.get());
    return handle;
}

inline std::vector EnumDirectory(const std::string& path)
{
    WIN32_FIND_DATA ffd;

    std::vector result;

    std::string query = path + "*";

    HANDLE hFind = FindFirstFile(query.c_str(), &ffd);

    if (hFind == INVALID_HANDLE_VALUE)
    {
        VERIFY_FAIL("FindFirstFile failed");
    }

    do
    {
        FileInfo fileInfo;
        fileInfo.Name = ffd.cFileName;

        if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        {
            fileInfo.IsFile = false;
        }
        else
        {
            fileInfo.FileSize = ffd.nFileSizeLow;
        }

        result.push_back(fileInfo);
    } while (FindNextFile(hFind, &ffd) != 0);

    DWORD dwError = GetLastError();
    if (dwError != ERROR_NO_MORE_FILES)
    {
        VERIFY_FAIL("FindNextFile failed");
    }

    FindClose(hFind);

    return result;
}

inline void WriteToFile(const std::string& path, const std::string content, bool isNewFile = false)
{
    HANDLE hFile = CreateFile(path.c_str(),
        GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        isNewFile ? CREATE_NEW : OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    if (hFile == INVALID_HANDLE_VALUE)
    {
        VERIFY_FAIL("CreateFile failed");
    }

    if (content.empty())
    {
        goto CleanUp;
    }

    DWORD dwBytesToWrite = (DWORD)content.size() * sizeof(content[0]);
    DWORD dwBytesWritten = 0;

    VERIFY_ARE_EQUAL(TRUE, WriteFile(
        hFile,           // open file handle
        content.c_str(), // start of data to write
        dwBytesToWrite,  // number of bytes to write
        &dwBytesWritten, // number of bytes that were written
        NULL));

    VERIFY_ARE_EQUAL(dwBytesToWrite, dwBytesWritten);

    VERIFY_ARE_EQUAL(TRUE, FlushFileBuffers(hFile));

CleanUp:
    CloseHandle(hFile);
}

inline void CreateNewFile(const std::string& path, const std::string content)
{
    WriteToFile(path, content, true);
}

inline void CreateNewFile(const std::string& path)
{
    CreateNewFile(path, "");
}

inline DWORD DelFile(const std::string& path, bool isSetDisposition = true)
{
    if (isSetDisposition) {
        BOOL success = DeleteFile(path.c_str());
        if (success) {
            return ERROR_SUCCESS;
        }
        else {
            return GetLastError();
        }
    }

    // delete on close
    HANDLE handle = CreateFile(
        path.c_str(),
        GENERIC_READ | GENERIC_WRITE,
        0,
        0,
        OPEN_EXISTING,
        FILE_FLAG_DELETE_ON_CLOSE,
        NULL);

    if (handle == INVALID_HANDLE_VALUE) {
        return GetLastError();
    }

    BOOL success = CloseHandle(handle);
    if (!success) {
        return GetLastError();
    }

    return ERROR_SUCCESS;
}

inline std::shared_ptr GetReparseInfo(const std::string& path)
{
    USHORT dataSize = MAXIMUM_REPARSE_DATA_BUFFER_SIZE;
    std::shared_ptr reparseInfo((PGV_REPARSE_INFO)calloc(1, dataSize), free);

    std::wstring_convert> utf16conv;
    
    ULONG reparseTag;
    HRESULT hr = PrjpReadPrjReparsePointData(utf16conv.from_bytes(path).c_str(), reparseInfo.get(), &reparseTag, &dataSize);
    if (FAILED(hr)) {
        if (hr == HRESULT_FROM_WIN32(ERROR_NOT_A_REPARSE_POINT)) {
            // ERROR: target is not a reparse point
            return false;
        }
        else {
            // ERROR: failed to read reparse point
            return false;
        }
    }

    return reparseInfo;
}

inline bool IsFullFolder(const std::string& path)
{
    unsigned long flag = GetReparseInfo(path)->Flags & GV_FLAG_FULLY_POPULATED;

    return flag != 0;
}

inline bool DoesFileExist(const std::string& path)
{
    HANDLE handle = CreateFile(path.c_str(),
        GENERIC_READ,
        FILE_SHARE_READ,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS,
        NULL);

    if (handle != INVALID_HANDLE_VALUE) {
        CloseHandle(handle);
        return true;
    }

    if (ERROR_FILE_NOT_FOUND == GetLastError()) {
        return false;
    }

    return false;
}

inline std::string ReadFileAsString(const std::string& path)
{
    std::shared_ptr hFile = OpenForRead(path);

    char DataBuffer[MAX_BUF_SIZE] = { 0 };
    DWORD dwbytesRead;

    VERIFY_ARE_NOT_EQUAL(ReadFile(
        hFile.get(),
        DataBuffer,
        MAX_BUF_SIZE,
        &dwbytesRead,
        NULL
    ), FALSE);

    return std::string(DataBuffer);
}

inline DWORD DelFolder(const std::string& path, bool isSetDisposition = true)
{
    if (isSetDisposition) {
        auto success = RemoveDirectory(path.c_str());
        if (success) {
            return ERROR_SUCCESS;
        }
        else {
            return GetLastError();
        }
    }

    // delete on close
    HANDLE handle = CreateFile(
        path.c_str(),
        GENERIC_READ | GENERIC_WRITE,
        0,
        0,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_DELETE_ON_CLOSE,
        NULL);

    if (handle == INVALID_HANDLE_VALUE) {
        return GetLastError();
    }

    BOOL success = CloseHandle(handle);
    if (!success) {
        return GetLastError();
    }

    return ERROR_SUCCESS;
}

inline HRESULT CreateDirectoryWithIntermediates(
    _In_ const std::string& directoryName
)
{
    if (!CreateDirectory(directoryName.c_str(), nullptr)) {

        int gle = GetLastError();
        if (gle == ERROR_ALREADY_EXISTS) {

            //  If the directory already exists just treat that as success.
            return S_OK;

        }
        else if (gle == ERROR_PATH_NOT_FOUND) {

            //  One or more intermediate directories don't exist.  Assume
            //  the incoming path starts with e.g "X:\"
            std::string ntPath = "\\\\?\\";
            size_t startPos = 3;
            if (directoryName.compare(0, ntPath.length(), ntPath) == 0) {
                startPos += ntPath.length();
            }

            std::string::size_type foundPos = directoryName.find_first_of("\\", startPos);
            while (foundPos != std::string::npos) {

                if (!CreateDirectory(directoryName.substr(0, foundPos).c_str(), nullptr)) {

                    gle = GetLastError();
                    if (gle != ERROR_ALREADY_EXISTS) {
                        return HRESULT_FROM_WIN32(gle);
                    }
                }

                foundPos = directoryName.find_first_of("\\", foundPos + 1);
            }

            //  The loop created all the intermediate directories.  Try creating the final
            //  part again unless the string ended in a "\".  In that case we created everything
            //  we need.

            if (directoryName.length() - 1 != directoryName.find_last_of("\\")) {

                if (!CreateDirectory(directoryName.c_str(), nullptr)) {
                    return HRESULT_FROM_WIN32(GetLastError());
                }
            }

        }
        else {
            return HRESULT_FROM_WIN32(gle);
        }
    }

    return S_OK;
}

inline std::shared_ptr OpenForQueryAttribute(const std::string& path)
{
    std::shared_ptr handle(
        CreateFile(path.c_str(),
            FILE_READ_ATTRIBUTES,
            FILE_SHARE_READ,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS,
            NULL),
        CloseHandle);

    VERIFY_ARE_NOT_EQUAL(INVALID_HANDLE_VALUE, handle.get());

    return handle;
}

inline FILETIME GetLastWriteTime(const std::string& path)
{
    std::shared_ptr hFile = OpenForQueryAttribute(path);

    FILETIME ftWrite;
    VERIFY_ARE_EQUAL(TRUE, GetFileTime(hFile.get(), NULL, NULL, &ftWrite));
    SYSTEMTIME systemTime = { 0 };

    BOOL success = FileTimeToSystemTime(&ftWrite, &systemTime);
    VERIFY_ARE_EQUAL(TRUE, success);    

    return ftWrite;
}

inline LARGE_INTEGER GetFileSize(const std::string& path)
{
    std::shared_ptr hFile = OpenForQueryAttribute(path);

    LARGE_INTEGER size;
    VERIFY_ARE_EQUAL(TRUE, GetFileSizeEx(hFile.get(), &size));
    return size;
}

inline NTSTATUS SetEAInfo(const std::string& path, PFILE_FULL_EA_INFORMATION pbEABuffer, ULONG size, const int attributeNum = 1) {

    HANDLE hFile = CreateFile(path.c_str(),
        FILE_WRITE_EA,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS,
        NULL);

    if (INVALID_HANDLE_VALUE == hFile)
    {
        printf("\nSetExtendedAttributes: Cannot create handle for file %s", path.c_str());
        VERIFY_FAIL(("SetExtendedAttributes: Cannot create handle for file " + path).c_str());
    }

    NTSTATUS NtStatus = STATUS_SUCCESS;
    IO_STATUS_BLOCK IoStatusBlock;

    PFILE_FULL_EA_INFORMATION pEABlock = NULL;
    CHAR xAttrName[MAX_PATH] = { 0 };
    CHAR xAttrValue[MAX_PATH] = { 0 };

    for (int i = 0; iNextEntryOffset = 0;
        pEABlock->Flags = 0;
        pEABlock->EaNameLength = (UCHAR)(lstrlenA(xAttrName) * sizeof(CHAR));   // in bytes;
        pEABlock->EaValueLength = (UCHAR)(lstrlenA(xAttrValue) * sizeof(CHAR)); // in bytes;

        CopyMemory(pEABlock->EaName, xAttrName, lstrlenA(xAttrName) * sizeof(CHAR));
        pEABlock->EaName[pEABlock->EaNameLength] = 0; // IO subsystem checks for this NULL

        CopyMemory(pEABlock->EaName + pEABlock->EaNameLength + 1, xAttrValue, pEABlock->EaValueLength + 1);
        pEABlock->EaName[pEABlock->EaNameLength + 1 + pEABlock->EaValueLength + 1] = 0; // IO subsystem checks for this NULL

        HMODULE ntdll = LoadLibrary("ntdll.dll");
        VERIFY_ARE_NOT_EQUAL(ntdll, NULL);

        PSetEaFile NtSetEaFile = (PSetEaFile)GetProcAddress(ntdll, "NtSetEaFile");
        VERIFY_ARE_NOT_EQUAL(NtSetEaFile, NULL);

        NtStatus = NtSetEaFile(hFile, &IoStatusBlock, (PVOID)pEABlock, size);
        if (!NT_SUCCESS(NtStatus))
        {
            printf("\n\tSetExtendedAttributes: Failed in NtSetEaFile (0x%08x)", NtStatus);
            VERIFY_FAIL("SetExtendedAttributes: Failed in NtSetEaFile");
        }
    }

    CloseHandle(hFile);

    return NtStatus;
}

inline NTSTATUS ReadEAInfo(const std::string& path, PFILE_FULL_EA_INFORMATION eaBuffer, PULONG length) {

    NTSTATUS status = STATUS_SUCCESS;

    HANDLE hFile = CreateFile(path.c_str(),
        FILE_READ_EA | SYNCHRONIZE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS,
        NULL);

    if (INVALID_HANDLE_VALUE == hFile)
    {
        VERIFY_FAIL("ReadEAInfo: Cannot create handle for file");
    }

    IO_STATUS_BLOCK IoStatusBlock;
    
    // In the ProjFS tests, Index of 0 is used, however, per the EA comments
    // "If the index value is zero, there are no Eas to return"  Confirmed index of 1
    // properly reads EAs created using ea.exe test tool provided by ProjFS
    ULONG Index = 1;
    FILE_EA_INFORMATION eaInfo = { 0 };

    HMODULE ntdll = LoadLibrary("ntdll.dll");
    VERIFY_ARE_NOT_EQUAL(ntdll, NULL);

    PQueryInformationFile NtQueryInformationFile = (PQueryInformationFile)GetProcAddress(ntdll, "NtQueryInformationFile");
    VERIFY_ARE_NOT_EQUAL(NtQueryInformationFile, NULL);

    status = NtQueryInformationFile(
        hFile,
        &IoStatusBlock,
        &eaInfo,
        sizeof(eaInfo),
        FileEaInformation
    );

    if (!NT_SUCCESS(status)) {
        printf("\n\tError: NtQueryInformationFile failed, status = 0x%lx\n", status);
        goto Cleanup;
    }

    if (eaInfo.EaSize) {

        if (*length < eaInfo.EaSize) {
            printf("\n\tNtQueryEaFile failed, buffer is too small\n");
            status = ERROR_NOT_ENOUGH_MEMORY;
            goto Cleanup;
        }

        *length = eaInfo.EaSize;

        PQueryEaFile NtQueryEaFile = (PQueryEaFile)GetProcAddress(ntdll, "NtQueryEaFile");
        VERIFY_ARE_NOT_EQUAL(NtQueryEaFile, NULL);

        status = NtQueryEaFile(
            hFile,
            &IoStatusBlock,
            eaBuffer,
            *length,
            FALSE,
            NULL,
            0,
            &Index,
            TRUE);

        if (!NT_SUCCESS(status)) {
            printf("\n\tNtQueryEaFile failed, status = 0x%lx\n", status);
            goto Cleanup;
        }
    }

Cleanup:
    CloseHandle(hFile);

    return status;
}

inline std::string CombinePath(const std::string& root, const std::string& relPath)
{
    std::string fullPath = root;

    if (root.empty() || root == "\\") {
        return relPath;
    }

    if (fullPath.back() == '\\') {
        fullPath.pop_back();
    }

    if (!relPath.empty()) {
        fullPath += '\\';
        fullPath += relPath;
    }

    if (fullPath.back() == '\\') {
        fullPath.pop_back();
    }

    return fullPath;
}

inline std::string ReadFileAsStringUncached(const std::string& path)
{
    HANDLE hFile = CreateFile(
        path.c_str(),
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_RANDOM_ACCESS,
        NULL);

    if (hFile == INVALID_HANDLE_VALUE) {
        VERIFY_FAIL("CreateFile failed");
    }

    VERIFY_ARE_NOT_EQUAL(INVALID_HANDLE_VALUE, hFile);
    VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, GetLastError());

    HANDLE hMapFile = CreateFileMapping(
        hFile,
        NULL,                    // default security
        PAGE_READWRITE | FILE_MAP_READ,          // read/write access
        0,                       // maximum object size (high-order DWORD)
        0,                      // maximum object size (low-order DWORD)
        NULL);                 // name of mapping object

    VERIFY_ARE_NOT_EQUAL(INVALID_HANDLE_VALUE, hMapFile);
    VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, GetLastError());

    LPCTSTR pBuf = (LPTSTR)MapViewOfFile(hMapFile,   // handle to map object
        FILE_MAP_READ,               // read permission
        0,
        0,
        0);

    VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, GetLastError());
    VERIFY_ARE_NOT_EQUAL(nullptr, pBuf);

    std::string result(pBuf);

    UnmapViewOfFile(pBuf);

    CloseHandle(hMapFile);

    CloseHandle(hFile);

    return result;
}

inline int MovFile(const std::string& from, const std::string& to)
{
    int ret = rename(from.c_str(), to.c_str());
    if (ret != 0) {
        errno_t err;
        _get_errno(&err);
        return err;
    }

    return ret;
}

inline bool NewHardLink(const std::string& newlink, const std::string& existingFile)
{
    auto created = CreateHardLink(newlink.c_str(), existingFile.c_str(), NULL);
    return created == TRUE;
}

inline void VerifyEnumerationMatches(void* folderHandle, PUNICODE_STRING filter, const std::vector& expectedContents)
{
    SHOULD_NOT_EQUAL(folderHandle, INVALID_HANDLE_VALUE);

    UCHAR buffer[2048];
    NTSTATUS status;
    IO_STATUS_BLOCK ioStatus;
    BOOLEAN restart = TRUE;
    size_t expectedIndex = 0;

    do
    {
        status = NtQueryDirectoryFile(folderHandle,
            NULL,
            NULL,
            NULL,
            &ioStatus,
            buffer,
            ARRAYSIZE(buffer),
            FileBothDirectoryInformation,
            FALSE,
            filter,
            restart);

        if (status == STATUS_SUCCESS)
        {
            PFILE_BOTH_DIR_INFORMATION dirInfo;
            PUCHAR entry = buffer;

            do
            {
                dirInfo = (PFILE_BOTH_DIR_INFORMATION)entry;

                std::wstring entryName(dirInfo->FileName, dirInfo->FileNameLength / sizeof(WCHAR));

                SHOULD_EQUAL(entryName, expectedContents[expectedIndex]);

                entry = entry + dirInfo->NextEntryOffset;
                ++expectedIndex;

            } while (dirInfo->NextEntryOffset > 0 && expectedIndex < expectedContents.size());

            restart = FALSE;
        }

    } while (status == STATUS_SUCCESS);

    SHOULD_EQUAL(expectedIndex, expectedContents.size());
    SHOULD_EQUAL(status, STATUS_NO_MORE_FILES);
}

inline void VerifyEnumerationMatches(void* folderHandle, const std::vector& expectedContents)
{
    VerifyEnumerationMatches(folderHandle, nullptr, expectedContents);
}

} // namespace TestHelpers

================================================
FILE: GVFS/GVFS.NativeTests/include/TestVerifiers.h
================================================
#pragma once

#include "Should.h"
#include "TestHelpers.h"

namespace TestVerifiers
{

inline void ExpectDirEntries(const std::string& path, std::vector& entries)
{
    std::vector result = TestHelpers::EnumDirectory(path);
    entries.push_back(".");
    entries.push_back("..");

    VERIFY_ARE_EQUAL(entries.size(), result.size());

    for (const std::string& entry : entries) 
    {
        bool found = false;
        for (std::vector::iterator resultIt = result.begin(); resultIt != result.end(); resultIt++)
        {
            if (resultIt->Name == entry) 
            {
                result.erase(resultIt);
                found = true;
                break;
            }
        }

        if (!found) 
        {
            VERIFY_FAIL(("  [" + entry + "] not found").c_str());
            return;
        }
    }

    if (!result.empty()) 
    {
        VERIFY_FAIL("Some expected results not found");
    }
}

inline void AreEqual(const std::string& str1, const std::string& str2)
{
    VERIFY_ARE_EQUAL(str1, str2);
}

} // namespace TestVerifiers

================================================
FILE: GVFS/GVFS.NativeTests/include/prjlib_internal.h
================================================
// prjlib_internal.h
//
// Function declarations for internal functions in prjlib (used in the ProjFS tests)
// that are not intended to be used by user applications (e.g. GVFS) built on GVFlt

#pragma once

#include "prjlibp.h"

#ifdef __cplusplus
extern "C" {
#endif

//
// Functions operating on GVFS reparse points
//
HRESULT
PrjpReadPrjReparsePointData(
    _In_ LPCWSTR FilePath,
    _Out_writes_bytes_(*DataSize) PGV_REPARSE_INFO ReparsePointData,
    _Out_opt_ PULONG ReparseTag,
    _Inout_ PUSHORT DataSize
);

#ifdef __cplusplus
}
#endif

================================================
FILE: GVFS/GVFS.NativeTests/include/prjlibp.h
================================================
// prjlibp.h
//
// Contains a subset of the contents of the PrjLib header files:


#pragma once

#define GV_FLAG_IMMUTABLE                 0x00000001
#define GV_FLAG_DIRTY                     0x00000002
#define GV_FLAG_FULLY_POPULATED           0x00000004
#define GV_FLAG_SAW_PRE_RENAME            0x00000008
#define PRJ_FLAG_VIRTUALIZATION_ROOT      0x00000010
#define GV_FLAG_FULL_DATA                 0x00000020

//
// Length of ContentID and EpochID in bytes
//

#define PRJ_PLACEHOLDER_ID_LENGTH      128
//
// Structure that uniquely identifies the version of the attributes, file streams etc for a placeholder file
//

typedef struct _PRJ_PLACEHOLDER_VERSION_INFO {

    UCHAR                               ProviderID[PRJ_PLACEHOLDER_ID_LENGTH];

    UCHAR                               ContentID[PRJ_PLACEHOLDER_ID_LENGTH];

} PRJ_PLACEHOLDER_VERSION_INFO, *PPRJ_PLACEHOLDER_VERSION_INFO;


//
// Data written into on-disk reparse point
//

typedef struct _GV_REPARSE_INFO {

    //
    //  Version of this struct for future app compat issues
    //

    DWORD Version;

    //
    // Additional flags
    //

    ULONG Flags;

    //
    //  ID of the Virtualization Instance associated with the Virtualization Root that contains this reparse point
    //

    GUID VirtualizationInstanceID;

    //
    // Version info for the placeholder file
    //

	PRJ_PLACEHOLDER_VERSION_INFO versionInfo;

    //
    // Virtual (i.e. relative to the Virtualization Instance root) name of the fully expanded file
    // The name does not include trailing zero
    // The length is in bytes
    //
    USHORT NameLength;

    WCHAR Name[ANYSIZE_ARRAY];

} GV_REPARSE_INFO, *PGV_REPARSE_INFO;

================================================
FILE: GVFS/GVFS.NativeTests/include/stdafx.h
================================================
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//

#pragma once

#include "targetver.h"

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files:
#include 
#define WIN32_NO_STATUS
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Shlwapi.h"
#include "Strsafe.h"

#ifdef GVFSNATIVETESTS_EXPORTS
#define NATIVE_TESTS_EXPORT __declspec(dllexport) 
#else 
#define NATIVE_TESTS_EXPORT  __declspec(dllimport) 
#endif

#define UNREFERENCED_PARAMETER(P) (P)

template < typename T >
struct delete_array
{
    void operator ()(T const* ptr)
    {
        delete[] ptr;
    }
};

typedef LONG NTSTATUS;

#define NT_SUCCESS(Status)  (((NTSTATUS)(Status)) >= 0)

#define FILE_SUPERSEDE                  0x00000000
#define FILE_OPEN                       0x00000001
#define FILE_CREATE                     0x00000002
#define FILE_OPEN_IF                    0x00000003
#define FILE_OVERWRITE                  0x00000004
#define FILE_OVERWRITE_IF               0x00000005

#define FILE_SYNCHRONOUS_IO_NONALERT    0x00000020

typedef struct _IO_STATUS_BLOCK {
    union {
        NTSTATUS Status;
        PVOID Pointer;
    };
    ULONG_PTR Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;

typedef struct _LSA_UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} LSA_UNICODE_STRING, *PLSA_UNICODE_STRING, UNICODE_STRING, *PUNICODE_STRING;

typedef struct _FILE_NAMES_INFORMATION {
    ULONG NextEntryOffset;
    ULONG FileIndex;
    ULONG FileNameLength;
    WCHAR FileName[1];
} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION;

typedef enum _FILE_INFORMATION_CLASS {
    FileDirectoryInformation = 1,
    FileFullDirectoryInformation,   // 2
    FileBothDirectoryInformation,   // 3
    FileBasicInformation,           // 4
    FileStandardInformation,        // 5
    FileInternalInformation,        // 6
    FileEaInformation,              // 7
    FileAccessInformation,          // 8
    FileNameInformation,            // 9
    FileRenameInformation,          // 10
    FileLinkInformation,            // 11
    FileNamesInformation,           // 12
    FileDispositionInformation,     // 13
    FilePositionInformation,        // 14
    FileFullEaInformation,          // 15
    FileModeInformation,            // 16
    FileAlignmentInformation,       // 17
    FileAllInformation,             // 18
    FileAllocationInformation,      // 19
    FileEndOfFileInformation,       // 20
    FileAlternateNameInformation,   // 21
    FileStreamInformation,          // 22
    FilePipeInformation,            // 23
    FilePipeLocalInformation,       // 24
    FilePipeRemoteInformation,      // 25
    FileMailslotQueryInformation,   // 26
    FileMailslotSetInformation,     // 27
    FileCompressionInformation,     // 28
    FileObjectIdInformation,        // 29
    FileCompletionInformation,      // 30
    FileMoveClusterInformation,     // 31
    FileQuotaInformation,           // 32
    FileReparsePointInformation,    // 33
    FileNetworkOpenInformation,     // 34
    FileAttributeTagInformation,    // 35
    FileTrackingInformation,        // 36
    FileIdBothDirectoryInformation, // 37
    FileIdFullDirectoryInformation, // 38
    FileValidDataLengthInformation, // 39
    FileShortNameInformation,       // 40
    FileIoCompletionNotificationInformation, // 41
    FileIoStatusBlockRangeInformation,       // 42
    FileIoPriorityHintInformation,           // 43
    FileSfioReserveInformation,              // 44
    FileSfioVolumeInformation,               // 45
    FileHardLinkInformation,                 // 46
    FileProcessIdsUsingFileInformation,      // 47
    FileNormalizedNameInformation,           // 48
    FileNetworkPhysicalNameInformation,      // 49
    FileIdGlobalTxDirectoryInformation,      // 50
    FileIsRemoteDeviceInformation,           // 51
    FileUnusedInformation,                   // 52
    FileNumaNodeInformation,                 // 53
    FileStandardLinkInformation,             // 54
    FileRemoteProtocolInformation,           // 55

   //
   //  These are special versions of these operations (defined earlier)
   //  which can be used by kernel mode drivers only to bypass security
   //  access checks for Rename and HardLink operations.  These operations
   //  are only recognized by the IOManager, a file system should never
   //  receive these.
   //
   FileRenameInformationBypassAccessCheck,  // 56
   FileLinkInformationBypassAccessCheck,    // 57
   FileVolumeNameInformation,               // 58
   FileIdInformation,                       // 59
   FileIdExtdDirectoryInformation,          // 60
   FileReplaceCompletionInformation,        // 61
   FileHardLinkFullIdInformation,           // 62
   FileIdExtdBothDirectoryInformation,      // 63
   FileMaximumInformation
} FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS;

typedef struct _FILE_BOTH_DIR_INFORMATION {
    ULONG NextEntryOffset;
    ULONG FileIndex;
    LARGE_INTEGER CreationTime;
    LARGE_INTEGER LastAccessTime;
    LARGE_INTEGER LastWriteTime;
    LARGE_INTEGER ChangeTime;
    LARGE_INTEGER EndOfFile;
    LARGE_INTEGER AllocationSize;
    ULONG FileAttributes;
    ULONG FileNameLength;
    ULONG EaSize;
    CCHAR ShortNameLength;
    WCHAR ShortName[12];
    WCHAR FileName[1];
} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION;

typedef struct _FILE_FULL_EA_INFORMATION {
	ULONG NextEntryOffset;
	UCHAR Flags;
	UCHAR EaNameLength;
	USHORT EaValueLength;
	CHAR EaName[1];
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;

typedef struct _FILE_EA_INFORMATION {
	ULONG EaSize;
} FILE_EA_INFORMATION, *PFILE_EA_INFORMATION;

typedef enum {
    FileFsVolumeInformation = 1,
    FileFsLabelInformation = 2,
    FileFsSizeInformation = 3,
    FileFsDeviceInformation = 4,
    FileFsAttributeInformation = 5,
    FileFsControlInformation = 6,
    FileFsFullSizeInformation = 7,
    FileFsObjectIdInformation = 8,
    FileFsDriverPathInformation = 9,
    FileFsVolumeFlagsInformation = 10,
    FileFsSectorSizeInformation = 11
} FS_INFORMATION_CLASS;

typedef struct _OBJECT_ATTRIBUTES {
    ULONG Length;
    HANDLE RootDirectory;
    PUNICODE_STRING ObjectName;
    ULONG Attributes;
    PVOID SecurityDescriptor;
    PVOID SecurityQualityOfService;
}  OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;

typedef VOID(NTAPI *PIO_APC_ROUTINE) (_In_ PVOID ApcContext, _In_ PIO_STATUS_BLOCK IoStatusBlock, _In_ ULONG Reserved);

typedef NTSTATUS(NTAPI *PQueryDirectoryFile)(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, PIO_STATUS_BLOCK, PVOID, ULONG, FILE_INFORMATION_CLASS, BOOLEAN, PUNICODE_STRING, BOOLEAN);

typedef NTSTATUS(NTAPI *PQueryVolumeInformationFile)(HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG, FS_INFORMATION_CLASS);

typedef NTSTATUS(NTAPI *PSetEaFile)(HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG);

typedef NTSTATUS(NTAPI* PQueryInformationFile)(HANDLE, PIO_STATUS_BLOCK, PVOID,	ULONG,	FILE_INFORMATION_CLASS);

typedef NTSTATUS(NTAPI* PQueryEaFile)(HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG, BOOLEAN, PVOID, ULONG, PULONG, BOOLEAN);

typedef NTSTATUS (NTAPI *PNtClose)(HANDLE);

typedef NTSTATUS(NTAPI *PNtCreateFile)(
    PHANDLE,
    ACCESS_MASK,
    POBJECT_ATTRIBUTES,
    PIO_STATUS_BLOCK,
    PLARGE_INTEGER,
    ULONG,
    ULONG,
    ULONG,
    ULONG,
    PVOID,
    ULONG);

typedef NTSTATUS(NTAPI *PNtWriteFile)(
    HANDLE,
    HANDLE,
    PVOID,
    PVOID,
    PIO_STATUS_BLOCK,
    PVOID,
    ULONG,
    PLARGE_INTEGER,
    PULONG);

typedef VOID(WINAPI* PRtlInitUnicodeString)(PUNICODE_STRING, PCWSTR);

#include "NtFunctions.h"

================================================
FILE: GVFS/GVFS.NativeTests/include/targetver.h
================================================
#pragma once

// Including SDKDDKVer.h defines the highest available Windows platform.

// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.

#include 


================================================
FILE: GVFS/GVFS.NativeTests/interface/NtQueryDirectoryFileTests.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool QueryDirectoryFileRestartScanResetsFilter(const char* folderPath);
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/PlaceholderUtils.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool PlaceHolderHasVersionInfo(const char* virtualPath, int version, const WCHAR* sha);
}

================================================
FILE: GVFS/GVFS.NativeTests/interface/ProjFS_BugRegressionTest.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool ProjFS_ModifyFileInScratchAndDir(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_RMDIRTest1(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_RMDIRTest2(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_RMDIRTest3(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_RMDIRTest4(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_RMDIRTest5(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeepNonExistFileUnderPartial(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_SupersededReparsePoint(const char* virtualRootPath);

    // Note the following tests were not ported from ProjFS:
    //
    // StartInstanceAndFreeCallbacks
    // QickAttachDetach
    //   - These timing scenarios don't need to be tested with GVFS
    //
    // UnableToReadPartialFile
    //   - This test requires control over the ProjFS callback implementation
    //
    // DeepNonExistFileUnderFull
    //   - Currently GVFS does not covert folders to full

    // The following were ported to the managed tests:
    //
    // CMDHangNoneActiveInstance
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/ProjFS_DeleteFileTest.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteVirtualFile_SetDisposition(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteVirtualFile_DeleteOnClose(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeletePlaceholder_SetDisposition(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeletePlaceholder_DeleteOnClose(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteFullFile_SetDisposition(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteFullFile_DeleteOnClose(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteLocalFile_SetDisposition(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteLocalFile_DeleteOnClose(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteNotExistFile_SetDisposition(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteNotExistFile_DeleteOnClose(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteNonRootVirtualFile_SetDisposition(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteNonRootVirtualFile_DeleteOnClose(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteFileOutsideVRoot_SetDisposition(const char* pathOutsideRepo);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteFileOutsideVRoot_DeleteOnClose(const char* pathOutsideRepo);

    // Note the following tests were not ported from ProjFS:
    //
    // DeleteFullFileWithoutFileContext_SetDisposition
    // DeleteFullFileWithoutFileContext_DeleteOnClose
    //    - GVFS will always project new files when its back layer changes 
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/ProjFS_DeleteFolderTest.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteVirtualNonEmptyFolder_SetDisposition(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteVirtualNonEmptyFolder_DeleteOnClose(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeletePlaceholderNonEmptyFolder_SetDisposition(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeletePlaceholderNonEmptyFolder_DeleteOnClose(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteLocalEmptyFolder_SetDisposition(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteLocalEmptyFolder_DeleteOnClose(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteNonRootVirtualFolder_SetDisposition(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteNonRootVirtualFolder_DeleteOnClose(const char* virtualRootPath);

    // Note the following tests were not ported from ProjFS:
    //
    // DeleteVirtualEmptyFolder_SetDisposition
    // DeleteVirtualEmptyFolder_DeleteOnClose
    //    - Git does not support empty folders
    //
    // DeleteFullNonEmptyFolder_SetDisposition
    // DeleteFullNonEmptyFolder_DeleteOnClose
    //    - GVFS does not allow full folders
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/ProjFS_DirEnumTest.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool ProjFS_EnumEmptyFolder(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_EnumFolderWithOneFileInPackage(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_EnumFolderWithOneFileInBoth(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_EnumFolderWithOneFileInBoth1(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_EnumFolderDeleteExistingFile(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_EnumFolderSmallBuffer(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_EnumTestNoMoreNoSuchReturnCodes(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_EnumTestQueryDirectoryFileRestartScanProjectedFile(const char* virtualRootPath);
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/ProjFS_FileAttributeTest.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool ProjFS_ModifyFileInScratchAndCheckLastWriteTime(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_FileSize(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_ModifyFileInScratchAndCheckFileSize(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_FileAttributes(const char* virtualRootPath);

    // Note the following tests were not ported from ProjFS:
    //
    // LastWriteTime
    //     - There is no last write time in the GVFS layer to compare with
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/ProjFS_FileEATest.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool ProjFS_OneEAAttributeWillPass(const char* virtualRootPath);
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/ProjFS_FileOperationTest.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool ProjFS_OpenRootFolder(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_WriteAndVerify(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_DeleteExistingFile(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_OpenNonExistingFile(const char* virtualRootPath);

    // Note the following tests were not ported from ProjFS:
    //
    // OpenFileForRead
    //    - Covered in GVFS.FunctionalTests.Tests.EnlistmentPerFixture.WorkingDirectoryTests.ProjectedFileHasExpectedContents
    // OpenFileForWrite
    //    - Covered in GVFS.FunctionalTests.Tests.LongRunningEnlistment.WorkingDirectoryTests.ShrinkFileContents (and other tests)
    // ReadFileAndVerifyContent
    //    - Covered in GVFS.FunctionalTests.Tests.EnlistmentPerFixture.WorkingDirectoryTests.ProjectedFileHasExpectedContents
    // WriteFileAndVerifyFileInScratch
    // OverwriteAndVerify
    //    - Does not apply: Tests that writing scratch layer does not impact backing layer contents
    // CreateNewFileInScratch
    // CreateNewFileAndWriteInScratch
    //    - Covered in GVFS.FunctionalTests.Tests.LongRunningEnlistment.WorkingDirectoryTests.ShrinkFileContents (and other tests)
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/ProjFS_MoveFileTest.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_NoneToNone(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_VirtualToNone(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_PartialToNone(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_FullToNone(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_LocalToNone(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_VirtualToVirtual(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_VirtualToVirtualFileNameChanged(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_VirtualToPartial(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_PartialToPartial(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_LocalToVirtual(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_VirtualToVirtualIntermidiateDirNotExist(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_VirtualToNoneIntermidiateDirNotExist(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_OutsideToNone(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_OutsideToVirtual(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_OutsideToPartial(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_NoneToOutside(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_VirtualToOutside(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_PartialToOutside(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_OutsideToOutside(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFile_LongFileName(const char* virtualRootPath);

    // Note the following tests were not ported from ProjFS:
    //
    // VirtualToFull
    //    - GVFS does not allow full folders
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/ProjFS_MoveFolderTest.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFolder_NoneToNone(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFolder_VirtualToNone(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFolder_PartialToNone(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFolder_VirtualToVirtual(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFolder_VirtualToPartial(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFolder_OutsideToNone(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFolder_OutsideToVirtual(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFolder_NoneToOutside(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFolder_VirtualToOutside(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_MoveFolder_OutsideToOutside(const char* pathOutsideRepo, const char* virtualRootPath);
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/ProjFS_MultiThreadsTest.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool ProjFS_OpenForReadsSameTime(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_OpenForWritesSameTime(const char* virtualRootPath);

    // Note the following tests were not ported from ProjFS:
    //
    // GetPlaceholderInfoAndStopInstance
    // GetStreamAndStopInstance
    // EnumAndStopInstance
    //    - These tests require precise control of when the virtualization instance is stopped

    // Note: ProjFS_OpenMultipleFilesForReadsSameTime was not ported from ProjFS code, it just follows
    // the same pattern as those tests
    NATIVE_TESTS_EXPORT bool ProjFS_OpenMultipleFilesForReadsSameTime(const char* virtualRootPath);
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/ProjFS_SetLinkTest.h
================================================
#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool ProjFS_SetLink_ToVirtualFile(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_SetLink_ToPlaceHolder(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_SetLink_ToFullFile(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_SetLink_ToNonExistFileWillFail(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_SetLink_NameAlreadyExistWillFail(const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_SetLink_FromOutside(const char* pathOutsideRepo, const char* virtualRootPath);
    NATIVE_TESTS_EXPORT bool ProjFS_SetLink_ToOutside(const char* pathOutsideRepo, const char* virtualRootPath);
}


================================================
FILE: GVFS/GVFS.NativeTests/interface/ReadAndWriteTests.h
================================================
#pragma once

extern "C" 
{

NATIVE_TESTS_EXPORT bool ReadAndWriteSeparateHandles(const char* fileVirtualPath);

NATIVE_TESTS_EXPORT bool ReadAndWriteSameHandle(const char* fileVirtualPath, bool synchronousIO);

NATIVE_TESTS_EXPORT bool ReadAndWriteRepeatedly(const char* fileVirtualPath, bool synchronousIO);

NATIVE_TESTS_EXPORT bool RemoveReadOnlyAttribute(const char* fileVirtualPath);

NATIVE_TESTS_EXPORT bool CannotWriteToReadOnlyFile(const char* fileVirtualPath);

NATIVE_TESTS_EXPORT bool EnumerateAndReadDoesNotChangeEnumerationOrder(const char* folderVirtualPath);

NATIVE_TESTS_EXPORT bool EnumerationErrorsMatchNTFSForNonExistentFolder(const char* nonExistentVirtualPath, const char* nonExistentPhysicalPath);

NATIVE_TESTS_EXPORT bool EnumerationErrorsMatchNTFSForEmptyFolder(const char* emptyFolderVirtualPath, const char* emptyFolderPhysicalPath);

NATIVE_TESTS_EXPORT bool CanDeleteEmptyFolderWithFileDispositionOnClose(const char* emptyFolderPath);

NATIVE_TESTS_EXPORT bool ErrorWhenPathTreatsFileAsFolderMatchesNTFS(const char* fileVirtualPath, const char* fileNTFSPath, int creationDisposition);

}


================================================
FILE: GVFS/GVFS.NativeTests/interface/TrailingSlashTests.h
================================================

#pragma once

extern "C"
{
    NATIVE_TESTS_EXPORT bool EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete(const char* virtualRootPath);
}

================================================
FILE: GVFS/GVFS.NativeTests/packages.config
================================================


  


================================================
FILE: GVFS/GVFS.NativeTests/source/NtFunctions.cpp
================================================
#include "stdafx.h"
#include "NtFunctions.h"
#include "Should.h"

namespace
{
    PQueryDirectoryFile ntQueryDirectoryFile;
    PNtCreateFile ntCreateFile;
    PNtClose ntClose;
    PNtWriteFile ntWriteFile;
    PRtlInitUnicodeString rtlInitUnicodeString;
}

NTSTATUS NtQueryDirectoryFile(
    _In_     HANDLE                 FileHandle,
    _In_opt_ HANDLE                 Event,
    _In_opt_ PIO_APC_ROUTINE        ApcRoutine,
    _In_opt_ PVOID                  ApcContext,
    _Out_    PIO_STATUS_BLOCK       IoStatusBlock,
    _Out_    PVOID                  FileInformation,
    _In_     ULONG                  Length,
    _In_     FILE_INFORMATION_CLASS FileInformationClass,
    _In_     BOOLEAN                ReturnSingleEntry,
    _In_opt_ PUNICODE_STRING        FileName,
    _In_     BOOLEAN                RestartScan
)
{
    if (ntQueryDirectoryFile == NULL)
    {
        HMODULE ntdll = LoadLibrary("ntdll.dll");
        SHOULD_NOT_EQUAL(ntdll, NULL);

        ntQueryDirectoryFile = (PQueryDirectoryFile)GetProcAddress(ntdll, "NtQueryDirectoryFile");
        SHOULD_NOT_EQUAL(ntQueryDirectoryFile, NULL);
    }

    return ntQueryDirectoryFile(
        FileHandle,
        Event,
        ApcRoutine,
        ApcContext,
        IoStatusBlock,
        FileInformation,
        Length,
        FileInformationClass,
        ReturnSingleEntry,
        FileName,
        RestartScan);
}

NTSTATUS NtCreateFile(
    _Out_    PHANDLE            FileHandle,
    _In_     ACCESS_MASK        DesiredAccess,
    _In_     POBJECT_ATTRIBUTES ObjectAttributes,
    _Out_    PIO_STATUS_BLOCK   IoStatusBlock,
    _In_opt_ PLARGE_INTEGER     AllocationSize,
    _In_     ULONG              FileAttributes,
    _In_     ULONG              ShareAccess,
    _In_     ULONG              CreateDisposition,
    _In_     ULONG              CreateOptions,
    _In_     PVOID              EaBuffer,
    _In_     ULONG              EaLength
)
{
    if (ntCreateFile == NULL)
    {
        HMODULE ntdll = LoadLibrary("ntdll.dll");
        SHOULD_NOT_EQUAL(ntdll, NULL);

        ntCreateFile = (PNtCreateFile)GetProcAddress(ntdll, "NtCreateFile");
        SHOULD_NOT_EQUAL(ntCreateFile, NULL);
    }

    return ntCreateFile(
        FileHandle,
        DesiredAccess,
        ObjectAttributes,
        IoStatusBlock,
        AllocationSize,
        FileAttributes,
        ShareAccess,
        CreateDisposition,
        CreateOptions,
        EaBuffer,
        EaLength);
}

NTSTATUS NtClose(
    _In_ HANDLE Handle
)
{
    if (ntClose == NULL)
    {
        HMODULE ntdll = LoadLibrary("ntdll.dll");
        SHOULD_NOT_EQUAL(ntdll, NULL);

        ntClose = (PNtClose)GetProcAddress(ntdll, "NtClose");
        SHOULD_NOT_EQUAL(ntClose, NULL);
    }

    return ntClose(Handle);
}

NTSTATUS NtWriteFile(
    _In_     HANDLE           FileHandle,
    _In_opt_ HANDLE           Event,
    _In_opt_ PIO_APC_ROUTINE  ApcRoutine,
    _In_opt_ PVOID            ApcContext,
    _Out_    PIO_STATUS_BLOCK IoStatusBlock,
    _In_     PVOID            Buffer,
    _In_     ULONG            Length,
    _In_opt_ PLARGE_INTEGER   ByteOffset,
    _In_opt_ PULONG           Key
)
{
    if (ntWriteFile == NULL)
    {
        HMODULE ntdll = LoadLibrary("ntdll.dll");
        SHOULD_NOT_EQUAL(ntdll, NULL);

        ntWriteFile = (PNtWriteFile)GetProcAddress(ntdll, "NtWriteFile");
        SHOULD_NOT_EQUAL(ntWriteFile, NULL);
    }

    return ntWriteFile(
        FileHandle,
        Event,
        ApcRoutine,
        ApcContext,
        IoStatusBlock,
        Buffer,
        Length,
        ByteOffset,
        Key);
}

VOID WINAPI RtlInitUnicodeString(
    _Inout_  PUNICODE_STRING DestinationString,
    _In_opt_ PCWSTR          SourceString
)
{
    if (rtlInitUnicodeString == NULL)
    {
        HMODULE ntdll = LoadLibrary("ntdll.dll");
        SHOULD_NOT_EQUAL(ntdll, NULL);

        rtlInitUnicodeString = (PRtlInitUnicodeString)GetProcAddress(ntdll, "RtlInitUnicodeString");
        SHOULD_NOT_EQUAL(rtlInitUnicodeString, NULL);
    }

    return rtlInitUnicodeString(DestinationString, SourceString);
}


================================================
FILE: GVFS/GVFS.NativeTests/source/NtQueryDirectoryFileTests.cpp
================================================
#include "stdafx.h"
#include "NtQueryDirectoryFileTests.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "Should.h"

bool QueryDirectoryFileRestartScanResetsFilter(const char* folderPath)
{
    try
    {
        SHOULD_BE_TRUE(PathIsDirectory(folderPath));

        SafeHandle folderHandle(CreateFile(
            folderPath,                              // lpFileName
            (GENERIC_READ),                          // dwDesiredAccess
            FILE_SHARE_READ,                         // dwShareMode
            NULL,                                    // lpSecurityAttributes
            OPEN_EXISTING,                           // dwCreationDisposition
            FILE_FLAG_BACKUP_SEMANTICS,              // dwFlagsAndAttributes
            NULL));                                  // hTemplateFile
        SHOULD_NOT_EQUAL(folderHandle.GetHandle(), INVALID_HANDLE_VALUE);

        IO_STATUS_BLOCK ioStatus;
        FILE_NAMES_INFORMATION namesInfo[64];
        memset(namesInfo, 0, sizeof(namesInfo));

        NTSTATUS status = NtQueryDirectoryFile(
            folderHandle.GetHandle(), // FileHandle
            NULL,                     // Event
            NULL,                     // ApcRoutine
            NULL,                     // ApcContext
            &ioStatus,                // IoStatusBlock
            namesInfo,                // FileInformation
            sizeof(namesInfo),        // Length
            FileNamesInformation,     // FileInformationClass
            FALSE,                    // ReturnSingleEntry
            NULL,                     // FileName
            FALSE);                   // RestartScan

        SHOULD_EQUAL(status, STATUS_SUCCESS);
        memset(namesInfo, 0, sizeof(namesInfo));

        status = NtQueryDirectoryFile(
            folderHandle.GetHandle(), // FileHandle
            NULL,                     // Event
            NULL,                     // ApcRoutine
            NULL,                     // ApcContext
            &ioStatus,                // IoStatusBlock
            namesInfo,                // FileInformation
            sizeof(namesInfo),        // Length
            FileNamesInformation,     // FileInformationClass
            FALSE,                    // ReturnSingleEntry
            NULL,                     // FileName
            TRUE);                    // RestartScan

        SHOULD_EQUAL(status, STATUS_SUCCESS);
        memset(namesInfo, 0, sizeof(namesInfo));

        wchar_t nonExistentFileName[] = L"IDontExist";
        UNICODE_STRING nonExistentFileFilter;
        nonExistentFileFilter.Buffer = nonExistentFileName;
        nonExistentFileFilter.Length = sizeof(nonExistentFileName) - sizeof(wchar_t); // Length should not include null terminator
        nonExistentFileFilter.MaximumLength = sizeof(nonExistentFileName);

        status = NtQueryDirectoryFile(
            folderHandle.GetHandle(), // FileHandle
            NULL,                     // Event
            NULL,                     // ApcRoutine
            NULL,                     // ApcContext
            &ioStatus,                // IoStatusBlock
            namesInfo,                // FileInformation
            sizeof(namesInfo),        // Length
            FileNamesInformation,     // FileInformationClass
            FALSE,                    // ReturnSingleEntry
            &nonExistentFileFilter,   // FileName
            TRUE);                    // RestartScan

        SHOULD_EQUAL(status, STATUS_NO_MORE_FILES);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}


================================================
FILE: GVFS/GVFS.NativeTests/source/PlaceholderUtils.cpp
================================================
#include "stdafx.h"
#include "PlaceholderUtils.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "TestVerifiers.h"
#include "Should.h"

using namespace TestHelpers;

bool PlaceHolderHasVersionInfo(const char* virtualPath, int version, const WCHAR* sha)
{
    try
    {
        std::string path(virtualPath);
        std::shared_ptr reparseInfo = GetReparseInfo(path);

        SHOULD_EQUAL(reparseInfo->versionInfo.ProviderID[0], static_cast(version));

        SHOULD_EQUAL(_wcsnicmp(sha, static_cast(static_cast(reparseInfo->versionInfo.ContentID)), PRJ_PLACEHOLDER_ID_LENGTH), 0);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;

}


================================================
FILE: GVFS/GVFS.NativeTests/source/ProjFS_BugRegressionTest.cpp
================================================
#include "stdafx.h"
#include "ProjFS_BugRegressionTest.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "TestVerifiers.h"
#include "Should.h"

using namespace TestHelpers;
using namespace TestVerifiers;

static const std::string TEST_ROOT_FOLDER("\\GVFlt_BugRegressionTest");

bool ProjFS_ModifyFileInScratchAndDir(const char* virtualRootPath)
{
    // For bug #7700746 - File size is not updated when writing to a file projected from ProjFS app (e.g. GVFS, test app)

    try
    {
        std::string testScratch = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_ModifyFileInScratchAndDir\\");
        std::string fileName = "ModifyFileInScratchAndDir.txt";

        WriteToFile(testScratch + fileName, "ModifyFileInScratchAndDir:test data", false);

        std::vector entries = EnumDirectory(testScratch);
        VERIFY_ARE_EQUAL((size_t)3, entries.size());
        VERIFY_ARE_EQUAL(fileName, entries[2].Name);
        VERIFY_ARE_EQUAL(true, entries[2].IsFile);
        VERIFY_ARE_EQUAL(35, (LONGLONG)entries[2].FileSize);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

void RMDIR(const std::string& path)
{
    WIN32_FIND_DATA ffd;

    std::vector folders;

    std::string query = path + "*";

    HANDLE hFind = FindFirstFile(query.c_str(), &ffd);

    if (hFind == INVALID_HANDLE_VALUE)
    {
        VERIFY_FAIL("FindFirstFile failed");
    }

    do
    {
        if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        {
            folders.push_back(ffd.cFileName);
        }
        else {
            auto fileName = CombinePath(path, ffd.cFileName);

            if (FALSE == DeleteFile(fileName.c_str())) {
                VERIFY_FAIL("DeleteFile failed");
            }
        }
    } while (FindNextFile(hFind, &ffd) != 0);

    auto dwError = GetLastError();
    if (dwError != ERROR_NO_MORE_FILES)
    {
        VERIFY_FAIL("FindNextFile failed");
    }

    FindClose(hFind);

    for (auto folder : folders) {
        if (folder != "." && folder != "..") {
            RMDIR(folder);
        }
    }

    if (FALSE == RemoveDirectory(path.c_str())) {
        VERIFY_FAIL("RemoveDirectory failed");
    }
}

void RMDIRTEST(const std::string& virtualRootPath, const std::string& testName, const std::vector& fileNamesInScratch)
{
    std::string testCaseScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + testName + "\\";

    for (const std::string& fileName : fileNamesInScratch) {
        CreateNewFile(testCaseScratchRoot + fileName);
    }

    RMDIR(testCaseScratchRoot);

    auto handle = CreateFile((TEST_ROOT_FOLDER + "\\" + testName).c_str(),
        GENERIC_READ,
        FILE_SHARE_READ,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS,
        NULL);

    VERIFY_ARE_EQUAL(INVALID_HANDLE_VALUE, handle);
}

bool ProjFS_RMDIRTest1(const char* virtualRootPath)
{
    // Bug #7703883 - ProjFS: RMDIR /s against a partial folder returns directory not empty error

    try
    {
        // layer: 1, 2
        // scratch: 3
        std::vector scratchNames = { "3" };
        RMDIRTEST(virtualRootPath, "RMDIRTest1", scratchNames);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_RMDIRTest2(const char* virtualRootPath)
{
    try
    {
        // layer: 1
        // scratch: 2, 3
        std::vector scratchNames = { "2", "3" };
        RMDIRTEST(virtualRootPath, "RMDIRTest2", scratchNames);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_RMDIRTest3(const char* virtualRootPath)
{
    try
    {
        // layer: 1, 3
        // scratch: 2
        std::vector scratchNames = { "2" };
        RMDIRTEST(virtualRootPath, "RMDIRTest3", scratchNames);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_RMDIRTest4(const char* virtualRootPath)
{
    try
    {
        // layer: 2
        // scratch: 1, 3
        std::vector scratchNames = { "1", "3" };
        RMDIRTEST(virtualRootPath, "RMDIRTest4", scratchNames);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_RMDIRTest5(const char* virtualRootPath)
{
    try
    {
        // layer: 1, 2, 4
        // scratch: 2, 3
        std::string testCaseScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\RMDIRTest5\\";
        OpenForRead(testCaseScratchRoot + "2");

        std::vector scratchNames = { "3" };
        RMDIRTEST(virtualRootPath, "RMDIRTest5", scratchNames);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeepNonExistFileUnderPartial(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\DeepNonExistFileUnderPartial\\";

        // try to open a deep non existing file
        CreateFile((testScratchRoot + "a\\b\\c\\d\\e").c_str(),
            GENERIC_READ,
            FILE_SHARE_READ,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS,
            NULL);

        VERIFY_ARE_EQUAL((DWORD)ERROR_PATH_NOT_FOUND, GetLastError());
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_SupersededReparsePoint(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\SupersededReparsePoint\\";

        std::string path = testScratchRoot + "test.txt";

        std::shared_ptr openThreadHandle;
        std::shared_ptr openThread1Handle;
        std::shared_ptr truncateThreadHandle;

        std::thread openThread([path, &openThreadHandle]() {

            openThreadHandle = std::shared_ptr(
                CreateFile(path.c_str(),
                    GENERIC_READ,
                    FILE_SHARE_READ | FILE_SHARE_WRITE,
                    NULL,
                    OPEN_EXISTING,
                    FILE_ATTRIBUTE_NORMAL,
                    NULL),
                CloseHandle);

            if (openThreadHandle.get() == INVALID_HANDLE_VALUE)
            {
                VERIFY_FAIL("CreateFile for read failed (openThread)");
            }
        });

        std::thread openThread1([path, &openThread1Handle]() {

            openThread1Handle = std::shared_ptr(
                CreateFile(path.c_str(),
                    GENERIC_READ,
                    FILE_SHARE_READ | FILE_SHARE_WRITE,
                    NULL,
                    OPEN_EXISTING,
                    FILE_ATTRIBUTE_NORMAL,
                    NULL),
                CloseHandle);

            if (openThread1Handle.get() == INVALID_HANDLE_VALUE)
            {
                VERIFY_FAIL("CreateFile for read failed (openThread1)");
            }
        });

        std::thread truncateThread([path, &truncateThreadHandle]() {
            truncateThreadHandle = std::shared_ptr(
                CreateFile(path.c_str(),
                    GENERIC_WRITE,
                    FILE_SHARE_READ | FILE_SHARE_WRITE,
                    NULL,
                    TRUNCATE_EXISTING,
                    FILE_ATTRIBUTE_NORMAL,
                    NULL),
                CloseHandle);

            if (truncateThreadHandle.get() == INVALID_HANDLE_VALUE)
            {
                VERIFY_FAIL("CreateFile for truncate failed (openThread1)");
            }
        });

        openThread.join();
        openThread1.join();
        truncateThread.join();
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

================================================
FILE: GVFS/GVFS.NativeTests/source/ProjFS_DeleteFileTest.cpp
================================================
#include "stdafx.h"
#include "ProjFS_DeleteFileTest.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "TestVerifiers.h"
#include "Should.h"

using namespace TestHelpers;
using namespace TestVerifiers;

static const std::string TEST_ROOT_FOLDER("\\GVFlt_DeleteFileTest");

bool ProjFS_DeleteVirtualFile_SetDisposition(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteVirtualFile_SetDisposition\\");
        DWORD error = DelFile(testScratchRoot + "a.txt");
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "b.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteVirtualFile_DeleteOnClose(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteVirtualFile_DeleteOnClose\\");
        DWORD error = DelFile(testScratchRoot + "a.txt", false);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "b.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeletePlaceholder_SetDisposition(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeletePlaceholder_SetDisposition\\");

        // make a.txt a placeholder
        ReadFileAsString(testScratchRoot + "a.txt");
        DWORD error = DelFile(testScratchRoot + "a.txt");
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "b.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeletePlaceholder_DeleteOnClose(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeletePlaceholder_DeleteOnClose\\");

        // make a.txt a placeholder
        ReadFileAsString(testScratchRoot + "a.txt");
        DWORD error = DelFile(testScratchRoot + "a.txt", false);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "b.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteFullFile_SetDisposition(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteFullFile_SetDisposition\\");

        // make a.txt a full file
        WriteToFile(testScratchRoot + "a.txt", "123123");
        DWORD error = DelFile(testScratchRoot + "a.txt");
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "b.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteFullFile_DeleteOnClose(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteFullFile_DeleteOnClose\\");

        // make a.txt a full file
        WriteToFile(testScratchRoot + "a.txt", "123123");
        DWORD error = DelFile(testScratchRoot + "a.txt", false);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "b.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteLocalFile_SetDisposition(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteLocalFile_SetDisposition\\");

        CreateNewFile(testScratchRoot + "c3.txt", "123123");
        DWORD error = DelFile(testScratchRoot + "c3.txt");
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "a.txt", "b.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "c3.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteLocalFile_DeleteOnClose(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteLocalFile_DeleteOnClose\\");

        CreateNewFile(testScratchRoot + "c4.txt", "123123");
        DWORD error = DelFile(testScratchRoot + "c4.txt", false);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "a.txt", "b.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "c4.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteNotExistFile_SetDisposition(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNotExistFile_SetDisposition\\");

        DWORD error = DelFile(testScratchRoot + "notexist.txt");
        VERIFY_ARE_EQUAL((DWORD)ERROR_FILE_NOT_FOUND, error);

        std::vector expected = { "a.txt", "b.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteNotExistFile_DeleteOnClose(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNotExistFile_DeleteOnClose\\");

        DWORD error = DelFile(testScratchRoot + "notexist.txt", false);
        VERIFY_ARE_EQUAL((DWORD)ERROR_FILE_NOT_FOUND, error);

        std::vector expected = { "a.txt", "b.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteNonRootVirtualFile_SetDisposition(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNonRootVirtualFile_SetDisposition\\");
        std::string testFolder = "A\\B\\C\\D\\";
        std::string testFile = "test.txt";

        DWORD error = DelFile(testScratchRoot + testFolder + testFile);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = {};
        ExpectDirEntries(testScratchRoot + testFolder, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + testFolder));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + testFolder + testFile));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteNonRootVirtualFile_DeleteOnClose(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNonRootVirtualFile_DeleteOnClose\\");
        std::string testFolder = "A1\\B\\C\\D\\";
        std::string testFile = "test.txt";

        DWORD error = DelFile(testScratchRoot + testFolder + testFile, false);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = {};
        ExpectDirEntries(testScratchRoot + testFolder, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + testFolder));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + testFolder + testFile));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteFileOutsideVRoot_SetDisposition(const char* pathOutsideRepo)
{
    try
    {
        std::string testFile = pathOutsideRepo + std::string("\\GVFlt_DeleteFileOutsideVRoot_SetDisposition.txt");
        CreateNewFile(testFile);

        DWORD error = DelFile(testFile);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);
        VERIFY_ARE_EQUAL(false, DoesFileExist(testFile));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteFileOutsideVRoot_DeleteOnClose(const char* pathOutsideRepo)
{
    try
    {
        std::string testFile = pathOutsideRepo + std::string("\\GVFlt_DeleteFileOutsideVRoot_DeleteOnClose.txt");
        CreateNewFile(testFile);

        DWORD error = DelFile(testFile, false);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);
        VERIFY_ARE_EQUAL(false, DoesFileExist(testFile));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

================================================
FILE: GVFS/GVFS.NativeTests/source/ProjFS_DeleteFolderTest.cpp
================================================
#include "stdafx.h"
#include "ProjFS_DeleteFolderTest.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "TestVerifiers.h"
#include "Should.h"

using namespace TestHelpers;
using namespace TestVerifiers;

static const std::string TEST_ROOT_FOLDER("\\GVFlt_DeleteFolderTest");


// --------------------
//
// Special note on "EmptyFolder".  In our tests, this folder actually has a single empty file because Git
// does not allow committing empty folders.
//
// --------------------


bool ProjFS_DeleteVirtualNonEmptyFolder_SetDisposition(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteVirtualNonEmptyFolder_SetDisposition\\");

        DWORD error = DelFolder(testScratchRoot + "NonEmptyFolder");
        VERIFY_ARE_EQUAL((DWORD)ERROR_DIR_NOT_EMPTY, error);

        std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder"));

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteVirtualNonEmptyFolder_DeleteOnClose(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose\\");

        DWORD error = DelFolder(testScratchRoot + "NonEmptyFolder", false);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder"));

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeletePlaceholderNonEmptyFolder_SetDisposition(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition\\");

        // make it a placeholder folder
        EnumDirectory(testScratchRoot + "NonEmptyFolder");

        DWORD error = DelFolder(testScratchRoot + "NonEmptyFolder");
        VERIFY_ARE_EQUAL((DWORD)ERROR_DIR_NOT_EMPTY, error);

        std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder"));

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeletePlaceholderNonEmptyFolder_DeleteOnClose(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeletePlaceholderNonEmptyFolder_DeleteOnClose\\");

        // make it a placeholder folder
        EnumDirectory(testScratchRoot + "NonEmptyFolder");

        DWORD error = DelFolder(testScratchRoot + "NonEmptyFolder", false);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder"));

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteLocalEmptyFolder_SetDisposition(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteLocalEmptyFolder_SetDisposition\\");

        // create a new local folder
        CreateDirectoryWithIntermediates(testScratchRoot + "localFolder\\");

        DWORD error = DelFolder(testScratchRoot + "localFolder");
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder\\" + "bar.txt"));

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteLocalEmptyFolder_DeleteOnClose(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteLocalEmptyFolder_DeleteOnClose\\");

        // create a new local folder
        CreateDirectoryWithIntermediates(testScratchRoot + "localFolder\\");

        DWORD error = DelFolder(testScratchRoot + "localFolder", false);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" };
        ExpectDirEntries(testScratchRoot, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot));

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder\\" + "bar.txt"));

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteNonRootVirtualFolder_SetDisposition(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNonRootVirtualFolder_SetDisposition\\");

        std::string testFolder = "A\\B\\C\\D\\";
        std::string targetFolder = "E\\";
        std::string testFile = "test.txt";

        // NOTE: Deviate from ProjFS's DeleteNonRootVirtualFolder_SetDisposition here by deleting a file first
        // Git will not allow empty folders to be commited, and so \E must have a file in it
        DWORD fileError = DelFile(testScratchRoot + testFolder + targetFolder + testFile);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, fileError);

        DWORD error = DelFolder(testScratchRoot + testFolder + targetFolder);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = {};
        ExpectDirEntries(testScratchRoot + testFolder, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + testFolder));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + testFolder + targetFolder));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteNonRootVirtualFolder_DeleteOnClose(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNonRootVirtualFolder_DeleteOnClose\\");

        std::string testFolder = "A\\B\\C\\D\\";
        std::string targetFolder = "E\\";
        std::string testFile = "test.txt";

        // NOTE: Deviate from ProjFS's DeleteNonRootVirtualFolder_DeleteOnClose here by deleting a file first
        // Git will not allow empty folders to be commited, and so \E must have a file in it
        DWORD fileError = DelFile(testScratchRoot + testFolder + targetFolder + testFile);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, fileError);

        DWORD error = DelFolder(testScratchRoot + testFolder + targetFolder, false);
        VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error);

        std::vector expected = {};
        ExpectDirEntries(testScratchRoot + testFolder, expected);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + testFolder));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + testFolder + targetFolder));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

================================================
FILE: GVFS/GVFS.NativeTests/source/ProjFS_DirEnumTest.cpp
================================================
#include "stdafx.h"
#include "ProjFS_DirEnumTest.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "Should.h"

using namespace TestHelpers;

static const std::string TEST_ROOT_FOLDER("\\GVFlt_EnumTest");

bool ProjFS_EnumEmptyFolder(const char* virtualRootPath)
{
    try
    {
        std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumEmptyFolder\\");
        CreateDirectoryWithIntermediates(folderPath);

        std::vector result = EnumDirectory(folderPath);
        VERIFY_ARE_EQUAL((size_t)2, result.size());
        VERIFY_ARE_EQUAL(false, result[0].IsFile);
        VERIFY_ARE_EQUAL(strcmp(".", result[0].Name.c_str()), 0);
        VERIFY_ARE_EQUAL(false, result[1].IsFile);
        VERIFY_ARE_EQUAL(strcmp("..", result[1].Name.c_str()), 0);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_EnumFolderWithOneFileInPackage(const char* virtualRootPath)
{
    try
    {
        std::vector result = EnumDirectory(virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumFolderWithOneFileInPackage\\"));
        VERIFY_ARE_EQUAL((size_t)3, result.size());
        VERIFY_ARE_EQUAL(false, result[0].IsFile);
        VERIFY_ARE_EQUAL(strcmp(".", result[0].Name.c_str()), 0);
        VERIFY_ARE_EQUAL(false, result[1].IsFile);
        VERIFY_ARE_EQUAL(strcmp("..", result[1].Name.c_str()), 0);
        VERIFY_ARE_EQUAL(true, result[2].IsFile);
        VERIFY_ARE_EQUAL("onlyFileInFolder.txt", result[2].Name);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_EnumFolderWithOneFileInBoth(const char* virtualRootPath)
{
    try
    {
        std::string existingFileName = "newfileInPackage.txt";
        std::string newFileName = "newfileInScratch.txt";
        std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumFolderWithOneFileInBoth\\");
        CreateNewFile(folderPath + newFileName);

        std::vector result = EnumDirectory(folderPath);
        VERIFY_ARE_EQUAL((size_t)4, result.size());
        VERIFY_ARE_EQUAL(false, result[0].IsFile);
        VERIFY_ARE_EQUAL(strcmp(".", result[0].Name.c_str()), 0);
        VERIFY_ARE_EQUAL(false, result[1].IsFile);
        VERIFY_ARE_EQUAL(strcmp("..", result[1].Name.c_str()), 0);
        VERIFY_ARE_EQUAL(true, result[2].IsFile);
        VERIFY_ARE_EQUAL(existingFileName, result[2].Name);
        VERIFY_ARE_EQUAL(true, result[3].IsFile);
        VERIFY_ARE_EQUAL(newFileName, result[3].Name);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_EnumFolderWithOneFileInBoth1(const char* virtualRootPath)
{
    try
    {
        std::string existingFileName = "newfileInPackage.txt";
        std::string newFileName = "123";
        std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumFolderWithOneFileInBoth1\\");
        CreateNewFile(folderPath + newFileName);

        std::vector result = EnumDirectory(folderPath);
        VERIFY_ARE_EQUAL((size_t)4, result.size());
        VERIFY_ARE_EQUAL(false, result[0].IsFile);
        VERIFY_ARE_EQUAL(strcmp(".", result[0].Name.c_str()), 0);
        VERIFY_ARE_EQUAL(false, result[1].IsFile);
        VERIFY_ARE_EQUAL(strcmp("..", result[1].Name.c_str()), 0);
        VERIFY_ARE_EQUAL(true, result[2].IsFile);
        VERIFY_ARE_EQUAL(newFileName, result[2].Name);
        VERIFY_ARE_EQUAL(true, result[3].IsFile);
        VERIFY_ARE_EQUAL(existingFileName, result[3].Name);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_EnumFolderDeleteExistingFile(const char* virtualRootPath)
{
    try
    {
        std::string fileName1 = "fileInPackage1.txt";
        std::string fileName2 = "fileInPackage2.txt";
        std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumFolderDeleteExistingFile\\");

        VERIFY_ARE_EQUAL(TRUE, DeleteFile((folderPath + fileName1).c_str()));

        std::vector  result = EnumDirectory(folderPath);
        VERIFY_ARE_EQUAL((size_t)3, result.size());
        VERIFY_ARE_EQUAL(false, result[0].IsFile);
        VERIFY_ARE_EQUAL(strcmp(".", result[0].Name.c_str()), 0);
        VERIFY_ARE_EQUAL(false, result[1].IsFile);
        VERIFY_ARE_EQUAL(strcmp("..", result[1].Name.c_str()), 0);
        VERIFY_ARE_EQUAL(true, result[2].IsFile);
        VERIFY_ARE_EQUAL(fileName2, result[2].Name);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_EnumFolderSmallBuffer(const char* virtualRootPath)
{
    std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumFolderSmallBuffer");

    try
    {
        UCHAR buffer[512];
        NTSTATUS status;
        BOOLEAN restart = TRUE;
        IO_STATUS_BLOCK ioStatus;
        USHORT count = 0;

        for (USHORT i = 0; i < 26; i += 2) {

            // open every other file to create placeholders
            std::string name(1, (char)('a' + i));
            OpenForRead(folderPath + std::string("\\") + name);
        }

        std::shared_ptr handle = OpenForRead(folderPath);

        do 
        {
            status = NtQueryDirectoryFile(handle.get(),
                NULL,
                NULL,
                NULL,
                &ioStatus,
                buffer,
                ARRAYSIZE(buffer),
                FileBothDirectoryInformation,
                FALSE,
                NULL,
                restart);

            if (status == STATUS_SUCCESS) {

                PFILE_BOTH_DIR_INFORMATION dirInfo;
                PUCHAR entry = buffer;

                do {

                    dirInfo = (PFILE_BOTH_DIR_INFORMATION)entry;

                    std::wstring entryName(dirInfo->FileName, dirInfo->FileNameLength / sizeof(WCHAR));

                    if ((entryName.compare(L".") != 0) && (entryName.compare(L"..") != 0)) {

                        std::wstring expectedName(1, L'a' + count);

                        VERIFY_ARE_EQUAL(entryName, expectedName);

                        count++;
                    }

                    entry = entry + dirInfo->NextEntryOffset;

                } while (dirInfo->NextEntryOffset > 0);

                restart = FALSE;
            }

        } while (status == STATUS_SUCCESS);

        VERIFY_ARE_EQUAL(count, 26);
        VERIFY_ARE_EQUAL(status, STATUS_NO_MORE_FILES);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}


bool ProjFS_EnumTestNoMoreNoSuchReturnCodes(const char* virtualRootPath)
{
    std::string fileName1 = "fileInPackage1.txt";
    std::string fileName2 = "fileInPackage2.txt";
    std::string fileName3 = "fileInPackage3.txt";
    std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumTestNoMoreNoSuchReturnCodes");

    try
    {
        VERIFY_ARE_EQUAL(TRUE, DeleteFile((folderPath + std::string("\\") + fileName1).c_str()));
        VERIFY_ARE_EQUAL(TRUE, DeleteFile((folderPath + std::string("\\") + fileName2).c_str()));
        VERIFY_ARE_EQUAL(TRUE, DeleteFile((folderPath + std::string("\\") + fileName3).c_str()));

        std::shared_ptr enumHandle = OpenForRead(folderPath);

        UCHAR buffer[512];
        BOOLEAN restartScan = TRUE;
        IO_STATUS_BLOCK ioStatus;
        UNICODE_STRING fileSpec;
        RtlInitUnicodeString(&fileSpec, L"fileInPack*");
        NTSTATUS initialStatus = NtQueryDirectoryFile(enumHandle.get(),
            nullptr,
            nullptr,
            nullptr,
            &ioStatus,
            buffer,
            ARRAYSIZE(buffer),
            FileFullDirectoryInformation,
            FALSE,
            &fileSpec,
            restartScan);

        // Check expected status code for the first query on a given handle for a non-existent name.
        VERIFY_ARE_EQUAL(STATUS_NO_SUCH_FILE, initialStatus);

        // Do another query on the handle for the non-existent names.  Leave SL_RESTART_SCAN set.
        NTSTATUS repeatStatus = NtQueryDirectoryFile(enumHandle.get(),
            nullptr,
            nullptr,
            nullptr,
            &ioStatus,
            buffer,
            ARRAYSIZE(buffer),
            FileFullDirectoryInformation,
            FALSE,
            &fileSpec,
            restartScan);

        // Check expected status code for a repeat query on a given handle for a non-existent name.
        VERIFY_ARE_EQUAL(STATUS_NO_MORE_FILES, repeatStatus);

        // Once more, this time without SL_RESTART_SCAN.
        restartScan = false;
        NTSTATUS finalStatus = NtQueryDirectoryFile(enumHandle.get(),
            nullptr,
            nullptr,
            nullptr,
            &ioStatus,
            buffer,
            ARRAYSIZE(buffer),
            FileFullDirectoryInformation,
            false,
            &fileSpec,
            restartScan);

        // Check expected status code for a repeat query on a given handle for a non-existent name.
        VERIFY_ARE_EQUAL(STATUS_NO_MORE_FILES, finalStatus);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_EnumTestQueryDirectoryFileRestartScanProjectedFile(const char* virtualRootPath)
{
    std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumTestQueryDirectoryFileRestartScanResetsFilter");

    try
    {
        std::shared_ptr enumHandle = OpenForRead(folderPath);

        std::vector expectedResults = { L".", L"..", L"fileInPackage1.txt", L"fileInPackage2.txt", L"fileInPackage3.txt" };
        VerifyEnumerationMatches(enumHandle.get(), expectedResults);

        // Query again, resetting the scan to the start.
        VerifyEnumerationMatches(enumHandle.get(), expectedResults);

        // Query again, using a filter
        UNICODE_STRING fileSpec;
        RtlInitUnicodeString(&fileSpec, L"fileInPackage2.txt");
        expectedResults = { L"fileInPackage2.txt", };
        VerifyEnumerationMatches(enumHandle.get(), &fileSpec, expectedResults);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

================================================
FILE: GVFS/GVFS.NativeTests/source/ProjFS_FileAttributeTest.cpp
================================================
#include "stdafx.h"
#include "ProjFS_FileAttributeTest.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "Should.h"

using namespace TestHelpers;

static const std::string TEST_ROOT_FOLDER("\\GVFlt_FileAttributeTest");

bool ProjFS_ModifyFileInScratchAndCheckLastWriteTime(const char* virtualRootPath)
{
    try
    {
        std::string fileName = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\ModifyFileInScratchAndCheckLastWriteTime.txt");

        FILETIME lastWriteTime_Package = GetLastWriteTime(fileName);
        WriteToFile(fileName, "test data", false);
        FILETIME lastWriteTime_scratch = GetLastWriteTime(fileName);

        // last write time is has been updated
        // NOTE: This is slightly different than the validate in ProjFS, which tests if the scratch time is different than the layer time
        VERIFY_ARE_NOT_EQUAL(0, CompareFileTime(&lastWriteTime_Package, &lastWriteTime_scratch));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_FileSize(const char* virtualRootPath)
{
    try
    {
        std::string fileName = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\FileSize.txt");
        LARGE_INTEGER file_size_scratch = GetFileSize(fileName);
        VERIFY_ARE_EQUAL(7, file_size_scratch.QuadPart);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_ModifyFileInScratchAndCheckFileSize(const char* virtualRootPath)
{
    try
    {
        std::string fileName = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\ModifyFileInScratchAndCheckFileSize.txt");

        LARGE_INTEGER fileSize_Package = GetFileSize(fileName);

        WriteToFile(fileName, "ModifyFileInScratchAndCheckFileSize:test data", false);
        LARGE_INTEGER file_size_scratch = GetFileSize(fileName);

        VERIFY_ARE_NOT_EQUAL(fileSize_Package.QuadPart, file_size_scratch.QuadPart);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

namespace
{

void TestFileAttribute(const std::string& fileName, DWORD attribute)
{
    std::string data = "TestFileAttribute: some test data";

    BOOL success = SetFileAttributes(fileName.c_str(), attribute);
    VERIFY_ARE_EQUAL(TRUE, success);

    BOOL attrScratch = GetFileAttributes(fileName.c_str());
    VERIFY_ARE_EQUAL(attribute, attribute&attrScratch);
}

}

bool ProjFS_FileAttributes(const char* virtualRootPath)
{
    try
    {
        std::string testRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\";

        TestFileAttribute(testRoot + "FileAttributes_ARCHIVE", FILE_ATTRIBUTE_ARCHIVE);
        TestFileAttribute(testRoot + "FileAttributes_HIDDEN", FILE_ATTRIBUTE_HIDDEN);
        TestFileAttribute(testRoot + "FileAttributes_NOT_CONTENT_INDEXED", FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
        //TestFileAttribute(FILE_ATTRIBUTE_OFFLINE);
        TestFileAttribute(testRoot + "FileAttributes_READONLY", FILE_ATTRIBUTE_READONLY);
        TestFileAttribute(testRoot + "FileAttributes_SYSTEM", FILE_ATTRIBUTE_SYSTEM);
        TestFileAttribute(testRoot + "FileAttributes_TEMPORARY", FILE_ATTRIBUTE_TEMPORARY);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

================================================
FILE: GVFS/GVFS.NativeTests/source/ProjFS_FileEATest.cpp
================================================
#include "stdafx.h"
#include "ProjFS_FileEATest.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "Should.h"

using namespace TestHelpers;

static const std::string TEST_ROOT_FOLDER("\\GVFlt_FileEATest");

bool ProjFS_OneEAAttributeWillPass(const char* virtualRootPath)
{
    try
    {
        std::string fileName = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\OneEAAttributeWillPass.txt");

        ULONG size = 2 * 65535;
        PFILE_FULL_EA_INFORMATION buffer = (PFILE_FULL_EA_INFORMATION)calloc(1, size);
        auto status = SetEAInfo(fileName, buffer, size);
        VERIFY_ARE_EQUAL(STATUS_SUCCESS, status);

        OpenForRead(fileName);
        status = ReadEAInfo(fileName, buffer, &size);
        VERIFY_ARE_EQUAL(STATUS_SUCCESS, status);
        VERIFY_ARE_NOT_EQUAL((ULONG)0, size);

    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

================================================
FILE: GVFS/GVFS.NativeTests/source/ProjFS_FileOperationTest.cpp
================================================
#include "stdafx.h"
#include "ProjFS_FileOperationTest.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "TestVerifiers.h"
#include "Should.h"

using namespace TestHelpers;
using namespace TestVerifiers;

static const std::string TEST_ROOT_FOLDER("\\GVFlt_FileOperationTest");

bool ProjFS_OpenRootFolder(const char* virtualRootPath)
{
    try
    {
        OpenForRead(virtualRootPath);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_WriteAndVerify(const char* virtualRootPath)
{
    try
    {
        std::string scratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\";
        std::string fileName = "WriteAndVerify.txt";
        std::string data = "test data\r\n";

        // write file in scratch
        std::string newData = "new data";
        WriteToFile(scratchRoot + fileName, newData, false);

        std::string newContent = newData + data.substr(newData.size());

        VERIFY_ARE_EQUAL(newContent, ReadFileAsString(scratchRoot + fileName));
        VERIFY_ARE_EQUAL(newContent, ReadFileAsStringUncached(scratchRoot + fileName));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_DeleteExistingFile(const char* virtualRootPath)
{
    try
    {
        std::string scratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\";
        std::string fileName = "DeleteExistingFile.txt";

        std::string fileInScratch = scratchRoot + fileName;
        // delete in scratch root
        VERIFY_ARE_EQUAL(TRUE, DeleteFile(fileInScratch.c_str()));

        // make sure the file can't be opened again
        auto hFileInScratch = CreateFile(fileInScratch.c_str(),
            GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            NULL);

        VERIFY_ARE_EQUAL(INVALID_HANDLE_VALUE, hFileInScratch);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_OpenNonExistingFile(const char* virtualRootPath)
{
    try
    {
        std::string scratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\";
        std::string fileName = "OpenNonExistingFile.txt";

        std::string fileInScratch = scratchRoot + fileName;

        // make sure the file can't be opened and last error is file not found
        HANDLE hFileInScratch = CreateFile(fileInScratch.c_str(),
            GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            NULL);

        VERIFY_ARE_EQUAL(INVALID_HANDLE_VALUE, hFileInScratch);
        VERIFY_ARE_EQUAL(ERROR_FILE_NOT_FOUND, (HRESULT)GetLastError());
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

================================================
FILE: GVFS/GVFS.NativeTests/source/ProjFS_MoveFileTest.cpp
================================================
#include "stdafx.h"
#include "ProjFS_MoveFileTest.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "TestVerifiers.h"
#include "Should.h"

using namespace TestHelpers;
using namespace TestVerifiers;

static const std::string TEST_ROOT_FOLDER("\\GVFlt_MoveFileTest");
static const std::string _lessData = "lessData";
static const std::string _moreData = "moreData, moreData, moreData";

bool ProjFS_MoveFile_NoneToNone(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "NoneToNone\\";

        int error = MovFile(testScratchRoot + "from\\filenotexist", testScratchRoot + "to\\filenotexist");
        VERIFY_ARE_EQUAL((int)ENOENT, error);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "from"));
        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "to"));

        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "ffrom\\filenotexist"));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "to\\filenotexist"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_VirtualToNone(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToNone\\";

        int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\notexistInTo.txt");
        VERIFY_ARE_EQUAL((int)0, error);

        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\notexistInTo.txt"));

        VERIFY_ARE_EQUAL(_lessData, ReadFileAsString(testScratchRoot + "to\\notexistInTo.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_PartialToNone(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "PartialToNone\\";

        std::string expected = ReadFileAsString(testScratchRoot + "from\\lessInFrom.txt");
        int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\PartialToNone.txt");
        VERIFY_ARE_EQUAL((int)0, error);

        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\PartialToNone.txt"));

        AreEqual(expected, ReadFileAsString(testScratchRoot + "to\\PartialToNone.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_FullToNone(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "FullToNone\\";

        WriteToFile(testScratchRoot + "from\\lessInFrom.txt", "testtest");
        int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\FullToNone.txt");
        VERIFY_ARE_EQUAL((int)0, error);

        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\FullToNone.txt"));

        AreEqual("testtest", ReadFileAsString(testScratchRoot + "to\\FullToNone.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_LocalToNone(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "LocalToNone\\";

        CreateNewFile(testScratchRoot + "from\\local.txt", "test");
        int error = MovFile(testScratchRoot + "from\\local.txt", testScratchRoot + "to\\notexistInTo.txt");
        VERIFY_ARE_EQUAL((int)0, error);

        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\local.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\notexistInTo.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_VirtualToVirtual(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToVirtual\\";

        int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\lessInFrom.txt");
        VERIFY_ARE_EQUAL((int)EEXIST, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_VirtualToVirtualFileNameChanged(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToVirtualFileNameChanged\\";

        int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\moreInFrom.txt");
        VERIFY_ARE_EQUAL((int)EEXIST, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\moreInFrom.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_VirtualToPartial(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToPartial\\";

        ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt");
        int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\lessInFrom.txt");
        VERIFY_ARE_EQUAL((int)EEXIST, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_PartialToPartial(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "PartialToPartial\\";

        ReadFileAsString(testScratchRoot + "from\\lessInFrom.txt");
        ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt");
        int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\lessInFrom.txt");
        VERIFY_ARE_EQUAL((int)EEXIST, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_LocalToVirtual(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "LocalToVirtual\\";

        CreateNewFile(testScratchRoot + "from\\local.txt", _lessData);

        int error = MovFile(testScratchRoot + "from\\local.txt", testScratchRoot + "to\\lessInFrom.txt");
        VERIFY_ARE_EQUAL((int)EEXIST, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\local.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_VirtualToVirtualIntermidiateDirNotExist(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToVirtualIntermidiateDirNotExist\\";

        int error = MovFile(testScratchRoot + "from\\subFolder\\from.txt", testScratchRoot + "to\\subfolder\\to.txt");
        VERIFY_ARE_EQUAL((int)EEXIST, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\subFolder\\from.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\subfolder\\to.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_VirtualToNoneIntermidiateDirNotExist(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToNoneIntermidiateDirNotExist\\";

        int error = MovFile(testScratchRoot + "from\\subFolder\\from.txt", testScratchRoot + "to\\notexist\\to.txt");
        VERIFY_ARE_EQUAL((int)ENOENT, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\subFolder\\from.txt"));
        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "to\\notexist\\to.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}


bool ProjFS_MoveFile_OutsideToNone(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToNone\\";

        std::string outsideFolder = std::string(pathOutsideRepo) + "\\OutsideToNone\\from\\";
        CreateDirectoryWithIntermediates(outsideFolder);
        CreateNewFile(outsideFolder + "lessInFrom.txt", _lessData);

        int error = MovFile(outsideFolder + "lessInFrom.txt", testScratchRoot + "to\\less.txt");
        VERIFY_ARE_EQUAL((int)0, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\less.txt"));
        VERIFY_ARE_EQUAL(false, DoesFileExist(outsideFolder + "lessInFrom.txt"));

        AreEqual(_lessData, ReadFileAsString(testScratchRoot + "to\\less.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}


bool ProjFS_MoveFile_OutsideToVirtual(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToVirtual\\";
        
        std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToVirtual\\";
        CreateDirectoryWithIntermediates(testLayerRoot);
        CreateNewFile(testLayerRoot + "test.txt");

        int error = MovFile(testLayerRoot + "test.txt", testScratchRoot + "to\\lessInFrom.txt");
        VERIFY_ARE_EQUAL((int)EEXIST, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "test.txt"));

        AreEqual(_moreData, ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}


bool ProjFS_MoveFile_OutsideToPartial(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToPartial\\";

        std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToPartial\\";
        CreateDirectoryWithIntermediates(testLayerRoot);
        CreateNewFile(testLayerRoot + "test.txt");

        ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt");
        int error = MovFile(testLayerRoot + "test.txt", testScratchRoot + "to\\lessInFrom.txt");
        VERIFY_ARE_EQUAL((int)EEXIST, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "test.txt"));

        AreEqual(_moreData, ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}


bool ProjFS_MoveFile_NoneToOutside(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "NoneToOutside\\";

        int error = MovFile(testScratchRoot + "to\\less.txt", std::string(pathOutsideRepo) + "\\" + "from\\less.txt");
        VERIFY_ARE_EQUAL((int)ENOENT, error);

        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "to\\less.txt"));
        VERIFY_ARE_EQUAL(false, DoesFileExist(std::string(pathOutsideRepo) + "\\" + "from\\less.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}


bool ProjFS_MoveFile_VirtualToOutside(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToOutside\\";
        std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "VirtualToOutside\\";
        CreateDirectoryWithIntermediates(testLayerRoot);

        int error = MovFile(testScratchRoot + "to\\lessInFrom.txt", testLayerRoot + "less.txt");
        VERIFY_ARE_EQUAL((int)0, error);

        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt"));
        // VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "less.txt"));

        AreEqual(_moreData, ReadFileAsString(testLayerRoot + "less.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_PartialToOutside(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "PartialToOutside\\";
        std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "PartialToOutside\\";
        CreateDirectoryWithIntermediates(testLayerRoot);

        ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt");
        int error = MovFile(testScratchRoot + "to\\lessInFrom.txt", testLayerRoot + "less.txt");
        VERIFY_ARE_EQUAL((int)0, error);

        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt"));
        // VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "less.txt"));

        AreEqual(_moreData, ReadFileAsString(testLayerRoot + "less.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_OutsideToOutside(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToOutside\\";

        std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToOutside\\";
        CreateDirectoryWithIntermediates(testLayerRoot);
        CreateNewFile(testLayerRoot + "from.txt", _lessData);

        int error = MovFile(testLayerRoot + "from.txt", testLayerRoot + "to.txt");
        VERIFY_ARE_EQUAL((int)0, error);

        VERIFY_ARE_EQUAL(false, DoesFileExist(testLayerRoot + "from.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "to.txt"));

        AreEqual(_lessData, ReadFileAsString(testLayerRoot + "to.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFile_LongFileName(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "LongFileName\\";
        std::string filename = "LLLLLLongName0ToRenameFileToWithForChangeJournalABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghojnklmopqrstuvwxyz0123456789aaaaa";

        int error = MovFile(testScratchRoot + filename, testScratchRoot + filename + "1");
        VERIFY_ARE_EQUAL((int)0, error);

        VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + filename));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + filename + "1"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

================================================
FILE: GVFS/GVFS.NativeTests/source/ProjFS_MoveFolderTest.cpp
================================================
#include "stdafx.h"
#include "ProjFS_MoveFolderTest.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "TestVerifiers.h"
#include "Should.h"

using namespace TestHelpers;
using namespace TestVerifiers;

static const std::string TEST_ROOT_FOLDER("\\GVFlt_MoveFolderTest");

bool ProjFS_MoveFolder_NoneToNone(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "NoneToNone\\";

        int error = MovFile(testScratchRoot + "fromfolderNotExist", testScratchRoot + "tofolderNotExist");
        VERIFY_ARE_EQUAL((int)ENOENT, error);

        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "from"));
        VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "to"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFolder_VirtualToNone(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToNone\\";

        int error = MovFile(testScratchRoot + "from", testScratchRoot + "tofolderNotExist");
        VERIFY_ARE_EQUAL((int)EINVAL, error);

        // VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\notexistInTo.txt"));
        // VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "tofolderNotExist\\notexistInTo.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFolder_PartialToNone(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "PartialToNone\\";

        ReadFileAsString(testScratchRoot + "from\\notexistInTo.txt");
        int error = MovFile(testScratchRoot + "from", testScratchRoot + "tofolderNotExist");
        VERIFY_ARE_EQUAL((int)EINVAL, error);

        // VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\notexistInTo.txt"));
        // VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "tofolderNotExist\\notexistInTo.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFolder_VirtualToVirtual(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToVirtual\\";

        int error = MovFile(testScratchRoot + "from", testScratchRoot + "to");
        VERIFY_ARE_EQUAL((int)EINVAL, error);
        /*
        VERIFY_ARE_EQUAL((int)EEXIST, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to"));
        */
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFolder_VirtualToPartial(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToPartial\\";

        ReadFileAsString(testScratchRoot + "to\\notexistInFrom.txt");
        int error = MovFile(testScratchRoot + "from", testScratchRoot + "to");
        VERIFY_ARE_EQUAL((int)EINVAL, error);

        /*
        VERIFY_ARE_EQUAL((int)EEXIST, error);

        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to"));
        */
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFolder_OutsideToNone(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToNone\\";

        std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToNone";
        CreateDirectoryWithIntermediates(testLayerRoot);

        int error = MovFile(testLayerRoot, testScratchRoot + "notexists");
        VERIFY_ARE_EQUAL((int)0, error);

        VERIFY_ARE_EQUAL(false, DoesFileExist(testLayerRoot));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "notexists"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFolder_OutsideToVirtual(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToVirtual\\";

        std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToVirtual\\";
        CreateDirectoryWithIntermediates(testLayerRoot);

        int error = MovFile(testLayerRoot, testScratchRoot);
        VERIFY_ARE_EQUAL((int)EEXIST, error);
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to"));

        error = MovFile(testLayerRoot, testScratchRoot + "to");
        VERIFY_ARE_EQUAL((int)EEXIST, error);
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFolder_NoneToOutside(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "NoneToOutside\\";

        std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "NoneToOutside\\";
        CreateDirectoryWithIntermediates(testLayerRoot);

        int error = MovFile(testScratchRoot + "NotExist", testLayerRoot);
        VERIFY_ARE_EQUAL((int)ENOENT, error);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFolder_VirtualToOutside(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToOutside\\";

        std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "VirtualToOutside\\";
        CreateDirectoryWithIntermediates(testLayerRoot);

        int error = MovFile(testScratchRoot + "from", testLayerRoot);
        VERIFY_ARE_EQUAL((int)EINVAL, error);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_MoveFolder_OutsideToOutside(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToOutside\\";

        std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToOutside\\";
        CreateDirectoryWithIntermediates(testLayerRoot + "from\\");
        CreateNewFile(testLayerRoot + "from\\" + "test.txt", "test data");

        int error = MovFile(testLayerRoot + "from\\", testLayerRoot + "to\\");
        VERIFY_ARE_EQUAL((int)0, error);

        VERIFY_ARE_EQUAL(false, DoesFileExist(testLayerRoot + "from\\" + "test.txt"));
        VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "to\\" + "test.txt"));

        VERIFY_ARE_EQUAL("test data", ReadFileAsString(testLayerRoot + "to\\" + "test.txt"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}


================================================
FILE: GVFS/GVFS.NativeTests/source/ProjFS_MultiThreadTest.cpp
================================================
#include "stdafx.h"
#include "ProjFS_MultiThreadsTest.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "TestVerifiers.h"
#include "Should.h"

using namespace TestHelpers;
using namespace TestVerifiers;

static const std::string TEST_ROOT_FOLDER("\\GVFLT_MultiThreadTest");

static bool ReadFileThreadProc(HANDLE& hFile, const std::string& path, LONGLONG expectedSize, std::shared_future mainThreadReadyFuture, std::promise& threadReadyPromise)
{
    // Notify the main thread that our thread is ready
    threadReadyPromise.set_value();

    // Wait for the main thread to ask us to start
    mainThreadReadyFuture.wait();

    hFile = CreateFile(path.c_str(),
            FILE_READ_ATTRIBUTES,
            FILE_SHARE_READ,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS,
            NULL);

    if (hFile == INVALID_HANDLE_VALUE)
    {
        VERIFY_FAIL("CreateFile failed");
        return false;
    }

    LARGE_INTEGER fileSize;
    VERIFY_ARE_NOT_EQUAL(GetFileSizeEx(hFile, &fileSize), 0);
    VERIFY_ARE_EQUAL(fileSize.QuadPart, expectedSize);

    return true;
}

bool ProjFS_OpenForReadsSameTime(const char* virtualRootPath)
{
    try
    {
        std::string scratchTestRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OpenForReadsSameTime\\";

        const int threadCount = 10;
        std::array handles;
        
        std::promise mainThreadReadyPromise;
        std::shared_future mainThreadReadyFuture(mainThreadReadyPromise.get_future());
        std::vector> threadReadyPromises;
        threadReadyPromises.resize(threadCount);
        std::vector> threadCompleteFutures;

        // Start std::async for each thread
        for (size_t i = 0; i < threadReadyPromises.size(); ++i)
        {
            std::promise& promise = threadReadyPromises[i];
            threadCompleteFutures.push_back(std::async(
                std::launch::async, 
                ReadFileThreadProc, 
                std::ref(handles[i]), 
                scratchTestRoot + "test", 
                6,
                mainThreadReadyFuture, 
                std::ref(promise)));
        }

        // Wait for all threads to become ready
        for (std::promise& promise : threadReadyPromises)
        {
            promise.get_future().wait();
        }

        // Signal the threads to run
        mainThreadReadyPromise.set_value();

        // Wait for threads to complete
        for (std::future& openFuture : threadCompleteFutures)
        {
            openFuture.get();
        }

        for (HANDLE hFile : handles)
        {
            CloseHandle(hFile);
        }
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_OpenMultipleFilesForReadsSameTime(const char* virtualRootPath)
{
    try
    {
        std::string scratchTestRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OpenMultipleFilesForReadsSameTime\\";
        std::string scratchTestRoot2 = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OpenMultipleFilesForReadsSameTime_2\\";

        const char threadCount = 8;
        std::array handles;
        std::promise mainThreadReadyPromise;
        std::shared_future mainThreadReadyFuture(mainThreadReadyPromise.get_future());
        std::vector> threadReadyPromises;
        threadReadyPromises.resize(threadCount);
        std::vector> threadCompleteFutures;

        // Start std::async for each thread
        for (size_t i = 0; i < threadReadyPromises.size(); ++i)
        {
            // Files are named:
            // GVFlt_MultiThreadTest\OpenMultipleFilesForReadsSameTime\test1
            // GVFlt_MultiThreadTest\OpenMultipleFilesForReadsSameTime\test2
            // GVFlt_MultiThreadTest\OpenMultipleFilesForReadsSameTime\test3
            // GVFlt_MultiThreadTest\OpenMultipleFilesForReadsSameTime\test4
            // GVFlt_MultiThreadTest\OpenMultipleFilesForReadsSameTime_2\test5
            // GVFlt_MultiThreadTest\OpenMultipleFilesForReadsSameTime_2\test6
            // GVFlt_MultiThreadTest\OpenMultipleFilesForReadsSameTime_2\test7
            // GVFlt_MultiThreadTest\OpenMultipleFilesForReadsSameTime_2\test8

            std::promise& promise = threadReadyPromises[i];
            threadCompleteFutures.push_back(std::async(
                std::launch::async,
                ReadFileThreadProc,
                std::ref(handles[i]),
                (i <= 3 ? scratchTestRoot : scratchTestRoot2) + "test" + static_cast('0' + i + 1),
                13704 + i, // expected size
                mainThreadReadyFuture,
                std::ref(promise)));
        }

        // Wait for all threads to become ready
        for (std::promise& promise : threadReadyPromises)
        {
            promise.get_future().wait();
        }

        // Signal the threads to run
        mainThreadReadyPromise.set_value();

        // Wait for threads to complete
        for (std::future& openFuture : threadCompleteFutures)
        {
            openFuture.get();
        }

        for (HANDLE hFile : handles)
        {
            CloseHandle(hFile);
        }
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

static void WriteFileThreadProc(HANDLE& hFile, const std::string& path)
{
    hFile = CreateFile(path.c_str(),
            GENERIC_WRITE,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS,
            NULL);

    if (hFile == INVALID_HANDLE_VALUE)
    {
        VERIFY_FAIL("CreateFile failed");
    }
}

bool ProjFS_OpenForWritesSameTime(const char* virtualRootPath)
{
    try
    {
        std::string scratchTestRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OpenForWritesSameTime\\";

        const int threadCount = 10;
        std::thread threadList[threadCount];
        std::array handles;
        for (auto i = 0; i < threadCount; i++) {
            threadList[i] = std::thread(WriteFileThreadProc, std::ref(handles[i]), scratchTestRoot + "test");
        }

        for (auto i = 0; i < threadCount; i++) {
            threadList[i].join();
        }

        for (HANDLE hFile : handles)
        {
            CloseHandle(hFile);
        }
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}


================================================
FILE: GVFS/GVFS.NativeTests/source/ProjFS_SetLinkTest.cpp
================================================
#include "stdafx.h"
#include "ProjFS_SetLinkTest.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "TestVerifiers.h"
#include "Should.h"

using namespace TestHelpers;
using namespace TestVerifiers;

static const std::string TEST_ROOT_FOLDER("\\GVFlt_SetLinkTest");
static const std::string _testFile("test.txt");
static const std::string _data("test data");

bool ProjFS_SetLink_ToVirtualFile(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "ToVirtualFile\\";

        bool created = NewHardLink(testScratchRoot + "newlink", testScratchRoot + _testFile);
        VERIFY_ARE_EQUAL(true, created);

        AreEqual(_data, ReadFileAsString(testScratchRoot + "newlink"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_SetLink_ToPlaceHolder(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "ToPlaceHolder\\";

        ReadFileAsString(testScratchRoot + _testFile);

        bool created = NewHardLink(testScratchRoot + "newlink", testScratchRoot + _testFile);
        VERIFY_ARE_EQUAL(true, created);

        AreEqual(_data, ReadFileAsString(testScratchRoot + "newlink"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_SetLink_ToFullFile(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "ToFullFile\\";

        WriteToFile(testScratchRoot + _testFile, "new content");

        bool created = NewHardLink(testScratchRoot + "newlink", testScratchRoot + _testFile);
        VERIFY_ARE_EQUAL(true, created);

        AreEqual("new content", ReadFileAsString(testScratchRoot + _testFile));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_SetLink_ToNonExistFileWillFail(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "ToNonExistFileWillFail\\";

        bool created = NewHardLink(testScratchRoot + "newlink", testScratchRoot + "nonexist");
        VERIFY_ARE_EQUAL(false, created);
        VERIFY_ARE_EQUAL((DWORD)ERROR_FILE_NOT_FOUND, GetLastError());
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_SetLink_NameAlreadyExistWillFail(const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "NameAlreadyExistWillFail\\";

        bool created = NewHardLink(testScratchRoot + "foo.txt", testScratchRoot + _testFile);
        VERIFY_ARE_EQUAL(false, created);
        VERIFY_ARE_EQUAL((DWORD)ERROR_ALREADY_EXISTS, GetLastError());
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_SetLink_FromOutside(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "FromOutside\\";

        bool created = NewHardLink(std::string(pathOutsideRepo) + "\\" + "FromOutsideLink", testScratchRoot + _testFile);
        VERIFY_ARE_EQUAL(true, created);
        AreEqual(_data, ReadFileAsString(std::string(pathOutsideRepo) + "\\" + "FromOutsideLink"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ProjFS_SetLink_ToOutside(const char* pathOutsideRepo, const char* virtualRootPath)
{
    try
    {
        std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "ToOutside\\";

        CreateNewFile(std::string(pathOutsideRepo) + "\\" + _testFile, _data);
        bool created = NewHardLink(testScratchRoot + "newlink", std::string(pathOutsideRepo) + "\\" + _testFile);
        VERIFY_ARE_EQUAL(true, created);
        AreEqual(_data, ReadFileAsString(testScratchRoot + "newlink"));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

================================================
FILE: GVFS/GVFS.NativeTests/source/ReadAndWriteTests.cpp
================================================
#include "stdafx.h"
#include "ReadAndWriteTests.h"
#include "SafeHandle.h"
#include "SafeOverlapped.h"
#include "TestException.h"
#include "Should.h"

namespace
{
    static const char* TEST_STRING = "*TEST*12345678#TEST#";

    // ReadOverlapped: Read text from the specified handle using async overlapped IO
    //
    // handle -> Handle to file
    // maxNumberOfBytesToRead -> Maximum number of bytes to read
    // expectedNumberOfBytesToRead -> Expected number of bytes to read
    // offset -> Offset (from the beginning of the file) where read should start
    // expectedContent -> Expected content or nullptr if content should not be validated
    //
    //
    // Returns -> Shared point to the contents that have been read
    std::shared_ptr ReadOverlapped(SafeHandle& handle, unsigned long maxNumberOfBytesToRead, unsigned long expectedNumberOfBytesToRead, unsigned long offset);

    // WriteOverlapped: Write text to the specified handle using async overlapped IO
    //
    // handle -> Handle to file
    // buffer -> Data to write to file
    // numberOfBytesToWrite -> Number of bytes to write to file
    // offset -> Offset (from the beginning of the file) where write should start
    void WriteOverlapped(SafeHandle& handle, LPCVOID buffer, unsigned long numberOfBytesToWrite, unsigned long offset);

    // GetAllFiles: Get all of the files in the folder at path, and in any subfolders
    //
    // path -> Path to folder to enumerate
    // files -> [Out] Vector of file names and sizes
    void GetAllFiles(const std::string& path, std::vector, DWORD>>* files);

    // OpenAndReadFiles: Open and read files
    //
    // files -> Files to be opened and read
    void OpenAndReadFiles(const std::vector, DWORD>>& files);

    // FindFileShouldSucceed: Confirms that specified path do exists using FindFirstFile
    void FindFileShouldSucceed(const std::string& path);

    // FindFileExShouldSucceed: Confirms that specified path do exists using FindFirstFileEx
    void FindFileExShouldSucceed(const std::string& path, FINDEX_INFO_LEVELS infoLevelId, FINDEX_SEARCH_OPS searchOp);

    // FindFileErrorsMatch: Confirms that specified paths do not exist, and that the error codes returned for nonExistentVirtualPath
    //                      and nonExistentPhysicalPath are the same.  Check is performed using FindFirstFile
    //
    // nonExistentVirtualPath -> Virtual path that is known to not exist, can contain wildcards
    // nonExistentPhysicalPath -> Physical path that is known to not exist, can contain wildcards
    void FindFileErrorsMatch(const std::string& nonExistentVirtualPath, const std::string& nonExistentPhysicalPath);

    // FindFileExErrorsMatch: Confirms that specified paths do not exist, and that the error codes returned for nonExistentVirtualPath
    //                        and nonExistentPhysicalPath are the same.  Check is performed using FindFirstFileEx
    //
    // nonExistentVirtualPath -> Virtual path that is known to not exist, can contain wildcards
    // nonExistentPhysicalPath -> Physical path that is known to not exist, can contain wildcards
    // infoLevelId -> The information level of the returned data (returned by FindFirstFileEx)
    // searchOp -> The type of filtering to perform that is different from wildcard matching
    void FindFileExErrorsMatch(const std::string& nonExistentVirtualPath, const std::string& nonExistentPhysicalPath, FINDEX_INFO_LEVELS infoLevelId, FINDEX_SEARCH_OPS searchOp);
}

// Read and write to a file, using synchronous IO and a different
// file handle for each read/write
bool ReadAndWriteSeparateHandles(const char* fileVirtualPath)
{
    try
    {
        // Build a long test string
        std::string writeContent;
        while (writeContent.length() < 512)
        {
            writeContent.append(TEST_STRING);
        }

        SafeHandle writeFile(CreateFile(
            fileVirtualPath,                // lpFileName
            (GENERIC_READ | GENERIC_WRITE), // dwDesiredAccess
            FILE_SHARE_READ,                // dwShareMode
            NULL,                           // lpSecurityAttributes
            CREATE_NEW,                     // dwCreationDisposition
            FILE_ATTRIBUTE_NORMAL,          // dwFlagsAndAttributes
            NULL));                         // hTemplateFile

        SHOULD_NOT_EQUAL(writeFile.GetHandle(), NULL);

        // Confirm there is nothing to read in the file
        const int readBufferLength = 48;
        char initialReadBuffer[readBufferLength];
        unsigned long numRead = 0;
        SHOULD_NOT_EQUAL(ReadFile(writeFile.GetHandle(), initialReadBuffer, readBufferLength - 1, &numRead, NULL), FALSE);
        SHOULD_EQUAL(numRead, 0);

        // Write test string	
        unsigned long numWritten = 0;
        WriteFile(writeFile.GetHandle(), writeContent.data(), static_cast(writeContent.length()), &numWritten, NULL);
        SHOULD_EQUAL(numWritten, writeContent.length());

        writeFile.CloseHandle();        
        
        // Re-open file for read
        SafeHandle readFile(CreateFile(
            fileVirtualPath,                // lpFileName
            (GENERIC_READ | GENERIC_WRITE), // dwDesiredAccess
            FILE_SHARE_READ,                // dwShareMode
            NULL,                           // lpSecurityAttributes
            OPEN_EXISTING,                  // dwCreationDisposition
            FILE_ATTRIBUTE_NORMAL,          // dwFlagsAndAttributes
            NULL));                         // hTemplateFile

        // Read test string
        unsigned long expectedContentLength = static_cast(writeContent.length());
        numRead = 0;
        std::shared_ptr readBuffer(new char[expectedContentLength + 1], delete_array());
        readBuffer.get()[expectedContentLength] = '\0';
        SHOULD_NOT_EQUAL(ReadFile(readFile.GetHandle(), readBuffer.get(), expectedContentLength, &numRead, NULL), FALSE);
        SHOULD_EQUAL(numRead, expectedContentLength);
        SHOULD_EQUAL(strcmp(writeContent.c_str(), readBuffer.get()), 0);

        readFile.CloseHandle();        

        SHOULD_NOT_EQUAL(DeleteFile(fileVirtualPath), 0);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

// Read and write to a file, using asynchronous IO and the same
// file handle for each read/write
bool ReadAndWriteSameHandle(const char* fileVirtualPath, bool synchronousIO)
{
    try
    {        
        SafeHandle file(CreateFile(
            fileVirtualPath,                                                      // lpFileName
            (GENERIC_READ | GENERIC_WRITE),                                       // dwDesiredAccess
            FILE_SHARE_READ,                                                      // dwShareMode
            NULL,                                                                 // lpSecurityAttributes
            CREATE_NEW,                                                           // dwCreationDisposition
            (FILE_ATTRIBUTE_NORMAL | (synchronousIO ? 0 : FILE_FLAG_OVERLAPPED)), // dwFlagsAndAttributes
            NULL));                                                               // hTemplateFile

        SHOULD_NOT_EQUAL(file.GetHandle(), NULL);

        // Confirm there is nothing to read in the file
        ReadOverlapped(file, 48 /*maxNumberOfBytesToRead*/, 0 /*expectedNumberOfBytesToRead*/, 0 /*offset*/);

        // Build a long test string
        std::string writeContent;
        while (writeContent.length() < 512000)
        {
            writeContent.append(TEST_STRING);
        }

        // Write test string
        WriteOverlapped(file, writeContent.data(), static_cast(writeContent.length()), 0);

        // Read back what was just written
        std::shared_ptr readContent = ReadOverlapped(
            file,
            static_cast(writeContent.length()) /*maxNumberOfBytesToRead*/,
            static_cast(writeContent.length()) /*expectedNumberOfBytesToRead*/,
            0 /*offset*/);

        SHOULD_EQUAL(strcmp(writeContent.c_str(), readContent.get()), 0);

        // Read back with two async requests, one with offset and one without
        {
            bool asyncReadNoOffset = false;
            SafeOverlapped overlappedRead;
            overlappedRead.overlapped.hEvent = CreateEvent(
                NULL,  // lpEventAttributes
                true,  // bManualReset
                false, // bInitialState
                NULL); // lpName

            bool asyncReadWithOffset = false;
            const int READ_OFFSET = 48;
            SafeOverlapped overlappedReadWithOffset;
            overlappedReadWithOffset.overlapped.Offset = READ_OFFSET;
            overlappedReadWithOffset.overlapped.hEvent = CreateEvent(
                NULL,  // lpEventAttributes
                true,  // bManualReset
                false, // bInitialState
                NULL); // lpName


            // Read without offset
            unsigned long bytesRead = 0;
            std::shared_ptr readBuffer(new char[writeContent.length() + 1], delete_array());
            if (!ReadFile(file.GetHandle(), readBuffer.get(), (DWORD)writeContent.length(), &bytesRead, &overlappedRead.overlapped))
            {
                unsigned long lastError = GetLastError();
                SHOULD_EQUAL(lastError, ERROR_IO_PENDING);

                asyncReadNoOffset = true;
            }
            else
            {
                SHOULD_EQUAL(bytesRead, writeContent.length());
            }

            // Read with offset
            std::shared_ptr readBufferWithOffset(new char[writeContent.length() + 1 - READ_OFFSET], delete_array());
            if (!ReadFile(file.GetHandle(), readBufferWithOffset.get(), (DWORD)writeContent.length() - READ_OFFSET, &bytesRead, &overlappedReadWithOffset.overlapped))
            {
                unsigned long lastError = GetLastError();
                SHOULD_EQUAL(lastError, ERROR_IO_PENDING);

                asyncReadWithOffset = true;
            }
            else
            {
                SHOULD_EQUAL(bytesRead, writeContent.length() - READ_OFFSET);
            }

            // Wait for async result
            if (asyncReadNoOffset)
            {
                GetOverlappedResult(file.GetHandle(), &overlappedRead.overlapped, &bytesRead, true);
                SHOULD_EQUAL(bytesRead, writeContent.length());
            }

            if (asyncReadWithOffset)
            {
                GetOverlappedResult(file.GetHandle(), &overlappedReadWithOffset.overlapped, &bytesRead, true);
                SHOULD_EQUAL(bytesRead, writeContent.length() - READ_OFFSET);
            }
        }
        
        file.CloseHandle();

        SHOULD_NOT_EQUAL(DeleteFile(fileVirtualPath), false);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

// Read and write to a file, using the same file handle for each read/write.  Reads and writes are done
// repeatedly using a pattern observed with tracer.exe (as part of the Windows Mobile build)
bool ReadAndWriteRepeatedly(const char* fileVirtualPath, bool synchronousIO)
{
    struct TestStep
    {
        TestStep(unsigned long offset, unsigned long maxBytesToRead, unsigned long expectedBytesToRead, unsigned long writeContentsLength)
            : offset(offset)
            , maxBytesToRead(maxBytesToRead)
            , expectedBytesToRead(expectedBytesToRead)
            , writeContentsLength(writeContentsLength)
        {
        }

        unsigned long offset;
        unsigned long maxBytesToRead;
        unsigned long expectedBytesToRead;
        unsigned long writeContentsLength;
    };

    try
    {
        SafeHandle file(CreateFile(
            fileVirtualPath,                                                      // lpFileName
            (GENERIC_READ | GENERIC_WRITE),                                       // dwDesiredAccess
            FILE_SHARE_READ,                                                      // dwShareMode
            NULL,                                                                 // lpSecurityAttributes
            CREATE_NEW,                                                           // dwCreationDisposition
            (FILE_ATTRIBUTE_NORMAL | (synchronousIO ? 0 : FILE_FLAG_OVERLAPPED)), // dwFlagsAndAttributes
            NULL));                                                               // hTemplateFile

        SHOULD_NOT_EQUAL(file.GetHandle(), NULL);

        // Test steps mimic the behavior exhibited by tracer.exe
        std::vector testSteps;

        // Start at an offset of 48, try to read some data but there will be nothing to read, then write 512000 bytes of data
        testSteps.push_back(
            TestStep(
                48 /*offset*/, 
                48 /*maxNumberOfBytesToRead*/, 
                0 /*expectedNumberOfBytesToRead*/, 
                512000 /*writeContentsLength*/)
            );

        // Back up to an offset of 32, try to read as much data as was last written ,and then write 876000 bytes of data
        testSteps.push_back(
            TestStep(
                32 /*offset*/, 
                (*testSteps.rbegin()).writeContentsLength /*maxNumberOfBytesToRead*/, 
                (*testSteps.rbegin()).writeContentsLength /*expectedNumberOfBytesToRead*/, 
                876000 /*writeContentsLength*/)
            );

        // Advance to where writing just left off, attempt to read 0 bytes of data, and then write 1000000  bytes of data
        testSteps.push_back(
            TestStep(
                (*testSteps.rbegin()).offset + (*testSteps.rbegin()).writeContentsLength /*offset */, 
                0 /*maxNumberOfBytesToRead*/, 
                0 /*expectedNumberOfBytesToRead*/, 
                1000000 /*writeContentsLength*/)
            );
        
        // Advance to where writing just left off, attempt to read 0 bytes of data, and then write 24000  bytes of data
        testSteps.push_back(
            TestStep((*testSteps.rbegin()).offset + (*testSteps.rbegin()).writeContentsLength /*offset */, 
                0 /*maxNumberOfBytesToRead*/, 
                0 /*expectedNumberOfBytesToRead*/, 
                24000 /*writeContentsLength*/)
            );

        // Run the above test steps
        for (const TestStep& step : testSteps)
        {
            ReadOverlapped(file, step.maxBytesToRead, step.expectedBytesToRead, step.offset);

            std::string writeContent;
            while (writeContent.length() < step.writeContentsLength)
            {
                writeContent.append(TEST_STRING);
            }

            WriteOverlapped(file, writeContent.data(), static_cast(writeContent.length()), step.offset);
        }

        // Final step, back up to an offset of 500000, and read the remainder of the file
        unsigned long fileLength = (*testSteps.rbegin()).offset + (*testSteps.rbegin()).writeContentsLength;
        unsigned long readOffset = 500000;
        ReadOverlapped(
            file,
            fileLength /*maxNumberOfBytesToRead*/,
            fileLength - readOffset /*expectedNumberOfBytesToRead*/,
            readOffset /*offset*/);

        file.CloseHandle();

        SHOULD_NOT_EQUAL(DeleteFile(fileVirtualPath), false);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool RemoveReadOnlyAttribute(const char* fileVirtualPath)
{
    try
    {
        // Create a file with ReadOnly attribute
        SafeHandle file(CreateFile(
            fileVirtualPath,                    // lpFileName
            (GENERIC_READ | GENERIC_WRITE),     // dwDesiredAccess
            FILE_SHARE_READ,                    // dwShareMode
            NULL,                               // lpSecurityAttributes
            CREATE_NEW,                         // dwCreationDisposition
            FILE_ATTRIBUTE_READONLY,            // dwFlagsAndAttributes
            NULL));                             // hTemplateFile

        SHOULD_NOT_EQUAL(file.GetHandle(), NULL);

        std::string writeContent(TEST_STRING);

        // Write test string
        WriteOverlapped(file, writeContent.data(), static_cast(writeContent.length()), 0);

        // Read back what was just written
        std::shared_ptr readContent = ReadOverlapped(
            file,
            static_cast(writeContent.length()) /*maxNumberOfBytesToRead*/,
            static_cast(writeContent.length()) /*expectedNumberOfBytesToRead*/,
            0 /*offset*/);

        SHOULD_EQUAL(strcmp(writeContent.c_str(), readContent.get()), 0);

        file.CloseHandle();

        // Confirm that FILE_ATTRIBUTE_READONLY is set
        DWORD attributes = GetFileAttributes(fileVirtualPath);
        SHOULD_EQUAL(attributes & FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_READONLY);

        // Open the file again so that the file is no longer read only
        SafeHandle existingFile(CreateFile(
            fileVirtualPath,                                // lpFileName
            (FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES), // dwDesiredAccess
            FILE_SHARE_READ,                                // dwShareMode
            NULL,                                           // lpSecurityAttributes
            OPEN_EXISTING,                                  // dwCreationDisposition
            FILE_ATTRIBUTE_NORMAL,                          // dwFlagsAndAttributes
            NULL));                                         // hTemplateFile

        SHOULD_NOT_EQUAL(existingFile.GetHandle(), NULL);

        // Confirm (by handle) that FILE_ATTRIBUTE_READONLY is set
        BY_HANDLE_FILE_INFORMATION fileInfo;
        SHOULD_BE_TRUE(GetFileInformationByHandle(existingFile.GetHandle(), &fileInfo));
        SHOULD_EQUAL(fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_READONLY);

        // Set the new file info (to clear read only)
        FILE_BASIC_INFO newInfo;
        memset(&newInfo, 0, sizeof(FILE_BASIC_INFO));
        newInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL;
        SHOULD_NOT_EQUAL(SetFileInformationByHandle(existingFile.GetHandle(), FileBasicInfo, &newInfo, sizeof(FILE_BASIC_INFO)), 0);

        // Confirm that FILE_ATTRIBUTE_READONLY has been cleared
        SHOULD_NOT_EQUAL(GetFileInformationByHandle(existingFile.GetHandle(), &fileInfo), 0);
        SHOULD_EQUAL(fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY, 0);

        existingFile.CloseHandle();

        // Confirm that FILE_ATTRIBUTE_READONLY has been cleared (by file name)
        attributes = GetFileAttributes(fileVirtualPath);
        SHOULD_EQUAL(attributes & FILE_ATTRIBUTE_READONLY, 0);

        // Cleanup
        SHOULD_NOT_EQUAL(DeleteFile(fileVirtualPath), 0);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool CannotWriteToReadOnlyFile(const char* fileVirtualPath)
{
    try
    {
        // Create a file with ReadOnly attribute and confirm that it can be written to (i.e. to
        // populate the intial contents)
        SafeHandle file(CreateFile(
            fileVirtualPath,                    // lpFileName
            (GENERIC_READ | GENERIC_WRITE),     // dwDesiredAccess
            FILE_SHARE_READ,                    // dwShareMode
            NULL,                               // lpSecurityAttributes
            CREATE_NEW,                         // dwCreationDisposition
            FILE_ATTRIBUTE_READONLY,            // dwFlagsAndAttributes
            NULL));                             // hTemplateFile

        SHOULD_NOT_EQUAL(file.GetHandle(), NULL);

        std::string writeContent("This file was created with the FILE_ATTRIBUTE_READONLY attribute");

        // Write test string
        WriteOverlapped(file, writeContent.data(), static_cast(writeContent.length()), 0);

        // Read back what was just written
        std::shared_ptr readContent = ReadOverlapped(
            file,
            static_cast(writeContent.length()) /*maxNumberOfBytesToRead*/,
            static_cast(writeContent.length()) /*expectedNumberOfBytesToRead*/,
            0 /*offset*/);

        SHOULD_EQUAL(strcmp(writeContent.c_str(), readContent.get()), 0);

        file.CloseHandle();

        // Try to open the file again for write access
        SafeHandle existingFile(CreateFile(
            fileVirtualPath,                // lpFileName
            (GENERIC_READ | GENERIC_WRITE), // dwDesiredAccess
            FILE_SHARE_READ,                // dwShareMode
            NULL,                           // lpSecurityAttributes
            OPEN_EXISTING,                  // dwCreationDisposition
            FILE_ATTRIBUTE_NORMAL,          // dwFlagsAndAttributes
            NULL));                         // hTemplateFile

        unsigned long lastError = GetLastError();

        // We should fail to get a handle (since we've requested GENERIC_WRITE access for a read only file)
        SHOULD_EQUAL(existingFile.GetHandle(), INVALID_HANDLE_VALUE);
        SHOULD_EQUAL(lastError, ERROR_ACCESS_DENIED);

        // Cleanup (remove read only attribute and delete file)
        SafeHandle changeAttribHandle(CreateFile(
            fileVirtualPath,                                // lpFileName
            (FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES), // dwDesiredAccess
            FILE_SHARE_READ,                                // dwShareMode
            NULL,                                           // lpSecurityAttributes
            OPEN_EXISTING,                                  // dwCreationDisposition
            FILE_ATTRIBUTE_NORMAL,                          // dwFlagsAndAttributes
            NULL));                                         // hTemplateFile

        FILE_BASIC_INFO newInfo;
        memset(&newInfo, 0, sizeof(FILE_BASIC_INFO));
        newInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL;
        SHOULD_NOT_EQUAL(SetFileInformationByHandle(changeAttribHandle.GetHandle(), FileBasicInfo, &newInfo, sizeof(FILE_BASIC_INFO)), 0);
        changeAttribHandle.CloseHandle();

        SHOULD_NOT_EQUAL(DeleteFile(fileVirtualPath), 0);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool EnumerateAndReadDoesNotChangeEnumerationOrder(const char* folderVirtualPath)
{
    try
    {
        std::vector, DWORD>> firstEnumerationFiles;
        GetAllFiles(folderVirtualPath, &firstEnumerationFiles);
        OpenAndReadFiles(firstEnumerationFiles);

        std::vector, DWORD>> secondEnumerationFiles;
        GetAllFiles(folderVirtualPath, &secondEnumerationFiles);

        SHOULD_EQUAL(firstEnumerationFiles.size(), secondEnumerationFiles.size());
        for (size_t i = 0; i < firstEnumerationFiles.size(); ++i)
        {
            SHOULD_EQUAL(firstEnumerationFiles[i].second, secondEnumerationFiles[i].second);
            SHOULD_EQUAL(strcmp(firstEnumerationFiles[i].first.data(), secondEnumerationFiles[i].first.data()), 0);
        }
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool EnumerationErrorsMatchNTFSForNonExistentFolder(const char* nonExistentVirtualPath, const char* nonExistentPhysicalPath)
{
    try
    {		
        FindFileErrorsMatch(nonExistentVirtualPath, nonExistentPhysicalPath);
        FindFileExErrorsMatch(nonExistentVirtualPath, nonExistentPhysicalPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExErrorsMatch(nonExistentVirtualPath, nonExistentPhysicalPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExErrorsMatch(nonExistentVirtualPath, nonExistentPhysicalPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExErrorsMatch(nonExistentVirtualPath, nonExistentPhysicalPath, FindExInfoStandard, FindExSearchLimitToDirectories);

        std::string virtualSubFolderPath  = nonExistentVirtualPath + std::string("\\non_existent_sub_item");
        std::string physicalSubFolderPath = nonExistentPhysicalPath + std::string("\\non_existent_sub_item");
        FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);

        virtualSubFolderPath = nonExistentVirtualPath + std::string("*");
        physicalSubFolderPath = nonExistentPhysicalPath + std::string("*");
        FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);

        virtualSubFolderPath = nonExistentVirtualPath + std::string("?");
        physicalSubFolderPath = nonExistentPhysicalPath + std::string("?");
        FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);

        virtualSubFolderPath = nonExistentVirtualPath + std::string("\\*");
        physicalSubFolderPath = nonExistentPhysicalPath + std::string("\\*");
        FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);

        virtualSubFolderPath = nonExistentVirtualPath + std::string("\\*.*");
        physicalSubFolderPath = nonExistentPhysicalPath + std::string("\\*.*");
        FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool EnumerationErrorsMatchNTFSForEmptyFolder(const char* emptyFolderVirtualPath, const char* emptyFolderPhysicalPath)
{
    try
    {
        SHOULD_BE_TRUE(PathIsDirectoryEmpty(emptyFolderVirtualPath));
        SHOULD_BE_TRUE(PathIsDirectoryEmpty(emptyFolderPhysicalPath));

        FindFileShouldSucceed(emptyFolderVirtualPath);
        FindFileShouldSucceed(emptyFolderPhysicalPath);

        FindFileExShouldSucceed(emptyFolderVirtualPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExShouldSucceed(emptyFolderVirtualPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExShouldSucceed(emptyFolderVirtualPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(emptyFolderVirtualPath, FindExInfoStandard, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(emptyFolderPhysicalPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExShouldSucceed(emptyFolderPhysicalPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExShouldSucceed(emptyFolderPhysicalPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(emptyFolderPhysicalPath, FindExInfoStandard, FindExSearchLimitToDirectories);

        std::string virtualSubFolderPath = emptyFolderVirtualPath + std::string("\\non_existent_sub_item");
        std::string physicalSubFolderPath = emptyFolderPhysicalPath + std::string("\\non_existent_sub_item");
        FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);

        virtualSubFolderPath = emptyFolderVirtualPath + std::string("*");
        physicalSubFolderPath = emptyFolderPhysicalPath + std::string("*");
        FindFileShouldSucceed(virtualSubFolderPath);
        FindFileShouldSucceed(physicalSubFolderPath);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);

        virtualSubFolderPath = emptyFolderVirtualPath + std::string("?");
        physicalSubFolderPath = emptyFolderPhysicalPath + std::string("?");
        FindFileShouldSucceed(virtualSubFolderPath);
        FindFileShouldSucceed(physicalSubFolderPath);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);

        virtualSubFolderPath = emptyFolderVirtualPath + std::string("\\*");
        physicalSubFolderPath = emptyFolderPhysicalPath + std::string("\\*");
        FindFileShouldSucceed(virtualSubFolderPath);
        FindFileShouldSucceed(physicalSubFolderPath);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);

        virtualSubFolderPath = emptyFolderVirtualPath + std::string("\\*.*");
        physicalSubFolderPath = emptyFolderPhysicalPath + std::string("\\*.*");
        FindFileShouldSucceed(virtualSubFolderPath);
        FindFileShouldSucceed(physicalSubFolderPath);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories);
        FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool CanDeleteEmptyFolderWithFileDispositionOnClose(const char* emptyFolderPath)
{
    try
    {
        SHOULD_BE_TRUE(PathIsDirectoryEmpty(emptyFolderPath));

        SafeHandle emptyFolder(CreateFile(
            emptyFolderPath,                         // lpFileName
            (GENERIC_READ | GENERIC_WRITE | DELETE), // dwDesiredAccess
            0,                                       // dwShareMode
            NULL,                                    // lpSecurityAttributes
            OPEN_EXISTING,                           // dwCreationDisposition
            FILE_FLAG_BACKUP_SEMANTICS,              // dwFlagsAndAttributes
            NULL));                                  // hTemplateFile
        SHOULD_NOT_EQUAL(emptyFolder.GetHandle(), INVALID_HANDLE_VALUE);

        FILE_DISPOSITION_INFO dispositionInfo;
        dispositionInfo.DeleteFile = TRUE;
        BOOL result = SetFileInformationByHandle(emptyFolder.GetHandle(), FileDispositionInfo, &dispositionInfo, sizeof(FILE_DISPOSITION_INFO));
        SHOULD_BE_TRUE(result);
        emptyFolder.CloseHandle();
        SHOULD_BE_TRUE(!PathIsDirectoryEmpty(emptyFolderPath));
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

bool ErrorWhenPathTreatsFileAsFolderMatchesNTFS(const char* fileVirtualPath, const char* fileNTFSPath, int creationDisposition)
{
    try
    {
        // Confirm the files exist
        SafeHandle physicalFile(CreateFile(
            fileNTFSPath,                   // lpFileName
            GENERIC_READ,                   // dwDesiredAccess
            0,                              // dwShareMode
            NULL,                           // lpSecurityAttributes
            OPEN_EXISTING,                  // dwCreationDisposition
            FILE_ATTRIBUTE_NORMAL,          // dwFlagsAndAttributes
            NULL));                         // hTemplateFile
        SHOULD_NOT_EQUAL(physicalFile.GetHandle(), INVALID_HANDLE_VALUE);
        physicalFile.CloseHandle();

        SafeHandle virtualFile(CreateFile(
            fileVirtualPath,                // lpFileName
            GENERIC_READ,                   // dwDesiredAccess
            0,                              // dwShareMode
            NULL,                           // lpSecurityAttributes
            OPEN_EXISTING,                  // dwCreationDisposition
            FILE_ATTRIBUTE_NORMAL,          // dwFlagsAndAttributes
            NULL));                         // hTemplateFile
        SHOULD_NOT_EQUAL(virtualFile.GetHandle(), INVALID_HANDLE_VALUE);
        virtualFile.CloseHandle();

        std::string bogusNTFSPath(fileNTFSPath);
        bogusNTFSPath += "\\HEAD";
        SafeHandle bogusNTFSFile(CreateFile(
            bogusNTFSPath.c_str(),          // lpFileName
            GENERIC_READ | GENERIC_WRITE,   // dwDesiredAccess
            0,                              // dwShareMode
            NULL,                           // lpSecurityAttributes
            creationDisposition,            // dwCreationDisposition
            FILE_ATTRIBUTE_NORMAL,          // dwFlagsAndAttributes
            NULL));                         // hTemplateFile
        SHOULD_EQUAL(bogusNTFSFile.GetHandle(), INVALID_HANDLE_VALUE);
        DWORD bogusNTFSFileError = GetLastError();

        std::string bogusVirtualPath(fileVirtualPath);
        bogusVirtualPath += "\\HEAD";
        SafeHandle bogusVirtualFile(CreateFile(
            bogusVirtualPath.c_str(),       // lpFileName
            GENERIC_READ | GENERIC_WRITE,   // dwDesiredAccess
            0,                              // dwShareMode
            NULL,                           // lpSecurityAttributes
            creationDisposition,            // dwCreationDisposition
            FILE_ATTRIBUTE_NORMAL,          // dwFlagsAndAttributes
            NULL));                         // hTemplateFile
        SHOULD_EQUAL(bogusVirtualFile.GetHandle(), INVALID_HANDLE_VALUE);
        DWORD bogusVirtualFileError = GetLastError();

        SHOULD_EQUAL(bogusVirtualFileError, bogusNTFSFileError);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

namespace
{
    std::shared_ptr ReadOverlapped(SafeHandle& handle, unsigned long maxNumberOfBytesToRead, unsigned long expectedNumberOfBytesToRead, unsigned long offset)
    {
        SafeOverlapped overlappedRead;
        overlappedRead.overlapped.Offset = offset;
        overlappedRead.overlapped.hEvent = CreateEvent(
            NULL,  // lpEventAttributes
            true,  // bManualReset
            false, // bInitialState
            NULL); // lpName

        unsigned long bytesRead = 0;
        std::shared_ptr readBuffer(new char[maxNumberOfBytesToRead + 1], delete_array());
        readBuffer.get()[0] = '\0';
        readBuffer.get()[maxNumberOfBytesToRead] = '\0';
        if (!ReadFile(handle.GetHandle(), readBuffer.get(), maxNumberOfBytesToRead, &bytesRead, &overlappedRead.overlapped))
        {
            unsigned long lastError = GetLastError();
            if (lastError == ERROR_IO_PENDING)
            {
                GetOverlappedResult(handle.GetHandle(), &overlappedRead.overlapped, &bytesRead, true);
                SHOULD_EQUAL(bytesRead, expectedNumberOfBytesToRead);
            }
            else if (lastError == ERROR_HANDLE_EOF)
            {
                SHOULD_EQUAL(bytesRead, expectedNumberOfBytesToRead);
            }
            else
            {
                // Unexpected lastError value
                FAIL_TEST("Unexpected lastError value");
            }
        }
        else
        {
            SHOULD_EQUAL(bytesRead, expectedNumberOfBytesToRead);
        }

        return readBuffer;
    }

    void WriteOverlapped(SafeHandle& handle, LPCVOID buffer, unsigned long numberOfBytesToWrite, unsigned long offset)
    {
        SafeOverlapped overlappedWrite;
        overlappedWrite.overlapped.Offset = offset;
        overlappedWrite.overlapped.hEvent = CreateEvent(
            NULL,  // lpEventAttributes
            true,  // bManualReset
            false, // bInitialState
            NULL); // lpName

        unsigned long numWritten = 0;
        if (!WriteFile(handle.GetHandle(), buffer, numberOfBytesToWrite, &numWritten, &overlappedWrite.overlapped))
        {
            unsigned long lastError = GetLastError();
            SHOULD_EQUAL(lastError, ERROR_IO_PENDING);

            GetOverlappedResult(handle.GetHandle(), &overlappedWrite.overlapped, &numWritten, true);
            SHOULD_EQUAL(numWritten, numberOfBytesToWrite);
        }
        else
        {
            SHOULD_EQUAL(numWritten, numberOfBytesToWrite);
        }
    }

    void GetAllFiles(const std::string& path, std::vector, DWORD>>* files)
    {
        WIN32_FIND_DATA ffd;
        HANDLE hFind = INVALID_HANDLE_VALUE;

        // List of directories and files
        std::list> directories;

        // Walk each directory, pushing new directory entries and file entries to directories and files
        directories.push_back(std::array());
        strcpy_s(directories.begin()->data(), MAX_PATH, path.c_str());

        std::list>::iterator iterDirectories = directories.begin();
        while (iterDirectories != directories.end())
        {
            char dirSearchPath[MAX_PATH];
            sprintf_s(dirSearchPath, "%s\\*", (*iterDirectories).data());

            hFind = FindFirstFile(dirSearchPath, &ffd);

            SHOULD_NOT_EQUAL(hFind, INVALID_HANDLE_VALUE);

            do
            {
                if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
                {
                    if (ffd.cFileName[0] != '.')
                    {
                        // Add new directory to the end of the list
                        directories.push_back(std::array());
                        sprintf_s((*directories.rbegin()).data(), MAX_PATH, "%s\\%s", (*iterDirectories).data(), ffd.cFileName);
                    }
                }
                else
                {
                    // Add a new file to the end of the list
                    files->resize(files->size() + 1);
                    sprintf_s((*files->rbegin()).first.data(), MAX_PATH, "%s\\%s", (*iterDirectories).data(), ffd.cFileName);
                    (*files->rbegin()).second = /*(ffd.nFileSizeHigh * (MAXDWORD + 1)) +*/ ffd.nFileSizeLow;
                }
            } while (FindNextFile(hFind, &ffd) != 0);

            FindClose(hFind);

            // Advance to next directory
            ++iterDirectories;
        }
    }

    void OpenAndReadFiles(const std::vector, DWORD>>& files)
    {
        const unsigned long bytesToRead = 20971520;
        std::vector readBuffer;
        unsigned long numRead;
        unsigned long totalRead;
        BOOL result = TRUE;
        HANDLE readFile;

        // Read 20MB at a time
        readBuffer.resize(bytesToRead);

        for (const std::pair, DWORD>& fileInfo : files)
        {
            numRead = 0;
            totalRead = 0;
            result = TRUE;

            readFile = CreateFile(
                fileInfo.first.data(),      // lpFileName
                (GENERIC_READ),             // dwDesiredAccess
                FILE_SHARE_READ,            // dwShareMode
                NULL,                       // lpSecurityAttributes
                OPEN_ALWAYS,                // dwCreationDisposition, NOTE: RouteToFile test fails if we use OPEN_EXISTING
                FILE_ATTRIBUTE_NORMAL,      // dwFlagsAndAttributes
                NULL);                      // hTemplateFile

            // Read the full file in chunks to avoid filling the memory with the files
            do {
                result = ReadFile(readFile, readBuffer.data(), (fileInfo.second - totalRead > bytesToRead) ? bytesToRead : fileInfo.second - totalRead, &numRead, NULL);
                totalRead += numRead;
            } while (result && numRead != 0);

            CloseHandle(readFile);

            SHOULD_EQUAL(totalRead, fileInfo.second);
        }
    }

    void FindFileShouldSucceed(const std::string& path)
    {
        WIN32_FIND_DATA ffd;
        HANDLE hFind = FindFirstFile(path.c_str(), &ffd);
        SHOULD_NOT_EQUAL(hFind, INVALID_HANDLE_VALUE);
        FindClose(hFind);
    }

    void FindFileExShouldSucceed(const std::string& path, FINDEX_INFO_LEVELS infoLevelId, FINDEX_SEARCH_OPS searchOp)
    {
        WIN32_FIND_DATA ffd;
        HANDLE hFind = FindFirstFileEx(path.c_str(), infoLevelId, &ffd, searchOp, NULL, 0);
        SHOULD_NOT_EQUAL(hFind, INVALID_HANDLE_VALUE);
        FindClose(hFind);
    }

    void FindFileErrorsMatch(const std::string& nonExistentVirtualPath, const std::string& nonExistentPhysicalPath)
    {
        WIN32_FIND_DATA ffd;
        HANDLE hFind = FindFirstFile(nonExistentVirtualPath.c_str(), &ffd);
        SHOULD_EQUAL(hFind, INVALID_HANDLE_VALUE);
        unsigned long lastVirtualError = GetLastError();

        hFind = FindFirstFile(nonExistentPhysicalPath.c_str(), &ffd);
        SHOULD_EQUAL(hFind, INVALID_HANDLE_VALUE);
        unsigned long lastPhysicalError = GetLastError();
        SHOULD_EQUAL(lastVirtualError, lastPhysicalError);
    }

    void FindFileExErrorsMatch(const std::string& nonExistentVirtualPath, const std::string& nonExistentPhysicalPath, FINDEX_INFO_LEVELS infoLevelId, FINDEX_SEARCH_OPS searchOp)
    {
        WIN32_FIND_DATA ffd;
        HANDLE hFind = FindFirstFileEx(nonExistentVirtualPath.c_str(), infoLevelId, &ffd, searchOp, NULL, 0);
        SHOULD_EQUAL(hFind, INVALID_HANDLE_VALUE);
        unsigned long lastVirtualError = GetLastError();

        hFind = FindFirstFileEx(nonExistentPhysicalPath.c_str(), infoLevelId, &ffd, searchOp, NULL, 0);
        SHOULD_EQUAL(hFind, INVALID_HANDLE_VALUE);
        unsigned long lastPhysicalError = GetLastError();
        SHOULD_EQUAL(lastVirtualError, lastPhysicalError);
    }
}

================================================
FILE: GVFS/GVFS.NativeTests/source/TrailingSlashTests.cpp
================================================
#include "stdafx.h"
#include "TrailingSlashTests.h"
#include "SafeHandle.h"
#include "TestException.h"
#include "TestHelpers.h"
#include "Should.h"

using namespace TestHelpers;

namespace
{
    const std::string TEST_ROOT_FOLDER("\\TrailingSlashTests");
    void VerifyPathEnumerationMatches(const std::string& path1, const std::vector& expectedContents);
}

bool EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete(const char* virtualRootPath)
{
    try
    {
        std::string testFolder = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete");

        // Folder contains "a.txt", "b.txt", and "c.txt"
        std::vector expectedResults = { L".", L"..", L"a.txt", L"b.txt", L"c.txt" };
        VerifyPathEnumerationMatches(testFolder, expectedResults);
        VerifyPathEnumerationMatches(testFolder + "\\", expectedResults);

        // Delete a file
        DWORD error = DelFile(testFolder + "\\b.txt");
        SHOULD_EQUAL((DWORD)ERROR_SUCCESS, error);

        expectedResults = { L".", L"..", L"a.txt", L"c.txt" };
        VerifyPathEnumerationMatches(testFolder, expectedResults);
        VerifyPathEnumerationMatches(testFolder + "\\", expectedResults);
    }
    catch (TestException&)
    {
        return false;
    }

    return true;
}

namespace
{
void VerifyPathEnumerationMatches(const std::string& path1, const std::vector& expectedContents)
{
    SafeHandle folderHandle(CreateFile(
        path1.c_str(),                           // lpFileName
        (GENERIC_READ),                          // dwDesiredAccess
        FILE_SHARE_READ,                         // dwShareMode
        NULL,                                    // lpSecurityAttributes
        OPEN_EXISTING,                           // dwCreationDisposition
        FILE_FLAG_BACKUP_SEMANTICS,              // dwFlagsAndAttributes
        NULL));                                  // hTemplateFile
    
    VerifyEnumerationMatches(folderHandle.GetHandle(), expectedContents);
}

}

================================================
FILE: GVFS/GVFS.NativeTests/source/dllmain.cpp
================================================
// dllmain.cpp : Defines the entry point for the DLL application.
#include "stdafx.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    UNREFERENCED_PARAMETER(hModule);
    UNREFERENCED_PARAMETER(lpReserved);

    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}



================================================
FILE: GVFS/GVFS.NativeTests/source/stdafx.cpp
================================================
// stdafx.cpp : source file that includes just the standard includes
// GVFS.NativeTests.pch will be the pre-compiled header
// stdafx.obj will contain the pre-compiled type information

#include "stdafx.h"

// Add any additional headers you need in STDAFX.H and not in this file


================================================
FILE: GVFS/GVFS.Payload/GVFS.Payload.csproj
================================================


  
    net471
    false
  

  
    
    
  

  
    
    
    
  

  
    
    
    
    
    
    
    
    
  

  
    
  

  
    
  

  
    
      Microsoft400
      false
    
  




================================================
FILE: GVFS/GVFS.Payload/layout.bat
================================================
@ECHO OFF
SETLOCAL

IF "%~1" == "" (
    ECHO error: missing configuration
    ECHO.
    GOTO USAGE
)

IF "%~2" == "" (
    ECHO error: missing version
    ECHO.
    GOTO USAGE
)

IF "%~3" == "" (
    ECHO error: missing ProjFS path
    ECHO.
    GOTO USAGE
)

IF "%~4" == "" (
    ECHO error: missing VCRuntime path
    ECHO.
    GOTO USAGE
)

IF "%~5" == "" (
    ECHO error: missing output path
    ECHO.
    GOTO USAGE
)

SET CONFIGURATION=%1
SET GVFSVERSION=%2
SET PROJFS=%3
SET VCRUNTIME=%4
SET OUTPUT=%5

SET ROOT=%~dp0..\..
SET BUILD_OUT="%ROOT%\..\out"
SET MANAGED_OUT_FRAGMENT=bin\%CONFIGURATION%\net471\win-x64
SET NATIVE_OUT_FRAGMENT=bin\x64\%CONFIGURATION%

ECHO Copying files...
xcopy /Y %PROJFS%\filter\PrjFlt.sys %OUTPUT%\Filter\
xcopy /Y %PROJFS%\filter\prjflt.inf %OUTPUT%\Filter\
xcopy /Y %PROJFS%\lib\ProjectedFSLib.dll %OUTPUT%\ProjFS\
xcopy /Y %VCRUNTIME%\lib\x64\msvcp140.dll %OUTPUT%
xcopy /Y %VCRUNTIME%\lib\x64\msvcp140_1.dll %OUTPUT%
xcopy /Y %VCRUNTIME%\lib\x64\msvcp140_2.dll %OUTPUT%
xcopy /Y %VCRUNTIME%\lib\x64\vcruntime140.dll %OUTPUT%
xcopy /Y /S %BUILD_OUT%\GVFS\%MANAGED_OUT_FRAGMENT%\* %OUTPUT%
xcopy /Y /S %BUILD_OUT%\GVFS.Hooks\%MANAGED_OUT_FRAGMENT%\* %OUTPUT%
xcopy /Y /S %BUILD_OUT%\GVFS.Mount\%MANAGED_OUT_FRAGMENT%\* %OUTPUT%
xcopy /Y /S %BUILD_OUT%\GVFS.Service\%MANAGED_OUT_FRAGMENT%\* %OUTPUT%
xcopy /Y /S %BUILD_OUT%\GitHooksLoader\%NATIVE_OUT_FRAGMENT%\* %OUTPUT%
xcopy /Y /S %BUILD_OUT%\GVFS.PostIndexChangedHook\%NATIVE_OUT_FRAGMENT%\* %OUTPUT%
xcopy /Y /S %BUILD_OUT%\GVFS.ReadObjectHook\%NATIVE_OUT_FRAGMENT%\* %OUTPUT%
xcopy /Y /S %BUILD_OUT%\GVFS.VirtualFileSystemHook\%NATIVE_OUT_FRAGMENT%\* %OUTPUT%

ECHO Cleaning up...
REM Remove unused LibGit2 files
RMDIR /S /Q %OUTPUT%\lib
REM Remove files for x86 (not supported)
RMDIR /S /Q %OUTPUT%\x86

GOTO EOF

:USAGE
ECHO usage: %~n0%~x0 ^ ^ ^ ^ ^
ECHO.
ECHO   configuration   Build configuration (Debug, Release).
ECHO   version         GVFS version string.
ECHO   projfs          Path to GVFS.ProjFS NuGet package contents.
ECHO   vcruntime       Path to GVFS.VCRuntime NuGet package contents.
ECHO   output          Output directory.
ECHO.
EXIT 1

:ERROR
ECHO Failed with error %ERRORLEVEL%
EXIT /B %ERRORLEVEL%

:EOF


================================================
FILE: GVFS/GVFS.PerfProfiling/GVFS.PerfProfiling.csproj
================================================


  
    Exe
    net471
  

  
    
    
    
  




================================================
FILE: GVFS/GVFS.PerfProfiling/ProfilingEnvironment.cs
================================================
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Http;
using GVFS.Common.Tracing;
using GVFS.Virtualization;

namespace GVFS.PerfProfiling
{
    internal class ProfilingEnvironment
    {
        private GVFSDatabase gvfsDatabase;

        public ProfilingEnvironment(string enlistmentRootPath)
        {
            this.Enlistment = this.CreateEnlistment(enlistmentRootPath);
            this.Context = this.CreateContext();
            this.FileSystemCallbacks = this.CreateFileSystemCallbacks();
        }

        public GVFSEnlistment Enlistment { get; private set; }
        public GVFSContext Context { get; private set; }
        public FileSystemCallbacks FileSystemCallbacks { get; private set; }

        private GVFSEnlistment CreateEnlistment(string enlistmentRootPath)
        {
            string gitBinPath = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath();
            return GVFSEnlistment.CreateFromDirectory(enlistmentRootPath, gitBinPath, authentication: null);
        }

        private GVFSContext CreateContext()
        {
            ITracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "GVFS.PerfProfiling", disableTelemetry: true);

            PhysicalFileSystem fileSystem = new PhysicalFileSystem();
            GitRepo gitRepo = new GitRepo(
                tracer, 
                this.Enlistment, 
                fileSystem);
            return new GVFSContext(tracer, fileSystem, gitRepo, this.Enlistment);
        }

        private FileSystemCallbacks CreateFileSystemCallbacks()
        {
            string error;
            if (!RepoMetadata.TryInitialize(this.Context.Tracer, this.Enlistment.DotGVFSRoot, out error))
            {
                throw new InvalidRepoException(error);
            }

            string gitObjectsRoot;
            if (!RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error))
            {
                throw new InvalidRepoException("Failed to determine git objects root from repo metadata: " + error);
            }

            string localCacheRoot;
            if (!RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error))
            {
                throw new InvalidRepoException("Failed to determine local cache path from repo metadata: " + error);
            }

            string blobSizesRoot;
            if (!RepoMetadata.Instance.TryGetBlobSizesRoot(out blobSizesRoot, out error))
            {
                throw new InvalidRepoException("Failed to determine blob sizes root from repo metadata: " + error);
            }

            this.Enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot);

            CacheServerInfo cacheServer = new CacheServerInfo(this.Context.Enlistment.RepoUrl, "None");
            GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(
                this.Context.Tracer, 
                this.Context.Enlistment,
                cacheServer,
                new RetryConfig());

            this.gvfsDatabase = new GVFSDatabase(this.Context.FileSystem, this.Context.Enlistment.EnlistmentRoot, new SqliteDatabase());
            GVFSGitObjects gitObjects = new GVFSGitObjects(this.Context, objectRequestor);
            return new FileSystemCallbacks(
                this.Context,
                gitObjects,
                RepoMetadata.Instance,
                blobSizes: null,
                gitIndexProjection: null,
                backgroundFileSystemTaskRunner: null,
                fileSystemVirtualizer: null,
                placeholderDatabase: new PlaceholderTable(this.gvfsDatabase),
                sparseCollection: new SparseTable(this.gvfsDatabase),
                gitStatusCache : null);
        }
    }
}


================================================
FILE: GVFS/GVFS.PerfProfiling/Program.cs
================================================
using GVFS.Common;
using GVFS.PlatformLoader;
using GVFS.Virtualization.Projection;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;

namespace GVFS.PerfProfiling
{
    internal class Program
    {
        [Flags]
        private enum TestsToRun
        {
            ValidateIndex = 1 << 0,
            RebuildProjection = 1 << 1,
            ValidateModifiedPaths = 1 << 2,
            All = -1,
        }

        private static void Main(string[] args)
        {
            GVFSPlatformLoader.Initialize();
            string enlistmentRootPath = @"M:\OS";
            if (args.Length > 0 && !string.IsNullOrWhiteSpace(args[0]))
            {
                enlistmentRootPath = args[0];
            }

            TestsToRun testsToRun = TestsToRun.All;
            if (args.Length > 1)
            {
                int tests;
                if (int.TryParse(args[1], out tests))
                {
                    testsToRun = (TestsToRun)tests;
                }
            }

            ProfilingEnvironment environment = new ProfilingEnvironment(enlistmentRootPath);

            Dictionary allTests = new Dictionary
            {
                { TestsToRun.ValidateIndex, () => GitIndexProjection.ReadIndex(environment.Context.Tracer, Path.Combine(environment.Enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Index)) },
                { TestsToRun.RebuildProjection, () => environment.FileSystemCallbacks.GitIndexProjectionProfiler.ForceRebuildProjection() },
                { TestsToRun.ValidateModifiedPaths, () => environment.FileSystemCallbacks.GitIndexProjectionProfiler.ForceAddMissingModifiedPaths(environment.Context.Tracer) },
            };

            long before = GetMemoryUsage();

            foreach (KeyValuePair test in allTests)
            {
                if (IsOn(testsToRun, test.Key))
                {
                    TimeIt(test.Key.ToString(), test.Value);
                }
            }

            long after = GetMemoryUsage();

            Console.WriteLine($"Memory Usage: {FormatByteCount(after - before)}");
            Console.WriteLine();
            Console.WriteLine("Press Enter to exit");
            Console.Read();
        }

        private static bool IsOn(TestsToRun value, TestsToRun flag)
        {
            return flag == (value & flag);
        }

        private static void TimeIt(string name, Action action)
        {
            List times = new List();
            const int runs = 10;

            Console.WriteLine();
            Console.WriteLine($"Measuring {name}:");
            Console.WriteLine();

            for (int i = 0; i < runs + 1; i++)
            {
                long before = GetMemoryUsage();

                Stopwatch stopwatch = Stopwatch.StartNew();
                action();
                stopwatch.Stop();

                long after = GetMemoryUsage();

                times.Add(stopwatch.Elapsed);
                Console.WriteLine($"Time: {stopwatch.Elapsed.TotalMilliseconds} ms");
                Console.WriteLine($"New allocations: {FormatByteCount(after - before)}");
            }

            Console.WriteLine();
            Console.WriteLine($"Average Time {runs} runs - {name} {times.Select(timespan => timespan.TotalMilliseconds).Skip(1).Average()} ms");
            Console.WriteLine("----------------------------");
        }

        private static long GetMemoryUsage()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect(generation: 2, mode: GCCollectionMode.Forced, blocking: true, compacting: true);
            using (Process process = Process.GetCurrentProcess())
            {
                return process.PrivateMemorySize64;
            }
        }

        private static string FormatByteCount(long byteCount)
        {
            const int Divisor = 1024;
            string[] unitStrings = { " B", " KB", " MB", " GB", " TB" };

            int unitIndex = 0;

            bool isNegative = false;
            if (byteCount < 0)
            {
                isNegative = true;
                byteCount *= -1;
            }

            while (byteCount >= Divisor && unitIndex < unitStrings.Length - 1)
            {
                unitIndex++;
                byteCount = byteCount / Divisor;
            }

            return (isNegative ? "-" : string.Empty) + byteCount.ToString("N0") + unitStrings[unitIndex];
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/ActiveEnumeration.cs
================================================
using GVFS.Common;
using GVFS.Virtualization.Projection;
using Microsoft.Windows.ProjFS;
using System.Collections.Generic;

namespace GVFS.Platform.Windows
{
    public class ActiveEnumeration
    {
        private static FileNamePatternMatcher doesWildcardPatternMatch = null;

        // Use our own enumerator to avoid having to dispose anything
        private ProjectedFileInfoEnumerator fileInfoEnumerator;
        private FileNamePatternMatcher doesPatternMatch;

        private string filterString = null;

        public ActiveEnumeration(List fileInfos)
        {
            this.fileInfoEnumerator = new ProjectedFileInfoEnumerator(fileInfos);
            this.ResetEnumerator();
            this.MoveNext();
        }

        public delegate bool FileNamePatternMatcher(string name, string pattern);

        /// 
        /// true if Current refers to an element in the enumeration, false if Current is past the end of the collection
        /// 
        public bool IsCurrentValid { get; private set; }

        /// 
        /// Gets the element in the collection at the current position of the enumerator
        /// 
        public ProjectedFileInfo Current
        {
            get { return this.fileInfoEnumerator.Current; }
        }

        /// 
        /// Sets the pattern matching delegate that will be used for file name comparisons when the filter
        /// contains wildcards.
        /// 
        /// FileNamePatternMatcher to be used by ActiveEnumeration
        public static void SetWildcardPatternMatcher(FileNamePatternMatcher patternMatcher)
        {
            doesWildcardPatternMatch = patternMatcher;
        }

        /// 
        /// Resets the enumerator and advances it to the first ProjectedFileInfo in the enumeration
        /// 
        /// Filter string to save.  Can be null.
        public void RestartEnumeration(string filter)
        {
            this.ResetEnumerator();
            this.IsCurrentValid = this.fileInfoEnumerator.MoveNext();
            this.SaveFilter(filter);
        }

        /// 
        /// Advances the enumerator to the next element of the collection (that is being projected).
        /// If a filter string is set, MoveNext will advance to the next entry that matches the filter.
        /// 
        /// 
        /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection
        /// 
        public bool MoveNext()
        {
            this.IsCurrentValid = this.fileInfoEnumerator.MoveNext();
            while (this.IsCurrentValid && this.IsCurrentHidden())
            {
                this.IsCurrentValid = this.fileInfoEnumerator.MoveNext();
            }

            return this.IsCurrentValid;
        }

        /// 
        /// Attempts to save the filter string for this enumeration.  When setting a filter string, if Current is valid
        /// and does not match the specified filter, the enumerator will be advanced until an element is found that
        /// matches the filter (or the end of the collection is reached).
        /// 
        /// Filter string to save.  Can be null.
        ///  True if the filter string was saved.  False if the filter string was not saved (because a filter string
        /// was previously saved).
        /// 
        /// 
        /// Per MSDN (https://msdn.microsoft.com/en-us/library/windows/hardware/ff567047(v=vs.85).aspx, the filter string
        /// specified in the first call to ZwQueryDirectoryFile will be used for all subsequent calls for the handle (and
        /// the string specified in subsequent calls should be ignored)
        /// 
        public bool TrySaveFilterString(string filter)
        {
            if (this.filterString == null)
            {
                this.SaveFilter(filter);
                return true;
            }

            return false;
        }

        /// 
        /// Returns the current filter string or null if no filter string has been saved
        /// 
        /// The current filter string or null if no filter string has been saved
        public string GetFilterString()
        {
            return this.filterString;
        }

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

        private void SaveFilter(string filter)
        {
            if (string.IsNullOrEmpty(filter))
            {
                this.filterString = string.Empty;
                this.doesPatternMatch = null;
            }
            else
            {
                this.filterString = filter;

                if (Utils.DoesNameContainWildCards(this.filterString))
                {
                    this.doesPatternMatch = doesWildcardPatternMatch;
                }
                else
                {
                    this.doesPatternMatch = NameMatchesNoWildcardFilter;
                }

                if (this.IsCurrentValid && this.IsCurrentHidden())
                {
                    this.MoveNext();
                }
            }
        }

        private bool IsCurrentHidden()
        {
            if (this.doesPatternMatch == null)
            {
                return false;
            }

            return !this.doesPatternMatch(this.Current.Name, this.GetFilterString());
        }

        private void ResetEnumerator()
        {
            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;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/CurrentUser.cs
================================================
using GVFS.Common.Tracing;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;

namespace GVFS.Platform.Windows
{
    public class CurrentUser : IDisposable
    {
        private const int TokenPrimary = 1;

        private const uint DuplicateTokenFlags = (uint)(TokenRights.Query | TokenRights.AssignPrimary | TokenRights.Duplicate | TokenRights.Default | TokenRights.SessionId);

        private const int StartInfoUseStdHandles = 0x00000100;
        private const uint HandleFlagInherit = 1;

        private readonly ITracer tracer;
        private readonly IntPtr token;

        public CurrentUser(ITracer tracer, int sessionId)
        {
            this.tracer = tracer;
            this.token = GetCurrentUserToken(this.tracer, sessionId);
            if (this.token != IntPtr.Zero)
            {
                this.Identity = new WindowsIdentity(this.token);
            }
            else
            {
                this.Identity = null;
            }
        }

        private enum TokenRights : uint
        {
            StandardRightsRequired = 0x000F0000,
            StandardRightsRead = 0x00020000,
            AssignPrimary = 0x0001,
            Duplicate = 0x0002,
            TokenImpersonate = 0x0004,
            Query = 0x0008,
            QuerySource = 0x0010,
            AdjustPrivileges = 0x0020,
            AdjustGroups = 0x0040,
            Default = 0x0080,
            SessionId = 0x0100,
            Read = (StandardRightsRead | Query),
            AllAccess = (StandardRightsRequired | AssignPrimary |
                Duplicate | TokenImpersonate | Query | QuerySource |
                AdjustPrivileges | AdjustGroups | Default |
                SessionId),
        }

        private enum SECURITY_IMPERSONATION_LEVEL
        {
            SecurityAnonymous,
            SecurityIdentification,
            SecurityImpersonation,
            SecurityDelegation
        }

        private enum WaitForObjectResults : uint
        {
            WaitSuccess = 0,
            WaitAbandoned = 0x80,
            WaitTimeout = 0x102,
            WaitFailed = 0xFFFFFFFF
        }

        private enum ConnectionState
        {
            Active,
            Connected,
            ConnectQuery,
            Shadowing,
            Disconnected,
            Idle,
            Listening,
            Reset,
            Down,
            Initializing
        }

        [Flags]
        private enum CreateProcessFlags : uint
        {
            CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
            CREATE_DEFAULT_ERROR_MODE = 0x04000000,
            CREATE_NEW_CONSOLE = 0x00000010,
            CREATE_NEW_PROCESS_GROUP = 0x00000200,
            CREATE_NO_WINDOW = 0x08000000,
            CREATE_PROTECTED_PROCESS = 0x00040000,
            CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
            CREATE_SEPARATE_WOW_VDM = 0x00000800,
            CREATE_SHARED_WOW_VDM = 0x00001000,
            CREATE_SUSPENDED = 0x00000004,
            CREATE_UNICODE_ENVIRONMENT = 0x00000400,
            DEBUG_ONLY_THIS_PROCESS = 0x00000002,
            DEBUG_PROCESS = 0x00000001,
            DETACHED_PROCESS = 0x00000008,
            EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
            INHERIT_PARENT_AFFINITY = 0x00010000
        }

        public WindowsIdentity Identity { get; }

        /// 
        /// Launches a process for the current user.
        /// This code will only work when running in a windows service running as LocalSystem
        /// with the SE_TCB_NAME privilege.
        /// 
        /// True on successful process start
        public bool RunAs(string processName, string args)
        {
            IntPtr environment = IntPtr.Zero;
            IntPtr duplicate = IntPtr.Zero;
            if (this.token == IntPtr.Zero)
            {
                return false;
            }

            try
            {
                if (DuplicateTokenEx(
                    this.token,
                    DuplicateTokenFlags,
                    IntPtr.Zero,
                    SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
                    TokenPrimary,
                    out duplicate))
                {
                    if (CreateEnvironmentBlock(ref environment, duplicate, false))
                    {
                        STARTUP_INFO info = new STARTUP_INFO();
                        info.Length = Marshal.SizeOf(typeof(STARTUP_INFO));

                        PROCESS_INFORMATION procInfo = new PROCESS_INFORMATION();
                        if (CreateProcessAsUser(
                            duplicate,
                            null,
                            string.Format("\"{0}\" {1}", processName, args),
                            IntPtr.Zero,
                            IntPtr.Zero,
                            inheritHandles: false,
                            creationFlags: CreateProcessFlags.CREATE_NO_WINDOW | CreateProcessFlags.CREATE_UNICODE_ENVIRONMENT,
                            environment: environment,
                            currentDirectory: null,
                            startupInfo: ref info,
                            processInformation: out procInfo))
                        {
                            try
                            {
                                this.tracer.RelatedInfo("Started process '{0} {1}' with Id {2}", processName, args, procInfo.ProcessId);
                            }
                            finally
                            {
                                CloseHandle(procInfo.ProcessHandle);
                                CloseHandle(procInfo.ThreadHandle);
                            }

                            return true;
                        }
                        else
                        {
                            TraceWin32Error(this.tracer, "Unable to start process.");
                        }
                    }
                    else
                    {
                        TraceWin32Error(this.tracer, "Unable to set child process environment block.");
                    }
                }
                else
                {
                    TraceWin32Error(this.tracer, "Unable to duplicate user token.");
                }
            }
            finally
            {
                if (environment != IntPtr.Zero)
                {
                    DestroyEnvironmentBlock(environment);
                }

                if (duplicate != IntPtr.Zero)
                {
                    CloseHandle(duplicate);
                }
            }

            return false;
        }

        public void Dispose()
        {
            if (this.token != IntPtr.Zero)
            {
                CloseHandle(this.token);
            }
        }

        private static void TraceWin32Error(ITracer tracer, string preface)
        {
            Win32Exception e = new Win32Exception(Marshal.GetLastWin32Error());
            tracer.RelatedError(preface + " Exception: " + e.Message);
        }

        private static IntPtr GetCurrentUserToken(ITracer tracer, int sessionId)
        {
            IntPtr output = IntPtr.Zero;
            if (WTSQueryUserToken((uint)sessionId, out output))
            {
                return output;
            }
            else
            {
                TraceWin32Error(tracer, string.Format("Unable to query user token from session '{0}'.", sessionId));
            }

            return IntPtr.Zero;
        }

        private static List ListSessions(ITracer tracer)
        {
            IntPtr sessionInfo = IntPtr.Zero;
            IntPtr server = IntPtr.Zero;
            List output = new List();

            try
            {
                int count = 0;
                int retval = WTSEnumerateSessions(IntPtr.Zero, 0, 1, ref sessionInfo, ref count);
                if (retval != 0)
                {
                    int dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
                    long current = sessionInfo.ToInt64();

                    for (int i = 0; i < count; i++)
                    {
                        WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO));
                        current += dataSize;

                        output.Add(si);
                    }
                }
                else
                {
                    TraceWin32Error(tracer, "Unable to enumerate sessions on the current host.");
                }
            }
            catch (Exception exception)
            {
                output.Clear();
                tracer.RelatedError(exception.ToString());
            }
            finally
            {
                if (sessionInfo != IntPtr.Zero)
                {
                    WTSFreeMemory(sessionInfo);
                }
            }

            return output;
        }

        [DllImport("kernel32.dll")]
        private static extern bool CloseHandle(IntPtr handle);

        [DllImport("kernel32.dll")]
        private static extern WaitForObjectResults WaitForSingleObject(IntPtr handle, uint timeout = uint.MaxValue);

        [DllImport("wtsapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int WTSEnumerateSessions(IntPtr server, int reserved, int version, ref IntPtr sessionInfo, ref int count);

        [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUserW", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern bool CreateProcessAsUser(
            IntPtr token,
            string applicationName,
            string commandLine,
            IntPtr processAttributes,
            IntPtr threadAttributes,
            bool inheritHandles,
            CreateProcessFlags creationFlags,
            IntPtr environment,
            string currentDirectory,
            ref STARTUP_INFO startupInfo,
            out PROCESS_INFORMATION processInformation);

        [DllImport("wtsapi32.dll")]
        private static extern void WTSFreeMemory(IntPtr memory);

        [DllImport("wtsapi32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool WTSQueryUserToken(uint sessionId, out IntPtr token);

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool DuplicateTokenEx(
            IntPtr existingToken,
            uint desiredAccess,
            IntPtr tokenAttributes,
            SECURITY_IMPERSONATION_LEVEL impersonationLevel,
            int tokenType,
            out IntPtr newToken);

        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool CreateEnvironmentBlock(ref IntPtr environment, IntPtr token, bool inherit);

        [DllImport("userenv.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool DestroyEnvironmentBlock(IntPtr environment);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        private struct STARTUP_INFO
        {
            public int Length;
            public string Reserved;
            public string DesktopName;
            public string Title;
            public int WindowX;
            public int WindowY;
            public int WindowWidth;
            public int WindowHeight;
            public int ConsoleBufferWidth;
            public int ConsoleBufferHeight;
            public int ConsoleColors;
            public int Flags;
            public short ShowWindow;
            public short Reserved2;
            public IntPtr Reserved3;
            public IntPtr StdInput;
            public IntPtr StdOutput;
            public IntPtr StdError;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct SECURITY_ATTRIBUTES
        {
            public int Length;
            public IntPtr SecurityDescriptor;
            public bool InheritHandle;
        }

        [StructLayoutAttribute(LayoutKind.Sequential)]
        private struct SECURITY_DESCRIPTOR
        {
            public byte Revision;
            public byte Size;
            public short Control;
            public IntPtr Owner;
            public IntPtr Group;
            public IntPtr Sacl;
            public IntPtr Dacl;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct PROCESS_INFORMATION
        {
            public IntPtr ProcessHandle;
            public IntPtr ThreadHandle;
            public int ProcessId;
            public int ThreadId;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct WTS_SESSION_INFO
        {
            public int SessionID;

            [MarshalAs(UnmanagedType.LPTStr)]
            public string WinStationName;
            public ConnectionState State;
        }
    }
}

================================================
FILE: GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout14to15Upgrade_ModifiedPaths.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Tracing;
using GVFS.DiskLayoutUpgrades;
using System;
using System.Collections.Generic;
using System.IO;

namespace GVFS.Platform.Windows.DiskLayoutUpgrades
{
    public class DiskLayout14to15Upgrade_ModifiedPaths : DiskLayoutUpgrade.MajorUpgrade
    {
        protected override int SourceMajorVersion => 14;

        public override bool TryUpgrade(ITracer tracer, string enlistmentRoot)
        {
            ModifiedPathsDatabase modifiedPaths = null;
            try
            {
                PhysicalFileSystem fileSystem = new PhysicalFileSystem();

                string modifiedPathsDatabasePath = Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.ModifiedPaths);
                string error;
                if (!ModifiedPathsDatabase.TryLoadOrCreate(tracer, modifiedPathsDatabasePath, fileSystem, out modifiedPaths, out error))
                {
                    tracer.RelatedError($"Unable to create the modified paths database. {error}");
                    return false;
                }

                string sparseCheckoutPath = Path.Combine(enlistmentRoot, GVFSConstants.WorkingDirectoryRootName, GVFSConstants.DotGit.Info.SparseCheckoutPath);
                bool isRetryable;
                using (FileStream fs = File.OpenRead(sparseCheckoutPath))
                using (StreamReader reader = new StreamReader(fs))
                {
                    string entry = reader.ReadLine();
                    while (entry != null)
                    {
                        entry = entry.Trim();
                        if (!string.IsNullOrWhiteSpace(entry))
                        {
                            bool isFolder = entry.EndsWith(GVFSConstants.GitPathSeparatorString);
                            if (!modifiedPaths.TryAdd(entry.Trim(GVFSConstants.GitPathSeparator), isFolder, out isRetryable))
                            {
                                tracer.RelatedError("Unable to add to the modified paths database.");
                                return false;
                            }
                        }

                        entry = reader.ReadLine();
                    }
                }

                string alwaysExcludePath = Path.Combine(enlistmentRoot, GVFSConstants.WorkingDirectoryRootName, GVFSConstants.DotGit.Info.AlwaysExcludePath);
                if (fileSystem.FileExists(alwaysExcludePath))
                {
                    string alwaysExcludeData = fileSystem.ReadAllText(alwaysExcludePath);

                    char[] carriageReturnOrLineFeed = new[] { '\r', '\n' };
                    int endPosition = alwaysExcludeData.Length;
                    while (endPosition > 0)
                    {
                        int startPosition = alwaysExcludeData.LastIndexOfAny(carriageReturnOrLineFeed, endPosition - 1);
                        if (startPosition < 0)
                        {
                            startPosition = 0;
                        }

                        string entry = alwaysExcludeData.Substring(startPosition, endPosition - startPosition).Trim();

                        if (entry.EndsWith("*"))
                        {
                            // This is the first entry using the old format and we don't want to process old entries
                            // because we would need folder entries since there isn't a file and that would cause sparse-checkout to
                            // recursively clear skip-worktree bits for everything under that folder
                            break;
                        }

                        // Substring will not return a null and the Trim will get rid of all the whitespace
                        // if there is a length it will be a valid path that we need to process
                        if (entry.Length > 0)
                        {
                            entry = entry.TrimStart('!');
                            bool isFolder = entry.EndsWith(GVFSConstants.GitPathSeparatorString);
                            if (!isFolder)
                            {
                                if (!modifiedPaths.TryAdd(entry.Trim(GVFSConstants.GitPathSeparator), isFolder, out isRetryable))
                                {
                                    tracer.RelatedError("Unable to add to the modified paths database.");
                                    return false;
                                }
                            }
                        }

                        endPosition = startPosition;
                    }
                }

                modifiedPaths.ForceFlush();
                fileSystem.WriteAllText(sparseCheckoutPath, "/.gitattributes" + Environment.NewLine);
                fileSystem.DeleteFile(alwaysExcludePath);
            }
            catch (IOException ex)
            {
                tracer.RelatedError($"IOException: {ex.ToString()}");
                return false;
            }
            finally
            {
                if (modifiedPaths != null)
                {
                    modifiedPaths.Dispose();
                    modifiedPaths = null;
                }
            }

            if (!this.TryIncrementMajorVersion(tracer, enlistmentRoot))
            {
                return false;
            }

            return true;
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout15to16Upgrade_GitStatusCache.cs
================================================
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;
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout16to17Upgrade_FolderPlaceholderValues.cs
================================================
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Common.FileSystem;
using GVFS.Common.Tracing;
using GVFS.DiskLayoutUpgrades;
using System;
using System.Collections.Generic;
using System.IO;

namespace GVFS.Platform.Windows.DiskLayoutUpgrades
{
    /// 
    /// Updates the values for folder placeholders from AllZeroSha to PlaceholderListDatabase.PartialFolderValue
    /// 
    public class DiskLayout16to17Upgrade_FolderPlaceholderValues : DiskLayoutUpgrade.MajorUpgrade
    {
        protected override int SourceMajorVersion => 16;

        public override bool TryUpgrade(ITracer tracer, string enlistmentRoot)
        {
            string dotGVFSRoot = Path.Combine(enlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot);
            try
            {
                string error;
                LegacyPlaceholderListDatabase placeholders;
                if (!LegacyPlaceholderListDatabase.TryCreate(
                    tracer,
                    Path.Combine(dotGVFSRoot, GVFSConstants.DotGVFS.Databases.PlaceholderList),
                    new PhysicalFileSystem(),
                    out placeholders,
                    out error))
                {
                    tracer.RelatedError("Failed to open placeholder database: " + error);
                    return false;
                }

                using (placeholders)
                {
                    List oldPlaceholderEntries = placeholders.GetAllEntries();
                    List newPlaceholderEntries = new List();

                    foreach (IPlaceholderData entry in oldPlaceholderEntries)
                    {
                        if (entry.Sha == GVFSConstants.AllZeroSha)
                        {
                            newPlaceholderEntries.Add(new LegacyPlaceholderListDatabase.PlaceholderData(entry.Path, LegacyPlaceholderListDatabase.PartialFolderValue));
                        }
                        else
                        {
                            newPlaceholderEntries.Add(entry);
                        }
                    }

                    placeholders.WriteAllEntriesAndFlush(newPlaceholderEntries);
                }
            }
            catch (IOException ex)
            {
                tracer.RelatedError("Could not write to placeholder database: " + ex.ToString());
                return false;
            }
            catch (Exception ex)
            {
                tracer.RelatedError("Error updating placeholder database folder entries: " + ex.ToString());
                return false;
            }

            if (!this.TryIncrementMajorVersion(tracer, enlistmentRoot))
            {
                return false;
            }

            return true;
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout17to18Upgrade_TombstoneFolderPlaceholders.cs
================================================
using GVFS.Common.Tracing;
using GVFS.DiskLayoutUpgrades;

namespace GVFS.Platform.Windows.DiskLayoutUpgrades
{
    public class DiskLayout17to18Upgrade_TombstoneFolderPlaceholders : DiskLayoutUpgrade.MajorUpgrade
    {
        protected override int SourceMajorVersion => 17;

        public override bool TryUpgrade(ITracer tracer, string enlistmentRoot)
        {
            // Don't need to upgrade since the tombstone folders are only needed when a git command deletes folders
            // And the git command would have needed to be cancelled or crashed to leave tombstones that would need
            // to be tracked and persisted to the placeholder database.
            if (!this.TryIncrementMajorVersion(tracer, enlistmentRoot))
            {
                return false;
            }

            return true;
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/DiskLayout18to19Upgrade_SqlitePlacholders.cs
================================================
using GVFS.Common.DiskLayoutUpgrades;

namespace GVFS.Platform.Windows.DiskLayoutUpgrades
{
    public class DiskLayout18to19Upgrade_SqlitePlacholders : DiskLayoutUpgrade_SqlitePlaceholders
    {
        protected override int SourceMajorVersion => 18;
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/DiskLayoutUpgrades/WindowsDiskLayoutUpgradeData.cs
================================================
using GVFS.Common;
using GVFS.DiskLayoutUpgrades;

namespace GVFS.Platform.Windows.DiskLayoutUpgrades
{
    public class WindowsDiskLayoutUpgradeData : IDiskLayoutUpgradeData
    {
        public DiskLayoutUpgrade[] Upgrades
        {
            get
            {
                return new DiskLayoutUpgrade[]
                {
                    new DiskLayout14to15Upgrade_ModifiedPaths(),
                    new DiskLayout15to16Upgrade_GitStatusCache(),
                    new DiskLayout16to17Upgrade_FolderPlaceholderValues(),
                    new DiskLayout17to18Upgrade_TombstoneFolderPlaceholders(),
                    new DiskLayout18to19Upgrade_SqlitePlacholders(),
                };
            }
        }

        public DiskLayoutVersion Version => new DiskLayoutVersion(
                    currentMajorVersion: 19,
                    currentMinorVersion: 0,
                    minimumSupportedMajorVersion: 14);

        public bool TryParseLegacyDiskLayoutVersion(string dotGVFSPath, out int majorVersion)
        {
            majorVersion = 0;
            return false;
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj
================================================


  
    net471
  

  
    
  

  
    
  

  
    
    
  




================================================
FILE: GVFS/GVFS.Platform.Windows/HResultExtensions.cs
================================================
using Microsoft.Windows.ProjFS;

namespace GVFS.Platform.Windows
{
    public class HResultExtensions
    {
        public const int GenericProjFSError = -2147024579; // returned by ProjFS::DeleteFile() on Win server 2016 while deleting a partial file

        private const int FacilityNtBit = 0x10000000; // FACILITY_NT_BIT
        private const int FacilityWin32 = 7;          // FACILITY_WIN32

        // #define HRESULT_FROM_NT(x)      ((HRESULT) ((x) | FACILITY_NT_BIT))
        public enum HResultFromNtStatus : int
        {
            FileNotAvailable = unchecked((int)0xC0000467) | FacilityNtBit,       // STATUS_FILE_NOT_AVAILABLE
            FileClosed = unchecked((int)0xC0000128) | FacilityNtBit,             // STATUS_FILE_CLOSED
            IoReparseTagNotHandled = unchecked((int)0xC0000279) | FacilityNtBit, // STATUS_IO_REPARSE_TAG_NOT_HANDLED
            DeviceNotReady = unchecked((int)0xC00000A3L) | FacilityNtBit,        // STATUS_DEVICE_NOT_READY
        }

        // HRESULT_FROM_WIN32(unsigned long x) { return (HRESULT)(x) <= 0 ? (HRESULT)(x) : (HRESULT) (((x) & 0x0000FFFF) | (FACILITY_WIN32 << 16) | 0x80000000);}
        public static HResult HResultFromWin32(int win32error)
        {
            return win32error <= 0 ? (HResult)win32error : (HResult)unchecked((win32error & 0x0000FFFF) | (FacilityWin32 << 16) | 0x80000000);
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/PatternMatcher.cs
================================================
using GVFS.Common;
using System;

namespace GVFS.Platform.Windows
{
    // From http://referencesource.microsoft.com/#System/services/io/system/io/PatternMatcher.cs
    //
    // Changes made after copying from above URL:
    //
    // - Changed ANSI_DOS_STAR to '<' and ANSI_DOS_QM to '>' (these are the correct values, see ntifs.h)
    // - Added code so that method is consistently case insensitive
    // - Updated method to return true when expression is null or empty
    // - Remove special casing of *.* wildcard expression

    public static class PatternMatcher
    {
        /// 
        ///     Private constants (directly from C header files)
        /// 
        private const int MATCHES_ARRAY_SIZE = 16;
        private const char ANSI_DOS_STAR = '<';
        private const char ANSI_DOS_QM = '>';
        private const char DOS_DOT = '"';

        /// 
        ///     Tells whether a given name matches the expression given with a strict (i.e. UNIX like)
        ///     semantics.  This code is a port of unmanaged code.  Original code comment follows:
        ///
        ///    Routine Description:
        ///
        ///        This routine compares a Dbcs name and an expression and tells the caller
        ///        if the name is in the language defined by the expression.  The input name
        ///        cannot contain wildcards, while the expression may contain wildcards.
        ///
        ///        Expression wild cards are evaluated as shown in the nondeterministic
        ///        finite automatons below.  Note that ~* and ~? are DOS_STAR and DOS_QM.
        ///
        ///
        ///                 ~* is DOS_STAR, ~? is DOS_QM, and ~. is DOS_DOT
        ///
        ///
        ///                                           S
        ///                                        <-----<
        ///                                     X  |     |  e       Y
        ///                 X * Y ==       (0)----->-(1)->-----(2)-----(3)
        ///
        ///
        ///                                          S-.
        ///                                        <-----<
        ///                                     X  |     |  e       Y
        ///                 X ~* Y ==      (0)----->-(1)->-----(2)-----(3)
        ///
        ///
        ///
        ///                                    X     S     S     Y
        ///                 X ?? Y ==      (0)---(1)---(2)---(3)---(4)
        ///
        ///
        ///
        ///                                    X     .        .      Y
        ///                 X ~.~. Y ==    (0)---(1)----(2)------(3)---(4)
        ///                                       |      |________|
        ///                                       |           ^   |
        ///                                       |_______________|
        ///                                          ^EOF or .^
        ///
        ///
        ///                                    X     S-.     S-.     Y
        ///                 X ~?~? Y ==    (0)---(1)-----(2)-----(3)---(4)
        ///                                       |      |________|
        ///                                       |           ^   |
        ///                                       |_______________|
        ///                                          ^EOF or .^
        ///
        ///
        ///
        ///             where S is any single character
        ///
        ///                   S-. is any single character except the final .
        ///
        ///                   e is a null character transition
        ///
        ///                   EOF is the end of the name string
        ///
        ///        In words:
        ///
        ///            * matches 0 or more characters.
        ///
        ///            ? matches exactly 1 character.
        ///
        ///            DOS_STAR matches 0 or more characters until encountering and matching
        ///                the final . in the name.
        ///
        ///            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.
        ///
        ///            DOS_DOT matches either a . or zero characters beyond name string.
        ///
        ///    Arguments:
        ///
        ///        Expression - Supplies the input expression to check against
        ///
        ///        Name - Supplies the input name to check for.
        ///
        ///    Return Value:
        ///
        ///        BOOLEAN - TRUE if Name is an element in the set of strings denoted
        ///            by the input Expression and FALSE otherwise.
        ///
        /// 
        public static bool StrictMatchPattern(string expression, string name)
        {
            int nameOffset;
            int exprOffset;
            int length;

            int srcCount;
            int destCount;
            int previousDestCount;
            int matchesCount;

            char nameChar = '\0';
            char exprChar = '\0';

            int[] previousMatches = new int[MATCHES_ARRAY_SIZE];
            int[] currentMatches = new int[MATCHES_ARRAY_SIZE];

            int maxState;
            int currentState;

            bool nameFinished = false;

            //
            //  The idea behind the algorithm is pretty simple.  We keep track of
            //  all possible locations in the regular expression that are matching
            //  the name.  If when the name has been exhausted one of the locations
            //  in the expression is also just exhausted, the name is in the language
            //  defined by the regular expression.
            //

            if (name == null || name.Length == 0)
            {
                return false;
            }

            if (expression == null || expression.Length == 0)
            {
                return true;
            }

            //
            //  Special case by far the most common wild card search of *
            //

            if (expression.Equals("*"))
            {
                return true;
            }

            // If this class is ever exposed for generic use,
            // we need to make sure that name doesn't contain wildcards. Currently
            // the only component that calls this method is FileSystemWatcher and
            // it will never pass a name that contains a wildcard.

            //
            //  Also special case expressions of the form *X.  With this and the prior
            //  case we have covered virtually all normal queries.
            //
            if (expression[0] == '*' && expression.IndexOf('*', 1) == -1)
            {
                int rightLength = expression.Length - 1;

                // if name is shorter that the stuff to the right of * in expression, we don't
                // need to do the string compare, otherwise we compare rightlength characters
                // and the end of both strings.
                if (name.Length >= rightLength && string.Compare(expression, 1, name, name.Length - rightLength, rightLength, GVFSPlatform.Instance.Constants.PathComparison) == 0)
                {
                    return true;
                }
            }

            //
            //  Walk through the name string, picking off characters.  We go one
            //  character beyond the end because some wild cards are able to match
            //  zero characters beyond the end of the string.
            //
            //  With each new name character we determine a new set of states that
            //  match the name so far.  We use two arrays that we swap back and forth
            //  for this purpose.  One array lists the possible expression states for
            //  all name characters up to but not including the current one, and other
            //  array is used to build up the list of states considering the current
            //  name character as well.  The arrays are then switched and the process
            //  repeated.
            //
            //  There is not a one-to-one correspondence between state number and
            //  offset into the expression.  This is evident from the NFAs in the
            //  initial comment to this function.  State numbering is not continuous.
            //  This allows a simple conversion between state number and expression
            //  offset.  Each character in the expression can represent one or two
            //  states.  * and DOS_STAR generate two states: ExprOffset*2 and
            //  ExprOffset*2 + 1.  All other expreesion characters can produce only
            //  a single state.  Thus ExprOffset = State/2.
            //
            //
            //  Here is a short description of the variables involved:
            //
            //  NameOffset  - The offset of the current name char being processed.
            //
            //  ExprOffset  - The offset of the current expression char being processed.
            //
            //  SrcCount    - Prior match being investigated with current name char
            //
            //  DestCount   - Next location to put a matching assuming current name char
            //
            //  NameFinished - Allows one more itteration through the Matches array
            //                 after the name is exhusted (to come *s for example)
            //
            //  PreviousDestCount - This is used to prevent entry duplication, see coment
            //
            //  PreviousMatches   - Holds the previous set of matches (the Src array)
            //
            //  CurrentMatches    - Holds the current set of matches (the Dest array)
            //
            //  AuxBuffer, LocalBuffer - the storage for the Matches arrays
            //

            //
            //  Set up the initial variables
            //

            previousMatches[0] = 0;
            matchesCount = 1;

            nameOffset = 0;
            maxState = expression.Length * 2;

            while (!nameFinished)
            {
                if (nameOffset < name.Length)
                {
                    nameChar = name[nameOffset];
                    length = 1;
                    nameOffset++;
                }
                else
                {
                    nameFinished = true;

                    //
                    //  if we have already exhasted the expression, C#.  Don't
                    //  continue.
                    //
                    if (previousMatches[matchesCount - 1] == maxState)
                    {
                        break;
                    }
                }

                //
                //  Now, for each of the previous stored expression matches, see what
                //  we can do with this name character.
                //
                srcCount = 0;
                destCount = 0;
                previousDestCount = 0;

                while (srcCount < matchesCount)
                {
                    //
                    //  We have to carry on our expression analysis as far as possible
                    //  for each character of name, so we loop here until the
                    //  expression stops matching.  A clue here is that expression
                    //  cases that can match zero or more characters end with a
                    //  continue, while those that can accept only a single character
                    //  end with a break.
                    //
                    exprOffset = ((previousMatches[srcCount++] + 1) / 2);
                    length = 0;

                    while (true)
                    {
                        if (exprOffset == expression.Length)
                        {
                            break;
                        }

                        //
                        //  The first time through the loop we don't want
                        //  to increment ExprOffset.
                        //

                        exprOffset += length;

                        currentState = exprOffset * 2;

                        if (exprOffset == expression.Length)
                        {
                            currentMatches[destCount++] = maxState;
                            break;
                        }

                        exprChar = expression[exprOffset];
                        length = 1;

                        //
                        //  Before we get started, we have to check for something
                        //  really gross.  We may be about to exhaust the local
                        //  space for ExpressionMatches[][], so we have to allocate
                        //  some pool if this is the case.  Yuk!
                        //

                        if (destCount >= MATCHES_ARRAY_SIZE - 2)
                        {
                            int newSize = currentMatches.Length * 2;
                            int[] tmp = new int[newSize];
                            Array.Copy(currentMatches, tmp, currentMatches.Length);
                            currentMatches = tmp;

                            tmp = new int[newSize];
                            Array.Copy(previousMatches, tmp, previousMatches.Length);
                            previousMatches = tmp;
                        }

                        //
                        //  * matches any character zero or more times.
                        //

                        if (exprChar == '*')
                        {
                            currentMatches[destCount++] = currentState;
                            currentMatches[destCount++] = (currentState + 1);
                            continue;
                        }

                        //
                        //  DOS_STAR matches any character except . zero or more times.
                        //

                        if (exprChar == ANSI_DOS_STAR)
                        {
                            bool iCanEatADot = false;

                            //
                            //  If we are at a period, determine if we are allowed to
                            //  consume it, ie. make sure it is not the last one.
                            //
                            if (!nameFinished && (nameChar == '.'))
                            {
                                char tmpChar;
                                int offset;

                                int nameLength = name.Length;
                                for (offset = nameOffset; offset < nameLength; offset++)
                                {
                                    tmpChar = name[offset];
                                    length = 1;

                                    if (tmpChar == '.')
                                    {
                                        iCanEatADot = true;
                                        break;
                                    }
                                }
                            }

                            if (nameFinished || (nameChar != '.') || iCanEatADot)
                            {
                                currentMatches[destCount++] = currentState;
                                currentMatches[destCount++] = (currentState + 1);
                                continue;
                            }
                            else
                            {
                                //
                                //  We are at a period.  We can only match zero
                                //  characters (ie. the epsilon transition).
                                //
                                currentMatches[destCount++] = (currentState + 1);
                                continue;
                            }
                        }

                        //
                        //  The following expreesion characters all match by consuming
                        //  a character, thus force the expression, and thus state
                        //  forward.
                        //
                        currentState += length * 2;

                        //
                        //  DOS_QM is the most complicated.  If the name is finished,
                        //  we can match zero characters.  If this name is a '.', we
                        //  don't match, but look at the next expression.  Otherwise
                        //  we match a single character.
                        //
                        if (exprChar == ANSI_DOS_QM)
                        {
                            if (nameFinished || (nameChar == '.'))
                            {
                                continue;
                            }

                            currentMatches[destCount++] = currentState;
                            break;
                        }

                        //
                        //  A DOS_DOT can match either a period, or zero characters
                        //  beyond the end of name.
                        //
                        if (exprChar == DOS_DOT)
                        {
                            if (nameFinished)
                            {
                                continue;
                            }

                            if (nameChar == '.')
                            {
                                currentMatches[destCount++] = currentState;
                                break;
                            }
                        }

                        //
                        //  From this point on a name character is required to even
                        //  continue, let alone make a match.
                        //
                        if (nameFinished)
                        {
                            break;
                        }

                        //
                        //  If this expression was a '?' we can match it once.
                        //
                        if (exprChar == '?')
                        {
                            currentMatches[destCount++] = currentState;
                            break;
                        }

                        //
                        //  Finally, check if the expression char matches the name char
                        //
                        if (char.ToUpperInvariant(exprChar) == char.ToUpperInvariant(nameChar))
                        {
                            currentMatches[destCount++] = currentState;
                            break;
                        }

                        //
                        //  The expression didn't match so go look at the next
                        //  previous match.
                        //

                        break;
                    }

                    //
                    //  Prevent duplication in the destination array.
                    //
                    //  Each of the arrays is montonically increasing and non-
                    //  duplicating, thus we skip over any source element in the src
                    //  array if we just added the same element to the destination
                    //  array.  This guarentees non-duplication in the dest. array.
                    //

                    if ((srcCount < matchesCount) && (previousDestCount < destCount))
                    {
                        while (previousDestCount < destCount)
                        {
                            int previousLength = previousMatches.Length;
                            while ((srcCount < previousLength) && (previousMatches[srcCount] < currentMatches[previousDestCount]))
                            {
                                srcCount += 1;
                            }

                            previousDestCount += 1;
                        }
                    }
                }

                //
                //  If we found no matches in the just finished itteration, it's time
                //  to bail.
                //

                if (destCount == 0)
                {
                    return false;
                }

                //
                //  Swap the meaning the two arrays
                //

                {
                    int[] tmp;

                    tmp = previousMatches;

                    previousMatches = currentMatches;

                    currentMatches = tmp;
                }

                matchesCount = destCount;
            }

            currentState = previousMatches[matchesCount - 1];

            return currentState == maxState;
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/PlatformLoader.Windows.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Platform.Windows;
using GVFS.Virtualization.FileSystem;

namespace GVFS.PlatformLoader
{
    public static class GVFSPlatformLoader
    {
        public static FileSystemVirtualizer CreateFileSystemVirtualizer(GVFSContext context, GVFSGitObjects gitObjects)
        {
            return new WindowsFileSystemVirtualizer(context, gitObjects);
        }

        public static void Initialize()
        {
            GVFSPlatform.Register(new WindowsPlatform());
            return;
        }
     }
}

================================================
FILE: GVFS/GVFS.Platform.Windows/ProjFSFilter.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Tracing;
using Microsoft.Win32;
using Microsoft.Windows.ProjFS;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.ServiceProcess;
using System.Text;

namespace GVFS.Platform.Windows
{
    public class ProjFSFilter : IKernelDriver
    {
        public const string ServiceName = "PrjFlt";
        private const string DriverName = "prjflt";
        private const string DriverFileName = DriverName + ".sys";
        private const string OptionalFeatureName = "Client-ProjFS";
        private const string EtwArea = nameof(ProjFSFilter);

        private const string PrjFltAutoLoggerKey = "SYSTEM\\CurrentControlSet\\Control\\WMI\\Autologger\\Microsoft-Windows-ProjFS-Filter-Log";
        private const string PrjFltAutoLoggerStartValue = "Start";

        private const string System32LogFilesRoot = @"%SystemRoot%\System32\LogFiles";
        private const string System32DriversRoot = @"%SystemRoot%\System32\drivers";

        // From "Autologger" section of prjflt.inf
        private const string FilterLoggerGuid = "ee4206ff-4a4d-452f-be56-6bd0ed272b44";
        private const string FilterLoggerSessionName = "Microsoft-Windows-ProjFS-Filter-Log";

        private const string ProjFSNativeLibFileName = "ProjectedFSLib.dll";
        private const string ProjFSManagedLibFileName = "ProjectedFSLib.Managed.dll";

        private const uint OkResult = 0;
        private const uint NameCollisionErrorResult = 0x801F0012;
        private const uint AccessDeniedResult = 0x80070005;

        private enum ProjFSInboxStatus
        {
            Invalid,
            NotInbox = 2,
            Enabled = 3,
            Disabled = 4,
        }

        public bool EnumerationExpandsDirectories { get; } = false;
        public bool EmptyPlaceholdersRequireFileSize { get; } = true;

        public string LogsFolderPath
        {
            get
            {
                return Path.Combine(Environment.ExpandEnvironmentVariables(System32LogFilesRoot), ProjFSFilter.ServiceName);
            }
        }

        public static bool TryAttach(string enlistmentRoot, out string errorMessage)
        {
            errorMessage = null;
            try
            {
                StringBuilder volumePathName = new StringBuilder(GVFSConstants.MaxPath);
                if (!NativeMethods.GetVolumePathName(enlistmentRoot, volumePathName, GVFSConstants.MaxPath))
                {
                    errorMessage = "Could not get volume path name";
                    return false;
                }

                uint result = NativeMethods.FilterAttach(DriverName, volumePathName.ToString(), null);
                if (result != OkResult && result != NameCollisionErrorResult)
                {
                    errorMessage = string.Format("Attaching the filter driver resulted in: {0}", result);
                    return false;
                }
            }
            catch (Exception e)
            {
                errorMessage = string.Format("Attaching the filter driver resulted in: {0}", e.Message);
                return false;
            }

            return true;
        }

        public static bool IsServiceRunning(ITracer tracer)
        {
            try
            {
                ServiceController controller = new ServiceController(DriverName);
                return controller.Status.Equals(ServiceControllerStatus.Running);
            }
            catch (InvalidOperationException e)
            {
                if (tracer != null)
                {
                    EventMetadata metadata = CreateEventMetadata();
                    metadata.Add("Exception", e.Message);
                    metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(IsServiceRunning)}: InvalidOperationException: {ServiceName} service was not found");
                    tracer.RelatedEvent(EventLevel.Informational, $"{nameof(IsServiceRunning)}_ServiceNotFound", metadata);
                }

                return false;
            }
        }

        public static bool IsServiceRunningAndInstalled(
            ITracer tracer,
            PhysicalFileSystem fileSystem,
            out bool isPrjfltServiceInstalled,
            out bool isPrjfltDriverInstalled,
            out bool isNativeProjFSLibInstalled)
        {
            bool isRunning = false;
            isPrjfltServiceInstalled = false;
            isPrjfltDriverInstalled = fileSystem.FileExists(Path.Combine(Environment.SystemDirectory, "drivers", DriverFileName));
            isNativeProjFSLibInstalled = IsNativeLibInstalled(tracer, fileSystem);

            try
            {
                ServiceController controller = new ServiceController(DriverName);
                isRunning = controller.Status.Equals(ServiceControllerStatus.Running);
                isPrjfltServiceInstalled = true;
            }
            catch (InvalidOperationException e)
            {
                if (tracer != null)
                {
                    EventMetadata metadata = CreateEventMetadata();
                    metadata.Add("Exception", e.Message);
                    metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(IsServiceRunningAndInstalled)}: InvalidOperationException: {ServiceName} service was not found");
                    tracer.RelatedEvent(EventLevel.Informational, $"{nameof(IsServiceRunningAndInstalled)}_ServiceNotFound", metadata);
                }

                return false;
            }

            return isRunning;
        }

        public static bool TryStartService(ITracer tracer)
        {
            try
            {
                ServiceController controller = new ServiceController(DriverName);
                if (!controller.Status.Equals(ServiceControllerStatus.Running))
                {
                    controller.Start();
                }

                return true;
            }
            catch (InvalidOperationException e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                tracer.RelatedError(metadata, $"{nameof(TryStartService)}: InvalidOperationException: {ServiceName} Service was not found");
            }
            catch (Win32Exception e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                tracer.RelatedError(metadata, $"{nameof(TryStartService)}: Win32Exception while trying to start prjflt");
            }

            return false;
        }

        public static bool IsAutoLoggerEnabled(ITracer tracer)
        {
            object startValue;

            try
            {
                startValue = WindowsPlatform.GetValueFromRegistry(RegistryHive.LocalMachine, PrjFltAutoLoggerKey, PrjFltAutoLoggerStartValue);

                if (startValue == null)
                {
                    tracer.RelatedError($"{nameof(IsAutoLoggerEnabled)}: Failed to find current Start value setting");
                    return false;
                }
            }
            catch (UnauthorizedAccessException e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                tracer.RelatedError(metadata, $"{nameof(IsAutoLoggerEnabled)}: UnauthorizedAccessException caught while trying to determine if auto-logger is enabled");
                return false;
            }
            catch (SecurityException e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                tracer.RelatedError(metadata, $"{nameof(IsAutoLoggerEnabled)}: SecurityException caught while trying to determine if auto-logger is enabled");
                return false;
            }

            try
            {
                return Convert.ToInt32(startValue) == 1;
            }
            catch (Exception e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                metadata.Add(nameof(startValue), startValue);
                tracer.RelatedError(metadata, $"{nameof(IsAutoLoggerEnabled)}: Exception caught while trying to determine if auto-logger is enabled");
                return false;
            }
        }

        public static bool TryEnableAutoLogger(ITracer tracer)
        {
            try
            {
                if (WindowsPlatform.GetValueFromRegistry(RegistryHive.LocalMachine, PrjFltAutoLoggerKey, PrjFltAutoLoggerStartValue) != null)
                {
                    if (WindowsPlatform.TrySetDWordInRegistry(RegistryHive.LocalMachine, PrjFltAutoLoggerKey, PrjFltAutoLoggerStartValue, 1))
                    {
                        return true;
                    }
                }
            }
            catch (UnauthorizedAccessException e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                tracer.RelatedError(metadata, $"{nameof(TryEnableAutoLogger)}: UnauthorizedAccessException caught while trying to enable auto-logger");
            }
            catch (SecurityException e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                tracer.RelatedError(metadata, $"{nameof(TryEnableAutoLogger)}: SecurityException caught while trying to enable auto-logger");
            }

            tracer.RelatedError($"{nameof(TryEnableAutoLogger)}: Failed to find AutoLogger Start value in registry");
            return false;
        }

        public static bool TryEnableOrInstallDriver(
            ITracer tracer,
            PhysicalFileSystem fileSystem,
            out uint windowsBuildNumber,
            out bool isInboxProjFSFinalAPI,
            out bool isProjFSFeatureAvailable)
        {
            isProjFSFeatureAvailable = false;
            if (!TryGetIsInboxProjFSFinalAPI(tracer, out windowsBuildNumber, out isInboxProjFSFinalAPI))
            {
                return false;
            }

            if (isInboxProjFSFinalAPI)
            {
                if (TryEnableProjFSOptionalFeature(tracer, fileSystem, out isProjFSFeatureAvailable))
                {
                    return true;
                }

                return false;
            }

            return TryInstallProjFSViaINF(tracer, fileSystem);
        }

        public static bool IsNativeLibInstalled(ITracer tracer, PhysicalFileSystem fileSystem)
        {
            string system32Path = Path.Combine(Environment.SystemDirectory, ProjFSNativeLibFileName);
            bool existsInSystem32 = fileSystem.FileExists(system32Path);

            string gvfsAppDirectory = ProcessHelper.GetCurrentProcessLocation();
            string nonInboxNativeLibInstallPath;
            string packagedNativeLibPath;
            GetNativeLibPaths(gvfsAppDirectory, out packagedNativeLibPath, out nonInboxNativeLibInstallPath);
            bool existsInAppDirectory = fileSystem.FileExists(nonInboxNativeLibInstallPath);

            EventMetadata metadata = CreateEventMetadata();
            metadata.Add(nameof(system32Path), system32Path);
            metadata.Add(nameof(existsInSystem32), existsInSystem32);
            metadata.Add(nameof(gvfsAppDirectory), gvfsAppDirectory);
            metadata.Add(nameof(nonInboxNativeLibInstallPath), nonInboxNativeLibInstallPath);
            metadata.Add(nameof(packagedNativeLibPath), packagedNativeLibPath);
            metadata.Add(nameof(existsInAppDirectory), existsInAppDirectory);
            tracer.RelatedEvent(EventLevel.Informational, nameof(IsNativeLibInstalled), metadata);
            return existsInSystem32 || existsInAppDirectory;
        }

        public static bool TryCopyNativeLibIfDriverVersionsMatch(ITracer tracer, PhysicalFileSystem fileSystem, out string copyNativeDllError)
        {
            string system32NativeLibraryPath = Path.Combine(Environment.SystemDirectory, ProjFSNativeLibFileName);
            if (fileSystem.FileExists(system32NativeLibraryPath))
            {
                copyNativeDllError = $"{ProjFSNativeLibFileName} already exists at {system32NativeLibraryPath}";
                return false;
            }

            string gvfsProcessLocation = ProcessHelper.GetCurrentProcessLocation();
            string nonInboxNativeLibInstallPath;
            string packagedNativeLibPath;
            GetNativeLibPaths(gvfsProcessLocation, out packagedNativeLibPath, out nonInboxNativeLibInstallPath);
            if (fileSystem.FileExists(nonInboxNativeLibInstallPath))
            {
                copyNativeDllError = $"{ProjFSNativeLibFileName} already exists at {nonInboxNativeLibInstallPath}";
                return false;
            }

            if (!fileSystem.FileExists(packagedNativeLibPath))
            {
                copyNativeDllError = $"{packagedNativeLibPath} not found, no {ProjFSNativeLibFileName} available to copy";
                return false;
            }

            string packagedPrjfltDriverPath = Path.Combine(gvfsProcessLocation, "Filter", DriverFileName);
            if (!fileSystem.FileExists(packagedPrjfltDriverPath))
            {
                copyNativeDllError = $"{packagedPrjfltDriverPath} not found, unable to validate that packaged driver matches installed driver";
                return false;
            }

            string system32PrjfltDriverPath = Path.Combine(Environment.ExpandEnvironmentVariables(System32DriversRoot), DriverFileName);
            if (!fileSystem.FileExists(system32PrjfltDriverPath))
            {
                copyNativeDllError = $"{system32PrjfltDriverPath} not found, unable to validate that packaged driver matches installed driver";
                return false;
            }

            FileVersionInfo packagedDriverVersion;
            FileVersionInfo system32DriverVersion;
            try
            {
                packagedDriverVersion = fileSystem.GetVersionInfo(packagedPrjfltDriverPath);
                system32DriverVersion = fileSystem.GetVersionInfo(system32PrjfltDriverPath);
                if (!fileSystem.FileVersionsMatch(packagedDriverVersion, system32DriverVersion))
                {
                    copyNativeDllError = $"Packaged sys FileVersion '{packagedDriverVersion.FileVersion}' does not match System32 sys FileVersion '{system32DriverVersion.FileVersion}'";
                    return false;
                }

                if (!fileSystem.ProductVersionsMatch(packagedDriverVersion, system32DriverVersion))
                {
                    copyNativeDllError = $"Packaged sys ProductVersion '{packagedDriverVersion.ProductVersion}' does not match System32 sys ProductVersion '{system32DriverVersion.ProductVersion}'";
                    return false;
                }
            }
            catch (FileNotFoundException e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                tracer.RelatedWarning(
                    metadata,
                    $"{nameof(TryCopyNativeLibIfDriverVersionsMatch)}: Exception caught while comparing sys versions");
                copyNativeDllError = $"Exception caught while comparing sys versions: {e.Message}";
                return false;
            }

            EventMetadata driverVersionMetadata = CreateEventMetadata();
            driverVersionMetadata.Add($"{nameof(packagedDriverVersion)}.FileVersion", packagedDriverVersion.FileVersion.ToString());
            driverVersionMetadata.Add($"{nameof(system32DriverVersion)}.FileVersion", system32DriverVersion.FileVersion.ToString());
            driverVersionMetadata.Add($"{nameof(packagedDriverVersion)}.ProductVersion", packagedDriverVersion.ProductVersion.ToString());
            driverVersionMetadata.Add($"{nameof(system32DriverVersion)}.ProductVersion", system32DriverVersion.ProductVersion.ToString());
            tracer.RelatedInfo(driverVersionMetadata, $"{nameof(TryCopyNativeLibIfDriverVersionsMatch)}: Copying native library");

            if (!TryCopyNativeLibToNonInboxInstallLocation(tracer, fileSystem, gvfsProcessLocation))
            {
                copyNativeDllError = "Failed to copy native library";
                return false;
            }

            copyNativeDllError = null;
            return true;
        }

        public bool IsGVFSUpgradeSupported()
        {
            return IsInboxAndEnabled();
        }

        public bool IsSupported(string normalizedEnlistmentRootPath, out string warning, out string error)
        {
            warning = null;
            error = null;

            string pathRoot = Path.GetPathRoot(normalizedEnlistmentRootPath);
            DriveInfo rootDriveInfo = DriveInfo.GetDrives().FirstOrDefault(x => x.Name == pathRoot);
            string[] requiredFormats = new[] { "NTFS", "ReFS" };
            if (rootDriveInfo == null)
            {
                warning = $"Unable to ensure that '{normalizedEnlistmentRootPath}' is an {string.Join(" or ", requiredFormats)} volume.";
            }
            else if (!requiredFormats.Any(requiredFormat => string.Equals(rootDriveInfo.DriveFormat, requiredFormat, StringComparison.OrdinalIgnoreCase)))
            {
                error = $"Only {string.Join(" and ", requiredFormats)} volumes are supported.  Ensure your repo is located in an {string.Join(" or ", requiredFormats)} volume.";
                return false;
            }

            if (Common.NativeMethods.IsFeatureSupportedByVolume(
                Directory.GetDirectoryRoot(normalizedEnlistmentRootPath),
                Common.NativeMethods.FileSystemFlags.FILE_RETURNS_CLEANUP_RESULT_INFO))
            {
                return true;
            }

            error = "File system does not support features required by VFS for Git. Confirm that Windows version is at or beyond that required by VFS for Git. A one-time reboot is required on Windows Server 2016 after installing VFS for Git.";
            return false;
        }

        public bool TryFlushLogs(out string error)
        {
            StringBuilder sb = new StringBuilder();
            try
            {
                string logfileName;
                uint result = Common.NativeMethods.FlushTraceLogger(FilterLoggerSessionName, FilterLoggerGuid, out logfileName);
                if (result != 0)
                {
                    sb.AppendFormat($"Failed to flush {ProjFSFilter.ServiceName} log buffers {result}");
                    error = sb.ToString();
                    return false;
                }
            }
            catch (Exception e)
            {
                sb.AppendFormat($"Failed to flush {ProjFSFilter.ServiceName} log buffers, exception: {e.ToString()}");
                error = sb.ToString();
                return false;
            }

            error = sb.ToString();
            return true;
        }

        public bool TryPrepareFolderForCallbacks(string folderPath, out string error, out Exception exception)
        {
            exception = null;
            try
            {
                return this.TryPrepareFolderForCallbacksImpl(folderPath, out error);
            }
            catch (FileNotFoundException e)
            {
                exception = e;

                if (e.FileName.Equals(ProjFSManagedLibFileName, GVFSPlatform.Instance.Constants.PathComparison))
                {
                    error = $"Failed to load {ProjFSManagedLibFileName}. Ensure that ProjFS is installed and enabled";
                }
                else
                {
                    error = $"FileNotFoundException while trying to prepare \"{folderPath}\" for callbacks: {e.Message}";
                }

                return false;
            }
            catch (Exception e)
            {
                exception = e;
                error = $"Exception while trying to prepare \"{folderPath}\" for callbacks: {e.Message}";
                return false;
            }
        }

        // TODO 1050199: Once the service is an optional component, GVFS should only attempt to attach
        // the filter via the service if the service is present\enabled
        public bool IsReady(JsonTracer tracer, string enlistmentRoot, TextWriter output, out string error)
        {
            error = string.Empty;
            if (!IsServiceRunning(tracer))
            {
                error = "ProjFS (prjflt) service is not running";
                return false;
            }

            if (!IsNativeLibInstalled(tracer, new PhysicalFileSystem()))
            {
                error = "ProjFS native library is not installed";
                return false;
            }

            if (!TryAttach(enlistmentRoot, out error))
            {
                // FilterAttach requires SE_LOAD_DRIVER_PRIVILEGE (admin). When running
                // non-elevated on a machine where ProjFS is already set up, the filter
                // is already attached to the volume and the only failure is ACCESS_DENIED.
                // Allow the mount to proceed in that specific case.
                if (error.Contains(AccessDeniedResult.ToString()))
                {
                    tracer.RelatedInfo($"IsReady: TryAttach returned ACCESS_DENIED, but ProjFS service is running. Proceeding.");
                    error = string.Empty;
                    return true;
                }

                return false;
            }

            return true;
        }

        public bool RegisterForOfflineIO()
        {
            return true;
        }

        public bool UnregisterForOfflineIO()
        {
            return true;
        }

        private static bool IsInboxAndEnabled()
        {
            ProcessResult getOptionalFeatureResult = GetProjFSOptionalFeatureStatus();
            return getOptionalFeatureResult.ExitCode == (int)ProjFSInboxStatus.Enabled;
        }

        private static bool TryGetIsInboxProjFSFinalAPI(ITracer tracer, out uint windowsBuildNumber, out bool isProjFSInbox)
        {
            isProjFSInbox = false;
            windowsBuildNumber = 0;
            try
            {
                windowsBuildNumber = Common.NativeMethods.GetWindowsBuildNumber();
                tracer.RelatedInfo($"{nameof(TryGetIsInboxProjFSFinalAPI)}: Build number = {windowsBuildNumber}");
            }
            catch (Win32Exception e)
            {
                tracer.RelatedError(CreateEventMetadata(e), $"{nameof(TryGetIsInboxProjFSFinalAPI)}: Exception while trying to get Windows build number");
                return false;
            }

            const uint MinRS4inboxVersion = 17121;
            const uint FirstRS5Version = 17600;
            const uint MinRS5inboxVersion = 17626;
            isProjFSInbox = !(windowsBuildNumber < MinRS4inboxVersion || (windowsBuildNumber >= FirstRS5Version && windowsBuildNumber < MinRS5inboxVersion));
            return true;
        }

        private static bool TryInstallProjFSViaINF(ITracer tracer, PhysicalFileSystem fileSystem)
        {
            string gvfsAppDirectory = ProcessHelper.GetCurrentProcessLocation();
            if (!TryCopyNativeLibToNonInboxInstallLocation(tracer, fileSystem, gvfsAppDirectory))
            {
                return false;
            }

            ProcessResult result = ProcessHelper.Run("RUNDLL32.EXE", $"SETUPAPI.DLL,InstallHinfSection DefaultInstall 128 {gvfsAppDirectory}\\Filter\\prjflt.inf");
            if (result.ExitCode == 0)
            {
                tracer.RelatedInfo($"{nameof(TryInstallProjFSViaINF)}: Installed PrjFlt via INF");
                return true;
            }
            else
            {
                EventMetadata metadata = CreateEventMetadata();
                metadata.Add("resultExitCode", result.ExitCode);
                metadata.Add("resultOutput", result.Output);
                tracer.RelatedError(metadata, $"{nameof(TryInstallProjFSViaINF)}: RUNDLL32.EXE failed to install PrjFlt");
            }

            return false;
        }

        private static bool TryCopyNativeLibToNonInboxInstallLocation(ITracer tracer, PhysicalFileSystem fileSystem, string gvfsAppDirectory)
        {
            string packagedNativeLibPath;
            string nonInboxNativeLibInstallPath;
            GetNativeLibPaths(gvfsAppDirectory, out packagedNativeLibPath, out nonInboxNativeLibInstallPath);

            EventMetadata pathMetadata = CreateEventMetadata();
            pathMetadata.Add(nameof(gvfsAppDirectory), gvfsAppDirectory);
            pathMetadata.Add(nameof(packagedNativeLibPath), packagedNativeLibPath);
            pathMetadata.Add(nameof(nonInboxNativeLibInstallPath), nonInboxNativeLibInstallPath);

            if (fileSystem.FileExists(packagedNativeLibPath))
            {
                tracer.RelatedEvent(EventLevel.Informational, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}_CopyingNativeLib", pathMetadata);

                try
                {
                    fileSystem.CopyFile(packagedNativeLibPath, nonInboxNativeLibInstallPath, overwrite: true);

                    try
                    {
                        fileSystem.FlushFileBuffers(nonInboxNativeLibInstallPath);
                    }
                    catch (Win32Exception e)
                    {
                        EventMetadata metadata = CreateEventMetadata(e);
                        metadata.Add(nameof(nonInboxNativeLibInstallPath), nonInboxNativeLibInstallPath);
                        metadata.Add(nameof(packagedNativeLibPath), packagedNativeLibPath);
                        tracer.RelatedWarning(metadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: Win32Exception while trying to flush file buffers", Keywords.Telemetry);
                    }
                }
                catch (UnauthorizedAccessException e)
                {
                    EventMetadata metadata = CreateEventMetadata(e);
                    tracer.RelatedError(metadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: UnauthorizedAccessException caught while trying to copy native lib");
                    return false;
                }
                catch (DirectoryNotFoundException e)
                {
                    EventMetadata metadata = CreateEventMetadata(e);
                    tracer.RelatedError(metadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: DirectoryNotFoundException caught while trying to copy native lib");
                    return false;
                }
                catch (FileNotFoundException e)
                {
                    EventMetadata metadata = CreateEventMetadata(e);
                    tracer.RelatedError(metadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: FileNotFoundException caught while trying to copy native lib");
                    return false;
                }
                catch (IOException e)
                {
                    EventMetadata metadata = CreateEventMetadata(e);
                    tracer.RelatedWarning(metadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: IOException caught while trying to copy native lib");

                    if (fileSystem.FileExists(nonInboxNativeLibInstallPath))
                    {
                        tracer.RelatedWarning(
                            CreateEventMetadata(),
                            "Could not copy native lib to app directory, but file already exists, continuing with install",
                            Keywords.Telemetry);
                    }
                    else
                    {
                        tracer.RelatedError($"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: Failed to copy native lib to app directory");
                        return false;
                    }
                }
            }
            else
            {
                tracer.RelatedError(pathMetadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: Native lib does not exist in install directory");
                return false;
            }

            return true;
        }

        private static void GetNativeLibPaths(string gvfsAppDirectory, out string packagedNativeLibPath, out string nonInboxNativeLibInstallPath)
        {
            packagedNativeLibPath = Path.Combine(gvfsAppDirectory, "ProjFS", ProjFSNativeLibFileName);
            nonInboxNativeLibInstallPath = Path.Combine(gvfsAppDirectory, ProjFSNativeLibFileName);
        }

        private static bool TryEnableProjFSOptionalFeature(ITracer tracer, PhysicalFileSystem fileSystem, out bool isProjFSFeatureAvailable)
        {
            EventMetadata metadata = CreateEventMetadata();
            ProcessResult getOptionalFeatureResult = GetProjFSOptionalFeatureStatus();

            isProjFSFeatureAvailable = true;
            bool projFSEnabled = false;
            switch (getOptionalFeatureResult.ExitCode)
            {
                case (int)ProjFSInboxStatus.NotInbox:
                    metadata.Add("getOptionalFeatureResult.Output", getOptionalFeatureResult.Output);
                    metadata.Add("getOptionalFeatureResult.Errors", getOptionalFeatureResult.Errors);
                    tracer.RelatedWarning(metadata, $"{nameof(TryEnableProjFSOptionalFeature)}: {OptionalFeatureName} optional feature is missing");

                    isProjFSFeatureAvailable = false;
                    break;

                case (int)ProjFSInboxStatus.Enabled:
                    tracer.RelatedEvent(
                        EventLevel.Informational,
                        $"{nameof(TryEnableProjFSOptionalFeature)}_ClientProjFSAlreadyEnabled",
                        metadata,
                        Keywords.Network);
                    projFSEnabled = true;
                    break;

                case (int)ProjFSInboxStatus.Disabled:
                    ProcessResult enableOptionalFeatureResult = CallPowershellCommand("try {Enable-WindowsOptionalFeature -Online -FeatureName " + OptionalFeatureName + " -NoRestart}catch{exit 1}");
                    metadata.Add("enableOptionalFeatureResult.Output", enableOptionalFeatureResult.Output.Trim().Replace("\r\n", ","));
                    metadata.Add("enableOptionalFeatureResult.Errors", enableOptionalFeatureResult.Errors);

                    if (enableOptionalFeatureResult.ExitCode == 0)
                    {
                        metadata.Add(TracingConstants.MessageKey.InfoMessage, "Enabled ProjFS optional feature");
                        tracer.RelatedEvent(EventLevel.Informational, $"{nameof(TryEnableProjFSOptionalFeature)}_ClientProjFSDisabled", metadata);
                        projFSEnabled = true;
                        break;
                    }

                    metadata.Add("enableOptionalFeatureResult.ExitCode", enableOptionalFeatureResult.ExitCode);
                    tracer.RelatedError(metadata, $"{nameof(TryEnableProjFSOptionalFeature)}: Failed to enable optional feature");
                    break;

                default:
                    metadata.Add("getOptionalFeatureResult.ExitCode", getOptionalFeatureResult.ExitCode);
                    metadata.Add("getOptionalFeatureResult.Output", getOptionalFeatureResult.Output);
                    metadata.Add("getOptionalFeatureResult.Errors", getOptionalFeatureResult.Errors);
                    tracer.RelatedError(metadata, $"{nameof(TryEnableProjFSOptionalFeature)}: Unexpected result");
                    isProjFSFeatureAvailable = false;
                    break;
            }

            if (projFSEnabled)
            {
                if (IsNativeLibInstalled(tracer, fileSystem))
                {
                    return true;
                }

                tracer.RelatedError($"{nameof(TryEnableProjFSOptionalFeature)}: {OptionalFeatureName} enabled, but native ProjFS library is not on path");
            }

            return false;
        }

        private static ProcessResult GetProjFSOptionalFeatureStatus()
        {
            try
            {
                return CallPowershellCommand(
                    "$var=(Get-WindowsOptionalFeature -Online -FeatureName " + OptionalFeatureName + ");  if($var -eq $null){exit " +
                    (int)ProjFSInboxStatus.NotInbox + "}else{if($var.State -eq 'Enabled'){exit " + (int)ProjFSInboxStatus.Enabled + "}else{exit " + (int)ProjFSInboxStatus.Disabled + "}}");
            }
            catch (PowershellNotFoundException e)
            {
                return new ProcessResult(string.Empty, e.Message, (int)ProjFSInboxStatus.Invalid);
            }
        }

        private static EventMetadata CreateEventMetadata(Exception e = null)
        {
            EventMetadata metadata = new EventMetadata();
            metadata.Add("Area", EtwArea);
            if (e != null)
            {
                metadata.Add("Exception", e.ToString());
            }

            return metadata;
        }

        private static ProcessResult CallPowershellCommand(string command)
        {
            ProcessResult whereResult = ProcessHelper.Run("where.exe", "powershell.exe");

            if (whereResult.ExitCode != 0)
            {
                throw new PowershellNotFoundException();
            }

            return ProcessHelper.Run(whereResult.Output.Trim(), "-NonInteractive -NoProfile -Command \"& { " + command + " }\"");
        }

        // Using an Impl method allows TryPrepareFolderForCallbacks to catch any ProjFS dependency related exceptions
        // thrown in the process of calling this method.
        private bool TryPrepareFolderForCallbacksImpl(string folderPath, out string error)
        {
            error = string.Empty;
            Guid virtualizationInstanceGuid = Guid.NewGuid();
            HResult result = VirtualizationInstance.MarkDirectoryAsVirtualizationRoot(folderPath, virtualizationInstanceGuid);
            if (result != HResult.Ok)
            {
                error = "Failed to prepare \"" + folderPath + "\" for callbacks, error: " + result.ToString("F");
                return false;
            }

            return true;
        }

        private static class NativeMethods
        {
            [DllImport("fltlib.dll", CharSet = CharSet.Unicode)]
            public static extern uint FilterAttach(
                string filterName,
                string volumeName,
                string instanceName,
                uint createdInstanceNameLength = 0,
                string createdInstanceName = null);

            [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool GetVolumePathName(
                string volumeName,
                StringBuilder volumePathName,
                uint bufferLength);
        }

        private class PowershellNotFoundException : Exception
        {
            public PowershellNotFoundException()
                : base("powershell.exe was not found")
            {
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/Readme.md
================================================
# Windows file system virtualization

The purpose of this document is to give a high level overview of how the virtualization on Windows works.  ProjFS is the file system level driver that is used to intercept file system calls and then call out to a user mode process, in this case GVFS.Mount.exe to get virtualized information or to notify of file system events. There are two interfaces that are exposed by ProjFS.  `IVirtualizationInstance` which has all the notifications callbacks and methods that can be called to manipulate the state of the virtual file system. `IRequiredCallbacks` are the methods that are required for virtualization to work. This interface is passed to the `StartVirtualizing` method.

## Required Callbacks - `IRequiredCallbacks` interface

The methods of this interface are required for virtualization and are for enumerating directories and getting placeholder and file data in order for ProjFS to project the files or provide the file content.

## Virtualization - `IVirtualizationInstance` interface

The ProjFS managed library provide the implementation of this interface with `VirtualizationInstance` which is created in the constructor of the `WindowsFileSystemVirtualizer`.  In addition to the root directory of the working directory which will be the virtualization root, it allows the caller to control thread counts, the negative path cache, and notification mappings.

### Negative path cache

The negative path cache is a feature in ProjFS that allows it to cache paths that VFSForGit has returned as not found. This gave significant performance benefit because ProjFS no longer needed to make the call to the user mode process (GVFS.Mount.exe) to find out that the path doesn't exist.  There is also a method on the `VirtualizationInstance` called `ClearNegativePathCache` that VFSForGit needs to call when it is changing the projection so that paths that may not have exists at the previous commits will now show up.

### Notification Mappings

Notification mappings are used to set what notification callbacks will be called for a certain path.  Any path in the virtualization root can have different notifications setup for it using bitwise OR-ed values of `NotificationType` from ProjFS.  VFSForGit has the combined values in the `Notifications` class for specific files and folders.

The `WindowsFileSystemVirtulizer` turns off notifications for the `.git` directory except for some specific files like the `index` file or the folder `refs/heads/`.  This helps the performance of git because for most file system access to the `.git` directory will be close to NTFS speed.

### Notification callbacks

The notifications callbacks are used to let the user mode process know about various file system actions that are about to take place or have taken place. These are used by VFSForGit to keep the modified paths (the files that git should be keeping up to date) and placeholders correct based on what has happened on the file system.

### Command methods

There are other methods that VFSForGit can use on the `VirtualizationInstance` to interact with the files/folders in the virtualization. A couple examples of these methods are listed.

#### `MarkDirectoryAsPlaceholder`

Used to change a directory to a placeholder so that ProjFS will start asking VFSForGit what the contents of that folder should be and merging that with what is on disk. This is called in a couple of places when a new folder is created.

1. When git creates a folder, it will only add the files that are in the modified paths so there might be files that git will not write to the new folder and need to start being projected.
2. When using the sparse feature and a folder is created that is in the repository but not in the sparse set for projection. The new folder gets added to the sparse set of folders and that new folder needs to start projecting the files and folders in it.

#### `DeleteFile` and `UpdateFileIfNeeded`

Used by VFSForGit when the projection changes to update with new file data and SHA1 or delete the placeholders that ProjFS has on disk so that it will match the new projection and files will have the correct content when read. Since what is on disk takes priority, these methods can fail if called after a file has been marked dirty, converted to a full file, or turned into a tombstone.

## File/Folder states

Files and folders in the projection can be in various states to keep the virtual state of the file system.  

### Virtual

Files and folders in this state have nothing on disk. They show when a directory is enumerated and ProjFS gets the list of files and folders from VFSForGit to satisfy the enumeration request.

### Placeholder

This is a file or folder that is on disk with a specific reparse point that means it doesn't have all the data.  For files that means it has the attributes for the file but not the content on disk.  There is a SHA1 stored as the `contentId` in the placeholder so that ProjFS can pass that back to VFSForGit to get the content. For folders it means that ProjFS will ask VFSForGit what the contents of the folder should be and merges that with what is on disk to give the view of the file system for that folder.

### Hydrated Placeholder

A file that has been read and the contents for the file have been retrieved from VFSForGit and is now on disk. This means any future reads are passthrough to the file system for native file system performance.

### Dirty Placeholder

When a placeholder that is hydrated or not has its attributes changed and it is now different from what the provider (VFSForGit) gave. This comes into play when trying to update or delete placeholders.  When it is dirty and the code didn't pass `AllowDirtyMetadata`, the update or delete will fail with a `UpdateFailureReason` of `DirtyMetadata`.

### Full file

File has been written to or opened for write. This means the file will no longer be updated by VFSForGit and is a regular NTFS file. The path will be added to the modified paths of VFSForGit and git will be the process updating/deleting the file.

### Tombstone

This is created when an item is deleted to track what items have been deleted so that they won't get projected again because when there is not a item then ProjFS uses the items from the projection. These need to be deleted when the projection changes so that the correct files and folders will be projected.

## Diagram

```
+-------------------------+
|                         |
|    Virtual              |
|                         |
+----+--------------------+
     |          |
     |        lstat
     |          |
     |          v
     |   +------+------+
     |   |             |
     |   | Placeholder +-------+
     |   |             |       |
     |   +-+-----+-----+       |
     |     |     |             |
     |     |    open          open
     |     |     |             |
     |     |    for           for
     |     |     |             |
     |     |    read          write
     |     |     |             |
     |     |     v             |
     |     |  +-------------+  |
     |     |  |             |  |
     |     |  | Hydrated    |  |
     |     |  | Placeholder |  |
     |     |  |             |  |
     |     |  +-+------+----+  |
     +<----+    |      |       |
     |          |     open     |
     |          |      |       |
     |          |     for      |
     |          |      |       |
     |          |     write    |
     |          |      |       |
     |          |      v       v
     |          |  +---------------+
     |          |  |               |
     |          |  |   Full File   |
     |          |  |               |
     |          |  +----+----------+
     |          |       |
     |          v       |
     +----------+-------+
     |
     |
  deleted
     |
     v
 +---+---------------------+
 |                         |
 |    Tombstone            |
 |                         |
 +-------------------------+
```

## Example

In the `src` folder which is the virtualization root after an initial `gvfs clone` there is a file (`file1.txt`).

***

### Enumerate `src`

1. ProjFS calls `StartDirectoryEnumerationCallback`.
2. VFSForGit creates an `ActiveEnumeration` from the current projection and adds to list.
3. ProjFS calls `GetDirectoryEnumerationCallback`.
4. VFSForGit gets the `ActiveEnumeration` by the enumeration `Guid` and add to the enumeration results via the `IDirectoryEnumerationResults` interface.
5. ProjFS calls `EndDirectoryEnumerationCallback` when done.
6. VFSForGit removes the `ActiveEnumeration` from list.

State of the files and folders are still all virtual, same as before the enumeration.

***

### Read attributes on `src/file1.txt`

1. ProjFS calls `GetPlaceholderInfoCallback`.
2. If path is not projected, return not found.
3. VFSForGit
   1. Calls `GetProjectedFileInfo` and if null returns not found.
   2. Calls `WritePlaceholderInfo` to create the placeholder file.
   3. Adds the placeholder to the placeholder database.

***

### Read content on `src/file1.txt`

1. ProjFS reads the on-disk placeholder data and calls `GetFileDataCallback`.
2. VFSForGit
   1. Uses the `contentId` which will be the blob's SHA1 to get the file content.
   2. Calls `CreateWriteBuffer` to create an `IWriteBuffer`.
   3. Copies the data to the `IWriteBuffer.Stream`.

***

### Write to `src/file1.txt`

1. ProjFS remove the reparse point so file is a NTFS file.
2. ProjFS calls `OnNotifyFilePreConvertToFull`.
3. VFSForGit if path is projected
   1. Adds path to the modified paths so git will keep it up to date.
   2. Removes path from the placeholder list.

***

### Delete `src/file1.txt`

1. ProjFS replaces file with tombstone file
2. ProjFS calls `OnNotifyFileHandleClosedFileModifiedOrDeleted`
3. VFSForGit
   1. Adds path to the modified paths so git will keep it up to date.
   2. Removes path from the placeholder list.

At this point `file1.txt` is still in the projection and will be return by enumeration requests but because ProjFS has the tombstone file and that is given precedence over projected files ProjFS will not return `file1.txt` for the enumeration.

================================================
FILE: GVFS/GVFS.Platform.Windows/WindowsFileBasedLock.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Tracing;
using System;
using System.ComponentModel;
using System.IO;
using System.Text;

namespace GVFS.Platform.Windows
{
    public class WindowsFileBasedLock : FileBasedLock
    {
        private const int HResultErrorSharingViolation = -2147024864; // -2147024864 = 0x80070020 = ERROR_SHARING_VIOLATION
        private const int HResultErrorFileExists = -2147024816; // -2147024816 = 0x80070050 = ERROR_FILE_EXISTS
        private const int DefaultStreamWriterBufferSize = 1024; // Copied from: http://referencesource.microsoft.com/#mscorlib/system/io/streamwriter.cs,5516ce201dc06b5f
        private const string EtwArea = nameof(WindowsFileBasedLock);
        private static readonly Encoding UTF8NoBOM = new UTF8Encoding(false, true); // Default encoding used by StreamWriter

        private readonly object deleteOnCloseStreamLock = new object();
        private Stream deleteOnCloseStream;

        /// 
        /// FileBasedLock constructor
        /// 
        /// Path to lock file
        /// Text to write in lock file
        /// 
        /// GVFS keeps an exclusive write handle open to lock files that it creates with FileBasedLock.  This means that
        /// FileBasedLock still ensures exclusivity when the lock file is used only for coordination between multiple GVFS processes.
        /// 
        public WindowsFileBasedLock(
            PhysicalFileSystem fileSystem,
            ITracer tracer,
            string lockPath)
            : base(fileSystem, tracer, lockPath)
        {
        }

        public override bool TryAcquireLock(out Exception lockException)
        {
            lockException = null;
            try
            {
                lock (this.deleteOnCloseStreamLock)
                {
                    if (this.deleteOnCloseStream != null)
                    {
                        throw new InvalidOperationException("Lock has already been acquired");
                    }

                    this.FileSystem.CreateDirectory(Path.GetDirectoryName(this.LockPath));

                    this.deleteOnCloseStream = this.FileSystem.OpenFileStream(
                        this.LockPath,
                        FileMode.Create,
                        FileAccess.ReadWrite,
                        FileShare.Read,
                        FileOptions.DeleteOnClose,
                        callFlushFileBuffers: false);

                    return true;
                }
            }
            catch (IOException e)
            {
                // HResultErrorFileExists is expected when the lock file exists
                // HResultErrorSharingViolation is expected when the lock file exists and another GVFS process has acquired the lock file
                if (e.HResult != HResultErrorFileExists && e.HResult != HResultErrorSharingViolation)
                {
                    EventMetadata metadata = this.CreateLockMetadata(e);
                    this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: IOException caught while trying to acquire lock");
                }

                lockException = e;
                this.DisposeStream();
                return false;
            }
            catch (UnauthorizedAccessException e)
            {
                EventMetadata metadata = this.CreateLockMetadata(e);
                this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: UnauthorizedAccessException caught while trying to acquire lock");

                lockException = e;
                this.DisposeStream();
                return false;
            }
            catch (Win32Exception e)
            {
                EventMetadata metadata = this.CreateLockMetadata(e);
                this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: Win32Exception caught while trying to acquire lock");

                lockException = e;
                this.DisposeStream();
                return false;
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateLockMetadata(e);
                this.Tracer.RelatedError(metadata, $"{nameof(this.TryAcquireLock)}: Unhandled exception caught while trying to acquire lock");

                this.DisposeStream();
                throw;
            }
        }

        public override void Dispose()
        {
            this.DisposeStream();
        }

        private EventMetadata CreateLockMetadata(Exception exception = null)
        {
            EventMetadata metadata = new EventMetadata();
            metadata.Add("Area", EtwArea);
            metadata.Add(nameof(this.LockPath), this.LockPath);
            if (exception != null)
            {
                metadata.Add("Exception", exception.ToString());
            }

            return metadata;
        }

        private bool DisposeStream()
        {
            lock (this.deleteOnCloseStreamLock)
            {
                if (this.deleteOnCloseStream != null)
                {
                    this.deleteOnCloseStream.Dispose();
                    this.deleteOnCloseStream = null;
                    return true;
                }
            }

            return false;
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/WindowsFileSystem.Shared.cs
================================================
using GVFS.Common;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;

namespace GVFS.Platform.Windows
{
    public partial class WindowsFileSystem
    {
        public static bool TryGetNormalizedPathImplementation(string path, out string normalizedPath, out string errorMessage)
        {
            normalizedPath = null;
            errorMessage = null;
            try
            {
                // The folder might not be on disk yet, walk up the path until we find a folder that's on disk
                Stack removedPathParts = new Stack();
                string parentPath = path;
                while (!string.IsNullOrWhiteSpace(parentPath) && !Directory.Exists(parentPath))
                {
                    removedPathParts.Push(Path.GetFileName(parentPath));
                    parentPath = Path.GetDirectoryName(parentPath);
                }

                if (string.IsNullOrWhiteSpace(parentPath))
                {
                    errorMessage = "Could not get path root. Specified path does not exist and unable to find ancestor of path on disk";
                    return false;
                }

                normalizedPath = NativeMethods.GetFinalPathName(parentPath);

                // normalizedPath now consists of all parts of the path currently on disk, re-add any parts of the path that were popped off
                while (removedPathParts.Count > 0)
                {
                    normalizedPath = Path.Combine(normalizedPath, removedPathParts.Pop());
                }
            }
            catch (Win32Exception e)
            {
                errorMessage = "Could not get path root. Failed to determine volume: " + e.Message;
                return false;
            }

            return true;
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/WindowsFileSystem.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Tracing;
using Microsoft.Win32.SafeHandles;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;

namespace GVFS.Platform.Windows
{
    public partial class WindowsFileSystem : IPlatformFileSystem
    {
        public bool SupportsFileMode { get; } = false;

        /// 
        /// Adds a new FileSystemAccessRule granting read (and optionally modify) access for all users.
        /// 
        /// DirectorySecurity to which a FileSystemAccessRule will be added.
        /// 
        /// True if all users should be given modify access, false if users should only be allowed read access
        /// 
        public static void AddUsersAccessRulesToDirectorySecurity(DirectorySecurity directorySecurity, bool grantUsersModifyPermissions)
        {
            SecurityIdentifier allUsers = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
            FileSystemRights rights = FileSystemRights.Read;
            if (grantUsersModifyPermissions)
            {
                rights = rights | FileSystemRights.Modify;
            }

            // InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit -> ACE is inherited by child directories and files
            // PropagationFlags.None -> Standard propagation rules, settings are applied to the directory and its children
            // AccessControlType.Allow -> Rule is used to allow access to an object
            directorySecurity.AddAccessRule(
                new FileSystemAccessRule(
                    allUsers,
                    rights,
                    InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
                    PropagationFlags.None,
                    AccessControlType.Allow));
        }

        /// 
        /// Adds a new FileSystemAccessRule granting read/exceute/modify/delete access for administrators.
        /// 
        /// DirectorySecurity to which a FileSystemAccessRule will be added.
        public static void AddAdminAccessRulesToDirectorySecurity(DirectorySecurity directorySecurity)
        {
            SecurityIdentifier administratorUsers = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null);

            // InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit -> ACE is inherited by child directories and files
            // PropagationFlags.None -> Standard propagation rules, settings are applied to the directory and its children
            // AccessControlType.Allow -> Rule is used to allow access to an object
            directorySecurity.AddAccessRule(
                new FileSystemAccessRule(
                    administratorUsers,
                    FileSystemRights.ReadAndExecute | FileSystemRights.Modify | FileSystemRights.Delete,
                    InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
                    PropagationFlags.None,
                    AccessControlType.Allow));
        }

        /// 
        /// Removes all FileSystemAccessRules from specified DirectorySecurity
        /// 
        /// DirectorySecurity from which to remove FileSystemAccessRules
        public static void RemoveAllFileSystemAccessRulesFromDirectorySecurity(DirectorySecurity directorySecurity)
        {
            AuthorizationRuleCollection currentRules = directorySecurity.GetAccessRules(includeExplicit: true, includeInherited: true, targetType: typeof(NTAccount));
            foreach (AuthorizationRule authorizationRule in currentRules)
            {
                FileSystemAccessRule fileSystemRule = authorizationRule as FileSystemAccessRule;
                if (fileSystemRule != null)
                {
                    directorySecurity.RemoveAccessRule(fileSystemRule);
                }
            }
        }

        public void FlushFileBuffers(string path)
        {
            NativeMethods.FlushFileBuffers(path);
        }

        public void MoveAndOverwriteFile(string sourceFileName, string destinationFilename)
        {
            NativeMethods.MoveFile(
                sourceFileName,
                destinationFilename,
                NativeMethods.MoveFileFlags.MoveFileReplaceExisting);
        }

        public void SetDirectoryLastWriteTime(string path, DateTime lastWriteTime, out bool directoryExists)
        {
            NativeMethods.SetDirectoryLastWriteTime(path, lastWriteTime, out directoryExists);
        }

        public void ChangeMode(string path, ushort mode)
        {
        }

        public bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage)
        {
            return WindowsFileSystem.TryGetNormalizedPathImplementation(path, out normalizedPath, out errorMessage);
        }

        public bool HydrateFile(string fileName, byte[] buffer)
        {
            return NativeFileReader.TryReadFirstByteOfFile(fileName, buffer);
        }

        public bool IsExecutable(string fileName)
        {
            string fileExtension = Path.GetExtension(fileName);
            return string.Equals(fileExtension, ".exe", GVFSPlatform.Instance.Constants.PathComparison);
        }

        public bool IsSocket(string fileName)
        {
            return false;
        }

        /// 
        /// Creates the specified directory (and its ancestors) if they do not
        /// already exist.
        ///
        /// If the specified directory does not exist this method:
        ///
        ///  - Creates the directory and its ancestors
        ///  - Adjusts the ACLs of 'directoryPath' (the ancestors' ACLs are not
        ///    modified).
        /// 
        /// 
        /// - true if the directory already exists -or- the directory was successfully created
        ///   with the proper ACLS
        /// - false otherwise
        /// 
        /// 
        /// The following permissions are typically present on deskop and missing on Server.
        /// These are the permissions added by this method.
        ///
        ///   ACCESS_ALLOWED_ACE_TYPE: NT AUTHORITY\Authenticated Users
        ///          [OBJECT_INHERIT_ACE]
        ///          [CONTAINER_INHERIT_ACE]
        ///          [INHERIT_ONLY_ACE]
        ///        DELETE
        ///        GENERIC_EXECUTE
        ///        GENERIC_WRITE
        ///        GENERIC_READ
        /// 
        public bool TryCreateDirectoryAccessibleByAuthUsers(string directoryPath, out string error, ITracer tracer = null)
        {
            if (Directory.Exists(directoryPath))
            {
                error = null;
                return true;
            }

            try
            {
                // Create the directory first and then adjust the ACLs as needed
                Directory.CreateDirectory(directoryPath);

                // Use AccessRuleFactory rather than creating a FileSystemAccessRule because the NativeMethods.FileAccess flags
                // we're specifying are not valid for the FileSystemRights parameter of the FileSystemAccessRule constructor
                DirectorySecurity directorySecurity = Directory.GetAccessControl(directoryPath);
                AccessRule authenticatedUsersAccessRule = directorySecurity.AccessRuleFactory(
                    new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null),
                    unchecked((int)(NativeMethods.FileAccess.DELETE | NativeMethods.FileAccess.GENERIC_EXECUTE | NativeMethods.FileAccess.GENERIC_WRITE | NativeMethods.FileAccess.GENERIC_READ)),
                    true,
                    InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
                    PropagationFlags.None,
                    AccessControlType.Allow);

                // The return type of the AccessRuleFactory method is the base class, AccessRule, but the return value can be cast safely to the derived class.
                // https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemsecurity.accessrulefactory(v=vs.110).aspx
                directorySecurity.AddAccessRule((FileSystemAccessRule)authenticatedUsersAccessRule);
                Directory.SetAccessControl(directoryPath, directorySecurity);
            }
            catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is SystemException)
            {
                if (tracer != null)
                {
                    EventMetadata metadataData = new EventMetadata();
                    metadataData.Add("Exception", e.ToString());
                    metadataData.Add(nameof(directoryPath), directoryPath);
                    tracer.RelatedError(metadataData, $"{nameof(this.TryCreateDirectoryAccessibleByAuthUsers)}: Failed to create and configure directory");
                }

                error = e.Message;
                return false;
            }

            error = null;
            return true;
        }

        public bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error)
        {
            try
            {
                DirectorySecurity directorySecurity = new DirectorySecurity();

                // Protect the access rules from inheritance and remove any inherited rules
                directorySecurity.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);

                // Add new ACLs for users and admins.  Users will be granted write permissions.
                AddUsersAccessRulesToDirectorySecurity(directorySecurity, grantUsersModifyPermissions: true);
                AddAdminAccessRulesToDirectorySecurity(directorySecurity);

                Directory.CreateDirectory(directoryPath, directorySecurity);
            }
            catch (Exception e) when (e is IOException ||
                                      e is UnauthorizedAccessException ||
                                      e is PathTooLongException ||
                                      e is DirectoryNotFoundException)
            {
                error = $"Exception while creating directory `{directoryPath}`: {e.Message}";
                return false;
            }

            error = null;
            return true;
        }

        public bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error)
        {
            try
            {
                DirectorySecurity directorySecurity;
                if (Directory.Exists(directoryPath))
                {
                    directorySecurity = Directory.GetAccessControl(directoryPath);
                }
                else
                {
                    directorySecurity = new DirectorySecurity();
                }

                // Protect the access rules from inheritance and remove any inherited rules
                directorySecurity.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);

                // Remove any existing ACLs and add new ACLs for users and admins
                RemoveAllFileSystemAccessRulesFromDirectorySecurity(directorySecurity);
                AddUsersAccessRulesToDirectorySecurity(directorySecurity, grantUsersModifyPermissions: false);
                AddAdminAccessRulesToDirectorySecurity(directorySecurity);

                Directory.CreateDirectory(directoryPath, directorySecurity);

                // Ensure the ACLs are set correctly if the directory already existed
                Directory.SetAccessControl(directoryPath, directorySecurity);
            }
            catch (Exception e) when (e is IOException || e is SystemException)
            {
                EventMetadata metadata = new EventMetadata();
                metadata.Add("Exception", e.ToString());
                tracer.RelatedError(metadata, $"{nameof(this.TryCreateOrUpdateDirectoryToAdminModifyPermissions)}: Exception while creating/configuring directory");

                error = e.Message;
                return false;
            }

            error = null;
            return true;
        }

        public bool IsFileSystemSupported(string path, out string error)
        {
            error = null;
            return true;
        }

        /// 
        /// On Windows, if the current user is elevated, the owner of the directory will be the Administrators group by default.
        /// This runs afoul of the git "dubious ownership" check, which can fail if either the .git directory or the working directory
        /// are not owned by the current user.
        ///
        /// At the moment git for windows does not consider a non-elevated admin to be the owner of a directory owned by the Administrators group,
        /// though a fix is in progress in the microsoft fork of git. Libgit2(sharp) also does not have this fix.
        ///
        /// Also, even if the fix were in place, automount would still fail because it runs under SYSTEM account.
        ///
        /// This method ensures that the directory is owned by the current user (which is verified to work for SYSTEM account for automount).
        /// 
        public void EnsureDirectoryIsOwnedByCurrentUser(string directoryPath)
        {
            // Ensure directory exists, inheriting all other ACLS
            Directory.CreateDirectory(directoryPath);
            // If the user is currently elevated, the owner of the directory will be the Administrators group.
            DirectorySecurity directorySecurity = Directory.GetAccessControl(directoryPath);
            IdentityReference directoryOwner = directorySecurity.GetOwner(typeof(SecurityIdentifier));
            SecurityIdentifier administratorsSid = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null);
            if (directoryOwner == administratorsSid)
            {
                WindowsIdentity currentUser = WindowsIdentity.GetCurrent();
                directorySecurity.SetOwner(currentUser.User);
                Directory.SetAccessControl(directoryPath, directorySecurity);
            }
        }

        private class NativeFileReader
        {
            private const uint GenericRead = 0x80000000;
            private const uint OpenExisting = 3;

            public static bool TryReadFirstByteOfFile(string fileName, byte[] buffer)
            {
                using (SafeFileHandle handle = Open(fileName))
                {
                    if (!handle.IsInvalid)
                    {
                        return ReadOneByte(handle, buffer);
                    }
                }

                return false;
            }

            private static SafeFileHandle Open(string fileName)
            {
                return CreateFile(fileName, GenericRead, (uint)(FileShare.ReadWrite | FileShare.Delete), 0, OpenExisting, 0, 0);
            }

            private static bool ReadOneByte(SafeFileHandle handle, byte[] buffer)
            {
                int bytesRead = 0;
                return ReadFile(handle, buffer, 1, ref bytesRead, 0);
            }

            [DllImport("kernel32", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Unicode)]
            private static extern SafeFileHandle CreateFile(
                string fileName,
                uint desiredAccess,
                uint shareMode,
                uint securityAttributes,
                uint creationDisposition,
                uint flagsAndAttributes,
                int hemplateFile);

            [DllImport("kernel32", SetLastError = true)]
            private static extern bool ReadFile(
                SafeFileHandle file,
                [Out] byte[] buffer,
                int numberOfBytesToRead,
                ref int numberOfBytesRead,
                int overlapped);
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.Virtualization;
using GVFS.Virtualization.BlobSize;
using GVFS.Virtualization.FileSystem;
using GVFS.Virtualization.Projection;
using Microsoft.Windows.ProjFS;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace GVFS.Platform.Windows
{
    public class WindowsFileSystemVirtualizer : FileSystemVirtualizer, IRequiredCallbacks
    {
        /// 
        /// GVFS uses the first byte of the providerId field of placeholders to version
        /// the data that it stores in the contentId (and providerId) fields of the placeholder
        /// 
        public static readonly byte[] PlaceholderVersionId = new byte[] { PlaceholderVersion };

        private const string ClassName = nameof(WindowsFileSystemVirtualizer);
        private const int MaxBlobStreamBufferSize = 64 * 1024;
        private const int MinPrjLibThreads = 5;

        private IVirtualizationInstance virtualizationInstance;
        private ConcurrentDictionary activeEnumerations;
        private ConcurrentDictionary activeCommands;

        public WindowsFileSystemVirtualizer(GVFSContext context, GVFSGitObjects gitObjects)
            : this(
                  context,
                  gitObjects,
                  virtualizationInstance: null,
                  numWorkerThreads: FileSystemVirtualizer.DefaultNumWorkerThreads)
        {
        }

        public WindowsFileSystemVirtualizer(
            GVFSContext context,
            GVFSGitObjects gitObjects,
            IVirtualizationInstance virtualizationInstance,
            int numWorkerThreads)
            : base(context, gitObjects, numWorkerThreads)
        {
            List notificationMappings = new List()
            {
                new NotificationMapping(Notifications.FilesInWorkingFolder | Notifications.FoldersInWorkingFolder, string.Empty),
                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
            // non-network operations to possibly succeed despite the connection limit
            uint threadCount = (uint)Math.Max(MinPrjLibThreads, Environment.ProcessorCount * 2);
            this.virtualizationInstance = virtualizationInstance ?? new VirtualizationInstance(
                context.Enlistment.WorkingDirectoryRoot,
                poolThreadCount: threadCount,
                concurrentThreadCount: threadCount,
                enableNegativePathCache: true,
                notificationMappings: notificationMappings);

            this.activeEnumerations = new ConcurrentDictionary();
            this.activeCommands = new ConcurrentDictionary();
        }

        protected override string EtwArea => ClassName;

        /// 
        /// Public for unit testing
        /// 
        public static bool InternalFileNameMatchesFilter(string name, string filter)
        {
            return PatternMatcher.StrictMatchPattern(filter, name);
        }

        public static FSResult HResultToFSResult(HResult result)
        {
            switch (result)
            {
                case HResult.Ok:
                    return FSResult.Ok;

                case HResult.DirNotEmpty:
                    return FSResult.DirectoryNotEmpty;

                case HResult.FileNotFound:
                case HResult.PathNotFound:
                    return FSResult.FileOrPathNotFound;

                case (HResult)HResultExtensions.HResultFromNtStatus.IoReparseTagNotHandled:
                    return FSResult.IoReparseTagNotHandled;

                case HResult.VirtualizationInvalidOp:
                    return FSResult.VirtualizationInvalidOperation;

                case (HResult)HResultExtensions.GenericProjFSError:
                    return FSResult.GenericProjFSError;

                default:
                    return FSResult.IOError;
            }
        }

        public override void Stop()
        {
            this.virtualizationInstance.StopVirtualizing();
        }

        public override FileSystemResult ClearNegativePathCache(out uint totalEntryCount)
        {
            HResult result = this.virtualizationInstance.ClearNegativePathCache(out totalEntryCount);
            return new FileSystemResult(HResultToFSResult(result), unchecked((int)result));
        }

        public override FileSystemResult DeleteFile(string relativePath, UpdatePlaceholderType updateFlags, out UpdateFailureReason failureReason)
        {
            UpdateFailureCause failureCause = UpdateFailureCause.NoFailure;
            HResult result = this.virtualizationInstance.DeleteFile(relativePath, (UpdateType)updateFlags, out failureCause);
            failureReason = (UpdateFailureReason)failureCause;
            return new FileSystemResult(HResultToFSResult(result), unchecked((int)result));
        }

        public override FileSystemResult WritePlaceholderFile(
            string relativePath,
            long endOfFile,
            string sha)
        {
            FileProperties properties = this.FileSystemCallbacks.GetLogsHeadFileProperties();
            HResult result = this.virtualizationInstance.WritePlaceholderInfo(
                relativePath,
                properties.CreationTimeUTC,
                properties.LastAccessTimeUTC,
                properties.LastWriteTimeUTC,
                changeTime: properties.LastWriteTimeUTC,
                fileAttributes: FileAttributes.Archive,
                endOfFile: endOfFile,
                isDirectory: false,
                contentId: FileSystemVirtualizer.ConvertShaToContentId(sha),
                providerId: PlaceholderVersionId);

            return new FileSystemResult(HResultToFSResult(result), unchecked((int)result));
        }

        public override FileSystemResult WritePlaceholderDirectory(string relativePath)
        {
            FileProperties properties = this.FileSystemCallbacks.GetLogsHeadFileProperties();
            HResult result = this.virtualizationInstance.WritePlaceholderInfo(
                relativePath,
                properties.CreationTimeUTC,
                properties.LastAccessTimeUTC,
                properties.LastWriteTimeUTC,
                changeTime: properties.LastWriteTimeUTC,
                fileAttributes: FileAttributes.Directory,
                endOfFile: 0,
                isDirectory: true,
                contentId: FolderContentId,
                providerId: PlaceholderVersionId);

            return new FileSystemResult(HResultToFSResult(result), unchecked((int)result));
        }

        public override FileSystemResult UpdatePlaceholderIfNeeded(
            string relativePath,
            DateTime creationTime,
            DateTime lastAccessTime,
            DateTime lastWriteTime,
            DateTime changeTime,
            FileAttributes fileAttributes,
            long endOfFile,
            string shaContentId,
            UpdatePlaceholderType updateFlags,
            out UpdateFailureReason failureReason)
        {
            UpdateFailureCause failureCause = UpdateFailureCause.NoFailure;
            HResult result = this.virtualizationInstance.UpdateFileIfNeeded(
                relativePath,
                creationTime,
                lastAccessTime,
                lastWriteTime,
                changeTime,
                fileAttributes,
                endOfFile,
                ConvertShaToContentId(shaContentId),
                PlaceholderVersionId,
                (UpdateType)updateFlags,
                out failureCause);
            failureReason = (UpdateFailureReason)failureCause;
            return new FileSystemResult(HResultToFSResult(result), unchecked((int)result));
        }

        public override FileSystemResult DehydrateFolder(string relativePath)
        {
            // Don't need to do anything here because the parent will reproject the folder.
            return new FileSystemResult(FSResult.Ok, 0);
        }

        // TODO: Need ProjFS 13150199 to be fixed so that GVFS doesn't leak memory if the enumeration cancelled.
        // Currently EndDirectoryEnumerationHandler must be called to remove the ActiveEnumeration from this.activeEnumerations
        public HResult StartDirectoryEnumerationCallback(int commandId, Guid enumerationId, string virtualPath, uint triggeringProcessId, string triggeringProcessImageFileName)
        {
            try
            {
                List projectedItems;
                if (this.FileSystemCallbacks.GitIndexProjection.TryGetProjectedItemsFromMemory(virtualPath, out projectedItems))
                {
                    ActiveEnumeration activeEnumeration = new ActiveEnumeration(projectedItems);
                    if (!this.activeEnumerations.TryAdd(enumerationId, activeEnumeration))
                    {
                        this.Context.Tracer.RelatedError(
                            this.CreateEventMetadata(enumerationId, virtualPath),
                            nameof(this.StartDirectoryEnumerationCallback) + ": Failed to add enumeration ID to active collection");

                        return HResult.InternalError;
                    }

                    return HResult.Ok;
                }

                CancellationTokenSource cancellationSource;
                if (!this.TryRegisterCommand(commandId, out cancellationSource))
                {
                    EventMetadata metadata = this.CreateEventMetadata(enumerationId, virtualPath);
                    metadata.Add("commandId", commandId);
                    this.Context.Tracer.RelatedWarning(metadata, nameof(this.StartDirectoryEnumerationCallback) + ": Failed to register command");
                }

                FileOrNetworkRequest startDirectoryEnumerationHandler = new FileOrNetworkRequest(
                    (blobSizesConnection) => this.StartDirectoryEnumerationAsyncHandler(
                        cancellationSource.Token,
                        blobSizesConnection,
                        commandId,
                        enumerationId,
                        virtualPath),
                    () => cancellationSource.Dispose());

                Exception e;
                if (!this.TryScheduleFileOrNetworkRequest(startDirectoryEnumerationHandler, out e))
                {
                    EventMetadata metadata = this.CreateEventMetadata(virtualPath, e);
                    metadata.Add("commandId", commandId);
                    metadata.Add(TracingConstants.MessageKey.WarningMessage, nameof(this.StartDirectoryEnumerationCallback) + ": Failed to schedule async handler");
                    this.Context.Tracer.RelatedEvent(EventLevel.Warning, nameof(this.StartDirectoryEnumerationCallback) + "_FailedToScheduleAsyncHandler", metadata);

                    cancellationSource.Dispose();

                    return (HResult)HResultExtensions.HResultFromNtStatus.DeviceNotReady;
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(enumerationId, virtualPath, e);
                metadata.Add("commandId", commandId);
                this.LogUnhandledExceptionAndExit(nameof(this.StartDirectoryEnumerationCallback), metadata);
            }

            return HResult.Pending;
        }

        public HResult GetDirectoryEnumerationCallback(
        int commandId,
        Guid enumerationId,
        string filterFileName,
        bool restartScan,
        IDirectoryEnumerationResults results)
        {
            try
            {
                ActiveEnumeration activeEnumeration = null;
                if (!this.activeEnumerations.TryGetValue(enumerationId, out activeEnumeration))
                {
                    EventMetadata metadata = this.CreateEventMetadata(enumerationId);
                    metadata.Add("filterFileName", filterFileName);
                    metadata.Add("restartScan", restartScan);
                    this.Context.Tracer.RelatedError(metadata, nameof(this.GetDirectoryEnumerationCallback) + ": Failed to find active enumeration ID");

                    return HResult.InternalError;
                }

                if (restartScan)
                {
                    activeEnumeration.RestartEnumeration(filterFileName);
                }
                else
                {
                    activeEnumeration.TrySaveFilterString(filterFileName);
                }

                HResult result = HResult.Ok;
                bool entryAdded = false;
                while (activeEnumeration.IsCurrentValid)
                {
                    ProjectedFileInfo fileInfo = activeEnumeration.Current;
                    FileProperties properties = this.FileSystemCallbacks.GetLogsHeadFileProperties();

                    bool addResult = results.Add(
                        fileName: fileInfo.Name,
                        fileSize: fileInfo.IsFolder ? 0 : fileInfo.Size,
                        isDirectory: fileInfo.IsFolder,
                        fileAttributes: fileInfo.IsFolder ? FileAttributes.Directory : FileAttributes.Archive,
                        creationTime: properties.CreationTimeUTC,
                        lastAccessTime: properties.LastAccessTimeUTC,
                        lastWriteTime: properties.LastWriteTimeUTC,
                        changeTime: properties.LastWriteTimeUTC);

                    if (addResult == true)
                    {
                        entryAdded = true;
                        activeEnumeration.MoveNext();
                    }
                    else
                    {
                        if (entryAdded)
                        {
                            result = HResult.Ok;
                        }

                        break;
                    }
                }

                return result;
            }
            catch (Win32Exception e)
            {
                this.Context.Tracer.RelatedWarning(
                    this.CreateEventMetadata(enumerationId, relativePath: null, exception: e),
                    nameof(this.GetDirectoryEnumerationCallback) + " caught Win32Exception");

                return HResultExtensions.HResultFromWin32(e.NativeErrorCode);
            }
            catch (Exception e)
            {
                this.LogUnhandledExceptionAndExit(
                    nameof(this.GetDirectoryEnumerationCallback),
                    this.CreateEventMetadata(enumerationId, relativePath: null, exception: e));

                return HResult.InternalError;
            }
        }

        public HResult EndDirectoryEnumerationCallback(Guid enumerationId)
        {
            try
            {
                ActiveEnumeration activeEnumeration;
                if (!this.activeEnumerations.TryRemove(enumerationId, out activeEnumeration))
                {
                    this.Context.Tracer.RelatedWarning(
                        this.CreateEventMetadata(enumerationId),
                        nameof(this.EndDirectoryEnumerationCallback) + ": Failed to remove enumeration ID from active collection",
                        Keywords.Telemetry);

                    return HResult.InternalError;
                }
            }
            catch (Exception e)
            {
                this.LogUnhandledExceptionAndExit(
                    nameof(this.EndDirectoryEnumerationCallback),
                    this.CreateEventMetadata(enumerationId, relativePath: null, exception: e));
            }

            return HResult.Ok;
        }

        public HResult GetPlaceholderInfoCallback(
        int commandId,
        string virtualPath,
        uint triggeringProcessId,
        string triggeringProcessImageFileName)
        {
            try
            {
                bool isFolder;
                string fileName;
                if (!this.FileSystemCallbacks.GitIndexProjection.IsPathProjected(virtualPath, out fileName, out isFolder))
                {
                    return HResult.FileNotFound;
                }

                if (!isFolder &&
                    !this.IsSpecialGitFile(fileName) &&
                    !this.CanCreatePlaceholder())
                {
                    EventMetadata metadata = this.CreateEventMetadata(virtualPath);
                    metadata.Add("commandId", commandId);
                    metadata.Add("triggeringProcessId", triggeringProcessId);
                    metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                    metadata.Add(TracingConstants.MessageKey.VerboseMessage, $"{nameof(this.GetPlaceholderInfoCallback)}: Not allowed to create placeholder");
                    this.Context.Tracer.RelatedEvent(EventLevel.Verbose, nameof(this.GetPlaceholderInfoCallback), metadata);

                    this.FileSystemCallbacks.OnPlaceholderCreateBlockedForGit();

                    // Another process is modifying the working directory so we cannot modify it
                    // until they are done.
                    return HResult.FileNotFound;
                }

                CancellationTokenSource cancellationSource;
                if (!this.TryRegisterCommand(commandId, out cancellationSource))
                {
                    EventMetadata metadata = this.CreateEventMetadata(virtualPath);
                    metadata.Add("commandId", commandId);
                    metadata.Add("triggeringProcessId", triggeringProcessId);
                    metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                    this.Context.Tracer.RelatedWarning(metadata, nameof(this.GetPlaceholderInfoCallback) + ": Failed to register command");
                }

                FileOrNetworkRequest getPlaceholderInformationHandler = new FileOrNetworkRequest(
                    (blobSizesConnection) => this.GetPlaceholderInformationAsyncHandler(
                        cancellationSource.Token,
                        blobSizesConnection,
                        commandId,
                        virtualPath,
                        triggeringProcessId,
                        triggeringProcessImageFileName),
                    () => cancellationSource.Dispose());

                Exception e;
                if (!this.TryScheduleFileOrNetworkRequest(getPlaceholderInformationHandler, out e))
                {
                    EventMetadata metadata = this.CreateEventMetadata(virtualPath, e);
                    metadata.Add("commandId", commandId);
                    metadata.Add("triggeringProcessId", triggeringProcessId);
                    metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                    metadata.Add(TracingConstants.MessageKey.WarningMessage, nameof(this.GetPlaceholderInfoCallback) + ": Failed to schedule async handler");
                    this.Context.Tracer.RelatedEvent(EventLevel.Warning, nameof(this.GetPlaceholderInfoCallback) + "_FailedToScheduleAsyncHandler", metadata);

                    cancellationSource.Dispose();

                    return (HResult)HResultExtensions.HResultFromNtStatus.DeviceNotReady;
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(virtualPath, e);
                metadata.Add("commandId", commandId);
                metadata.Add("triggeringProcessId", triggeringProcessId);
                metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                this.LogUnhandledExceptionAndExit(nameof(this.GetPlaceholderInfoCallback), metadata);
            }

            return HResult.Pending;
        }

        public HResult GetFileDataCallback(
        int commandId,
        string virtualPath,
        ulong byteOffset,
        uint length,
        Guid streamGuid,
        byte[] contentId,
        byte[] providerId,
        uint triggeringProcessId,
        string triggeringProcessImageFileName)
        {
            try
            {
                if (contentId == null)
                {
                    this.Context.Tracer.RelatedError($"{nameof(this.GetFileDataCallback)} called with null contentId, path: " + virtualPath);
                    return HResult.InternalError;
                }

                if (providerId == null)
                {
                    this.Context.Tracer.RelatedError($"{nameof(this.GetFileDataCallback)} called with null epochId, path: " + virtualPath);
                    return HResult.InternalError;
                }

                string sha = GetShaFromContentId(contentId);
                byte placeholderVersion = GetPlaceholderVersionFromProviderId(providerId);

                EventMetadata metadata = new EventMetadata();
                metadata.Add("originalVirtualPath", virtualPath);
                metadata.Add("byteOffset", byteOffset);
                metadata.Add("length", length);
                metadata.Add("streamGuid", streamGuid);
                metadata.Add("triggeringProcessId", triggeringProcessId);
                metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                metadata.Add("sha", sha);
                metadata.Add("placeholderVersion", placeholderVersion);
                metadata.Add("commandId", commandId);

                if (byteOffset != 0)
                {
                    this.Context.Tracer.RelatedError(metadata, "Invalid Parameter: byteOffset must be 0");
                    return HResult.InternalError;
                }

                if (placeholderVersion != FileSystemVirtualizer.PlaceholderVersion)
                {
                    this.Context.Tracer.RelatedError(metadata, nameof(this.GetFileDataCallback) + ": Unexpected placeholder version");
                    return HResult.InternalError;
                }

                CancellationTokenSource cancellationSource;
                if (!this.TryRegisterCommand(commandId, out cancellationSource))
                {
                    metadata.Add(TracingConstants.MessageKey.WarningMessage, nameof(this.GetFileDataCallback) + ": Failed to register command");
                    this.Context.Tracer.RelatedEvent(EventLevel.Warning, nameof(this.GetFileDataCallback) + "_FailedToRegisterCommand", metadata);
                }

                FileOrNetworkRequest getFileStreamHandler = new FileOrNetworkRequest(
                    (blobSizesConnection) => this.GetFileStreamHandlerAsyncHandler(
                        cancellationSource.Token,
                        commandId,
                        length,
                        streamGuid,
                        sha,
                        metadata,
                        triggeringProcessImageFileName),
                    () =>
                    {
                        cancellationSource.Dispose();
                    });

                Exception e;
                if (!this.TryScheduleFileOrNetworkRequest(getFileStreamHandler, out e))
                {
                    metadata.Add("Exception", e?.ToString());
                    metadata.Add(TracingConstants.MessageKey.WarningMessage, nameof(this.GetFileDataCallback) + ": Failed to schedule async handler");
                    this.Context.Tracer.RelatedEvent(EventLevel.Warning, nameof(this.GetFileDataCallback) + "_FailedToScheduleAsyncHandler", metadata);

                    cancellationSource.Dispose();

                    return (HResult)HResultExtensions.HResultFromNtStatus.DeviceNotReady;
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(virtualPath, e);
                metadata.Add("originalVirtualPath", virtualPath);
                metadata.Add("byteOffset", byteOffset);
                metadata.Add("length", length);
                metadata.Add("streamGuid", streamGuid);
                metadata.Add("triggeringProcessId", triggeringProcessId);
                metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                metadata.Add("commandId", commandId);
                this.LogUnhandledExceptionAndExit(nameof(this.GetFileDataCallback), metadata);
            }

            return HResult.Pending;
        }

        public override bool TryStart(out string error)
        {
            error = string.Empty;

            this.InitializeEnumerationPatternMatcher();

            this.virtualizationInstance.OnNotifyFileOverwritten = this.NotifyFileOverwrittenHandler;
            this.virtualizationInstance.OnNotifyPreCreateHardlink = null;
            this.virtualizationInstance.OnQueryFileName = this.QueryFileNameHandler;
            this.virtualizationInstance.OnNotifyFileOpened = null;
            this.virtualizationInstance.OnNotifyNewFileCreated = this.NotifyNewFileCreatedHandler;
            this.virtualizationInstance.OnNotifyPreDelete = this.NotifyPreDeleteHandler;
            this.virtualizationInstance.OnNotifyPreRename = this.NotifyPreRenameHandler;
            this.virtualizationInstance.OnNotifyFileRenamed = this.NotifyFileRenamedHandler;
            this.virtualizationInstance.OnNotifyHardlinkCreated = this.NotifyHardlinkCreated;
            this.virtualizationInstance.OnNotifyFileHandleClosedNoModification = null;
            this.virtualizationInstance.OnNotifyFileHandleClosedFileModifiedOrDeleted = this.NotifyFileHandleClosedFileModifiedOrDeletedHandler;
            this.virtualizationInstance.OnNotifyFilePreConvertToFull = this.NotifyFilePreConvertToFullHandler;

            this.virtualizationInstance.OnCancelCommand = this.CancelCommandHandler;

            HResult result = this.virtualizationInstance.StartVirtualizing(this);

            if (result != HResult.Ok)
            {
                this.Context.Tracer.RelatedError($"{nameof(this.virtualizationInstance.StartVirtualizing)} failed: " + result.ToString("X") + "(" + result.ToString("G") + ")");
                error = "Failed to start virtualization instance (" + result.ToString() + ")";
                return false;
            }

            return true;
        }

        protected override void OnPossibleTombstoneFolderCreated(string relativePath)
        {
            this.FileSystemCallbacks.OnPossibleTombstoneFolderCreated(relativePath);
        }

        private static void StreamCopyBlockTo(Stream input, Stream destination, long numBytes, byte[] buffer)
        {
            int read;
            while (numBytes > 0)
            {
                int bytesToRead = Math.Min(buffer.Length, (int)numBytes);
                read = input.Read(buffer, 0, bytesToRead);
                if (read <= 0)
                {
                    break;
                }

                destination.Write(buffer, 0, read);
                numBytes -= read;
            }
        }

        private static bool ProjFSPatternMatchingWorks()
        {
            const char DOSQm = '>';
            if (Utils.IsFileNameMatch("Test", "Test" + DOSQm))
            {
                // The installed version of ProjFS has been fixed to handle the special DOS characters
                return true;
            }

            return false;
        }

        private void InitializeEnumerationPatternMatcher()
        {
            bool projFSPatternMatchingWorks = ProjFSPatternMatchingWorks();

            if (projFSPatternMatchingWorks)
            {
                ActiveEnumeration.SetWildcardPatternMatcher(Utils.IsFileNameMatch);
            }
            else
            {
                ActiveEnumeration.SetWildcardPatternMatcher(InternalFileNameMatchesFilter);
            }

            this.Context.Tracer.RelatedEvent(
                EventLevel.Informational,
                nameof(this.InitializeEnumerationPatternMatcher),
                new EventMetadata() { { nameof(projFSPatternMatchingWorks), projFSPatternMatchingWorks } },
                Keywords.Telemetry);
        }

        private bool TryRegisterCommand(int commandId, out CancellationTokenSource cancellationSource)
        {
            cancellationSource = new CancellationTokenSource();
            return this.activeCommands.TryAdd(commandId, cancellationSource);
        }

        private bool TryCompleteCommand(int commandId, HResult result)
        {
            CancellationTokenSource cancellationSource;
            if (this.activeCommands.TryRemove(commandId, out cancellationSource))
            {
                this.virtualizationInstance.CompleteCommand(commandId, result);
                return true;
            }

            return false;
        }

        private void StartDirectoryEnumerationAsyncHandler(
            CancellationToken cancellationToken,
            BlobSizes.BlobSizesConnection blobSizesConnection,
            int commandId,
            Guid enumerationId,
            string virtualPath)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                return;
            }

            HResult result;
            try
            {
                ActiveEnumeration activeEnumeration = new ActiveEnumeration(this.FileSystemCallbacks.GitIndexProjection.GetProjectedItems(cancellationToken, blobSizesConnection, virtualPath));

                if (!this.activeEnumerations.TryAdd(enumerationId, activeEnumeration))
                {
                    this.Context.Tracer.RelatedError(
                        this.CreateEventMetadata(enumerationId, virtualPath),
                        nameof(this.StartDirectoryEnumerationAsyncHandler) + ": Failed to add enumeration ID to active collection");

                    result = HResult.InternalError;
                }
                else
                {
                    result = HResult.Ok;
                }
            }
            catch (OperationCanceledException)
            {
                EventMetadata metadata = this.CreateEventMetadata(virtualPath);
                metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.StartDirectoryEnumerationAsyncHandler) + ": Operation cancelled");
                this.Context.Tracer.RelatedEvent(
                    EventLevel.Informational,
                    nameof(this.StartDirectoryEnumerationAsyncHandler) + "_Cancelled",
                    metadata);

                return;
            }
            catch (SizesUnavailableException e)
            {
                result = (HResult)HResultExtensions.HResultFromNtStatus.FileNotAvailable;

                EventMetadata metadata = this.CreateEventMetadata(enumerationId, virtualPath, e);
                metadata.Add("commandId", commandId);
                metadata.Add(nameof(result), result.ToString("X") + "(" + result.ToString("G") + ")");
                this.Context.Tracer.RelatedError(metadata, nameof(this.StartDirectoryEnumerationAsyncHandler) + ": caught SizesUnavailableException");
            }
            catch (Exception e)
            {
                result = HResult.InternalError;

                EventMetadata metadata = this.CreateEventMetadata(enumerationId, virtualPath, e);
                metadata.Add("commandId", commandId);
                this.LogUnhandledExceptionAndExit(nameof(this.StartDirectoryEnumerationAsyncHandler), metadata);
            }

            if (!this.TryCompleteCommand(commandId, result))
            {
                // Command has already been canceled, and no EndDirectoryEnumeration callback will be received

                EventMetadata metadata = this.CreateEventMetadata(virtualPath);
                metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.StartDirectoryEnumerationAsyncHandler)}: TryCompleteCommand returned false, command already canceled");
                metadata.Add("commandId", commandId);
                metadata.Add("enumerationId", enumerationId);
                metadata.Add(nameof(result), result.ToString("X") + "(" + result.ToString("G") + ")");

                ActiveEnumeration activeEnumeration;
                bool activeEnumerationsUpdated = this.activeEnumerations.TryRemove(enumerationId, out activeEnumeration);
                metadata.Add("activeEnumerationsUpdated", activeEnumerationsUpdated);
                this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.StartDirectoryEnumerationAsyncHandler)}_CommandAlreadyCanceled", metadata);
            }
        }

        /// 
        /// QueryFileNameHandler is called by ProjFS when a file is being deleted or renamed.  It is an optimization so that ProjFS
        /// can avoid calling Start\Get\End enumeration to check if GVFS is still projecting a file.  This method uses the same
        /// rules for deciding what is projected as the enumeration callbacks.
        /// 
        private HResult QueryFileNameHandler(string virtualPath)
        {
            try
            {
                if (FileSystemCallbacks.IsPathInsideDotGit(virtualPath))
                {
                    return HResult.FileNotFound;
                }

                bool isFolder;
                string fileName;
                if (!this.FileSystemCallbacks.GitIndexProjection.IsPathProjected(virtualPath, out fileName, out isFolder))
                {
                    return HResult.FileNotFound;
                }
            }
            catch (Exception e)
            {
                this.LogUnhandledExceptionAndExit(nameof(this.QueryFileNameHandler), this.CreateEventMetadata(virtualPath, e));
            }

            return HResult.Ok;
        }

        private void GetPlaceholderInformationAsyncHandler(
            CancellationToken cancellationToken,
            BlobSizes.BlobSizesConnection blobSizesConnection,
            int commandId,
            string virtualPath,
            uint triggeringProcessId,
            string triggeringProcessImageFileName)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                return;
            }

            HResult result = HResult.Ok;

            try
            {
                ProjectedFileInfo fileInfo;
                string parentFolderPath;
                try
                {
                    fileInfo = this.FileSystemCallbacks.GitIndexProjection.GetProjectedFileInfo(cancellationToken, blobSizesConnection, virtualPath, out parentFolderPath);
                    if (fileInfo == null)
                    {
                        this.TryCompleteCommand(commandId, HResult.FileNotFound);
                        return;
                    }
                }
                catch (OperationCanceledException)
                {
                    EventMetadata metadata = this.CreateEventMetadata(virtualPath);
                    metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.GetPlaceholderInformationAsyncHandler) + ": Operation cancelled");
                    this.Context.Tracer.RelatedEvent(
                        EventLevel.Informational,
                        $"{nameof(this.GetPlaceholderInformationAsyncHandler)}_{nameof(this.FileSystemCallbacks.GitIndexProjection.GetProjectedFileInfo)}_Cancelled",
                        metadata);
                    return;
                }

                // The file name case in the virtualPath parameter might be different than the file name case in the repo.
                // Build a new virtualPath that preserves the case in the repo so that the placeholder file is created
                // with proper case.
                string gitCaseVirtualPath = Path.Combine(parentFolderPath, fileInfo.Name);

                string sha;
                FileSystemResult fileSystemResult;
                if (fileInfo.IsFolder)
                {
                    sha = string.Empty;
                    fileSystemResult = this.WritePlaceholderDirectory(gitCaseVirtualPath);
                }
                else
                {
                    sha = fileInfo.Sha.ToString();
                    fileSystemResult = this.WritePlaceholderFile(gitCaseVirtualPath, fileInfo.Size, sha);
                }

                result = (HResult)fileSystemResult.RawResult;
                if (result != HResult.Ok)
                {
                    EventMetadata metadata = this.CreateEventMetadata(virtualPath);
                    metadata.Add("gitCaseVirtualPath", gitCaseVirtualPath);
                    metadata.Add("triggeringProcessId", triggeringProcessId);
                    metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                    metadata.Add("FileName", fileInfo.Name);
                    metadata.Add("IsFolder", fileInfo.IsFolder);
                    metadata.Add(nameof(sha), sha);
                    metadata.Add(nameof(result), result.ToString("X") + "(" + result.ToString("G") + ")");
                    this.Context.Tracer.RelatedError(metadata, $"{nameof(this.GetPlaceholderInformationAsyncHandler)}: {nameof(this.virtualizationInstance.WritePlaceholderInfo)} failed");
                }
                else
                {
                    if (fileInfo.IsFolder)
                    {
                        this.FileSystemCallbacks.OnPlaceholderFolderCreated(gitCaseVirtualPath, triggeringProcessImageFileName);
                    }
                    else
                    {
                        this.FileSystemCallbacks.OnPlaceholderFileCreated(gitCaseVirtualPath, sha, triggeringProcessImageFileName);
                    }
                }
            }
            catch (SizesUnavailableException e)
            {
                result = (HResult)HResultExtensions.HResultFromNtStatus.FileNotAvailable;

                EventMetadata metadata = this.CreateEventMetadata(virtualPath, e);
                metadata.Add("commandId", commandId);
                metadata.Add("triggeringProcessId", triggeringProcessId);
                metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                metadata.Add(nameof(result), result.ToString("X") + "(" + result.ToString("G") + ")");
                this.Context.Tracer.RelatedError(metadata, nameof(this.GetPlaceholderInformationAsyncHandler) + ": caught SizesUnavailableException");
            }
            catch (Win32Exception e)
            {
                result = HResultExtensions.HResultFromWin32(e.NativeErrorCode);

                EventMetadata metadata = this.CreateEventMetadata(virtualPath, e);
                metadata.Add("commandId", commandId);
                metadata.Add("triggeringProcessId", triggeringProcessId);
                metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                metadata.Add(nameof(result), result.ToString("X") + "(" + result.ToString("G") + ")");
                metadata.Add("NativeErrorCode", e.NativeErrorCode.ToString("X") + "(" + e.NativeErrorCode.ToString("G") + ")");
                this.Context.Tracer.RelatedWarning(metadata, nameof(this.GetPlaceholderInformationAsyncHandler) + ": caught Win32Exception");
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(virtualPath, e);
                metadata.Add("commandId", commandId);
                metadata.Add("triggeringProcessId", triggeringProcessId);
                metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                this.LogUnhandledExceptionAndExit(nameof(this.GetPlaceholderInformationAsyncHandler), metadata);
            }

            this.TryCompleteCommand(commandId, result);
        }

        private void GetFileStreamHandlerAsyncHandler(
            CancellationToken cancellationToken,
            int commandId,
            uint length,
            Guid streamGuid,
            string sha,
            EventMetadata requestMetadata,
            string triggeringProcessImageFileName)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                return;
            }

            try
            {
                if (!this.GitObjects.TryCopyBlobContentStream(
                    sha,
                    cancellationToken,
                    GVFSGitObjects.RequestSource.FileStreamCallback,
                    (stream, blobLength) =>
                    {
                        if (blobLength != length)
                        {
                            requestMetadata.Add("blobLength", blobLength);
                            this.Context.Tracer.RelatedError(requestMetadata, $"{nameof(this.GetFileStreamHandlerAsyncHandler)}: Actual file length (blobLength) does not match requested length");

                            throw new GetFileStreamException(HResult.InternalError);
                        }

                        byte[] buffer = new byte[Math.Min(MaxBlobStreamBufferSize, blobLength)];
                        long remainingData = blobLength;

                        using (IWriteBuffer targetBuffer = this.virtualizationInstance.CreateWriteBuffer((uint)buffer.Length))
                        {
                            while (remainingData > 0)
                            {
                                cancellationToken.ThrowIfCancellationRequested();

                                uint bytesToCopy = (uint)Math.Min(remainingData, targetBuffer.Length);

                                try
                                {
                                    targetBuffer.Stream.Seek(0, SeekOrigin.Begin);
                                    StreamCopyBlockTo(stream, targetBuffer.Stream, bytesToCopy, buffer);
                                }
                                catch (IOException e)
                                {
                                    requestMetadata.Add("Exception", e.ToString());
                                    this.Context.Tracer.RelatedError(requestMetadata, "IOException while copying to unmanaged buffer.");

                                    throw new GetFileStreamException("IOException while copying to unmanaged buffer: " + e.Message, (HResult)HResultExtensions.HResultFromNtStatus.FileNotAvailable);
                                }

                                long writeOffset = length - remainingData;

                                HResult writeResult = this.virtualizationInstance.WriteFileData(streamGuid, targetBuffer, (ulong)writeOffset, bytesToCopy);
                                remainingData -= bytesToCopy;

                                if (writeResult != HResult.Ok)
                                {
                                    switch (writeResult)
                                    {
                                        case HResult.Handle:
                                            // HResult.Handle is expected, and occurs when an application closes a file handle before OnGetFileStream
                                            // is complete
                                            break;

                                        default:
                                            {
                                                this.Context.Tracer.RelatedError(requestMetadata, $"{nameof(this.virtualizationInstance.WriteFileData)} failed, error: " + writeResult.ToString("X") + "(" + writeResult.ToString("G") + ")");
                                            }

                                            break;
                                    }

                                    throw new GetFileStreamException(writeResult);
                                }
                            }
                        }
                    }))
                {
                    this.Context.Tracer.RelatedError(requestMetadata, $"{nameof(this.GetFileStreamHandlerAsyncHandler)}: TryCopyBlobContentStream failed");

                    this.TryCompleteCommand(commandId, (HResult)HResultExtensions.HResultFromNtStatus.FileNotAvailable);
                    return;
                }
            }
            catch (OperationCanceledException)
            {
                requestMetadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.GetFileStreamHandlerAsyncHandler)}: Operation cancelled");
                this.Context.Tracer.RelatedEvent(
                    EventLevel.Informational,
                    nameof(this.GetFileStreamHandlerAsyncHandler) + "_OperationCancelled",
                    requestMetadata);

                return;
            }
            catch (GetFileStreamException e)
            {
                this.TryCompleteCommand(commandId, (HResult)e.HResult);
                return;
            }
            catch (Exception e)
            {
                requestMetadata.Add("Exception", e.ToString());
                this.Context.Tracer.RelatedError(requestMetadata, $"{nameof(this.GetFileStreamHandlerAsyncHandler)}: TryCopyBlobContentStream failed");

                this.TryCompleteCommand(commandId, (HResult)HResultExtensions.HResultFromNtStatus.FileNotAvailable);
                return;
            }

            this.FileSystemCallbacks.OnPlaceholderFileHydrated(triggeringProcessImageFileName);
            this.TryCompleteCommand(commandId, HResult.Ok);
        }

        private void NotifyNewFileCreatedHandler(
            string virtualPath,
            bool isDirectory,
            uint triggeringProcessId,
            string triggeringProcessImageFileName,
            out NotificationType notificationMask)
        {
            notificationMask = NotificationType.UseExistingMask;
            try
            {
                if (!FileSystemCallbacks.IsPathInsideDotGit(virtualPath))
                {
                    if (isDirectory)
                    {
                        GitCommandLineParser gitCommand = new GitCommandLineParser(this.Context.Repository.GVFSLock.GetLockedGitCommand());
                        if (gitCommand.IsValidGitCommand)
                        {
                            // When git recreates a directory that was previously deleted (and is
                            // tracked in ModifiedPaths), skip marking it as a ProjFS placeholder.
                            // Otherwise ProjFS would immediately project all children into it,
                            // conflicting with git's own attempt to populate the directory.
                            //
                            // This check is safe from races with the background task that updates
                            // ModifiedPaths: the deletion happens from a non-git process (e.g.,
                            // rmdir), and IsReadyForExternalAcquireLockRequests() blocks git from
                            // acquiring the GVFS lock until the background queue is drained. When
                            // git itself deletes a folder, the code takes the IsValidGitCommand
                            // path in OnWorkingDirectoryFileOrFolderDeleteNotification and calls
                            // OnPossibleTombstoneFolderCreated instead of OnFolderDeleted, so
                            // ModifiedPaths is not involved.
                            //
                            // See https://github.com/microsoft/VFSForGit/issues/1901
                            if (!this.FileSystemCallbacks.IsPathOrParentInModifiedPaths(virtualPath, isFolder: true))
                            {
                                this.MarkDirectoryAsPlaceholder(virtualPath, triggeringProcessId, triggeringProcessImageFileName);
                            }
                        }
                        else
                        {
                            this.FileSystemCallbacks.OnFolderCreated(virtualPath, out bool sparseFoldersUpdated);
                            if (sparseFoldersUpdated)
                            {
                                // When sparseFoldersUpdated is true it means the folder was previously excluded from the projection and was
                                // included so it needs to be marked as a placeholder so that it will start projecting items in the folder
                                this.MarkDirectoryAsPlaceholder(virtualPath, triggeringProcessId, triggeringProcessImageFileName);
                            }
                        }
                    }
                    else
                    {
                        this.FileSystemCallbacks.OnFileCreated(virtualPath);
                    }
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(virtualPath, e);
                metadata.Add("isDirectory", isDirectory);
                metadata.Add("triggeringProcessId", triggeringProcessId);
                metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                this.LogUnhandledExceptionAndExit(nameof(this.NotifyNewFileCreatedHandler), metadata);
            }
        }

        private void MarkDirectoryAsPlaceholder(
            string virtualPath,
            uint triggeringProcessId,
            string triggeringProcessImageFileName)
        {
            string directoryPath = Path.Combine(this.Context.Enlistment.WorkingDirectoryRoot, virtualPath);
            HResult hr = this.virtualizationInstance.MarkDirectoryAsPlaceholder(
                directoryPath,
                FolderContentId,
                PlaceholderVersionId);

            if (hr == HResult.Ok)
            {
                this.FileSystemCallbacks.OnPlaceholderFolderCreated(virtualPath, triggeringProcessImageFileName);
            }
            else
            {
                EventMetadata metadata = this.CreateEventMetadata(virtualPath);
                metadata.Add("isDirectory", true);
                metadata.Add("triggeringProcessId", triggeringProcessId);
                metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                metadata.Add("HResult", hr.ToString());
                this.Context.Tracer.RelatedError(metadata, nameof(this.MarkDirectoryAsPlaceholder) + " error");
            }
        }

        private void NotifyFileOverwrittenHandler(
            string virtualPath,
            bool isDirectory,
            uint triggeringProcessId,
            string triggeringProcessImageFileName,
            out NotificationType notificationMask)
        {
            notificationMask = NotificationType.UseExistingMask;
            try
            {
                if (!FileSystemCallbacks.IsPathInsideDotGit(virtualPath)
                    && !isDirectory)
                {
                    this.FileSystemCallbacks.OnFileOverwritten(virtualPath);
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(virtualPath, e);
                metadata.Add("isDirectory", isDirectory);
                metadata.Add("triggeringProcessId", triggeringProcessId);
                metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName);
                this.LogUnhandledExceptionAndExit(nameof(this.NotifyFileOverwrittenHandler), metadata);
            }
        }

        private bool NotifyPreRenameHandler(string relativePath, string destinationPath, uint triggeringProcessId, string triggeringProcessImageFileName)
        {
            try
            {
                if (destinationPath.Equals(GVFSConstants.DotGit.Index, GVFSPlatform.Instance.Constants.PathComparison))
                {
                    string lockedGitCommand = this.Context.Repository.GVFSLock.GetLockedGitCommand();
                    if (string.IsNullOrEmpty(lockedGitCommand))
                    {
                        EventMetadata metadata = this.CreateEventMetadata(relativePath);
                        metadata.Add(TracingConstants.MessageKey.WarningMessage, "Blocked index rename outside the lock");
                        this.Context.Tracer.RelatedEvent(EventLevel.Warning, $"{nameof(this.NotifyPreRenameHandler)}_BlockedIndexRename", metadata);

                        return false;
                    }
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(relativePath, e);
                metadata.Add("destinationPath", destinationPath);
                this.LogUnhandledExceptionAndExit(nameof(this.NotifyPreRenameHandler), metadata);
            }

            return true;
        }

        private bool NotifyPreDeleteHandler(string virtualPath, bool isDirectory, uint triggeringProcessId, string triggeringProcessImageFileName)
        {
            // Only the path to the index should be registered for this handler
            return false;
        }

        private void NotifyFileRenamedHandler(
            string virtualPath,
            string destinationPath,
            bool isDirectory,
            uint triggeringProcessId,
            string triggeringProcessImageFileName,
            out NotificationType notificationMask)
        {
            notificationMask = NotificationType.UseExistingMask;
            this.OnFileRenamed(virtualPath, destinationPath, isDirectory);
        }

        private void NotifyHardlinkCreated(
            string relativeExistingFilePath,
            string relativeNewLinkPath,
            uint triggeringProcessId,
            string triggeringProcessImageFileName)
        {
            this.OnHardLinkCreated(relativeExistingFilePath, relativeNewLinkPath);
        }

        private void NotifyFileHandleClosedFileModifiedOrDeletedHandler(
            string virtualPath,
            bool isDirectory,
            bool isFileModified,
            bool isFileDeleted,
            uint triggeringProcessId,
            string triggeringProcessImageFileName)
        {
            try
            {
                bool pathInsideDotGit = FileSystemCallbacks.IsPathInsideDotGit(virtualPath);

                if (isFileModified)
                {
                    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)
                {
                    if (pathInsideDotGit)
                    {
                        this.OnDotGitFileOrFolderDeleted(virtualPath);
                    }
                    else
                    {
                        this.OnWorkingDirectoryFileOrFolderDeleteNotification(virtualPath, isDirectory, isPreDelete: false);
                    }
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(virtualPath, e);
                metadata.Add("isDirectory", isDirectory);
                metadata.Add("isFileModified", isFileModified);
                metadata.Add("isFileDeleted", isFileDeleted);
                this.LogUnhandledExceptionAndExit(nameof(this.NotifyFileHandleClosedFileModifiedOrDeletedHandler), metadata);
            }
        }

        private bool NotifyFilePreConvertToFullHandler(string relativePath, uint triggeringProcessId, string triggeringProcessImageFileName)
        {
            this.OnFilePreConvertToFull(relativePath);
            return true;
        }

        private void CancelCommandHandler(int commandId)
        {
            try
            {
                CancellationTokenSource cancellationSource;
                if (this.activeCommands.TryRemove(commandId, out cancellationSource))
                {
                    try
                    {
                        cancellationSource.Cancel();
                    }
                    catch (ObjectDisposedException)
                    {
                        // Task already completed
                    }
                    catch (AggregateException e)
                    {
                        // An aggregate exception containing all the exceptions thrown by
                        // the registered callbacks on the associated CancellationToken

                        foreach (Exception innerException in e.Flatten().InnerExceptions)
                        {
                            if (!(innerException is OperationCanceledException) && !(innerException is TaskCanceledException))
                            {
                                EventMetadata metadata = this.CreateEventMetadata(relativePath: null, exception: innerException);
                                metadata.Add("commandId", commandId);
                                this.Context.Tracer.RelatedError(metadata, $"{nameof(this.CancelCommandHandler)}: AggregateException while requesting cancellation");
                            }
                        }
                    }
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(relativePath: null, exception: e);
                metadata.Add("commandId", commandId);
                this.LogUnhandledExceptionAndExit(nameof(this.CancelCommandHandler), metadata);
            }
        }

        private class GetFileStreamException : Exception
        {
            public GetFileStreamException(HResult errorCode)
                : this("GetFileStreamException exception, error: " + errorCode.ToString(), errorCode)
            {
            }

            public GetFileStreamException(string message, HResult result)
                : base(message)
            {
                this.HResult = (int)result;
            }
        }

        private class Notifications
        {
            public const NotificationType IndexFile =
                NotificationType.PreRename |
                NotificationType.PreDelete |
                NotificationType.FileRenamed |
                NotificationType.HardlinkCreated |
                NotificationType.FileHandleClosedFileModified;

            public const NotificationType LogsHeadFile =
                NotificationType.FileRenamed |
                NotificationType.HardlinkCreated |
                NotificationType.FileHandleClosedFileModified;

            public const NotificationType ExcludeAndHeadFile =
                NotificationType.FileRenamed |
                NotificationType.HardlinkCreated |
                NotificationType.FileHandleClosedFileDeleted |
                NotificationType.FileHandleClosedFileModified;

            public const NotificationType FilesAndFoldersInRefsHeads =
                NotificationType.FileRenamed |
                NotificationType.HardlinkCreated |
                NotificationType.FileHandleClosedFileDeleted |
                NotificationType.FileHandleClosedFileModified;

            public const NotificationType FilesInWorkingFolder =
                NotificationType.NewFileCreated |
                NotificationType.FileOverwritten |
                NotificationType.FileRenamed |
                NotificationType.HardlinkCreated |
                NotificationType.FileHandleClosedFileDeleted |
                NotificationType.FilePreConvertToFull |
                NotificationType.FileHandleClosedFileModified;

            public const NotificationType FoldersInWorkingFolder =
                NotificationType.NewFileCreated |
                NotificationType.FileRenamed |
                NotificationType.FileHandleClosedFileDeleted;
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/WindowsGitHooksInstaller.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using System;
using System.IO;

namespace GVFS.Platform.Windows
{
    internal static class WindowsGitHooksInstaller
    {
        private const string HooksConfigContentTemplate =
@"########################################################################
#   Automatically generated file, do not modify.
#   See {0} config setting
########################################################################
{1}";

        public static void CreateHookCommandConfig(GVFSContext context, string hookName, string commandHookPath)
        {
            string targetPath = commandHookPath + GVFSConstants.GitConfig.HooksExtension;

            try
            {
                string configSetting = GVFSConstants.GitConfig.HooksPrefix + hookName;
                string mergedHooks = MergeHooks(context, configSetting, hookName);

                string contents = string.Format(HooksConfigContentTemplate, configSetting, mergedHooks);
                Exception ex;
                if (!context.FileSystem.TryWriteTempFileAndRename(targetPath, contents, out ex))
                {
                    throw new RetryableException("Error installing " + targetPath, ex);
                }
            }
            catch (IOException io)
            {
                throw new RetryableException("Error installing " + targetPath, io);
            }
        }

        private static string MergeHooks(GVFSContext context, string configSettingName, string hookName)
        {
            GitProcess configProcess = new GitProcess(context.Enlistment);
            string filename;
            string[] defaultHooksLines = { };

            // Pass false for forceOutsideEnlistment to allow hooks to be configured at the per-repo level
            if (configProcess.TryGetFromConfig(configSettingName, forceOutsideEnlistment: false, value: out filename) && filename != null)
            {
                filename = filename.Trim(' ', '\n');
                defaultHooksLines = File.ReadAllLines(filename);
            }

            return HooksInstaller.MergeHooksData(defaultHooksLines, filename, hookName);
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/WindowsGitInstallation.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using Microsoft.Win32;
using System.IO;

namespace GVFS.Platform.Windows
{
    public class WindowsGitInstallation : IGitInstallation
    {
        private const string GitProcessName = "git.exe";
        private const string GitBinRelativePath = "cmd\\git.exe";
        private const string GitInstallationRegistryKey = "SOFTWARE\\GitForWindows";
        private const string GitInstallationRegistryInstallPathValue = "InstallPath";

        public bool GitExists(string gitBinPath)
        {
            if (!string.IsNullOrWhiteSpace(gitBinPath))
            {
                return File.Exists(gitBinPath);
            }

            return !string.IsNullOrEmpty(GetInstalledGitBinPath());
        }

        public string GetInstalledGitBinPath()
        {
            string gitBinPath = WindowsPlatform.GetStringFromRegistry(GitInstallationRegistryKey, GitInstallationRegistryInstallPathValue);
            if (!string.IsNullOrWhiteSpace(gitBinPath))
            {
                gitBinPath = Path.Combine(gitBinPath, GitBinRelativePath);
                if (File.Exists(gitBinPath))
                {
                    return gitBinPath;
                }
            }

            return null;
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/WindowsPhysicalDiskInfo.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management;

namespace GVFS.Platform.Windows
{
    public class WindowsPhysicalDiskInfo
    {
        private static readonly Dictionary MapBusType = new Dictionary()
        {
            { 0, "unknwon" },
            { 1, "SCSI" },
            { 2, "ATAPI" },
            { 3, "ATA" },
            { 4, "1394" },
            { 5, "SSA" },
            { 6, "FibreChannel" },
            { 7, "USB" },
            { 8, "RAID" },
            { 9, "iSCSI" },
            { 10, "SAS" },
            { 11, "SATA" },
            { 12, "SD" },
            { 13, "MMC" },
            { 14, "Virtual" },
            { 15, "FileBackedVirtual" },
            { 16, "StorageSpaces" },
            { 17, "NVMe" },
        };

        private static readonly Dictionary MapMediaType = new Dictionary()
        {
            { 0, "unspecified" },
            { 3, "HDD" },
            { 4, "SSD" },
            { 5, "SCM" },
        };

        private static readonly Dictionary MapDriveType = new Dictionary()
        {
            { 0, "unknown" },
            { 1, "InvalidRootPath" },
            { 2, "Removable" },
            { 3, "Fixed" },
            { 4, "Remote" },
            { 5, "CDROM" },
            { 6, "RAMDisk" },
        };

        /// 
        /// Get the properties of the drive/volume/partition/physical disk associated
        /// the given pathname.  For example, whether the drive is an SSD or HDD.
        /// 
        /// A dictionary of platform-specific keywords and values.
        public static Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly)
        {
            // Use the WMI APIs to get details about the physical disk associated with the given path.
            // Some of these fields are avilable using normal classes, such as System.IO.DriveInfo:
            // https://msdn.microsoft.com/en-us/library/system.io.driveinfo(v=vs.110).aspx
            //
            // But the lower-level fields, such as the BusType and SpindleSpeed, are not.
            //
            // MSFT_Partition:
            // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830524(v=vs.85).aspx
            //
            // MSFT_Disk:
            // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830493(v=vs.85).aspx
            //
            // MSFT_Volume:
            // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830604(v=vs.85).aspx
            //
            // MSFT_PhysicalDisk:
            // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830532(v=vs.85)
            //
            // An overview of these "classes" can be found here:
            // https://msdn.microsoft.com/en-us/library/hh830612.aspx
            //
            // The map variables defined above are based on property values documented in one of the above APIs.
            // There are helper functions below to convert from ManagementBaseObject values into the map values.
            // These do not do strict validation because the OS can add new values at any time.  For example, the
            // integer code for NVMe bus drives was recently added.  If an unrecognized value is received, the
            // raw integer value is used untranslated.
            //
            // They are accessed via a generic WQL language that is similar to SQL.  See here for an example:
            // https://blogs.technet.microsoft.com/josebda/2014/08/11/sample-c-code-for-using-the-latest-wmi-classes-to-manage-windows-storage/

            Dictionary result = new Dictionary();

            try
            {
                char driveLetter = PathToDriveLetter(path);
                result.Add("DriveLetter", driveLetter.ToString());

                ManagementScope scope = new ManagementScope(@"\\.\root\microsoft\windows\storage");
                scope.Connect();

                DiskSizeStatistics(scope, driveLetter, ref result);

                if (sizeStatsOnly)
                {
                    return result;
                }

                DiskTypeInfo(scope, driveLetter, ref result);
            }
            catch (Exception e)
            {
                result.Add("Error", e.Message);
            }

            return result;
        }

        private static void DiskSizeStatistics(ManagementScope scope, char driveLetter, ref Dictionary result)
        {
            string queryVolumeString = $"SELECT DriveType,FileSystem,FileSystemLabel,Size,SizeRemaining FROM MSFT_Volume WHERE DriveLetter=\"{driveLetter}\"";
            ManagementBaseObject mbo = GetFirstRecord(scope, queryVolumeString);
            if (mbo != null)
            {
                result.Add("VolumeDriveType", GetMapValue(MapDriveType, FetchValue(mbo, "DriveType")));
                result.Add("VolumeFileSystem", FetchValue(mbo, "FileSystem"));
                result.Add("VolumeFileSystemLabel", FetchValue(mbo, "FileSystemLabel"));
                result.Add("VolumeSize", FetchValue(mbo, "Size"));
                result.Add("VolumeSizeRemaining", FetchValue(mbo, "SizeRemaining"));
            }
        }

        private static void DiskTypeInfo(ManagementScope scope, char driveLetter, ref Dictionary result)
        {
            string queryPartitionString = $"SELECT DiskNumber FROM MSFT_Partition WHERE DriveLetter=\"{driveLetter}\"";
            ManagementBaseObject mbo = GetFirstRecord(scope, queryPartitionString);
            if (mbo != null)
            {
                string diskNumber = FetchValue(mbo, "DiskNumber");
                result.Add("DiskNumber", diskNumber);

                if (diskNumber.Length > 0)
                {
                    string queryDiskString = $"SELECT Model,IsBoot,IsSystem,SerialNumber FROM MSFT_Disk WHERE Number=\"{diskNumber}\"";
                    mbo = GetFirstRecord(scope, queryDiskString);
                    if (mbo != null)
                    {
                        result.Add("DiskModel", FetchValue(mbo, "Model"));
                        result.Add("DiskIsSystem", FetchValue(mbo, "IsSystem"));
                        result.Add("DiskIsBoot", FetchValue(mbo, "IsBoot"));
                        result.Add("DiskSerialNumber", FetchValue(mbo, "SerialNumber"));
                    }

                    string queryPhysicalDiskString = $"SELECT MediaType,BusType,SpindleSpeed FROM MSFT_PhysicalDisk WHERE DeviceId=\"{diskNumber}\"";
                    mbo = GetFirstRecord(scope, queryPhysicalDiskString);
                    if (mbo != null)
                    {
                        result.Add("PhysicalMediaType", GetMapValue(MapMediaType, FetchValue(mbo, "MediaType")));
                        result.Add("PhysicalBusType", GetMapValue(MapBusType, FetchValue(mbo, "BusType")));
                        result.Add("PhysicalSpindleSpeed", FetchValue(mbo, "SpindleSpeed"));
                    }
                }
            }
        }

        private static string FetchValue(ManagementBaseObject mbo, string key)
        {
            return (mbo[key] != null) ? mbo[key].ToString().Trim() : string.Empty;
        }

        private static string GetMapValue(Dictionary map, string rawValue)
        {
            return int.TryParse(rawValue, out int key) && map.Keys.Contains(key) ? map[key] : rawValue;
        }

        private static char PathToDriveLetter(string path)
        {
            FileInfo fi = new FileInfo(path);
            string drive = Path.GetPathRoot(fi.FullName);
            if ((drive.Length == 3) && (drive[1] == ':') && (drive[2] == '\\'))
            {
                if ((drive[0] >= 'A') && (drive[0] <= 'Z'))
                {
                    return drive[0];
                }

                if ((drive[0] >= 'a') && (drive[0] <= 'z'))
                {
                    return char.ToUpper(drive[0]);
                }
            }

            // A bogus path or a UNC path.  This should not happen since the path should already
            // have been validated.
            throw new ArgumentException($"Could not map path '{path}' to a drive letter.");
        }

        private static ManagementBaseObject GetFirstRecord(ManagementScope scope, string queryString)
        {
            ObjectQuery q = new ObjectQuery(queryString);
            ManagementObjectSearcher s = new ManagementObjectSearcher(scope, q);

            // Only return the first result.  (There should only be one row returned for each of these queries.)
            return s.Get().Cast().FirstOrDefault();
        }
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/WindowsPlatform.Shared.cs
================================================
using GVFS.Common;
using Microsoft.Win32.SafeHandles;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Principal;

namespace GVFS.Platform.Windows
{
    public partial class WindowsPlatform
    {
        public const string DotGVFSRoot = ".gvfs";
        public const string UpgradeConfirmMessage = "`gvfs upgrade --confirm`";

        private const int StillActive = 259; /* from Win32 STILL_ACTIVE */

        private enum StdHandle
        {
            Stdin = -10,
            Stdout = -11,
            Stderr = -12
        }

        private enum FileType : uint
        {
            Unknown = 0x0000,
            Disk = 0x0001,
            Char = 0x0002,
            Pipe = 0x0003,
            Remote = 0x8000,
        }

        public static bool IsElevatedImplementation()
        {
            using (WindowsIdentity id = WindowsIdentity.GetCurrent())
            {
                return new WindowsPrincipal(id).IsInRole(WindowsBuiltInRole.Administrator);
            }
        }

        public static bool IsProcessActiveImplementation(int processId, bool tryGetProcessById)
        {
            using (SafeFileHandle process = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.QueryLimitedInformation, false, processId))
            {
                if (!process.IsInvalid)
                {
                    uint exitCode;
                    if (NativeMethods.GetExitCodeProcess(process, out exitCode) && exitCode == StillActive)
                    {
                        return true;
                    }
                }
                else if (tryGetProcessById)
                {
                    // The process.IsInvalid may be true when the mount process doesn't have access to call
                    // OpenProcess for the specified processId. Fallback to slow way of finding process.
                    try
                    {
                        Process.GetProcessById(processId);
                        return true;
                    }
                    catch (ArgumentException)
                    {
                        return false;
                    }
                }

                return false;
            }
        }

        public static string GetNamedPipeNameImplementation(string enlistmentRoot)
        {
            return "GVFS_" + enlistmentRoot.ToUpper().Replace(':', '_');
        }

        public static string GetSecureDataRootForGVFSImplementation()
        {
            string envOverride = Environment.GetEnvironmentVariable("GVFS_SECURE_DATA_ROOT");
            if (!string.IsNullOrEmpty(envOverride))
            {
                return envOverride;
            }

            return Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles, Environment.SpecialFolderOption.Create),
                 "GVFS",
                 "ProgramData");
        }

        public static string GetCommonAppDataRootForGVFSImplementation()
        {
            string envOverride = Environment.GetEnvironmentVariable("GVFS_COMMON_APPDATA_ROOT");
            if (!string.IsNullOrEmpty(envOverride))
            {
                return envOverride;
            }

            return Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData, Environment.SpecialFolderOption.Create),
                "GVFS");
        }

        public static string GetLogsDirectoryForGVFSComponentImplementation(string componentName)
        {
            return Path.Combine(
                GetCommonAppDataRootForGVFSImplementation(),
                componentName,
                "Logs");
        }

        public static string GetSecureDataRootForGVFSComponentImplementation(string componentName)
        {
            return Path.Combine(GetSecureDataRootForGVFSImplementation(), componentName);
        }

        public static bool IsConsoleOutputRedirectedToFileImplementation()
        {
            return FileType.Disk == GetFileType(GetStdHandle(StdHandle.Stdout));
        }

        public static bool TryGetGVFSEnlistmentRootImplementation(string directory, out string enlistmentRoot, out string errorMessage)
        {
            enlistmentRoot = null;

            string finalDirectory;
            if (!WindowsFileSystem.TryGetNormalizedPathImplementation(directory, out finalDirectory, out errorMessage))
            {
                return false;
            }

            enlistmentRoot = Paths.GetRoot(finalDirectory, DotGVFSRoot);
            if (enlistmentRoot == null)
            {
                errorMessage = $"Failed to find the root directory for {DotGVFSRoot} in {finalDirectory}";
                return false;
            }

            return true;
        }

        [DllImport("kernel32.dll")]
        private static extern IntPtr GetStdHandle(StdHandle std);

        [DllImport("kernel32.dll")]
        private static extern FileType GetFileType(IntPtr hdl);
    }
}


================================================
FILE: GVFS/GVFS.Platform.Windows/WindowsPlatform.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.Platform.Windows.DiskLayoutUpgrades;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Security.AccessControl;
using System.Security.Principal;
using System.ServiceProcess;
using System.Text;

namespace GVFS.Platform.Windows
{
    public partial class WindowsPlatform : GVFSPlatform
    {
        private const string WindowsVersionRegistryKey = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
        private const string BuildLabRegistryValue = "BuildLab";
        private const string BuildLabExRegistryValue = "BuildLabEx";

        public WindowsPlatform() : base(underConstruction: new UnderConstructionFlags())
        {
        }

        public override IKernelDriver KernelDriver { get; } = new ProjFSFilter();
        public override IGitInstallation GitInstallation { get; } = new WindowsGitInstallation();
        public override IDiskLayoutUpgradeData DiskLayoutUpgrade { get; } = new WindowsDiskLayoutUpgradeData();
        public override IPlatformFileSystem FileSystem { get; } = new WindowsFileSystem();
        public override string Name { get => "Windows"; }
        public override GVFSPlatformConstants Constants { get; } = new WindowsPlatformConstants();

        public override string GVFSConfigPath
        {
            get
            {
                string servicePath = GVFSPlatform.Instance.GetSecureDataRootForGVFSComponent(GVFSConstants.Service.ServiceName);
                string gvfsDirectory = Path.GetDirectoryName(servicePath);

                return Path.Combine(gvfsDirectory, LocalGVFSConfig.FileName);
            }
        }

        /// 
        /// On Windows VFSForGit does not need to use system wide logs to track
        /// installer messages. VFSForGit is able to specifiy a custom installer
        /// log file as a commandline argument to the installer.
        /// 
        public override bool SupportsSystemInstallLog
        {
            get
            {
                return false;
            }
        }

        public static string GetStringFromRegistry(string key, string valueName)
        {
            object value = GetValueFromRegistry(RegistryHive.LocalMachine, key, valueName);
            return value as string;
        }

        public static object GetValueFromRegistry(RegistryHive registryHive, string key, string valueName)
        {
            object value = GetValueFromRegistry(registryHive, key, valueName, RegistryView.Registry64);
            if (value == null)
            {
                value = GetValueFromRegistry(registryHive, key, valueName, RegistryView.Registry32);
            }

            return value;
        }

        public static bool TrySetDWordInRegistry(RegistryHive registryHive, string key, string valueName, uint value)
        {
            RegistryKey localKey = RegistryKey.OpenBaseKey(registryHive, RegistryView.Registry64);
            RegistryKey localKeySub = localKey.OpenSubKey(key, writable: true);

            if (localKeySub == null)
            {
                localKey = RegistryKey.OpenBaseKey(registryHive, RegistryView.Registry32);
                localKeySub = localKey.OpenSubKey(key, writable: true);
            }

            if (localKeySub == null)
            {
                return false;
            }

            localKeySub.SetValue(valueName, value, RegistryValueKind.DWord);
            return true;
        }

        public override string GetOSVersionInformation()
        {
            StringBuilder sb = new StringBuilder();
            try
            {
                string buildLabVersion = GetStringFromRegistry(WindowsVersionRegistryKey, BuildLabRegistryValue);
                sb.AppendFormat($"Windows BuildLab version {buildLabVersion}");
                sb.AppendLine();

                string buildLabExVersion = GetStringFromRegistry(WindowsVersionRegistryKey, BuildLabExRegistryValue);
                sb.AppendFormat($"Windows BuildLabEx version {buildLabExVersion}");
                sb.AppendLine();
            }
            catch (Exception e)
            {
                sb.AppendFormat($"Failed to record Windows version information. Exception: {e}");
            }

            return sb.ToString();
        }

        public override string GetSecureDataRootForGVFS()
        {
            return WindowsPlatform.GetSecureDataRootForGVFSImplementation();
        }

        public override string GetSecureDataRootForGVFSComponent(string componentName)
        {
            return WindowsPlatform.GetSecureDataRootForGVFSComponentImplementation(componentName);
        }

        public override string GetCommonAppDataRootForGVFS()
        {
            return WindowsPlatform.GetCommonAppDataRootForGVFSImplementation();
        }

        public override string GetLogsDirectoryForGVFSComponent(string componentName)
        {
            return WindowsPlatform.GetLogsDirectoryForGVFSComponentImplementation(componentName);
        }

        public override void StartBackgroundVFS4GProcess(ITracer tracer, string programName, string[] args)
        {
            string programArguments = string.Empty;
            try
            {
                programArguments = string.Join(" ", args.Select(arg => arg.Contains(' ') ? "\"" + arg + "\"" : arg));
                ProcessStartInfo processInfo = new ProcessStartInfo(programName, programArguments);
                processInfo.WindowStyle = ProcessWindowStyle.Hidden;

                Process executingProcess = new Process();
                executingProcess.StartInfo = processInfo;
                executingProcess.Start();
            }
            catch (Exception ex)
            {
                EventMetadata metadata = new EventMetadata();
                metadata.Add(nameof(programName), programName);
                metadata.Add(nameof(programArguments), programArguments);
                metadata.Add("Exception", ex.ToString());
                tracer.RelatedError(metadata, "Failed to start background process.");
                throw;
            }
        }

        public override void PrepareProcessToRunInBackground()
        {
            // No additional work required
        }

        public override NamedPipeServerStream CreatePipeByName(string pipeName)
        {
            PipeSecurity security = new PipeSecurity();
            security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, AccessControlType.Allow));
            security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.CreatorOwnerSid, null), PipeAccessRights.FullControl, AccessControlType.Allow));
            security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), PipeAccessRights.FullControl, AccessControlType.Allow));

            NamedPipeServerStream pipe = new NamedPipeServerStream(
                pipeName,
                PipeDirection.InOut,
                NamedPipeServerStream.MaxAllowedServerInstances,
                PipeTransmissionMode.Byte,
                PipeOptions.WriteThrough | PipeOptions.Asynchronous,
                0, // default inBufferSize
                0, // default outBufferSize
                security,
                HandleInheritability.None);

            return pipe;
        }

        public override bool IsElevated()
        {
            return WindowsPlatform.IsElevatedImplementation();
        }

        public override bool IsProcessActive(int processId)
        {
            return WindowsPlatform.IsProcessActiveImplementation(processId, tryGetProcessById: true);
        }

        public override void IsServiceInstalledAndRunning(string name, out bool installed, out bool running)
        {
            ServiceController service = ServiceController.GetServices().FirstOrDefault(s => s.ServiceName.Equals(name, StringComparison.Ordinal));

            installed = service != null;
            running = service != null ? service.Status == ServiceControllerStatus.Running : false;
        }

        public override string GetNamedPipeName(string enlistmentRoot)
        {
            return WindowsPlatform.GetNamedPipeNameImplementation(enlistmentRoot);
        }

        public override string GetGVFSServiceNamedPipeName(string serviceName)
        {
            return serviceName + ".pipe";
        }

        public override void ConfigureVisualStudio(string gitBinPath, ITracer tracer)
        {
            try
            {
                const string GitBinPathEnd = "\\cmd\\git.exe";
                const string VSRegistryKeyRoot = @"Software\Microsoft\VSCommon";
                const string GitVSRegistrySubKey = @"TeamFoundation\GitSourceControl";
                const string GitVSRegistryValueName = "GitPath";

                if (!gitBinPath.EndsWith(GitBinPathEnd))
                {
                    return;
                }

                string regKeyValue = gitBinPath.Substring(0, gitBinPath.Length - GitBinPathEnd.Length);

                /* Get all versions of Visual Studio that exist in the registry at least 15.0.
                 * This attempts to future proof (current version is 17.0), but may need to be
                 * revisited in the future if VS changes how it stores this. */
                var vsVersions = Registry.CurrentUser.OpenSubKey(VSRegistryKeyRoot)
                    ?.GetSubKeyNames()
                    .Where(name => Version.TryParse(name, out var version) && version.Major >= 15)
                    .ToArray() ?? Array.Empty();

                foreach (string version in vsVersions)
                {
                    var registryKeyName = $@"{Registry.CurrentUser.Name}\{VSRegistryKeyRoot}\{version}\{GitVSRegistrySubKey}";
                    Registry.SetValue(registryKeyName, GitVSRegistryValueName, regKeyValue);
                }
            }
            catch (Exception ex)
            {
                EventMetadata metadata = new EventMetadata();
                metadata.Add("Operation", nameof(this.ConfigureVisualStudio));
                metadata.Add("Exception", ex.ToString());
                tracer.RelatedWarning(metadata, "Error while trying to set Visual Studio’s GitSourceControl regkey");
            }
        }

        public override bool TryGetGVFSHooksVersion(out string hooksVersion, out string error)
        {
            error = null;
            hooksVersion = null;
            string hooksPath = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), GVFSPlatform.Instance.Constants.GVFSHooksExecutableName);
            if (hooksPath == null || !File.Exists(hooksPath))
            {
                error = "Could not find " + GVFSPlatform.Instance.Constants.GVFSHooksExecutableName;
                return false;
            }

            FileVersionInfo hooksFileVersionInfo = FileVersionInfo.GetVersionInfo(hooksPath);
            hooksVersion = hooksFileVersionInfo.ProductVersion;
            return true;
        }

        public override bool TryInstallGitCommandHooks(GVFSContext context, string executingDirectory, string hookName, string commandHookPath, out string errorMessage)
        {
            // The GitHooksLoader requires the following setup to invoke a hook:
            //      Copy GithooksLoader.exe to hook-name.exe
            //      Create a text file named hook-name.hooks that lists the applications to execute for the hook, one application per line

            string gitHooksloaderPath = Path.Combine(executingDirectory, GVFSConstants.DotGit.Hooks.LoaderExecutable);
            if (!HooksInstaller.TryHooksInstallationAction(
                () => HooksInstaller.CopyHook(context, gitHooksloaderPath, commandHookPath + GVFSPlatform.Instance.Constants.ExecutableExtension),
                out errorMessage))
            {
                errorMessage = "Failed to copy " + GVFSConstants.DotGit.Hooks.LoaderExecutable + " to " + commandHookPath + GVFSPlatform.Instance.Constants.ExecutableExtension + "\n" + errorMessage;
                return false;
            }

            if (!HooksInstaller.TryHooksInstallationAction(
                () => WindowsGitHooksInstaller.CreateHookCommandConfig(context, hookName, commandHookPath),
                out errorMessage))
            {
                errorMessage = "Failed to create " + commandHookPath + GVFSConstants.GitConfig.HooksExtension + "\n" + errorMessage;
                return false;
            }

            return true;
        }

        public override string GetCurrentUser()
        {
            WindowsIdentity identity = WindowsIdentity.GetCurrent();
            WindowsPrincipal principal = new WindowsPrincipal(identity);
            return identity.User.Value;
        }

        public override string GetUserIdFromLoginSessionId(int sessionId, ITracer tracer)
        {
            using (CurrentUser currentUser = new CurrentUser(tracer, sessionId))
            {
                return currentUser.Identity.User.Value;
            }
        }

        public override string GetSystemInstallerLogPath()
        {
            return null;
        }

        public override Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly) => WindowsPhysicalDiskInfo.GetPhysicalDiskInfo(path, sizeStatsOnly);

        public override bool IsConsoleOutputRedirectedToFile()
        {
            return WindowsPlatform.IsConsoleOutputRedirectedToFileImplementation();
        }

        public override bool IsGitStatusCacheSupported()
        {
            return File.Exists(Path.Combine(GVFSPlatform.Instance.GetSecureDataRootForGVFSComponent(GVFSConstants.Service.ServiceName), GVFSConstants.GitStatusCache.EnableGitStatusCacheTokenFile));
        }

        public override FileBasedLock CreateFileBasedLock(
            PhysicalFileSystem fileSystem,
            ITracer tracer,
            string lockPath)
        {
            return new WindowsFileBasedLock(fileSystem, tracer, lockPath);
        }

        public override bool TryGetGVFSEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage)
        {
            return WindowsPlatform.TryGetGVFSEnlistmentRootImplementation(directory, out enlistmentRoot, out errorMessage);
        }

        public override bool TryGetDefaultLocalCacheRoot(string enlistmentRoot, out string localCacheRoot, out string localCacheRootError)
        {
            string pathRoot;

            try
            {
                pathRoot = Path.GetPathRoot(enlistmentRoot);
            }
            catch (ArgumentException e)
            {
                localCacheRoot = null;
                localCacheRootError = $"Failed to determine the root of '{enlistmentRoot}'): {e.Message}";
                return false;
            }

            if (string.IsNullOrEmpty(pathRoot))
            {
                localCacheRoot = null;
                localCacheRootError = $"Failed to determine the root of '{enlistmentRoot}', path does not contain root directory information";
                return false;
            }

            try
            {
                localCacheRoot = Path.Combine(pathRoot, GVFSConstants.DefaultGVFSCacheFolderName);
                localCacheRootError = null;
                return true;
            }
            catch (ArgumentException e)
            {
                localCacheRoot = null;
                localCacheRootError = $"Failed to build local cache path using root directory '{pathRoot}'): {e.Message}";
                return false;
            }
        }

        public override bool TryKillProcessTree(int processId, out int exitCode, out string error)
        {
            ProcessResult result = ProcessHelper.Run("taskkill", $"/pid {processId} /f /t");
            error = result.Errors;
            exitCode = result.ExitCode;
            return result.ExitCode == 0;
        }

        public override bool TryCopyPanicLogs(string copyToDir, out string error)
        {
            error = null;
            return true;
        }

        private static object GetValueFromRegistry(RegistryHive registryHive, string key, string valueName, RegistryView view)
        {
            RegistryKey localKey = RegistryKey.OpenBaseKey(registryHive, view);
            RegistryKey localKeySub = localKey.OpenSubKey(key);

            object value = localKeySub == null ? null : localKeySub.GetValue(valueName);
            return value;
        }

        public class WindowsPlatformConstants : GVFSPlatformConstants
        {
            public override string ExecutableExtension
            {
                get { return ".exe"; }
            }

            public override string InstallerExtension
            {
                get { return ".exe"; }
            }

            public override bool SupportsUpgradeWhileRunning => false;

            public override string WorkingDirectoryBackingRootPath
            {
                get { return GVFSConstants.WorkingDirectoryRootName; }
            }

            public override string DotGVFSRoot
            {
                get { return WindowsPlatform.DotGVFSRoot; }
            }

            public override string GVFSBinDirectoryPath
            {
                get
                {
                    return Path.Combine(
                        Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
                        this.GVFSBinDirectoryName);
                }
            }

            public override string GVFSBinDirectoryName
            {
                get { return "GVFS"; }
            }

            public override string GVFSExecutableName
            {
                get { return "GVFS" + this.ExecutableExtension; }
            }

            public override HashSet UpgradeBlockingProcesses
            {
                get { return new HashSet(GVFSPlatform.Instance.Constants.PathComparer) { "GVFS", "GVFS.Mount", "git", "ssh-agent", "wish", "bash" }; }
            }

            // Tests show that 250 is the max supported pipe name length
            public override int MaxPipePathLength => 250;

            public override string UpgradeInstallAdviceMessage
            {
                get { return $"When ready, run {this.UpgradeConfirmCommandMessage} from an elevated command prompt."; }
            }

            public override string UpgradeConfirmCommandMessage
            {
                get { return UpgradeConfirmMessage; }
            }

            public override string StartServiceCommandMessage
            {
                get { return $"`sc start GVFS.Service`";  }
            }

            public override string RunUpdateMessage
            {
                get { return $"Run {UpgradeConfirmMessage} from an elevated command prompt."; }
            }

            public override bool CaseSensitiveFileSystem => false;
        }
    }
}


================================================
FILE: GVFS/GVFS.PostIndexChangedHook/GVFS.PostIndexChangedHook.vcxproj
================================================


  
    
      Debug
      x64
    
    
      Release
      x64
    
  
  
    {24D161E9-D1F0-4299-BBD3-5D940BEDD535}
    Win32Proj
    GVFSPostIndexChangedHook
    10.0
    GVFS.PostIndexChangedHook
    GVFS.PostIndexChangedHook
  
  
  
    Application
    true
    v143
    MultiByte
  
  
    Application
    false
    v143
    true
    MultiByte
  
  
  
  
  
  
  
    
  
  
    
  
  
  
    true
  
  
    false
  
  
    
      Use
      Level4
      Disabled
      _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0\ucrt;..\GVFS.NativeHooks.Common;%(AdditionalIncludeDirectories)
      /Zc:__cplusplus
      MultiThreadedDebug
    
    
      Console
      true
      C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64;%(AdditionalLibraryDirectories)
    
    
      $(IntDir)\$(MSBuildProjectName).log
    
    
    
    
      $(GeneratedIncludePath)
    
  
  
    
      Level4
      Use
      MaxSpeed
      true
      true
      NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0\ucrt;..\GVFS.NativeHooks.Common;%(AdditionalIncludeDirectories)
      /Zc:__cplusplus
      MultiThreaded
    
    
      Console
      true
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64;%(AdditionalLibraryDirectories)
    
    
      $(IntDir)\$(MSBuildProjectName).log
    
    
    
    
      $(GeneratedIncludePath)
    
  
  
    
    
    
    
  
  
    
    
    
      Create
      Create
    
  
  
    
  
  
  
  


================================================
FILE: GVFS/GVFS.PostIndexChangedHook/GVFS.PostIndexChangedHook.vcxproj.filters
================================================


  
    
      {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
      cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
    
    
      {93995380-89BD-4b04-88EB-625FBE52EBFB}
      h;hh;hpp;hxx;hm;inl;inc;xsd
    
    
      {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
      rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
    
    
      {dc184179-b81b-462c-bc71-f6735e699448}
    
    
      {19cb5377-2e7a-49d0-b976-a200efb400f4}
    
  
  
    
      Header Files
    
    
      Header Files
    
    
      Header Files
    
    
      Shared Header Files
    
  
  
    
      Source Files
    
    
      Source Files
    
    
      Shared Source Files
    
  
  
    
      Resource Files
    
  


================================================
FILE: GVFS/GVFS.PostIndexChangedHook/main.cpp
================================================
#include "stdafx.h"
#include "common.h"

enum PostIndexChangedErrorReturnCode
{
    ErrorPostIndexChangedProtocol = ReturnCode::LastError + 1,
};

const int PIPE_BUFFER_SIZE = 1024;

// Returns true if GIT_INDEX_FILE refers to a non-canonical (temp) index.
// The canonical index path is $GIT_DIR/index; anything else is a temp
// index that GVFS doesn't need to be notified about.
//
// GIT_DIR is always set by git.exe itself (via xsetenv in setup.c) before
// any hook runs, so it is reliably present. GIT_INDEX_FILE is only present
// when an external caller (script, build tool, etc.) explicitly exports it
// before invoking git, to redirect index operations to a temp file.
static bool IsNonCanonicalIndex()
{
    char *indexFileEnv = NULL;
    size_t indexLen = 0;
    _dupenv_s(&indexFileEnv, &indexLen, "GIT_INDEX_FILE");

    if (indexFileEnv == NULL || indexFileEnv[0] == '\0')
    {
        free(indexFileEnv);
        return false;
    }

    char *gitDirEnv = NULL;
    size_t gitDirLen = 0;
    _dupenv_s(&gitDirEnv, &gitDirLen, "GIT_DIR");

    if (gitDirEnv == NULL || gitDirEnv[0] == '\0')
    {
        // GIT_INDEX_FILE is set but GIT_DIR is not — shouldn't happen
        // inside a hook (git.exe always sets GIT_DIR), but err on the
        // side of correctness: proceed with the notification.
        free(indexFileEnv);
        free(gitDirEnv);
        return false;
    }

    // Build the canonical index path: /index
    std::string canonical(gitDirEnv);
    if (!canonical.empty() && canonical.back() != '\\' && canonical.back() != '/')
        canonical += '\\';
    canonical += "index";

    // Resolve both paths to absolute form so that relative GIT_DIR
    // (e.g. ".git") and absolute GIT_INDEX_FILE compare correctly.
    char canonicalFull[MAX_PATH];
    char actualFull[MAX_PATH];
    DWORD canonLen = GetFullPathNameA(canonical.c_str(), MAX_PATH, canonicalFull, NULL);
    DWORD actualLen = GetFullPathNameA(indexFileEnv, MAX_PATH, actualFull, NULL);

    free(indexFileEnv);
    free(gitDirEnv);

    if (canonLen == 0 || canonLen >= MAX_PATH ||
        actualLen == 0 || actualLen >= MAX_PATH)
    {
        // Path resolution failed — err on the side of correctness.
        return false;
    }

    return _stricmp(actualFull, canonicalFull) != 0;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        die(ReturnCode::InvalidArgCount, "Invalid arguments");
    }

    // Skip notification for non-canonical (temp) index files.
    // Git fires post-index-change for every index write, including temp
    // indexes created via GIT_INDEX_FILE redirect (e.g. read-tree
    // --index-output, git add with a temp index). GVFS only needs to
    // know about changes to the real $GIT_DIR/index.
    if (IsNonCanonicalIndex())
    {
        return 0;
    }

    if (strcmp(argv[1], "1") && strcmp(argv[1], "0"))
    {
        die(PostIndexChangedErrorReturnCode::ErrorPostIndexChangedProtocol, "Invalid value passed for first argument");
    }

    if (strcmp(argv[2], "1") && strcmp(argv[2], "0"))
    {
        die(PostIndexChangedErrorReturnCode::ErrorPostIndexChangedProtocol, "Invalid value passed for second argument");
    }

    DisableCRLFTranslationOnStdPipes();

    PATH_STRING pipeName(GetGVFSPipeName(argv[0]));
    PIPE_HANDLE pipeHandle = CreatePipeToGVFS(pipeName);

    // Construct index changed request message
    // Format:  "PICN|"
    // Example: "PICN|10"
    // Example: "PICN|01"
    // Example: "PICN|00"
    unsigned long bytesWritten;
    const unsigned long messageLength = 8;
    int error = 0;
    char request[messageLength];
    if (snprintf(request, messageLength, "PICN|%s%s", argv[1], argv[2]) != messageLength - 1)
    {
        die(PostIndexChangedErrorReturnCode::ErrorPostIndexChangedProtocol, "Invalid value for message");
    }

    request[messageLength - 1] = 0x03;
    bool success = WriteToPipe(
        pipeHandle,
        request,
        messageLength,
        &bytesWritten,
        &error);

    if (!success || bytesWritten != messageLength)
    {
        die(ReturnCode::PipeWriteFailed, "Failed to write to pipe (%d)\n", error);
    }

    // Allow for 1 extra character in case we need to
    // null terminate the message, and the message
    // is PIPE_BUFFER_SIZE chars long.
    char message[PIPE_BUFFER_SIZE + 1];
    unsigned long bytesRead;
    int lastError;
    success = ReadFromPipe(
        pipeHandle,
        message,
        PIPE_BUFFER_SIZE,
        &bytesRead,
        &lastError);

    if (!success)
    {
        die(ReturnCode::PipeReadFailed, "Read response from pipe failed (%d)\n", lastError);
    }

    if (message[0] != 'S')
    {
        die(ReturnCode::PipeReadFailed, "Read response from pipe failed (%s)\n", message);
    }

    return 0;
}



================================================
FILE: GVFS/GVFS.PostIndexChangedHook/resource.h
================================================
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Version.rc

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        101
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1001
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif


================================================
FILE: GVFS/GVFS.PostIndexChangedHook/stdafx.cpp
================================================
// stdafx.cpp : source file that includes just the standard includes
// GVFS.PostIndexChangedHook.pch will be the pre-compiled header
// stdafx.obj will contain the pre-compiled type information

#include "stdafx.h"

// TODO: reference any additional headers you need in STDAFX.H
// and not in this file


================================================
FILE: GVFS/GVFS.PostIndexChangedHook/stdafx.h
================================================
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//

#pragma once

#ifdef _WIN32
#include "targetver.h"
#include 
#endif

#include 
#include 



// TODO: reference additional headers your program requires here


================================================
FILE: GVFS/GVFS.PostIndexChangedHook/targetver.h
================================================
#pragma once

// Including SDKDDKVer.h defines the highest available Windows platform.

// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.

#include 


================================================
FILE: GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj
================================================


  
    
      Debug
      x64
    
    
      Release
      x64
    
  
  
    {5A6656D5-81C7-472C-9DC8-32D071CB2258}
    Win32Proj
    readobject
    10.0
    GVFS.ReadObjectHook
    GVFS.ReadObjectHook
  
  
  
    Application
    true
    v143
    MultiByte
  
  
    Application
    false
    v143
    true
    MultiByte
  
  
  
  
  
  
  
    
  
  
    
  
  
  
    true
  
  
    false
  
  
    
      Use
      Level4
      Disabled
      _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0\ucrt;..\GVFS.NativeHooks.Common;%(AdditionalIncludeDirectories)
      /Zc:__cplusplus
      MultiThreadedDebug
    
    
      Console
      true
      C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64;%(AdditionalLibraryDirectories)
    
    
      $(IntDir)\$(MSBuildProjectName).log
    
    
    
    
      $(GeneratedIncludePath)
    
  
  
    
      Level4
      Use
      MaxSpeed
      true
      true
      NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0\ucrt;..\GVFS.NativeHooks.Common;%(AdditionalIncludeDirectories)
      /Zc:__cplusplus
      MultiThreaded
    
    
      Console
      true
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64;%(AdditionalLibraryDirectories)
    
    
      $(IntDir)\$(MSBuildProjectName).log
    
    
    
    
      $(GeneratedIncludePath)
    
  
  
    
    
    
    
    
  
  
    
    
    
    
      Create
      Create
    
  
  
    
  
  
  
  


================================================
FILE: GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj.filters
================================================


  
    
      {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
      cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
    
    
      {93995380-89BD-4b04-88EB-625FBE52EBFB}
      h;hh;hpp;hxx;hm;inl;inc;xsd
    
    
      {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
      rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
    
    
      {c3243239-d853-4df9-bdbb-9a4efa72a827}
    
    
      {e6c30bd2-e246-47d9-ad10-137165f4628c}
    
  
  
    
      Header Files
    
    
      Header Files
    
    
      Header Files
    
    
      Header Files
    
    
      Shared Header Files
    
  
  
    
      Source Files
    
    
      Source Files
    
    
      Source Files
    
    
      Shared Source Files
    
  
  
    
      Resource Files
    
  


================================================
FILE: GVFS/GVFS.ReadObjectHook/main.cpp
================================================
// GVFS.ReadObjectHook
//
// When GVFS installs GVFS.ReadObjectHook, it copies the file to
// the .git\hooks folder, and renames the executable to read-object
// read-object is called by git when it fails to find the object it's looking for on disk.
//
// Git and read-object negotiate an interface and capabilities then git issues a "get" command for the missing SHA.
// See Git Documentation/Technical/read-object-protocol.txt for details.
// GVFS.ReadObjectHook decides which GVFS instance to connect to based on its path.
// It then connects to GVFS and asks GVFS to download the requested object (to the .git\objects folder).

#include "stdafx.h"
#include "packet.h"
#include "common.h"

#define MAX_PACKET_LENGTH 512
#define SHA1_LENGTH 40
#define DLO_REQUEST_LENGTH (4 + SHA1_LENGTH + 1)

// Expected response:
// "S\x3" -> Success
// "F\x3" -> Failure
#define DLO_RESPONSE_LENGTH 2

enum ReadObjectHookErrorReturnCode
{
    ErrorReadObjectProtocol = ReturnCode::LastError + 1,
};

int DownloadSHA(PIPE_HANDLE pipeHandle, const char *sha1)
{
    // Construct download request message
    // Format:  "DLO|<40 character SHA>"
    // Example: "DLO|920C34DCDDFC8F07AC4704C8C0D087D6F2095729"
    char request[DLO_REQUEST_LENGTH+1];
    if (snprintf(request, DLO_REQUEST_LENGTH+1, "DLO|%s\x3", sha1) != DLO_REQUEST_LENGTH)
    {
        die(ReturnCode::InvalidSHA, "First argument must be a 40 character SHA, actual value: %s\n", sha1);
    }

    unsigned long bytesWritten;
    int error = 0;
    bool success = WriteToPipe(
        pipeHandle,
        request,
        DLO_REQUEST_LENGTH,
        &bytesWritten,
        &error);

    if (!success || bytesWritten != DLO_REQUEST_LENGTH)
    {
        die(ReturnCode::PipeWriteFailed, "Failed to write to pipe (%d)\n", error);
    }

    char response[DLO_RESPONSE_LENGTH];
    unsigned long totalBytesRead = 0;
    error = 0;
    do
    {
        unsigned long bytesRead = 0;
        success = ReadFromPipe(
            pipeHandle,
            response + totalBytesRead,
            sizeof(response) - (sizeof(char) * totalBytesRead),
            &bytesRead,
            &error);
        totalBytesRead += bytesRead;
    } while (success && totalBytesRead < DLO_RESPONSE_LENGTH);
    
    if (!success)
    {
        die(ReturnCode::PipeReadFailed, "Read response from pipe failed (%d)\n", error);
    }

    return *response == 'S' ? ReturnCode::Success : ReturnCode::FailureToDownload;
}

int main(int, char *argv[])
{
    char packet_buffer[MAX_PACKET_LENGTH];
    size_t len;
    int err;

    DisableCRLFTranslationOnStdPipes();

    packet_txt_read(packet_buffer, sizeof(packet_buffer));
    if (strcmp(packet_buffer, "git-read-object-client")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s
    {
        die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad welcome message\n");
    }

    packet_txt_read(packet_buffer, sizeof(packet_buffer));
    if (strcmp(packet_buffer, "version=1")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s
    {
        die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad version\n");
    }

    if (packet_txt_read(packet_buffer, sizeof(packet_buffer)))
    {
        die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad version end\n");
    }

    packet_txt_write("git-read-object-server");
    packet_txt_write("version=1");
    packet_flush();

    packet_txt_read(packet_buffer, sizeof(packet_buffer));
    if (strcmp(packet_buffer, "capability=get")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s
    {
        die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad capability\n");
    }

    if (packet_txt_read(packet_buffer, sizeof(packet_buffer)))
    {
        die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad capability end\n");
    }

    packet_txt_write("capability=get");
    packet_flush();

    PATH_STRING pipeName(GetGVFSPipeName(argv[0]));

    PIPE_HANDLE pipeHandle = CreatePipeToGVFS(pipeName);

    while (1)
    {
        packet_txt_read(packet_buffer, sizeof(packet_buffer));
        if (strcmp(packet_buffer, "command=get")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s
        {
            die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad command\n");
        }

        len = packet_txt_read(packet_buffer, sizeof(packet_buffer));
        if ((len != SHA1_LENGTH + 5) || strncmp(packet_buffer, "sha1=", 5)) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s
        {
            die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad sha1 in get command\n");
        }

        if (packet_txt_read(packet_buffer, sizeof(packet_buffer)))
        {
            die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad command end\n");
        }

        err = DownloadSHA(pipeHandle, packet_buffer + 5);
        packet_txt_write(err ? "status=error" : "status=success");
        packet_flush();
    }

    // we'll never reach here as the signal to exit is having stdin closed which is handled in packet_bin_read
}


================================================
FILE: GVFS/GVFS.ReadObjectHook/packet.cpp
================================================
#include "stdafx.h"
#include "packet.h"
#include "common.h"

static void set_packet_header(char *buf, const size_t size)
{
	static char hexchar[] = "0123456789abcdef";

#define hex(a) (hexchar[(a) & 15])
	buf[0] = hex(size >> 12);
	buf[1] = hex(size >> 8);
	buf[2] = hex(size >> 4);
	buf[3] = hex(size);
#undef hex
}

const signed char hexval_table[256] = {
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 00-07 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 08-0f */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 10-17 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 18-1f */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 20-27 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 28-2f */
	0,  1,  2,  3,  4,  5,  6,  7,		/* 30-37 */
	8,  9, -1, -1, -1, -1, -1, -1,		/* 38-3f */
	-1, 10, 11, 12, 13, 14, 15, -1,		/* 40-47 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 48-4f */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 50-57 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 58-5f */
	-1, 10, 11, 12, 13, 14, 15, -1,		/* 60-67 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 68-67 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 70-77 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 78-7f */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 80-87 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 88-8f */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 90-97 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* 98-9f */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* a0-a7 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* a8-af */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* b0-b7 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* b8-bf */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* c0-c7 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* c8-cf */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* d0-d7 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* d8-df */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* e0-e7 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* e8-ef */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* f0-f7 */
	-1, -1, -1, -1, -1, -1, -1, -1,		/* f8-ff */
};

static inline unsigned int hexval(unsigned char c)
{
	return hexval_table[c];
}

static inline int hex2chr(const char *s)
{
	int val = hexval(s[0]);
	return (val < 0) ? val : (val << 4) | hexval(s[1]);
}

static int packet_length(const char *packetlen)
{
	int val = hex2chr(packetlen);
	return (val < 0) ? val : (val << 8) | hex2chr(packetlen + 2);
}

static size_t packet_bin_read(void *buf, size_t count, FILE *stream)
{
	char packetlen[4];
	size_t len, ret;

	/* if we timeout waiting for input, exit and git will restart us if needed */
	size_t bytes_read = fread(packetlen, 1, 4, stream);
	if (0 == bytes_read)
	{
		exit(0);
	}
	if (4 != bytes_read)
	{
		die(-1, "invalid packet length");
	}

	len = packet_length(packetlen);
	if (!len)
	{
		return 0;
	}
	if (len < 4)
	{
		die(-1, "protocol error: bad line length character: %.4s", packetlen);
	}
	len -= 4;
	if (len >= count)
	{
		die(-1, "protocol error: bad line length %zu", len);
	}
	ret = fread(buf, 1, len, stream);
	if (ret != len)
	{
		die(-1, "invalid packet (%zu bytes expected; %zu bytes read)", len, ret);
	}

	return len;
}

size_t packet_txt_read(char *buf, size_t count, FILE *stream)
{
	size_t len;
	
	len = packet_bin_read(buf, count, stream);
	if (len && buf[len - 1] == '\n')
	{
		len--;
	}

	buf[len] = 0;
	return len;
}

void packet_txt_write(const char *buf, FILE *stream)
{
	char packetlen[4];
	size_t len, count = strlen(buf);

	set_packet_header(packetlen, count + 5);
	len = fwrite(packetlen, 1, 4, stream);
	if (len != 4)
	{
		die(-1, "error writing packet length");
	}
	len = fwrite(buf, 1, count, stream);
	if (len != count)
	{
		die(-1, "error writing packet");
	}
	len = fwrite("\n", 1, 1, stream);
	if (len != 1)
	{
		die(-1, "error writing packet");
	}
	fflush(stream);
}

void packet_flush(FILE *stream)
{
	size_t len;

	len = fwrite("0000", 1, 4, stream);
	if (len != 4)
	{
		die(-1, "error writing flush packet");
	}
	fflush(stream);
}


================================================
FILE: GVFS/GVFS.ReadObjectHook/packet.h
================================================
#pragma once
#include 

size_t packet_txt_read(char *buf, size_t count, FILE *stream = stdin);
void packet_txt_write(const char *buf, FILE *stream = stdout);
void packet_flush(FILE *stream = stdout);


================================================
FILE: GVFS/GVFS.ReadObjectHook/resource.h
================================================
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Version.rc

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        101
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1001
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif


================================================
FILE: GVFS/GVFS.ReadObjectHook/stdafx.cpp
================================================
// stdafx.cpp : source file that includes just the standard includes
// GVFS.ReadObjectHook.pch will be the pre-compiled header
// stdafx.obj will contain the pre-compiled type information

#include "stdafx.h"

// TODO: reference any additional headers you need in STDAFX.H
// and not in this file


================================================
FILE: GVFS/GVFS.ReadObjectHook/stdafx.h
================================================
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//

#pragma once

#ifdef _WIN32
#include "targetver.h"
#include 
#endif

#include 
#include 


================================================
FILE: GVFS/GVFS.ReadObjectHook/targetver.h
================================================
#pragma once

// Including SDKDDKVer.h defines the highest available Windows platform.

// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.

#include 


================================================
FILE: GVFS/GVFS.Service/Configuration.cs
================================================
using GVFS.Common;
using System.IO;

namespace GVFS.Service
{
    public class Configuration
    {
        private static Configuration instance = new Configuration();
        private static string assemblyPath = null;

        private Configuration()
        {
            this.GVFSLocation = Path.Combine(AssemblyPath, GVFSPlatform.Instance.Constants.GVFSExecutableName);
        }

        public static Configuration Instance
        {
            get
            {
                return instance;
            }
        }

        public static string AssemblyPath
        {
            get
            {
                if (assemblyPath == null)
                {
                    assemblyPath = ProcessHelper.GetCurrentProcessLocation();
                }

                return assemblyPath;
            }
        }

        public string GVFSLocation { get; private set; }
    }
}


================================================
FILE: GVFS/GVFS.Service/GVFS.Service.csproj
================================================


  
    Exe
    net471
    true
  

  
    
  

  
    
    
  

  
    
  




================================================
FILE: GVFS/GVFS.Service/GVFSMountProcess.cs
================================================
using GVFS.Common;
using GVFS.Common.Tracing;
using GVFS.Platform.Windows;
using GVFS.Service.Handlers;

namespace GVFS.Service
{
    public class GVFSMountProcess : IRepoMounter
    {
        private readonly ITracer tracer;

        public GVFSMountProcess(ITracer tracer)
        {
            this.tracer = tracer;
        }

        public bool MountRepository(string repoRoot, int sessionId)
        {
            if (!ProjFSFilter.IsServiceRunning(this.tracer))
            {
                string error;
                if (!EnableAndAttachProjFSHandler.TryEnablePrjFlt(this.tracer, out error))
                {
                    this.tracer.RelatedError($"{nameof(this.MountRepository)}: Could not enable PrjFlt: {error}");
                    return false;
                }
            }

            using (CurrentUser currentUser = new CurrentUser(this.tracer, sessionId))
            {
                if (!this.CallGVFSMount(repoRoot, currentUser))
                {
                    this.tracer.RelatedError($"{nameof(this.MountRepository)}: Unable to start the GVFS.exe process.");
                    return false;
                }

                string errorMessage;
                string pipeName = GVFSPlatform.Instance.GetNamedPipeName(repoRoot);
                string worktreeError;
                GVFSEnlistment.WorktreeInfo wtInfo = GVFSEnlistment.TryGetWorktreeInfo(repoRoot, out worktreeError);
                if (worktreeError != null)
                {
                    this.tracer.RelatedError($"Failed to check worktree status for '{repoRoot}': {worktreeError}");
                    return false;
                }

                if (wtInfo?.SharedGitDir != null)
                {
                    string enlistmentRoot = wtInfo.GetEnlistmentRoot();
                    if (enlistmentRoot != null)
                    {
                        pipeName = GVFSPlatform.Instance.GetNamedPipeName(enlistmentRoot) + wtInfo.PipeSuffix;
                    }
                }

                if (!GVFSEnlistment.WaitUntilMounted(this.tracer, pipeName, repoRoot, false, out errorMessage))
                {
                    this.tracer.RelatedError(errorMessage);
                    return false;
                }
            }

            return true;
        }

        private bool CallGVFSMount(string repoRoot, CurrentUser currentUser)
        {
            InternalVerbParameters mountInternal = new InternalVerbParameters(startedByService: true);
            return currentUser.RunAs(
                Configuration.Instance.GVFSLocation,
                $"mount {repoRoot} --{GVFSConstants.VerbParameters.InternalUseOnly} {mountInternal.ToJson()}");
        }
    }
}


================================================
FILE: GVFS/GVFS.Service/GVFSService.Windows.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Platform.Windows;
using GVFS.Service.Handlers;
using System;
using System.IO;
using System.Linq;
using System.Security.AccessControl;
using System.ServiceProcess;
using System.Threading;

namespace GVFS.Service
{
    public class GVFSService : ServiceBase
    {
        private const string ServiceNameArgPrefix = "--servicename=";
        private const string EtwArea = nameof(GVFSService);

        private JsonTracer tracer;
        private Thread serviceThread;
        private ManualResetEvent serviceStopped;
        private string serviceName;
        private string serviceDataLocation;
        private RepoRegistry repoRegistry;
        private WindowsRequestHandler requestHandler;
        private INotificationHandler notificationHandler;

        public GVFSService(JsonTracer tracer)
        {
            this.tracer = tracer;
            this.serviceName = GVFSConstants.Service.ServiceName;
            this.CanHandleSessionChangeEvent = true;
            this.notificationHandler = new NotificationHandler(tracer);
        }

        public void Run()
        {
            try
            {
                EventMetadata metadata = new EventMetadata();
                metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion());
                this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(GVFSService)}_{nameof(this.Run)}", metadata);

                this.repoRegistry = new RepoRegistry(
                    this.tracer,
                    new PhysicalFileSystem(),
                    Path.Combine(GVFSPlatform.Instance.GetCommonAppDataRootForGVFS(), this.serviceName),
                    new GVFSMountProcess(this.tracer),
                    this.notificationHandler);
                this.repoRegistry.Upgrade();
                this.requestHandler = new WindowsRequestHandler(this.tracer, EtwArea, this.repoRegistry);

                string pipeName = GVFSPlatform.Instance.GetGVFSServiceNamedPipeName(this.serviceName);
                this.tracer.RelatedInfo("Starting pipe server with name: " + pipeName);

                using (NamedPipeServer pipeServer = NamedPipeServer.StartNewServer(
                    pipeName,
                    this.tracer,
                    this.requestHandler.HandleRequest))
                {
                    this.CheckEnableGitStatusCacheTokenFile();

                    using (ITracer activity = this.tracer.StartActivity("EnsurePrjFltHealthy", EventLevel.Informational))
                    {
                        // Make a best-effort to enable PrjFlt. Continue even if it fails.
                        // This will be tried again when user attempts to mount an enlistment.
                        string error;
                        EnableAndAttachProjFSHandler.TryEnablePrjFlt(activity, out error);
                    }

                    this.serviceStopped.WaitOne();
                }
            }
            catch (Exception e)
            {
                this.LogExceptionAndExit(e, nameof(this.Run));
            }
        }

        public void StopRunning()
        {
            if (this.serviceStopped == null)
            {
                return;
            }

            try
            {
                if (this.tracer != null)
                {
                    this.tracer.RelatedInfo("Stopping");
                }

                if (this.serviceStopped != null)
                {
                    this.serviceStopped.Set();
                }

                if (this.serviceThread != null)
                {
                    this.serviceThread.Join();
                    this.serviceThread = null;

                    if (this.serviceStopped != null)
                    {
                        this.serviceStopped.Dispose();
                        this.serviceStopped = null;
                    }
                }
            }
            catch (Exception e)
            {
                this.LogExceptionAndExit(e, nameof(this.StopRunning));
            }
        }

        protected override void OnSessionChange(SessionChangeDescription changeDescription)
        {
            try
            {
                base.OnSessionChange(changeDescription);

                if (!GVFSEnlistment.IsUnattended(tracer: null))
                {
                    if (changeDescription.Reason == SessionChangeReason.SessionLogon)
                    {
                        this.tracer.RelatedInfo("SessionLogon detected, sessionId: {0}", changeDescription.SessionId);

                        using (ITracer activity = this.tracer.StartActivity("LogonAutomount", EventLevel.Informational))
                        {
                            this.repoRegistry.AutoMountRepos(
                                GVFSPlatform.Instance.GetUserIdFromLoginSessionId(changeDescription.SessionId, this.tracer),
                                changeDescription.SessionId);
                            this.repoRegistry.TraceStatus();
                        }
                    }
                    else if (changeDescription.Reason == SessionChangeReason.SessionLogoff)
                    {
                        this.tracer.RelatedInfo("SessionLogoff detected");
                    }
                }
            }
            catch (Exception e)
            {
                this.LogExceptionAndExit(e, nameof(this.OnSessionChange));
            }
        }

        public void RunInConsoleMode(string[] args)
        {
            if (this.serviceThread != null)
            {
                throw new InvalidOperationException("Cannot start service twice in a row.");
            }

            string serviceName = args.FirstOrDefault(arg => arg.StartsWith(ServiceNameArgPrefix));
            if (serviceName != null)
            {
                this.serviceName = serviceName.Substring(ServiceNameArgPrefix.Length);
            }

            string serviceLogsDirectoryPath = GVFSPlatform.Instance.GetLogsDirectoryForGVFSComponent(this.serviceName);

            Directory.CreateDirectory(serviceLogsDirectoryPath);
            this.tracer.AddLogFileEventListener(
                GVFSEnlistment.GetNewGVFSLogFileName(serviceLogsDirectoryPath, GVFSConstants.LogFileTypes.Service),
                EventLevel.Verbose,
                Keywords.Any);

            try
            {
                this.serviceDataLocation = GVFSPlatform.Instance.GetSecureDataRootForGVFSComponent(this.serviceName);
                Directory.CreateDirectory(this.serviceDataLocation);
                Directory.CreateDirectory(Path.GetDirectoryName(this.serviceDataLocation));

                this.serviceStopped = new ManualResetEvent(false);

                Console.WriteLine($"GVFS.Service running in console mode as '{this.serviceName}'");
                Console.WriteLine("Press Ctrl+C to stop.");

                Console.CancelKeyPress += (sender, e) =>
                {
                    e.Cancel = true;
                    this.StopRunning();
                };

                this.Run();
            }
            catch (Exception e)
            {
                this.tracer.RelatedError($"Console mode failed: {e}");
                throw;
            }
        }

        protected override void OnStart(string[] args)
        {
            if (this.serviceThread != null)
            {
                throw new InvalidOperationException("Cannot start service twice in a row.");
            }

            // TODO: 865304 Used for functional tests and development only. Replace with a smarter appConfig-based solution
            string serviceName = args.FirstOrDefault(arg => arg.StartsWith(ServiceNameArgPrefix));
            if (serviceName != null)
            {
                this.serviceName = serviceName.Substring(ServiceNameArgPrefix.Length);
            }

            string serviceLogsDirectoryPath = GVFSPlatform.Instance.GetLogsDirectoryForGVFSComponent(this.serviceName);

            // Create the logs directory explicitly *before* creating a log file event listener to ensure that it
            // and its ancestor directories are created with the correct ACLs.
            this.CreateServiceLogsDirectory(serviceLogsDirectoryPath);
            this.tracer.AddLogFileEventListener(
                GVFSEnlistment.GetNewGVFSLogFileName(serviceLogsDirectoryPath, GVFSConstants.LogFileTypes.Service),
                EventLevel.Verbose,
                Keywords.Any);

            try
            {
                this.serviceDataLocation = GVFSPlatform.Instance.GetSecureDataRootForGVFSComponent(this.serviceName);
                this.CreateAndConfigureProgramDataDirectories();
                this.Start();
            }
            catch (Exception e)
            {
                this.LogExceptionAndExit(e, nameof(this.OnStart));
            }
        }

        protected override void OnStop()
        {
            try
            {
                this.StopRunning();
            }
            catch (Exception e)
            {
                this.LogExceptionAndExit(e, nameof(this.OnStart));
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                this.StopRunning();

                if (this.tracer != null)
                {
                    this.tracer.Dispose();
                    this.tracer = null;
                }
            }

            base.Dispose(disposing);
        }

        private void Start()
        {
            if (this.serviceStopped != null)
            {
                return;
            }

            this.serviceStopped = new ManualResetEvent(false);
            this.serviceThread = new Thread(this.Run);

            this.serviceThread.Start();
        }

        /// 
        /// 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(GVFSPlatform.Instance.GetSecureDataRootForGVFSComponent(GVFSConstants.Service.ServiceName), GVFSConstants.GitStatusCache.EnableGitStatusCacheTokenFile);
                if (File.Exists(statusCacheVersionTokenPath))
                {
                    this.tracer.RelatedInfo($"CheckEnableGitStatusCache: EnableGitStatusCacheToken file already exists at {statusCacheVersionTokenPath}.");
                    return;
                }

                DateTime lastRebootTime = NativeMethods.GetLastRebootTime();

                // GitStatusCache was included with GVFS on disk version 16. The 1st time GVFS that is at or above on disk version
                // is installed, it will write out a file indicating that the installation is "OnDiskVersion16Capable".
                // We can query the properties of this file to get the installation time, and compare this with the last reboot time for
                // this machine.
                string fileToCheck = Path.Combine(Configuration.AssemblyPath, GVFSConstants.InstallationCapabilityFiles.OnDiskVersion16CapableInstallation);

                if (File.Exists(fileToCheck))
                {
                    DateTime installTime = File.GetCreationTime(fileToCheck);
                    if (lastRebootTime > installTime)
                    {
                        this.tracer.RelatedInfo($"CheckEnableGitStatusCache: Writing out EnableGitStatusCacheToken file. GVFS installation time: {installTime}, last Reboot time: {lastRebootTime}.");
                        File.WriteAllText(statusCacheVersionTokenPath, string.Empty);
                    }
                    else
                    {
                        this.tracer.RelatedInfo($"CheckEnableGitStatusCache: Not writing EnableGitStatusCacheToken file - machine has not been rebooted since OnDiskVersion16Capable installation. GVFS installation time: {installTime}, last reboot time: {lastRebootTime}");
                    }
                }
                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(this.CheckEnableGitStatusCacheTokenFile)}: Unable to determine GVFS installation time or write EnableGitStatusCacheToken file due to exception. Exception: {ex.ToString()}");
            }
        }

        private void LogExceptionAndExit(Exception e, string method)
        {
            EventMetadata metadata = new EventMetadata();
            metadata.Add("Area", EtwArea);
            metadata.Add("Exception", e.ToString());
            this.tracer.RelatedError(metadata, "Unhandled exception in " + method);
            Environment.Exit((int)ReturnCode.GenericError);
        }

        private void CreateServiceLogsDirectory(string serviceLogsDirectoryPath)
        {
            if (!Directory.Exists(serviceLogsDirectoryPath))
            {
                DirectorySecurity serviceDataRootSecurity = this.GetServiceDirectorySecurity(serviceLogsDirectoryPath);
                Directory.CreateDirectory(serviceLogsDirectoryPath);
            }
        }

        private void CreateAndConfigureProgramDataDirectories()
        {
            string serviceDataRootPath = Path.GetDirectoryName(this.serviceDataLocation);

            DirectorySecurity serviceDataRootSecurity = this.GetServiceDirectorySecurity(serviceDataRootPath);

            // Create GVFS.Service related directories (if they don't already exist)
            Directory.CreateDirectory(serviceDataRootPath, serviceDataRootSecurity);
            Directory.CreateDirectory(this.serviceDataLocation, serviceDataRootSecurity);

            // Ensure the ACLs are set correctly on any files or directories that were already created (e.g. after upgrading VFS4G)
            Directory.SetAccessControl(serviceDataRootPath, serviceDataRootSecurity);
        }

        private void CreateAndConfigureLogDirectory(string path)
        {
            string error;
            if (!GVFSPlatform.Instance.FileSystem.TryCreateDirectoryWithAdminAndUserModifyPermissions(path, out error))
            {
                EventMetadata metadata = new EventMetadata();
                metadata.Add("Area", EtwArea);
                metadata.Add(nameof(path), path);
                metadata.Add(nameof(error), error);
                this.tracer.RelatedWarning(
                    metadata,
                    $"{nameof(this.CreateAndConfigureLogDirectory)}: Failed to create logs directory",
                    Keywords.Telemetry);
            }
        }

        private DirectorySecurity GetServiceDirectorySecurity(string serviceDataRootPath)
        {
            DirectorySecurity serviceDataRootSecurity;
            if (Directory.Exists(serviceDataRootPath))
            {
                this.tracer.RelatedInfo($"{nameof(this.GetServiceDirectorySecurity)}: {serviceDataRootPath} exists, modifying ACLs.");
                serviceDataRootSecurity = Directory.GetAccessControl(serviceDataRootPath);
            }
            else
            {
                this.tracer.RelatedInfo($"{nameof(this.GetServiceDirectorySecurity)}: {serviceDataRootPath} does not exist, creating new ACLs.");
                serviceDataRootSecurity = new DirectorySecurity();
            }

            // Protect the access rules from inheritance and remove any inherited rules
            serviceDataRootSecurity.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);

            // Remove any existing ACLs and add new ACLs for users and admins
            WindowsFileSystem.RemoveAllFileSystemAccessRulesFromDirectorySecurity(serviceDataRootSecurity);
            WindowsFileSystem.AddUsersAccessRulesToDirectorySecurity(serviceDataRootSecurity, grantUsersModifyPermissions: false);
            WindowsFileSystem.AddAdminAccessRulesToDirectorySecurity(serviceDataRootSecurity);

            return serviceDataRootSecurity;
        }

    }
}


================================================
FILE: GVFS/GVFS.Service/Handlers/EnableAndAttachProjFSHandler.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Platform.Windows;

namespace GVFS.Service.Handlers
{
    public class EnableAndAttachProjFSHandler : MessageHandler
    {
        private const string EtwArea = nameof(EnableAndAttachProjFSHandler);

        private static object enablePrjFltLock = new object();

        private NamedPipeServer.Connection connection;
        private NamedPipeMessages.EnableAndAttachProjFSRequest request;
        private ITracer tracer;

        public EnableAndAttachProjFSHandler(
            ITracer tracer,
            NamedPipeServer.Connection connection,
            NamedPipeMessages.EnableAndAttachProjFSRequest request)
        {
            this.tracer = tracer;
            this.connection = connection;
            this.request = request;
        }

        public static bool TryEnablePrjFlt(ITracer tracer, out string error)
        {
            error = null;
            EventMetadata prjFltHealthMetadata = new EventMetadata();
            prjFltHealthMetadata.Add("Area", EtwArea);

            PhysicalFileSystem fileSystem = new PhysicalFileSystem();

            lock (enablePrjFltLock)
            {
                bool isPrjfltServiceInstalled;
                bool isPrjfltDriverInstalled;
                bool isNativeProjFSLibInstalled;
                bool isPrjfltServiceRunning = ProjFSFilter.IsServiceRunningAndInstalled(tracer, fileSystem, out isPrjfltServiceInstalled, out isPrjfltDriverInstalled, out isNativeProjFSLibInstalled);

                prjFltHealthMetadata.Add($"Initial_{nameof(isPrjfltDriverInstalled)}", isPrjfltDriverInstalled);
                prjFltHealthMetadata.Add($"Initial_{nameof(isPrjfltServiceInstalled)}", isPrjfltServiceInstalled);
                prjFltHealthMetadata.Add($"Initial_{nameof(isPrjfltServiceRunning)}", isPrjfltServiceRunning);
                prjFltHealthMetadata.Add($"Initial_{nameof(isNativeProjFSLibInstalled)}", isNativeProjFSLibInstalled);

                if (!isPrjfltServiceRunning)
                {
                    if (!isPrjfltServiceInstalled || !isPrjfltDriverInstalled)
                    {
                        uint windowsBuildNumber;
                        bool isInboxProjFSFinalAPI;
                        bool isProjFSFeatureAvailable;
                        if (ProjFSFilter.TryEnableOrInstallDriver(tracer, fileSystem, out windowsBuildNumber, out isInboxProjFSFinalAPI, out isProjFSFeatureAvailable))
                        {
                            isPrjfltServiceInstalled = true;
                            isPrjfltDriverInstalled = true;
                        }
                        else
                        {
                            error = "Failed to install (or enable) PrjFlt";
                            tracer.RelatedError($"{nameof(TryEnablePrjFlt)}: {error}");
                        }

                        prjFltHealthMetadata.Add(nameof(windowsBuildNumber), windowsBuildNumber);
                        prjFltHealthMetadata.Add(nameof(isInboxProjFSFinalAPI), isInboxProjFSFinalAPI);
                        prjFltHealthMetadata.Add(nameof(isProjFSFeatureAvailable), isProjFSFeatureAvailable);
                    }

                    if (isPrjfltServiceInstalled)
                    {
                        if (ProjFSFilter.TryStartService(tracer))
                        {
                            isPrjfltServiceRunning = true;
                        }
                        else
                        {
                            error = "Failed to start prjflt service";
                            tracer.RelatedError($"{nameof(TryEnablePrjFlt)}: {error}");
                        }
                    }
                }

                // Check again if the native library is installed.  If the code above enabled the
                // ProjFS optional feature then the native library will now be installed.
                isNativeProjFSLibInstalled = ProjFSFilter.IsNativeLibInstalled(tracer, fileSystem);
                if (!isNativeProjFSLibInstalled)
                {
                    if (isPrjfltServiceRunning)
                    {
                        tracer.RelatedInfo($"{nameof(TryEnablePrjFlt)}: Native ProjFS library is not installed, attempting to copy version packaged with VFS for Git");

                        EventLevel eventLevel;
                        EventMetadata copyNativeLibMetadata = new EventMetadata();
                        copyNativeLibMetadata.Add("Area", EtwArea);
                        string copyNativeDllError = string.Empty;
                        if (ProjFSFilter.TryCopyNativeLibIfDriverVersionsMatch(tracer, new PhysicalFileSystem(), out copyNativeDllError))
                        {
                            isNativeProjFSLibInstalled = true;

                            eventLevel = EventLevel.Warning;
                            copyNativeLibMetadata.Add(TracingConstants.MessageKey.WarningMessage, $"{nameof(TryEnablePrjFlt)}: Successfully copied ProjFS native library");
                        }
                        else
                        {
                            error = $"Native ProjFS library is not installed and could not be copied: {copyNativeDllError}";

                            eventLevel = EventLevel.Error;
                            copyNativeLibMetadata.Add(nameof(copyNativeDllError), copyNativeDllError);
                            copyNativeLibMetadata.Add(TracingConstants.MessageKey.ErrorMessage, $"{nameof(TryEnablePrjFlt)}: Failed to copy ProjFS native library");
                        }

                        copyNativeLibMetadata.Add(nameof(isNativeProjFSLibInstalled), isNativeProjFSLibInstalled);
                        tracer.RelatedEvent(
                            eventLevel,
                            $"{nameof(TryEnablePrjFlt)}_{nameof(ProjFSFilter.TryCopyNativeLibIfDriverVersionsMatch)}",
                            copyNativeLibMetadata,
                            Keywords.Telemetry);
                    }
                    else
                    {
                        error = "Native ProjFS library is not installed, did not attempt to copy library because prjflt service is not running";
                        tracer.RelatedError($"{nameof(TryEnablePrjFlt)}: {error}");
                    }
                }

                bool isAutoLoggerEnabled = ProjFSFilter.IsAutoLoggerEnabled(tracer);
                prjFltHealthMetadata.Add($"Initial_{nameof(isAutoLoggerEnabled)}", isAutoLoggerEnabled);

                if (!isAutoLoggerEnabled)
                {
                    if (ProjFSFilter.TryEnableAutoLogger(tracer))
                    {
                        isAutoLoggerEnabled = true;
                    }
                    else
                    {
                        tracer.RelatedError($"{nameof(TryEnablePrjFlt)}: Failed to enable prjflt AutoLogger");
                    }
                }

                prjFltHealthMetadata.Add(nameof(isPrjfltDriverInstalled), isPrjfltDriverInstalled);
                prjFltHealthMetadata.Add(nameof(isPrjfltServiceInstalled), isPrjfltServiceInstalled);
                prjFltHealthMetadata.Add(nameof(isPrjfltServiceRunning), isPrjfltServiceRunning);
                prjFltHealthMetadata.Add(nameof(isNativeProjFSLibInstalled), isNativeProjFSLibInstalled);
                prjFltHealthMetadata.Add(nameof(isAutoLoggerEnabled), isAutoLoggerEnabled);
                tracer.RelatedEvent(EventLevel.Informational, $"{nameof(TryEnablePrjFlt)}_Summary", prjFltHealthMetadata, Keywords.Telemetry);

                return isPrjfltDriverInstalled && isPrjfltServiceInstalled && isPrjfltServiceRunning && isNativeProjFSLibInstalled;
            }
        }

        public void Run()
        {
            string errorMessage;
            NamedPipeMessages.CompletionState state = NamedPipeMessages.CompletionState.Success;

            if (!TryEnablePrjFlt(this.tracer, out errorMessage))
            {
                state = NamedPipeMessages.CompletionState.Failure;
                this.tracer.RelatedError("Unable to install or enable PrjFlt. Enlistment root: {0} \nError: {1} ", this.request.EnlistmentRoot, errorMessage);
            }

            if (!string.IsNullOrEmpty(this.request.EnlistmentRoot))
            {
                if (!ProjFSFilter.TryAttach(this.request.EnlistmentRoot, out errorMessage))
                {
                    state = NamedPipeMessages.CompletionState.Failure;
                    this.tracer.RelatedError("Unable to attach filter to volume. Enlistment root: {0} \nError: {1} ", this.request.EnlistmentRoot, errorMessage);
                }
            }

            NamedPipeMessages.EnableAndAttachProjFSRequest.Response response = new NamedPipeMessages.EnableAndAttachProjFSRequest.Response();

            response.State = state;
            response.ErrorMessage = errorMessage;

            this.WriteToClient(response.ToMessage(), this.connection, this.tracer);
        }
    }
}


================================================
FILE: GVFS/GVFS.Service/Handlers/GetActiveRepoListHandler.cs
================================================
using GVFS.Common;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace GVFS.Service.Handlers
{
    public class GetActiveRepoListHandler : MessageHandler
    {
        private NamedPipeServer.Connection connection;
        private NamedPipeMessages.GetActiveRepoListRequest request;
        private ITracer tracer;
        private IRepoRegistry registry;

        public GetActiveRepoListHandler(
            ITracer tracer,
            IRepoRegistry registry,
            NamedPipeServer.Connection connection,
            NamedPipeMessages.GetActiveRepoListRequest request)
        {
            this.tracer = tracer;
            this.registry = registry;
            this.connection = connection;
            this.request = request;
        }

        public void Run()
        {
            string errorMessage;
            NamedPipeMessages.GetActiveRepoListRequest.Response response = new NamedPipeMessages.GetActiveRepoListRequest.Response();
            response.State = NamedPipeMessages.CompletionState.Success;
            response.RepoList = new List();

            List repos;
            if (this.registry.TryGetActiveRepos(out repos, out errorMessage))
            {
                List tempRepoList = repos.Select(repo => repo.EnlistmentRoot).ToList();

                foreach (string repoRoot in tempRepoList)
                {
                    if (!this.IsValidRepo(repoRoot))
                    {
                        if (!this.registry.TryRemoveRepo(repoRoot, out errorMessage))
                        {
                            this.tracer.RelatedInfo("Removing an invalid repo failed with error: " + response.ErrorMessage);
                        }
                        else
                        {
                            this.tracer.RelatedInfo("Removed invalid repo entry from registry: " + repoRoot);
                        }
                    }
                    else
                    {
                        response.RepoList.Add(repoRoot);
                    }
                }
            }
            else
            {
                response.ErrorMessage = errorMessage;
                response.State = NamedPipeMessages.CompletionState.Failure;
                this.tracer.RelatedError("Get active repo list failed with error: " + response.ErrorMessage);
            }

            this.WriteToClient(response.ToMessage(), this.connection, this.tracer);
        }

        private bool IsValidRepo(string repoRoot)
        {
            if (!Directory.Exists(repoRoot))
            {
                return false;
            }

            string gitBinPath = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath();

            string hooksVersion = null;
            string error = null;
            if (GVFSPlatform.Instance.TryGetGVFSHooksVersion(out hooksVersion, out error))
            {
                try
                {
                    GVFSEnlistment enlistment = GVFSEnlistment.CreateFromDirectory(
                        repoRoot,
                        gitBinPath,
                        authentication: null);
                }
                catch (InvalidRepoException e)
                {
                    EventMetadata metadata = new EventMetadata();
                    metadata.Add(nameof(repoRoot), repoRoot);
                    metadata.Add(nameof(gitBinPath), gitBinPath);
                    metadata.Add("Exception", e.ToString());
                    this.tracer.RelatedInfo(metadata, $"{nameof(this.IsValidRepo)}: Found invalid repo");

                    return false;
                }
            }
            else
            {
                this.tracer.RelatedError($"{nameof(this.IsValidRepo)}: {nameof(GVFSPlatform.Instance.TryGetGVFSHooksVersion)} failed. {error}");
                return false;
            }

            return true;
        }
    }
}


================================================
FILE: GVFS/GVFS.Service/Handlers/INotificationHandler.cs
================================================
using GVFS.Common.NamedPipes;

namespace GVFS.Service.Handlers
{
    public interface INotificationHandler
    {
        void SendNotification(NamedPipeMessages.Notification.Request request);
    }
}


================================================
FILE: GVFS/GVFS.Service/Handlers/MessageHandler.cs
================================================
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;

namespace GVFS.Service.Handlers
{
    public abstract class MessageHandler
    {
        protected void WriteToClient(NamedPipeMessages.Message message, NamedPipeServer.Connection connection, ITracer tracer)
        {
            if (!connection.TrySendResponse(message))
            {
                tracer.RelatedError("Failed to send line to client: {0}", message);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Service/Handlers/NotificationHandler.cs
================================================
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;

namespace GVFS.Service.Handlers
{
    public class NotificationHandler : INotificationHandler
    {
        public NotificationHandler(ITracer tracer)
        {
        }

        public void SendNotification(NamedPipeMessages.Notification.Request request)
        {
        }
    }
}


================================================
FILE: GVFS/GVFS.Service/Handlers/RegisterRepoHandler.cs
================================================
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;

namespace GVFS.Service.Handlers
{
    public class RegisterRepoHandler : MessageHandler
    {
        private NamedPipeServer.Connection connection;
        private NamedPipeMessages.RegisterRepoRequest request;
        private ITracer tracer;
        private IRepoRegistry registry;

        public RegisterRepoHandler(
            ITracer tracer,
            IRepoRegistry registry,
            NamedPipeServer.Connection connection,
            NamedPipeMessages.RegisterRepoRequest request)
        {
            this.tracer = tracer;
            this.registry = registry;
            this.connection = connection;
            this.request = request;
        }

        public void Run()
        {
            string errorMessage = string.Empty;
            NamedPipeMessages.RegisterRepoRequest.Response response = new NamedPipeMessages.RegisterRepoRequest.Response();

            if (this.registry.TryRegisterRepo(this.request.EnlistmentRoot, this.request.OwnerSID, out errorMessage))
            {
                response.State = NamedPipeMessages.CompletionState.Success;
                this.tracer.RelatedInfo("Registered repo {0}", this.request.EnlistmentRoot);
            }
            else
            {
                response.ErrorMessage = errorMessage;
                response.State = NamedPipeMessages.CompletionState.Failure;
                this.tracer.RelatedError("Failed to register repo {0} with error: {1}", this.request.EnlistmentRoot, errorMessage);
            }

            this.WriteToClient(response.ToMessage(), this.connection, this.tracer);
        }
    }
}


================================================
FILE: GVFS/GVFS.Service/Handlers/RequestHandler.Windows.cs
================================================
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Service.Handlers;
using System.Runtime.Serialization;

namespace GVFS.Service.Handlers
{
    public class WindowsRequestHandler : RequestHandler
    {
        public WindowsRequestHandler(
            ITracer tracer,
            string etwArea,
            RepoRegistry repoRegistry) : base(tracer, etwArea, repoRegistry)
        {
        }

        protected override void HandleMessage(
            ITracer tracer,
            NamedPipeMessages.Message message,
            NamedPipeServer.Connection connection)
        {
            if (message.Header == NamedPipeMessages.EnableAndAttachProjFSRequest.Header)
            {
                this.requestDescription = EnableProjFSRequestDescription;
                NamedPipeMessages.EnableAndAttachProjFSRequest attachRequest = NamedPipeMessages.EnableAndAttachProjFSRequest.FromMessage(message);
                EnableAndAttachProjFSHandler attachHandler = new EnableAndAttachProjFSHandler(tracer, connection, attachRequest);
                attachHandler.Run();
            }
            else
            {
                base.HandleMessage(tracer, message, connection);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Service/Handlers/RequestHandler.cs
================================================
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using System.Runtime.Serialization;

namespace GVFS.Service.Handlers
{
    /// 
    /// RequestHandler - Routes client requests that reach GVFS.Service to
    /// appropriate MessageHandler object.
    /// Example requests - gvfs mount/unmount command sends requests to
    /// register/un-register repositories for automount. RequestHandler
    /// routes them to RegisterRepoHandler and UnRegisterRepoHandler
    /// respectively.
    /// 
    public class RequestHandler
    {
        protected const string EnableProjFSRequestDescription = "attach volume";
        protected string requestDescription;

        private const string MountRequestDescription = "mount";
        private const string UnmountRequestDescription = "unmount";
        private const string RepoListRequestDescription = "repo list";
        private const string UnknownRequestDescription = "unknown";

        private string etwArea;
        private ITracer tracer;
        private IRepoRegistry repoRegistry;

        public RequestHandler(ITracer tracer, string etwArea, IRepoRegistry repoRegistry)
        {
            this.tracer = tracer;
            this.etwArea = etwArea;
            this.repoRegistry = repoRegistry;
        }

        public void HandleRequest(ITracer tracer, string request, NamedPipeServer.Connection connection)
        {
            NamedPipeMessages.Message message = NamedPipeMessages.Message.FromString(request);
            if (string.IsNullOrWhiteSpace(message.Header))
            {
                return;
            }

            using (ITracer activity = this.tracer.StartActivity(message.Header, EventLevel.Informational, new EventMetadata { { nameof(request), request } }))
            {
                try
                {
                    this.HandleMessage(activity, message, connection);
                }
                catch (SerializationException ex)
                {
                    EventMetadata metadata = new EventMetadata();
                    metadata.Add("Area", this.etwArea);
                    metadata.Add("Header", message.Header);
                    metadata.Add("Exception", ex.ToString());

                    activity.RelatedError(metadata, $"Could not deserialize {this.requestDescription} request: {ex.Message}");
                }
            }
        }

        protected virtual void HandleMessage(
            ITracer tracer,
            NamedPipeMessages.Message message,
            NamedPipeServer.Connection connection)
        {
            switch (message.Header)
            {
                case NamedPipeMessages.RegisterRepoRequest.Header:
                    this.requestDescription = MountRequestDescription;
                    NamedPipeMessages.RegisterRepoRequest mountRequest = NamedPipeMessages.RegisterRepoRequest.FromMessage(message);
                    RegisterRepoHandler mountHandler = new RegisterRepoHandler(tracer, this.repoRegistry, connection, mountRequest);
                    mountHandler.Run();

                    break;

                case NamedPipeMessages.UnregisterRepoRequest.Header:
                    this.requestDescription = UnmountRequestDescription;
                    NamedPipeMessages.UnregisterRepoRequest unmountRequest = NamedPipeMessages.UnregisterRepoRequest.FromMessage(message);
                    UnregisterRepoHandler unmountHandler = new UnregisterRepoHandler(tracer, this.repoRegistry, connection, unmountRequest);
                    unmountHandler.Run();

                    break;

                case NamedPipeMessages.GetActiveRepoListRequest.Header:
                    this.requestDescription = RepoListRequestDescription;
                    NamedPipeMessages.GetActiveRepoListRequest repoListRequest = NamedPipeMessages.GetActiveRepoListRequest.FromMessage(message);
                    GetActiveRepoListHandler excludeHandler = new GetActiveRepoListHandler(tracer, this.repoRegistry, connection, repoListRequest);
                    excludeHandler.Run();

                    break;

                case NamedPipeMessages.EnableAndAttachProjFSRequest.Header:

                    // This request is ignored on non Windows platforms.
                    NamedPipeMessages.EnableAndAttachProjFSRequest.Response response = new NamedPipeMessages.EnableAndAttachProjFSRequest.Response();
                    response.State = NamedPipeMessages.CompletionState.Success;

                    this.TrySendResponse(tracer, response.ToMessage().ToString(), connection);
                    break;

                default:
                    this.requestDescription = UnknownRequestDescription;
                    EventMetadata metadata = new EventMetadata();
                    metadata.Add("Area", this.etwArea);
                    metadata.Add("Header", message.Header);
                    tracer.RelatedWarning(metadata, "HandleNewConnection: Unknown request", Keywords.Telemetry);

                    this.TrySendResponse(tracer, NamedPipeMessages.UnknownRequest, connection);
                    break;
            }
        }

        private void TrySendResponse(
            ITracer tracer,
            string message,
            NamedPipeServer.Connection connection)
        {
            if (!connection.TrySendResponse(message))
            {
                tracer.RelatedError($"{nameof(this.TrySendResponse)}: Could not send response to client. Reply Info: {message}");
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Service/Handlers/UnregisterRepoHandler.cs
================================================
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;

namespace GVFS.Service.Handlers
{
    public class UnregisterRepoHandler : MessageHandler
    {
        private NamedPipeServer.Connection connection;
        private NamedPipeMessages.UnregisterRepoRequest request;
        private ITracer tracer;
        private IRepoRegistry registry;

        public UnregisterRepoHandler(
            ITracer tracer,
            IRepoRegistry registry,
            NamedPipeServer.Connection connection,
            NamedPipeMessages.UnregisterRepoRequest request)
        {
            this.tracer = tracer;
            this.registry = registry;
            this.connection = connection;
            this.request = request;
        }

        public void Run()
        {
            string errorMessage = string.Empty;
            NamedPipeMessages.UnregisterRepoRequest.Response response = new NamedPipeMessages.UnregisterRepoRequest.Response();

            if (this.registry.TryDeactivateRepo(this.request.EnlistmentRoot, out errorMessage))
            {
                response.State = NamedPipeMessages.CompletionState.Success;
                this.tracer.RelatedInfo("Deactivated repo {0}", this.request.EnlistmentRoot);
            }
            else
            {
                response.ErrorMessage = errorMessage;
                response.State = NamedPipeMessages.CompletionState.Failure;
                this.tracer.RelatedError("Failed to deactivate repo {0} with error: {1}", this.request.EnlistmentRoot, errorMessage);
            }

            this.WriteToClient(response.ToMessage(), this.connection, this.tracer);
        }
    }
}


================================================
FILE: GVFS/GVFS.Service/IRepoMounter.cs
================================================
namespace GVFS.Service
{
    public interface IRepoMounter
    {
        bool MountRepository(string repoRoot, int sessionId);
    }
}


================================================
FILE: GVFS/GVFS.Service/IRepoRegistry.cs
================================================
using System.Collections.Generic;

namespace GVFS.Service
{
    public interface IRepoRegistry
    {
        bool TryRegisterRepo(string repoRoot, string ownerSID, out string errorMessage);
        bool TryDeactivateRepo(string repoRoot, out string errorMessage);
        bool TryGetActiveRepos(out List repoList, out string errorMessage);
        bool TryRemoveRepo(string repoRoot, out string errorMessage);
        void AutoMountRepos(string userId, int sessionId, bool checkDirectoryExists = true);
        void TraceStatus();
    }
}


================================================
FILE: GVFS/GVFS.Service/Program.cs
================================================
using GVFS.Common;
using GVFS.Common.Tracing;
using GVFS.PlatformLoader;
using System;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Threading;

namespace GVFS.Service
{
    public static class Program
    {
        private const string ConsoleFlag = "--console";

        public static void Main(string[] args)
        {
            GVFSPlatformLoader.Initialize();

            AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler;

            if (args.Any(arg => arg.Equals(ConsoleFlag, StringComparison.OrdinalIgnoreCase)))
            {
                RunAsConsoleApp(args);
            }
            else
            {
                using (JsonTracer tracer = new JsonTracer(GVFSConstants.Service.ServiceName, GVFSConstants.Service.ServiceName))
                {
                    using (GVFSService service = new GVFSService(tracer))
                    {
                        // This will fail with a popup from a command prompt. To install as a service, run:
                        // %windir%\Microsoft.NET\Framework64\v4.0.30319\installutil GVFS.Service.exe
                        ServiceBase.Run(service);
                    }
                }
            }
        }

        private static void RunAsConsoleApp(string[] args)
        {
            using (JsonTracer tracer = new JsonTracer(GVFSConstants.Service.ServiceName, GVFSConstants.Service.ServiceName))
            {
                using (GVFSService service = new GVFSService(tracer))
                {
                    service.RunInConsoleMode(args);
                }
            }
        }

        private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
        {
            using (EventLog eventLog = new EventLog("Application"))
            {
                eventLog.Source = "Application";
                eventLog.WriteEntry(
                    "Unhandled exception in GVFS.Service: " + e.ExceptionObject.ToString(),
                    EventLogEntryType.Error);
            }
        }
    }
}

================================================
FILE: GVFS/GVFS.Service/RepoRegistration.cs
================================================
using Newtonsoft.Json;

namespace GVFS.Service
{
    public class RepoRegistration
    {
        public RepoRegistration()
        {
        }

        public RepoRegistration(string enlistmentRoot, string ownerSID)
        {
            this.EnlistmentRoot = enlistmentRoot;
            this.OwnerSID = ownerSID;
            this.IsActive = true;
        }

        public string EnlistmentRoot { get; set; }
        public string OwnerSID { get; set; }
        public bool IsActive { get; set; }

        public static RepoRegistration FromJson(string json)
        {
            return JsonConvert.DeserializeObject(
                json,
                new JsonSerializerSettings
                {
                    MissingMemberHandling = MissingMemberHandling.Ignore
                });
        }

        public override string ToString()
        {
            return
                string.Format(
                    "({0} - {1}) {2}",
                    this.IsActive ? "Active" : "Inactive",
                    this.OwnerSID,
                    this.EnlistmentRoot);
        }

        public string ToJson()
        {
            return JsonConvert.SerializeObject(this);
        }
    }
}

================================================
FILE: GVFS/GVFS.Service/RepoRegistry.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Service.Handlers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace GVFS.Service
{
    public class RepoRegistry : IRepoRegistry
    {
        public const string RegistryName = "repo-registry";
        private const string EtwArea = nameof(RepoRegistry);
        private const string RegistryTempName = "repo-registry.lock";
        private const int RegistryVersion = 2;

        private string registryParentFolderPath;
        private ITracer tracer;
        private PhysicalFileSystem fileSystem;
        private object repoLock = new object();
        private IRepoMounter repoMounter;
        private INotificationHandler notificationHandler;

        public RepoRegistry(
            ITracer tracer,
            PhysicalFileSystem fileSystem,
            string serviceDataLocation,
            IRepoMounter repoMounter,
            INotificationHandler notificationHandler)
        {
            this.tracer = tracer;
            this.fileSystem = fileSystem;
            this.registryParentFolderPath = serviceDataLocation;
            this.repoMounter = repoMounter;
            this.notificationHandler = notificationHandler;

            EventMetadata metadata = new EventMetadata();
            metadata.Add("Area", EtwArea);
            metadata.Add("registryParentFolderPath", this.registryParentFolderPath);
            metadata.Add(TracingConstants.MessageKey.InfoMessage, "RepoRegistry created");
            this.tracer.RelatedEvent(EventLevel.Informational, "RepoRegistry_Created", metadata);
        }

        public void Upgrade()
        {
            // Version 1 to Version 2, added OwnerSID
            Dictionary allRepos = this.ReadRegistry();
            if (allRepos.Any())
            {
                this.WriteRegistry(allRepos);
            }
        }

        public bool TryRegisterRepo(string repoRoot, string ownerSID, out string errorMessage)
        {
            errorMessage = null;

            try
            {
                lock (this.repoLock)
                {
                    Dictionary allRepos = this.ReadRegistry();
                    RepoRegistration repo;
                    if (allRepos.TryGetValue(repoRoot, out repo))
                    {
                        if (!repo.IsActive)
                        {
                            repo.IsActive = true;
                            repo.OwnerSID = ownerSID;
                            this.WriteRegistry(allRepos);
                        }
                    }
                    else
                    {
                        allRepos[repoRoot] = new RepoRegistration(repoRoot, ownerSID);
                        this.WriteRegistry(allRepos);
                    }
                }

                return true;
            }
            catch (Exception e)
            {
                errorMessage = string.Format("Error while registering repo {0}: {1}", repoRoot, e.ToString());
            }

            return false;
        }

        public void TraceStatus()
        {
            try
            {
                lock (this.repoLock)
                {
                    Dictionary allRepos = this.ReadRegistry();
                    foreach (RepoRegistration repo in allRepos.Values)
                    {
                        this.tracer.RelatedInfo(repo.ToString());
                    }
                }
            }
            catch (Exception e)
            {
                this.tracer.RelatedError("Error while tracing repos: {0}", e.ToString());
            }
        }

        public bool TryDeactivateRepo(string repoRoot, out string errorMessage)
        {
            errorMessage = null;

            try
            {
                lock (this.repoLock)
                {
                    Dictionary allRepos = this.ReadRegistry();
                    RepoRegistration repo;
                    if (allRepos.TryGetValue(repoRoot, out repo))
                    {
                        if (repo.IsActive)
                        {
                            repo.IsActive = false;
                            this.WriteRegistry(allRepos);
                        }

                        return true;
                    }
                    else
                    {
                        errorMessage = string.Format("Attempted to deactivate non-existent repo at '{0}'", repoRoot);
                    }
                }
            }
            catch (Exception e)
            {
                errorMessage = string.Format("Error while deactivating repo {0}: {1}", repoRoot, e.ToString());
            }

            return false;
        }

        public bool TryRemoveRepo(string repoRoot, out string errorMessage)
        {
            errorMessage = null;

            try
            {
                lock (this.repoLock)
                {
                    Dictionary allRepos = this.ReadRegistry();
                    if (allRepos.Remove(repoRoot))
                    {
                        this.WriteRegistry(allRepos);
                        return true;
                    }
                    else
                    {
                        errorMessage = string.Format("Attempted to remove non-existent repo at '{0}'", repoRoot);
                    }
                }
            }
            catch (Exception e)
            {
                errorMessage = string.Format("Error while removing repo {0}: {1}", repoRoot, e.ToString());
            }

            return false;
        }

        public void AutoMountRepos(string userId, int sessionId, bool checkDirectoryExists = true)
        {
            using (ITracer activity = this.tracer.StartActivity("AutoMount", EventLevel.Informational))
            {
                List activeRepos = this.GetActiveReposForUser(userId);
                foreach (RepoRegistration repo in activeRepos)
                {
                    // TODO #1089: We need to respect the elevation level of the original mount
                    if (checkDirectoryExists && !Directory.Exists(repo.EnlistmentRoot))
                    {
                        continue;
                    }

                    if (!this.repoMounter.MountRepository(repo.EnlistmentRoot, sessionId))
                    {
                        this.SendNotification(
                            sessionId,
                            NamedPipeMessages.Notification.Request.Identifier.MountFailure,
                            repo.EnlistmentRoot);
                    }
                }
            }
        }

        public Dictionary ReadRegistry()
        {
            Dictionary allRepos = new Dictionary(GVFSPlatform.Instance.Constants.PathComparer);

            using (Stream stream = this.fileSystem.OpenFileStream(
                    Path.Combine(this.registryParentFolderPath, RegistryName),
                    FileMode.OpenOrCreate,
                    FileAccess.Read,
                    FileShare.Read,
                    callFlushFileBuffers: false))
            {
                using (StreamReader reader = new StreamReader(stream))
                {
                    string versionString = reader.ReadLine();
                    int version;
                    if (!int.TryParse(versionString, out version) ||
                        version > RegistryVersion)
                    {
                        if (versionString != null)
                        {
                            EventMetadata metadata = new EventMetadata();
                            metadata.Add("Area", EtwArea);
                            metadata.Add("OnDiskVersion", versionString);
                            metadata.Add("ExpectedVersion", versionString);
                            this.tracer.RelatedError(metadata, "ReadRegistry: Unsupported version");
                        }

                        return allRepos;
                    }

                    while (!reader.EndOfStream)
                    {
                        string entry = reader.ReadLine();
                        if (entry.Length > 0)
                        {
                            try
                            {
                                RepoRegistration registration = RepoRegistration.FromJson(entry);

                                string errorMessage;
                                string normalizedEnlistmentRootPath = registration.EnlistmentRoot;
                                if (this.fileSystem.TryGetNormalizedPath(registration.EnlistmentRoot, out normalizedEnlistmentRootPath, out errorMessage))
                                {
                                    if (!normalizedEnlistmentRootPath.Equals(registration.EnlistmentRoot, GVFSPlatform.Instance.Constants.PathComparison))
                                    {
                                        EventMetadata metadata = new EventMetadata();
                                        metadata.Add("registration.EnlistmentRoot", registration.EnlistmentRoot);
                                        metadata.Add(nameof(normalizedEnlistmentRootPath), normalizedEnlistmentRootPath);
                                        metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.ReadRegistry)}: Mapping registered enlistment root to final path");
                                        this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.ReadRegistry)}_NormalizedPathMapping", metadata);
                                    }
                                }
                                else
                                {
                                    EventMetadata metadata = new EventMetadata();
                                    metadata.Add("registration.EnlistmentRoot", registration.EnlistmentRoot);
                                    metadata.Add("NormalizedEnlistmentRootPath", normalizedEnlistmentRootPath);
                                    metadata.Add("ErrorMessage", errorMessage);
                                    this.tracer.RelatedWarning(metadata, $"{nameof(this.ReadRegistry)}: Failed to get normalized path name for registed enlistment root");
                                }

                                if (normalizedEnlistmentRootPath != null)
                                {
                                    allRepos[normalizedEnlistmentRootPath] = registration;
                                }
                            }
                            catch (Exception e)
                            {
                                EventMetadata metadata = new EventMetadata();
                                metadata.Add("Area", EtwArea);
                                metadata.Add("entry", entry);
                                metadata.Add("Exception", e.ToString());
                                this.tracer.RelatedError(metadata, "ReadRegistry: Failed to read entry");
                            }
                        }
                    }
                }
            }

            return allRepos;
        }

        public bool TryGetActiveRepos(out List repoList, out string errorMessage)
        {
            repoList = null;
            errorMessage = null;

            lock (this.repoLock)
            {
                try
                {
                    Dictionary repos = this.ReadRegistry();
                    repoList = repos
                        .Values
                        .Where(repo => repo.IsActive)
                        .ToList();
                    return true;
                }
                catch (Exception e)
                {
                    errorMessage = string.Format("Unable to get list of active repos: {0}", e.ToString());
                    return false;
                }
            }
        }

        private List GetActiveReposForUser(string ownerSID)
        {
            lock (this.repoLock)
            {
                try
                {
                    Dictionary repos = this.ReadRegistry();
                    return repos
                        .Values
                        .Where(repo => repo.IsActive)
                        .Where(repo => string.Equals(repo.OwnerSID, ownerSID, StringComparison.InvariantCultureIgnoreCase))
                        .ToList();
                }
                catch (Exception e)
                {
                    this.tracer.RelatedError("Unable to get list of active repos for user {0}: {1}", ownerSID, e.ToString());
                    return new List();
                }
            }
        }

        private void SendNotification(
            int sessionId,
            NamedPipeMessages.Notification.Request.Identifier requestId,
            string enlistment = null,
            int enlistmentCount = 0)
        {
            NamedPipeMessages.Notification.Request request = new NamedPipeMessages.Notification.Request();
            request.Id = requestId;
            request.Enlistment = enlistment;
            request.EnlistmentCount = enlistmentCount;

            this.notificationHandler.SendNotification(request);
        }

        private void WriteRegistry(Dictionary registry)
        {
            string tempFilePath = Path.Combine(this.registryParentFolderPath, RegistryTempName);
            using (Stream stream = this.fileSystem.OpenFileStream(
                    tempFilePath,
                    FileMode.Create,
                    FileAccess.Write,
                    FileShare.None,
                    callFlushFileBuffers: true))
            using (StreamWriter writer = new StreamWriter(stream))
            {
                writer.WriteLine(RegistryVersion);

                foreach (RepoRegistration repo in registry.Values)
                {
                    writer.WriteLine(repo.ToJson());
                }

                stream.Flush();
            }

            this.fileSystem.MoveAndOverwriteFile(tempFilePath, Path.Combine(this.registryParentFolderPath, RegistryName));
        }
    }
}


================================================
FILE: GVFS/GVFS.Tests/DataSources.cs
================================================
namespace GVFS.Tests
{
    public class DataSources
    {
        public static object[] AllBools
        {
            get
            {
                return new object[]
                {
                     new object[] { true },
                     new object[] { false },
                };
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Tests/GVFS.Tests.csproj
================================================


  
    net471
  

  
    
    
  




================================================
FILE: GVFS/GVFS.Tests/NUnitRunner.cs
================================================
using NUnitLite;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

namespace GVFS.Tests
{
    public class NUnitRunner
    {
        private List args;

        public NUnitRunner(string[] args)
        {
            this.args = new List(args);
        }

        public string GetCustomArgWithParam(string arg)
        {
            string match = this.args.Where(a => a.StartsWith(arg + "=")).SingleOrDefault();
            if (match == null)
            {
                return null;
            }

            this.args.Remove(match);
            return match.Substring(arg.Length + 1);
        }

        public bool HasCustomArg(string arg)
        {
            // We also remove it as we're checking, because nunit wouldn't understand what it means
            return this.args.Remove(arg);
        }

        public void AddGlobalSetupIfNeeded(string globalSetup)
        {
            // If there are any test filters, the GlobalSetup still needs to run so add it.
            if (this.args.Any(x => x.StartsWith("--test=")))
            {
                this.args.Add($"--test={globalSetup}");
            }
        }

        public void PrepareTestSlice(string filters, (uint, uint) testSlice)
        {
            IEnumerable args = this.args.Concat(new[] { "--explore" });
            if (filters.Length > 0)
            {
                args = args.Concat(new[] { "--where", filters });
            }

            // Temporarily redirect Console.Out to capture the output of --explore
            var stringWriter = new StringWriter();
            var originalOut = Console.Out;

            string[] list;
            try
            {
                Console.SetOut(stringWriter);
                int exploreResult = new AutoRun(Assembly.GetEntryAssembly()).Execute(args.ToArray());
                if (exploreResult != 0)
                {
                    throw new Exception("--explore failed with " + exploreResult);
                }

                list = stringWriter.ToString().Split(new[] { "\n" }, StringSplitOptions.None);
            }
            finally
            {
                Console.SetOut(originalOut); // Ensure we restore Console.Out
            }

            // Sort the test cases into roughly equal-sized buckets;
            // Care must be taken to ensure that all test cases for a given
            // EnlistmentPerFixture class go into the same bucket, as they
            // may very well be dependent on each other.

            // First, create the buckets
            List[] buckets = new List[testSlice.Item2];
            // There is no PriorityQueue in .NET Framework; Emulate one via
            // a sorted set that contains tuples of (bucket index, bucket size).
            var priorityQueue = new SortedSet<(int, int)>(
                    Comparer<(int, int)>.Create((x, y) =>
                    {
                        if (x.Item2 != y.Item2)
                        {
                            return x.Item2.CompareTo(y.Item2);
                        }
                        return x.Item1.CompareTo(y.Item1);
                    }));
            for (int i = 0; i < buckets.Length; i++)
            {
                buckets[i] = new List();
                priorityQueue.Add((i, buckets[i].Count));
            }

            // Now distribute the tests into the buckets
            Regex perFixtureRegex = new Regex(
                @"^.*\.EnlistmentPerFixture\..+\.",
                // @"^.*\.",
                RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
            for (uint i = 0; i < list.Length; i++)
            {
                var test = list[i].Trim();
                if (!test.StartsWith("GVFS.")) continue;

                var bucket = priorityQueue.Min;
                priorityQueue.Remove(bucket);

                buckets[bucket.Item1].Add(test);

                // Ensure that EnlistmentPerFixture tests of the same class are all in the same bucket
                var match = perFixtureRegex.Match(test);
                if (match.Success)
                {
                    string prefix = match.Value;
                    while (i + 1 < list.Length && list[i + 1].StartsWith(prefix))
                    {
                        buckets[bucket.Item1].Add(list[++i].Trim());
                    }
                }

                bucket.Item2 = buckets[bucket.Item1].Count;
                priorityQueue.Add(bucket);
            }

            // Write the respective bucket's contents to a file
            string listFile = $"GVFS_test_slice_{testSlice.Item1}_of_{testSlice.Item2}.txt";
            File.WriteAllLines(listFile, buckets[testSlice.Item1]);
            Console.WriteLine($"Wrote {buckets[testSlice.Item1].Count} test cases to {listFile}");

            this.args.Add($"--testlist={listFile}");
        }

        public int RunTests(ICollection includeCategories, ICollection excludeCategories, (uint, uint)? testSlice = null)
        {
            string filters = GetFiltersArgument(includeCategories, excludeCategories);

            if (testSlice.HasValue && testSlice.Value.Item2 != 1)
            {
                this.PrepareTestSlice(filters, testSlice.Value);
            }
            else if (filters.Length > 0)
            {
                this.args.Add("--where");
                this.args.Add(filters);
            }

            DateTime now = DateTime.Now;
            int result = new AutoRun(Assembly.GetEntryAssembly()).Execute(this.args.ToArray());

            Console.WriteLine("Completed test pass in {0}", DateTime.Now - now);
            Console.WriteLine();

            return result;
        }

        private static string GetFiltersArgument(ICollection includeCategories, ICollection excludeCategories)
        {
            string filters = string.Empty;
            if (includeCategories != null && includeCategories.Any())
            {
                filters = "(" + string.Join("||", includeCategories.Select(x => $"cat=={x}")) + ")";
            }

            if (excludeCategories != null && excludeCategories.Any())
            {
                filters += (filters.Length > 0 ? "&&" : string.Empty) + string.Join("&&", excludeCategories.Select(x => $"cat!={x}"));
            }

            return filters;
        }
    }
}


================================================
FILE: GVFS/GVFS.Tests/Should/EnumerableShouldExtensions.cs
================================================
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace GVFS.Tests.Should
{
    public static class EnumerableShouldExtensions
    {
        public static IEnumerable ShouldBeEmpty(this IEnumerable group, string message = null)
        {
            CollectionAssert.IsEmpty(group, message);
            return group;
        }

        public static IEnumerable ShouldBeNonEmpty(this IEnumerable group)
        {
            CollectionAssert.IsNotEmpty(group);
            return group;
        }

        public static Dictionary ShouldContain(this Dictionary dictionary, TKey key, TValue value)
        {
            TValue dictionaryValue;
            dictionary.TryGetValue(key, out dictionaryValue).ShouldBeTrue($"Dictionary {nameof(ShouldContain)} does not contain {key}");
            dictionaryValue.ShouldEqual(value, $"Dictionary {nameof(ShouldContain)} does not match on key {key} expected: {value} actual: {dictionaryValue}");

            return dictionary;
        }

        public static T ShouldContain(this IEnumerable group, Func predicate)
        {
            T item = group.FirstOrDefault(predicate);
            item.ShouldNotEqual(default(T), "No matching entries found in {" + string.Join(",", group.ToArray()) + "}");

            return item;
        }

        public static T ShouldContainSingle(this IEnumerable group, Func predicate)
        {
            T item = group.Single(predicate);
            item.ShouldNotEqual(default(T));

            return item;
        }

        public static void ShouldNotContain(this IEnumerable group, Func predicate)
        {
            T item = group.SingleOrDefault(predicate);
            item.ShouldEqual(default(T), "Unexpected matching entry found in {" + string.Join(",", group) + "}");
        }

        public static IEnumerable ShouldNotContain(this IEnumerable group, IEnumerable unexpectedValues, Func predicate)
        {
            List groupList = new List(group);

            foreach (T unexpectedValue in unexpectedValues)
            {
                Assert.IsFalse(groupList.Any(item => predicate(item, unexpectedValue)));
            }

            return group;
        }

        public static IEnumerable ShouldContain(this IEnumerable group, IEnumerable expectedValues, Func predicate)
        {
            List groupList = new List(group);

            foreach (T expectedValue in expectedValues)
            {
                Assert.IsTrue(groupList.Any(item => predicate(item, expectedValue)));
            }

            return group;
        }

        public static IEnumerable ShouldMatchInOrder(this IEnumerable group, params Action[] itemCheckers)
        {
            List groupList = new List(group);
            List> itemCheckersList = new List>(itemCheckers);

            for (int i = 0; i < groupList.Count; i++)
            {
                itemCheckersList[i](groupList[i]);
            }

            return group;
        }

        public static IEnumerable ShouldMatchInOrder(this IEnumerable group, IEnumerable expectedValues, Func equals, string message = "")
        {
            List groupList = new List(group);
            List expectedValuesList = new List(expectedValues);

            Comparer comparer = new Comparer(equals);
            List groupExtraItems = groupList.Except(expectedValues, comparer).ToList();
            List groupMissingItems = expectedValues.Except(groupList, comparer).ToList();

            StringBuilder errorMessage = new StringBuilder();

            if (groupList.Count != expectedValuesList.Count)
            {
                errorMessage.AppendLine(string.Format("{0} counts do not match. was: {1} expected: {2}", message, groupList.Count, expectedValuesList.Count));
            }

            foreach (T groupExtraItem in groupExtraItems)
            {
                errorMessage.AppendLine(string.Format("Extra: {0}", groupExtraItem));
            }

            foreach (T groupMissingItem in groupMissingItems)
            {
                errorMessage.AppendLine(string.Format("Missing: {0}", groupMissingItem));
            }

            if (!Enumerable.SequenceEqual(group, expectedValues, comparer))
            {
                errorMessage.AppendLine(string.Format("Items are not in the same order: '{0}' vs. '{1}'", string.Join(",", group), string.Join(",", expectedValues)));
            }

            if (errorMessage.Length > 0)
            {
                Assert.Fail("{0}\r\n{1}", message, errorMessage);
            }

            return group;
        }

        public static IEnumerable ShouldMatchInOrder(this IEnumerable group, params T[] expectedValues)
        {
            return group.ShouldMatchInOrder((IEnumerable)expectedValues);
        }

        public static IEnumerable ShouldMatchInOrder(this IEnumerable group, IEnumerable expectedValues)
        {
            return group.ShouldMatchInOrder(expectedValues, (t1, t2) => t1.Equals(t2));
        }

        private class Comparer : IEqualityComparer
        {
            private Func equals;

            public Comparer(Func equals)
            {
                this.equals = equals;
            }

            public bool Equals(T x, T y)
            {
                return this.equals(x, y);
            }

            public int GetHashCode(T obj)
            {
                return obj.GetHashCode();
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Tests/Should/StringExtensions.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GVFS.Tests.Should
{
    public static class StringExtensions
    {
        public static string Repeat(this string self, int count)
        {
            return string.Join(string.Empty, Enumerable.Range(0, count).Select(x => self).ToArray());
        }
    }
}


================================================
FILE: GVFS/GVFS.Tests/Should/StringShouldExtensions.cs
================================================
using NUnit.Framework;
using System;

namespace GVFS.Tests.Should
{
    public static class StringShouldExtensions
    {
        public static int ShouldBeAnInt(this string value, string message)
        {
            int output;
            Assert.IsTrue(int.TryParse(value, out output), message);
            return output;
        }

        public static string ShouldContain(this string actualValue, params string[] expectedSubstrings)
        {
            foreach (string expectedSubstring in expectedSubstrings)
            {
                Assert.IsTrue(
                     actualValue.Contains(expectedSubstring),
                     "Expected substring '{0}' not found in '{1}'",
                     expectedSubstring,
                     actualValue);
            }

            return actualValue;
        }

        public static string ShouldNotContain(this string actualValue, bool ignoreCase, params string[] unexpectedSubstrings)
        {
            foreach (string unexpectedSubstring in unexpectedSubstrings)
            {
                if (ignoreCase)
                {
                    Assert.IsFalse(
                         actualValue.IndexOf(unexpectedSubstring, 0, StringComparison.OrdinalIgnoreCase) >= 0,
                         "Unexpected substring '{0}' found in '{1}'",
                         unexpectedSubstring,
                         actualValue);
                }
                else
                {
                    Assert.IsFalse(
                         actualValue.Contains(unexpectedSubstring),
                         "Unexpected substring '{0}' found in '{1}'",
                         unexpectedSubstring,
                         actualValue);
                }
            }

            return actualValue;
        }

        public static string ShouldContainOneOf(this string actualValue, params string[] expectedSubstrings)
        {
            for (int i = 0; i < expectedSubstrings.Length; i++)
            {
                if (actualValue.Contains(expectedSubstrings[i]))
                {
                    return actualValue;
                }
            }

            Assert.Fail("No expected substrings found in '{0}'", actualValue);
            return actualValue;
        }
    }
}


================================================
FILE: GVFS/GVFS.Tests/Should/ValueShouldExtensions.cs
================================================
using NUnit.Framework;
using System;

namespace GVFS.Tests.Should
{
    public static class ValueShouldExtensions
    {
        public static bool ShouldBeTrue(this bool actualValue, string message = "")
        {
            actualValue.ShouldEqual(true, message);
            return actualValue;
        }

        public static bool ShouldBeFalse(this bool actualValue, string message = "")
        {
            actualValue.ShouldEqual(false, message);
            return actualValue;
        }

        public static T ShouldBeAtLeast(this T actualValue, T expectedValue, string message = "") where T : IComparable
        {
            Assert.GreaterOrEqual(actualValue, expectedValue, message);
            return actualValue;
        }

        public static T ShouldBeAtMost(this T actualValue, T expectedValue, string message = "") where T : IComparable
        {
            Assert.LessOrEqual(actualValue, expectedValue, message);
            return actualValue;
        }

        public static T ShouldEqual(this T actualValue, T expectedValue, string message = "")
        {
            Assert.AreEqual(expectedValue, actualValue, message);
            return actualValue;
        }

        public static T[] ShouldEqual(this T[] actualValue, T[] expectedValue, int start, int count)
        {
            expectedValue.Length.ShouldBeAtLeast(start + count);
            for (int i = 0; i < count; ++i)
            {
                actualValue[i].ShouldEqual(expectedValue[i + start]);
            }

            return actualValue;
        }

        public static T ShouldNotEqual(this T actualValue, T unexpectedValue, string message = "")
        {
            Assert.AreNotEqual(unexpectedValue, actualValue, message);
            return actualValue;
        }

        public static T ShouldBeSameAs(this T actualValue, T expectedValue, string message = "")
        {
            Assert.AreSame(expectedValue, actualValue, message);
            return actualValue;
        }

        public static T ShouldNotBeSameAs(this T actualValue, T expectedValue, string message = "")
        {
            Assert.AreNotSame(expectedValue, actualValue, message);
            return actualValue;
        }

        public static T ShouldBeOfType(this object obj)
        {
            Assert.IsTrue(obj is T, "Expected type {0}, but the object is actually of type {1}", typeof(T), obj.GetType());
            return (T)obj;
        }

        public static void ShouldBeNull(this T obj, string message = "")
            where T : class
        {
            Assert.IsNull(obj, message);
        }

        public static T ShouldNotBeNull(this T obj, string message = "")
            where T : class
        {
            Assert.IsNotNull(obj, message);
            return obj;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Category/CategoryConstants.cs
================================================
namespace GVFS.UnitTests.Category
{
    public static class CategoryConstants
    {
        public const string ExceptionExpected = "ExceptionExpected";
        public const string CaseInsensitiveFileSystemOnly = "CaseInsensitiveFileSystemOnly";
        public const string CaseSensitiveFileSystemOnly = "CaseSensitiveFileSystemOnly";
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/CommandLine/CacheVerbTests.cs
================================================
using GVFS.CommandLine;
using NUnit.Framework;
using System;
using System.Globalization;
using System.IO;

namespace GVFS.UnitTests.CommandLine
{
    [TestFixture]
    public class CacheVerbTests
    {
        private CacheVerb cacheVerb;
        private string testDir;

        [SetUp]
        public void Setup()
        {
            this.cacheVerb = new CacheVerb();
            this.testDir = Path.Combine(Path.GetTempPath(), "CacheVerbTests_" + Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(this.testDir);
        }

        [TearDown]
        public void TearDown()
        {
            if (Directory.Exists(this.testDir))
            {
                Directory.Delete(this.testDir, recursive: true);
            }
        }

        [TestCase(0, "0 bytes")]
        [TestCase(512, "512 bytes")]
        [TestCase(1023, "1023 bytes")]
        [TestCase(1024, "1.0 KB")]
        [TestCase(1536, "1.5 KB")]
        [TestCase(1048576, "1.0 MB")]
        [TestCase(1572864, "1.5 MB")]
        [TestCase(1073741824, "1.0 GB")]
        [TestCase(1610612736, "1.5 GB")]
        [TestCase(10737418240, "10.0 GB")]
        public void FormatSizeForUserDisplayReturnsExpectedString(long bytes, string expected)
        {
            CultureInfo savedCulture = CultureInfo.CurrentCulture;
            try
            {
                CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
                Assert.AreEqual(expected, this.cacheVerb.FormatSizeForUserDisplay(bytes));
            }
            finally
            {
                CultureInfo.CurrentCulture = savedCulture;
            }
        }

        [TestCase]
        public void GetPackSummaryWithNoPacks()
        {
            string packDir = Path.Combine(this.testDir, "pack");
            Directory.CreateDirectory(packDir);

            this.cacheVerb.GetPackSummary(
                packDir,
                out int prefetchCount,
                out long prefetchSize,
                out int otherCount,
                out long otherSize,
                out long latestTimestamp);

            Assert.AreEqual(0, prefetchCount);
            Assert.AreEqual(0, prefetchSize);
            Assert.AreEqual(0, otherCount);
            Assert.AreEqual(0, otherSize);
            Assert.AreEqual(0, latestTimestamp);
        }

        [TestCase]
        public void GetPackSummaryCategorizesPrefetchAndOtherPacks()
        {
            string packDir = Path.Combine(this.testDir, "pack");
            Directory.CreateDirectory(packDir);

            this.CreateFileWithSize(Path.Combine(packDir, "prefetch-1000-aabbccdd.pack"), 100);
            this.CreateFileWithSize(Path.Combine(packDir, "prefetch-2000-eeff0011.pack"), 200);
            this.CreateFileWithSize(Path.Combine(packDir, "pack-abcdef1234567890.pack"), 50);

            this.cacheVerb.GetPackSummary(
                packDir,
                out int prefetchCount,
                out long prefetchSize,
                out int otherCount,
                out long otherSize,
                out long latestTimestamp);

            Assert.AreEqual(2, prefetchCount);
            Assert.AreEqual(300, prefetchSize);
            Assert.AreEqual(1, otherCount);
            Assert.AreEqual(50, otherSize);
            Assert.AreEqual(2000, latestTimestamp);
        }

        [TestCase]
        public void GetPackSummaryIgnoresNonPackFiles()
        {
            string packDir = Path.Combine(this.testDir, "pack");
            Directory.CreateDirectory(packDir);

            this.CreateFileWithSize(Path.Combine(packDir, "prefetch-1000-aabb.pack"), 100);
            this.CreateFileWithSize(Path.Combine(packDir, "prefetch-1000-aabb.idx"), 50);
            this.CreateFileWithSize(Path.Combine(packDir, "multi-pack-index"), 10);

            this.cacheVerb.GetPackSummary(
                packDir,
                out int prefetchCount,
                out long prefetchSize,
                out int otherCount,
                out long otherSize,
                out long latestTimestamp);

            Assert.AreEqual(1, prefetchCount);
            Assert.AreEqual(100, prefetchSize);
            Assert.AreEqual(0, otherCount);
            Assert.AreEqual(0, otherSize);
        }

        [TestCase]
        public void GetPackSummaryHandlesBothGuidAndSHA1HashFormats()
        {
            string packDir = Path.Combine(this.testDir, "pack");
            Directory.CreateDirectory(packDir);

            // GVFS format: 32-char GUID
            this.CreateFileWithSize(Path.Combine(packDir, "prefetch-1000-b8d9efad32194d98894532905daf88ec.pack"), 100);
            // Scalar format: 40-char SHA1
            this.CreateFileWithSize(Path.Combine(packDir, "prefetch-2000-9babd9b75521f9caf693b485329d3d5669c88564.pack"), 200);

            this.cacheVerb.GetPackSummary(
                packDir,
                out int prefetchCount,
                out long prefetchSize,
                out int otherCount,
                out long otherSize,
                out long latestTimestamp);

            Assert.AreEqual(2, prefetchCount);
            Assert.AreEqual(300, prefetchSize);
            Assert.AreEqual(2000, latestTimestamp);
        }

        [TestCase]
        public void CountLooseObjectsWithNoObjects()
        {
            int count = this.cacheVerb.CountLooseObjects(this.testDir);
            Assert.AreEqual(0, count);
        }

        [TestCase]
        public void CountLooseObjectsCountsFilesInHexDirectories()
        {
            Directory.CreateDirectory(Path.Combine(this.testDir, "00"));
            File.WriteAllText(Path.Combine(this.testDir, "00", "abc123"), string.Empty);
            File.WriteAllText(Path.Combine(this.testDir, "00", "def456"), string.Empty);

            Directory.CreateDirectory(Path.Combine(this.testDir, "ff"));
            File.WriteAllText(Path.Combine(this.testDir, "ff", "789abc"), string.Empty);

            int count = this.cacheVerb.CountLooseObjects(this.testDir);
            Assert.AreEqual(3, count);
        }

        [TestCase]
        public void CountLooseObjectsIgnoresNonHexDirectories()
        {
            // "pack" and "info" are valid directories in a git objects dir but not hex dirs
            Directory.CreateDirectory(Path.Combine(this.testDir, "pack"));
            File.WriteAllText(Path.Combine(this.testDir, "pack", "somefile"), string.Empty);

            Directory.CreateDirectory(Path.Combine(this.testDir, "info"));
            File.WriteAllText(Path.Combine(this.testDir, "info", "somefile"), string.Empty);

            // "ab" is a valid hex dir
            Directory.CreateDirectory(Path.Combine(this.testDir, "ab"));
            File.WriteAllText(Path.Combine(this.testDir, "ab", "object1"), string.Empty);

            int count = this.cacheVerb.CountLooseObjects(this.testDir);
            Assert.AreEqual(1, count);
        }

        private void CreateFileWithSize(string path, int size)
        {
            byte[] data = new byte[size];
            File.WriteAllBytes(path, data);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/CommandLine/HooksInstallerTests.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace GVFS.UnitTests.CommandLine
{
    [TestFixture]
    public class HooksInstallerTests
    {
        private const string Filename = "hooksfile";
        private readonly string expectedAbsoluteGvfsHookPath =
            $"\"{Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), GVFSPlatform.Instance.Constants.GVFSHooksExecutableName)}\"";

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void MergeHooksDataThrowsOnFoundGVFSHooks()
        {
            Assert.Throws(
                () => HooksInstaller.MergeHooksData(
                    new string[] { "first", GVFSPlatform.Instance.Constants.GVFSHooksExecutableName },
                    Filename,
                    this.expectedAbsoluteGvfsHookPath));
        }

        [TestCase]
        public void MergeHooksDataEmptyConfig()
        {
            string result = HooksInstaller.MergeHooksData(new string[] { }, Filename, GVFSConstants.DotGit.Hooks.PreCommandHookName);
            IEnumerable resultLines = result
                .Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
                .Where(line => !line.StartsWith("#"));

            resultLines.Single().ShouldEqual(this.expectedAbsoluteGvfsHookPath);
        }

        [TestCase]
        public void MergeHooksDataPreCommandLast()
        {
            string result = HooksInstaller.MergeHooksData(new string[] { "first", "second" }, Filename, GVFSConstants.DotGit.Hooks.PreCommandHookName);
            IEnumerable resultLines = result
                .Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
                .Where(line => !line.StartsWith("#"));

            resultLines.Count().ShouldEqual(3);
            resultLines.ElementAt(0).ShouldEqual("first");
            resultLines.ElementAt(1).ShouldEqual("second");
            resultLines.ElementAt(2).ShouldEqual(this.expectedAbsoluteGvfsHookPath);
        }

        [TestCase]
        public void MergeHooksDataPostCommandFirst()
        {
            string result = HooksInstaller.MergeHooksData(new string[] { "first", "second" }, Filename, GVFSConstants.DotGit.Hooks.PostCommandHookName);
            IEnumerable resultLines = result
                .Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
                .Where(line => !line.StartsWith("#"));

            resultLines.Count().ShouldEqual(3);
            resultLines.ElementAt(0).ShouldEqual(this.expectedAbsoluteGvfsHookPath);
            resultLines.ElementAt(1).ShouldEqual("first");
            resultLines.ElementAt(2).ShouldEqual("second");
        }

        [TestCase]
        public void MergeHooksDataDiscardBlankLines()
        {
            string result = HooksInstaller.MergeHooksData(new string[] { "first", "second", string.Empty, " " }, Filename, GVFSConstants.DotGit.Hooks.PreCommandHookName);
            IEnumerable resultLines = result
                .Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
                .Where(line => !line.StartsWith("#"));

            resultLines.Count().ShouldEqual(3);
            resultLines.ElementAt(0).ShouldEqual("first");
            resultLines.ElementAt(1).ShouldEqual("second");
            resultLines.ElementAt(2).ShouldEqual(this.expectedAbsoluteGvfsHookPath);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/AzDevOpsOrgFromNuGetFeedTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class NuGetUpgraderTests
    {
        [TestCase("https://pkgs.dev.azure.com/test-pat/_packaging/Test-GVFS-Installers-Custom/nuget/v3/index.json", "https://test-pat.visualstudio.com")]
        [TestCase("https://PKGS.DEV.azure.com/test-pat/_packaging/Test-GVFS-Installers-Custom/nuget/v3/index.json", "https://test-pat.visualstudio.com")]
        [TestCase("https://dev.azure.com/test-pat/_packaging/Test-GVFS-Installers-Custom/nuget/v3/index.json", null)]
        [TestCase("http://pkgs.dev.azure.com/test-pat/_packaging/Test-GVFS-Installers-Custom/nuget/v3/index.json", null)]
        public void CanConstructAzureDevOpsUrlFromPackageFeedUrl(string packageFeedUrl, string expectedAzureDevOpsUrl)
        {
            bool success = AzDevOpsOrgFromNuGetFeed.TryCreateCredentialQueryUrl(
                packageFeedUrl,
                out string azureDevOpsUrl,
                out string error);

            if (expectedAzureDevOpsUrl != null)
            {
                success.ShouldBeTrue();
                azureDevOpsUrl.ShouldEqual(expectedAzureDevOpsUrl);
                error.ShouldBeNull();
            }
            else
            {
                success.ShouldBeFalse();
                azureDevOpsUrl.ShouldBeNull();
                error.ShouldNotBeNull();
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/BackgroundTaskQueueTests.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock;
using GVFS.Virtualization.Background;
using NUnit.Framework;
using System.IO;
using System.Text;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class FileSystemTaskQueueTests
    {
        private const string MockEntryFileName = "mock:\\entries.dat";

        private const string NonAsciiString = @"ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك";

        private const string Item1EntryText = "A 1\00\0mock:\\VirtualPath\0" + NonAsciiString + "\r\n";
        private const string Item2EntryText = "A 2\01\0mock:\\VirtualPath2\0mock:\\OldVirtualPath2\r\n";

        private const string CorruptEntryText = Item1EntryText + "A 1\0\"item1";

        private static readonly FileSystemTask Item1Payload = new FileSystemTask(FileSystemTask.OperationType.Invalid, "mock:\\VirtualPath", NonAsciiString);
        private static readonly FileSystemTask Item2Payload = new FileSystemTask(FileSystemTask.OperationType.OnFileCreated, "mock:\\VirtualPath2", "mock:\\OldVirtualPath2");

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ReturnsFalseWhenOpenFails()
        {
            MockFileSystem fs = new MockFileSystem();
            fs.File = new ReusableMemoryStream(string.Empty);
            fs.ThrowDuringOpen = true;

            string error;
            FileSystemTaskQueue dut;
            FileSystemTaskQueue.TryCreate(null, MockEntryFileName, fs, out dut, out error).ShouldEqual(false);
            dut.ShouldBeNull();
            error.ShouldNotBeNull();
        }

        [TestCase]
        public void TryPeekDoesNotDequeue()
        {
            MockFileSystem fs = new MockFileSystem();
            FileSystemTaskQueue dut = CreateFileBasedQueue(fs, Item1EntryText);
            dut.IsEmpty.ShouldBeFalse();

            for (int i = 0; i < 5; ++i)
            {
                FileSystemTask item;
                dut.TryPeek(out item).ShouldEqual(true);
                item.ShouldEqual(Item1Payload);
            }

            fs.File.ReadAsString().ShouldEqual(Item1EntryText);
        }

        [TestCase]
        public void StoresAddRecord()
        {
            MockFileSystem fs = new MockFileSystem();
            FileSystemTaskQueue dut = CreateFileBasedQueue(fs, string.Empty);
            dut.IsEmpty.ShouldBeTrue();

            dut.EnqueueAndFlush(Item1Payload);
            dut.IsEmpty.ShouldBeFalse();

            fs.File.ReadAsString().ShouldEqual(Item1EntryText);
        }

        [TestCase]
        public void TruncatesWhenEmpty()
        {
            MockFileSystem fs = new MockFileSystem();
            FileSystemTaskQueue dut = CreateFileBasedQueue(fs, Item1EntryText);
            dut.IsEmpty.ShouldBeFalse();

            dut.DequeueAndFlush(Item1Payload);
            dut.IsEmpty.ShouldBeTrue();

            fs.File.Length.ShouldEqual(0);
        }

        [TestCase]
        public void RecoversWhenCorrupt()
        {
            MockFileSystem fs = new MockFileSystem();
            FileSystemTaskQueue dut = CreateFileBasedQueue(fs, CorruptEntryText);
            dut.IsEmpty.ShouldBeFalse();

            fs.File.ReadAsString().ShouldEqual(Item1EntryText);
            dut.Count.ShouldEqual(1);
        }

        [TestCase]
        public void StoresDeleteRecord()
        {
            const string DeleteRecord = "D 1\r\n";

            MockFileSystem fs = new MockFileSystem();
            FileSystemTaskQueue dut = CreateFileBasedQueue(fs, Item1EntryText);
            dut.IsEmpty.ShouldBeFalse();

            // Add a second entry to keep FileBasedQueue from setting the stream length to 0
            dut.EnqueueAndFlush(Item2Payload);
            dut.IsEmpty.ShouldBeFalse();

            fs.File.ReadAsString().ShouldEqual(Item1EntryText + Item2EntryText);
            fs.File.ReadAt(fs.File.Length - 2, 2).ShouldEqual("\r\n");

            dut.DequeueAndFlush(Item1Payload);
            dut.IsEmpty.ShouldBeFalse();
            dut.Count.ShouldEqual(1);

            FileSystemTask item;
            dut.TryPeek(out item).ShouldEqual(true);
            item.ShouldEqual(Item2Payload);
            dut.IsEmpty.ShouldBeFalse();
            dut.Count.ShouldEqual(1);

            fs.File.Length.ShouldEqual(Encoding.UTF8.GetByteCount(Item1EntryText) + Item2EntryText.Length + DeleteRecord.Length);
            fs.File.ReadAt(Encoding.UTF8.GetByteCount(Item1EntryText) + Item2EntryText.Length, DeleteRecord.Length).ShouldEqual(DeleteRecord);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void WrapsIOExceptionsDuringWrite()
        {
            MockFileSystem fs = new MockFileSystem();
            FileSystemTaskQueue dut = CreateFileBasedQueue(fs, Item1EntryText);
            dut.IsEmpty.ShouldBeFalse();

            fs.File.TruncateWrites = true;

            Assert.Throws(() => dut.EnqueueAndFlush(Item2Payload));

            fs.File.TruncateWrites = false;
            fs.File.ReadAt(fs.File.Length - 2, 2).ShouldNotEqual("\r\n", "Bad Test: The file is supposed to be corrupt.");

            string error;
            FileSystemTaskQueue.TryCreate(null, MockEntryFileName, fs, out dut, out error).ShouldEqual(true);
            dut.IsEmpty.ShouldBeFalse();
            using (dut)
            {
                FileSystemTask output;
                dut.TryPeek(out output).ShouldEqual(true);
                output.ShouldEqual(Item1Payload);
                dut.DequeueAndFlush(output);
            }

            dut.IsEmpty.ShouldBeTrue();
        }

        private static FileSystemTaskQueue CreateFileBasedQueue(MockFileSystem fs, string initialContents)
        {
            fs.File = new ReusableMemoryStream(initialContents);
            fs.ExpectedPath = MockEntryFileName;

            string error;
            FileSystemTaskQueue dut;
            FileSystemTaskQueue.TryCreate(null, MockEntryFileName, fs, out dut, out error).ShouldEqual(true, error);
            dut.ShouldNotBeNull();
            return dut;
        }

        private class MockFileSystem : PhysicalFileSystem
        {
            public bool ThrowDuringOpen { get; set; }

            public string ExpectedPath { get; set; }
            public ReusableMemoryStream File { get; set; }

            public override void CreateDirectory(string path)
            {
            }

            public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk)
            {
                if (this.ThrowDuringOpen)
                {
                    throw new IOException("Test Error");
                }

                path.ShouldEqual(this.ExpectedPath);
                return this.File;
            }

            public override bool FileExists(string path)
            {
                return true;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/CacheServerResolverTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Http;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.Git;
using Newtonsoft.Json;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class CacheServerResolverTests
    {
        private const string CacheServerUrl = "https://cache/server";
        private const string CacheServerName = "TestCacheServer";

        [TestCase]
        public void CanGetCacheServerFromNewConfig()
        {
            MockGVFSEnlistment enlistment = this.CreateEnlistment(CacheServerUrl);
            CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment);

            cacheServer.Url.ShouldEqual(CacheServerUrl);
            CacheServerResolver.GetUrlFromConfig(enlistment).ShouldEqual(CacheServerUrl);
        }

        [TestCase]
        public void CanGetCacheServerFromOldConfig()
        {
            MockGVFSEnlistment enlistment = this.CreateEnlistment(null, CacheServerUrl);
            CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment);

            cacheServer.Url.ShouldEqual(CacheServerUrl);
            CacheServerResolver.GetUrlFromConfig(enlistment).ShouldEqual(CacheServerUrl);
        }

        [TestCase]
        public void CanGetCacheServerWithNoConfig()
        {
            MockGVFSEnlistment enlistment = this.CreateEnlistment();

            this.ValidateIsNone(enlistment, CacheServerResolver.GetCacheServerFromConfig(enlistment));
            CacheServerResolver.GetUrlFromConfig(enlistment).ShouldEqual(enlistment.RepoUrl);
        }

        [TestCase]
        public void CanResolveUrlForKnownName()
        {
            CacheServerResolver resolver = this.CreateResolver();

            CacheServerInfo resolvedCacheServer;
            string error;
            resolver.TryResolveUrlFromRemote(CacheServerName, this.CreateGVFSConfig(), out resolvedCacheServer, out error);

            resolvedCacheServer.Url.ShouldEqual(CacheServerUrl);
            resolvedCacheServer.Name.ShouldEqual(CacheServerName);
        }

        [TestCase]
        public void CanResolveNameFromKnownUrl()
        {
            CacheServerResolver resolver = this.CreateResolver();
            CacheServerInfo resolvedCacheServer = resolver.ResolveNameFromRemote(CacheServerUrl, this.CreateGVFSConfig());

            resolvedCacheServer.Url.ShouldEqual(CacheServerUrl);
            resolvedCacheServer.Name.ShouldEqual(CacheServerName);
        }

        [TestCase]
        public void CanResolveNameFromCustomUrl()
        {
            const string CustomUrl = "https://not/a/known/cache/server";

            CacheServerResolver resolver = this.CreateResolver();
            CacheServerInfo resolvedCacheServer = resolver.ResolveNameFromRemote(CustomUrl, this.CreateGVFSConfig());

            resolvedCacheServer.Url.ShouldEqual(CustomUrl);
            resolvedCacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.UserDefined);
        }

        [TestCase]
        public void CanResolveUrlAsRepoUrl()
        {
            MockGVFSEnlistment enlistment = this.CreateEnlistment();
            CacheServerResolver resolver = this.CreateResolver(enlistment);

            this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl, this.CreateGVFSConfig()));
            this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl + "/", this.CreateGVFSConfig()));
            this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl + "//", this.CreateGVFSConfig()));
            this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToUpper(), this.CreateGVFSConfig()));
            this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToUpper() + "/", this.CreateGVFSConfig()));
            this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToLower(), this.CreateGVFSConfig()));
            this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToLower() + "/", this.CreateGVFSConfig()));
        }

        [TestCase]
        public void CanParseUrl()
        {
            CacheServerResolver resolver = new CacheServerResolver(new MockTracer(), this.CreateEnlistment());
            CacheServerInfo parsedCacheServer = resolver.ParseUrlOrFriendlyName(CacheServerUrl);

            parsedCacheServer.Url.ShouldEqual(CacheServerUrl);
            parsedCacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.UserDefined);
        }

        [TestCase]
        public void CanParseName()
        {
            CacheServerResolver resolver = new CacheServerResolver(new MockTracer(), this.CreateEnlistment());
            CacheServerInfo parsedCacheServer = resolver.ParseUrlOrFriendlyName(CacheServerName);

            parsedCacheServer.Url.ShouldEqual(null);
            parsedCacheServer.Name.ShouldEqual(CacheServerName);
        }

        [TestCase]
        public void CanParseAndResolveDefault()
        {
            CacheServerResolver resolver = this.CreateResolver();

            CacheServerInfo parsedCacheServer = resolver.ParseUrlOrFriendlyName(null);
            parsedCacheServer.Url.ShouldEqual(null);
            parsedCacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.Default);

            CacheServerInfo resolvedCacheServer;
            string error;
            resolver.TryResolveUrlFromRemote(parsedCacheServer.Name, this.CreateGVFSConfig(), out resolvedCacheServer, out error);

            resolvedCacheServer.Url.ShouldEqual(CacheServerUrl);
            resolvedCacheServer.Name.ShouldEqual(CacheServerName);
        }

        [TestCase]
        public void CanParseAndResolveNoCacheServer()
        {
            MockGVFSEnlistment enlistment = this.CreateEnlistment();
            CacheServerResolver resolver = this.CreateResolver(enlistment);

            this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(CacheServerInfo.ReservedNames.None));
            this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl));
            this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl));
            this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl + "/"));
            this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl + "//"));
            this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToUpper()));
            this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToUpper() + "/"));
            this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToLower()));
            this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToLower() + "/"));

            CacheServerInfo resolvedCacheServer;
            string error;
            resolver.TryResolveUrlFromRemote(CacheServerInfo.ReservedNames.None, this.CreateGVFSConfig(), out resolvedCacheServer, out error)
                .ShouldEqual(false, "Should not succeed in resolving the name 'None'");

            resolvedCacheServer.ShouldEqual(null);
            error.ShouldNotBeNull();
        }

        [TestCase]
        public void CanParseAndResolveDefaultWhenServerAdvertisesNullListOfCacheServers()
        {
            MockGVFSEnlistment enlistment = this.CreateEnlistment();
            CacheServerResolver resolver = this.CreateResolver(enlistment);

            CacheServerInfo resolvedCacheServer;
            string error;
            resolver.TryResolveUrlFromRemote(CacheServerInfo.ReservedNames.Default, this.CreateDefaultDeserializedGVFSConfig(), out resolvedCacheServer, out error)
                .ShouldEqual(true);

            this.ValidateIsNone(enlistment, resolvedCacheServer);
        }

        [TestCase]
        public void CanParseAndResolveOtherWhenServerAdvertisesNullListOfCacheServers()
        {
            MockGVFSEnlistment enlistment = this.CreateEnlistment();
            CacheServerResolver resolver = this.CreateResolver(enlistment);

            CacheServerInfo resolvedCacheServer;
            string error;
            resolver.TryResolveUrlFromRemote(CacheServerInfo.ReservedNames.None, this.CreateDefaultDeserializedGVFSConfig(), out resolvedCacheServer, out error)
                .ShouldEqual(false, "Should not succeed in resolving the name 'None'");

            resolvedCacheServer.ShouldEqual(null);
            error.ShouldNotBeNull();
        }

        private void ValidateIsNone(Enlistment enlistment, CacheServerInfo cacheServer)
        {
            cacheServer.Url.ShouldEqual(enlistment.RepoUrl);
            cacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.None);
        }

        private MockGVFSEnlistment CreateEnlistment(string newConfigValue = null, string oldConfigValue = null)
        {
            MockGitProcess gitProcess = new MockGitProcess();
            gitProcess.SetExpectedCommandResult(
                "config --local gvfs.cache-server",
                () => new GitProcess.Result(newConfigValue ?? string.Empty, string.Empty, newConfigValue != null ? GitProcess.Result.SuccessCode : GitProcess.Result.GenericFailureCode));
            gitProcess.SetExpectedCommandResult(
                "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 MockGVFSEnlistment(gitProcess);
        }

        private ServerGVFSConfig CreateGVFSConfig()
        {
            return new ServerGVFSConfig
            {
                CacheServers = new[]
                {
                    new CacheServerInfo(CacheServerUrl, CacheServerName, globalDefault: true),
                }
            };
        }

        private ServerGVFSConfig CreateDefaultDeserializedGVFSConfig()
        {
            return JsonConvert.DeserializeObject("{}");
        }

        private CacheServerResolver CreateResolver(MockGVFSEnlistment enlistment = null)
        {
            enlistment = enlistment ?? this.CreateEnlistment();
            return new CacheServerResolver(new MockTracer(), enlistment);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/Database/GVFSDatabaseTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.FileSystem;
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;

namespace GVFS.UnitTests.Common.Database
{
    [TestFixture]
    public class GVFSDatabaseTests
    {
        [TestCase]
        public void ConstructorTest()
        {
            this.TestGVFSDatabase(null);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ConstructorThrowsGVFSDatabaseException()
        {
            GVFSDatabaseException ex = Assert.Throws(() => this.TestGVFSDatabase(null, throwException: true));
            ex.Message.ShouldEqual("GVFSDatabase constructor threw exception setting up connection pool and initializing");
            ex.InnerException.Message.ShouldEqual("Error");
        }

        [TestCase]
        public void DisposeTest()
        {
            this.TestGVFSDatabase(database => database.Dispose());
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void GetConnectionAfterDisposeShouldThrowException()
        {
            this.TestGVFSDatabase(database =>
                {
                    database.Dispose();
                    IGVFSConnectionPool connectionPool = database;
                    Assert.Throws(() => connectionPool.GetConnection());
                });
        }

        [TestCase]
        public void GetConnectionMoreThanInPoolTest()
        {
            this.TestGVFSDatabase(database =>
            {
                IGVFSConnectionPool connectionPool = database;
                using (IDbConnection pooledConnection1 = connectionPool.GetConnection())
                using (IDbConnection pooledConnection2 = connectionPool.GetConnection())
                {
                    pooledConnection1.Equals(pooledConnection2).ShouldBeFalse();
                }
            });
        }

        private void TestGVFSDatabase(Action testCode, bool throwException = false)
        {
            MockFileSystem fileSystem = new MockFileSystem(new MockDirectory("GVFSDatabaseTests", null, null));

            Mock mockCommand = new Mock(MockBehavior.Strict);
            mockCommand.SetupSet(x => x.CommandText = "PRAGMA journal_mode=WAL;");
            mockCommand.SetupSet(x => x.CommandText = "PRAGMA cache_size=-40000;");
            mockCommand.SetupSet(x => x.CommandText = "PRAGMA synchronous=NORMAL;");
            mockCommand.SetupSet(x => x.CommandText = "PRAGMA user_version;");
            mockCommand.Setup(x => x.ExecuteNonQuery()).Returns(1);
            mockCommand.Setup(x => x.ExecuteScalar()).Returns(1);
            mockCommand.Setup(x => x.Dispose());

            string collateConstraint = GVFSPlatform.Instance.Constants.CaseSensitiveFileSystem ? string.Empty : " COLLATE NOCASE";
            Mock mockCommand2 = new Mock(MockBehavior.Strict);
            mockCommand2.SetupSet(x => x.CommandText = $"CREATE TABLE IF NOT EXISTS [Placeholder] (path TEXT PRIMARY KEY{collateConstraint}, pathType TINYINT NOT NULL, sha char(40) ) WITHOUT ROWID;");
            if (throwException)
            {
                mockCommand2.Setup(x => x.ExecuteNonQuery()).Throws(new Exception("Error"));
            }
            else
            {
                mockCommand2.Setup(x => x.ExecuteNonQuery()).Returns(1);
            }

            mockCommand2.Setup(x => x.Dispose());

            Mock mockCommand3 = new Mock(MockBehavior.Strict);
            mockCommand3.SetupSet(x => x.CommandText = $"CREATE TABLE IF NOT EXISTS [Sparse] (path TEXT PRIMARY KEY{collateConstraint}) WITHOUT ROWID;");
            if (throwException)
            {
                mockCommand3.Setup(x => x.ExecuteNonQuery()).Throws(new Exception("Error"));
            }
            else
            {
                mockCommand3.Setup(x => x.ExecuteNonQuery()).Returns(1);
            }

            mockCommand3.Setup(x => x.Dispose());

            List> mockConnections = new List>();
            Mock mockConnection = new Mock(MockBehavior.Strict);
            mockConnection.SetupSequence(x => x.CreateCommand())
                          .Returns(mockCommand.Object)
                          .Returns(mockCommand2.Object)
                          .Returns(mockCommand3.Object);
            mockConnection.Setup(x => x.Dispose());
            mockConnections.Add(mockConnection);

            Mock mockConnectionFactory = new Mock(MockBehavior.Strict);
            bool firstConnection = true;
            string databasePath = Path.Combine("mock:root", ".mockvfsforgit", "databases", "VFSForGit.sqlite");
            mockConnectionFactory.Setup(x => x.OpenNewConnection(databasePath)).Returns(() =>
            {
                if (firstConnection)
                {
                    firstConnection = false;
                    return mockConnection.Object;
                }
                else
                {
                    Mock newMockConnection = new Mock(MockBehavior.Strict);
                    newMockConnection.Setup(x => x.Dispose());
                    mockConnections.Add(newMockConnection);
                    return newMockConnection.Object;
                }
            });

            using (GVFSDatabase database = new GVFSDatabase(fileSystem, "mock:root", mockConnectionFactory.Object, initialPooledConnections: 1))
            {
                testCode?.Invoke(database);
            }

            mockCommand.Verify(x => x.Dispose(), Times.Once);
            mockCommand2.Verify(x => x.Dispose(), Times.Once);
            mockCommand3.Verify(x => x.Dispose(), Times.Once);
            mockConnections.ForEach(connection => connection.Verify(x => x.Dispose(), Times.Once));

            mockCommand.VerifyAll();
            mockCommand2.VerifyAll();
            mockCommand3.VerifyAll();
            mockConnections.ForEach(connection => connection.VerifyAll());
            mockConnectionFactory.VerifyAll();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/Database/PlaceholderTableTests.cs
================================================
using GVFS.Common.Database;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;

namespace GVFS.UnitTests.Common.Database
{
    [TestFixture]
    public class PlaceholderTableTests : TableTests
    {
        private const string DefaultPath = "test";
        private const byte PathTypeFile = 0;
        private const byte PathTypePartialFolder = 1;
        private const byte PathTypeExpandedFolder = 2;
        private const byte PathTypePossibleTombstoneFolder = 3;
        private const string DefaultSha = "1234567890123456789012345678901234567890";

        protected override string CreateTableCommandString => "CREATE TABLE IF NOT EXISTS [Placeholder] (path TEXT PRIMARY KEY COLLATE NOCASE, pathType TINYINT NOT NULL, sha char(40) ) WITHOUT ROWID;";

        [TestCase]
        public void GetCountTest()
        {
            this.TestTable(
                (placeholders, mockCommand) =>
                {
                    mockCommand.SetupSet(x => x.CommandText = "SELECT count(path) FROM Placeholder;");
                    mockCommand.Setup(x => x.ExecuteScalar()).Returns(123);
                    placeholders.GetCount().ShouldEqual(123);
                });
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void GetCountThrowsGVFSDatabaseException()
        {
            this.TestTable(
                (placeholders, mockCommand) =>
                {
                    mockCommand.SetupSet(x => x.CommandText = "SELECT count(path) FROM Placeholder;");
                    mockCommand.Setup(x => x.ExecuteScalar()).Throws(new Exception(DefaultExceptionMessage));
                    GVFSDatabaseException ex = Assert.Throws(() => placeholders.GetCount());
                    ex.Message.ShouldEqual("PlaceholderTable.GetCount Exception");
                    ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
                });
        }

        [TestCase]
        public void GetAllFilePathsWithNoResults()
        {
            this.TestTableWithReader(
               (placeholders, mockCommand, mockReader) =>
               {
                   mockReader.Setup(x => x.Read()).Returns(false);
                   mockCommand.SetupSet(x => x.CommandText = "SELECT path FROM Placeholder WHERE pathType = 0;");

                   HashSet filePaths = placeholders.GetAllFilePaths();
                   filePaths.ShouldNotBeNull();
                   filePaths.Count.ShouldEqual(0);
               });
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void GetAllFilePathsThrowsGVFSDatabaseException()
        {
            this.TestTableWithReader(
               (placeholders, mockCommand, mockReader) =>
               {
                   mockReader.Setup(x => x.Read()).Throws(new Exception(DefaultExceptionMessage));
                   mockCommand.SetupSet(x => x.CommandText = "SELECT path FROM Placeholder WHERE pathType = 0;");

                   GVFSDatabaseException ex = Assert.Throws(() => placeholders.GetAllFilePaths());
                   ex.Message.ShouldEqual("PlaceholderTable.GetAllFilePaths Exception");
                   ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
               });
        }

        [TestCase]
        public void GetAllFilePathsTest()
        {
            this.TestTableWithReader(
               (placeholders, mockCommand, mockReader) =>
               {
                   int readCalls = 0;
                   mockReader.Setup(x => x.Read()).Returns(() =>
                   {
                       ++readCalls;
                       return readCalls == 1;
                   });

                   mockReader.Setup(x => x.GetString(0)).Returns(DefaultPath);
                   mockCommand.SetupSet(x => x.CommandText = "SELECT path FROM Placeholder WHERE pathType = 0;");

                   HashSet filePaths = placeholders.GetAllFilePaths();
                   filePaths.ShouldNotBeNull();
                   filePaths.Count.ShouldEqual(1);
                   filePaths.Contains(DefaultPath).ShouldBeTrue();
               });
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void GetAllEntriesThrowsGVFSDatabaseException()
        {
            List expectedPlacholders = new List();
            this.TestTableWithReader(
               (placeholders, mockCommand, mockReader) =>
               {
                   mockCommand.SetupSet(x => x.CommandText = "SELECT path, pathType, sha FROM Placeholder;");
                   mockReader.Setup(x => x.Read()).Throws(new Exception(DefaultExceptionMessage));

                   GVFSDatabaseException ex = Assert.Throws(() => placeholders.GetAllEntries(out List filePlaceholders, out List folderPlaceholders));
                   ex.Message.ShouldEqual("PlaceholderTable.GetAllEntries Exception");
                   ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
               });
        }

        [TestCase]
        public void GetAllEntriesReturnsNothing()
        {
            List expectedPlacholders = new List();
            this.TestTableWithReader(
               (placeholders, mockCommand, mockReader) =>
               {
                   this.SetupMockReader(mockReader, expectedPlacholders);
                   mockCommand.SetupSet(x => x.CommandText = "SELECT path, pathType, sha FROM Placeholder;");

                   placeholders.GetAllEntries(out List filePlaceholders, out List folderPlaceholders);
                   filePlaceholders.ShouldNotBeNull();
                   filePlaceholders.Count.ShouldEqual(0);

                   folderPlaceholders.ShouldNotBeNull();
                   folderPlaceholders.Count.ShouldEqual(0);
               });
        }

        [TestCase]
        public void GetAllEntriesReturnsOneFile()
        {
            List expectedPlacholders = new List();
            expectedPlacholders.Add(new PlaceholderTable.PlaceholderData() { Path = DefaultPath, PathType = PlaceholderTable.PlaceholderData.PlaceholderType.File, Sha = DefaultSha });
            this.TestTableWithReader(
               (placeholders, mockCommand, mockReader) =>
               {
                   this.SetupMockReader(mockReader, expectedPlacholders);
                   mockCommand.SetupSet(x => x.CommandText = "SELECT path, pathType, sha FROM Placeholder;");

                   placeholders.GetAllEntries(out List filePlaceholders, out List folderPlaceholders);
                   filePlaceholders.ShouldNotBeNull();
                   this.PlaceholderListShouldMatch(expectedPlacholders, filePlaceholders);

                   folderPlaceholders.ShouldNotBeNull();
                   folderPlaceholders.Count.ShouldEqual(0);
               });
        }

        [TestCase]
        public void GetAllEntriesReturnsOneFolder()
        {
            List expectedPlacholders = new List();
            expectedPlacholders.Add(new PlaceholderTable.PlaceholderData() { Path = DefaultPath, PathType = PlaceholderTable.PlaceholderData.PlaceholderType.PartialFolder, Sha = null });
            this.TestTableWithReader(
               (placeholders, mockCommand, mockReader) =>
               {
                   this.SetupMockReader(mockReader, expectedPlacholders);
                   mockCommand.SetupSet(x => x.CommandText = "SELECT path, pathType, sha FROM Placeholder;");

                   placeholders.GetAllEntries(out List filePlaceholders, out List folderPlaceholders);
                   filePlaceholders.ShouldNotBeNull();
                   filePlaceholders.Count.ShouldEqual(0);

                   folderPlaceholders.ShouldNotBeNull();
                   this.PlaceholderListShouldMatch(expectedPlacholders, folderPlaceholders);
               });
        }

        [TestCase]
        public void GetAllEntriesReturnsMultiple()
        {
            List expectedFilePlacholders = new List();
            expectedFilePlacholders.Add(new PlaceholderTable.PlaceholderData() { Path = DefaultPath, PathType = PlaceholderTable.PlaceholderData.PlaceholderType.File, Sha = DefaultSha });
            List expectedFolderPlacholders = new List();
            expectedFolderPlacholders.Add(new PlaceholderTable.PlaceholderData() { Path = "test1", PathType = PlaceholderTable.PlaceholderData.PlaceholderType.PartialFolder, Sha = null });
            expectedFolderPlacholders.Add(new PlaceholderTable.PlaceholderData() { Path = "test2", PathType = PlaceholderTable.PlaceholderData.PlaceholderType.ExpandedFolder, Sha = null });
            expectedFolderPlacholders.Add(new PlaceholderTable.PlaceholderData() { Path = "test3", PathType = PlaceholderTable.PlaceholderData.PlaceholderType.PossibleTombstoneFolder, Sha = null });
            this.TestTableWithReader(
               (placeholders, mockCommand, mockReader) =>
               {
                   this.SetupMockReader(mockReader, expectedFilePlacholders.Union(expectedFolderPlacholders).ToList());
                   mockCommand.SetupSet(x => x.CommandText = "SELECT path, pathType, sha FROM Placeholder;");

                   placeholders.GetAllEntries(out List filePlaceholders, out List folderPlaceholders);
                   filePlaceholders.ShouldNotBeNull();
                   this.PlaceholderListShouldMatch(expectedFilePlacholders, filePlaceholders);

                   folderPlaceholders.ShouldNotBeNull();
                   this.PlaceholderListShouldMatch(expectedFolderPlacholders, folderPlaceholders);
               });
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void AddFilePlaceholderDataWithNullShaThrowsException()
        {
            PlaceholderTable.PlaceholderData placeholderData = new PlaceholderTable.PlaceholderData()
            {
                Path = DefaultPath,
                PathType = PlaceholderTable.PlaceholderData.PlaceholderType.File,
                Sha = null
            };

            GVFSDatabaseException ex = Assert.Throws(() => this.TestPlaceholdersInsert(
                placeholders => placeholders.AddPlaceholderData(placeholderData),
                DefaultPath,
                PathTypeFile,
                sha: null,
                throwException: true));
            ex.Message.ShouldEqual($"Invalid SHA 'null' for file {DefaultPath}");
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void AddPlaceholderDataThrowsGVFSDatabaseException()
        {
            PlaceholderTable.PlaceholderData placeholderData = new PlaceholderTable.PlaceholderData()
            {
                Path = DefaultPath,
                PathType = PlaceholderTable.PlaceholderData.PlaceholderType.File,
                Sha = DefaultSha
            };

            GVFSDatabaseException ex = Assert.Throws(() => this.TestPlaceholdersInsert(
                placeholders => placeholders.AddPlaceholderData(placeholderData),
                DefaultPath,
                PathTypeFile,
                DefaultSha,
                throwException: true));
            ex.Message.ShouldEqual($"PlaceholderTable.Insert({DefaultPath}, {PlaceholderTable.PlaceholderData.PlaceholderType.File}, {DefaultSha}) Exception");
            ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
        }

        [TestCase]
        public void AddPlaceholderDataWithFile()
        {
            PlaceholderTable.PlaceholderData placeholderData = new PlaceholderTable.PlaceholderData()
            {
                Path = DefaultPath,
                PathType = PlaceholderTable.PlaceholderData.PlaceholderType.File,
                Sha = DefaultSha
            };

            this.TestPlaceholdersInsert(
                placeholders => placeholders.AddPlaceholderData(placeholderData),
                DefaultPath,
                PathTypeFile,
                DefaultSha);
        }

        [TestCase]
        public void AddPlaceholderDataWithPartialFolder()
        {
            PlaceholderTable.PlaceholderData placeholderData = new PlaceholderTable.PlaceholderData()
            {
                Path = DefaultPath,
                PathType = PlaceholderTable.PlaceholderData.PlaceholderType.PartialFolder,
                Sha = null
            };

            this.TestPlaceholdersInsert(
                placeholders => placeholders.AddPlaceholderData(placeholderData),
                DefaultPath,
                PathTypePartialFolder,
                sha: null);
        }

        [TestCase]
        public void AddPlaceholderDataWithExpandedFolder()
        {
            PlaceholderTable.PlaceholderData placeholderData = new PlaceholderTable.PlaceholderData()
            {
                Path = DefaultPath,
                PathType = PlaceholderTable.PlaceholderData.PlaceholderType.ExpandedFolder,
                Sha = null
            };

            this.TestPlaceholdersInsert(
                placeholders => placeholders.AddPlaceholderData(placeholderData),
                DefaultPath,
                PathTypeExpandedFolder,
                sha: null);
        }

        [TestCase]
        public void AddPlaceholderDataWithPossibleTombstoneFolder()
        {
            PlaceholderTable.PlaceholderData placeholderData = new PlaceholderTable.PlaceholderData()
            {
                Path = DefaultPath,
                PathType = PlaceholderTable.PlaceholderData.PlaceholderType.PossibleTombstoneFolder,
                Sha = null
            };

            this.TestPlaceholdersInsert(
                placeholders => placeholders.AddPlaceholderData(placeholderData),
                DefaultPath,
                PathTypePossibleTombstoneFolder,
                sha: null);
        }

        [TestCase]
        public void AddFileTest()
        {
            this.TestPlaceholdersInsert(
                placeholders => placeholders.AddFile(DefaultPath, DefaultSha),
                DefaultPath,
                PathTypeFile,
                DefaultSha);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void AddFileWithNullShaThrowsException()
        {
            GVFSDatabaseException ex = Assert.Throws(() => this.TestPlaceholdersInsert(
                placeholders => placeholders.AddFile(DefaultPath, sha: null),
                DefaultPath,
                PathTypeFile,
                sha: null,
                throwException: true));
            ex.Message.ShouldEqual($"Invalid SHA 'null' for file {DefaultPath}");
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void AddFileWithEmptyShaThrowsException()
        {
            string emptySha = string.Empty;
            GVFSDatabaseException ex = Assert.Throws(() => this.TestPlaceholdersInsert(
                placeholders => placeholders.AddFile(DefaultPath, emptySha),
                DefaultPath,
                PathTypeFile,
                emptySha,
                throwException: true));
            ex.Message.ShouldEqual($"Invalid SHA '' for file {DefaultPath}");
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void AddFileWithInvalidLengthShaThrowsException()
        {
            string badSha = "BAD SHA";
            GVFSDatabaseException ex = Assert.Throws(() => this.TestPlaceholdersInsert(
                placeholders => placeholders.AddFile(DefaultPath, badSha),
                DefaultPath,
                PathTypeFile,
                badSha,
                throwException: true));
            ex.Message.ShouldEqual($"Invalid SHA '{badSha}' for file {DefaultPath}");
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void AddFileThrowsGVFSDatabaseException()
        {
            GVFSDatabaseException ex = Assert.Throws(() => this.TestPlaceholdersInsert(
                placeholders => placeholders.AddFile(DefaultPath, DefaultSha),
                DefaultPath,
                PathTypeFile,
                DefaultSha,
                throwException: true));
            ex.Message.ShouldEqual($"PlaceholderTable.Insert({DefaultPath}, {PlaceholderTable.PlaceholderData.PlaceholderType.File}, {DefaultSha}) Exception");
            ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
        }

        [TestCase]
        public void AddPartialFolder()
        {
            this.TestPlaceholdersInsert(
                placeholders => placeholders.AddPartialFolder(DefaultPath, sha: null),
                DefaultPath,
                PathTypePartialFolder,
                sha: null);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void AddPartialFolderThrowsGVFSDatabaseException()
        {
            GVFSDatabaseException ex = Assert.Throws(() => this.TestPlaceholdersInsert(
                placeholders => placeholders.AddPartialFolder(DefaultPath, sha: null),
                DefaultPath,
                PathTypePartialFolder,
                sha: null,
                throwException: true));
            ex.Message.ShouldEqual($"PlaceholderTable.Insert({DefaultPath}, {PlaceholderTable.PlaceholderData.PlaceholderType.PartialFolder}, ) Exception");
            ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
        }

        [TestCase]
        public void AddExpandedFolder()
        {
            this.TestPlaceholdersInsert(
                placeholders => placeholders.AddExpandedFolder(DefaultPath),
                DefaultPath,
                PathTypeExpandedFolder,
                sha: null);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void AddExpandedFolderThrowsGVFSDatabaseException()
        {
            GVFSDatabaseException ex = Assert.Throws(() => this.TestPlaceholdersInsert(
                placeholders => placeholders.AddExpandedFolder(DefaultPath),
                DefaultPath,
                PathTypeExpandedFolder,
                sha: null,
                throwException: true));
            ex.Message.ShouldEqual($"PlaceholderTable.Insert({DefaultPath}, {PlaceholderTable.PlaceholderData.PlaceholderType.ExpandedFolder}, ) Exception");
            ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
        }

        [TestCase]
        public void AddPossibleTombstoneFolder()
        {
            this.TestPlaceholdersInsert(
                placeholders => placeholders.AddPossibleTombstoneFolder(DefaultPath),
                DefaultPath,
                PathTypePossibleTombstoneFolder,
                sha: null);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void AddPossibleTombstoneFolderThrowsGVFSDatabaseException()
        {
            GVFSDatabaseException ex = Assert.Throws(() => this.TestPlaceholdersInsert(
                placeholders => placeholders.AddPossibleTombstoneFolder(DefaultPath),
                DefaultPath,
                PathTypePossibleTombstoneFolder,
                sha: null,
                throwException: true));
            ex.Message.ShouldEqual($"PlaceholderTable.Insert({DefaultPath}, {PlaceholderTable.PlaceholderData.PlaceholderType.PossibleTombstoneFolder}, ) Exception");
            ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
        }

        [TestCase]
        public void RemoveTest()
        {
            this.TestTable(
                (placeholders, mockCommand) =>
                {
                    Mock mockParameter = new Mock(MockBehavior.Strict);
                    mockParameter.SetupSet(x => x.ParameterName = "@path");
                    mockParameter.SetupSet(x => x.DbType = DbType.String);
                    mockParameter.SetupSet(x => x.Value = DefaultPath);

                    Mock mockParameters = new Mock(MockBehavior.Strict);
                    mockParameters.Setup(x => x.Add(mockParameter.Object)).Returns(0);

                    mockCommand.SetupSet(x => x.CommandText = "DELETE FROM Placeholder WHERE path = @path;");
                    mockCommand.Setup(x => x.CreateParameter()).Returns(mockParameter.Object);
                    mockCommand.SetupGet(x => x.Parameters).Returns(mockParameters.Object);
                    mockCommand.Setup(x => x.ExecuteNonQuery()).Returns(1);

                    placeholders.Remove(DefaultPath);

                    mockParameters.VerifyAll();
                    mockParameter.VerifyAll();
                });
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void RemoveThrowsGVFSDatabaseException()
        {
            this.TestTable(
                (placeholders, mockCommand) =>
                {
                    mockCommand.SetupSet(x => x.CommandText = "DELETE FROM Placeholder WHERE path = @path;").Throws(new Exception(DefaultExceptionMessage));

                    GVFSDatabaseException ex = Assert.Throws(() => placeholders.Remove(DefaultPath));
                    ex.Message.ShouldEqual($"PlaceholderTable.Remove({DefaultPath}) Exception");
                    ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
                });
        }

        [TestCase]
        public void RemoveAllEntriesForFolderTest()
        {
            List expectedPlacholders = new List();
            this.TestTableWithReader(
                (placeholders, mockCommand, mockReader) =>
                {
                    this.SetupMockReader(mockReader, expectedPlacholders);

                    mockCommand.SetupSet(x => x.CommandText = "SELECT path, pathType, sha FROM Placeholder WHERE path = @path OR path LIKE @pathWithDirectorySeparator;");

                    Mock mockParameter = new Mock(MockBehavior.Strict);
                    mockParameter.SetupSet(x => x.ParameterName = "@path");
                    mockParameter.SetupSet(x => x.DbType = DbType.String);
                    mockParameter.SetupSet(x => x.Value = DefaultPath);

                    Mock mockParameter2 = new Mock(MockBehavior.Strict);
                    mockParameter2.SetupSet(x => x.ParameterName = "@pathWithDirectorySeparator");
                    mockParameter2.SetupSet(x => x.DbType = DbType.String);
                    mockParameter2.SetupSet(x => x.Value = DefaultPath + Path.DirectorySeparatorChar + "%");

                    Mock mockParameters = new Mock(MockBehavior.Strict);
                    mockParameters.Setup(x => x.Add(mockParameter.Object)).Returns(0);
                    mockParameters.Setup(x => x.Add(mockParameter2.Object)).Returns(0);

                    mockCommand.SetupSet(x => x.CommandText = "DELETE FROM Placeholder WHERE path = @path OR path LIKE @pathWithDirectorySeparator;");
                    mockCommand.SetupSequence(x => x.CreateParameter())
                        .Returns(mockParameter.Object)
                        .Returns(mockParameter2.Object);
                    mockCommand.SetupGet(x => x.Parameters).Returns(mockParameters.Object);
                    mockCommand.Setup(x => x.ExecuteNonQuery()).Returns(1);

                    placeholders.RemoveAllEntriesForFolder(DefaultPath);

                    mockParameters.VerifyAll();
                    mockParameter.VerifyAll();
                });
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void RemoveAllEntriesForFolderThrowsGVFSDatabaseException()
        {
            this.TestTable(
                (placeholders, mockCommand) =>
                {
                    mockCommand.SetupSet(x => x.CommandText = "SELECT path, pathType, sha FROM Placeholder WHERE path = @path OR path LIKE @pathWithDirectorySeparator;").Throws(new Exception(DefaultExceptionMessage));

                    GVFSDatabaseException ex = Assert.Throws(() => placeholders.RemoveAllEntriesForFolder(DefaultPath));
                    ex.Message.ShouldEqual($"PlaceholderTable.RemoveAllEntriesForFolder({DefaultPath}) Exception");
                    ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
                });
        }

        protected override PlaceholderTable TableFactory(IGVFSConnectionPool pool)
        {
            return new PlaceholderTable(pool);
        }

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

        private void TestPlaceholdersInsert(Action testCode, string path, int pathType, string sha, bool throwException = false)
        {
            this.TestTable(
                (placeholders, mockCommand) =>
                {
                    Mock mockPathParameter = new Mock(MockBehavior.Strict);
                    mockPathParameter.SetupSet(x => x.ParameterName = "@path");
                    mockPathParameter.SetupSet(x => x.DbType = DbType.String);
                    mockPathParameter.SetupSet(x => x.Value = path);
                    Mock mockPathTypeParameter = new Mock(MockBehavior.Strict);
                    mockPathTypeParameter.SetupSet(x => x.ParameterName = "@pathType");
                    mockPathTypeParameter.SetupSet(x => x.DbType = DbType.Int32);
                    mockPathTypeParameter.SetupSet(x => x.Value = pathType);
                    Mock mockShaParameter = new Mock(MockBehavior.Strict);
                    mockShaParameter.SetupSet(x => x.ParameterName = "@sha");
                    mockShaParameter.SetupSet(x => x.DbType = DbType.String);
                    if (sha == null)
                    {
                        mockShaParameter.SetupSet(x => x.Value = DBNull.Value);
                    }
                    else
                    {
                        mockShaParameter.SetupSet(x => x.Value = sha);
                    }

                    Mock mockParameters = new Mock(MockBehavior.Strict);
                    mockParameters.Setup(x => x.Add(mockPathParameter.Object)).Returns(0);
                    mockParameters.Setup(x => x.Add(mockPathTypeParameter.Object)).Returns(0);
                    mockParameters.Setup(x => x.Add(mockShaParameter.Object)).Returns(0);

                    mockCommand.Setup(x => x.CreateParameter()).Returns(mockPathParameter.Object);
                    mockCommand.SetupSequence(x => x.CreateParameter())
                        .Returns(mockPathParameter.Object)
                        .Returns(mockPathTypeParameter.Object)
                        .Returns(mockShaParameter.Object);
                    mockCommand.SetupGet(x => x.Parameters).Returns(mockParameters.Object);

                    mockCommand.SetupSet(x => x.CommandText = "INSERT OR REPLACE INTO Placeholder (path, pathType, sha) VALUES (@path, @pathType, @sha);");
                    if (throwException)
                    {
                        mockCommand.Setup(x => x.ExecuteNonQuery()).Throws(new Exception(DefaultExceptionMessage));
                    }
                    else
                    {
                        mockCommand.Setup(x => x.ExecuteNonQuery()).Returns(1);
                    }

                    testCode(placeholders);

                    mockParameters.VerifyAll();
                    mockPathParameter.VerifyAll();
                    mockPathTypeParameter.VerifyAll();
                    mockShaParameter.VerifyAll();
                });
        }

        private void SetupMockReader(Mock mockReader, List data)
        {
            int readCalls = -1;
            mockReader.Setup(x => x.Read()).Returns(() =>
            {
                ++readCalls;
                return readCalls < data.Count;
            });

            if (data.Count > 0)
            {
                mockReader.Setup(x => x.GetString(0)).Returns(() => data[readCalls].Path);
                mockReader.Setup(x => x.GetByte(1)).Returns(() => (byte)data[readCalls].PathType);
                mockReader.Setup(x => x.IsDBNull(2)).Returns(() => data[readCalls].Sha == null);

                if (data.Any(x => !x.IsFolder))
                {
                    mockReader.Setup(x => x.GetString(2)).Returns(() => data[readCalls].Sha);
                }
            }
        }

        private void PlaceholderListShouldMatch(IReadOnlyList expected, IReadOnlyList actual)
        {
            actual.Count.ShouldEqual(expected.Count);

            for (int i = 0; i < actual.Count; i++)
            {
                this.PlaceholderDataShouldMatch(expected[i], actual[i]);
            }
        }

        private void PlaceholderDataShouldMatch(IPlaceholderData expected, IPlaceholderData actual)
        {
            actual.Path.ShouldEqual(expected.Path);
            actual.IsFolder.ShouldEqual(expected.IsFolder);
            actual.IsExpandedFolder.ShouldEqual(expected.IsExpandedFolder);
            actual.IsPossibleTombstoneFolder.ShouldEqual(expected.IsPossibleTombstoneFolder);
            actual.Sha.ShouldEqual(expected.Sha);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/Database/SparseTableTests.cs
================================================
using GVFS.Common.Database;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;

namespace GVFS.UnitTests.Common.Database
{
    [TestFixture]
    public class SparseTableTests : TableTests
    {
        private const string GetAllCommandString = "SELECT path FROM Sparse;";
        private const string RemoveCommandString = "DELETE FROM Sparse WHERE path = @path;";
        private const string AddCommandString = "INSERT OR REPLACE INTO Sparse (path) VALUES (@path);";
        private static readonly string DefaultFolderPath = Path.Combine("GVFS", "GVFS");

        private static PathData[] pathsToTest = new[]
        {
            new PathData("GVFS", "GVFS"),
            new PathData(CombineAlt("GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")),
            new PathData(CombineAltForTrim(Path.AltDirectorySeparatorChar, "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")),
            new PathData(CombineAltForTrim(Path.DirectorySeparatorChar, "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")),
            new PathData(CombineAltForTrim(' ', "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")),
            new PathData(CombineAltForTrim('\r', "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")),
            new PathData(CombineAltForTrim('\n', "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")),
            new PathData(CombineAltForTrim('\t', "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")),
        };

        protected override string CreateTableCommandString => "CREATE TABLE IF NOT EXISTS [Sparse] (path TEXT PRIMARY KEY COLLATE NOCASE) WITHOUT ROWID;";

        [TestCase]
        public void GetAllWithNoResults()
        {
            this.TestTableWithReader((sparseTable, mockCommand, mockReader) =>
            {
                mockReader.Setup(x => x.Read()).Returns(false);
                mockCommand.SetupSet(x => x.CommandText = GetAllCommandString);

                HashSet sparseEntries = sparseTable.GetAll();
                sparseEntries.Count.ShouldEqual(0);
            });
        }

        [TestCase]
        public void GetAllWithWithOneResult()
        {
            this.TestTableWithReader((sparseTable, mockCommand, mockReader) =>
            {
                mockCommand.SetupSet(x => x.CommandText = GetAllCommandString);
                this.SetupMockReader(mockReader, DefaultFolderPath);

                HashSet sparseEntries = sparseTable.GetAll();
                sparseEntries.Count.ShouldEqual(1);
                sparseEntries.First().ShouldEqual(DefaultFolderPath);
            });
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void GetAllThrowsGVFSDatabaseException()
        {
            this.TestTableWithReader(
                (sparseTable, mockCommand, mockReader) =>
                {
                    mockCommand.SetupSet(x => x.CommandText = GetAllCommandString);
                    mockReader.Setup(x => x.Read()).Throws(new Exception(DefaultExceptionMessage));
                    GVFSDatabaseException ex = Assert.Throws(() => sparseTable.GetAll());
                    ex.Message.ShouldContain("SparseTable.GetAll Exception:");
                    ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
                });
        }

        [TestCase]
        public void AddVariousPaths()
        {
            foreach (PathData pathData in pathsToTest)
            {
                this.TestSparseTableAddOrRemove(
                    isAdd: true,
                    pathToPass: pathData.PathToPassMethod,
                    expectedPath: pathData.ExpectedPathInTable);
            }
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void AddThrowsGVFSDatabaseException()
        {
            this.TestSparseTableAddOrRemove(
                isAdd: true,
                pathToPass: DefaultFolderPath,
                expectedPath: DefaultFolderPath,
                throwException: true);
        }

        [TestCase]
        public void RemoveVariousPaths()
        {
            foreach (PathData pathData in pathsToTest)
            {
                this.TestSparseTableAddOrRemove(
                    isAdd: false,
                    pathToPass: pathData.PathToPassMethod,
                    expectedPath: pathData.ExpectedPathInTable);
            }
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void RemoveThrowsGVFSDatabaseException()
        {
            this.TestSparseTableAddOrRemove(
                isAdd: false,
                pathToPass: DefaultFolderPath,
                expectedPath: DefaultFolderPath,
                throwException: true);
        }

        protected override SparseTable TableFactory(IGVFSConnectionPool pool)
        {
            return new SparseTable(pool);
        }

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

        private static string CombineAltForTrim(char character, params string[] folders)
        {
            return $"{character}{CombineAlt(folders)}{character}";
        }

        private static string CombineAlt(params string[] folders)
        {
            return string.Join(Path.AltDirectorySeparatorChar.ToString(), folders);
        }

        private void SetupMockReader(Mock mockReader, params string[] data)
        {
            int readCalls = -1;
            mockReader.Setup(x => x.Read()).Returns(() =>
            {
                ++readCalls;
                return readCalls < data.Length;
            });

            if (data.Length > 0)
            {
                mockReader.Setup(x => x.GetString(0)).Returns(() => data[readCalls]);
            }
        }

        private void TestSparseTableAddOrRemove(bool isAdd, string pathToPass, string expectedPath, bool throwException = false)
        {
            this.TestTable(
                (sparseTable, mockCommand) =>
                {
                    Mock mockParameter = new Mock(MockBehavior.Strict);
                    mockParameter.SetupSet(x => x.ParameterName = "@path");
                    mockParameter.SetupSet(x => x.DbType = DbType.String);
                    mockParameter.SetupSet(x => x.Value = expectedPath);

                    Mock mockParameters = new Mock(MockBehavior.Strict);
                    mockParameters.Setup(x => x.Add(mockParameter.Object)).Returns(0);

                    mockCommand.Setup(x => x.CreateParameter()).Returns(mockParameter.Object);
                    mockCommand.SetupGet(x => x.Parameters).Returns(mockParameters.Object);
                    if (throwException)
                    {
                        mockCommand.Setup(x => x.ExecuteNonQuery()).Throws(new Exception(DefaultExceptionMessage));
                    }
                    else
                    {
                        mockCommand.Setup(x => x.ExecuteNonQuery()).Returns(1);
                    }

                    if (isAdd)
                    {
                        mockCommand.SetupSet(x => x.CommandText = AddCommandString);
                        if (throwException)
                        {
                            GVFSDatabaseException ex = Assert.Throws(() => sparseTable.Add(pathToPass));
                            ex.Message.ShouldContain($"SparseTable.Add({expectedPath}) Exception");
                            ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
                        }
                        else
                        {
                            sparseTable.Add(pathToPass);
                        }
                    }
                    else
                    {
                        mockCommand.SetupSet(x => x.CommandText = RemoveCommandString);
                        if (throwException)
                        {
                            GVFSDatabaseException ex = Assert.Throws(() => sparseTable.Remove(pathToPass));
                            ex.Message.ShouldContain($"SparseTable.Remove({expectedPath}) Exception");
                            ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage);
                        }
                        else
                        {
                            sparseTable.Remove(pathToPass);
                        }
                    }

                    mockParameters.VerifyAll();
                    mockParameter.VerifyAll();
                });
        }

        private class PathData
        {
            public PathData(string path, string expected)
            {
                this.PathToPassMethod = path;
                this.ExpectedPathInTable = expected;
            }

            public string PathToPassMethod { get; }
            public string ExpectedPathInTable { get; }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/Database/TableTests.cs
================================================
using GVFS.Common.Database;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using Moq;
using NUnit.Framework;
using System;
using System.Data;

namespace GVFS.UnitTests.Common.Database
{
    public abstract class TableTests
    {
        protected const string DefaultExceptionMessage = "Somethind bad.";

        protected abstract string CreateTableCommandString { get; }

        [TestCase]
        public void ConstructorTest()
        {
            Mock mockConnectionPool = new Mock(MockBehavior.Strict);
            T table = this.TableFactory(mockConnectionPool.Object);
            mockConnectionPool.VerifyAll();
        }

        [TestCase]
        public void CreateTableTest()
        {
            Mock mockCommand = new Mock(MockBehavior.Strict);
            mockCommand.SetupSet(x => x.CommandText = this.CreateTableCommandString);
            mockCommand.Setup(x => x.ExecuteNonQuery()).Returns(1);
            mockCommand.Setup(x => x.Dispose());

            Mock mockConnection = new Mock(MockBehavior.Strict);
            mockConnection.Setup(x => x.CreateCommand()).Returns(mockCommand.Object);

            this.CreateTable(mockConnection.Object, caseSensitiveFileSystem: false);
            mockCommand.VerifyAll();
            mockConnection.VerifyAll();
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void CreateTableThrowsExceptionNotWrappedInGVFSDatabaseException()
        {
            Mock mockCommand = new Mock(MockBehavior.Strict);
            mockCommand.SetupSet(x => x.CommandText = this.CreateTableCommandString);
            mockCommand.Setup(x => x.ExecuteNonQuery()).Throws(new Exception(DefaultExceptionMessage));
            mockCommand.Setup(x => x.Dispose());

            Mock mockConnection = new Mock(MockBehavior.Strict);
            mockConnection.Setup(x => x.CreateCommand()).Returns(mockCommand.Object);

            Exception ex = Assert.Throws(() => this.CreateTable(mockConnection.Object, caseSensitiveFileSystem: false));
            ex.Message.ShouldEqual(DefaultExceptionMessage);
            mockCommand.VerifyAll();
            mockConnection.VerifyAll();
        }

        protected abstract T TableFactory(IGVFSConnectionPool pool);
        protected abstract void CreateTable(IDbConnection connection, bool caseSensitiveFileSystem);

        protected void TestTableWithReader(Action, Mock> testCode)
        {
            this.TestTable(
                (table, mockCommand) =>
                {
                    Mock mockReader = new Mock(MockBehavior.Strict);
                    mockReader.Setup(x => x.Dispose());

                    mockCommand.Setup(x => x.ExecuteReader()).Returns(mockReader.Object);
                    testCode(table, mockCommand, mockReader);
                    mockReader.Verify(x => x.Dispose(), Times.Once);
                    mockReader.VerifyAll();
                });
        }

        protected void TestTable(Action> testCode)
        {
            Mock mockCommand = new Mock(MockBehavior.Strict);
            mockCommand.Setup(x => x.Dispose());

            Mock mockConnection = new Mock(MockBehavior.Strict);
            mockConnection.Setup(x => x.CreateCommand()).Returns(mockCommand.Object);
            mockConnection.Setup(x => x.Dispose());

            Mock mockConnectionPool = new Mock(MockBehavior.Strict);
            mockConnectionPool.Setup(x => x.GetConnection()).Returns(mockConnection.Object);

            T table = this.TableFactory(mockConnectionPool.Object);
            testCode(table, mockCommand);

            mockCommand.Verify(x => x.Dispose(), Times.Once);
            mockCommand.VerifyAll();
            mockConnection.Verify(x => x.Dispose(), Times.Once);
            mockConnection.VerifyAll();
            mockConnectionPool.VerifyAll();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/EnlistmentHydrationSummaryTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using GVFS.UnitTests.Mock.Git;
using GVFS.Virtualization.Projection;
using NUnit.Framework;
using System;
using System.IO;
using System.Text;
using System.Threading;
using static GVFS.Virtualization.Projection.GitIndexProjection.GitIndexParser;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class EnlistmentHydrationSummaryTests
    {
        [TestCase]
        public void CountIndexFolders_FlatDirectories()
        {
            int count = CountFoldersInIndex(new[] { "src/file1.cs", "test/file2.cs" });
            Assert.AreEqual(2, count); // "src", "test"
        }

        [TestCase]
        public void CountIndexFolders_NestedDirectories()
        {
            int count = CountFoldersInIndex(new[] { "a/b/c/file1.cs", "a/b/file2.cs", "x/file3.cs" });
            Assert.AreEqual(4, count); // "a", "a/b", "a/b/c", "x"
        }

        [TestCase]
        public void CountIndexFolders_RootFilesOnly()
        {
            int count = CountFoldersInIndex(new[] { "README.md", ".gitignore" });
            Assert.AreEqual(0, count);
        }

        [TestCase]
        public void CountIndexFolders_EmptyIndex()
        {
            int count = CountFoldersInIndex(new string[0]);
            Assert.AreEqual(0, count);
        }

        [TestCase]
        public void CountIndexFolders_DeepNesting()
        {
            int count = CountFoldersInIndex(new[] { "a/b/c/d/e/file.txt" });
            Assert.AreEqual(5, count); // "a", "a/b", "a/b/c", "a/b/c/d", "a/b/c/d/e"
        }

        [TestCase]
        public void CountIndexFolders_ExcludesNonSkipWorktree()
        {
            // Entries without skip-worktree and with NoConflicts merge state are not
            // projected, so their directories should not be counted.
            IndexEntryInfo[] entries = new[]
            {
                new IndexEntryInfo("src/file1.cs", skipWorktree: true),
                new IndexEntryInfo("vendor/lib/file2.cs", skipWorktree: false),
            };

            int count = CountFoldersInIndex(entries);
            Assert.AreEqual(1, count); // only "src"
        }

        [TestCase]
        public void CountIndexFolders_ExcludesCommonAncestor()
        {
            // CommonAncestor entries are excluded even when skip-worktree is set.
            IndexEntryInfo[] entries = new[]
            {
                new IndexEntryInfo("src/file1.cs", skipWorktree: true),
                new IndexEntryInfo("conflict/file2.cs", skipWorktree: true, mergeState: MergeStage.CommonAncestor),
            };

            int count = CountFoldersInIndex(entries);
            Assert.AreEqual(1, count); // only "src"
        }

        [TestCase]
        public void CountIndexFolders_IncludesYoursMergeState()
        {
            // Yours merge-state entries are projected even without skip-worktree.
            IndexEntryInfo[] entries = new[]
            {
                new IndexEntryInfo("src/file1.cs", skipWorktree: true),
                new IndexEntryInfo("merge/file2.cs", skipWorktree: false, mergeState: MergeStage.Yours),
            };

            int count = CountFoldersInIndex(entries);
            Assert.AreEqual(2, count); // "src" and "merge"
        }

        private static int CountFoldersInIndex(string[] paths)
        {
            byte[] indexBytes = CreateV4Index(paths);
            using (MemoryStream stream = new MemoryStream(indexBytes))
            {
                return GitIndexProjection.CountIndexFolders(new MockTracer(), stream);
            }
        }

        private static int CountFoldersInIndex(IndexEntryInfo[] entries)
        {
            byte[] indexBytes = CreateV4Index(entries);
            using (MemoryStream stream = new MemoryStream(indexBytes))
            {
                return GitIndexProjection.CountIndexFolders(new MockTracer(), stream);
            }
        }

        /// 
        /// Create a minimal git index v4 binary matching the format GitIndexGenerator produces.
        /// Uses prefix-compression for paths (v4 format).
        /// 
        private static byte[] CreateV4Index(string[] paths)
        {
            IndexEntryInfo[] entries = new IndexEntryInfo[paths.Length];
            for (int i = 0; i < paths.Length; i++)
            {
                entries[i] = new IndexEntryInfo(paths[i], skipWorktree: true);
            }

            return CreateV4Index(entries);
        }

        private static byte[] CreateV4Index(IndexEntryInfo[] entries)
        {
            // Stat entry header matching GitIndexGenerator.EntryHeader:
            // 40 bytes with file mode 0x81A4 (regular file, 644) at offset 24-27
            byte[] entryHeader = new byte[40];
            entryHeader[26] = 0x81;
            entryHeader[27] = 0xA4;

            using (MemoryStream ms = new MemoryStream())
            using (BinaryWriter bw = new BinaryWriter(ms))
            {
                // Header
                bw.Write(new byte[] { (byte)'D', (byte)'I', (byte)'R', (byte)'C' });
                WriteBigEndian32(bw, 4); // version 4
                WriteBigEndian32(bw, (uint)entries.Length);

                string previousPath = string.Empty;
                foreach (IndexEntryInfo entry in entries)
                {
                    // 40-byte stat entry header with valid file mode
                    bw.Write(entryHeader);
                    // 20 bytes SHA-1 (zeros)
                    bw.Write(new byte[20]);
                    // Flags: path length in low 12 bits, merge state in bits 12-13, extended bit 14
                    byte[] pathBytes = Encoding.UTF8.GetBytes(entry.Path);
                    ushort flags = (ushort)(Math.Min(pathBytes.Length, 0xFFF) | 0x4000 | ((ushort)entry.MergeState << 12));
                    WriteBigEndian16(bw, flags);
                    // Extended flags: skip-worktree bit
                    ushort extendedFlags = entry.SkipWorktree ? (ushort)0x4000 : (ushort)0;
                    WriteBigEndian16(bw, extendedFlags);

                    // V4 prefix compression: compute common prefix with previous path
                    int commonLen = 0;
                    int maxCommon = Math.Min(previousPath.Length, entry.Path.Length);
                    while (commonLen < maxCommon && previousPath[commonLen] == entry.Path[commonLen])
                    {
                        commonLen++;
                    }

                    int replaceLen = previousPath.Length - commonLen;
                    string suffix = entry.Path.Substring(commonLen);

                    // Write replace length as varint
                    WriteVarint(bw, replaceLen);
                    // Write suffix + null terminator
                    bw.Write(Encoding.UTF8.GetBytes(suffix));
                    bw.Write((byte)0);

                    previousPath = entry.Path;
                }

                return ms.ToArray();
            }
        }

        private struct IndexEntryInfo
        {
            public string Path;
            public bool SkipWorktree;
            public MergeStage MergeState;

            public IndexEntryInfo(string path, bool skipWorktree, MergeStage mergeState = MergeStage.NoConflicts)
            {
                this.Path = path;
                this.SkipWorktree = skipWorktree;
                this.MergeState = mergeState;
            }
        }

        private static void WriteBigEndian32(BinaryWriter bw, uint value)
        {
            bw.Write((byte)((value >> 24) & 0xFF));
            bw.Write((byte)((value >> 16) & 0xFF));
            bw.Write((byte)((value >> 8) & 0xFF));
            bw.Write((byte)(value & 0xFF));
        }

        private static void WriteBigEndian16(BinaryWriter bw, ushort value)
        {
            bw.Write((byte)((value >> 8) & 0xFF));
            bw.Write((byte)(value & 0xFF));
        }

        private static void WriteVarint(BinaryWriter bw, int value)
        {
            // Git index v4 varint encoding (same as ReadReplaceLength in GitIndexParser)
            if (value < 0x80)
            {
                bw.Write((byte)value);
                return;
            }

            byte[] bytes = new byte[5];
            int pos = 4;
            bytes[pos] = (byte)(value & 0x7F);
            value = (value >> 7) - 1;
            while (value >= 0)
            {
                pos--;
                bytes[pos] = (byte)(0x80 | (value & 0x7F));
                value = (value >> 7) - 1;
            }

            bw.Write(bytes, pos, 5 - pos);
        }
    }

    /// 
    /// Tests for EnlistmentHydrationSummary that require the full mock filesystem/context.
    /// 
    [TestFixture]
    public class EnlistmentHydrationSummaryContextTests
    {
        private MockFileSystem fileSystem;
        private MockTracer tracer;
        private GVFSContext context;
        private string gitParentPath;
        private MockDirectory enlistmentDirectory;

        [SetUp]
        public void Setup()
        {
            this.tracer = new MockTracer();

            string enlistmentRoot = Path.Combine("mock:", "GVFS", "UnitTests", "Repo");
            string statusCachePath = Path.Combine("mock:", "GVFS", "UnitTests", "Repo", GVFSPlatform.Instance.Constants.DotGVFSRoot, "gitStatusCache");

            MockGitProcess gitProcess = new MockGitProcess();
            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", gitProcess);
            enlistment.InitializeCachePathsFromKey("fake:\\gvfsSharedCache", "fakeCacheKey");

            this.gitParentPath = enlistment.WorkingDirectoryBackingRoot;

            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(
                this.tracer,
                this.fileSystem,
                new MockGitRepo(this.tracer, enlistment, this.fileSystem),
                enlistment);
        }

        [TestCase]
        public void GetIndexFileCount_IndexTooSmall_ReturnsNegativeOne()
        {
            string indexPath = Path.Combine(this.gitParentPath, ".git", "index");
            this.enlistmentDirectory.CreateFile(indexPath, "short", createDirectories: true);

            int result = EnlistmentHydrationSummary.GetIndexFileCount(
                this.context.Enlistment, this.context.FileSystem);

            Assert.AreEqual(-1, result);
        }

        [TestCase]
        public void CreateSummary_CancelledToken_ReturnsInvalidSummary()
        {
            // Set up a valid index file so CreateSummary gets past GetIndexFileCount
            // before hitting the first cancellation check.
            string indexPath = Path.Combine(this.gitParentPath, ".git", "index");
            byte[] indexBytes = new byte[12];
            indexBytes[11] = 100; // file count = 100 (big-endian)
            MockFile indexFile = new MockFile(indexPath, indexBytes);
            MockDirectory gitDir = this.enlistmentDirectory.FindDirectory(Path.Combine(this.gitParentPath, ".git"));
            gitDir.Files.Add(indexFile.FullName, indexFile);

            CancellationTokenSource cts = new CancellationTokenSource();
            cts.Cancel();

            Func dummyProvider = () => 0;
            EnlistmentHydrationSummary result = EnlistmentHydrationSummary.CreateSummary(
                this.context.Enlistment, this.context.FileSystem, this.context.Tracer, dummyProvider, cts.Token);

            Assert.IsFalse(result.IsValid);
            Assert.IsNull(result.Error);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/EpochConverterTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class EpochConverterTests
    {
        [TestCase]
        public void DateToEpochToDate()
        {
            DateTime time = new DateTime(2018, 12, 18, 8, 12, 13, DateTimeKind.Utc);
            DateTime converted = EpochConverter.FromUnixEpochSeconds(EpochConverter.ToUnixEpochSeconds(time));

            time.ShouldEqual(converted);
        }

        [TestCase]
        public void EpochToDateToEpoch()
        {
            long time = 15237623489;
            long converted = EpochConverter.ToUnixEpochSeconds(EpochConverter.FromUnixEpochSeconds(time));

            time.ShouldEqual(converted);
        }

        [TestCase]
        public void FixedDates()
        {
            DateTime[] times = new DateTime[]
            {
                new DateTime(2018, 12, 13, 20, 53, 30, DateTimeKind.Utc),
                new DateTime(2035, 1, 3, 5, 0, 59, DateTimeKind.Utc),
                new DateTime(1989, 12, 31, 23, 59, 59, DateTimeKind.Utc)
            };
            long[] epochs = new long[]
            {
                1544734410,
                2051413259,
                631151999
            };

            for (int i = 0; i < times.Length; i++)
            {
                long epoch = EpochConverter.ToUnixEpochSeconds(times[i]);
                epoch.ShouldEqual(epochs[i]);

                DateTime time = EpochConverter.FromUnixEpochSeconds(epochs[i]);
                time.ShouldEqual(times[i]);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/FileBasedDictionaryTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock;
using GVFS.UnitTests.Mock.FileSystem;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class FileBasedDictionaryTests
    {
        private const string MockEntryFileName = "mock:\\entries.dat";

        private const string TestKey = "akey";
        private const string TestValue = "avalue";
        private const string UpdatedTestValue = "avalue2";

        private const string TestEntry = "A {\"Key\":\"akey\",\"Value\":\"avalue\"}\r\n";
        private const string UpdatedTestEntry = "A {\"Key\":\"akey\",\"Value\":\"avalue2\"}\r\n";

        private const string TestKey2 = "bkey";
        private const string TestValue2 = "bvalue";
        private const string UpdatedTestValue2 = "bvalue2";

        private const string TestEntry2 = "A {\"Key\":\"bkey\",\"Value\":\"bvalue\"}\r\n";
        private const string UpdatedTestEntry2 = "A {\"Key\":\"bkey\",\"Value\":\"bvalue2\"}\r\n";

        [TestCase]
        public void ParsesExistingDataCorrectly()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem();
            FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry);

            string value;
            dut.TryGetValue(TestKey, out value).ShouldEqual(true);
            value.ShouldEqual(TestValue);
        }

        [TestCase]
        public void SetValueAndFlushWritesEntryToDisk()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem();
            FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty);
            dut.SetValueAndFlush(TestKey, TestValue);

            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry });
        }

        [TestCase]
        public void SetValuesAndFlushWritesEntriesToDisk()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem();
            FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty);
            dut.SetValuesAndFlush(
                new[]
                {
                    new KeyValuePair(TestKey, TestValue),
                    new KeyValuePair(TestKey2, TestValue2),
                });
            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry, TestEntry2 });
        }

        [TestCase]
        public void SetValuesAndFlushWritesNewEntryAndUpdatesExistingEntryOnDisk()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem();
            FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty);

            // Add TestKey to disk
            dut.SetValueAndFlush(TestKey, TestValue);
            fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldEqual(TestEntry);

            // This call to SetValuesAndFlush should update TestKey and write TestKey2
            dut.SetValuesAndFlush(
                new[]
                {
                    new KeyValuePair(TestKey, UpdatedTestValue),
                    new KeyValuePair(TestKey2, TestValue2),
                });
            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry, TestEntry2 });
        }

        [TestCase]
        public void SetValuesAndFlushWritesUpdatesExistingEntriesOnDisk()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem();
            FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty);

            dut.SetValuesAndFlush(
                new[]
                {
                    new KeyValuePair(TestKey, TestValue),
                    new KeyValuePair(TestKey2, TestValue2),
                });
            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry, TestEntry2 });

            dut.SetValuesAndFlush(
                new[]
                {
                    new KeyValuePair(TestKey, UpdatedTestValue),
                    new KeyValuePair(TestKey2, UpdatedTestValue2),
                });
            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry, UpdatedTestEntry2 });
        }

        [TestCase]
        public void SetValuesAndFlushUsesLastValueWhenKeyDuplicated()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem();
            FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty);

            dut.SetValuesAndFlush(
                new[]
                {
                    new KeyValuePair(TestKey, TestValue),
                    new KeyValuePair(TestKey, UpdatedTestValue),
                });
            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry });
        }

        [TestCase]
        public void SetValueAndFlushUpdatedEntryOnDisk()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem();
            FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry);
            dut.SetValueAndFlush(TestKey, UpdatedTestValue);

            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry });
        }

        [TestCase]
        [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)]
        public void SetValueAndFlushRecoversFromFailedOpenFileStream()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(
                openFileStreamFailurePath: MockEntryFileName + ".tmp",
                maxOpenFileStreamFailures: 5,
                fileExistsFailurePath: null,
                maxFileExistsFailures: 0,
                maxMoveAndOverwriteFileFailures: 5);

            FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty);
            dut.SetValueAndFlush(TestKey, TestValue);

            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry });
        }

        [TestCase]
        public void SetValueAndFlushRecoversFromDeletedTmp()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(
                openFileStreamFailurePath: null,
                maxOpenFileStreamFailures: 0,
                fileExistsFailurePath: MockEntryFileName + ".tmp",
                maxFileExistsFailures: 5,
                maxMoveAndOverwriteFileFailures: 0);

            FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty);
            dut.SetValueAndFlush(TestKey, TestValue);

            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry });
        }

        [TestCase]
        [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)]
        public void SetValueAndFlushRecoversFromFailedOverwrite()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(
                openFileStreamFailurePath: null,
                maxOpenFileStreamFailures: 0,
                fileExistsFailurePath: null,
                maxFileExistsFailures: 0,
                maxMoveAndOverwriteFileFailures: 5);

            FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty);
            dut.SetValueAndFlush(TestKey, TestValue);

            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry });
        }

        [TestCase]
        [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)]
        public void SetValueAndFlushRecoversFromDeletedTempAndFailedOverwrite()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(
                openFileStreamFailurePath: null,
                maxOpenFileStreamFailures: 0,
                fileExistsFailurePath: MockEntryFileName + ".tmp",
                maxFileExistsFailures: 5,
                maxMoveAndOverwriteFileFailures: 5);

            FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty);
            dut.SetValueAndFlush(TestKey, TestValue);

            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry });
        }

        [TestCase]
        [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)]
        public void SetValueAndFlushRecoversFromMixOfFailures()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(failuresAcrossOpenExistsAndOverwritePath: MockEntryFileName + ".tmp");

            FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty);
            dut.SetValueAndFlush(TestKey, TestValue);

            this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry });
        }

        [TestCase]
        public void DeleteFlushesToDisk()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem();
            FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry);
            dut.RemoveAndFlush(TestKey);

            fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldBeEmpty();
        }

        [TestCase]
        public void DeleteUnusedKeyFlushesToDisk()
        {
            FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem();
            FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry);
            dut.RemoveAndFlush("UnusedKey");

            fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldEqual(TestEntry);
        }

        private static FileBasedDictionary CreateFileBasedDictionary(FileBasedDictionaryFileSystem fs, string initialContents)
        {
            fs.ExpectedFiles.Add(MockEntryFileName, new ReusableMemoryStream(initialContents));

            fs.ExpectedOpenFileStreams.Add(MockEntryFileName + ".tmp", new ReusableMemoryStream(string.Empty));
            fs.ExpectedOpenFileStreams.Add(MockEntryFileName, fs.ExpectedFiles[MockEntryFileName]);

            string error;
            FileBasedDictionary dut;
            FileBasedDictionary.TryCreate(null, MockEntryFileName, fs, out dut, out error).ShouldEqual(true, error);
            dut.ShouldNotBeNull();

            // FileBasedDictionary should only open a file stream to the non-tmp file when being created.  At all other times it should
            // write to a tmp file and overwrite the non-tmp file
            fs.ExpectedOpenFileStreams.Remove(MockEntryFileName);

            return dut;
        }

        private void FileBasedDictionaryFileSystemShouldContain(
            FileBasedDictionaryFileSystem fs,
            IList expectedEntries)
        {
            string delimiter = "\r\n";
            string fileContents = fs.ExpectedFiles[MockEntryFileName].ReadAsString();
            fileContents.Substring(fileContents.Length - delimiter.Length).ShouldEqual(delimiter);

            // Remove the trailing delimiter
            fileContents = fileContents.Substring(0, fileContents.Length - delimiter.Length);

            string[] fileLines = fileContents.Split(new[] { delimiter }, StringSplitOptions.None);
            fileLines.Length.ShouldEqual(expectedEntries.Count);

            foreach (string expectedEntry in expectedEntries)
            {
                fileLines.ShouldContain(line => line.Equals(expectedEntry.Substring(0, expectedEntry.Length - delimiter.Length)));
            }
        }

        private class FileBasedDictionaryFileSystem : ConfigurableFileSystem
        {
            private int openFileStreamFailureCount;
            private int maxOpenFileStreamFailures;
            private string openFileStreamFailurePath;

            private int fileExistsFailureCount;
            private int maxFileExistsFailures;
            private string fileExistsFailurePath;

            private int moveAndOverwriteFileFailureCount;
            private int maxMoveAndOverwriteFileFailures;

            private string failuresAcrossOpenExistsAndOverwritePath;
            private int failuresAcrossOpenExistsAndOverwriteCount;

            public FileBasedDictionaryFileSystem()
            {
                this.ExpectedOpenFileStreams = new Dictionary();
            }

            public FileBasedDictionaryFileSystem(
                string openFileStreamFailurePath,
                int maxOpenFileStreamFailures,
                string fileExistsFailurePath,
                int maxFileExistsFailures,
                int maxMoveAndOverwriteFileFailures)
            {
                this.maxOpenFileStreamFailures = maxOpenFileStreamFailures;
                this.openFileStreamFailurePath = openFileStreamFailurePath;
                this.fileExistsFailurePath = fileExistsFailurePath;
                this.maxFileExistsFailures = maxFileExistsFailures;
                this.maxMoveAndOverwriteFileFailures = maxMoveAndOverwriteFileFailures;
                this.ExpectedOpenFileStreams = new Dictionary();
            }

            /// 
            /// Fail a mix of OpenFileStream, FileExists, and Overwrite.
            /// 
            /// 
            /// Order of failures will be:
            ///  1. OpenFileStream
            ///  2. FileExists
            ///  3. Overwrite
            /// 
            public FileBasedDictionaryFileSystem(string failuresAcrossOpenExistsAndOverwritePath)
            {
                this.failuresAcrossOpenExistsAndOverwritePath = failuresAcrossOpenExistsAndOverwritePath;
                this.ExpectedOpenFileStreams = new Dictionary();
            }

            public Dictionary ExpectedOpenFileStreams { get; }

            public override bool FileExists(string path)
            {
                if (this.maxFileExistsFailures > 0)
                {
                    if (this.fileExistsFailureCount < this.maxFileExistsFailures &&
                        string.Equals(path, this.fileExistsFailurePath, GVFSPlatform.Instance.Constants.PathComparison))
                    {
                        if (this.ExpectedFiles.ContainsKey(path))
                        {
                            this.ExpectedFiles.Remove(path);
                        }

                        ++this.fileExistsFailureCount;
                    }
                }
                else if (this.failuresAcrossOpenExistsAndOverwritePath != null)
                {
                    if (this.failuresAcrossOpenExistsAndOverwriteCount == 1 &&
                        string.Equals(path, this.failuresAcrossOpenExistsAndOverwritePath, GVFSPlatform.Instance.Constants.PathComparison))
                    {
                        if (this.ExpectedFiles.ContainsKey(path))
                        {
                            this.ExpectedFiles.Remove(path);
                        }

                        ++this.failuresAcrossOpenExistsAndOverwriteCount;
                    }
                }

                return this.ExpectedFiles.ContainsKey(path);
            }

            public override void MoveAndOverwriteFile(string sourceFileName, string destinationFilename)
            {
                if (this.maxMoveAndOverwriteFileFailures > 0)
                {
                    if (this.moveAndOverwriteFileFailureCount < this.maxMoveAndOverwriteFileFailures)
                    {
                        ++this.moveAndOverwriteFileFailureCount;
                        throw new Win32Exception();
                    }
                }
                else if (this.failuresAcrossOpenExistsAndOverwritePath != null)
                {
                    if (this.failuresAcrossOpenExistsAndOverwriteCount == 2)
                    {
                        ++this.failuresAcrossOpenExistsAndOverwriteCount;
                        throw new Win32Exception();
                    }
                }

                ReusableMemoryStream source;
                this.ExpectedFiles.TryGetValue(sourceFileName, out source).ShouldEqual(true, "Source file does not exist: " + sourceFileName);
                this.ExpectedFiles.ContainsKey(destinationFilename).ShouldEqual(true, "MoveAndOverwriteFile expects the destination file to exist: " + destinationFilename);

                this.ExpectedFiles.Remove(sourceFileName);
                this.ExpectedFiles[destinationFilename] = source;
            }

            public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk)
            {
                ReusableMemoryStream stream;
                this.ExpectedOpenFileStreams.TryGetValue(path, out stream).ShouldEqual(true, "Unexpected access of file: " + path);

                if (this.maxOpenFileStreamFailures > 0)
                {
                    if (this.openFileStreamFailureCount < this.maxOpenFileStreamFailures &&
                        string.Equals(path, this.openFileStreamFailurePath, GVFSPlatform.Instance.Constants.PathComparison))
                    {
                        ++this.openFileStreamFailureCount;

                        if (this.openFileStreamFailureCount % 2 == 0)
                        {
                            throw new IOException();
                        }
                        else
                        {
                            throw new UnauthorizedAccessException();
                        }
                    }
                }
                else if (this.failuresAcrossOpenExistsAndOverwritePath != null)
                {
                    if (this.failuresAcrossOpenExistsAndOverwriteCount == 0 &&
                        string.Equals(path, this.failuresAcrossOpenExistsAndOverwritePath, GVFSPlatform.Instance.Constants.PathComparison))
                    {
                        ++this.failuresAcrossOpenExistsAndOverwriteCount;
                        throw new IOException();
                    }
                }

                if (fileMode == FileMode.Create)
                {
                    this.ExpectedFiles[path] = new ReusableMemoryStream(string.Empty);
                }

                this.ExpectedFiles.TryGetValue(path, out stream).ShouldEqual(true, "Unexpected access of file: " + path);
                return stream;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/GVFSEnlistmentHealthTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class GVFSEnlistmentHealthTests
    {
        private readonly char sep = GVFSPlatform.GVFSPlatformConstants.PathSeparator;
        private EnlistmentHealthCalculator enlistmentHealthCalculator;
        private EnlistmentHealthData enlistmentHealthData;

        [TestCase]
        public void SingleHydratedDirectoryShouldHaveOtherDirectoriesCompletelyHealthy()
        {
            EnlistmentPathData pathData = new PathDataBuilder()
                .AddGitFiles("A/1.js", "A/2.js", "A/3.js", "A/4.js", "B/1.js", "B/2.js", "B/3.js", "B/4.js", "C/1.js", "C/2.js", "C/3.js", "C/4.js")
                .AddPlaceholderFiles("A/1.js", "A/2.js", "A/3.js")
                .Build();

            this.enlistmentHealthData = this.GenerateStatistics(pathData, string.Empty);

            this.enlistmentHealthData.DirectoryHydrationLevels[0].Name.ShouldEqual("A");
            this.enlistmentHealthData.DirectoryHydrationLevels[0].HydratedFileCount.ShouldEqual(3);
            this.enlistmentHealthData.DirectoryHydrationLevels[0].TotalFileCount.ShouldEqual(4);
            this.enlistmentHealthData.DirectoryHydrationLevels[1].Name.ShouldEqual("B");
            this.enlistmentHealthData.DirectoryHydrationLevels[1].HydratedFileCount.ShouldEqual(0);
            this.enlistmentHealthData.DirectoryHydrationLevels[1].TotalFileCount.ShouldEqual(4);
            this.enlistmentHealthData.DirectoryHydrationLevels[2].Name.ShouldEqual("C");
            this.enlistmentHealthData.DirectoryHydrationLevels[2].HydratedFileCount.ShouldEqual(0);
            this.enlistmentHealthData.DirectoryHydrationLevels[2].TotalFileCount.ShouldEqual(4);
            this.enlistmentHealthData.GitTrackedItemsCount.ShouldEqual(pathData.GitFilePaths.Count);
            this.enlistmentHealthData.PlaceholderCount.ShouldEqual(pathData.PlaceholderFilePaths.Count);
            this.enlistmentHealthData.ModifiedPathsCount.ShouldEqual(0);
            this.enlistmentHealthData.PlaceholderPercentage.ShouldEqual((decimal)pathData.PlaceholderFilePaths.Count / (decimal)pathData.GitFilePaths.Count);
            this.enlistmentHealthData.ModifiedPathsPercentage.ShouldEqual(0);
        }

        [TestCase]
        public void AllEmptyLists()
        {
            EnlistmentPathData pathData = new PathDataBuilder()
                .Build();

            this.enlistmentHealthData = this.GenerateStatistics(pathData, string.Empty);

            this.enlistmentHealthData.GitTrackedItemsCount.ShouldEqual(0);
            this.enlistmentHealthData.PlaceholderCount.ShouldEqual(0);
            this.enlistmentHealthData.ModifiedPathsCount.ShouldEqual(0);
            this.enlistmentHealthData.PlaceholderPercentage.ShouldEqual(0);
            this.enlistmentHealthData.ModifiedPathsPercentage.ShouldEqual(0);
        }

        [TestCase]
        public void OverHydrated()
        {
            EnlistmentPathData pathData = new PathDataBuilder()
                .AddGitFiles("A/1.js", "A/2.js", "A/3.js", "A/4.js", "B/1.js", "B/2.js", "B/3.js", "B/4.js", "C/1.js", "C/2.js", "C/3.js", "C/4.js")
                .AddPlaceholderFiles("A/1.js", "A/2.js", "A/3.js", "A/4.js", "B/1.js", "B/2.js", "B/3.js", "B/4.js", "C/1.js", "C/2.js", "C/3.js", "C/4.js")
                .AddModifiedPathFiles("A/1.js", "A/2.js", "A/3.js", "A/4.js", "B/1.js", "B/2.js", "B/3.js", "B/4.js", "C/1.js", "C/2.js", "C/3.js", "C/4.js")
                .Build();

            this.enlistmentHealthData = this.GenerateStatistics(pathData, string.Empty);

            this.enlistmentHealthData.GitTrackedItemsCount.ShouldEqual(pathData.GitFilePaths.Count);
            this.enlistmentHealthData.PlaceholderCount.ShouldEqual(pathData.PlaceholderFilePaths.Count);
            this.enlistmentHealthData.ModifiedPathsCount.ShouldEqual(pathData.ModifiedFilePaths.Count);
            this.enlistmentHealthData.PlaceholderPercentage.ShouldEqual(1);
            this.enlistmentHealthData.ModifiedPathsPercentage.ShouldEqual(1);
        }

        [TestCase]
        public void SortByHydration()
        {
            EnlistmentPathData pathData = new PathDataBuilder()
                .AddGitFiles("C/1.js", "A/1.js", "B/1.js", "B/2.js", "A/2.js", "C/2.js", "A/3.js", "C/3.js", "B/3.js")
                .AddModifiedPathFiles("C/1.js", "B/2.js", "A/3.js")
                .AddPlaceholderFiles("B/1.js", "A/2.js")
                .AddModifiedPathFiles("A/1.js")
                .Build();

            this.enlistmentHealthData = this.GenerateStatistics(pathData, string.Empty);

            this.enlistmentHealthData.PlaceholderCount.ShouldEqual(pathData.PlaceholderFilePaths.Count);
            this.enlistmentHealthData.ModifiedPathsCount.ShouldEqual(pathData.ModifiedFilePaths.Count);
            this.enlistmentHealthData.DirectoryHydrationLevels[0].Name.ShouldEqual("A");
            this.enlistmentHealthData.DirectoryHydrationLevels[1].Name.ShouldEqual("B");
            this.enlistmentHealthData.DirectoryHydrationLevels[2].Name.ShouldEqual("C");
        }

        [TestCase]
        public void VariedDirectoryFormatting()
        {
            EnlistmentPathData pathData = new PathDataBuilder()
                .AddGitFiles("A/1.js", "A/2.js", "A/3.js", "B/1.js", "B/2.js", "B/3.js")
                .AddPlaceholderFolders("/A/", $"{this.sep}B{this.sep}", "A/", $"B{this.sep}", "/A", $"{this.sep}B", "A", "B")
                .Build();

            this.enlistmentHealthData = this.GenerateStatistics(pathData, string.Empty);

            this.enlistmentHealthData.PlaceholderCount.ShouldEqual(pathData.PlaceholderFolderPaths.Count);

            // If the count of the sorted list is not 2, the different directory formats were considered distinct
            this.enlistmentHealthData.DirectoryHydrationLevels.Count.ShouldEqual(2);
        }

        [TestCase]
        public void VariedFilePathFormatting()
        {
            EnlistmentPathData pathData = new PathDataBuilder()
                .AddGitFiles("A/1.js", "A/2.js", "A/3.js", "B/1.js", "B/2.js", "B/3.js")
                .AddPlaceholderFiles("A/1.js", $"A{this.sep}2.js", "/A/1.js", $"{this.sep}A{this.sep}1.js")
                .AddModifiedPathFiles($"B{this.sep}2.js", $"{this.sep}B{this.sep}1.js")
                .Build();

            this.enlistmentHealthData = this.GenerateStatistics(pathData, string.Empty);

            this.enlistmentHealthData.PlaceholderCount.ShouldEqual(pathData.PlaceholderFilePaths.Count);
            this.enlistmentHealthData.ModifiedPathsCount.ShouldEqual(pathData.ModifiedFilePaths.Count);
            this.enlistmentHealthData.GitTrackedItemsCount.ShouldEqual(pathData.GitFilePaths.Count);
            this.enlistmentHealthData.DirectoryHydrationLevels.Count.ShouldEqual(2);
            this.enlistmentHealthData.PlaceholderPercentage.ShouldEqual((decimal)pathData.PlaceholderFilePaths.Count / (decimal)pathData.GitFilePaths.Count);
            this.enlistmentHealthData.ModifiedPathsPercentage.ShouldEqual((decimal)pathData.ModifiedFilePaths.Count / (decimal)pathData.GitFilePaths.Count);
            this.enlistmentHealthData.DirectoryHydrationLevels[0].HydratedFileCount.ShouldEqual(4);
            this.enlistmentHealthData.DirectoryHydrationLevels[0].TotalFileCount.ShouldEqual(3);
            this.enlistmentHealthData.DirectoryHydrationLevels[1].HydratedFileCount.ShouldEqual(2);
            this.enlistmentHealthData.DirectoryHydrationLevels[1].TotalFileCount.ShouldEqual(3);
        }

        [TestCase]
        public void FilterByDirectory()
        {
            string[] gitFilesDirectoryA = new string[] { "A/1.js", "A/2.js", "A/3.js" };
            string[] gitFilesDirectoryB = new string[] { "B/1.js", "B/2.js", "B/3.js" };

            // Duplicate modified paths get cleaned up when unioned with non skip worktree paths from git
            // Duplicate placeholders remain as read from the placeholder database to most accurately represent information

            EnlistmentPathData pathData = new PathDataBuilder()
                .AddGitFiles(gitFilesDirectoryA)
                .AddGitFiles(gitFilesDirectoryB)
                .AddPlaceholderFiles("A/1.js", $"A{this.sep}2.js", "/A/1.js", $"{this.sep}A{this.sep}1.js")
                .AddModifiedPathFiles("B/1.js", $"B{this.sep}2.js", "/B/1.js", $"{this.sep}B{this.sep}1.js")
                .Build();

            this.enlistmentHealthData = this.GenerateStatistics(pathData, "A/");

            this.enlistmentHealthData.GitTrackedItemsCount.ShouldEqual(gitFilesDirectoryA.Length);
            this.enlistmentHealthData.PlaceholderCount.ShouldEqual(pathData.PlaceholderFilePaths.Count);
            this.enlistmentHealthData.PlaceholderPercentage.ShouldEqual(4.0m / 3.0m);
            this.enlistmentHealthData.ModifiedPathsCount.ShouldEqual(0);
            this.enlistmentHealthData.ModifiedPathsPercentage.ShouldEqual(0);

            this.enlistmentHealthData = this.GenerateStatistics(pathData, "/B/");

            this.enlistmentHealthData.GitTrackedItemsCount.ShouldEqual(gitFilesDirectoryB.Length);
            this.enlistmentHealthData.PlaceholderCount.ShouldEqual(0);
            this.enlistmentHealthData.PlaceholderPercentage.ShouldEqual(0);
            this.enlistmentHealthData.ModifiedPathsCount.ShouldEqual(pathData.ModifiedFilePaths.Count);
            this.enlistmentHealthData.ModifiedPathsPercentage.ShouldEqual(2.0m / 3.0m);
        }

        [TestCase]
        public void FilterByDirectoryWithoutPathSeparator()
        {
            EnlistmentPathData pathData = new PathDataBuilder()
                .AddGitFiles("Directory1/Child1/File1.js", "Directory1/Child1/File2.exe", "Directory2/Child2/File1.bat", "Directory2/Child2/File2.css")
                .AddPlaceholderFiles("Directory1/File1.js", "Directory1/File2.exe", "Directory2/File1.bat", "Directory2/File2.css")
                .Build();

            // With no target should get both directories back
            this.enlistmentHealthData = this.GenerateStatistics(pathData, string.Empty);
            this.enlistmentHealthData.DirectoryHydrationLevels.Count.ShouldEqual(2);

            // With a root target ('/') should also get both directories back
            this.enlistmentHealthData = this.GenerateStatistics(pathData, GVFSConstants.GitPathSeparatorString);
            this.enlistmentHealthData.DirectoryHydrationLevels.Count.ShouldEqual(2);

            // Filtering by a substring of a directory shouldn't get the directories back
            this.enlistmentHealthData = this.GenerateStatistics(pathData, "Directory");
            this.enlistmentHealthData.GitTrackedItemsCount.ShouldEqual(0);
            this.enlistmentHealthData.ModifiedPathsCount.ShouldEqual(0);
            this.enlistmentHealthData.PlaceholderCount.ShouldEqual(0);
        }

        [TestCase]
        public void EnsureFolderNotIncludedInOwnCount()
        {
            EnlistmentPathData pathData = new PathDataBuilder()
                .AddGitFolders("foo/")
                .AddGitFiles("foo/file1.jpg", "foo/file2.jpg", "foo/file3.jpg", "foo/file4.jpg", "foo/file5.jpg")
                .AddPlaceholderFiles("foo/file1.jpg", "foo/file2.jpg", "foo/file3.jpg", "foo/file4.jpg", "foo/file5.jpg")
                .Build();

            this.enlistmentHealthData = this.GenerateStatistics(pathData, string.Empty);

            this.enlistmentHealthData.DirectoryHydrationLevels[0].HydratedFileCount.ShouldEqual(pathData.PlaceholderFilePaths.Count);
            this.enlistmentHealthData.DirectoryHydrationLevels[0].TotalFileCount.ShouldEqual(pathData.GitFilePaths.Count);
            this.enlistmentHealthData.PlaceholderCount.ShouldEqual(pathData.PlaceholderFilePaths.Count);
            this.enlistmentHealthData.GitTrackedItemsCount.ShouldEqual(pathData.GitFilePaths.Count + pathData.GitFolderPaths.Count);
            this.enlistmentHealthData.PlaceholderPercentage.ShouldEqual(5m / 6m);
        }

        [TestCase]
        public void FolderNotDoubleCounted()
        {
            EnlistmentPathData pathData = new PathDataBuilder()
                .AddGitFolders("foo/")
                .AddGitFiles("foo/file1.jpg", "foo/file2.jpg", "foo/file3.jpg", "foo/file4.jpg", "foo/file5.jpg")
                .AddPlaceholderFiles("foo/file1.jpg", "foo/file2.jpg", "foo/file3.jpg", "foo/file4.jpg", "foo/file5.jpg")
                .AddPlaceholderFolders("/foo")
                .Build();

            this.enlistmentHealthData = this.GenerateStatistics(pathData, string.Empty);
            this.enlistmentHealthData.DirectoryHydrationLevels[0].HydratedFileCount.ShouldEqual(pathData.PlaceholderFilePaths.Count);
            this.enlistmentHealthData.DirectoryHydrationLevels[0].TotalFileCount.ShouldEqual(pathData.GitFilePaths.Count);
            this.enlistmentHealthData.PlaceholderCount.ShouldEqual(pathData.PlaceholderFilePaths.Count + pathData.PlaceholderFolderPaths.Count);
            this.enlistmentHealthData.GitTrackedItemsCount.ShouldEqual(pathData.GitFilePaths.Count + pathData.GitFolderPaths.Count);
            this.enlistmentHealthData.PlaceholderPercentage.ShouldEqual(1);
        }

        [TestCase]
        public void UnionOfSkipWorktreeAndModifiedPathsNoDuplicates()
        {
            EnlistmentPathData pathData = new PathDataBuilder()
                .AddGitFiles("A/1.js", "A/2.js", "A/3.js", "B/1.js", "B/2.js", "B/3.js")
                .AddModifiedPathFiles("A/1.js", "A/2.js", "/A/3.js", "B/1.js", "B/2.js", "B/3.js")
                .AddNonSkipWorktreeFiles("A/1.js", "/A/2.js", $"{this.sep}A/3.js", "B/1.js", $"B{this.sep}2.js", $"/B{this.sep}3.js")
                .Build();

            this.enlistmentHealthData = this.GenerateStatistics(pathData, string.Empty);

            // ModifiedPaths are unioned with NonSkipWorktreePaths to get a total count of fully git tracked files
            // The six ModifiedPaths overlap with the six NonSkipWorktreePaths, so only 6 should be counted
            this.enlistmentHealthData.ModifiedPathsCount.ShouldEqual(6);
            this.enlistmentHealthData.ModifiedPathsPercentage.ShouldEqual(1);
            this.enlistmentHealthData.DirectoryHydrationLevels.Count.ShouldEqual(2);
        }

        private EnlistmentHealthData GenerateStatistics(EnlistmentPathData pathData, string directory)
        {
            this.enlistmentHealthCalculator = new EnlistmentHealthCalculator(pathData);
            return this.enlistmentHealthCalculator.CalculateStatistics(directory);
        }

        public class PathDataBuilder
        {
            private readonly EnlistmentPathData pathData = new EnlistmentPathData();

            public PathDataBuilder AddPlaceholderFiles(params string[] placeholderFilePaths)
            {
                this.pathData.PlaceholderFilePaths.AddRange(placeholderFilePaths);
                return this;
            }

            public PathDataBuilder AddPlaceholderFolders(params string[] placeholderFolderPaths)
            {
                this.pathData.PlaceholderFolderPaths.AddRange(placeholderFolderPaths);
                return this;
            }

            public PathDataBuilder AddModifiedPathFiles(params string[] modifiedFilePaths)
            {
                this.pathData.ModifiedFilePaths.AddRange(modifiedFilePaths);
                return this;
            }

            public PathDataBuilder AddModifiedPathFolders(params string[] modifiedFolderPaths)
            {
                this.pathData.ModifiedFolderPaths.AddRange(modifiedFolderPaths);
                return this;
            }

            public PathDataBuilder AddGitFiles(params string[] gitFilePaths)
            {
                this.pathData.GitFilePaths.AddRange(gitFilePaths);
                return this;
            }

            public PathDataBuilder AddGitFolders(params string[] gitFolderPaths)
            {
                this.pathData.GitFolderPaths.AddRange(gitFolderPaths);
                return this;
            }

            public PathDataBuilder AddNonSkipWorktreeFiles(params string[] nonSkipWorktreeFiles)
            {
                this.pathData.GitTrackingPaths.AddRange(nonSkipWorktreeFiles);
                return this;
            }

            public EnlistmentPathData Build()
            {
                this.pathData.NormalizeAllPaths();
                return this.pathData;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/GVFSEnlistmentTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class GVFSEnlistmentTests
    {
        private const string MountId = "85576f54f9ab4388bcdc19b4f6c17696";
        private const string EnlistmentId = "520dcf634ce34065a06abaa4010a256f";

        [TestCase]
        public void CanGetMountId()
        {
            TestGVFSEnlistment enlistment = new TestGVFSEnlistment();
            enlistment.GetMountId().ShouldEqual(MountId);
        }

        [TestCase]
        public void CanGetEnlistmentId()
        {
            TestGVFSEnlistment enlistment = new TestGVFSEnlistment();
            enlistment.GetEnlistmentId().ShouldEqual(EnlistmentId);
        }

        private class TestGVFSEnlistment : GVFSEnlistment
        {
            private MockGitProcess gitProcess;

            public TestGVFSEnlistment()
                : base("mock:\\path", "mock://repoUrl", "mock:\\git", authentication: null)
            {
                this.gitProcess = new MockGitProcess();
                this.gitProcess.SetExpectedCommandResult(
                    "config --local gvfs.mount-id",
                    () => new GitProcess.Result(MountId, string.Empty, GitProcess.Result.SuccessCode));
                this.gitProcess.SetExpectedCommandResult(
                    "config --local gvfs.enlistment-id",
                    () => new GitProcess.Result(EnlistmentId, string.Empty, GitProcess.Result.SuccessCode));
            }

            public override GitProcess CreateGitProcess()
            {
                return this.gitProcess;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/GVFSLockTests.cs
================================================
using GVFS.Common;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Virtual;
using Moq;
using NUnit.Framework;
using System;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class GVFSLockTests : TestsWithCommonRepo
    {
        private static readonly NamedPipeMessages.LockData DefaultLockData = new NamedPipeMessages.LockData(
            pid: 1234,
            isElevated: false,
            checkAvailabilityOnly: false,
            parsedCommand: "git command",
            gitCommandSessionId: "123");

        [TestCase]
        public void TryAcquireAndReleaseLockForExternalRequestor()
        {
            Mock mockTracer = new Mock(MockBehavior.Strict);
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Informational, "TryAcquireLockExternal", It.IsAny()));
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Informational, "ReleaseLockHeldByExternalProcess", It.IsAny(), Keywords.Telemetry));
            MockPlatform mockPlatform = (MockPlatform)GVFSPlatform.Instance;
            GVFSLock gvfsLock = this.AcquireDefaultLock(mockPlatform, mockTracer.Object);

            mockPlatform.ActiveProcesses.Remove(DefaultLockData.PID);
            gvfsLock.ReleaseLockHeldByExternalProcess(DefaultLockData.PID);
            this.ValidateLockIsFree(gvfsLock);
            mockTracer.VerifyAll();
        }

        [TestCase]
        public void ReleaseLockHeldByExternalProcess_WhenNoLock()
        {
            Mock mockTracer = new Mock(MockBehavior.Strict);
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Informational, "ReleaseLockHeldByExternalProcess", It.IsAny(), Keywords.Telemetry));
            GVFSLock gvfsLock = new GVFSLock(mockTracer.Object);
            this.ValidateLockIsFree(gvfsLock);
            gvfsLock.ReleaseLockHeldByExternalProcess(DefaultLockData.PID).ShouldBeFalse();
            this.ValidateLockIsFree(gvfsLock);
            mockTracer.VerifyAll();
        }

        [TestCase]
        public void ReleaseLockHeldByExternalProcess_DifferentPID()
        {
            Mock mockTracer = new Mock(MockBehavior.Strict);
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Informational, "ReleaseLockHeldByExternalProcess", It.IsAny(), Keywords.Telemetry));
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Informational, "TryAcquireLockExternal", It.IsAny()));
            MockPlatform mockPlatform = (MockPlatform)GVFSPlatform.Instance;
            GVFSLock gvfsLock = this.AcquireDefaultLock(mockPlatform, mockTracer.Object);
            gvfsLock.ReleaseLockHeldByExternalProcess(4321).ShouldBeFalse();
            this.ValidateLockHeld(gvfsLock, DefaultLockData);
            mockTracer.VerifyAll();
        }

        [TestCase]
        public void ReleaseLockHeldByExternalProcess_WhenGVFSHasLock()
        {
            Mock mockTracer = new Mock(MockBehavior.Strict);
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Verbose, "TryAcquireLockInternal", It.IsAny()));
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Informational, "ReleaseLockHeldByExternalProcess", It.IsAny(), Keywords.Telemetry));
            GVFSLock gvfsLock = this.AcquireGVFSLock(mockTracer.Object);

            gvfsLock.ReleaseLockHeldByExternalProcess(DefaultLockData.PID).ShouldBeFalse();
            this.ValidateLockHeldByGVFS(gvfsLock);
            mockTracer.VerifyAll();
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ReleaseLockHeldByGVFS_WhenNoLock()
        {
            Mock mockTracer = new Mock(MockBehavior.Strict);
            GVFSLock gvfsLock = new GVFSLock(mockTracer.Object);
            this.ValidateLockIsFree(gvfsLock);
            Assert.Throws(() => gvfsLock.ReleaseLockHeldByGVFS());
            mockTracer.VerifyAll();
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ReleaseLockHeldByGVFS_WhenExternalHasLockShouldThrow()
        {
            Mock mockTracer = new Mock(MockBehavior.Strict);
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Informational, "TryAcquireLockExternal", It.IsAny()));
            MockPlatform mockPlatform = (MockPlatform)GVFSPlatform.Instance;
            GVFSLock gvfsLock = this.AcquireDefaultLock(mockPlatform, mockTracer.Object);

            Assert.Throws(() => gvfsLock.ReleaseLockHeldByGVFS());
            mockTracer.VerifyAll();
        }

        [TestCase]
        public void TryAcquireLockForGVFS()
        {
            Mock mockTracer = new Mock(MockBehavior.Strict);
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Verbose, "TryAcquireLockInternal", It.IsAny()));
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Verbose, "ReleaseLockHeldByGVFS", It.IsAny()));
            GVFSLock gvfsLock = this.AcquireGVFSLock(mockTracer.Object);

            // Should be able to call again when GVFS has the lock
            gvfsLock.TryAcquireLockForGVFS().ShouldBeTrue();
            this.ValidateLockHeldByGVFS(gvfsLock);

            gvfsLock.ReleaseLockHeldByGVFS();
            this.ValidateLockIsFree(gvfsLock);
            mockTracer.VerifyAll();
        }

        [TestCase]
        public void TryAcquireLockForGVFS_WhenExternalLock()
        {
            Mock mockTracer = new Mock(MockBehavior.Strict);
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Informational, "TryAcquireLockExternal", It.IsAny()));
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Verbose, "TryAcquireLockInternal", It.IsAny()));
            MockPlatform mockPlatform = (MockPlatform)GVFSPlatform.Instance;
            GVFSLock gvfsLock = this.AcquireDefaultLock(mockPlatform, mockTracer.Object);

            gvfsLock.TryAcquireLockForGVFS().ShouldBeFalse();
            mockPlatform.ActiveProcesses.Remove(DefaultLockData.PID);
            mockTracer.VerifyAll();
        }

        [TestCase]
        public void TryAcquireLockForExternalRequestor_WhenGVFSLock()
        {
            Mock mockTracer = new Mock(MockBehavior.Strict);
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Verbose, "TryAcquireLockInternal", It.IsAny()));
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Verbose, "TryAcquireLockExternal", It.IsAny()));
            GVFSLock gvfsLock = this.AcquireGVFSLock(mockTracer.Object);

            NamedPipeMessages.LockData existingExternalHolder;
            gvfsLock.TryAcquireLockForExternalRequestor(DefaultLockData, out existingExternalHolder).ShouldBeFalse();
            this.ValidateLockHeldByGVFS(gvfsLock);
            existingExternalHolder.ShouldBeNull();
            mockTracer.VerifyAll();
        }

        [TestCase]
        public void TryAcquireLockForExternalRequestor_WhenExternalLock()
        {
            Mock mockTracer = new Mock(MockBehavior.Strict);
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Informational, "TryAcquireLockExternal", It.IsAny()));
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Verbose, "TryAcquireLockExternal", It.IsAny()));
            MockPlatform mockPlatform = (MockPlatform)GVFSPlatform.Instance;
            GVFSLock gvfsLock = this.AcquireDefaultLock(mockPlatform, mockTracer.Object);

            NamedPipeMessages.LockData newLockData = new NamedPipeMessages.LockData(4321, false, false, "git new", "123");
            NamedPipeMessages.LockData existingExternalHolder;
            gvfsLock.TryAcquireLockForExternalRequestor(newLockData, out existingExternalHolder).ShouldBeFalse();
            this.ValidateLockHeld(gvfsLock, DefaultLockData);
            this.ValidateExistingExternalHolder(DefaultLockData, existingExternalHolder);
            mockPlatform.ActiveProcesses.Remove(DefaultLockData.PID);
            mockTracer.VerifyAll();
        }

        [TestCase]
        public void TryAcquireLockForExternalRequestor_WhenExternalHolderTerminated()
        {
            Mock mockTracer = new Mock(MockBehavior.Strict);
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Informational, "TryAcquireLockExternal", It.IsAny()));
            mockTracer.Setup(x => x.RelatedEvent(EventLevel.Informational, "ExternalLockHolderExited", It.IsAny(), Keywords.Telemetry));
            mockTracer.Setup(x => x.SetGitCommandSessionId(string.Empty));
            MockPlatform mockPlatform = (MockPlatform)GVFSPlatform.Instance;
            GVFSLock gvfsLock = this.AcquireDefaultLock(mockPlatform, mockTracer.Object);
            mockPlatform.ActiveProcesses.Remove(DefaultLockData.PID);

            NamedPipeMessages.LockData newLockData = new NamedPipeMessages.LockData(4321, false, false, "git new", "123");
            mockPlatform.ActiveProcesses.Add(newLockData.PID);
            NamedPipeMessages.LockData existingExternalHolder;
            gvfsLock.TryAcquireLockForExternalRequestor(newLockData, out existingExternalHolder).ShouldBeTrue();
            existingExternalHolder.ShouldBeNull();
            this.ValidateLockHeld(gvfsLock, newLockData);
            mockTracer.VerifyAll();
        }

        private GVFSLock AcquireDefaultLock(MockPlatform mockPlatform, ITracer mockTracer)
        {
            GVFSLock gvfsLock = new GVFSLock(mockTracer);
            this.ValidateLockIsFree(gvfsLock);
            NamedPipeMessages.LockData existingExternalHolder;
            gvfsLock.TryAcquireLockForExternalRequestor(DefaultLockData, out existingExternalHolder).ShouldBeTrue();
            existingExternalHolder.ShouldBeNull();
            mockPlatform.ActiveProcesses.Add(DefaultLockData.PID);
            this.ValidateLockHeld(gvfsLock, DefaultLockData);
            return gvfsLock;
        }

        private GVFSLock AcquireGVFSLock(ITracer mockTracer)
        {
            GVFSLock gvfsLock = new GVFSLock(mockTracer);
            this.ValidateLockIsFree(gvfsLock);
            gvfsLock.TryAcquireLockForGVFS().ShouldBeTrue();
            this.ValidateLockHeldByGVFS(gvfsLock);
            return gvfsLock;
        }

        private void ValidateLockIsFree(GVFSLock gvfsLock)
        {
            this.ValidateLock(gvfsLock, null, expectedStatus: "Free", expectedGitCommand: null, expectedIsAvailable: true);
        }

        private void ValidateLockHeldByGVFS(GVFSLock gvfsLock)
        {
            this.ValidateLock(gvfsLock, null, expectedStatus: "Held by GVFS.", expectedGitCommand: null, expectedIsAvailable: false);
        }

        private void ValidateLockHeld(GVFSLock gvfsLock, NamedPipeMessages.LockData expected)
        {
            this.ValidateLock(gvfsLock, expected, expectedStatus: $"Held by {expected.ParsedCommand} (PID:{expected.PID})", expectedGitCommand: expected.ParsedCommand, expectedIsAvailable: false);
        }

        private void ValidateLock(
            GVFSLock gvfsLock,
            NamedPipeMessages.LockData expected,
            string expectedStatus,
            string expectedGitCommand,
            bool expectedIsAvailable)
        {
            gvfsLock.GetStatus().ShouldEqual(expectedStatus);
            NamedPipeMessages.LockData existingHolder;
            gvfsLock.IsLockAvailableForExternalRequestor(out existingHolder).ShouldEqual(expectedIsAvailable);
            this.ValidateExistingExternalHolder(expected, existingHolder);
            gvfsLock.GetLockedGitCommand().ShouldEqual(expectedGitCommand);
            NamedPipeMessages.LockData externalHolder = gvfsLock.GetExternalHolder();
            this.ValidateExistingExternalHolder(expected, externalHolder);
        }

        private void ValidateExistingExternalHolder(NamedPipeMessages.LockData expected, NamedPipeMessages.LockData actual)
        {
            if (actual != null)
            {
                expected.ShouldNotBeNull();
                actual.ShouldNotBeNull();
                actual.PID.ShouldEqual(expected.PID);
                actual.IsElevated.ShouldEqual(expected.IsElevated);
                actual.CheckAvailabilityOnly.ShouldEqual(expected.CheckAvailabilityOnly);
                actual.ParsedCommand.ShouldEqual(expected.ParsedCommand);
                actual.GitCommandSessionId.ShouldEqual(expected.GitCommandSessionId);
            }
            else
            {
                expected.ShouldBeNull();
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/Git/GitSslTests.cs
================================================
#if NETCOREAPP2_1
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.X509Certificates;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using GVFS.UnitTests.Mock.Git;
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace GVFS.UnitTests.Common.Git
{
    [TestFixture]
    public class GitSslTests
    {
        public static object[] BoolGitSettings = new[]
        {
            new object[] { GitConfigSetting.HttpSslCertPasswordProtected },
            new object[] { GitConfigSetting.HttpSslVerify }
        };

        private const string CertificateName = "TestCert";
        private const string CertificatePassword = "SecurePassword";

        private MockTracer tracer;
        private MockGitProcess gitProcess;

        private Mock certificateVerifierMock;
        private Mock certificateStoreMock;

        private MockDirectory mockDirectory;
        private MockFileSystem fileSystem;

        [SetUp]
        public void Setup()
        {
            this.tracer = new MockTracer();
            this.gitProcess = new MockGitProcess();
            this.mockDirectory = new MockDirectory("mock://root", null, null);
            this.fileSystem = new MockFileSystem(this.mockDirectory);
            this.certificateVerifierMock = new Mock();
            this.certificateStoreMock = new Mock();
        }

        [Category(CategoryConstants.ExceptionExpected)]
        [TestCaseSource(typeof(GitSslTests), nameof(GitSslTests.BoolGitSettings))]
        public void ConstructorShouldThrowWhenLastBoolSettingNotABool(string setting)
        {
            IDictionary gitConfig = new Dictionary();
            gitConfig.Add(setting, new GitConfigSetting(setting, "true", "this is true"));

            Assert.Throws(() => new GitSsl(gitConfig));
        }

        [TestCaseSource(typeof(GitSslTests), nameof(GitSslTests.BoolGitSettings))]
        public void ConstructorShouldNotThrowWhenLastBoolSettingIsABool(string setting)
        {
            IDictionary gitConfig = new Dictionary();
            gitConfig.Add(setting, new GitConfigSetting(setting, "this is true", "true"));

            Assert.DoesNotThrow(() => new GitSsl(gitConfig));
        }

        [TestCase]
        public void GetCertificateShouldReturnNullWhenCertificateCommonNameSettingIsEmpty()
        {
            GitSsl sut = new GitSsl(new Dictionary());
            X509Certificate2 result = sut.GetCertificate(this.tracer, this.gitProcess);
            result.ShouldBeNull();
        }

        [TestCase]
        public void GetCertificateShouldReturnCertificateFromFileWhenFileExistsAndIsPasswordProtectedAndIsValid()
        {
            X509Certificate2 certificate = GenerateTestCertificate();
            this.SetupCertificateFile(certificate, CertificatePassword);
            this.SetupGitCertificatePassword();
            this.MakeCertificateValid(certificate);
            GitSsl gitSsl = new GitSsl(GetGitConfig(), () => this.certificateStoreMock.Object, this.certificateVerifierMock.Object, this.fileSystem);

            X509Certificate2 result = gitSsl.GetCertificate(this.tracer, this.gitProcess);

            result.ShouldNotBeNull();
            result.ShouldEqual(certificate);
        }

        [TestCase]
        public void GetCertificateShouldReturnCertificateFromFileWhenFileExistsAndIsNotPasswordProtectedAndIsValid()
        {
            X509Certificate2 certificate = GenerateTestCertificate();
            this.SetupCertificateFile(certificate);
            this.MakeCertificateValid(certificate);
            GitSsl gitSsl = new GitSsl(
                GetGitConfig(
                    new GitConfigSetting(GitConfigSetting.HttpSslCertPasswordProtected, "false")),
                 () => this.certificateStoreMock.Object,
                 this.certificateVerifierMock.Object,
                 this.fileSystem);

            X509Certificate2 result = gitSsl.GetCertificate(this.tracer, this.gitProcess);

            result.ShouldNotBeNull();
            result.ShouldEqual(certificate);
        }

        [TestCase]
        public void GetCertificateShouldReturnNullWhenFileExistsAndIsNotPasswordProtectedAndIsInvalid()
        {
            X509Certificate2 certificate = GenerateTestCertificate();
            this.SetupCertificateFile(certificate);
            this.MakeCertificateValid(certificate, false);
            GitSsl gitSsl = new GitSsl(
                GetGitConfig(
                    new GitConfigSetting(GitConfigSetting.HttpSslCertPasswordProtected, "false")),
                 () => this.certificateStoreMock.Object,
                 this.certificateVerifierMock.Object,
                 this.fileSystem);

            X509Certificate2 result = gitSsl.GetCertificate(this.tracer, this.gitProcess);

            result.ShouldBeNull();
        }

        [TestCase]
        public void GetCertificateShouldReturnCertificateFromFileWhenFileExistsAndIsNotPasswordProtectedAndIsInvalidAndShouldVerifyIsFalse()
        {
            X509Certificate2 certificate = GenerateTestCertificate();
            this.SetupCertificateFile(certificate);
            this.MakeCertificateValid(certificate, false);
            GitSsl gitSsl = new GitSsl(
                GetGitConfig(
                    new GitConfigSetting(GitConfigSetting.HttpSslCertPasswordProtected, "false"),
                    new GitConfigSetting(GitConfigSetting.HttpSslVerify, "false")),
                () => this.certificateStoreMock.Object,
                this.certificateVerifierMock.Object,
                this.fileSystem);

            X509Certificate2 result = gitSsl.GetCertificate(this.tracer, this.gitProcess);

            result.ShouldNotBeNull();
            result.ShouldEqual(certificate);
        }

        [TestCase]
        public void GetCertificateShouldReturnCertificateFromStoreAccordingToRulesWhenFileDoesNotExist()
        {
            X509Certificate2 certificate = this.MakeCertificateValid(GenerateTestCertificate());
            this.SetupGitCertificatePassword();
            GitSsl gitSsl = new GitSsl(
                GetGitConfig(),
                () => this.certificateStoreMock.Object,
                this.certificateVerifierMock.Object,
                this.fileSystem);

            this.SetupCertificateStore(
                true,
                this.MakeCertificateValid(GenerateTestCertificate(CertificateName + "suphix")),
                this.MakeCertificateValid(GenerateTestCertificate("prefix" + CertificateName)),
                this.MakeCertificateValid(GenerateTestCertificate("not this certificate")),
                this.MakeCertificateValid(GenerateTestCertificate(), false),
                this.MakeCertificateValid(GenerateTestCertificate(validFrom: DateTimeOffset.Now.AddDays(-4))),
                this.MakeCertificateValid(GenerateTestCertificate(validTo: DateTimeOffset.Now.AddDays(4))),
                certificate);

            X509Certificate2 result = gitSsl.GetCertificate(this.tracer, this.gitProcess);

            result.ShouldNotBeNull();
            result.ShouldEqual(certificate);
        }

        [TestCase]
        public void GetCertificateShouldReturnNullWhenNoMatchingCertificatesExist()
        {
            this.SetupGitCertificatePassword();
            GitSsl gitSsl = new GitSsl(
                GetGitConfig(),
                () => this.certificateStoreMock.Object,
                this.certificateVerifierMock.Object,
                this.fileSystem);

            this.SetupCertificateStore(
                true,
                this.MakeCertificateValid(GenerateTestCertificate(CertificateName + "suphix")),
                this.MakeCertificateValid(GenerateTestCertificate("prefix" + CertificateName)),
                this.MakeCertificateValid(GenerateTestCertificate("not this certificate")));

            X509Certificate2 result = gitSsl.GetCertificate(this.tracer, this.gitProcess);

            result.ShouldBeNull();
        }

        private static IDictionary GetGitConfig(params GitConfigSetting[] overrides)
        {
            IDictionary gitConfig = new Dictionary();
            gitConfig.Add(GitConfigSetting.HttpSslCert, new GitConfigSetting(GitConfigSetting.HttpSslCert, CertificateName));
            gitConfig.Add(GitConfigSetting.HttpSslCertPasswordProtected, new GitConfigSetting(GitConfigSetting.HttpSslCertPasswordProtected, "true"));
            gitConfig.Add(GitConfigSetting.HttpSslVerify, new GitConfigSetting(GitConfigSetting.HttpSslVerify, "true"));

            foreach (GitConfigSetting settingOverride in overrides)
            {
                gitConfig[settingOverride.Name] = settingOverride;
            }

            return gitConfig;
        }

        private static X509Certificate2 GenerateTestCertificate(
            string subjectName = null,
            DateTimeOffset? validFrom = null,
            DateTimeOffset? validTo = null)
        {
            ECDsa ecdsa = ECDsa.Create();
            CertificateRequest req = new CertificateRequest($"cn={subjectName ?? CertificateName}", ecdsa, HashAlgorithmName.SHA256);
            X509Certificate2 cert = req.CreateSelfSigned(validFrom ?? DateTimeOffset.Now.AddDays(-5), validTo ?? DateTimeOffset.Now.AddDays(5));

            return cert;
        }

        private X509Certificate2 MakeCertificateValid(X509Certificate2 certificate, bool isValid = true)
        {
            this.certificateVerifierMock.Setup(x => x.Verify(certificate)).Returns(isValid);
            return certificate;
        }

        private void SetupGitCertificatePassword()
        {
            this.gitProcess.SetExpectedCommandResult("credential fill", () => new GitProcess.Result($"password={CertificatePassword}\n", null, 0));
        }

        private void SetupCertificateStore(bool onlyValid, params X509Certificate2[] results)
        {
            this.SetupCertificateStore(X509FindType.FindBySubjectName, CertificateName, onlyValid, results);
        }

        private void SetupCertificateStore(
            X509FindType findType,
            string certificateName,
            bool onlyValid,
            params X509Certificate2[] results)
        {
            X509Certificate2Collection result = new X509Certificate2Collection();
            result.AddRange(results);
            this.certificateStoreMock.Setup(x => x.Find(findType, certificateName, onlyValid)).Returns(result);
        }

        private void SetupCertificateFile(X509Certificate2 certificate, string password = null)
        {
            byte[] certificateContents;
            if (password == null)
            {
                certificateContents = certificate.Export(X509ContentType.Pkcs12);
            }
            else
            {
                certificateContents = certificate.Export(X509ContentType.Pkcs12, password);
            }

            this.mockDirectory.AddFile(
                new MockFile(CertificateName, certificateContents),
                Path.Combine(this.mockDirectory.FullName, CertificateName));
        }
    }
}
#endif

================================================
FILE: GVFS/GVFS.UnitTests/Common/Git/Sha1IdTests.cs
================================================
using GVFS.Common.Git;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using NUnit.Framework;

namespace GVFS.UnitTests.Common.Git
{
    [TestFixture]
    public class Sha1IdTests
    {
        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void TryParseFailsForLowerCaseShas()
        {
            Sha1Id sha1;
            string error;
            Sha1Id.TryParse("abcdef7890123456789012345678901234567890", out sha1, out error).ShouldBeFalse();
            Sha1Id.TryParse(new string('a', 40), out sha1, out error).ShouldBeFalse();
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void TryParseFailsForInvalidShas()
        {
            Sha1Id sha1;
            string error;
            Sha1Id.TryParse(null, out sha1, out error).ShouldBeFalse();
            Sha1Id.TryParse("0", out sha1, out error).ShouldBeFalse();
            Sha1Id.TryParse("abcdef", out sha1, out error).ShouldBeFalse();
            Sha1Id.TryParse(new string('H', 40), out sha1, out error).ShouldBeFalse();
        }

        [TestCase]
        public void TryParseSucceedsForUpperCaseShas()
        {
            Sha1Id sha1Id;
            string error;
            string sha = "ABCDEF7890123456789012345678901234567890";
            Sha1Id.TryParse(sha, out sha1Id, out error).ShouldBeTrue();
            sha1Id.ToString().ShouldEqual(sha);

            sha = new string('A', 40);
            Sha1Id.TryParse(sha, out sha1Id, out error).ShouldBeTrue();
            sha1Id.ToString().ShouldEqual(sha);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/GitCommandLineParserTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class GitCommandLineParserTests
    {
        [TestCase]
        public void IsVerbTests()
        {
            new GitCommandLineParser("gits status --no-idea").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false);

            new GitCommandLineParser("git status --no-idea").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(true);
            new GitCommandLineParser("git status").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(true);
            new GitCommandLineParser("git statuses --no-idea").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false);
            new GitCommandLineParser("git statuses").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false);

            new GitCommandLineParser("git add").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false);
            new GitCommandLineParser("git checkout").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false);
            new GitCommandLineParser("git clean").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false);
            new GitCommandLineParser("git commit").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false);
            new GitCommandLineParser("git mv").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false);
            new GitCommandLineParser("git reset").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false);
            new GitCommandLineParser("git stage").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false);
            new GitCommandLineParser("git update-index").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false);

            new GitCommandLineParser("git add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true);
            new GitCommandLineParser("git checkout").IsVerb(GitCommandLineParser.Verbs.Checkout).ShouldEqual(true);
            new GitCommandLineParser("git commit").IsVerb(GitCommandLineParser.Verbs.Commit).ShouldEqual(true);
            new GitCommandLineParser("git mv").IsVerb(GitCommandLineParser.Verbs.Move).ShouldEqual(true);
            new GitCommandLineParser("git reset").IsVerb(GitCommandLineParser.Verbs.Reset).ShouldEqual(true);
            new GitCommandLineParser("git stage").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true);
            new GitCommandLineParser("git update-index").IsVerb(GitCommandLineParser.Verbs.UpdateIndex).ShouldEqual(true);
            new GitCommandLineParser("git updateindex").IsVerb(GitCommandLineParser.Verbs.UpdateIndex).ShouldEqual(false);

            new GitCommandLineParser("git add some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true);
            new GitCommandLineParser("git stage some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true);
            new GitCommandLineParser("git adds some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false);
            new GitCommandLineParser("git stages some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false);
            new GitCommandLineParser("git adding add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false);
            new GitCommandLineParser("git adding add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false);
            new GitCommandLineParser("git adding add").IsVerb(GitCommandLineParser.Verbs.Other).ShouldEqual(true);
        }

        [TestCase]
        public void IsResetSoftOrMixedTests()
        {
            new GitCommandLineParser("gits reset --soft").IsResetSoftOrMixed().ShouldEqual(false);

            new GitCommandLineParser("git reset --soft").IsResetSoftOrMixed().ShouldEqual(true);
            new GitCommandLineParser("git reset --mixed").IsResetSoftOrMixed().ShouldEqual(true);
            new GitCommandLineParser("git reset").IsResetSoftOrMixed().ShouldEqual(true);

            new GitCommandLineParser("git reset --hard").IsResetSoftOrMixed().ShouldEqual(false);
            new GitCommandLineParser("git reset --keep").IsResetSoftOrMixed().ShouldEqual(false);
            new GitCommandLineParser("git reset --merge").IsResetSoftOrMixed().ShouldEqual(false);

            new GitCommandLineParser("git checkout").IsResetSoftOrMixed().ShouldEqual(false);
            new GitCommandLineParser("git status").IsResetSoftOrMixed().ShouldEqual(false);
        }

        [TestCase]
        public void IsCheckoutWithFilePathsTests()
        {
            new GitCommandLineParser("gits checkout branch -- file").IsCheckoutWithFilePaths().ShouldEqual(false);

            new GitCommandLineParser("git checkout branch -- file").IsCheckoutWithFilePaths().ShouldEqual(true);
            new GitCommandLineParser("git checkout branch -- file1 file2").IsCheckoutWithFilePaths().ShouldEqual(true);
            new GitCommandLineParser("git checkout HEAD -- file").IsCheckoutWithFilePaths().ShouldEqual(true);

            new GitCommandLineParser("git checkout HEAD file").IsCheckoutWithFilePaths().ShouldEqual(true);
            new GitCommandLineParser("git checkout HEAD file1 file2").IsCheckoutWithFilePaths().ShouldEqual(true);

            new GitCommandLineParser("git checkout branch file").IsCheckoutWithFilePaths().ShouldEqual(false);
            new GitCommandLineParser("git checkout branch").IsCheckoutWithFilePaths().ShouldEqual(false);
            new GitCommandLineParser("git checkout HEAD").IsCheckoutWithFilePaths().ShouldEqual(false);

            new GitCommandLineParser("git checkout -b topic").IsCheckoutWithFilePaths().ShouldEqual(false);

            new GitCommandLineParser("git checkout -b topic --").IsCheckoutWithFilePaths().ShouldEqual(false);
            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);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/GitConfigHelperTests.cs
================================================
using GVFS.Common.Git;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.Collections.Generic;
using System.Linq;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class GitConfigHelperTests
    {
        [TestCase]
        public void SanitizeEmptyString()
        {
            string outputString;
            GitConfigHelper.TrySanitizeConfigFileLine(string.Empty, out outputString).ShouldEqual(false);
        }

        [TestCase]
        public void SanitizePureWhiteSpace()
        {
            string outputString;
            GitConfigHelper.TrySanitizeConfigFileLine("   ", out outputString).ShouldEqual(false);
            GitConfigHelper.TrySanitizeConfigFileLine(" \t\t  ", out outputString).ShouldEqual(false);
            GitConfigHelper.TrySanitizeConfigFileLine(" \t\t\n\n  ", out outputString).ShouldEqual(false);
        }

        [TestCase]
        public void SanitizeComment()
        {
            string outputString;
            GitConfigHelper.TrySanitizeConfigFileLine("# This is a comment ", out outputString).ShouldEqual(false);
            GitConfigHelper.TrySanitizeConfigFileLine("# This is a comment #", out outputString).ShouldEqual(false);
            GitConfigHelper.TrySanitizeConfigFileLine("## This is a comment ##", out outputString).ShouldEqual(false);
            GitConfigHelper.TrySanitizeConfigFileLine(" ## This is a comment ## ", out outputString).ShouldEqual(false);
            GitConfigHelper.TrySanitizeConfigFileLine("\t ## This is a comment ## \t ", out outputString).ShouldEqual(false);
        }

        [TestCase]
        public void TrimWhitspace()
        {
            string outputString;
            GitConfigHelper.TrySanitizeConfigFileLine(" // ", out outputString).ShouldEqual(true);
            outputString.ShouldEqual("//");

            GitConfigHelper.TrySanitizeConfigFileLine(" /* ", out outputString).ShouldEqual(true);
            outputString.ShouldEqual("/*");

            GitConfigHelper.TrySanitizeConfigFileLine(" /A ", out outputString).ShouldEqual(true);
            outputString.ShouldEqual("/A");

            GitConfigHelper.TrySanitizeConfigFileLine("\t /A \t", out outputString).ShouldEqual(true);
            outputString.ShouldEqual("/A");

            GitConfigHelper.TrySanitizeConfigFileLine("  \t /A   \t", out outputString).ShouldEqual(true);
            outputString.ShouldEqual("/A");
        }

        [TestCase]
        public void TrimTrailingComment()
        {
            string outputString;
            GitConfigHelper.TrySanitizeConfigFileLine(" // # Trailing comment!", out outputString).ShouldEqual(true);
            outputString.ShouldEqual("//");

            GitConfigHelper.TrySanitizeConfigFileLine(" /* # Trailing comment!", out outputString).ShouldEqual(true);
            outputString.ShouldEqual("/*");

            GitConfigHelper.TrySanitizeConfigFileLine(" /A # Trailing comment!", out outputString).ShouldEqual(true);
            outputString.ShouldEqual("/A");

            GitConfigHelper.TrySanitizeConfigFileLine("\t /A \t # Trailing comment! \t", out outputString).ShouldEqual(true);
            outputString.ShouldEqual("/A");

            GitConfigHelper.TrySanitizeConfigFileLine("  \t /A   \t # Trailing comment!", out outputString).ShouldEqual(true);
            outputString.ShouldEqual("/A");
        }

        [TestCase]
        public void ParseKeyValuesTest()
        {
            string input = @"
core.gvfs=true
gc.auto=0
section.key=value1
section.key= value2
section.key =value3
section.key = value4
section.KEY=value5
section.empty=
";
            Dictionary result = GitConfigHelper.ParseKeyValues(input);

            result.Count.ShouldEqual(4);
            result["core.gvfs"].Values.Single().ShouldEqual("true");
            result["gc.auto"].Values.Single().ShouldEqual("0");
            result["section.key"].Values.Count.ShouldEqual(5);
            result["section.key"].Values.ShouldContain(v => v == "value1");
            result["section.key"].Values.ShouldContain(v => v == "value2");
            result["section.key"].Values.ShouldContain(v => v == "value3");
            result["section.key"].Values.ShouldContain(v => v == "value4");
            result["section.key"].Values.ShouldContain(v => v == "value5");
            result["section.empty"].Values.Single().ShouldEqual(string.Empty);
        }

        [TestCase]
        public void ParseSpaceSeparatedKeyValuesTest()
        {
            string input = @"
core.gvfs true
gc.auto 0
section.key value1
section.key  value2
section.key  value3
section.key   value4
section.KEY value5" +
"\nsection.empty ";

            Dictionary result = GitConfigHelper.ParseKeyValues(input, ' ');

            result.Count.ShouldEqual(4);
            result["core.gvfs"].Values.Single().ShouldEqual("true");
            result["gc.auto"].Values.Single().ShouldEqual("0");
            result["section.key"].Values.Count.ShouldEqual(5);
            result["section.key"].Values.ShouldContain(v => v == "value1");
            result["section.key"].Values.ShouldContain(v => v == "value2");
            result["section.key"].Values.ShouldContain(v => v == "value3");
            result["section.key"].Values.ShouldContain(v => v == "value4");
            result["section.key"].Values.ShouldContain(v => v == "value5");
            result["section.empty"].Values.Single().ShouldEqual(string.Empty);
        }

        [TestCase]
        public void GetSettingsTest()
        {
            string fileContents = @"
[core]
    gvfs = true
[gc]
    auto = 0
[section]
    key1 = 1
    key2 = 2
    key3 = 3
[notsection]
    keyN1 = N1
    keyN2 = N2
    keyN3 = N3
[section]
[section]
    key4 = 4
    key5 = 5
[section]
    key6 = 6
    key7 =
         = emptyKey";

            Dictionary result = GitConfigHelper.GetSettings(fileContents.Split('\r', '\n'), "Section");

            int expectedCount = 7; // empty keys will not be included.
            result.Count.ShouldEqual(expectedCount);

            // Verify keyN = N
            for (int i = 1; i <= expectedCount - 1; i++)
            {
                result["key" + i.ToString()].Values.ShouldContain(v => v == i.ToString());
            }

            // Verify empty value
            result["key7"].Values.Single().ShouldEqual(string.Empty);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/GitObjectsTests.cs
================================================
using GVFS.Common.Git;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class GitObjectsTests
    {
        [TestCase]
        public void IsLooseObjectsDirectory_ValidDirectories()
        {
            GitObjects.IsLooseObjectsDirectory("BB").ShouldBeTrue();
            GitObjects.IsLooseObjectsDirectory("bb").ShouldBeTrue();
            GitObjects.IsLooseObjectsDirectory("A7").ShouldBeTrue();
            GitObjects.IsLooseObjectsDirectory("55").ShouldBeTrue();
        }

        [TestCase]
        public void IsLooseObjectsDirectory_InvalidDirectories()
        {
            GitObjects.IsLooseObjectsDirectory("K7").ShouldBeFalse();
            GitObjects.IsLooseObjectsDirectory("A-").ShouldBeFalse();
            GitObjects.IsLooseObjectsDirectory("?B").ShouldBeFalse();
            GitObjects.IsLooseObjectsDirectory("BBB").ShouldBeFalse();
            GitObjects.IsLooseObjectsDirectory("B-B").ShouldBeFalse();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/GitPathConverterTests.cs
================================================
using GVFS.Common.Git;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class GitPathConverterTests
    {
        private const string OctetEncoded = @"\330\261\331\212\331\204\331\214\330\243\331\203\330\252\331\210\330\250\330\261\303\273\331\205\330\247\330\261\330\263\330\243\330\272\330\263\330\267\330\263\302\272\331\260\331\260\333\202\331\227\331\222\333\265\330\261\331\212\331\204\331\214\330\243\331\203";
        private const string Utf8Encoded = @"ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك";
        private const string TestPath = @"/GVFS/";

        [TestCase]
        public void NullFilepathTest()
        {
            GitPathConverter.ConvertPathOctetsToUtf8(null).ShouldEqual(null);
        }

        [TestCase]
        public void EmptyFilepathTest()
        {
            GitPathConverter.ConvertPathOctetsToUtf8(string.Empty).ShouldEqual(string.Empty);
        }

        [TestCase]
        public void FilepathWithoutOctets()
        {
            GitPathConverter.ConvertPathOctetsToUtf8(TestPath + "test.cs").ShouldEqual(TestPath + "test.cs");
        }

        [TestCase]
        public void FilepathWithoutOctetsAsFilename()
        {
            GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded).ShouldEqual(TestPath + Utf8Encoded);
        }

        [TestCase]
        public void FilepathWithoutOctetsAsFilenameNoExtension()
        {
            GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + ".txt").ShouldEqual(TestPath + Utf8Encoded + ".txt");
        }

        [TestCase]
        public void FilepathWithoutOctetsAsFolder()
        {
            GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + "/file.txt").ShouldEqual(TestPath + Utf8Encoded + "/file.txt");
        }

        [TestCase]
        public void FilepathWithoutOctetsAsFileAndFolder()
        {
            GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + TestPath + OctetEncoded + ".txt").ShouldEqual(TestPath + Utf8Encoded + TestPath + Utf8Encoded + ".txt");
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/GitStatusCacheTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.NamedPipes;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class GitStatusCacheTests
    {
        private static NamedPipeMessages.LockData statusCommandLockData = new NamedPipeMessages.LockData(123, false, false, "git status", "123");

        private MockFileSystem fileSystem;
        private MockGitProcess gitProcess;
        private GVFSContext context;
        private string gitParentPath;
        private string gvfsMetadataPath;
        private MockDirectory enlistmentDirectory;

        public static IEnumerable ExceptionsThrownByCreateDirectory
        {
            get
            {
                yield return new IOException("Error creating directory");
                yield return new UnauthorizedAccessException("Error creating directory");
            }
        }

        [SetUp]
        public void SetUp()
        {
            MockTracer tracer = new MockTracer();

            string enlistmentRoot = Path.Combine("mock:", "GVFS", "UnitTests", "Repo");
            string statusCachePath = Path.Combine("mock:", "GVFS", "UnitTests", "Repo", GVFSPlatform.Instance.Constants.DotGVFSRoot, "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", this.gitProcess);
            enlistment.InitializeCachePathsFromKey("fake:\\gvfsSharedCache", "fakeCacheKey");

            this.gitParentPath = enlistment.WorkingDirectoryBackingRoot;
            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);
            GitStatusCache.TEST_EnableHydrationSummaryOverride = false;
        }

        [TearDown]
        public void TearDown()
        {
            this.fileSystem = null;
            this.gitProcess = null;
            this.context = null;
            this.gitParentPath = null;
            this.gvfsMetadataPath = null;
            this.enlistmentDirectory = null;
            GitStatusCache.TEST_EnableHydrationSummaryOverride = 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]
        [Category(CategoryConstants.ExceptionExpected)]
        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();
            }
        }

        [TestCaseSource("ExceptionsThrownByCreateDirectory")]
        [Category(CategoryConstants.ExceptionExpected)]
        public void HandlesExceptionsCreatingDirectory(Exception exceptionToThrow)
        {
            this.enlistmentDirectory.CreateFile(Path.Combine(this.gvfsMetadataPath, GVFSConstants.DotGVFS.GitStatusCache.CachePath), "Git status cache contents", createDirectories: true);
            this.fileSystem.ExceptionThrownByCreateDirectory = exceptionToThrow;
            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().ShouldBeFalse();

                statusCache.Shutdown();
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/GitVersionTests.cs
================================================
using GVFS.Common.Git;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class GitVersionTests
    {
        [TestCase]
        public void Version_Data_Null_Returns_False()
        {
            GitVersion version;
            bool success = GitVersion.TryParseVersion(null, out version);
            success.ShouldEqual(false);
        }

        [TestCase]
        public void Version_Data_Empty_Returns_False()
        {
            GitVersion version;
            bool success = GitVersion.TryParseVersion(string.Empty, out version);
            success.ShouldEqual(false);
        }

        [TestCase]
        public void Version_Data_Not_Enough_Numbers_Sets_Zeroes()
        {
            GitVersion version;
            bool success = GitVersion.TryParseVersion("2.0.1.test", out version);
            success.ShouldEqual(true);
            version.Revision.ShouldEqual(0);
            version.MinorRevision.ShouldEqual(0);
        }

        [TestCase]
        public void Version_Data_Too_Many_Numbers_Returns_True()
        {
            GitVersion version;
            bool success = GitVersion.TryParseVersion("2.0.1.test.1.4.3.6", out version);
            success.ShouldEqual(true);
        }

        [TestCase]
        public void Version_Data_Valid_Returns_True()
        {
            GitVersion version;
            bool success = GitVersion.TryParseVersion("2.0.1.test.1.2", out version);
            success.ShouldEqual(true);
        }

        [TestCase]
        public void Compare_Different_Platforms_Returns_False()
        {
            GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, "test1", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(false);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_Version_Equal()
        {
            GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(false);
            version1.IsEqualTo(version2).ShouldEqual(true);
        }

        [TestCase]
        public void Compare_Version_Major_Less()
        {
            GitVersion version1 = new GitVersion(0, 2, 3, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(true);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_Version_Major_Greater()
        {
            GitVersion version1 = new GitVersion(2, 2, 3, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(false);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_Version_Minor_Less()
        {
            GitVersion version1 = new GitVersion(1, 1, 3, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(true);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_Version_Minor_Greater()
        {
            GitVersion version1 = new GitVersion(1, 3, 3, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(false);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_ReleaseCandidate_Less()
        {
            GitVersion version1 = new GitVersion(1, 2, 3, 1, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, 2, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(true);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_ReleaseCandidate_Greater()
        {
            GitVersion version1 = new GitVersion(1, 2, 3, 2, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, 1, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(false);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_ReleaseCandidate_NonRC_Less()
        {
            GitVersion version1 = new GitVersion(1, 2, 3, 0, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, null, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(true);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_ReleaseCandidate_NonRC_Greater()
        {
            GitVersion version1 = new GitVersion(1, 2, 3, null, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, 0, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(false);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_Version_Build_Less()
        {
            GitVersion version1 = new GitVersion(1, 2, 2, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(true);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_Version_Build_Greater()
        {
            GitVersion version1 = new GitVersion(1, 2, 4, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(false);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_Version_Revision_Less()
        {
            GitVersion version1 = new GitVersion(1, 2, 3, "test", 3, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(true);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_Version_Revision_Greater()
        {
            GitVersion version1 = new GitVersion(1, 2, 3, "test", 5, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(false);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_Version_MinorRevision_Less()
        {
            GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1);
            GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 2);

            version1.IsLessThan(version2).ShouldEqual(true);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Compare_Version_MinorRevision_Greater()
        {
            GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 2);
            GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1);

            version1.IsLessThan(version2).ShouldEqual(false);
            version1.IsEqualTo(version2).ShouldEqual(false);
        }

        [TestCase]
        public void Allow_Blank_Minor_Revision()
        {
            GitVersion version;
            GitVersion.TryParseVersion("1.2.3.test.4", out version).ShouldEqual(true);

            version.Major.ShouldEqual(1);
            version.Minor.ShouldEqual(2);
            version.Build.ShouldEqual(3);
            version.ReleaseCandidate.ShouldEqual(null);
            version.Platform.ShouldEqual("test");
            version.Revision.ShouldEqual(4);
            version.MinorRevision.ShouldEqual(0);
        }

        [TestCase]
        public void Allow_Invalid_Minor_Revision()
        {
            GitVersion version;
            GitVersion.TryParseVersion("1.2.3.test.4.notint", out version).ShouldEqual(true);

            version.Major.ShouldEqual(1);
            version.Minor.ShouldEqual(2);
            version.Build.ShouldEqual(3);
            version.ReleaseCandidate.ShouldEqual(null);
            version.Platform.ShouldEqual("test");
            version.Revision.ShouldEqual(4);
            version.MinorRevision.ShouldEqual(0);
        }

        [TestCase]
        public void Allow_ReleaseCandidates()
        {
            GitVersion version;
            GitVersion.TryParseVersion("1.2.3.rc2.test.4.5", out version).ShouldEqual(true);

            version.Major.ShouldEqual(1);
            version.Minor.ShouldEqual(2);
            version.Build.ShouldEqual(3);
            version.ReleaseCandidate.ShouldEqual(2);
            version.Platform.ShouldEqual("test");
            version.Revision.ShouldEqual(4);
            version.MinorRevision.ShouldEqual(5);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/HydrationStatusCircuitBreakerTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using NUnit.Framework;
using System.IO;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class HydrationStatusCircuitBreakerTests
    {
        private MockTracer tracer;
        private string dotGVFSRoot;
        private string tempDir;

        [SetUp]
        public void Setup()
        {
            this.tempDir = Path.Combine(Path.GetTempPath(), "GVFS_CircuitBreakerTest_" + Path.GetRandomFileName());
            this.dotGVFSRoot = Path.Combine(this.tempDir, ".gvfs");
            Directory.CreateDirectory(Path.Combine(this.dotGVFSRoot, "gitStatusCache"));
            this.tracer = new MockTracer();
        }

        [TearDown]
        public void TearDown()
        {
            if (Directory.Exists(this.tempDir))
            {
                Directory.Delete(this.tempDir, recursive: true);
            }
        }

        [Test]
        public void IsDisabledReturnsFalseWhenNoMarkerFile()
        {
            HydrationStatusCircuitBreaker breaker = this.CreateBreaker();
            breaker.IsDisabled().ShouldBeFalse();
        }

        [Test]
        public void SingleFailureDoesNotDisable()
        {
            HydrationStatusCircuitBreaker breaker = this.CreateBreaker();
            breaker.RecordFailure();
            breaker.IsDisabled().ShouldBeFalse();
        }

        [Test]
        public void TwoFailuresDoNotDisable()
        {
            HydrationStatusCircuitBreaker breaker = this.CreateBreaker();
            breaker.RecordFailure();
            breaker.RecordFailure();
            breaker.IsDisabled().ShouldBeFalse();
        }

        [Test]
        public void ThreeFailuresTripsBreaker()
        {
            HydrationStatusCircuitBreaker breaker = this.CreateBreaker();
            breaker.RecordFailure();
            breaker.RecordFailure();
            breaker.RecordFailure();
            breaker.IsDisabled().ShouldBeTrue();
        }

        [Test]
        public void BreakerResetsOnNewDay()
        {
            HydrationStatusCircuitBreaker breaker = this.CreateBreaker();

            // Simulate a marker file from yesterday
            string markerPath = Path.Combine(
                this.dotGVFSRoot,
                GVFSConstants.DotGVFS.HydrationStatus.DisabledMarkerFile);
            File.WriteAllText(
                markerPath,
                $"2020-01-01\n{ProcessHelper.GetCurrentProcessVersion()}\n5");

            breaker.IsDisabled().ShouldBeFalse("Circuit breaker should reset on a new day");
        }

        [Test]
        public void BreakerResetsOnVersionChange()
        {
            HydrationStatusCircuitBreaker breaker = this.CreateBreaker();

            // Simulate a marker file with a different GVFS version
            string markerPath = Path.Combine(
                this.dotGVFSRoot,
                GVFSConstants.DotGVFS.HydrationStatus.DisabledMarkerFile);
            string today = System.DateTime.UtcNow.ToString("yyyy-MM-dd");
            File.WriteAllText(
                markerPath,
                $"{today}\n99.99.99.99\n5");

            breaker.IsDisabled().ShouldBeFalse("Circuit breaker should reset when GVFS version changes");
        }

        [Test]
        public void BreakerStaysTrippedOnSameDayAndVersion()
        {
            HydrationStatusCircuitBreaker breaker = this.CreateBreaker();

            string markerPath = Path.Combine(
                this.dotGVFSRoot,
                GVFSConstants.DotGVFS.HydrationStatus.DisabledMarkerFile);
            string today = System.DateTime.UtcNow.ToString("yyyy-MM-dd");
            string currentVersion = ProcessHelper.GetCurrentProcessVersion();
            File.WriteAllText(
                markerPath,
                $"{today}\n{currentVersion}\n3");

            breaker.IsDisabled().ShouldBeTrue("Circuit breaker should remain tripped on same day and version");
        }

        [Test]
        public void TryParseMarkerFileHandlesValidContent()
        {
            bool result = HydrationStatusCircuitBreaker.TryParseMarkerFile(
                "2026-03-11\n0.2.26070.19566\n3",
                out string date,
                out string version,
                out int count);

            result.ShouldBeTrue();
            date.ShouldEqual("2026-03-11");
            version.ShouldEqual("0.2.26070.19566");
            count.ShouldEqual(3);
        }

        [Test]
        public void TryParseMarkerFileHandlesEmptyContent()
        {
            HydrationStatusCircuitBreaker.TryParseMarkerFile(
                string.Empty,
                out string _,
                out string _,
                out int _).ShouldBeFalse();
        }

        [Test]
        public void TryParseMarkerFileHandlesCorruptContent()
        {
            HydrationStatusCircuitBreaker.TryParseMarkerFile(
                "garbage",
                out string _,
                out string _,
                out int _).ShouldBeFalse();
        }

        [Test]
        public void TryParseMarkerFileHandlesNonNumericCount()
        {
            HydrationStatusCircuitBreaker.TryParseMarkerFile(
                "2026-03-11\n0.2.26070.19566\nabc",
                out string _,
                out string _,
                out int _).ShouldBeFalse();
        }

        [Test]
        public void RecordFailureLogsWarningWhenBreakerTrips()
        {
            HydrationStatusCircuitBreaker breaker = this.CreateBreaker();
            breaker.RecordFailure();
            breaker.RecordFailure();
            breaker.RecordFailure();

            this.tracer.RelatedWarningEvents.Count.ShouldBeAtLeast(
                1,
                "Should log a warning when circuit breaker trips");
        }

        private HydrationStatusCircuitBreaker CreateBreaker()
        {
            return new HydrationStatusCircuitBreaker(
                this.dotGVFSRoot,
                this.tracer);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/HydrationStatusErrorPathTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;
using System.IO;
using System.Linq;
using System.Threading;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class HydrationStatusErrorPathTests
    {
        private const string HeadTreeId = "0123456789012345678901234567890123456789";
        private const int HeadPathCount = 42;

        private MockFileSystem fileSystem;
        private MockGitProcess gitProcess;
        private MockTracer tracer;
        private GVFSContext context;
        private string gitParentPath;
        private string gvfsMetadataPath;
        private MockDirectory enlistmentDirectory;

        [SetUp]
        public void Setup()
        {
            this.tracer = new MockTracer();

            string enlistmentRoot = Path.Combine("mock:", "GVFS", "UnitTests", "Repo");
            string statusCachePath = Path.Combine("mock:", "GVFS", "UnitTests", "Repo", GVFSPlatform.Instance.Constants.DotGVFSRoot, "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", this.gitProcess);
            enlistment.InitializeCachePathsFromKey("fake:\\gvfsSharedCache", "fakeCacheKey");

            this.gitParentPath = enlistment.WorkingDirectoryBackingRoot;
            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(
                this.tracer,
                this.fileSystem,
                new MockGitRepo(this.tracer, enlistment, this.fileSystem),
                enlistment);
        }

        [TearDown]
        public void TearDown()
        {
            this.fileSystem = null;
            this.gitProcess = null;
            this.tracer = null;
            this.context = null;
        }

        #region HydrationStatus.Response TryParse error paths

        [TestCase(null)]
        [TestCase("")]
        public void TryParse_NullOrEmpty_ReturnsFalse(string body)
        {
            bool result = NamedPipeMessages.HydrationStatus.Response.TryParse(body, out NamedPipeMessages.HydrationStatus.Response response);
            Assert.IsFalse(result);
            Assert.IsNull(response);
        }

        [TestCase("1,2,3")]
        [TestCase("1,2,3,4,5")]
        public void TryParse_TooFewParts_ReturnsFalse(string body)
        {
            bool result = NamedPipeMessages.HydrationStatus.Response.TryParse(body, out NamedPipeMessages.HydrationStatus.Response response);
            Assert.IsFalse(result);
            Assert.IsNull(response);
        }

        [TestCase("abc,2,3,4,5,6")]
        [TestCase("1,2,three,4,5,6")]
        [TestCase("1,2,3,4,5,six")]
        public void TryParse_NonIntegerValues_ReturnsFalse(string body)
        {
            bool result = NamedPipeMessages.HydrationStatus.Response.TryParse(body, out NamedPipeMessages.HydrationStatus.Response response);
            Assert.IsFalse(result);
            Assert.IsNull(response);
        }

        [TestCase("-1,0,0,0,10,5")]
        [TestCase("0,-1,0,0,10,5")]
        [TestCase("0,0,-1,0,10,5")]
        [TestCase("0,0,0,-1,10,5")]
        public void TryParse_NegativeCounts_ReturnsFalse(string body)
        {
            bool result = NamedPipeMessages.HydrationStatus.Response.TryParse(body, out NamedPipeMessages.HydrationStatus.Response response);
            Assert.IsFalse(result);
        }

        [TestCase("100,0,100,0,50,5")]
        [TestCase("0,100,0,100,10,5")]
        public void TryParse_HydratedExceedsTotal_ReturnsFalse(string body)
        {
            bool result = NamedPipeMessages.HydrationStatus.Response.TryParse(body, out NamedPipeMessages.HydrationStatus.Response response);
            Assert.IsFalse(result);
        }

        [TestCase]
        public void TryParse_ValidResponse_Succeeds()
        {
            bool result = NamedPipeMessages.HydrationStatus.Response.TryParse(
                "10,5,3,2,100,50",
                out NamedPipeMessages.HydrationStatus.Response response);

            Assert.IsTrue(result);
            Assert.AreEqual(10, response.PlaceholderFileCount);
            Assert.AreEqual(5, response.PlaceholderFolderCount);
            Assert.AreEqual(3, response.ModifiedFileCount);
            Assert.AreEqual(2, response.ModifiedFolderCount);
            Assert.AreEqual(100, response.TotalFileCount);
            Assert.AreEqual(50, response.TotalFolderCount);
            Assert.AreEqual(13, response.HydratedFileCount);
            Assert.AreEqual(7, response.HydratedFolderCount);
        }

        [TestCase]
        public void TryParse_ExtraFields_IgnoredAndSucceeds()
        {
            bool result = NamedPipeMessages.HydrationStatus.Response.TryParse(
                "10,5,3,2,100,50,extra,fields",
                out NamedPipeMessages.HydrationStatus.Response response);

            Assert.IsTrue(result);
            Assert.AreEqual(10, response.PlaceholderFileCount);
            Assert.AreEqual(100, response.TotalFileCount);
        }

        [TestCase]
        public void TryParse_ZeroCounts_IsValid()
        {
            bool result = NamedPipeMessages.HydrationStatus.Response.TryParse(
                "0,0,0,0,0,0",
                out NamedPipeMessages.HydrationStatus.Response response);

            Assert.IsTrue(result);
            Assert.IsTrue(response.IsValid);
        }

        [TestCase]
        public void ToBody_RoundTrips_WithTryParse()
        {
            NamedPipeMessages.HydrationStatus.Response original = new NamedPipeMessages.HydrationStatus.Response
            {
                PlaceholderFileCount = 42,
                PlaceholderFolderCount = 10,
                ModifiedFileCount = 8,
                ModifiedFolderCount = 3,
                TotalFileCount = 1000,
                TotalFolderCount = 200,
            };

            string body = original.ToBody();
            bool result = NamedPipeMessages.HydrationStatus.Response.TryParse(body, out NamedPipeMessages.HydrationStatus.Response parsed);

            Assert.IsTrue(result);
            Assert.AreEqual(original.PlaceholderFileCount, parsed.PlaceholderFileCount);
            Assert.AreEqual(original.PlaceholderFolderCount, parsed.PlaceholderFolderCount);
            Assert.AreEqual(original.ModifiedFileCount, parsed.ModifiedFileCount);
            Assert.AreEqual(original.ModifiedFolderCount, parsed.ModifiedFolderCount);
            Assert.AreEqual(original.TotalFileCount, parsed.TotalFileCount);
            Assert.AreEqual(original.TotalFolderCount, parsed.TotalFolderCount);
        }

        [TestCase]
        public void ToDisplayMessage_InvalidResponse_ReturnsNull()
        {
            NamedPipeMessages.HydrationStatus.Response response = new NamedPipeMessages.HydrationStatus.Response
            {
                PlaceholderFileCount = -1,
                TotalFileCount = 100,
            };

            Assert.IsNull(response.ToDisplayMessage());
        }

        [TestCase]
        public void ToDisplayMessage_ValidResponse_FormatsCorrectly()
        {
            NamedPipeMessages.HydrationStatus.Response response = new NamedPipeMessages.HydrationStatus.Response
            {
                PlaceholderFileCount = 40,
                PlaceholderFolderCount = 10,
                ModifiedFileCount = 10,
                ModifiedFolderCount = 5,
                TotalFileCount = 100,
                TotalFolderCount = 50,
            };

            string message = response.ToDisplayMessage();
            Assert.IsNotNull(message);
            Assert.That(message, Does.Contain("50%"));
            Assert.That(message, Does.Contain("30%"));
        }

        #endregion
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/JsonTracerTests.cs
================================================
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common.Tracing;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class JsonTracerTests
    {
        [TestCase]
        public void EventsAreFilteredByVerbosity()
        {
            using (JsonTracer tracer = new JsonTracer("Microsoft-GVFS-Test", "EventsAreFilteredByVerbosity1", disableTelemetry: true))
            using (MockListener listener = new MockListener(EventLevel.Informational, Keywords.Any))
            {
                tracer.AddEventListener(listener);

                tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null);
                listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive"));

                tracer.RelatedEvent(EventLevel.Verbose, "ShouldNotReceive", metadata: null);
                listener.EventNamesRead.ShouldNotContain(name => name.Equals("ShouldNotReceive"));
            }

            using (JsonTracer tracer = new JsonTracer("Microsoft-GVFS-Test", "EventsAreFilteredByVerbosity2", disableTelemetry: true))
            using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any))
            {
                tracer.AddEventListener(listener);

                tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null);
                listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive"));

                tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoReceive", metadata: null);
                listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldAlsoReceive"));
            }
        }

        [TestCase]
        public void EventsAreFilteredByKeyword()
        {
            // Network filters all but network out
            using (JsonTracer tracer = new JsonTracer("Microsoft-GVFS-Test", "EventsAreFilteredByKeyword1", disableTelemetry: true))
            using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Network))
            {
                tracer.AddEventListener(listener);

                tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null, keyword: Keywords.Network);
                listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive"));

                tracer.RelatedEvent(EventLevel.Verbose, "ShouldNotReceive", metadata: null);
                listener.EventNamesRead.ShouldNotContain(name => name.Equals("ShouldNotReceive"));
            }

            // Any filters nothing out
            using (JsonTracer tracer = new JsonTracer("Microsoft-GVFS-Test", "EventsAreFilteredByKeyword2", disableTelemetry: true))
            using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any))
            {
                tracer.AddEventListener(listener);

                tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null, keyword: Keywords.Network);
                listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive"));

                tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoReceive", metadata: null);
                listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldAlsoReceive"));
            }

            // None filters everything out (including events marked as none)
            using (JsonTracer tracer = new JsonTracer("Microsoft-GVFS-Test", "EventsAreFilteredByKeyword3", disableTelemetry: true))
            using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.None))
            {
                tracer.AddEventListener(listener);

                tracer.RelatedEvent(EventLevel.Informational, "ShouldNotReceive", metadata: null, keyword: Keywords.Network);
                listener.EventNamesRead.ShouldBeEmpty();

                tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoNotReceive", metadata: null);
                listener.EventNamesRead.ShouldBeEmpty();
            }
        }

        [TestCase]
        public void EventMetadataWithKeywordsIsOptional()
        {
            using (JsonTracer tracer = new JsonTracer("Microsoft-GVFS-Test", "EventMetadataWithKeywordsIsOptional", disableTelemetry: true))
            using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any))
            {
                tracer.AddEventListener(listener);

                tracer.RelatedWarning(metadata: null, message: string.Empty, keywords: Keywords.Telemetry);
                listener.EventNamesRead.ShouldContain(x => x.Equals("Warning"));

                tracer.RelatedError(metadata: null, message: string.Empty, keywords: Keywords.Telemetry);
                listener.EventNamesRead.ShouldContain(x => x.Equals("Error"));
            }
        }

        [TestCase]
        public void StartEventDoesNotDispatchTelemetry()
        {
            using (JsonTracer tracer = new JsonTracer("Microsoft-GVFS-Test", "StartEventDoesNotDispatchTelemetry", disableTelemetry: true))
            using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Telemetry))
            {
                tracer.AddEventListener(listener);

                using (ITracer activity = tracer.StartActivity("TestActivity", EventLevel.Informational, Keywords.Telemetry, null))
                {
                    listener.EventNamesRead.ShouldBeEmpty();

                    activity.Stop(null);
                    listener.EventNamesRead.ShouldContain(x => x.Equals("TestActivity"));
                }
            }
        }

        [TestCase]
        public void StopEventIsDispatchedOnDispose()
        {
            using (JsonTracer tracer = new JsonTracer("Microsoft-GVFS-Test", "StopEventIsDispatchedOnDispose", disableTelemetry: true))
            using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Telemetry))
            {
                tracer.AddEventListener(listener);

                using (ITracer activity = tracer.StartActivity("TestActivity", EventLevel.Informational, Keywords.Telemetry, null))
                {
                    listener.EventNamesRead.ShouldBeEmpty();
                }

                listener.EventNamesRead.ShouldContain(x => x.Equals("TestActivity"));
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/LegacyPlaceholderDatabaseTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock;
using GVFS.UnitTests.Mock.FileSystem;
using NUnit.Framework;
using System.Collections.Generic;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class LegacyPlaceholderDatabaseTests
    {
        private const string MockEntryFileName = "mock:\\entries.dat";

        private const string InputGitIgnorePath = ".gitignore";
        private const string InputGitIgnoreSHA = "AE930E4CF715315FC90D4AEC98E16A7398F8BF64";

        private const string InputGitAttributesPath = ".gitattributes";
        private const string InputGitAttributesSHA = "BB9630E4CF715315FC90D4AEC98E167398F8BF66";

        private const string InputThirdFilePath = "thirdFile";
        private const string InputThirdFileSHA = "ff9630E00F715315FC90D4AEC98E6A7398F8BF11";

        private const string PlaceholderDatabaseNewLine = "\r\n";
        private const string ExpectedGitIgnoreEntry = "A " + InputGitIgnorePath + "\0" + InputGitIgnoreSHA + PlaceholderDatabaseNewLine;
        private const string ExpectedGitAttributesEntry = "A " + InputGitAttributesPath + "\0" + InputGitAttributesSHA + PlaceholderDatabaseNewLine;

        private const string ExpectedTwoEntries = ExpectedGitIgnoreEntry + ExpectedGitAttributesEntry;

        [TestCase]
        public void ParsesExistingDataCorrectly()
        {
            ConfigurableFileSystem fs = new ConfigurableFileSystem();
            LegacyPlaceholderListDatabase dut = CreatePlaceholderListDatabase(
                fs,
                "A .gitignore\0AE930E4CF715315FC90D4AEC98E16A7398F8BF64\r\n" +
                "A Test_EPF_UpdatePlaceholderTests\\LockToPreventDelete\\test.txt\0B6948308A8633CC1ED94285A1F6BF33E35B7C321\r\n" +
                "A Test_EPF_UpdatePlaceholderTests\\LockToPreventDelete\\test.txt\0C7048308A8633CC1ED94285A1F6BF33E35B7C321\r\n" +
                "A Test_EPF_UpdatePlaceholderTests\\LockToPreventDelete\\test2.txt\0D19198D6EA60F0D66F0432FEC6638D0A73B16E81\r\n" +
                "A Test_EPF_UpdatePlaceholderTests\\LockToPreventDelete\\test3.txt\0E45EA0D328E581696CAF1F823686F3665A5F05C1\r\n" +
                "A Test_EPF_UpdatePlaceholderTests\\LockToPreventDelete\\test4.txt\0FCB3E2C561649F102DD8110A87DA82F27CC05833\r\n" +
                "A Test_EPF_UpdatePlaceholderTests\\LockToPreventUpdate\\test.txt\0E51B377C95076E4C6A9E22A658C5690F324FD0AD\r\n" +
                "D Test_EPF_UpdatePlaceholderTests\\LockToPreventUpdate\\test.txt\r\n" +
                "D Test_EPF_UpdatePlaceholderTests\\LockToPreventUpdate\\test.txt\r\n" +
                "D Test_EPF_UpdatePlaceholderTests\\LockToPreventUpdate\\test.txt\r\n");
            dut.GetCount().ShouldEqual(5);
        }

        [TestCase]
        public void WritesPlaceholderAddToFile()
        {
            ConfigurableFileSystem fs = new ConfigurableFileSystem();
            LegacyPlaceholderListDatabase dut = CreatePlaceholderListDatabase(fs, string.Empty);
            dut.AddFile(InputGitIgnorePath, InputGitIgnoreSHA);

            fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldEqual(ExpectedGitIgnoreEntry);

            dut.AddFile(InputGitAttributesPath, InputGitAttributesSHA);

            fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldEqual(ExpectedTwoEntries);
        }

        [TestCase]
        public void GetAllEntriesReturnsCorrectEntries()
        {
            ConfigurableFileSystem fs = new ConfigurableFileSystem();
            using (LegacyPlaceholderListDatabase dut1 = CreatePlaceholderListDatabase(fs, string.Empty))
            {
                dut1.AddFile(InputGitIgnorePath, InputGitIgnoreSHA);
                dut1.AddFile(InputGitAttributesPath, InputGitAttributesSHA);
                dut1.AddFile(InputThirdFilePath, InputThirdFileSHA);
                dut1.Remove(InputThirdFilePath);
            }

            string error;
            LegacyPlaceholderListDatabase dut2;
            LegacyPlaceholderListDatabase.TryCreate(null, MockEntryFileName, fs, out dut2, out error).ShouldEqual(true, error);
            List allData = dut2.GetAllEntries();
            allData.Count.ShouldEqual(2);
        }

        [TestCase]
        public void GetAllEntriesSplitsFilesAndFoldersCorrectly()
        {
            ConfigurableFileSystem fs = new ConfigurableFileSystem();
            using (LegacyPlaceholderListDatabase dut1 = CreatePlaceholderListDatabase(fs, string.Empty))
            {
                dut1.AddFile(InputGitIgnorePath, InputGitIgnoreSHA);
                dut1.AddPartialFolder("partialFolder", sha: null);
                dut1.AddFile(InputGitAttributesPath, InputGitAttributesSHA);
                dut1.AddExpandedFolder("expandedFolder");
                dut1.AddFile(InputThirdFilePath, InputThirdFileSHA);
                dut1.AddPossibleTombstoneFolder("tombstone");
                dut1.Remove(InputThirdFilePath);
            }

            string error;
            LegacyPlaceholderListDatabase dut2;
            LegacyPlaceholderListDatabase.TryCreate(null, MockEntryFileName, fs, out dut2, out error).ShouldEqual(true, error);
            List fileData;
            List folderData;
            dut2.GetAllEntries(out fileData, out folderData);
            fileData.Count.ShouldEqual(2);
            folderData.Count.ShouldEqual(3);
            folderData.ShouldContain(
                new[]
                {
                    new LegacyPlaceholderListDatabase.PlaceholderData("partialFolder", LegacyPlaceholderListDatabase.PartialFolderValue),
                    new LegacyPlaceholderListDatabase.PlaceholderData("expandedFolder", LegacyPlaceholderListDatabase.ExpandedFolderValue),
                    new LegacyPlaceholderListDatabase.PlaceholderData("tombstone", LegacyPlaceholderListDatabase.PossibleTombstoneFolderValue),
                },
                (data1, data2) => data1.Path == data2.Path && data1.Sha == data2.Sha);
        }

        [TestCase]
        public void WriteAllEntriesCorrectlyWritesFile()
        {
            ConfigurableFileSystem fs = new ConfigurableFileSystem();
            fs.ExpectedFiles.Add(MockEntryFileName + ".tmp", new ReusableMemoryStream(string.Empty));

            LegacyPlaceholderListDatabase dut = CreatePlaceholderListDatabase(fs, string.Empty);

            List allData = new List()
            {
                new LegacyPlaceholderListDatabase.PlaceholderData(InputGitIgnorePath, InputGitIgnoreSHA),
                new LegacyPlaceholderListDatabase.PlaceholderData(InputGitAttributesPath, InputGitAttributesSHA)
            };

            dut.WriteAllEntriesAndFlush(allData);
            fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldEqual(ExpectedTwoEntries);
        }

        [TestCase]
        public void HandlesRaceBetweenAddAndWriteAllEntries()
        {
            ConfigurableFileSystem fs = new ConfigurableFileSystem();
            fs.ExpectedFiles.Add(MockEntryFileName + ".tmp", new ReusableMemoryStream(string.Empty));

            LegacyPlaceholderListDatabase dut = CreatePlaceholderListDatabase(fs, ExpectedGitIgnoreEntry);

            List existingEntries = dut.GetAllEntries();

            dut.AddFile(InputGitAttributesPath, InputGitAttributesSHA);

            dut.WriteAllEntriesAndFlush(existingEntries);
            fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldEqual(ExpectedTwoEntries);
        }

        [TestCase]
        public void HandlesRaceBetweenRemoveAndWriteAllEntries()
        {
            const string DeleteGitAttributesEntry = "D .gitattributes" + PlaceholderDatabaseNewLine;

            ConfigurableFileSystem fs = new ConfigurableFileSystem();
            fs.ExpectedFiles.Add(MockEntryFileName + ".tmp", new ReusableMemoryStream(string.Empty));

            LegacyPlaceholderListDatabase dut = CreatePlaceholderListDatabase(fs, ExpectedTwoEntries);

            List existingEntries = dut.GetAllEntries();

            dut.Remove(InputGitAttributesPath);

            dut.WriteAllEntriesAndFlush(existingEntries);
            fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldEqual(ExpectedTwoEntries + DeleteGitAttributesEntry);
        }

        private static LegacyPlaceholderListDatabase CreatePlaceholderListDatabase(ConfigurableFileSystem fs, string initialContents)
        {
            fs.ExpectedFiles.Add(MockEntryFileName, new ReusableMemoryStream(initialContents));

            string error;
            LegacyPlaceholderListDatabase dut;
            LegacyPlaceholderListDatabase.TryCreate(null, MockEntryFileName, fs, out dut, out error).ShouldEqual(true, error);
            dut.ShouldNotBeNull();
            return dut;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/LibGit2RepoInvokerTests.cs
================================================
using GVFS.Common.Git;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using NUnit.Framework;
using System.Collections.Concurrent;
using System.Threading;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class LibGit2RepoInvokerTests
    {
        private MockTracer tracer;
        private LibGit2RepoInvoker invoker;
        private int numConstructors;
        private int numDisposals;

        public BlockingCollection DisposalTriggers { get; set; }

        [SetUp]
        public void Setup()
        {
            this.invoker?.Dispose();

            this.tracer = new MockTracer();
            this.numConstructors = 0;
            this.numDisposals = 0;
            this.DisposalTriggers = new BlockingCollection();

            this.invoker = new LibGit2RepoInvoker(this.tracer, this.CreateRepo);
        }

        [TestCase]
        public void DoesCreateRepoOnConstruction()
        {
            this.numConstructors.ShouldEqual(1);
        }

        [TestCase]
        public void CreatedByInitializeAfterClosed()
        {
            this.numDisposals.ShouldEqual(0);
            this.numConstructors.ShouldEqual(1);

            this.invoker.DisposeSharedRepo();

            this.numDisposals.ShouldEqual(1);
            this.numConstructors.ShouldEqual(1);

            this.invoker.InitializeSharedRepo();

            this.numDisposals.ShouldEqual(1);
            this.numConstructors.ShouldEqual(2);

            // This should not create another repo
            this.invoker.TryInvoke(repo => { return true; }, out bool result);

            this.numDisposals.ShouldEqual(1);
            this.numConstructors.ShouldEqual(2);
        }

        [TestCase]
        public void CreatesOnInvokeAfterClosed()
        {
            this.numConstructors.ShouldEqual(1);

            this.invoker.DisposeSharedRepo();

            this.numDisposals.ShouldEqual(1);
            this.numConstructors.ShouldEqual(1);

            this.invoker.TryInvoke(repo => { return true; }, out bool result);

            this.numDisposals.ShouldEqual(1);
            this.numConstructors.ShouldEqual(2);

            // This should not create another repo
            this.invoker.InitializeSharedRepo();

            this.numDisposals.ShouldEqual(1);
            this.numConstructors.ShouldEqual(2);
        }

        [TestCase]
        public void DoesNotCreateMultipleRepos()
        {
            this.numConstructors.ShouldEqual(1);

            this.invoker.TryInvoke(repo => { return true; }, out bool result);
            result.ShouldEqual(true);
            this.numConstructors.ShouldEqual(1);

            this.invoker.TryInvoke(repo => { return true; }, out result);
            result.ShouldEqual(true);
            this.numConstructors.ShouldEqual(1);

            this.invoker.InitializeSharedRepo();
            this.numConstructors.ShouldEqual(1);
        }

        [TestCase]
        public void DoesNotCreateRepoAfterDisposal()
        {
            this.numConstructors.ShouldEqual(1);
            this.invoker.Dispose();
            this.invoker.TryInvoke(repo => { return true; }, out bool result);
            result.ShouldEqual(false);
            this.numConstructors.ShouldEqual(1);
        }

        [TestCase]
        public void DisposesSharedRepo()
        {
            this.numConstructors.ShouldEqual(1);
            this.numDisposals.ShouldEqual(0);

            this.invoker.TryInvoke(repo => { return true; }, out bool result);
            result.ShouldEqual(true);
            this.numConstructors.ShouldEqual(1);

            this.invoker.Dispose();
            this.numConstructors.ShouldEqual(1);
            this.numDisposals.ShouldEqual(1);
        }

        [TestCase]
        public void UsesOnlyOneRepoMultipleThreads()
        {
            this.numConstructors.ShouldEqual(1);

            Thread[] threads = new Thread[10];
            BlockingCollection threadStarted = new BlockingCollection();
            BlockingCollection allowNextThreadToContinue = new BlockingCollection();

            for (int i = 0; i < threads.Length; i++)
            {
                threads[i] = new Thread(() =>
                {
                    this.invoker.TryInvoke(
                        repo =>
                        {
                            threadStarted.Add(new object());
                            allowNextThreadToContinue.Take();

                            // Give the timer an opportunity to fire
                            Thread.Sleep(2);

                            allowNextThreadToContinue.Add(new object());
                            return true;
                        },
                        out bool result);
                    result.ShouldEqual(true);
                    this.numConstructors.ShouldEqual(1);
                });
            }

            // Ensure all threads are started before letting them continue
            for (int i = 0; i < threads.Length; i++)
            {
                threads[i].Start();
                threadStarted.Take();
            }

            allowNextThreadToContinue.Add(new object());

            for (int i = 0; i < threads.Length; i++)
            {
                threads[i].Join();
            }

            this.numConstructors.ShouldEqual(1);
        }

        private LibGit2Repo CreateRepo()
        {
            Interlocked.Increment(ref this.numConstructors);
            return new MockLibGit2Repo(this);
        }

        private class MockLibGit2Repo : LibGit2Repo
        {
            private readonly LibGit2RepoInvokerTests parent;

            public MockLibGit2Repo(LibGit2RepoInvokerTests parent)
            {
                this.parent = parent;
            }

            public override bool ObjectExists(string sha)
            {
                return false;
            }

            protected override void Dispose(bool disposing)
            {
                if (disposing)
                {
                    Interlocked.Increment(ref this.parent.numDisposals);
                    this.parent.DisposalTriggers.Add(new object());
                }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/LibGit2RepoSafeDirectoryTests.cs
================================================
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class LibGit2RepoSafeDirectoryTests
    {
        // ───────────────────────────────────────────────
        //  Layer 1 – NormalizePathForSafeDirectoryComparison (pure string tests)
        // ───────────────────────────────────────────────

        [TestCase(@"C:\Repos\Foo", "C:/REPOS/FOO")]
        [TestCase(@"c:\repos\foo", "C:/REPOS/FOO")]
        [TestCase("c:/repos/foo", "C:/REPOS/FOO")]
        [TestCase("C:/Repos/Foo/", "C:/REPOS/FOO")]
        [TestCase(@"C:\Repos\Foo\", "C:/REPOS/FOO")]
        [TestCase("C:/Repos/Foo///", "C:/REPOS/FOO")]
        [TestCase(@"C:\Repos/Mixed\Path", "C:/REPOS/MIXED/PATH")]
        [TestCase("already/normalized", "ALREADY/NORMALIZED")]
        public void NormalizePathForSafeDirectoryComparison_ProducesExpectedResult(string input, string expected)
        {
            LibGit2Repo.NormalizePathForSafeDirectoryComparison(input).ShouldEqual(expected);
        }

        [TestCase(null)]
        [TestCase("")]
        public void NormalizePathForSafeDirectoryComparison_HandlesNullAndEmpty(string input)
        {
            LibGit2Repo.NormalizePathForSafeDirectoryComparison(input).ShouldEqual(input);
        }

        [TestCase(@"C:\Repos\Foo", "c:/repos/foo")]
        [TestCase(@"C:\Repos\Foo", @"c:\Repos\Foo")]
        [TestCase("C:/Repos/Foo/", @"c:\repos\foo")]
        public void NormalizePathForSafeDirectoryComparison_CaseInsensitiveMatch(string a, string b)
        {
            LibGit2Repo.NormalizePathForSafeDirectoryComparison(a).ShouldEqual(LibGit2Repo.NormalizePathForSafeDirectoryComparison(b));
        }

        // ───────────────────────────────────────────────
        //  Layer 2 – Constructor control-flow tests via mock
        //  Tests go through the public LibGit2Repo(ITracer, string)
        //  constructor, which is the real entry point.
        // ───────────────────────────────────────────────

        [TestCase]
        public void Constructor_OwnershipError_WithMatchingConfigEntry_OpensSuccessfully()
        {
            // First Open() fails with ownership error, config has a case-variant match,
            // second Open() with the configured path succeeds → constructor completes.
            string requestedPath = @"C:\Repos\MyProject";
            string configuredPath = @"c:\repos\myproject";

            using (MockSafeDirectoryRepo repo = MockSafeDirectoryRepo.Create(
                requestedPath,
                safeDirectoryEntries: new[] { configuredPath },
                openableRepos: new HashSet(StringComparer.Ordinal) { configuredPath }))
            {
                // Constructor completed without throwing — the workaround succeeded.
                repo.OpenedPaths.ShouldContain(p => p == configuredPath);
            }
        }

        [TestCase]
        public void Constructor_OwnershipError_NoMatchingConfigEntry_Throws()
        {
            // Open() fails with ownership error, config has no matching entry → throws.
            string requestedPath = @"C:\Repos\MyProject";

            Assert.Throws(() =>
            {
                MockSafeDirectoryRepo.Create(
                    requestedPath,
                    safeDirectoryEntries: new[] { @"D:\Other\Repo" },
                    openableRepos: new HashSet(StringComparer.Ordinal));
            });
        }

        [TestCase]
        public void Constructor_OwnershipError_MatchButOpenFails_Throws()
        {
            // Open() fails with ownership error, config entry matches but
            // the retry also fails → throws.
            string requestedPath = @"C:\Repos\MyProject";
            string configuredPath = @"c:\repos\myproject";

            Assert.Throws(() =>
            {
                MockSafeDirectoryRepo.Create(
                    requestedPath,
                    safeDirectoryEntries: new[] { configuredPath },
                    openableRepos: new HashSet(StringComparer.Ordinal));
            });
        }

        [TestCase]
        public void Constructor_OwnershipError_EmptyConfig_Throws()
        {
            string requestedPath = @"C:\Repos\MyProject";

            Assert.Throws(() =>
            {
                MockSafeDirectoryRepo.Create(
                    requestedPath,
                    safeDirectoryEntries: Array.Empty(),
                    openableRepos: new HashSet(StringComparer.Ordinal));
            });
        }

        [TestCase]
        public void Constructor_OwnershipError_MultipleEntries_PicksCorrectMatch()
        {
            // Config has several entries; only one is a case-variant match.
            string requestedPath = @"C:\Repos\Target";
            string correctConfigEntry = @"c:/repos/target";

            using (MockSafeDirectoryRepo repo = MockSafeDirectoryRepo.Create(
                requestedPath,
                safeDirectoryEntries: new[]
                {
                    @"D:\Other\Repo",
                    correctConfigEntry,
                    @"E:\Unrelated\Path",
                },
                openableRepos: new HashSet(StringComparer.Ordinal)
                {
                    correctConfigEntry,
                }))
            {
                repo.OpenedPaths.ShouldContain(p => p == correctConfigEntry);
            }
        }

        [TestCase]
        public void Constructor_NonOwnershipError_Throws()
        {
            // Open() fails with a different error (not ownership) → throws
            // without attempting safe.directory workaround.
            string requestedPath = @"C:\Repos\MyProject";

            Assert.Throws(() =>
            {
                MockSafeDirectoryRepo.Create(
                    requestedPath,
                    safeDirectoryEntries: new[] { requestedPath },
                    openableRepos: new HashSet(StringComparer.Ordinal),
                    nativeError: "repository not found");
            });

            MockSafeDirectoryRepo.LastCreatedInstance
                .SafeDirectoryCheckAttempted
                .ShouldBeFalse("Safe.directory workaround should not be attempted for non-ownership errors");
        }

        [TestCase]
        public void Constructor_OpenSucceedsFirstTime_NoWorkaround()
        {
            // Open() succeeds immediately → no safe.directory logic triggered.
            string requestedPath = @"C:\Repos\MyProject";

            using (MockSafeDirectoryRepo repo = MockSafeDirectoryRepo.Create(
                requestedPath,
                safeDirectoryEntries: Array.Empty(),
                openableRepos: new HashSet(StringComparer.Ordinal) { requestedPath }))
            {
                // Only one Open call (the initial one), no retry.
                repo.OpenedPaths.Count.ShouldEqual(1);
                repo.OpenedPaths.ShouldContain(p => p == requestedPath);
            }
        }

        /// 
        /// Mock that intercepts all native P/Invoke calls so the public
        /// constructor can be exercised without touching libgit2.
        /// Uses thread-static config to work around virtual-call-from-
        /// constructor ordering (base ctor runs before derived fields init).
        /// 
        private class MockSafeDirectoryRepo : LibGit2Repo
        {
            [ThreadStatic]
            private static MockConfig pendingConfig;

            [ThreadStatic]
            private static MockSafeDirectoryRepo lastCreatedInstance;

            private string[] safeDirectoryEntries;
            private HashSet openableRepos;
            private string nativeError;

            public List OpenedPaths { get; } = new List();
            public bool SafeDirectoryCheckAttempted { get; private set; }

            /// 
            /// Returns the most recently constructed instance on the current
            /// thread, even if the constructor threw an exception.
            /// 
            public static MockSafeDirectoryRepo LastCreatedInstance => lastCreatedInstance;

            private MockSafeDirectoryRepo(ITracer tracer, string repoPath)
                : base(tracer, repoPath)
            {
                // Fields already populated from pendingConfig by the time
                // virtual methods are called from base ctor.
            }

            public static MockSafeDirectoryRepo Create(
                string repoPath,
                string[] safeDirectoryEntries,
                HashSet openableRepos,
                string nativeError = "repository path '/some/path' is not owned by current user")
            {
                pendingConfig = new MockConfig
                {
                    SafeDirectoryEntries = safeDirectoryEntries,
                    OpenableRepos = openableRepos,
                    NativeError = nativeError,
                };

                try
                {
                    return new MockSafeDirectoryRepo(NullTracer.Instance, repoPath);
                }
                finally
                {
                    pendingConfig = null;
                }
            }

            protected override void InitNative()
            {
                // Grab config from thread-static before base ctor proceeds.
                this.safeDirectoryEntries = pendingConfig.SafeDirectoryEntries;
                this.openableRepos = pendingConfig.OpenableRepos;
                this.nativeError = pendingConfig.NativeError;
                lastCreatedInstance = this;
            }

            protected override void ShutdownNative()
            {
            }

            protected override string GetLastNativeError()
            {
                return this.nativeError;
            }

            protected override void GetSafeDirectoryConfigEntries(MultiVarConfigCallback callback)
            {
                this.SafeDirectoryCheckAttempted = true;
                foreach (string entry in this.safeDirectoryEntries)
                {
                    callback(entry);
                }
            }

            protected override Native.ResultCode TryOpenRepo(string path, out IntPtr repoHandle)
            {
                this.OpenedPaths.Add(path);
                repoHandle = IntPtr.Zero;
                return this.openableRepos.Contains(path)
                    ? Native.ResultCode.Success
                    : Native.ResultCode.Failure;
            }

            protected override void Dispose(bool disposing)
            {
            }

            private class MockConfig
            {
                public string[] SafeDirectoryEntries;
                public HashSet OpenableRepos;
                public string NativeError;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/MissingTreeTrackerTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class MissingTreeTrackerTests
    {
        private static MissingTreeTracker CreateTracker(int treeCapacity)
        {
            return new MissingTreeTracker(new MockTracer(), treeCapacity);
        }

        // -------------------------------------------------------------------------
        // AddMissingRootTree
        // -------------------------------------------------------------------------

        [TestCase]
        public void AddMissingRootTree_SingleTreeAndCommit()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            tracker.AddMissingRootTree("tree1", "commit1");

            tracker.TryGetCommits("tree1", out string[] commits).ShouldEqual(true);
            commits.Length.ShouldEqual(1);
            commits[0].ShouldEqual("commit1");
            tracker.GetHighestMissingTreeCount(commits, out _).ShouldEqual(1);
        }

        [TestCase]
        public void AddMissingRootTree_MultipleTreesForSameCommit()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree2", "commit1");
            tracker.AddMissingRootTree("tree3", "commit1");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(3);

            tracker.TryGetCommits("tree1", out string[] c1).ShouldEqual(true);
            c1[0].ShouldEqual("commit1");

            tracker.TryGetCommits("tree2", out string[] c2).ShouldEqual(true);
            c2[0].ShouldEqual("commit1");

            tracker.TryGetCommits("tree3", out string[] c3).ShouldEqual(true);
            c3[0].ShouldEqual("commit1");
        }

        [TestCase]
        public void AddMissingRootTree_SameTreeAddedTwiceToSameCommit()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree1", "commit1");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(1);
        }

        [TestCase]
        public void AddMissingRootTree_SameTreeAddedToMultipleCommits()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree1", "commit2");

            // tree1 is now tracked under both commits
            tracker.TryGetCommits("tree1", out string[] commits).ShouldEqual(true);
            commits.Length.ShouldEqual(2);

            // Both commits each have 1 tree
            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(1);
        }

        [TestCase]
        public void AddMissingRootTree_MultipleTrees_ChecksCount()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(1);

            tracker.AddMissingRootTree("tree2", "commit1");
            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(2);

            tracker.AddMissingRootTree("tree3", "commit1");
            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(3);

            tracker.AddMissingRootTree("tree4", "commit1");
            tracker.AddMissingRootTree("tree5", "commit1");
            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(5);
        }

        // -------------------------------------------------------------------------
        // AddMissingSubTrees
        // -------------------------------------------------------------------------

        [TestCase]
        public void AddMissingSubTrees_AddsSubTreesUnderParentsCommits()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            tracker.AddMissingRootTree("rootTree", "commit1");
            tracker.AddMissingSubTrees("rootTree", new[] { "sub1", "sub2" });

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(3);

            tracker.TryGetCommits("sub1", out string[] c1).ShouldEqual(true);
            c1[0].ShouldEqual("commit1");

            tracker.TryGetCommits("sub2", out string[] c2).ShouldEqual(true);
            c2[0].ShouldEqual("commit1");
        }

        [TestCase]
        public void AddMissingSubTrees_PropagatesAcrossAllSharingCommits()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            // Two commits share the same root tree
            tracker.AddMissingRootTree("rootTree", "commit1");
            tracker.AddMissingRootTree("rootTree", "commit2");

            tracker.AddMissingSubTrees("rootTree", new[] { "sub1" });

            // sub1 should be tracked under both commits
            tracker.TryGetCommits("sub1", out string[] commits).ShouldEqual(true);
            commits.Length.ShouldEqual(2);

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(2);
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(2);
        }

        [TestCase]
        public void AddMissingSubTrees_NoOp_WhenParentNotTracked()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            // Should not throw; parent is not tracked
            tracker.AddMissingSubTrees("unknownParent", new[] { "sub1" });

            tracker.TryGetCommits("sub1", out _).ShouldEqual(false);
        }

        [TestCase]
        public void AddMissingSubTrees_SkipsCommitEvictedDuringLoop()
        {
            // treeCapacity = 2: rootTree fills slot 1, rootTree2 fills slot 2.
            // commit1 and commit2 both share rootTree (1 unique tree so far).
            // commit3 holds rootTree2 (2 unique trees, at capacity).
            // AddMissingSubTrees(rootTree, [sub1]) must add sub1 to commit1 then commit2.
            // Adding sub1 for commit1 fills the 3rd slot, which evicts the LRU commit.
            // commit2 is LRU (added to the tracker last among commit1/commit2 and then not used
            // again, while commit1 just got used), so it is evicted before we process commit2.
            // The loop must skip commit2 rather than crashing.
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 2);

            tracker.AddMissingRootTree("rootTree", "commit1");
            tracker.AddMissingRootTree("rootTree", "commit2");
            tracker.AddMissingRootTree("rootTree2", "commit3");

            // Does not throw, and sub1 ends up under whichever commit survived eviction
            tracker.AddMissingSubTrees("rootTree", new[] { "sub1" });

            // Exactly one of commit1/commit2 was evicted; sub1 exists under the survivor
            bool commit1HasSub1 = tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _) == 2;
            bool commit2HasSub1 = tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _) == 2;
            (commit1HasSub1 || commit2HasSub1).ShouldEqual(true);
            (commit1HasSub1 && commit2HasSub1).ShouldEqual(false);
        }

        [TestCase]
        public void AddMissingSubTrees_DoesNotEvictIfOnlyOneCommit()
        {
            /* This shouldn't be possible if user has a proper threshold and is marking commits
             * as completed, but test to be safe. */
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 2);
            tracker.AddMissingRootTree("rootTree", "commit1");
            tracker.AddMissingSubTrees("rootTree", new[] { "sub1" });
            tracker.AddMissingSubTrees("rootTree", new[] { "sub2" });
            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(3);
        }

        // -------------------------------------------------------------------------
        // TryGetCommits
        // -------------------------------------------------------------------------

        [TestCase]
        public void TryGetCommits_NonExistentTree()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            tracker.TryGetCommits("nonexistent", out string[] commits).ShouldEqual(false);
            commits.ShouldBeNull();
        }

        [TestCase]
        public void TryGetCommits_MarksAllCommitsAsRecentlyUsed()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 3);

            tracker.AddMissingRootTree("sharedTree", "commit1");
            tracker.AddMissingRootTree("sharedTree", "commit2");
            tracker.AddMissingRootTree("tree2", "commit3");
            tracker.AddMissingRootTree("tree3", "commit4");

            // Access commit1 and commit2 via TryGetCommits
            tracker.TryGetCommits("sharedTree", out _);

            // Adding a fourth tree should evict commit3 (oldest unused)
            tracker.AddMissingRootTree("tree4", "commit5");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit3" }, out _).ShouldEqual(0);
            tracker.GetHighestMissingTreeCount(new[] { "commit4" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit5" }, out _).ShouldEqual(1);
        }

        // -------------------------------------------------------------------------
        // GetHighestMissingTreeCount
        // -------------------------------------------------------------------------

        [TestCase]
        public void GetHighestMissingTreeCount_NonExistentCommit()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            tracker.GetHighestMissingTreeCount(new[] { "nonexistent" }, out string highest).ShouldEqual(0);
            highest.ShouldBeNull();
        }

        [TestCase]
        public void GetHighestMissingTreeCount_ReturnsCommitWithMostTrees()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree2", "commit1");
            tracker.AddMissingRootTree("tree3", "commit2");

            int count = tracker.GetHighestMissingTreeCount(new[] { "commit1", "commit2" }, out string highest);
            count.ShouldEqual(2);
            highest.ShouldEqual("commit1");
        }

        [TestCase]
        public void GetHighestMissingTreeCount_DoesNotUpdateLru()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 3);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree2", "commit2");
            tracker.AddMissingRootTree("tree3", "commit3");

            // Query commit1's count (should not update LRU)
            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _);

            // Adding a fourth commit should still evict commit1 (oldest)
            tracker.AddMissingRootTree("tree4", "commit4");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(0);
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit3" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit4" }, out _).ShouldEqual(1);
        }

        // -------------------------------------------------------------------------
        // MarkCommitComplete (cascade removal)
        // -------------------------------------------------------------------------

        [TestCase]
        public void MarkCommitComplete_RemovesAllTreesForCommit()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree2", "commit1");
            tracker.AddMissingRootTree("tree3", "commit1");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(3);

            tracker.MarkCommitComplete("commit1");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(0);
            tracker.TryGetCommits("tree1", out _).ShouldEqual(false);
            tracker.TryGetCommits("tree2", out _).ShouldEqual(false);
            tracker.TryGetCommits("tree3", out _).ShouldEqual(false);
        }

        [TestCase]
        public void MarkCommitComplete_NonExistentCommit()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            // Should not throw
            tracker.MarkCommitComplete("nonexistent");
        }

        [TestCase]
        public void MarkCommitComplete_CascadesSharedTreesToOtherCommits()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            // commit1 and commit2 share tree1; commit2 also has tree2
            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree1", "commit2");
            tracker.AddMissingRootTree("tree2", "commit2");

            tracker.MarkCommitComplete("commit1");

            // tree1 was in commit1, so it should be removed from commit2 as well
            tracker.TryGetCommits("tree1", out _).ShouldEqual(false);

            // tree2 is unrelated to commit1, so commit2 still has it
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(1);
            tracker.TryGetCommits("tree2", out string[] c2).ShouldEqual(true);
            c2[0].ShouldEqual("commit2");
        }

        [TestCase]
        public void MarkCommitComplete_RemovesOtherCommitWhenItBecomesEmpty()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            // commit2's only tree is shared with commit1
            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree1", "commit2");

            tracker.MarkCommitComplete("commit1");

            // commit2 had only tree1, which was cascaded away, so commit2 should be gone too
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(0);
            tracker.TryGetCommits("tree1", out _).ShouldEqual(false);
        }

        [TestCase]
        public void MarkCommitComplete_DoesNotAffectUnrelatedCommits()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 10);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree2", "commit2");

            tracker.MarkCommitComplete("commit1");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(0);
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(1);
            tracker.TryGetCommits("tree2", out string[] c).ShouldEqual(true);
            c[0].ShouldEqual("commit2");
        }

        // -------------------------------------------------------------------------
        // LRU eviction (no cascade)
        // -------------------------------------------------------------------------

        [TestCase]
        public void LruEviction_EvictsOldestCommit()
        {
            // treeCapacity = 3 trees; one tree per commit
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 3);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree2", "commit2");
            tracker.AddMissingRootTree("tree3", "commit3");

            // Adding a fourth tree exceeds treeCapacity, so commit1 (LRU) is evicted
            tracker.AddMissingRootTree("tree4", "commit4");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(0);
            tracker.TryGetCommits("tree1", out _).ShouldEqual(false);

            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit3" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit4" }, out _).ShouldEqual(1);
        }

        [TestCase]
        public void LruEviction_DoesNotCascadeSharedTreesToOtherCommits()
        {
            // treeCapacity = 3 trees; tree1 is shared so only 2 unique trees + tree3 = 3 total
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 3);

            // tree1 is shared between commit1 and commit2 (counts as 1 unique tree)
            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree2", "commit1");
            tracker.AddMissingRootTree("tree1", "commit2");
            tracker.AddMissingRootTree("tree3", "commit3");

            // tree4 is the 4th unique tree, exceeding treeCapacity; evicts commit1 (LRU)
            // which removes tree2, freeing up capacity.
            tracker.AddMissingRootTree("tree4", "commit4");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(0);

            // tree1 is still missing (not yet downloaded), so commit2 retains it
            tracker.TryGetCommits("tree1", out string[] commits).ShouldEqual(true);
            commits.Length.ShouldEqual(1);
            commits[0].ShouldEqual("commit2");
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(1);
        }

        [TestCase]
        public void LruEviction_AddingTreeToExistingCommitUpdatesLru()
        {
            // treeCapacity = 4 trees; tree1, tree2, tree3 fill it, then tree1b re-uses commit1
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 4);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree2", "commit2");
            tracker.AddMissingRootTree("tree3", "commit3");

            // Adding tree1b to commit1 marks commit1 as recently used (it's a new unique tree)
            tracker.AddMissingRootTree("tree1b", "commit1");

            // tree4 is the 5th unique tree, exceeding treeCapacity; commit2 is now LRU
            tracker.AddMissingRootTree("tree4", "commit4");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(2);
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(0);
            tracker.GetHighestMissingTreeCount(new[] { "commit3" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit4" }, out _).ShouldEqual(1);
        }

        [TestCase]
        public void LruEviction_MultipleTreesPerCommit_EvictsEntireCommit()
        {
            // treeCapacity = 4 trees; commit1 holds 3, commit2 holds 1
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 4);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree2", "commit1");
            tracker.AddMissingRootTree("tree3", "commit1");
            tracker.AddMissingRootTree("tree4", "commit2");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(3);
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(1);

            // tree5 is the 5th unique tree; evict LRU (commit1) freeing 3 slots, then add tree5
            tracker.AddMissingRootTree("tree5", "commit3");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(0);
            tracker.TryGetCommits("tree1", out _).ShouldEqual(false);
            tracker.TryGetCommits("tree2", out _).ShouldEqual(false);
            tracker.TryGetCommits("tree3", out _).ShouldEqual(false);

            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit3" }, out _).ShouldEqual(1);
        }

        [TestCase]
        public void LruEviction_CapacityOne()
        {
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 1);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(1);

            tracker.AddMissingRootTree("tree2", "commit2");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(0);
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(1);
        }

        [TestCase]
        public void LruEviction_ManyTreesOneCommit_ExceedsCapacity()
        {
            // treeCapacity = 3 trees; all trees belong to commit1
            // Adding a 4th tree must evict commit1 (the only commit) to make room
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 3);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree2", "commit1");
            tracker.AddMissingRootTree("tree3", "commit1");

            // tree4 exceeds the tree treeCapacity; the LRU commit (commit1) is evicted
            // and then commit2 with tree4 is added fresh
            tracker.AddMissingRootTree("tree4", "commit2");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(0);
            tracker.TryGetCommits("tree1", out _).ShouldEqual(false);
            tracker.TryGetCommits("tree2", out _).ShouldEqual(false);
            tracker.TryGetCommits("tree3", out _).ShouldEqual(false);

            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(1);
        }

        [TestCase]
        public void LruEviction_TryGetCommitsUpdatesLru()
        {
            // treeCapacity = 3 trees, one per commit
            MissingTreeTracker tracker = CreateTracker(treeCapacity: 3);

            tracker.AddMissingRootTree("tree1", "commit1");
            tracker.AddMissingRootTree("tree2", "commit2");
            tracker.AddMissingRootTree("tree3", "commit3");

            // Access commit1 via TryGetCommits (marks it as recently used)
            tracker.TryGetCommits("tree1", out _);

            // tree4 exceeds treeCapacity; commit2 is now LRU
            tracker.AddMissingRootTree("tree4", "commit4");

            tracker.GetHighestMissingTreeCount(new[] { "commit1" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit2" }, out _).ShouldEqual(0);
            tracker.GetHighestMissingTreeCount(new[] { "commit3" }, out _).ShouldEqual(1);
            tracker.GetHighestMissingTreeCount(new[] { "commit4" }, out _).ShouldEqual(1);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/ModifiedPathsDatabaseTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using NUnit.Framework;
using System;
using System.IO;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class ModifiedPathsDatabaseTests
    {
        private const string MockEntryFileName = "mock:\\entries.dat";

        private const string DefaultEntry = @".gitattributes";
        private const string ExistingEntries = @"A file.txt
A dir/file2.txt
A dir1/dir2/file3.txt
";
        private const string EntriesToCompress = @"A file.txt
D deleted.txt
A dir/file2.txt
A dir/dir3/dir4/
A dir1/dir2/file3.txt
A dir/
D deleted/
A dir1/dir2/
A dir1/file.txt
A dir1/dir2/dir3/dir4/dir5/
A dir/dir2/file3.txt
A dir/dir4/dir5/
D dir/dir2/deleted.txt
A dir1/dir2
";

        [TestCase]
        public void ParsesExistingDataCorrectly()
        {
            ModifiedPathsDatabase modifiedPathsDatabase = CreateModifiedPathsDatabase(ExistingEntries);
            modifiedPathsDatabase.Count.ShouldEqual(3);
            modifiedPathsDatabase.Contains("file.txt", isFolder: false).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir/file2.txt", isFolder: false).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir1/dir2/file3.txt", isFolder: false).ShouldBeTrue();
        }

        [TestCase]
        public void AddsDefaultEntry()
        {
            ModifiedPathsDatabase modifiedPathsDatabase = CreateModifiedPathsDatabase(initialContents: string.Empty);
            modifiedPathsDatabase.Count.ShouldEqual(1);
            modifiedPathsDatabase.Contains(DefaultEntry, isFolder: false).ShouldBeTrue();
        }

        [TestCase]
        public void BadDataFailsToLoad()
        {
            ConfigurableFileSystem configurableFileSystem = new ConfigurableFileSystem();
            configurableFileSystem.ExpectedFiles.Add(MockEntryFileName, new ReusableMemoryStream("This is bad data!\r\n"));

            string error;
            ModifiedPathsDatabase modifiedPathsDatabase;
            ModifiedPathsDatabase.TryLoadOrCreate(null, MockEntryFileName, configurableFileSystem, out modifiedPathsDatabase, out error).ShouldBeFalse();
            modifiedPathsDatabase.ShouldBeNull();
        }

        [TestCase]
        public void BasicAddFile()
        {
            TestAddingPath(Path.Combine("dir", "somefile.txt"));
        }

        [TestCase]
        public void DirectorySeparatorsNormalized()
        {
            TestAddingPath(Path.Combine("dir", "dir2", "dir3", "somefile.txt"));
        }

        [TestCase]
        public void BeginningDirectorySeparatorRemoved()
        {
            string filePath = Path.Combine("dir", "somefile.txt");
            TestAddingPath(pathToAdd: Path.DirectorySeparatorChar + filePath, pathInList: filePath);
        }

        [TestCase]
        public void DirectorySeparatorAddedForFolder()
        {
            TestAddingPath(pathToAdd: Path.Combine("dir", "subdir"), pathInList: Path.Combine("dir", "subdir") + Path.DirectorySeparatorChar, isFolder: true);
        }

        [TestCase]
        public void EntryNotAddedIfParentDirectoryExists()
        {
            ModifiedPathsDatabase modifiedPathsDatabase = CreateModifiedPathsDatabase(initialContents: "A dir/\r\n");
            modifiedPathsDatabase.Count.ShouldEqual(1);
            modifiedPathsDatabase.Contains("dir", isFolder: true).ShouldBeTrue();

            // Try adding a file for the directory that is in the modified paths
            modifiedPathsDatabase.TryAdd("dir/file.txt", isFolder: false, isRetryable: out _);
            modifiedPathsDatabase.Count.ShouldEqual(1);
            modifiedPathsDatabase.Contains("dir", isFolder: true).ShouldBeTrue();

            // Try adding a directory for the directory that is in the modified paths
            modifiedPathsDatabase.TryAdd("dir/dir2", isFolder: true, isRetryable: out _);
            modifiedPathsDatabase.Count.ShouldEqual(1);
            modifiedPathsDatabase.Contains("dir", isFolder: true).ShouldBeTrue();

            // Try adding a file for a directory that is not in the modified paths
            modifiedPathsDatabase.TryAdd("dir2/file.txt", isFolder: false, isRetryable: out _);
            modifiedPathsDatabase.Count.ShouldEqual(2);
            modifiedPathsDatabase.Contains("dir", isFolder: true).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir2/file.txt", isFolder: false).ShouldBeTrue();

            // Try adding a directory for a the directory that is not in the modified paths
            modifiedPathsDatabase.TryAdd("dir2/dir", isFolder: true, isRetryable: out _);
            modifiedPathsDatabase.Count.ShouldEqual(3);
            modifiedPathsDatabase.Contains("dir", isFolder: true).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir2/file.txt", isFolder: false).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir2/dir", isFolder: true).ShouldBeTrue();

            // Try adding a file in a subdirectory that is in the modified paths
            modifiedPathsDatabase.TryAdd("dir2/dir/file.txt", isFolder: false, isRetryable: out _);
            modifiedPathsDatabase.Count.ShouldEqual(3);
            modifiedPathsDatabase.Contains("dir", isFolder: true).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir2/file.txt", isFolder: false).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir2/dir", isFolder: true).ShouldBeTrue();

            // Try adding a directory for a subdirectory that is in the modified paths
            modifiedPathsDatabase.TryAdd("dir2/dir/dir3", isFolder: true, isRetryable: out _);
            modifiedPathsDatabase.Count.ShouldEqual(3);
            modifiedPathsDatabase.Contains("dir", isFolder: true).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir2/file.txt", isFolder: false).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir2/dir", isFolder: true).ShouldBeTrue();
        }

        [TestCase]
        public void RemoveEntriesWithParentFolderEntry()
        {
            ModifiedPathsDatabase modifiedPathsDatabase = CreateModifiedPathsDatabase(EntriesToCompress);
            modifiedPathsDatabase.RemoveEntriesWithParentFolderEntry(new MockTracer());
            modifiedPathsDatabase.Count.ShouldEqual(5);
            modifiedPathsDatabase.Contains("file.txt", isFolder: false).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir/", isFolder: true).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir1/dir2", isFolder: false).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir1/dir2/", isFolder: true).ShouldBeTrue();
            modifiedPathsDatabase.Contains("dir1/file.txt", isFolder: false).ShouldBeTrue();
        }

        private static void TestAddingPath(string path, bool isFolder = false)
        {
            TestAddingPath(path, path, isFolder);
        }

        private static void TestAddingPath(string pathToAdd, string pathInList, bool isFolder = false)
        {
            ModifiedPathsDatabase modifiedPathsDatabase = CreateModifiedPathsDatabase(initialContents: $"A {DefaultEntry}\r\n");
            bool isRetryable;
            modifiedPathsDatabase.TryAdd(pathToAdd, isFolder, out isRetryable);
            modifiedPathsDatabase.Count.ShouldEqual(2);
            modifiedPathsDatabase.Contains(pathInList, isFolder).ShouldBeTrue();
            modifiedPathsDatabase.Contains(ToGitPathSeparators(pathInList), isFolder).ShouldBeTrue();
            modifiedPathsDatabase.GetAllModifiedPaths().ShouldContainSingle(x => string.Compare(x, ToGitPathSeparators(pathInList), GVFSPlatform.Instance.Constants.PathComparison) == 0);
        }

        private static string ToGitPathSeparators(string path)
        {
            return path.Replace(Path.DirectorySeparatorChar, GVFSConstants.GitPathSeparator);
        }

        private static string ToPathSeparators(string path)
        {
            return path.Replace(GVFSConstants.GitPathSeparator, Path.DirectorySeparatorChar);
        }

        private static ModifiedPathsDatabase CreateModifiedPathsDatabase(string initialContents)
        {
            ConfigurableFileSystem configurableFileSystem = new ConfigurableFileSystem();
            configurableFileSystem.ExpectedFiles.Add(MockEntryFileName, new ReusableMemoryStream(initialContents));

            string error;
            ModifiedPathsDatabase modifiedPathsDatabase;
            ModifiedPathsDatabase.TryLoadOrCreate(null, MockEntryFileName, configurableFileSystem, out modifiedPathsDatabase, out error).ShouldBeTrue();
            modifiedPathsDatabase.ShouldNotBeNull();
            return modifiedPathsDatabase;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/NamedPipeStreamReaderWriterTests.cs
================================================
using GVFS.Common.NamedPipes;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using NUnit.Framework;
using System.IO;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class NamedPipeStreamReaderWriterTests
    {
        private MemoryStream stream;
        private NamedPipeStreamWriter streamWriter;
        private NamedPipeStreamReader streamReader;

        [SetUp]
        public void Setup()
        {
            this.stream = new MemoryStream();
            this.streamWriter = new NamedPipeStreamWriter(this.stream);
            this.streamReader = new NamedPipeStreamReader(this.stream);
        }

        [Test]
        public void CanWriteAndReadMessages()
        {
            string firstMessage = @"This is a new message";
            this.TestTransmitMessage(firstMessage);

            string secondMessage = @"This is another message";
            this.TestTransmitMessage(secondMessage);

            string thirdMessage = @"This is the third message in a series of messages";
            this.TestTransmitMessage(thirdMessage);

            string longMessage = new string('T', 1024 * 5);
            this.TestTransmitMessage(longMessage);
        }

        [Test]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ReadingPartialMessgeThrows()
        {
            byte[] bytes = System.Text.Encoding.ASCII.GetBytes("This is a partial message");

            this.stream.Write(bytes, 0, bytes.Length);
            this.stream.Seek(0, SeekOrigin.Begin);

            Assert.Throws(() => this.streamReader.ReadMessage());
        }

        [Test]
        public void CanSendMessagesWithNewLines()
        {
            string messageWithNewLines = "This is a \nstringwith\nnewlines";
            this.TestTransmitMessage(messageWithNewLines);
        }

        [Test]
        public void CanSendMultipleMessagesSequentially()
        {
            string[] messages = new string[]
            {
                "This is a new message",
                "This is another message",
                "This is the third message in a series of messages"
            };

            this.TestTransmitMessages(messages);
        }

        private void TestTransmitMessage(string message)
        {
            long pos = this.ReadStreamPosition();
            this.streamWriter.WriteMessage(message);

            this.SetStreamPosition(pos);

            string readMessage = this.streamReader.ReadMessage();
            readMessage.ShouldEqual(message, "The message read from the stream reader is not the same as the message that was sent.");
        }

        private void TestTransmitMessages(string[] messages)
        {
            long pos = this.ReadStreamPosition();

            foreach (string message in messages)
            {
                this.streamWriter.WriteMessage(message);
            }

            this.SetStreamPosition(pos);

            foreach (string message in messages)
            {
                string readMessage = this.streamReader.ReadMessage();
                readMessage.ShouldEqual(message, "The message read from the stream reader is not the same as the message that was sent.");
            }
        }

        private long ReadStreamPosition()
        {
            return this.stream.Position;
        }

        private void SetStreamPosition(long position)
        {
            this.stream.Seek(position, SeekOrigin.Begin);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/NamedPipeTests.cs
================================================
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using NUnit.Framework;
using System;
using static GVFS.Common.NamedPipes.NamedPipeMessages;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class NamedPipeTests
    {
        [TestCase]
        public void LockData_FromBody_Simple()
        {
            // Verify simple vanilla parsing
            LockData lockDataBefore = new LockData(1, true, true, "git status --serialize=D:\\Sources\\tqoscy2l.ud0_status.tmp --ignored=matching --untracked-files=complete", "123");
            LockData lockDataAfter = LockData.FromBody(lockDataBefore.ToMessage());
            lockDataAfter.PID.ShouldEqual(1);
            lockDataAfter.IsElevated.ShouldEqual(true);
            lockDataAfter.CheckAvailabilityOnly.ShouldEqual(true);
            lockDataAfter.ParsedCommand.ShouldEqual("git status --serialize=D:\\Sources\\tqoscy2l.ud0_status.tmp --ignored=matching --untracked-files=complete");
            lockDataAfter.GitCommandSessionId.ShouldEqual("123");
        }

        [TestCase]
        public void LockData_FromBody_WithDelimiters()
        {
            // Verify strings with "|" will work
            LockData lockDataWithPipeBefore = new LockData(1, true, true, "git commit -m 'message with a | and another |'", "123|321");
            LockData lockDataWithPipeAfter = LockData.FromBody(lockDataWithPipeBefore.ToMessage());
            lockDataWithPipeAfter.PID.ShouldEqual(1);
            lockDataWithPipeAfter.IsElevated.ShouldEqual(true);
            lockDataWithPipeAfter.CheckAvailabilityOnly.ShouldEqual(true);
            lockDataWithPipeAfter.ParsedCommand.ShouldEqual("git commit -m 'message with a | and another |'");
            lockDataWithPipeAfter.GitCommandSessionId.ShouldEqual("123|321");
        }

        [TestCase("1|true|true", "Invalid lock message. Expected at least 7 parts, got: 3 from message: '1|true|true'")]
        [TestCase("123|true|true|10|git status", "Invalid lock message. Expected at least 7 parts, got: 5 from message: '123|true|true|10|git status'")]
        [TestCase("blah|true|true|10|git status|9|sessionId", "Invalid lock message. Expected PID, got: blah from message: 'blah|true|true|10|git status|9|sessionId'")]
        [TestCase("1|true|1|10|git status|9|sessionId", "Invalid lock message. Expected bool for checkAvailabilityOnly, got: 1 from message: '1|true|1|10|git status|9|sessionId'")]
        [TestCase("1|1|true|10|git status|9|sessionId", "Invalid lock message. Expected bool for isElevated, got: 1 from message: '1|1|true|10|git status|9|sessionId'")]
        [TestCase("1|true|true|true|git status|9|sessionId", "Invalid lock message. Expected command length, got: true from message: '1|true|true|true|git status|9|sessionId'")]
        [TestCase("1|true|true|5|git status|9|sessionId", "Invalid lock message. Expected session id length, got: atus from message: '1|true|true|5|git status|9|sessionId'")]
        [TestCase("1|true|true|10|git status|bad|sessionId", "Invalid lock message. Expected session id length, got: bad from message: '1|true|true|10|git status|bad|sessionId'")]
        [TestCase("1|true|true|20|git status|9|sessionId", "Invalid lock message. Expected session id length, got: d from message: '1|true|true|20|git status|9|sessionId'")]
        [TestCase("1|true|true|10|git status|5|sessionId", "Invalid lock message. The sessionId is an unexpected length, got: 5 from message: '1|true|true|10|git status|5|sessionId'")]
        [TestCase("1|true|true|10|git status|20|sessionId", "Invalid lock message. The sessionId is an unexpected length, got: 20 from message: '1|true|true|10|git status|20|sessionId'")]
        [Category(CategoryConstants.ExceptionExpected)]
        public void LockData_FromBody_Exception(string body, string exceptionMessage)
        {
            InvalidOperationException exception = Assert.Throws(() => LockData.FromBody(body));
            exception.Message.ShouldEqual(exceptionMessage);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/OrgInfoApiClientTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using Moq;
using Moq.Protected;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class OrgInfoServerTests
    {
        public static List TestOrgInfo = new List()
        {
            new OrgInfo() { OrgName = "org1", Platform = "windows", Ring = "fast", Version = "1.2.3.1" },
            new OrgInfo() { OrgName = "org1", Platform = "windows", Ring = "slow", Version = "1.2.3.2" },
            new OrgInfo() { OrgName = "org1", Platform = "macOS", Ring = "fast", Version = "1.2.3.3" },
            new OrgInfo() { OrgName = "org1", Platform = "macOS", Ring = "slow", Version = "1.2.3.4" },
            new OrgInfo() { OrgName = "org2", Platform = "windows", Ring = "fast", Version = "1.2.3.5" },
            new OrgInfo() { OrgName = "org2", Platform = "windows", Ring = "slow", Version = "1.2.3.6" },
            new OrgInfo() { OrgName = "org2", Platform = "macOS", Ring = "fast", Version = "1.2.3.7" },
            new OrgInfo() { OrgName = "org2", Platform = "macOS", Ring = "slow", Version = "1.2.3.8" },
        };

        private string baseUrl = "https://www.contoso.com";

        private interface IHttpMessageHandlerProtectedMembers
        {
            Task  SendAsync(HttpRequestMessage message, CancellationToken token);
        }

        [TestCaseSource("TestOrgInfo")]
        public void QueryNewestVersionWithParams(OrgInfo orgInfo)
        {
            Mock handlerMock = new Mock(MockBehavior.Strict);

            handlerMock.Protected().As()
                .Setup(m => m.SendAsync(It.Is(request => this.UriMatches(request.RequestUri, this.baseUrl, orgInfo.OrgName, orgInfo.Platform, orgInfo.Ring)), It.IsAny()))
                .ReturnsAsync(new HttpResponseMessage()
                {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(this.ConstructResponseContent(orgInfo.Version))
                });

            HttpClient httpClient = new HttpClient(handlerMock.Object);

            OrgInfoApiClient upgradeChecker = new OrgInfoApiClient(httpClient, this.baseUrl);
            Version version = upgradeChecker.QueryNewestVersion(orgInfo.OrgName, orgInfo.Platform, orgInfo.Ring);

            version.ShouldEqual(new Version(orgInfo.Version));

            handlerMock.VerifyAll();
        }

        private bool UriMatches(Uri uri, string baseUrl, string expectedOrgName, string expectedPlatform, string expectedRing)
        {
            bool hostMatches = uri.Host.Equals(baseUrl);

            Dictionary queryParams = new Dictionary(StringComparer.OrdinalIgnoreCase);
            foreach (string param in uri.Query.Substring(1).Split('&'))
            {
                string[] fields = param.Split('=');
                string key = fields[0];
                string value = fields[1];

                queryParams.Add(key, value);
            }

            if (queryParams.Count != 3)
            {
                return false;
            }

            if (!queryParams.TryGetValue("Organization", out string orgName) || !string.Equals(orgName, expectedOrgName, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            if (!queryParams.TryGetValue("platform", out string platform) || !string.Equals(platform, expectedPlatform, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            if (!queryParams.TryGetValue("ring", out string ring) || !string.Equals(ring, expectedRing, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            return true;
        }

        private string ConstructResponseContent(string version)
        {
            return $"{{\"version\" : \"{version}\"}} ";
        }

        public class OrgInfo
        {
            public string OrgName { get; set; }
            public string Ring { get; set; }
            public string Platform { get; set; }
            public string Version { get; set; }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/PathsTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.Runtime.InteropServices;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class PathsTests
    {
        [TestCase]
        public void CanConvertOSPathToGitFormat()
        {
            string systemPath;
            string expectedGitPath;

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                systemPath = @"C:\This\is\a\path";
                expectedGitPath = @"C:/This/is/a/path";
            }
            else
            {
                systemPath = @"/This/is/a/path";
                expectedGitPath = systemPath;
            }

            string actualTransformedPath = Paths.ConvertPathToGitFormat(systemPath);
            actualTransformedPath.ShouldEqual(expectedGitPath);

            string doubleTransformedPath = Paths.ConvertPathToGitFormat(actualTransformedPath);
            doubleTransformedPath.ShouldEqual(expectedGitPath);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/PhysicalFileSystemDeleteTests.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class PhysicalFileSystemDeleteTests
    {
        [TestCase]
        public void TryDeleteFileDeletesFile()
        {
            string path = "mock:\\file.txt";
            DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) });

            fileSystem.TryDeleteFile(path);
            fileSystem.ExistingFiles.ContainsKey(path).ShouldBeFalse("DeleteUtils failed to delete file");
        }

        [TestCase]
        public void TryDeleteFileSetsAttributesToNormalBeforeDeletingFile()
        {
            string path = "mock:\\file.txt";
            DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(
                new[] { new KeyValuePair(path, FileAttributes.ReadOnly) },
                allFilesExist: false,
                noOpDelete: true);

            fileSystem.TryDeleteFile(path);
            fileSystem.ExistingFiles.ContainsKey(path).ShouldBeTrue("DeleteTestsFileSystem is configured as no-op delete, file should still be present");
            fileSystem.ExistingFiles[path].ShouldEqual(FileAttributes.Normal, "TryDeleteFile should set attributes to Normal before deleting");
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void TryDeleteFileReturnsTrueWhenSetAttributesFailsToFindFile()
        {
            string path = "mock:\\file.txt";
            DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(
                Enumerable.Empty>(),
                allFilesExist: true,
                noOpDelete: false);

            fileSystem.TryDeleteFile(path).ShouldEqual(true, "TryDeleteFile should return true when SetAttributes throws FileNotFoundException");
        }

        [TestCase]
        public void TryDeleteFileReturnsNullExceptionOnSuccess()
        {
            string path = "mock:\\file.txt";
            DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) });

            Exception e = new Exception();
            fileSystem.TryDeleteFile(path, out e);
            fileSystem.ExistingFiles.ContainsKey(path).ShouldBeFalse("DeleteUtils failed to delete file");
            e.ShouldBeNull("Exception should be null when TryDeleteFile succeeds");
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void TryDeleteFileReturnsThrownException()
        {
            string path = "mock:\\file.txt";
            Exception deleteException = new IOException();
            DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) });
            fileSystem.DeleteException = deleteException;

            Exception e;
            fileSystem.TryDeleteFile(path, out e).ShouldBeFalse("TryDeleteFile should fail on IOException");
            ReferenceEquals(e, deleteException).ShouldBeTrue("TryDeleteFile should return the thrown exception");

            deleteException = new UnauthorizedAccessException();
            fileSystem.DeleteException = deleteException;
            fileSystem.TryDeleteFile(path, out e).ShouldBeFalse("TryDeleteFile should fail on UnauthorizedAccessException");
            ReferenceEquals(e, deleteException).ShouldBeTrue("TryDeleteFile should return the thrown exception");
        }

        [TestCase]
        public void TryDeleteFileDoesNotUpdateMetadataOnSuccess()
        {
            string path = "mock:\\file.txt";
            DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) });

            EventMetadata metadata = new EventMetadata();
            fileSystem.TryDeleteFile(path, "metadataKey", metadata).ShouldBeTrue("TryDeleteFile should succeed");
            metadata.ShouldBeEmpty("TryDeleteFile should not update metadata on success");
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void TryDeleteFileUpdatesMetadataOnFailure()
        {
            string path = "mock:\\file.txt";
            DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) });
            fileSystem.DeleteException = new IOException();

            EventMetadata metadata = new EventMetadata();
            fileSystem.TryDeleteFile(path, "testKey", metadata).ShouldBeFalse("TryDeleteFile should fail when IOException is thrown");
            metadata.ContainsKey("testKey_DeleteFailed").ShouldBeTrue();
            metadata["testKey_DeleteFailed"].ShouldEqual("true");
            metadata.ContainsKey("testKey_DeleteException").ShouldBeTrue();
            metadata["testKey_DeleteException"].ShouldBeOfType().ShouldContain("IOException");
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void TryWaitForDeleteSucceedsAfterFailures()
        {
            string path = "mock:\\file.txt";
            DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) });
            fileSystem.DeleteException = new IOException();

            fileSystem.MaxDeleteFileExceptions = 5;
            fileSystem.TryWaitForDelete(null, path, retryDelayMs: 0, maxRetries: 10, retryLoggingThreshold: 1).ShouldBeTrue();
            fileSystem.DeleteFileCallCount.ShouldEqual(fileSystem.MaxDeleteFileExceptions + 1);

            fileSystem.ExistingFiles.Add(path, FileAttributes.ReadOnly);
            fileSystem.DeleteFileCallCount = 0;
            fileSystem.MaxDeleteFileExceptions = 9;
            fileSystem.TryWaitForDelete(null, path, retryDelayMs: 0, maxRetries: 10, retryLoggingThreshold: 1).ShouldBeTrue();
            fileSystem.DeleteFileCallCount.ShouldEqual(fileSystem.MaxDeleteFileExceptions + 1);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void TryWaitForDeleteFailsAfterMaxRetries()
        {
            string path = "mock:\\file.txt";
            DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) });
            fileSystem.DeleteException = new IOException();

            int maxRetries = 10;
            fileSystem.TryWaitForDelete(null, path, retryDelayMs: 0, maxRetries: maxRetries, retryLoggingThreshold: 1).ShouldBeFalse();
            fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1);

            fileSystem.DeleteFileCallCount = 0;
            fileSystem.TryWaitForDelete(null, path, retryDelayMs: 1, maxRetries: maxRetries, retryLoggingThreshold: 1).ShouldBeFalse();
            fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1);

            fileSystem.DeleteFileCallCount = 0;
            fileSystem.TryWaitForDelete(null, path, retryDelayMs: 1, maxRetries: 0, retryLoggingThreshold: 1).ShouldBeFalse();
            fileSystem.DeleteFileCallCount.ShouldEqual(1);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void TryWaitForDeleteAlwaysLogsFirstAndLastFailure()
        {
            string path = "mock:\\file.txt";
            DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) });
            fileSystem.DeleteException = new IOException();

            MockTracer mockTracer = new MockTracer();
            int maxRetries = 10;
            fileSystem.TryWaitForDelete(mockTracer, path, retryDelayMs: 0, maxRetries: maxRetries, retryLoggingThreshold: 1000).ShouldBeFalse();
            fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1);

            mockTracer.RelatedWarningEvents.Count.ShouldEqual(2, "There should be two warning events, the first and last");
            mockTracer.RelatedWarningEvents[0].ShouldContain(
                new[]
                {
                    "Failed to delete file, retrying ...",
                    "\"failureCount\":1",
                    "IOException"
                });
            mockTracer.RelatedWarningEvents[1].ShouldContain(
                new[]
                {
                    "Failed to delete file.",
                    "\"failureCount\":11",
                    "IOException"
                });
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void TryWaitForDeleteLogsAtSpecifiedInterval()
        {
            string path = "mock:\\file.txt";
            DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) });
            fileSystem.DeleteException = new IOException();

            MockTracer mockTracer = new MockTracer();
            int maxRetries = 10;
            fileSystem.TryWaitForDelete(mockTracer, path, retryDelayMs: 0, maxRetries: maxRetries, retryLoggingThreshold: 3).ShouldBeFalse();
            fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1);

            mockTracer.RelatedWarningEvents.Count.ShouldEqual(5, "There should be five warning events, the first and last, and the 4th, 7th, and 10th");
            mockTracer.RelatedWarningEvents[0].ShouldContain(
                new[]
                {
                    "Failed to delete file, retrying ...",
                    "\"failureCount\":1",
                    "IOException"
                });

            mockTracer.RelatedWarningEvents[1].ShouldContain(
                new[]
                {
                    "Failed to delete file, retrying ...",
                    "\"failureCount\":4",
                    "IOException"
                });
            mockTracer.RelatedWarningEvents[2].ShouldContain(
                new[]
                {
                    "Failed to delete file, retrying ...",
                    "\"failureCount\":7",
                    "IOException"
                });
            mockTracer.RelatedWarningEvents[3].ShouldContain(
                new[]
                {
                    "Failed to delete file, retrying ...",
                    "\"failureCount\":10",
                    "IOException"
                });
            mockTracer.RelatedWarningEvents[4].ShouldContain(
                new[]
                {
                    "Failed to delete file.",
                    "\"failureCount\":11",
                    "IOException"
                });
        }

        private class DeleteTestsFileSystem : PhysicalFileSystem
        {
            private bool allFilesExist;
            private bool noOpDelete;

            public DeleteTestsFileSystem(
                IEnumerable> existingFiles,
                bool allFilesExist = false,
                bool noOpDelete = false)
            {
                this.ExistingFiles = new Dictionary(GVFSPlatform.Instance.Constants.PathComparer);
                foreach (KeyValuePair kvp in existingFiles)
                {
                    this.ExistingFiles[kvp.Key] = kvp.Value;
                }

                this.allFilesExist = allFilesExist;
                this.noOpDelete = noOpDelete;
                this.DeleteFileCallCount = 0;
                this.MaxDeleteFileExceptions = -1;
            }

            public Dictionary ExistingFiles { get; private set; }
            public Exception DeleteException { get; set; }
            public int MaxDeleteFileExceptions { get; set; }
            public int DeleteFileCallCount { get; set; }

            public override bool FileExists(string path)
            {
                if (this.allFilesExist)
                {
                    return true;
                }

                return this.ExistingFiles.ContainsKey(path);
            }

            public override void SetAttributes(string path, FileAttributes fileAttributes)
            {
                if (this.ExistingFiles.ContainsKey(path))
                {
                    this.ExistingFiles[path] = fileAttributes;
                }
                else
                {
                    throw new FileNotFoundException();
                }
            }

            public override void DeleteFile(string path)
            {
                this.DeleteFileCallCount++;

                if (!this.noOpDelete)
                {
                    if (this.DeleteException != null &&
                        (this.MaxDeleteFileExceptions == -1 || this.MaxDeleteFileExceptions >= this.DeleteFileCallCount))
                    {
                        throw this.DeleteException;
                    }

                    if (this.ExistingFiles.ContainsKey(path))
                    {
                        if ((this.ExistingFiles[path] & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
                        {
                            throw new UnauthorizedAccessException();
                        }
                        else
                        {
                            this.ExistingFiles.Remove(path);
                        }
                    }
                }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/RefLogEntryTests.cs
================================================
using GVFS.Common.Git;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class RefLogEntryTests
    {
        [TestCase]
        public void ParsesValidRefLog()
        {
            const string SourceSha = "0000000000000000000000000000000000000000";
            const string TargetSha = "d249e0fea84484eb105d52174cf326958ee87ab4";
            const string Reason = "clone: from https://repourl";
            string testLine = string.Format("{0} {1} author  1478738341 -0800\t{2}", SourceSha, TargetSha, Reason);

            RefLogEntry output;
            RefLogEntry.TryParse(testLine, out output).ShouldEqual(true);

            output.ShouldNotBeNull();
            output.SourceSha.ShouldEqual(SourceSha);
            output.TargetSha.ShouldEqual(TargetSha);
            output.Reason.ShouldEqual(Reason);
        }

        [TestCase]
        public void FailsForMissingReason()
        {
            const string SourceSha = "0000000000000000000000000000000000000000";
            const string TargetSha = "d249e0fea84484eb105d52174cf326958ee87ab4";
            string testLine = string.Format("{0} {1} author  1478738341 -0800", SourceSha, TargetSha);

            RefLogEntry output;
            RefLogEntry.TryParse(testLine, out output).ShouldEqual(false);

            output.ShouldBeNull();
        }

        [TestCase]
        public void FailsForMissingTargetSha()
        {
            const string SourceSha = "0000000000000000000000000000000000000000";
            string testLine = string.Format("{0} ", SourceSha);

            RefLogEntry output;
            RefLogEntry.TryParse(testLine, out output).ShouldEqual(false);

            output.ShouldBeNull();
        }

        [TestCase]
        public void FailsForNull()
        {
            string testLine = null;

            RefLogEntry output;
            RefLogEntry.TryParse(testLine, out output).ShouldEqual(false);

            output.ShouldBeNull();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/RetryBackoffTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Threading;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class RetryBackoffTests
    {
        [TestCase]
        public void CalculateBackoffReturnsZeroForFirstAttempt()
        {
            int failedAttempt = 1;
            int maxBackoff = 300;

            RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff).ShouldEqual(0);
        }

        [TestCase]
        public void CalculateBackoff()
        {
            int failedAttempt = 2;
            int maxBackoff = 300;

            double backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff);
            this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase);

            backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1);
            this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1);

            ++failedAttempt;
            backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff);
            this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase);

            backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1);
            this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1);
        }

        [TestCase]
        public void CalculateBackoffThatWouldExceedMaxBackoff()
        {
            int failedAttempt = 30;
            int maxBackoff = 300;
            double backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff);
            this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase);
        }

        [TestCase]
        public void CalculateBackoffAcrossMultipleThreads()
        {
            int failedAttempt = 2;
            int maxBackoff = 300;
            int numThreads = 10;

            Thread[] calcThreads = new Thread[numThreads];

            for (int i = 0; i < numThreads; i++)
            {
                calcThreads[i] = new Thread(
                    () =>
                    {
                        double backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff);
                        this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase);
                    });

                calcThreads[i].Start();
            }

            for (int i = 0; i < calcThreads.Length; i++)
            {
                calcThreads[i].Join();
            }
        }

        private void ValidateBackoff(double backoff, int failedAttempt, double maxBackoff, double exponentialBackoffBase)
        {
            backoff.ShouldBeAtLeast(Math.Min(Math.Pow(exponentialBackoffBase, failedAttempt), maxBackoff) * .9);
            backoff.ShouldBeAtMost(Math.Min(Math.Pow(exponentialBackoffBase, failedAttempt), maxBackoff) * 1.1);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/RetryConfigTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;
using System;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class RetryConfigTests
    {
        private const string ReadConfigFailureMessage = "Failed to read config";
        [TestCase]
        public void TryLoadConfigFailsWhenGitFailsToReadConfig()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = new MockGitProcess();
            gitProcess.SetExpectedCommandResult("config gvfs.max-retries", () => new GitProcess.Result(string.Empty, ReadConfigFailureMessage, GitProcess.Result.GenericFailureCode));
            gitProcess.SetExpectedCommandResult("config gvfs.timeout-seconds", () => new GitProcess.Result(string.Empty, ReadConfigFailureMessage, GitProcess.Result.GenericFailureCode));

            RetryConfig config;
            string error;
            RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(false);
            error.ShouldContain(ReadConfigFailureMessage);
        }

        [TestCase]
        public void TryLoadConfigUsesDefaultValuesWhenEntriesNotInConfig()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = new MockGitProcess();
            gitProcess.SetExpectedCommandResult("config gvfs.max-retries", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode));
            gitProcess.SetExpectedCommandResult("config gvfs.timeout-seconds", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode));

            RetryConfig config;
            string error;
            RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(true);
            error.ShouldEqual(string.Empty);
            config.MaxRetries.ShouldEqual(RetryConfig.DefaultMaxRetries);
            config.MaxAttempts.ShouldEqual(config.MaxRetries + 1);
            config.Timeout.ShouldEqual(TimeSpan.FromSeconds(RetryConfig.DefaultTimeoutSeconds));
        }

        [TestCase]
        public void TryLoadConfigUsesDefaultValuesWhenEntriesAreBlank()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = new MockGitProcess();
            gitProcess.SetExpectedCommandResult("config gvfs.max-retries", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));
            gitProcess.SetExpectedCommandResult("config gvfs.timeout-seconds", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));

            RetryConfig config;
            string error;
            RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(true);
            error.ShouldEqual(string.Empty);
            config.MaxRetries.ShouldEqual(RetryConfig.DefaultMaxRetries);
            config.MaxAttempts.ShouldEqual(config.MaxRetries + 1);
            config.Timeout.ShouldEqual(TimeSpan.FromSeconds(RetryConfig.DefaultTimeoutSeconds));
        }

        [TestCase]
        public void TryLoadConfigEnforcesMinimumValuesOnMaxRetries()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = new MockGitProcess();
            gitProcess.SetExpectedCommandResult("config gvfs.max-retries", () => new GitProcess.Result("-1", string.Empty, GitProcess.Result.SuccessCode));
            gitProcess.SetExpectedCommandResult("config gvfs.timeout-seconds", () => new GitProcess.Result("30", string.Empty, GitProcess.Result.SuccessCode));

            RetryConfig config;
            string error;
            RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(false);
            error.ShouldContain("Invalid value -1 for setting gvfs.max-retries, value must be greater than or equal to 0");
        }

        [TestCase]
        public void TryLoadConfigEnforcesMinimumValuesOnTimeout()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = new MockGitProcess();
            gitProcess.SetExpectedCommandResult("config gvfs.max-retries", () => new GitProcess.Result("3", string.Empty, GitProcess.Result.SuccessCode));
            gitProcess.SetExpectedCommandResult("config gvfs.timeout-seconds", () => new GitProcess.Result("-1", string.Empty, GitProcess.Result.SuccessCode));

            RetryConfig config;
            string error;
            RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(false);
            error.ShouldContain("Invalid value -1 for setting gvfs.timeout-seconds, value must be greater than or equal to 0");
        }

        [TestCase]
        public void TryLoadConfigUsesConfiguredValues()
        {
            int maxRetries = RetryConfig.DefaultMaxRetries + 1;
            int timeoutSeconds = RetryConfig.DefaultTimeoutSeconds + 1;

            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = new MockGitProcess();
            gitProcess.SetExpectedCommandResult("config gvfs.max-retries", () => new GitProcess.Result(maxRetries.ToString(), string.Empty, GitProcess.Result.SuccessCode));
            gitProcess.SetExpectedCommandResult("config gvfs.timeout-seconds", () => new GitProcess.Result(timeoutSeconds.ToString(), string.Empty, GitProcess.Result.SuccessCode));

            RetryConfig config;
            string error;
            RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(true);
            error.ShouldEqual(string.Empty);
            config.MaxRetries.ShouldEqual(maxRetries);
            config.MaxAttempts.ShouldEqual(config.MaxRetries + 1);
            config.Timeout.ShouldEqual(TimeSpan.FromSeconds(timeoutSeconds));
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/RetryWrapperTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using NUnit.Framework;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class RetryWrapperTests
    {
        [SetUp]
        public void SetUp()
        {
            RetryCircuitBreaker.Reset();
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void WillRetryOnIOException()
        {
            const int ExpectedTries = 5;

            RetryWrapper dut = new RetryWrapper(ExpectedTries, CancellationToken.None, exponentialBackoffBase: 0);

            int actualTries = 0;
            RetryWrapper.InvocationResult output = dut.Invoke(
                tryCount =>
                {
                    actualTries++;
                    throw new IOException();
                });

            output.Succeeded.ShouldEqual(false);
            actualTries.ShouldEqual(ExpectedTries);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void WillNotRetryForGenericExceptions()
        {
            const int MaxTries = 5;

            RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0);

            Assert.Throws(
                () =>
                {
                    RetryWrapper.InvocationResult output = dut.Invoke(tryCount => { throw new Exception(); });
                });
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void WillNotMakeAnyAttemptWhenInitiallyCanceled()
        {
            const int MaxTries = 5;
            int actualTries = 0;

            RetryWrapper dut = new RetryWrapper(MaxTries, new CancellationToken(canceled: true), exponentialBackoffBase: 0);

            Assert.Throws(
                () =>
                {
                    RetryWrapper.InvocationResult output = dut.Invoke(tryCount =>
                    {
                        ++actualTries;
                        return new RetryWrapper.CallbackResult(true);
                    });
                });

            actualTries.ShouldEqual(0);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void WillNotRetryForWhenCanceledDuringAttempts()
        {
            const int MaxTries = 5;
            int actualTries = 0;
            int expectedTries = 3;

            using (CancellationTokenSource tokenSource = new CancellationTokenSource())
            {
                RetryWrapper dut = new RetryWrapper(MaxTries, tokenSource.Token, exponentialBackoffBase: 0);

                Assert.Throws(
                    () =>
                    {
                        RetryWrapper.InvocationResult output = dut.Invoke(tryCount =>
                        {
                            ++actualTries;

                            if (actualTries == expectedTries)
                            {
                                tokenSource.Cancel();
                            }

                            return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: true);
                        });
                    });

                actualTries.ShouldEqual(expectedTries);
            }
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void WillNotRetryWhenCancelledDuringBackoff()
        {
            const int MaxTries = 5;
            int actualTries = 0;
            int expectedTries = 2; // 2 because RetryWrapper does not wait after the first failure

            using (CancellationTokenSource tokenSource = new CancellationTokenSource())
            {
                RetryWrapper dut = new RetryWrapper(MaxTries, tokenSource.Token, exponentialBackoffBase: 300);

                Task.Run(() =>
                {
                    // Wait 3 seconds and cancel
                    Thread.Sleep(1000 * 3);
                    tokenSource.Cancel();
                });

                Assert.Throws(
                    () =>
                    {
                        RetryWrapper.InvocationResult output = dut.Invoke(tryCount =>
                        {
                            ++actualTries;
                            return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: true);
                        });
                    });

                actualTries.ShouldEqual(expectedTries);
            }
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void OnFailureIsCalledWhenEventHandlerAttached()
        {
            const int MaxTries = 5;
            const int ExpectedFailures = 5;

            RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0);

            int actualFailures = 0;
            dut.OnFailure += errorArgs => actualFailures++;

            RetryWrapper.InvocationResult output = dut.Invoke(
                tryCount =>
                {
                    throw new IOException();
                });

            output.Succeeded.ShouldEqual(false);
            actualFailures.ShouldEqual(ExpectedFailures);
        }

        [TestCase]
        public void OnSuccessIsOnlyCalledOnce()
        {
            const int MaxTries = 5;
            const int ExpectedFailures = 0;
            const int ExpectedTries = 1;

            RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0);

            int actualFailures = 0;
            dut.OnFailure += errorArgs => actualFailures++;

            int actualTries = 0;
            RetryWrapper.InvocationResult output = dut.Invoke(
                tryCount =>
                {
                    actualTries++;
                    return new RetryWrapper.CallbackResult(true);
                });

            output.Succeeded.ShouldEqual(true);
            output.Result.ShouldEqual(true);
            actualTries.ShouldEqual(ExpectedTries);
            actualFailures.ShouldEqual(ExpectedFailures);
        }

        [TestCase]
        public void WillNotRetryWhenNotRequested()
        {
            const int MaxTries = 5;
            const int ExpectedFailures = 1;
            const int ExpectedTries = 1;

            RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0);

            int actualFailures = 0;
            dut.OnFailure += errorArgs => actualFailures++;

            int actualTries = 0;
            RetryWrapper.InvocationResult output = dut.Invoke(
                tryCount =>
                {
                    actualTries++;
                    return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: false);
                });

            output.Succeeded.ShouldEqual(false);
            output.Result.ShouldEqual(false);
            actualTries.ShouldEqual(ExpectedTries);
            actualFailures.ShouldEqual(ExpectedFailures);
        }

        [TestCase]
        public void WillRetryWhenRequested()
        {
            const int MaxTries = 5;
            const int ExpectedFailures = 5;
            const int ExpectedTries = 5;

            RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0);

            int actualFailures = 0;
            dut.OnFailure += errorArgs => actualFailures++;

            int actualTries = 0;
            RetryWrapper.InvocationResult output = dut.Invoke(
                tryCount =>
                {
                    actualTries++;
                    return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: true);
                });

            output.Succeeded.ShouldEqual(false);
            output.Result.ShouldEqual(false);
            actualTries.ShouldEqual(ExpectedTries);
            actualFailures.ShouldEqual(ExpectedFailures);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void CircuitBreakerOpensAfterConsecutiveFailures()
        {
            const int Threshold = 5;
            const int CooldownMs = 5000;
            RetryCircuitBreaker.Configure(Threshold, CooldownMs);

            // Generate enough failures to trip the circuit breaker
            for (int i = 0; i < Threshold; i++)
            {
                RetryWrapper wrapper = new RetryWrapper(1, CancellationToken.None, exponentialBackoffBase: 0);
                wrapper.Invoke(tryCount => throw new IOException("simulated failure"));
            }

            RetryCircuitBreaker.IsOpen.ShouldBeTrue("Circuit breaker should be open after threshold failures");

            // Next invocation should fail fast without calling the callback
            int callbackInvocations = 0;
            RetryWrapper dut = new RetryWrapper(5, CancellationToken.None, exponentialBackoffBase: 0);
            RetryWrapper.InvocationResult result = dut.Invoke(
                tryCount =>
                {
                    callbackInvocations++;
                    return new RetryWrapper.CallbackResult(true);
                });

            result.Succeeded.ShouldEqual(false);
            callbackInvocations.ShouldEqual(0);
        }

        [TestCase]
        public void CircuitBreakerResetsOnSuccess()
        {
            const int Threshold = 3;
            RetryCircuitBreaker.Configure(Threshold, 30_000);

            // Record failures just below threshold
            for (int i = 0; i < Threshold - 1; i++)
            {
                RetryCircuitBreaker.RecordFailure();
            }

            RetryCircuitBreaker.IsOpen.ShouldBeFalse("Circuit should still be closed below threshold");

            // A successful invocation resets the counter
            RetryWrapper dut = new RetryWrapper(1, CancellationToken.None, exponentialBackoffBase: 0);
            dut.Invoke(tryCount => new RetryWrapper.CallbackResult(true));

            RetryCircuitBreaker.ConsecutiveFailures.ShouldEqual(0);

            // Now threshold more failures are needed to trip it again
            for (int i = 0; i < Threshold - 1; i++)
            {
                RetryCircuitBreaker.RecordFailure();
            }

            RetryCircuitBreaker.IsOpen.ShouldBeFalse("Circuit should still be closed after reset");
        }

        [TestCase]
        public void CircuitBreakerIgnoresNonRetryableErrors()
        {
            const int Threshold = 3;
            RetryCircuitBreaker.Configure(Threshold, 30_000);

            // Generate non-retryable failures (e.g., 404/400) — these should NOT count
            for (int i = 0; i < Threshold + 5; i++)
            {
                RetryWrapper wrapper = new RetryWrapper(1, CancellationToken.None, exponentialBackoffBase: 0);
                wrapper.Invoke(tryCount => new RetryWrapper.CallbackResult(new Exception("404 Not Found"), shouldRetry: false));
            }

            RetryCircuitBreaker.IsOpen.ShouldBeFalse("Non-retryable errors should not trip the circuit breaker");
            RetryCircuitBreaker.ConsecutiveFailures.ShouldEqual(0);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void CircuitBreakerClosesAfterCooldown()
        {
            const int Threshold = 3;
            const int CooldownMs = 100; // Very short cooldown for testing
            RetryCircuitBreaker.Configure(Threshold, CooldownMs);

            // Trip the circuit breaker
            for (int i = 0; i < Threshold; i++)
            {
                RetryWrapper wrapper = new RetryWrapper(1, CancellationToken.None, exponentialBackoffBase: 0);
                wrapper.Invoke(tryCount => throw new IOException("simulated failure"));
            }

            RetryCircuitBreaker.IsOpen.ShouldBeTrue("Circuit should be open");

            // Wait for cooldown to expire
            Thread.Sleep(CooldownMs + 50);

            RetryCircuitBreaker.IsOpen.ShouldBeFalse("Circuit should be closed after cooldown");

            // Should be able to invoke successfully now
            int callbackInvocations = 0;
            RetryWrapper dut = new RetryWrapper(1, CancellationToken.None, exponentialBackoffBase: 0);
            RetryWrapper.InvocationResult result = dut.Invoke(
                tryCount =>
                {
                    callbackInvocations++;
                    return new RetryWrapper.CallbackResult(true);
                });

            result.Succeeded.ShouldEqual(true);
            callbackInvocations.ShouldEqual(1);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/SHA1UtilTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.Text;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class SHA1UtilTests
    {
        private const string TestString = "c:\\Repos\\GVFS\\src\\.gittattributes";
        private const string TestResultSha1 = "ced5ad9680c1a05e9100680c2b3432de23bb7d6d";
        private const string TestResultHex = "633a5c5265706f735c475646535c7372635c2e6769747461747472696275746573";

        [TestCase]
        public void SHA1HashStringForUTF8String()
        {
            SHA1Util.SHA1HashStringForUTF8String(TestString).ShouldEqual(TestResultSha1);
        }

        [TestCase]
        public void HexStringFromBytes()
        {
            byte[] bytes = Encoding.UTF8.GetBytes(TestString);
            SHA1Util.HexStringFromBytes(bytes).ShouldEqual(TestResultHex);
        }

        [TestCase]
        public void IsValidFullSHAIsFalseForEmptyString()
        {
            SHA1Util.IsValidShaFormat(string.Empty).ShouldEqual(false);
        }

        [TestCase]
        public void IsValidFullSHAIsFalseForHexStringsNot40Chars()
        {
            SHA1Util.IsValidShaFormat("1").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("9").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("A").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("a").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("f").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("f").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("1234567890abcdefABCDEF").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("12345678901234567890123456789012345678901").ShouldEqual(false);
        }

        [TestCase]
        public void IsValidFullSHAFalseForNonHexStrings()
        {
            SHA1Util.IsValidShaFormat("@").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("g").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("G").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("~").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("_").ShouldEqual(false);
            SHA1Util.IsValidShaFormat(".").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("1234567890abcdefABCDEF.tmp").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("G1234567890abcdefABCDEF.tmp").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("_G1234567890abcdefABCDEF.tmp").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("@234567890123456789012345678901234567890").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("g234567890123456789012345678901234567890").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("G234567890123456789012345678901234567890").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("~234567890123456789012345678901234567890").ShouldEqual(false);
            SHA1Util.IsValidShaFormat("_234567890123456789012345678901234567890").ShouldEqual(false);
            SHA1Util.IsValidShaFormat(".234567890123456789012345678901234567890").ShouldEqual(false);
        }

        [TestCase]
        public void IsValidFullSHATrueForLength40HexStrings()
        {
            SHA1Util.IsValidShaFormat("1234567890123456789012345678901234567890").ShouldEqual(true);
            SHA1Util.IsValidShaFormat("abcdef7890123456789012345678901234567890").ShouldEqual(true);
            SHA1Util.IsValidShaFormat("ABCDEF7890123456789012345678901234567890").ShouldEqual(true);
            SHA1Util.IsValidShaFormat("1234567890123456789012345678901234ABCDEF").ShouldEqual(true);
            SHA1Util.IsValidShaFormat("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").ShouldEqual(true);
            SHA1Util.IsValidShaFormat("ffffffffffffffffffffffffffffffffffffffff").ShouldEqual(true);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/WorktreeCommandParserTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class WorktreeCommandParserTests
    {
        [TestCase]
        public void GetSubcommandReturnsAdd()
        {
            string[] args = { "post-command", "worktree", "add", "-b", "branch", @"C:\wt" };
            WorktreeCommandParser.GetSubcommand(args).ShouldEqual("add");
        }

        [TestCase]
        public void GetSubcommandReturnsRemove()
        {
            string[] args = { "pre-command", "worktree", "remove", @"C:\wt" };
            WorktreeCommandParser.GetSubcommand(args).ShouldEqual("remove");
        }

        [TestCase]
        public void GetSubcommandSkipsLeadingDoubleHyphenArgs()
        {
            string[] args = { "post-command", "worktree", "--git-pid=1234", "add", @"C:\wt" };
            WorktreeCommandParser.GetSubcommand(args).ShouldEqual("add");
        }

        [TestCase]
        public void GetSubcommandReturnsNullWhenNoSubcommand()
        {
            string[] args = { "post-command", "worktree" };
            WorktreeCommandParser.GetSubcommand(args).ShouldBeNull();
        }

        [TestCase]
        public void GetSubcommandNormalizesToLowercase()
        {
            string[] args = { "post-command", "worktree", "Add" };
            WorktreeCommandParser.GetSubcommand(args).ShouldEqual("add");
        }

        [TestCase]
        public void GetPathArgExtractsPathFromAddWithBranch()
        {
            // git worktree add -b branch C:\worktree
            string[] args = { "post-command", "worktree", "add", "-b", "my-branch", @"C:\repos\wt", "--git-pid=123", "--exit_code=0" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\wt");
        }

        [TestCase]
        public void GetPathArgExtractsPathFromAddWithoutBranch()
        {
            // git worktree add C:\worktree
            string[] args = { "post-command", "worktree", "add", @"C:\repos\wt", "--git-pid=123", "--exit_code=0" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\wt");
        }

        [TestCase]
        public void GetPathArgExtractsPathFromRemove()
        {
            string[] args = { "pre-command", "worktree", "remove", @"C:\repos\wt", "--git-pid=456" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\wt");
        }

        [TestCase]
        public void GetPathArgExtractsPathFromRemoveWithForce()
        {
            string[] args = { "pre-command", "worktree", "remove", "--force", @"C:\repos\wt" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\wt");
        }

        [TestCase]
        public void GetPathArgSkipsBranchNameAfterDashB()
        {
            // -b takes a value — the path is the arg AFTER the branch name
            string[] args = { "post-command", "worktree", "add", "-b", "feature", @"C:\repos\feature" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\feature");
        }

        [TestCase]
        public void GetPathArgSkipsBranchNameAfterDashCapitalB()
        {
            string[] args = { "post-command", "worktree", "add", "-B", "feature", @"C:\repos\feature" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\feature");
        }

        [TestCase]
        public void GetPathArgSkipsAllOptionFlags()
        {
            // -f, -d, -q, --detach, --checkout, --lock, --no-checkout
            string[] args = { "post-command", "worktree", "add", "-f", "--no-checkout", "--lock", "--reason", "testing", @"C:\repos\wt" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\wt");
        }

        [TestCase]
        public void GetPathArgHandlesSeparator()
        {
            // After --, everything is positional
            string[] args = { "post-command", "worktree", "add", "--", @"C:\repos\wt" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\wt");
        }

        [TestCase]
        public void GetPathArgSkipsGitPidAndExitCode()
        {
            string[] args = { "post-command", "worktree", "add", @"C:\wt", "--git-pid=99", "--exit_code=0" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\wt");
        }

        [TestCase]
        public void GetPathArgReturnsNullWhenNoPath()
        {
            string[] args = { "post-command", "worktree", "list" };
            WorktreeCommandParser.GetPathArg(args).ShouldBeNull();
        }

        [TestCase]
        public void GetPositionalArgReturnsSecondPositional()
        {
            // git worktree move  
            string[] args = { "post-command", "worktree", "move", @"C:\old", @"C:\new" };
            WorktreeCommandParser.GetPositionalArg(args, 0).ShouldEqual(@"C:\old");
            WorktreeCommandParser.GetPositionalArg(args, 1).ShouldEqual(@"C:\new");
        }

        [TestCase]
        public void GetPositionalArgReturnsNullForOutOfRangeIndex()
        {
            string[] args = { "post-command", "worktree", "remove", @"C:\wt" };
            WorktreeCommandParser.GetPositionalArg(args, 1).ShouldBeNull();
        }

        [TestCase]
        public void GetPathArgHandlesShortArgs()
        {
            // Ensure single-char flags without values are skipped
            string[] args = { "post-command", "worktree", "add", "-f", "-q", @"C:\repos\wt" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\wt");
        }

        [TestCase]
        public void GetPathArgHandlesCombinedShortFlags()
        {
            // -fd = --force --detach combined into one arg
            string[] args = { "post-command", "worktree", "add", "-fd", @"C:\repos\wt", "HEAD" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\wt");
        }

        [TestCase]
        public void GetPathArgHandlesCombinedFlagWithBranch()
        {
            // -fb = --force + -b, next arg is the branch name
            string[] args = { "post-command", "worktree", "add", "-fb", "my-branch", @"C:\repos\wt" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\wt");
        }

        [TestCase]
        public void GetPathArgHandlesBranchValueBakedIn()
        {
            // -bfd = -b with value "fd" baked in, no next-arg consumption
            string[] args = { "post-command", "worktree", "add", "-bfd", @"C:\repos\wt" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\wt");
        }

        [TestCase]
        public void GetPathArgHandlesTwoValueOptionsFirstConsumes()
        {
            // -Bb = -B with value "b" baked in, no next-arg consumption
            string[] args = { "post-command", "worktree", "add", "-Bb", @"C:\repos\wt" };
            WorktreeCommandParser.GetPathArg(args).ShouldEqual(@"C:\repos\wt");
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/WorktreeEnlistmentTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.IO;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class WorktreeEnlistmentTests
    {
        private string testRoot;
        private string primaryRoot;
        private string sharedGitDir;
        private string worktreePath;
        private string worktreeGitDir;

        [SetUp]
        public void SetUp()
        {
            this.testRoot = Path.Combine(Path.GetTempPath(), "GVFSWTEnlTests_" + Path.GetRandomFileName());
            this.primaryRoot = Path.Combine(this.testRoot, "enlistment");
            string primarySrc = Path.Combine(this.primaryRoot, "src");
            this.sharedGitDir = Path.Combine(primarySrc, ".git");
            this.worktreePath = Path.Combine(this.testRoot, "agent-wt-1");
            this.worktreeGitDir = Path.Combine(this.sharedGitDir, "worktrees", "agent-wt-1");

            Directory.CreateDirectory(this.sharedGitDir);
            Directory.CreateDirectory(this.worktreeGitDir);
            Directory.CreateDirectory(this.worktreePath);
            Directory.CreateDirectory(Path.Combine(this.primaryRoot, ".gvfs"));

            File.WriteAllText(
                Path.Combine(this.sharedGitDir, "config"),
                "[core]\n\trepositoryformatversion = 0\n[remote \"origin\"]\n\turl = https://mock/repo\n");
            File.WriteAllText(
                Path.Combine(this.sharedGitDir, "HEAD"),
                "ref: refs/heads/main\n");
            File.WriteAllText(
                Path.Combine(this.worktreePath, ".git"),
                "gitdir: " + this.worktreeGitDir);
            File.WriteAllText(
                Path.Combine(this.worktreeGitDir, "commondir"),
                "../..");
            File.WriteAllText(
                Path.Combine(this.worktreeGitDir, "HEAD"),
                "ref: refs/heads/agent-wt-1\n");
            File.WriteAllText(
                Path.Combine(this.worktreeGitDir, "gitdir"),
                Path.Combine(this.worktreePath, ".git"));
        }

        [TearDown]
        public void TearDown()
        {
            if (Directory.Exists(this.testRoot))
            {
                Directory.Delete(this.testRoot, recursive: true);
            }
        }

        private GVFSEnlistment CreateWorktreeEnlistment()
        {
            string gitBinPath = GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath()
                ?? @"C:\Program Files\Git\cmd\git.exe";
            return GVFSEnlistment.CreateForWorktree(
                this.primaryRoot, gitBinPath, authentication: null,
                GVFSEnlistment.TryGetWorktreeInfo(this.worktreePath),
                repoUrl: "https://mock/repo");
        }

        [TestCase]
        public void IsWorktreeReturnsTrueForWorktreeEnlistment()
        {
            GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
            enlistment.IsWorktree.ShouldBeTrue();
        }

        [TestCase]
        public void WorktreeInfoIsPopulated()
        {
            GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
            enlistment.Worktree.ShouldNotBeNull();
            enlistment.Worktree.Name.ShouldEqual("agent-wt-1");
            enlistment.Worktree.WorktreePath.ShouldEqual(this.worktreePath);
        }

        [TestCase]
        public void DotGitRootPointsToSharedGitDir()
        {
            GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
            enlistment.DotGitRoot.ShouldEqual(this.sharedGitDir);
        }

        [TestCase]
        public void WorkingDirectoryRootIsWorktreePath()
        {
            GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
            enlistment.WorkingDirectoryRoot.ShouldEqual(this.worktreePath);
        }

        [TestCase]
        public void LocalObjectsRootIsSharedGitObjects()
        {
            GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
            enlistment.LocalObjectsRoot.ShouldEqual(
                Path.Combine(this.sharedGitDir, "objects"));
        }

        [TestCase]
        public void LocalObjectsRootDoesNotDoubleGitPath()
        {
            GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
            Assert.IsFalse(
                enlistment.LocalObjectsRoot.Contains(Path.Combine(".git", ".git")),
                "LocalObjectsRoot should not have doubled .git path");
        }

        [TestCase]
        public void GitIndexPathUsesWorktreeGitDir()
        {
            GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
            enlistment.GitIndexPath.ShouldEqual(
                Path.Combine(this.worktreeGitDir, "index"));
        }

        [TestCase]
        public void NamedPipeNameIncludesWorktreeSuffix()
        {
            GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
            Assert.IsTrue(
                enlistment.NamedPipeName.Contains("_WT_AGENT-WT-1"),
                "NamedPipeName should contain worktree suffix");
        }

        [TestCase]
        public void DotGVFSRootIsInWorktreeGitDir()
        {
            GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
            Assert.IsTrue(
                enlistment.DotGVFSRoot.Contains(this.worktreeGitDir),
                "DotGVFSRoot should be inside worktree git dir");
        }

        [TestCase]
        public void EnlistmentRootIsPrimaryRoot()
        {
            GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
            enlistment.EnlistmentRoot.ShouldEqual(this.primaryRoot);
        }

        [TestCase]
        public void RepoUrlIsReadFromSharedConfig()
        {
            GVFSEnlistment enlistment = this.CreateWorktreeEnlistment();
            enlistment.RepoUrl.ShouldEqual("https://mock/repo");
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/WorktreeInfoTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using NUnit.Framework;
using System.IO;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class WorktreeInfoTests
    {
        private string testRoot;

        [SetUp]
        public void SetUp()
        {
            this.testRoot = Path.Combine(Path.GetTempPath(), "GVFSWorktreeTests_" + Path.GetRandomFileName());
            Directory.CreateDirectory(this.testRoot);
        }

        [TearDown]
        public void TearDown()
        {
            if (Directory.Exists(this.testRoot))
            {
                Directory.Delete(this.testRoot, recursive: true);
            }
        }

        [TestCase]
        public void ReturnsNullForNonWorktreeDirectory()
        {
            // A directory without a .git file is not a worktree
            GVFSEnlistment.WorktreeInfo info = GVFSEnlistment.TryGetWorktreeInfo(this.testRoot);
            info.ShouldBeNull();
        }

        [TestCase]
        public void ReturnsNullWhenDotGitIsDirectory()
        {
            // A .git directory (not file) means primary enlistment, not a worktree
            Directory.CreateDirectory(Path.Combine(this.testRoot, ".git"));
            GVFSEnlistment.WorktreeInfo info = GVFSEnlistment.TryGetWorktreeInfo(this.testRoot);
            info.ShouldBeNull();
        }

        [TestCase]
        public void ReturnsNullWhenDotGitFileHasNoGitdirPrefix()
        {
            File.WriteAllText(Path.Combine(this.testRoot, ".git"), "not a gitdir line");
            GVFSEnlistment.WorktreeInfo info = GVFSEnlistment.TryGetWorktreeInfo(this.testRoot);
            info.ShouldBeNull();
        }

        [TestCase]
        public void DetectsWorktreeFromAbsoluteGitdir()
        {
            // Simulate a worktree: .git file pointing to .git/worktrees/
            string primaryGitDir = Path.Combine(this.testRoot, "primary", ".git");
            string worktreeGitDir = Path.Combine(primaryGitDir, "worktrees", "agent-1");
            Directory.CreateDirectory(worktreeGitDir);

            // Create commondir file pointing back to shared .git
            File.WriteAllText(Path.Combine(worktreeGitDir, "commondir"), "../..");

            // Create the worktree directory with a .git file
            string worktreeDir = Path.Combine(this.testRoot, "wt");
            Directory.CreateDirectory(worktreeDir);
            File.WriteAllText(Path.Combine(worktreeDir, ".git"), "gitdir: " + worktreeGitDir);

            GVFSEnlistment.WorktreeInfo info = GVFSEnlistment.TryGetWorktreeInfo(worktreeDir);
            info.ShouldNotBeNull();
            info.Name.ShouldEqual("agent-1");
            info.WorktreePath.ShouldEqual(worktreeDir);
            info.WorktreeGitDir.ShouldEqual(worktreeGitDir);
            info.SharedGitDir.ShouldEqual(primaryGitDir);
            info.PipeSuffix.ShouldEqual("_WT_AGENT-1");
        }

        [TestCase]
        public void DetectsWorktreeFromRelativeGitdir()
        {
            // Simulate worktree with relative gitdir path
            string primaryGitDir = Path.Combine(this.testRoot, "primary", ".git");
            string worktreeGitDir = Path.Combine(primaryGitDir, "worktrees", "feature-branch");
            Directory.CreateDirectory(worktreeGitDir);

            File.WriteAllText(Path.Combine(worktreeGitDir, "commondir"), "../..");

            // Worktree as sibling of primary
            string worktreeDir = Path.Combine(this.testRoot, "feature-branch");
            Directory.CreateDirectory(worktreeDir);

            // Use a relative path: ../primary/.git/worktrees/feature-branch
            string relativePath = "../primary/.git/worktrees/feature-branch";
            File.WriteAllText(Path.Combine(worktreeDir, ".git"), "gitdir: " + relativePath);

            GVFSEnlistment.WorktreeInfo info = GVFSEnlistment.TryGetWorktreeInfo(worktreeDir);
            info.ShouldNotBeNull();
            info.Name.ShouldEqual("feature-branch");
            info.PipeSuffix.ShouldEqual("_WT_FEATURE-BRANCH");
        }

        [TestCase]
        public void ReturnsNullWithoutCommondirFile()
        {
            // Worktree git dir without a commondir file is invalid
            string worktreeGitDir = Path.Combine(this.testRoot, "primary", ".git", "worktrees", "no-common");
            Directory.CreateDirectory(worktreeGitDir);

            string worktreeDir = Path.Combine(this.testRoot, "no-common");
            Directory.CreateDirectory(worktreeDir);
            File.WriteAllText(Path.Combine(worktreeDir, ".git"), "gitdir: " + worktreeGitDir);

            GVFSEnlistment.WorktreeInfo info = GVFSEnlistment.TryGetWorktreeInfo(worktreeDir);
            info.ShouldBeNull();
        }

        [TestCase]
        public void PipeSuffixReturnsNullForNonWorktree()
        {
            string suffix = GVFSEnlistment.GetWorktreePipeSuffix(this.testRoot);
            suffix.ShouldBeNull();
        }

        [TestCase]
        public void PipeSuffixReturnsCorrectValueForWorktree()
        {
            string worktreeGitDir = Path.Combine(this.testRoot, "primary", ".git", "worktrees", "my-wt");
            Directory.CreateDirectory(worktreeGitDir);
            File.WriteAllText(Path.Combine(worktreeGitDir, "commondir"), "../..");

            string worktreeDir = Path.Combine(this.testRoot, "my-wt");
            Directory.CreateDirectory(worktreeDir);
            File.WriteAllText(Path.Combine(worktreeDir, ".git"), "gitdir: " + worktreeGitDir);

            string suffix = GVFSEnlistment.GetWorktreePipeSuffix(worktreeDir);
            suffix.ShouldEqual("_WT_MY-WT");
        }

        [TestCase]
        public void ReturnsNullForNonexistentDirectory()
        {
            string nonexistent = Path.Combine(this.testRoot, "does-not-exist");
            GVFSEnlistment.WorktreeInfo info = GVFSEnlistment.TryGetWorktreeInfo(nonexistent);
            info.ShouldBeNull();
        }

        [TestCase]
        public void DetectsWorktreeFromSubdirectory()
        {
            // Set up a worktree at testRoot/wt-sub with .git file
            string primaryGitDir = Path.Combine(this.testRoot, "primary", ".git");
            string worktreeGitDir = Path.Combine(primaryGitDir, "worktrees", "wt-sub");
            Directory.CreateDirectory(worktreeGitDir);
            File.WriteAllText(Path.Combine(worktreeGitDir, "commondir"), "../..");

            string worktreeDir = Path.Combine(this.testRoot, "wt-sub");
            Directory.CreateDirectory(worktreeDir);
            File.WriteAllText(Path.Combine(worktreeDir, ".git"), "gitdir: " + worktreeGitDir);

            // Create a subdirectory inside the worktree
            string subDir = Path.Combine(worktreeDir, "a", "b", "c");
            Directory.CreateDirectory(subDir);

            // TryGetWorktreeInfo should walk up and find the worktree root
            GVFSEnlistment.WorktreeInfo info = GVFSEnlistment.TryGetWorktreeInfo(subDir);
            info.ShouldNotBeNull();
            info.Name.ShouldEqual("wt-sub");
            info.WorktreePath.ShouldEqual(worktreeDir);
        }

        [TestCase]
        public void ReturnsNullForPrimaryFromSubdirectory()
        {
            // Set up a primary repo with a real .git directory
            string primaryDir = Path.Combine(this.testRoot, "primary-repo");
            Directory.CreateDirectory(Path.Combine(primaryDir, ".git"));

            // Walking up from a subdirectory should find the .git dir and return null
            string subDir = Path.Combine(primaryDir, "src", "folder");
            Directory.CreateDirectory(subDir);

            GVFSEnlistment.WorktreeInfo info = GVFSEnlistment.TryGetWorktreeInfo(subDir);
            info.ShouldBeNull();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Common/WorktreeNestedPathTests.cs
================================================
using GVFS.Common;
using NUnit.Framework;
using System.IO;

namespace GVFS.UnitTests.Common
{
    [TestFixture]
    public class WorktreeNestedPathTests
    {
        // Basic containment
        [TestCase(@"C:\repo\src\subfolder",      @"C:\repo\src",    true,  Description = "Child path is inside directory")]
        [TestCase(@"C:\repo\src",                 @"C:\repo\src",    true,  Description = "Equal path is inside directory")]
        [TestCase(@"C:\repo\src\a\b\c\d",         @"C:\repo\src",    true,  Description = "Deeply nested path is inside")]
        [TestCase(@"C:\repo\src.worktrees\wt1",   @"C:\repo\src",    false, Description = "Path with prefix overlap is outside")]
        [TestCase(@"C:\repo\src2",                @"C:\repo\src",    false, Description = "Sibling path is outside")]

        // Path traversal normalization
        [TestCase(@"C:\repo\src\..\..\..\evil",   @"C:\repo\src",    false, Description = "Traversal escaping directory is outside")]
        [TestCase(@"C:\repo\src\..",              @"C:\repo\src",    false, Description = "Traversal to parent is outside")]
        [TestCase(@"C:\repo\src\..\other",        @"C:\repo\src",    false, Description = "Traversal to sibling is outside")]
        [TestCase(@"C:\repo\src\sub\..\other",    @"C:\repo\src",    true,  Description = "Traversal staying inside directory")]
        [TestCase(@"C:\repo\src\.\subfolder",     @"C:\repo\src",    true,  Description = "Dot segment resolves to same path")]
        [TestCase(@"C:\repo\src\subfolder",       @"C:\repo\.\src",  true,  Description = "Dot segment in directory")]

        // Trailing separators
        [TestCase(@"C:\repo\src\subfolder",       @"C:\repo\src\",   true,  Description = "Trailing slash on directory")]
        [TestCase(@"C:\repo\src\subfolder\",      @"C:\repo\src",    true,  Description = "Trailing slash on path")]

        // Case sensitivity
        [TestCase(@"C:\Repo\SRC\subfolder",       @"C:\repo\src",    true,  Description = "Case-insensitive child path")]
        [TestCase(@"C:\REPO\SRC",                 @"C:\repo\src",    true,  Description = "Case-insensitive equal path")]
        [TestCase(@"c:\repo\src\subfolder",       @"C:\REPO\SRC",    true,  Description = "Lower drive letter vs upper")]
        [TestCase(@"C:\Repo\Src2",                @"C:\repo\src",    false, Description = "Case-insensitive sibling is outside")]

        // Mixed forward and backward slashes
        [TestCase(@"C:\repo\src/subfolder",       @"C:\repo\src",    true,  Description = "Forward slash in child path")]
        [TestCase("C:/repo/src/subfolder",        @"C:\repo\src",    true,  Description = "All forward slashes in path")]
        [TestCase(@"C:\repo\src\subfolder",       "C:/repo/src",     true,  Description = "All forward slashes in directory")]
        [TestCase("C:/repo/src",                  "C:/repo/src",     true,  Description = "Both paths with forward slashes")]
        [TestCase("C:/repo/src/../other",         @"C:\repo\src",    false, Description = "Forward slashes with traversal")]
        public void IsPathInsideDirectory(string path, string directory, bool expected)
        {
            Assert.AreEqual(expected, GVFSEnlistment.IsPathInsideDirectory(path, directory));
        }

        private string testDir;

        [SetUp]
        public void SetUp()
        {
            this.testDir = Path.Combine(Path.GetTempPath(), "WorktreeNestedPathTests_" + Path.GetRandomFileName());
            Directory.CreateDirectory(this.testDir);
        }

        [TearDown]
        public void TearDown()
        {
            if (Directory.Exists(this.testDir))
            {
                Directory.Delete(this.testDir, recursive: true);
            }
        }

        [TestCase]
        public void GetKnownWorktreePathsReturnsEmptyWhenNoWorktreesDir()
        {
            string[] paths = GVFSEnlistment.GetKnownWorktreePaths(this.testDir);
            Assert.AreEqual(0, paths.Length);
        }

        [TestCase]
        public void GetKnownWorktreePathsReturnsEmptyWhenWorktreesDirIsEmpty()
        {
            Directory.CreateDirectory(Path.Combine(this.testDir, "worktrees"));

            string[] paths = GVFSEnlistment.GetKnownWorktreePaths(this.testDir);
            Assert.AreEqual(0, paths.Length);
        }

        [TestCase]
        public void GetKnownWorktreePathsReadsGitdirFiles()
        {
            string wt1Dir = Path.Combine(this.testDir, "worktrees", "wt1");
            string wt2Dir = Path.Combine(this.testDir, "worktrees", "wt2");
            Directory.CreateDirectory(wt1Dir);
            Directory.CreateDirectory(wt2Dir);

            File.WriteAllText(Path.Combine(wt1Dir, "gitdir"), @"C:\worktrees\wt1\.git" + "\n");
            File.WriteAllText(Path.Combine(wt2Dir, "gitdir"), @"C:\worktrees\wt2\.git" + "\n");

            string[] paths = GVFSEnlistment.GetKnownWorktreePaths(this.testDir);
            Assert.AreEqual(2, paths.Length);
            Assert.That(paths, Has.Member(@"C:\worktrees\wt1"));
            Assert.That(paths, Has.Member(@"C:\worktrees\wt2"));
        }

        [TestCase]
        public void GetKnownWorktreePathsSkipsEntriesWithoutGitdirFile()
        {
            string wt1Dir = Path.Combine(this.testDir, "worktrees", "wt1");
            string wt2Dir = Path.Combine(this.testDir, "worktrees", "wt2");
            Directory.CreateDirectory(wt1Dir);
            Directory.CreateDirectory(wt2Dir);

            File.WriteAllText(Path.Combine(wt1Dir, "gitdir"), @"C:\worktrees\wt1\.git" + "\n");
            // wt2 has no gitdir file

            string[] paths = GVFSEnlistment.GetKnownWorktreePaths(this.testDir);
            Assert.AreEqual(1, paths.Length);
            Assert.AreEqual(@"C:\worktrees\wt1", paths[0]);
        }

        [TestCase]
        public void GetKnownWorktreePathsNormalizesForwardSlashes()
        {
            string wtDir = Path.Combine(this.testDir, "worktrees", "wt1");
            Directory.CreateDirectory(wtDir);

            File.WriteAllText(Path.Combine(wtDir, "gitdir"), "C:/worktrees/wt1/.git\n");

            string[] paths = GVFSEnlistment.GetKnownWorktreePaths(this.testDir);
            Assert.AreEqual(1, paths.Length);
            Assert.AreEqual(@"C:\worktrees\wt1", paths[0]);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Data/backward.txt
================================================
:040000 000000 34df85f635cb0e04c6d4ecedefe2707951616059 0000000000000000000000000000000000000000 D	New folder
:100644 000000 953905e07b8e3fb9f8c9e5b8bd0e6cd1a2b3fbae 0000000000000000000000000000000000000000 D	New folder/newFile.txt
:000000 040000 0000000000000000000000000000000000000000 2c877e829ad58c051e5f1b30391e41341d4ff56c A	deepfolderdelete
:000000 040000 0000000000000000000000000000000000000000 cd3eb0505948ac13b8b550891be81fdf4def0dae A	deepfolderdelete/subfolder
:000000 100644 0000000000000000000000000000000000000000 e02f6e8a454811f5a613743d3e9133afd545e36b A	deepfolderdelete/subfolder/necessary.txt
:000000 100644 0000000000000000000000000000000000000000 1e2db0ae91731f7862918bb50b2af43050719cbd A	fileToBecomeFolder
:040000 000000 f826e58b21f78935976a9779b27773e55e3b6429 0000000000000000000000000000000000000000 D	fileToBecomeFolder
:100644 000000 39d05dc47b628f461ef92a4531db129c5d7ea4fb 0000000000000000000000000000000000000000 D	fileToBecomeFolder/newdoc.txt
:000000 100644 0000000000000000000000000000000000000000 05c2b79a2c84782364b0f27244454ea29187bde6 A	fileToDelete.txt
:100644 100644 c97bc99f8894348d4a8e4b8146fbac3384ac8cac 3afc1ef879df949927c85b4c849a65e39aa472c7 M	fileToEdit.txt
:000000 100644 0000000000000000000000000000000000000000 0fb01bbb41482aec747112b58f1a11cf41bb7a11 A	fileToRename.txt
:000000 100644 0000000000000000000000000000000000000000 ff236b434e083db032f4ac0f452303f4eeb8f4be A	fileToRenameEdit.txt
:100644 000000 382d0750aef7b7e47b08f12befb7274311c4a850 0000000000000000000000000000000000000000 D	fileWasRename.txt
:100644 000000 0fb01bbb41482aec747112b58f1a11cf41bb7a11 0000000000000000000000000000000000000000 D	fileWasRenamed.txt
:100644 000000 c419470b3e2ee3b43d9a13edb180a453b06c0fb5 0000000000000000000000000000000000000000 D	folderToBeFile
:000000 040000 0000000000000000000000000000000000000000 184fd663a53e325add265346ddd024b5a8829301 A	folderToBeFile
:000000 100644 0000000000000000000000000000000000000000 f51ba4dac00d291e491906a99698c01f802e652d A	folderToBeFile/existence.txt
:000000 040000 0000000000000000000000000000000000000000 a5731b081f875263a428c126a3242587fdaea6c9 A	folderToDelete
:000000 100644 0000000000000000000000000000000000000000 8724404da33605d27cfa5490d412f0dfea16a277 A	folderToDelete/fileToEdit2.txt
:040000 040000 403c21d5d12f6e6a6659460b5cec7e4b66b6c0f2 9a894ca28a2a8d813f2bae305cd94faf366ad5e0 M	folderToEdit
:100644 100644 45f57f3292952208ad72232bc2a64038a3d0987f 59b502537b91543cb8aa7e6a26ee235197fa154b M	folderToEdit/fileToEdit2.txt
:000000 100644 bbbb404da33605d27cfa5490d412f0dfea16eeee 0000000000000000000000000000000000000000 D	folderToEdit/fileToDelete.txt
:000000 100644 cccc404da33605d27cfa5490d412f0dfea16ffff 0000000000000000000000000000000000000000 D	folderToEdit/fileToDelete.txt.bak
:000000 040000 0000000000000000000000000000000000000000 e30358a677e3a55aa838d0bbe0207f61ce40f401 A	folderToRename
:000000 100644 0000000000000000000000000000000000000000 35151eac3bb089589836bfece4dfbb84fae502de A	folderToRename/existence.txt
:040000 000000 e30358a677e3a55aa838d0bbe0207f61ce40f401 0000000000000000000000000000000000000000 D	folderWasRenamed
:100644 000000 35151eac3bb089589836bfece4dfbb84fae502de 0000000000000000000000000000000000000000 D	folderWasRenamed/existence.txt
:100644 000000 9ff3c67e2792e4c17672c6b04a8a6efe732cb07a 0000000000000000000000000000000000000000 D	newFile.txt


================================================
FILE: GVFS/GVFS.UnitTests/Data/caseChange.txt
================================================
:040000 000000 d813c8227132c3bf73c013f8913f207b4876b2bf 0000000000000000000000000000000000000000 D	GVFLT_MultiThreadTest
:040000 000000 1260ecb71f2be8eb92ea904c6dffa3e40eaaf1bf 0000000000000000000000000000000000000000 D	GVFLT_MultiThreadTest/OpenForReadsSameTime
:100644 000000 eabe8d5ec569cc7e199e77411ad935f101414032 0000000000000000000000000000000000000000 D	GVFLT_MultiThreadTest/OpenForReadsSameTime/test
:040000 000000 1260ecb71f2be8eb92ea904c6dffa3e40eaaf1bf 0000000000000000000000000000000000000000 D	GVFLT_MultiThreadTest/OpenForWritesSameTime
:100644 000000 eabe8d5ec569cc7e199e77411ad935f101414032 0000000000000000000000000000000000000000 D	GVFLT_MultiThreadTest/OpenForWritesSameTime/test
:000000 040000 0000000000000000000000000000000000000000 d813c8227132c3bf73c013f8913f207b4876b2bf A	GVFlt_MultiThreadTest
:000000 040000 0000000000000000000000000000000000000000 1260ecb71f2be8eb92ea904c6dffa3e40eaaf1bf A	GVFlt_MultiThreadTest/OpenForReadsSameTime
:000000 100644 0000000000000000000000000000000000000000 eabe8d5ec569cc7e199e77411ad935f101414032 A	GVFlt_MultiThreadTest/OpenForReadsSameTime/test
:000000 040000 0000000000000000000000000000000000000000 1260ecb71f2be8eb92ea904c6dffa3e40eaaf1bf A	GVFlt_MultiThreadTest/OpenForWritesSameTime
:000000 100644 0000000000000000000000000000000000000000 eabe8d5ec569cc7e199e77411ad935f101414032 A	GVFlt_MultiThreadTest/OpenForWritesSameTime/test


================================================
FILE: GVFS/GVFS.UnitTests/Data/forward.txt
================================================
:000000 040000 0000000000000000000000000000000000000000 34df85f635cb0e04c6d4ecedefe2707951616059 A	New folder
:000000 100644 0000000000000000000000000000000000000000 953905e07b8e3fb9f8c9e5b8bd0e6cd1a2b3fbae A	New folder/newFile.txt
:040000 000000 2c877e829ad58c051e5f1b30391e41341d4ff56c 0000000000000000000000000000000000000000 D	deepfolderdelete
:040000 000000 cd3eb0505948ac13b8b550891be81fdf4def0dae 0000000000000000000000000000000000000000 D	deepfolderdelete/subfolder
:100644 000000 e02f6e8a454811f5a613743d3e9133afd545e36b 0000000000000000000000000000000000000000 D	deepfolderdelete/subfolder/necessary.txt
:100644 000000 1e2db0ae91731f7862918bb50b2af43050719cbd 0000000000000000000000000000000000000000 D	fileToBecomeFolder
:000000 040000 0000000000000000000000000000000000000000 f826e58b21f78935976a9779b27773e55e3b6429 A	fileToBecomeFolder
:000000 100644 0000000000000000000000000000000000000000 39d05dc47b628f461ef92a4531db129c5d7ea4fb A	fileToBecomeFolder/newdoc.txt
:100644 000000 05c2b79a2c84782364b0f27244454ea29187bde6 0000000000000000000000000000000000000000 D	fileToDelete.txt
:100644 100644 3afc1ef879df949927c85b4c849a65e39aa472c7 c97bc99f8894348d4a8e4b8146fbac3384ac8cac M	fileToEdit.txt
:100644 000000 0fb01bbb41482aec747112b58f1a11cf41bb7a11 0000000000000000000000000000000000000000 D	fileToRename.txt
:100644 000000 ff236b434e083db032f4ac0f452303f4eeb8f4be 0000000000000000000000000000000000000000 D	fileToRenameEdit.txt
:000000 100644 0000000000000000000000000000000000000000 382d0750aef7b7e47b08f12befb7274311c4a850 A	fileWasRename.txt
:000000 100644 0000000000000000000000000000000000000000 0fb01bbb41482aec747112b58f1a11cf41bb7a11 A	fileWasRenamed.txt
:000000 100644 0000000000000000000000000000000000000000 c419470b3e2ee3b43d9a13edb180a453b06c0fb5 A	folderToBeFile
:040000 000000 184fd663a53e325add265346ddd024b5a8829301 0000000000000000000000000000000000000000 D	folderToBeFile
:100644 000000 f51ba4dac00d291e491906a99698c01f802e652d 0000000000000000000000000000000000000000 D	folderToBeFile/existence.txt
:040000 000000 a5731b081f875263a428c126a3242587fdaea6c9 0000000000000000000000000000000000000000 D	folderToDelete
:100644 000000 8724404da33605d27cfa5490d412f0dfea16a277 0000000000000000000000000000000000000000 D	folderToDelete/fileToEdit2.txt
:040000 040000 9a894ca28a2a8d813f2bae305cd94faf366ad5e0 403c21d5d12f6e6a6659460b5cec7e4b66b6c0f2 M	folderToEdit
:100644 100644 59b502537b91543cb8aa7e6a26ee235197fa154b 45f57f3292952208ad72232bc2a64038a3d0987f M	folderToEdit/fileToEdit2.txt
:040000 000000 e30358a677e3a55aa838d0bbe0207f61ce40f401 0000000000000000000000000000000000000000 D	folderToRename
:100644 000000 35151eac3bb089589836bfece4dfbb84fae502de 0000000000000000000000000000000000000000 D	folderToRename/existence.txt
:000000 040000 0000000000000000000000000000000000000000 e30358a677e3a55aa838d0bbe0207f61ce40f401 A	folderWasRenamed
:000000 100644 0000000000000000000000000000000000000000 35151eac3bb089589836bfece4dfbb84fae502de A	folderWasRenamed/existence.txt
:000000 100644 0000000000000000000000000000000000000000 9ff3c67e2792e4c17672c6b04a8a6efe732cb07a A	newFile.txt
:000000 120000 0000000000000000000000000000000000000000 3bd509d373734a9f9685d6a73ba73324f72931e3 A	symLinkToBeCreated.txt

================================================
FILE: GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj
================================================


  
    net471
    Exe
    true
  

  
    
    
    
    
    
  

  
    
    
    
  


    
        
    

  
  
    
    
      ProjectedFSLib.dll
      PreserveNewest
    
  

  
    
      Hooks\UnstageCommandParser.cs
    
  

  
    
      Always
    
    
      Always
    
    
      Always
    
    
      Always
    
  




================================================
FILE: GVFS/GVFS.UnitTests/Git/GVFSGitObjectsTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Http;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Reflection;
using System.Threading;

namespace GVFS.UnitTests.Git
{
    [TestFixture]
    public class GVFSGitObjectsTests
    {
        private const string ValidTestObjectFileSha1 = "c1f2535cd983afa20de0d64fcaaba06ce535aa30";
        private const string TestEnlistmentRoot = "mock:\\src";
        private const string TestLocalCacheRoot = "mock:\\.gvfs";
        private const string TestObjectRoot = "mock:\\.gvfs\\gitObjectCache";
        private readonly byte[] validTestObjectFileContents = new byte[]
        {
            0x78, 0x01, 0x4B, 0xCA, 0xC9, 0x4F, 0x52, 0x30, 0x62,
            0x48, 0xE4, 0x02, 0x00, 0x0E, 0x64, 0x02, 0x5D
        };

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void CatchesFileNotFoundAfterFileDeleted()
        {
            MockFileSystemWithCallbacks fileSystem = new MockFileSystemWithCallbacks();
            fileSystem.OnFileExists = (path) => true;
            fileSystem.OnOpenFileStream = (path, fileMode, fileAccess) =>
            {
                if (fileAccess == FileAccess.Write)
                {
                    return new MemoryStream();
                }

                throw new FileNotFoundException();
            };

            MockHttpGitObjects httpObjects = new MockHttpGitObjects();
            using (httpObjects.InputStream = new MemoryStream(this.validTestObjectFileContents))
            {
                httpObjects.MediaType = GVFSConstants.MediaTypes.LooseObjectMediaType;
                GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects, fileSystem);

                dut.TryCopyBlobContentStream(
                    ValidTestObjectFileSha1,
                    new CancellationToken(),
                    GVFSGitObjects.RequestSource.FileStreamCallback,
                    (stream, length) => Assert.Fail("Should not be able to call copy stream callback"))
                    .ShouldEqual(false);
            }
        }

        [TestCase]
        public void SucceedsForNormalLookingLooseObjectDownloads()
        {
            MockFileSystemWithCallbacks fileSystem = new Mock.FileSystem.MockFileSystemWithCallbacks();
            fileSystem.OnFileExists = (path) => true;
            fileSystem.OnOpenFileStream = (path, mode, access) => new MemoryStream();
            MockHttpGitObjects httpObjects = new MockHttpGitObjects();
            using (httpObjects.InputStream = new MemoryStream(this.validTestObjectFileContents))
            {
                httpObjects.MediaType = GVFSConstants.MediaTypes.LooseObjectMediaType;
                GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects, fileSystem);

                dut.TryDownloadAndSaveObject(ValidTestObjectFileSha1, GVFSGitObjects.RequestSource.FileStreamCallback)
                    .ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Success);
            }
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void FailsZeroByteLooseObjectsDownloads()
        {
            this.AssertRetryableExceptionOnDownload(
                new MemoryStream(),
                GVFSConstants.MediaTypes.LooseObjectMediaType,
                gitObjects => gitObjects.TryDownloadAndSaveObject(ValidTestObjectFileSha1, GVFSGitObjects.RequestSource.FileStreamCallback));
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void FailsNullByteLooseObjectsDownloads()
        {
            this.AssertRetryableExceptionOnDownload(
                new MemoryStream(new byte[256]),
                GVFSConstants.MediaTypes.LooseObjectMediaType,
                gitObjects => gitObjects.TryDownloadAndSaveObject("b376885ac8452b6cbf9ced81b1080bfd570d9b91", GVFSGitObjects.RequestSource.FileStreamCallback));
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void FailsZeroBytePackDownloads()
        {
            this.AssertRetryableExceptionOnDownload(
                new MemoryStream(),
                GVFSConstants.MediaTypes.PackFileMediaType,
                gitObjects => gitObjects.TryDownloadCommit("object0"));
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void FailsNullBytePackDownloads()
        {
            this.AssertRetryableExceptionOnDownload(
                new MemoryStream(new byte[256]),
                GVFSConstants.MediaTypes.PackFileMediaType,
                gitObjects => gitObjects.TryDownloadCommit("object0"));
        }

        [TestCase]
        public void CoalescesMultipleConcurrentRequestsForSameObject()
        {
            ManualResetEventSlim downloadStarted = new ManualResetEventSlim(false);
            ManualResetEventSlim downloadGate = new ManualResetEventSlim(false);
            int downloadCount = 0;

            CoalescingTestHttpGitObjects httpObjects = new CoalescingTestHttpGitObjects(
                this.validTestObjectFileContents,
                onDownloadStarting: () =>
                {
                    Interlocked.Increment(ref downloadCount);
                    downloadStarted.Set();
                    downloadGate.Wait();
                });

            MockFileSystemWithCallbacks fileSystem = new MockFileSystemWithCallbacks();
            fileSystem.OnFileExists = (path) => false;
            fileSystem.OnMoveFile = (source, target) => { };
            fileSystem.OnOpenFileStream = (path, mode, access) =>
            {
                if (access == FileAccess.Read)
                {
                    return new MemoryStream(this.validTestObjectFileContents);
                }

                return new MemoryStream();
            };

            GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects, fileSystem);

            const int threadCount = 10;
            GitObjects.DownloadAndSaveObjectResult[] results = new GitObjects.DownloadAndSaveObjectResult[threadCount];
            Thread[] threads = new Thread[threadCount];
            CountdownEvent allReady = new CountdownEvent(threadCount);
            ManualResetEventSlim go = new ManualResetEventSlim(false);

            for (int i = 0; i < threadCount; i++)
            {
                int idx = i;
                threads[i] = new Thread(() =>
                {
                    allReady.Signal();
                    go.Wait();
                    results[idx] = dut.TryDownloadAndSaveObject(
                        ValidTestObjectFileSha1,
                        GVFSGitObjects.RequestSource.NamedPipeMessage);
                });
                threads[i].Start();
            }

            // Release all threads simultaneously
            allReady.Wait();
            go.Set();

            // Wait for the first download to start (proves one thread entered the factory)
            downloadStarted.Wait(TimeSpan.FromSeconds(5)).ShouldBeTrue("Download should have started");

            // Give other threads time to pile up on the Lazy
            Thread.Sleep(200);

            // Release the download
            downloadGate.Set();

            // Wait for all threads
            foreach (Thread t in threads)
            {
                t.Join(TimeSpan.FromSeconds(10)).ShouldBeTrue("Thread should complete");
            }

            // Only one download should have occurred
            downloadCount.ShouldEqual(1);

            // All threads should have gotten Success
            foreach (GitObjects.DownloadAndSaveObjectResult result in results)
            {
                result.ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Success);
            }
        }

        [TestCase]
        public void DifferentObjectsAreNotCoalesced()
        {
            string secondSha = "b376885ac8452b6cbf9ced81b1080bfd570d9b91";
            int downloadCount = 0;

            CoalescingTestHttpGitObjects httpObjects = new CoalescingTestHttpGitObjects(
                this.validTestObjectFileContents,
                onDownloadStarting: () => Interlocked.Increment(ref downloadCount));

            MockFileSystemWithCallbacks fileSystem = new MockFileSystemWithCallbacks();
            fileSystem.OnFileExists = (path) => false;
            fileSystem.OnMoveFile = (source, target) => { };
            fileSystem.OnOpenFileStream = (path, mode, access) =>
            {
                if (access == FileAccess.Read)
                {
                    return new MemoryStream(this.validTestObjectFileContents);
                }

                return new MemoryStream();
            };

            GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects, fileSystem);

            dut.TryDownloadAndSaveObject(ValidTestObjectFileSha1, GVFSGitObjects.RequestSource.NamedPipeMessage)
                .ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Success);

            dut.TryDownloadAndSaveObject(secondSha, GVFSGitObjects.RequestSource.NamedPipeMessage)
                .ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Success);

            downloadCount.ShouldEqual(2);
        }

        [TestCase]
        public void FailedDownloadAllowsSubsequentRetry()
        {
            int downloadCount = 0;

            CoalescingTestHttpGitObjects httpObjects = new CoalescingTestHttpGitObjects(
                this.validTestObjectFileContents,
                onDownloadStarting: () => Interlocked.Increment(ref downloadCount),
                failUntilAttempt: 2);

            MockFileSystemWithCallbacks fileSystem = new MockFileSystemWithCallbacks();
            fileSystem.OnFileExists = (path) => false;
            fileSystem.OnMoveFile = (source, target) => { };
            fileSystem.OnOpenFileStream = (path, mode, access) =>
            {
                if (access == FileAccess.Read)
                {
                    return new MemoryStream(this.validTestObjectFileContents);
                }

                return new MemoryStream();
            };

            GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects, fileSystem);

            // First attempt fails
            dut.TryDownloadAndSaveObject(ValidTestObjectFileSha1, GVFSGitObjects.RequestSource.NamedPipeMessage)
                .ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Error);

            // Second attempt should start a new download (not reuse cached failure)
            dut.TryDownloadAndSaveObject(ValidTestObjectFileSha1, GVFSGitObjects.RequestSource.NamedPipeMessage)
                .ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Success);

            // Two separate downloads should have occurred
            downloadCount.ShouldEqual(2);
        }

        [TestCase]
        public void ConcurrentFailedDownloadAllowsSubsequentRetry()
        {
            ManualResetEventSlim downloadStarted = new ManualResetEventSlim(false);
            ManualResetEventSlim downloadGate = new ManualResetEventSlim(false);
            int downloadCount = 0;

            CoalescingTestHttpGitObjects httpObjects = new CoalescingTestHttpGitObjects(
                this.validTestObjectFileContents,
                onDownloadStarting: () =>
                {
                    int count = Interlocked.Increment(ref downloadCount);
                    if (count == 1)
                    {
                        downloadStarted.Set();
                        downloadGate.Wait();
                    }
                },
                failUntilAttempt: 2);

            MockFileSystemWithCallbacks fileSystem = new MockFileSystemWithCallbacks();
            fileSystem.OnFileExists = (path) => false;
            fileSystem.OnMoveFile = (source, target) => { };
            fileSystem.OnOpenFileStream = (path, mode, access) =>
            {
                if (access == FileAccess.Read)
                {
                    return new MemoryStream(this.validTestObjectFileContents);
                }

                return new MemoryStream();
            };

            GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects, fileSystem);

            const int threadCount = 5;
            GitObjects.DownloadAndSaveObjectResult[] results = new GitObjects.DownloadAndSaveObjectResult[threadCount];
            Thread[] threads = new Thread[threadCount];
            CountdownEvent allReady = new CountdownEvent(threadCount);
            ManualResetEventSlim go = new ManualResetEventSlim(false);

            for (int i = 0; i < threadCount; i++)
            {
                int idx = i;
                threads[i] = new Thread(() =>
                {
                    allReady.Signal();
                    go.Wait();
                    results[idx] = dut.TryDownloadAndSaveObject(
                        ValidTestObjectFileSha1,
                        GVFSGitObjects.RequestSource.NamedPipeMessage);
                });
                threads[i].Start();
            }

            allReady.Wait();
            go.Set();

            downloadStarted.Wait(TimeSpan.FromSeconds(5)).ShouldBeTrue("Download should have started");
            Thread.Sleep(200);
            downloadGate.Set();

            foreach (Thread t in threads)
            {
                t.Join(TimeSpan.FromSeconds(10)).ShouldBeTrue("Thread should complete");
            }

            // All coalesced threads should have gotten Error
            foreach (GitObjects.DownloadAndSaveObjectResult result in results)
            {
                result.ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Error);
            }

            // Subsequent request should succeed (new download, not cached failure)
            dut.TryDownloadAndSaveObject(ValidTestObjectFileSha1, GVFSGitObjects.RequestSource.NamedPipeMessage)
                .ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Success);

            downloadCount.ShouldEqual(2);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ExceptionInDownloadFactoryAllowsRetry()
        {
            ManualResetEventSlim downloadStarted = new ManualResetEventSlim(false);
            ManualResetEventSlim downloadGate = new ManualResetEventSlim(false);
            int downloadCount = 0;

            CoalescingTestHttpGitObjects httpObjects = new CoalescingTestHttpGitObjects(
                this.validTestObjectFileContents,
                onDownloadStarting: () =>
                {
                    int count = Interlocked.Increment(ref downloadCount);
                    if (count == 1)
                    {
                        downloadStarted.Set();
                        downloadGate.Wait();
                    }
                },
                throwUntilAttempt: 2);

            MockFileSystemWithCallbacks fileSystem = new MockFileSystemWithCallbacks();
            fileSystem.OnFileExists = (path) => false;
            fileSystem.OnMoveFile = (source, target) => { };
            fileSystem.OnOpenFileStream = (path, mode, access) =>
            {
                if (access == FileAccess.Read)
                {
                    return new MemoryStream(this.validTestObjectFileContents);
                }

                return new MemoryStream();
            };

            GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects, fileSystem);

            const int threadCount = 5;
            Exception[] exceptions = new Exception[threadCount];
            Thread[] threads = new Thread[threadCount];
            CountdownEvent allReady = new CountdownEvent(threadCount);
            ManualResetEventSlim go = new ManualResetEventSlim(false);

            for (int i = 0; i < threadCount; i++)
            {
                int idx = i;
                threads[i] = new Thread(() =>
                {
                    allReady.Signal();
                    go.Wait();
                    try
                    {
                        dut.TryDownloadAndSaveObject(
                            ValidTestObjectFileSha1,
                            GVFSGitObjects.RequestSource.NamedPipeMessage);
                    }
                    catch (Exception ex)
                    {
                        exceptions[idx] = ex;
                    }
                });
                threads[i].Start();
            }

            allReady.Wait();
            go.Set();

            downloadStarted.Wait(TimeSpan.FromSeconds(5)).ShouldBeTrue("Download should have started");
            Thread.Sleep(200);
            downloadGate.Set();

            foreach (Thread t in threads)
            {
                t.Join(TimeSpan.FromSeconds(10)).ShouldBeTrue("Thread should complete");
            }

            // All coalesced threads should have caught the exception
            foreach (Exception ex in exceptions)
            {
                Assert.IsNotNull(ex, "Each coalesced caller should receive the exception");
                Assert.IsInstanceOf(ex);
            }

            // Subsequent retry should succeed (inflight entry was cleaned up)
            dut.TryDownloadAndSaveObject(ValidTestObjectFileSha1, GVFSGitObjects.RequestSource.NamedPipeMessage)
                .ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Success);

            downloadCount.ShouldEqual(2);
        }

        [TestCase]
        public void StragglingFinallyDoesNotRemoveNewInflightDownload()
        {
            // Deterministically reproduce the ABA race against the real inflightDownloads
            // dictionary: a straggling wave-1 thread's TryRemoveInflightDownload must not
            // remove a wave-2 Lazy that was added for the same key.
            ManualResetEventSlim wave2Started = new ManualResetEventSlim(false);
            ManualResetEventSlim wave2Gate = new ManualResetEventSlim(false);
            int downloadCount = 0;

            CoalescingTestHttpGitObjects httpObjects = new CoalescingTestHttpGitObjects(
                this.validTestObjectFileContents,
                onDownloadStarting: () =>
                {
                    int count = Interlocked.Increment(ref downloadCount);
                    if (count == 2)
                    {
                        // Wave 2's download: signal that it's in-flight, then block
                        wave2Started.Set();
                        wave2Gate.Wait();
                    }
                });

            MockFileSystemWithCallbacks fileSystem = new MockFileSystemWithCallbacks();
            fileSystem.OnFileExists = (path) => false;
            fileSystem.OnMoveFile = (source, target) => { };
            fileSystem.OnOpenFileStream = (path, mode, access) =>
            {
                if (access == FileAccess.Read)
                {
                    return new MemoryStream(this.validTestObjectFileContents);
                }

                return new MemoryStream();
            };

            GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects, fileSystem);

            // Wave 1: single download completes immediately (downloadCount becomes 1)
            dut.TryDownloadAndSaveObject(ValidTestObjectFileSha1, GVFSGitObjects.RequestSource.NamedPipeMessage)
                .ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Success);

            // After wave 1, the inflight entry should be cleaned up
            dut.inflightDownloads.ContainsKey(ValidTestObjectFileSha1).ShouldBeFalse("Wave 1 should have cleaned up");

            // Wave 2: start a new download that blocks inside its factory (downloadCount becomes 2)
            Thread wave2Thread = new Thread(() =>
            {
                dut.TryDownloadAndSaveObject(ValidTestObjectFileSha1, GVFSGitObjects.RequestSource.NamedPipeMessage)
                    .ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Success);
            });
            wave2Thread.Start();

            // Wait until wave 2's download factory is executing (Lazy is in the dictionary)
            wave2Started.Wait(TimeSpan.FromSeconds(5)).ShouldBeTrue("Wave 2 download should have started");

            // Capture wave 2's Lazy from the dictionary
            Lazy wave2Lazy;
            dut.inflightDownloads.TryGetValue(ValidTestObjectFileSha1, out wave2Lazy).ShouldBeTrue("Wave 2 Lazy should be in dictionary");

            // Simulate a straggling wave-1 thread: create a different Lazy and try to remove it.
            // With value-aware removal, this must NOT remove wave 2's Lazy.
            Lazy staleLazy =
                new Lazy(() => GitObjects.DownloadAndSaveObjectResult.Success);
            bool staleRemoved = ((ICollection>>)dut.inflightDownloads)
                .Remove(new KeyValuePair>(ValidTestObjectFileSha1, staleLazy));

            staleRemoved.ShouldBeFalse("Straggling finally must not remove wave 2's Lazy");
            dut.inflightDownloads.ContainsKey(ValidTestObjectFileSha1).ShouldBeTrue("Wave 2 Lazy must survive");
            ReferenceEquals(dut.inflightDownloads[ValidTestObjectFileSha1], wave2Lazy).ShouldBeTrue("The entry should still be wave 2's Lazy");

            // Release wave 2 and verify it completes
            wave2Gate.Set();
            wave2Thread.Join(TimeSpan.FromSeconds(10)).ShouldBeTrue("Wave 2 thread should complete");

            // Both waves should have triggered separate downloads
            downloadCount.ShouldEqual(2);
        }

        private void AssertRetryableExceptionOnDownload(
            MemoryStream inputStream,
            string mediaType,
            Action download)
        {
            MockHttpGitObjects httpObjects = new MockHttpGitObjects();
            httpObjects.InputStream = inputStream;
            httpObjects.MediaType = mediaType;
            MockFileSystemWithCallbacks fileSystem = new MockFileSystemWithCallbacks();

            using (ReusableMemoryStream downloadDestination = new ReusableMemoryStream(string.Empty))
            {
                fileSystem.OnFileExists = (path) => false;
                fileSystem.OnOpenFileStream = (path, mode, access) => downloadDestination;

                GVFSGitObjects gitObjects = this.CreateTestableGVFSGitObjects(httpObjects, fileSystem);

                Assert.Throws(() => download(gitObjects));
                inputStream.Dispose();
            }
        }

        private GVFSGitObjects CreateTestableGVFSGitObjects(GitObjectsHttpRequestor httpObjects, MockFileSystemWithCallbacks fileSystem)
        {
            MockTracer tracer = new MockTracer();
            GVFSEnlistment enlistment = new GVFSEnlistment(TestEnlistmentRoot, "https://fakeRepoUrl", "fakeGitBinPath", authentication: null);
            enlistment.InitializeCachePathsFromKey(TestLocalCacheRoot, TestObjectRoot);
            GitRepo repo = new GitRepo(tracer, enlistment, fileSystem, () => new MockLibGit2Repo(tracer));

            GVFSContext context = new GVFSContext(tracer, fileSystem, repo, enlistment);
            GVFSGitObjects dut = new UnsafeGVFSGitObjects(context, httpObjects);
            return dut;
        }

        private class MockHttpGitObjects : GitObjectsHttpRequestor
        {
            public MockHttpGitObjects()
                : this(new MockGVFSEnlistment())
            {
            }

            private MockHttpGitObjects(MockGVFSEnlistment enlistment)
                : base(new MockTracer(), enlistment, new MockCacheServerInfo(), new RetryConfig(maxRetries: 1))
            {
            }

            public Stream InputStream { get; set; }
            public string MediaType { get; set; }

            public static MemoryStream GetRandomStream(int size)
            {
                Random randy = new Random(0);
                MemoryStream stream = new MemoryStream();
                byte[] buffer = new byte[size];

                randy.NextBytes(buffer);
                stream.Write(buffer, 0, buffer.Length);

                stream.Position = 0;
                return stream;
            }

            public override RetryWrapper.InvocationResult TryDownloadLooseObject(
                string objectId,
                bool retryOnFailure,
                CancellationToken cancellationToken,
                string requestSource,
                Func.CallbackResult> onSuccess)
            {
                return this.TryDownloadObjects(new[] { objectId }, onSuccess, null, false);
            }

            public override RetryWrapper.InvocationResult TryDownloadObjects(
                IEnumerable objectIds,
                Func.CallbackResult> onSuccess,
                Action.ErrorEventArgs> onFailure,
                bool preferBatchedLooseObjects)
            {
                using (GitEndPointResponseData response = new GitEndPointResponseData(
                    HttpStatusCode.OK,
                    this.MediaType,
                    this.InputStream,
                    message: null,
                    onResponseDisposed: null))
                {
                    onSuccess(0, response);
                }

                GitObjectTaskResult result = new GitObjectTaskResult(true);
                return new RetryWrapper.InvocationResult(0, true, result);
            }

            public override List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken)
            {
                throw new NotImplementedException();
            }
        }

        private class UnsafeGVFSGitObjects : GVFSGitObjects
        {
            public UnsafeGVFSGitObjects(GVFSContext context, GitObjectsHttpRequestor objectRequestor)
                : base(context, objectRequestor)
            {
                this.checkData = false;
            }
        }

        private class CoalescingTestHttpGitObjects : GitObjectsHttpRequestor
        {
            private readonly byte[] objectContents;
            private readonly Action onDownloadStarting;
            private readonly int failUntilAttempt;
            private readonly int throwUntilAttempt;
            private int attemptCount;

            public CoalescingTestHttpGitObjects(byte[] objectContents, Action onDownloadStarting, int failUntilAttempt = 0, int throwUntilAttempt = 0)
                : this(new MockGVFSEnlistment(), objectContents, onDownloadStarting, failUntilAttempt, throwUntilAttempt)
            {
            }

            private CoalescingTestHttpGitObjects(MockGVFSEnlistment enlistment, byte[] objectContents, Action onDownloadStarting, int failUntilAttempt, int throwUntilAttempt)
                : base(new MockTracer(), enlistment, new MockCacheServerInfo(), new RetryConfig(maxRetries: 1))
            {
                this.objectContents = objectContents;
                this.onDownloadStarting = onDownloadStarting;
                this.failUntilAttempt = failUntilAttempt;
                this.throwUntilAttempt = throwUntilAttempt;
            }

            public override RetryWrapper.InvocationResult TryDownloadLooseObject(
                string objectId,
                bool retryOnFailure,
                CancellationToken cancellationToken,
                string requestSource,
                Func.CallbackResult> onSuccess)
            {
                this.onDownloadStarting?.Invoke();

                int attempt = Interlocked.Increment(ref this.attemptCount);
                if (attempt < this.throwUntilAttempt)
                {
                    throw new IOException("Simulated download exception");
                }

                if (attempt < this.failUntilAttempt)
                {
                    GitObjectTaskResult failResult = new GitObjectTaskResult(false);
                    return new RetryWrapper.InvocationResult(0, false, failResult);
                }

                using (MemoryStream stream = new MemoryStream(this.objectContents))
                using (GitEndPointResponseData response = new GitEndPointResponseData(
                    HttpStatusCode.OK,
                    GVFSConstants.MediaTypes.LooseObjectMediaType,
                    stream,
                    message: null,
                    onResponseDisposed: null))
                {
                    onSuccess(0, response);
                }

                GitObjectTaskResult result = new GitObjectTaskResult(true);
                return new RetryWrapper.InvocationResult(0, true, result);
            }

            public override RetryWrapper.InvocationResult TryDownloadObjects(
                IEnumerable objectIds,
                Func.CallbackResult> onSuccess,
                Action.ErrorEventArgs> onFailure,
                bool preferBatchedLooseObjects)
            {
                throw new NotImplementedException();
            }

            public override List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken)
            {
                throw new NotImplementedException();
            }
        }
    }
}

================================================
FILE: GVFS/GVFS.UnitTests/Git/GitAuthenticationTests.cs
================================================
using System;
using System.Linq;
using GVFS.Common.Git;
using GVFS.Tests;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;

namespace GVFS.UnitTests.Git
{
    [TestFixtureSource(typeof(DataSources), nameof(DataSources.AllBools))]
    public class GitAuthenticationTests
    {
        private const string CertificatePath = "certificatePath";
        private const string AzureDevOpsUseHttpPathString = "-c credential.\"https://dev.azure.com\".useHttpPath=true";

        private readonly bool sslSettingsPresent;

        public GitAuthenticationTests(bool sslSettingsPresent)
        {
            this.sslSettingsPresent = sslSettingsPresent;
        }

        [TestCase]
        public void AuthShouldBackoffAfterFirstRetryFailure()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = this.GetGitProcess();

            GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl");
            dut.TryInitializeAndRequireAuth(tracer, out _);

            string authString;
            string error;

            dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to get initial credential");

            dut.RejectCredentials(tracer, authString);
            dut.IsBackingOff.ShouldEqual(false, "Should not backoff after credentials initially rejected");
            gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1);

            dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to retry getting credential on iteration");
            dut.IsBackingOff.ShouldEqual(false, "Should not backoff after successfully getting credentials");

            dut.RejectCredentials(tracer, authString);
            dut.IsBackingOff.ShouldEqual(true, "Should continue to backoff after rejecting credentials");
            dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(false, "TryGetCredential should not succeed during backoff");
            gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(2);
        }

        [TestCase]
        public void BackoffIsNotInEffectAfterSuccess()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = this.GetGitProcess();

            GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl");
            dut.TryInitializeAndRequireAuth(tracer, out _);

            string authString;
            string error;

            for (int i = 0; i < 5; ++i)
            {
                dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to get credential on iteration " + i + ": " + error);
                dut.RejectCredentials(tracer, authString);
                dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to retry getting credential on iteration " + i + ": " + error);
                dut.ApproveCredentials(tracer, authString);
                dut.IsBackingOff.ShouldEqual(false, "Should reset backoff after successfully refreshing credentials");
                gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(i+1, $"Should have {i+1} credentials rejection");
                gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(i+1, $"Should have {i+1} credential approvals");
            }
        }

        [TestCase]
        public void ContinuesToBackoffIfTryGetCredentialsFails()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = this.GetGitProcess();

            GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl");
            dut.TryInitializeAndRequireAuth(tracer, out _);

            string authString;
            string error;

            dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to get initial credential");
            dut.RejectCredentials(tracer, authString);
            gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1);

            gitProcess.ShouldFail = true;

            dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(false, "Succeeded despite GitProcess returning failure");
            dut.IsBackingOff.ShouldEqual(true, "Should continue to backoff if failed to get credentials");

            dut.RejectCredentials(tracer, authString);
            dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(false, "TryGetCredential should not succeed during backoff");
            dut.IsBackingOff.ShouldEqual(true, "Should continue to backoff if failed to get credentials");
            gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1);
        }

        [TestCase]
        public void TwoThreadsFailAtOnceStillRetriesOnce()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = this.GetGitProcess();

            GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl");
            dut.TryInitializeAndRequireAuth(tracer, out _);

            string authString;
            string error;

            // Populate an initial PAT on two threads
            dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true);
            dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true);

            // Simulate a 401 error on two threads
            dut.RejectCredentials(tracer, authString);
            dut.RejectCredentials(tracer, authString);
            gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1);
            gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(authString);

            // Both threads should still be able to get a PAT for retry purposes
            dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "The second thread caused back off when it shouldn't");
            dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true);
        }

        [TestCase]
        public void TwoThreadsInterleavingFailuresStillRetriesOnce()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = this.GetGitProcess();

            GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl");
            dut.TryInitializeAndRequireAuth(tracer, out _);

            string thread1Auth;
            string thread1AuthRetry;
            string thread2Auth;
            string thread2AuthRetry;
            string error;

            // Populate an initial PAT on two threads
            dut.TryGetCredentials(tracer, out thread1Auth, out error).ShouldEqual(true);
            dut.TryGetCredentials(tracer, out thread2Auth, out error).ShouldEqual(true);

            // Simulate a 401 error on one threads
            dut.RejectCredentials(tracer, thread1Auth);
            gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1);
            gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth);

            // That thread then retries
            dut.TryGetCredentials(tracer, out thread1AuthRetry, out error).ShouldEqual(true);

            // The second thread fails with the old PAT
            dut.RejectCredentials(tracer, thread2Auth);
            gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1, "Should not have rejected a second time");
            gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth, "Should only have rejected thread1's initial credential");

            // The second thread should be able to get a PAT
            dut.TryGetCredentials(tracer, out thread2AuthRetry, out error).ShouldEqual(true, error);
        }

        [TestCase]
        public void TwoThreadsInterleavingFailuresShouldntStompASuccess()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = this.GetGitProcess();

            GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl");
            dut.TryInitializeAndRequireAuth(tracer, out _);

            string thread1Auth;
            string thread2Auth;
            string error;

            // Populate an initial PAT on two threads
            dut.TryGetCredentials(tracer, out thread1Auth, out error).ShouldEqual(true);
            dut.TryGetCredentials(tracer, out thread2Auth, out error).ShouldEqual(true);

            // Simulate a 401 error on one threads
            dut.RejectCredentials(tracer, thread1Auth);
            gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1);
            gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth);

            // That thread then retries and succeeds
            dut.TryGetCredentials(tracer, out thread1Auth, out error).ShouldEqual(true);
            dut.ApproveCredentials(tracer, thread1Auth);
            gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(1);
            gitProcess.CredentialApprovals["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth);

            // If the second thread fails with the old PAT, it shouldn't stomp the new PAT
            dut.RejectCredentials(tracer, thread2Auth);
            gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1);

            // The second thread should be able to get a PAT
            dut.TryGetCredentials(tracer, out thread2Auth, out error).ShouldEqual(true);
            thread2Auth.ShouldEqual(thread1Auth, "The second thread stomp the first threads good auth string");
        }

        [TestCase]
        public void DontDoubleStoreExistingCredential()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = this.GetGitProcess();

            GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl");
            dut.TryInitializeAndRequireAuth(tracer, out _);

            string authString;
            dut.TryGetCredentials(tracer, out authString, out _).ShouldBeTrue();
            dut.ApproveCredentials(tracer, authString);
            dut.ApproveCredentials(tracer, authString);
            dut.ApproveCredentials(tracer, authString);
            dut.ApproveCredentials(tracer, authString);
            dut.ApproveCredentials(tracer, authString);

            gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(1);
            gitProcess.CredentialRejections.Count.ShouldEqual(0);
            gitProcess.StoredCredentials.Count.ShouldEqual(1);
            gitProcess.StoredCredentials.Single().Key.ShouldEqual("mock://repoUrl");
        }

        [TestCase]
        public void DontStoreDifferentCredentialFromCachedValue()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = this.GetGitProcess();

            GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl");
            dut.TryInitializeAndRequireAuth(tracer, out _);

            // Get and store an initial value that will be cached
            string authString;
            dut.TryGetCredentials(tracer, out authString, out _).ShouldBeTrue();
            dut.ApproveCredentials(tracer, authString);

            // Try and store a different value from the one that is cached
            dut.ApproveCredentials(tracer, "different value");

            gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(1);
            gitProcess.CredentialRejections.Count.ShouldEqual(0);
            gitProcess.StoredCredentials.Count.ShouldEqual(1);
            gitProcess.StoredCredentials.Single().Key.ShouldEqual("mock://repoUrl");
        }

        [TestCase]
        public void RejectionShouldNotBeSentIfUnderlyingTokenHasChanged()
        {
            MockTracer tracer = new MockTracer();
            MockGitProcess gitProcess = this.GetGitProcess();

            GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl");
            dut.TryInitializeAndRequireAuth(tracer, out _);

            // Get and store an initial value that will be cached
            string authString;
            dut.TryGetCredentials(tracer, out authString, out _).ShouldBeTrue();
            dut.ApproveCredentials(tracer, authString);

            // Change the underlying token
            gitProcess.SetExpectedCommandResult(
                $"{AzureDevOpsUseHttpPathString} credential fill",
                () => new GitProcess.Result("username=username\r\npassword=password" + Guid.NewGuid() + "\r\n", string.Empty, GitProcess.Result.SuccessCode));

            // Try and reject it. We should get a new token, but without forwarding the rejection to the
            // underlying credential store
            dut.RejectCredentials(tracer, authString);
            dut.TryGetCredentials(tracer, out var newAuthString, out _).ShouldBeTrue();
            newAuthString.ShouldNotEqual(authString);
            gitProcess.CredentialRejections.ShouldBeEmpty();
        }

        private MockGitProcess GetGitProcess()
        {
            MockGitProcess gitProcess = new MockGitProcess();
            gitProcess.SetExpectedCommandResult("config gvfs.FunctionalTests.UserName", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode));
            gitProcess.SetExpectedCommandResult("config gvfs.FunctionalTests.Password", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode));

            if (this.sslSettingsPresent)
            {
                gitProcess.SetExpectedCommandResult("config --get-urlmatch http mock://repoUrl", () => new GitProcess.Result($"http.sslCert {CertificatePath}\nhttp.sslCertPasswordProtected true\n\n", string.Empty, GitProcess.Result.SuccessCode));
            }
            else
            {
                gitProcess.SetExpectedCommandResult("config --get-urlmatch http mock://repoUrl", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));
            }

            int approvals = 0;
            int rejections = 0;
            gitProcess.SetExpectedCommandResult(
                $"{AzureDevOpsUseHttpPathString} credential fill",
                () => new GitProcess.Result("username=username\r\npassword=password" + rejections + "\r\n", string.Empty, GitProcess.Result.SuccessCode));

            gitProcess.SetExpectedCommandResult(
                $"{AzureDevOpsUseHttpPathString} credential approve",
                () =>
                {
                    approvals++;
                    return new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode);
                });

            gitProcess.SetExpectedCommandResult(
                $"{AzureDevOpsUseHttpPathString} credential reject",
                () =>
                {
                    rejections++;
                    return new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode);
                });
            return gitProcess;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Git/GitObjectsTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using NUnit.Framework;
using System.Collections.Generic;
using System.IO;
using System.Security;

namespace GVFS.UnitTests.Git
{
    [TestFixture]
    public class GitObjectsTests
    {
        private const string EmptySha = "0000000000000000000000000000000000000000";
        private const string RealSha = "78981922613b2afb6025042ff6bd878ac1994e85";
        private readonly byte[] realData = new byte[]
        {
            0x78, 0x01, 0x4B, 0xCA, 0xC9, 0x4F, 0x52, 0x30, 0x62,
            0x48, 0xE4, 0x02, 0x00, 0x0E, 0x64, 0x02, 0x5D
        };
        private readonly List openedPaths = new List();
        private readonly Dictionary pathsToData = new Dictionary();

        [TestCase]
        public void WriteLooseObject_DetectsDataNotCompressed()
        {
            ITracer tracer = new MockTracer();
            GVFSEnlistment enlistment = new MockGVFSEnlistment();
            MockFileSystemWithCallbacks filesystem = new MockFileSystemWithCallbacks();
            GVFSContext context = new GVFSContext(tracer, filesystem, null, enlistment);

            GitObjects gitObjects = new GVFSGitObjects(context, null);

            this.openedPaths.Clear();
            filesystem.OnOpenFileStream = this.OnOpenFileStream;
            filesystem.OnFileExists = this.OnFileExists;

            bool foundException = false;

            try
            {
                using (Stream stream = new MemoryStream())
                {
                    stream.Write(new byte[] { 0, 1, 2, 3, 4 }, 0, 5);
                    stream.Position = 0;
                    gitObjects.WriteLooseObject(stream, EmptySha, true, new byte[128]);
                }
            }
            catch (RetryableException ex)
            {
                foundException = true;
                ex.Message.ShouldContain($"Requested object with hash {EmptySha} but received data that failed decompression.");
            }

            foundException.ShouldBeTrue("Failed to throw RetryableException");
            this.openedPaths.Count.ShouldEqual(1, "Incorrect number of opened paths (one to write temp file)");
            this.openedPaths[0].IndexOf(EmptySha.Substring(2)).ShouldBeAtMost(-1, "Should not have written to the loose object location");
        }

        [TestCase]
        public void WriteLooseObject_DetectsIncorrectData()
        {
            ITracer tracer = new MockTracer();
            GVFSEnlistment enlistment = new MockGVFSEnlistment();
            MockFileSystemWithCallbacks filesystem = new MockFileSystemWithCallbacks();
            GVFSContext context = new GVFSContext(tracer, filesystem, null, enlistment);

            GitObjects gitObjects = new GVFSGitObjects(context, null);

            this.openedPaths.Clear();
            filesystem.OnOpenFileStream = this.OnOpenFileStream;
            filesystem.OnFileExists = this.OnFileExists;

            bool foundException = false;

            try
            {
                using (Stream stream = new MemoryStream())
                {
                    stream.Write(this.realData, 0, this.realData.Length);
                    stream.Position = 0;
                    gitObjects.WriteLooseObject(stream, EmptySha, true, new byte[128]);
                }
            }
            catch (SecurityException ex)
            {
                foundException = true;
                ex.Message.ShouldContain($"Requested object with hash {EmptySha} but received object with hash");
            }

            foundException.ShouldBeTrue("Failed to throw SecurityException");
            this.openedPaths.Count.ShouldEqual(1, "Incorrect number of opened paths (one to write temp file)");
            this.openedPaths[0].IndexOf(EmptySha.Substring(2)).ShouldBeAtMost(-1, "Should not have written to the loose object location");
        }

        [TestCase]
        public void WriteLooseObject_Success()
        {
            ITracer tracer = new MockTracer();
            GVFSEnlistment enlistment = new MockGVFSEnlistment();
            MockFileSystemWithCallbacks filesystem = new MockFileSystemWithCallbacks();
            GVFSContext context = new GVFSContext(tracer, filesystem, null, enlistment);

            GitObjects gitObjects = new GVFSGitObjects(context, null);

            this.openedPaths.Clear();
            filesystem.OnOpenFileStream = this.OnOpenFileStream;
            filesystem.OnFileExists = this.OnFileExists;

            bool moved = false;
            filesystem.OnMoveFile = (path1, path2) => { moved = true; };

            using (Stream stream = new MemoryStream())
            {
                stream.Write(this.realData, 0, this.realData.Length);
                stream.Position = 0;
                gitObjects.WriteLooseObject(stream, RealSha, true, new byte[128]);
            }

            this.openedPaths.Count.ShouldEqual(2, "Incorrect number of opened paths");
            moved.ShouldBeTrue("File was not moved");
        }

        private Stream OnOpenFileStream(string path, FileMode mode, FileAccess access)
        {
            this.openedPaths.Add(path);

            if (this.pathsToData.TryGetValue(path, out MemoryStream stream))
            {
                this.pathsToData[path] = new MemoryStream(stream.ToArray());
            }
            else
            {
                this.pathsToData[path] = new MemoryStream();
            }

            return this.pathsToData[path];
        }

        private bool OnFileExists(string path)
        {
            return this.pathsToData.TryGetValue(path, out _);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Git/GitProcessTests.cs
================================================
using GVFS.Common.Git;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using NUnit.Framework;
using System.Diagnostics;

namespace GVFS.UnitTests.Git
{
    [TestFixture]
    public class GitProcessTests
    {
        [TestCase]
        public void TryKillRunningProcess_NeverRan()
        {
            GitProcess process = new GitProcess(new MockGVFSEnlistment());
            process.TryKillRunningProcess(out string processName, out int exitCode, out string error).ShouldBeTrue();

            processName.ShouldBeNull();
            exitCode.ShouldEqual(-1);
            error.ShouldBeNull();
        }

        [TestCase]
        public void ResultHasNoErrors()
        {
            GitProcess.Result result = new GitProcess.Result(
                string.Empty,
                string.Empty,
                0);

            result.ExitCodeIsFailure.ShouldBeFalse();
            result.StderrContainsErrors().ShouldBeFalse();
        }

        [TestCase]
        public void ResultHasWarnings()
        {
            GitProcess.Result result = new GitProcess.Result(
                string.Empty,
                "Warning: this is fine.\n",
                0);

            result.ExitCodeIsFailure.ShouldBeFalse();
            result.StderrContainsErrors().ShouldBeFalse();
        }

        [TestCase]
        public void ResultHasNonWarningErrors_SingleLine_AllWarnings()
        {
            GitProcess.Result result = new GitProcess.Result(
                string.Empty,
                "warning: this line should not be considered an error",
                1);

            result.ExitCodeIsFailure.ShouldBeTrue();
            result.StderrContainsErrors().ShouldBeFalse();
        }

        [TestCase]
        public void ResultHasNonWarningErrors_Multiline_AllWarnings()
        {
            GitProcess.Result result = new GitProcess.Result(
                string.Empty,
                @"warning: this line should not be considered an error
WARNING: neither should this.",
                1);

            result.ExitCodeIsFailure.ShouldBeTrue();
            result.StderrContainsErrors().ShouldBeFalse();
        }

        [TestCase]
        public void ResultHasNonWarningErrors_Multiline_EmptyLines()
        {
            GitProcess.Result result = new GitProcess.Result(
                string.Empty,
                @"
warning: this is fine

warning: this is too

",
                1);

            result.ExitCodeIsFailure.ShouldBeTrue();
            result.StderrContainsErrors().ShouldBeFalse();
        }

        [TestCase]
        public void ResultHasNonWarningErrors_Singleline_AllErrors()
        {
            GitProcess.Result result = new GitProcess.Result(
                string.Empty,
                "this is an error",
                1);

            result.ExitCodeIsFailure.ShouldBeTrue();
            result.StderrContainsErrors().ShouldBeTrue();
        }

        [TestCase]
        public void ResultHasNonWarningErrors_Multiline_AllErrors()
        {
            GitProcess.Result result = new GitProcess.Result(
                string.Empty,
                @"error1
error2",
                1);

            result.ExitCodeIsFailure.ShouldBeTrue();
            result.StderrContainsErrors().ShouldBeTrue();
        }

        [TestCase]
        public void ResultHasNonWarningErrors_Multiline_ErrorsAndWarnings()
        {
            GitProcess.Result result = new GitProcess.Result(
                string.Empty,
                @"WARNING: this is fine
this is an error",
                1);

            result.ExitCodeIsFailure.ShouldBeTrue();
            result.StderrContainsErrors().ShouldBeTrue();
        }

        [TestCase]
        public void ResultHasNonWarningErrors_TrailingWhitespace_Warning()
        {
            GitProcess.Result result = new GitProcess.Result(
                string.Empty,
                "Warning: this is fine\n",
                1);

            result.ExitCodeIsFailure.ShouldBeTrue();
            result.StderrContainsErrors().ShouldBeFalse();
        }

        [TestCase]
        public void ConfigResult_TryParseAsString_DefaultIsNull()
        {
            GitProcess.ConfigResult result = new GitProcess.ConfigResult(
                new GitProcess.Result(string.Empty, string.Empty, 1),
                "settingName");

            result.TryParseAsString(out string expectedValue, out string _).ShouldBeTrue();
            expectedValue.ShouldBeNull();
        }

        [TestCase]
        public void ConfigResult_TryParseAsString_FailsWhenErrors()
        {
            GitProcess.ConfigResult result = new GitProcess.ConfigResult(
                new GitProcess.Result(string.Empty, "errors", 1),
                "settingName");

            result.TryParseAsString(out string expectedValue, out string _).ShouldBeFalse();
        }

        [TestCase]
        public void ConfigResult_TryParseAsString_NullWhenUnsetAndWarnings()
        {
            GitProcess.ConfigResult result = new GitProcess.ConfigResult(
                new GitProcess.Result(string.Empty, "warning: ignored", 1),
                "settingName");

            result.TryParseAsString(out string expectedValue, out string _).ShouldBeTrue();
            expectedValue.ShouldBeNull();
        }

        [TestCase]
        public void ConfigResult_TryParseAsString_PassesThroughErrors()
        {
            GitProcess.ConfigResult result = new GitProcess.ConfigResult(
                new GitProcess.Result(string.Empty, "--local can only be used inside a git repository", 1),
                "settingName");

            result.TryParseAsString(out string expectedValue, out string error).ShouldBeFalse();
            error.Contains("--local").ShouldBeTrue();
        }

        [TestCase]
        public void ConfigResult_TryParseAsString_RespectsDefaultOnFailure()
        {
            GitProcess.ConfigResult result = new GitProcess.ConfigResult(
                new GitProcess.Result(string.Empty, string.Empty, 1),
                "settingName");

            result.TryParseAsString(out string expectedValue, out string _, "default").ShouldBeTrue();
            expectedValue.ShouldEqual("default");
        }

        [TestCase]
        public void ConfigResult_TryParseAsString_OverridesDefaultOnSuccess()
        {
            GitProcess.ConfigResult result = new GitProcess.ConfigResult(
                new GitProcess.Result("expected", string.Empty, 0),
                "settingName");

            result.TryParseAsString(out string expectedValue, out string _, "default").ShouldBeTrue();
            expectedValue.ShouldEqual("expected");
        }

        [TestCase]
        public void ConfigResult_TryParseAsInt_FailsWithErrors()
        {
            GitProcess.ConfigResult result = new GitProcess.ConfigResult(
                new GitProcess.Result(string.Empty, "errors", 1),
                "settingName");

            result.TryParseAsInt(0, -1, out int value, out string error).ShouldBeFalse();
        }

        [TestCase]
        public void ConfigResult_TryParseAsInt_DefaultWhenUnset()
        {
            GitProcess.ConfigResult result = new GitProcess.ConfigResult(
                new GitProcess.Result(string.Empty, string.Empty, 1),
                "settingName");

            result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue();
            value.ShouldEqual(1);
        }

        [TestCase]
        public void ConfigResult_TryParseAsInt_ParsesWhenNoError()
        {
            GitProcess.ConfigResult result = new GitProcess.ConfigResult(
                new GitProcess.Result("32", string.Empty, 0),
                "settingName");

            result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue();
            value.ShouldEqual(32);
        }

        [TestCase]
        public void ConfigResult_TryParseAsInt_ParsesWhenWarnings()
        {
            GitProcess.ConfigResult result = new GitProcess.ConfigResult(
                new GitProcess.Result("32", "warning: ignored", 0),
                "settingName");

            result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue();
            value.ShouldEqual(32);
        }

        [TestCase]
        public void ConfigResult_TryParseAsInt_ParsesWhenOutputIncludesWhitespace()
        {
            GitProcess.ConfigResult result = new GitProcess.ConfigResult(
                new GitProcess.Result("\n\t 32\t\r\n", "warning: ignored", 0),
                "settingName");

            result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue();
            value.ShouldEqual(32);
        }

        [TestCase("dir/file.txt", "\"dir/file.txt\"")]
        [TestCase("my dir/my file.txt", "\"my dir/my file.txt\"")]
        [TestCase("dir/file\"name.txt", "\"dir/file\\\"name.txt\"")]
        [TestCase("\"quoted\"", "\"\\\"quoted\\\"\"")]
        [TestCase("dir\\subdir\\file.txt", "\"dir\\subdir\\file.txt\"")] // Backslashes as path separators left as-is
        [TestCase("", "\"\"")]
        [TestCase("dir\\\"file.txt", "\"dir\\\\\\\"file.txt\"")] // Backslash before quote: doubled, then quote escaped
        [TestCase("dir\\subdir\\", "\"dir\\subdir\\\\\"")] // Trailing backslash doubled
        public void QuoteGitPath(string input, string expected)
        {
            GitProcess.QuoteGitPath(input).ShouldEqual(expected);
        }

        [TestCase]
        [Description("Integration test: verify QuoteGitPath produces arguments that git actually receives correctly")]
        public void QuoteGitPath_RoundTripThroughProcess()
        {
            // Test that paths with special characters survive the
            // ProcessStartInfo.Arguments → Windows CRT argument parsing → git round-trip.
            // We use "git rev-parse --sq-quote " which echoes the path back
            // in shell-quoted form, proving git received it correctly.
            string[] testPaths = new[]
            {
                "simple/path.txt",
                "path with spaces/file name.txt",
                "path\\with\\backslashes\\file.txt",
            };

            string gitPath = "C:\\Program Files\\Git\\cmd\\git.exe";
            if (!System.IO.File.Exists(gitPath))
            {
                Assert.Ignore("Git not found at expected path — skipping integration test");
            }

            foreach (string testPath in testPaths)
            {
                string quoted = GitProcess.QuoteGitPath(testPath);
                ProcessStartInfo psi = new ProcessStartInfo(gitPath)
                {
                    Arguments = "rev-parse --sq-quote " + quoted,
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    CreateNoWindow = true,
                };

                using (Process proc = Process.Start(psi))
                {
                    string output = proc.StandardOutput.ReadToEnd().Trim();
                    proc.WaitForExit();

                    // git sq-quote wraps in single quotes and escapes single quotes
                    // For a simple path "foo/bar.txt" → output is "'foo/bar.txt'"
                    // Strip the outer single quotes to get the raw path back
                    if (output.StartsWith("'") && output.EndsWith("'"))
                    {
                        output = output.Substring(1, output.Length - 2);
                    }

                    output.ShouldEqual(
                        testPath,
                        $"Path round-trip failed for: {testPath} (quoted as: {quoted})");
                }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Hooks/PostIndexChangedHookTests.cs
================================================
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.IO;

namespace GVFS.UnitTests.Hooks
{
    [TestFixture]
    public class PostIndexChangedHookTests
    {
        // Exit code from common.h ReturnCode::NotInGVFSEnlistment.
        // The hook dies with this code when it can't find a .gvfs folder.
        private const int NotInGVFSEnlistment = 3;

        // The hook exe is built to the same output root as the test runner.
        // Walk up from the unit test output dir to find the hook exe under
        // the shared build output tree.
        private static readonly string HookExePath = FindHookExe();

        private static string FindHookExe()
        {
            // Test runner lives at: out\GVFS.UnitTests\bin\Debug\net471\win-x64\
            // Hook exe lives at:    out\GVFS.PostIndexChangedHook\bin\x64\Debug\
            string testDir = Path.GetDirectoryName(typeof(PostIndexChangedHookTests).Assembly.Location);
            string outDir = Path.GetFullPath(Path.Combine(testDir, "..", "..", "..", "..", ".."));
            string hookPath = Path.Combine(outDir, "GVFS.PostIndexChangedHook", "bin", "x64", "Debug", "GVFS.PostIndexChangedHook.exe");

            // Also check via VFS_OUTDIR if available
            if (!File.Exists(hookPath))
            {
                string vfsOutDir = Environment.GetEnvironmentVariable("VFS_OUTDIR");
                if (!string.IsNullOrEmpty(vfsOutDir))
                {
                    hookPath = Path.Combine(vfsOutDir, "GVFS.PostIndexChangedHook", "bin", "x64", "Debug", "GVFS.PostIndexChangedHook.exe");
                }
            }

            return hookPath;
        }

        [SetUp]
        public void EnsureHookExists()
        {
            if (!File.Exists(HookExePath))
            {
                Assert.Ignore($"Hook exe not found at {HookExePath} — build the full solution first.");
            }
        }

        /// 
        /// When GIT_INDEX_FILE points to a non-canonical (temp) index,
        /// the hook should exit immediately with code 0 without trying
        /// to connect to the GVFS pipe.
        /// 
        [TestCase("C:\\repo\\.git\\tmp_index_1234", "C:\\repo\\.git")]
        [TestCase("/repo/.git/some_other_index", "/repo/.git")]
        [TestCase("D:\\src\\.git\\index.lock", "D:\\src\\.git")]
        [TestCase("C:\\tmp\\scratch_index", "C:\\repo\\.git")]
        public void SkipsNotification_WhenIndexIsNonCanonical(string indexFile, string gitDir)
        {
            int exitCode = RunHook(indexFile, gitDir);
            Assert.AreEqual(0, exitCode, "Hook should exit 0 (skip) for non-canonical index");
        }

        /// 
        /// When GIT_INDEX_FILE matches the canonical $GIT_DIR/index,
        /// the hook should NOT skip — it should proceed and attempt the
        /// pipe connection. Outside a GVFS mount (WorkingDirectory is
        /// %TEMP%), the hook fails with NotInGVFSEnlistment, proving
        /// the guard did not fire.
        /// 
        [TestCase("C:\\repo\\.git\\index", "C:\\repo\\.git")]
        [TestCase("C:\\repo\\.git/index", "C:\\repo\\.git\\")]
        public void DoesNotSkip_WhenIndexIsCanonical(string indexFile, string gitDir)
        {
            int exitCode = RunHook(indexFile, gitDir);
            Assert.AreEqual(NotInGVFSEnlistment, exitCode,
                "Hook should NOT skip for canonical index (NotInGVFSEnlistment = guard didn't fire)");
        }

        /// 
        /// When GIT_INDEX_FILE is not set at all, the hook should NOT
        /// skip — this is the normal case where git writes the default index.
        /// 
        [Test]
        public void DoesNotSkip_WhenGitIndexFileNotSet()
        {
            int exitCode = RunHook(null, "C:\\repo\\.git");
            Assert.AreEqual(NotInGVFSEnlistment, exitCode,
                "Hook should NOT skip when GIT_INDEX_FILE is unset");
        }

        /// 
        /// When GIT_INDEX_FILE is set but GIT_DIR is empty/missing,
        /// the hook should NOT skip — err on the side of correctness
        /// when the environment is unexpected.
        /// 
        [TestCase("C:\\repo\\.git\\tmp_index", null)]
        [TestCase("C:\\repo\\.git\\tmp_index", "")]
        public void DoesNotSkip_WhenGitDirMissing(string indexFile, string gitDir)
        {
            int exitCode = RunHook(indexFile, gitDir);
            Assert.AreEqual(NotInGVFSEnlistment, exitCode,
                "Hook should NOT skip when GIT_DIR is absent — err on the side of correctness");
        }

        /// 
        /// Case-insensitive matching: mixed-case paths that resolve to
        /// the canonical index should NOT skip.
        /// 
        [Test]
        public void DoesNotSkip_WhenCanonicalPathDiffersOnlyInCase()
        {
            int exitCode = RunHook("C:\\Repo\\.GIT\\INDEX", "C:\\Repo\\.GIT");
            Assert.AreEqual(NotInGVFSEnlistment, exitCode,
                "Case-insensitive canonical match should NOT skip");
        }

        /// 
        /// Separator normalization: forward vs backslash in canonical
        /// path should still match.
        /// 
        [Test]
        public void SkipsNotification_ForwardSlashTempIndex()
        {
            int exitCode = RunHook("C:/repo/.git/tmp_idx", "C:\\repo\\.git");
            Assert.AreEqual(0, exitCode, "Forward-slash temp index should still be detected as non-canonical");
        }

        private int RunHook(string gitIndexFile, string gitDir)
        {
            ProcessStartInfo psi = new ProcessStartInfo
            {
                FileName = HookExePath,
                Arguments = "1 0",
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardError = true,
                RedirectStandardOutput = true,

                // Run outside any GVFS enlistment so the pipe lookup
                // fails predictably with NotInGVFSEnlistment.
                WorkingDirectory = Path.GetTempPath(),
            };

            // Set or remove env vars
            if (gitIndexFile != null)
            {
                psi.Environment["GIT_INDEX_FILE"] = gitIndexFile;
            }
            else
            {
                psi.Environment.Remove("GIT_INDEX_FILE");
            }

            if (gitDir != null)
            {
                psi.Environment["GIT_DIR"] = gitDir;
            }
            else
            {
                psi.Environment.Remove("GIT_DIR");
            }

            using (Process process = Process.Start(psi))
            {
                process.WaitForExit(5000);
                if (!process.HasExited)
                {
                    process.Kill();
                    Assert.Fail("Hook process timed out (5s) — likely blocked on pipe connect");
                }

                return process.ExitCode;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Hooks/UnstageTests.cs
================================================
using GVFS.Hooks;
using GVFS.Tests.Should;
using NUnit.Framework;

namespace GVFS.UnitTests.Hooks
{
    [TestFixture]
    public class UnstageTests
    {
        // ── IsUnstageOperation ──────────────────────────────────────────

        [TestCase]
        public void IsUnstageOperation_RestoreStaged()
        {
            UnstageCommandParser.IsUnstageOperation(
                "restore",
                new[] { "pre-command", "restore", "--staged", "." })
                .ShouldBeTrue();
        }

        [TestCase]
        public void IsUnstageOperation_RestoreShortFlag()
        {
            UnstageCommandParser.IsUnstageOperation(
                "restore",
                new[] { "pre-command", "restore", "-S", "file.txt" })
                .ShouldBeTrue();
        }

        [TestCase]
        public void IsUnstageOperation_RestoreCombinedShortFlags()
        {
            // -WS means --worktree --staged
            UnstageCommandParser.IsUnstageOperation(
                "restore",
                new[] { "pre-command", "restore", "-WS", "file.txt" })
                .ShouldBeTrue();
        }

        [TestCase]
        public void IsUnstageOperation_RestoreLowerS_NotStaged()
        {
            // -s means --source, not --staged
            UnstageCommandParser.IsUnstageOperation(
                "restore",
                new[] { "pre-command", "restore", "-s", "HEAD~1", "file.txt" })
                .ShouldBeFalse();
        }

        [TestCase]
        public void IsUnstageOperation_RestoreWithoutStaged()
        {
            UnstageCommandParser.IsUnstageOperation(
                "restore",
                new[] { "pre-command", "restore", "file.txt" })
                .ShouldBeFalse();
        }

        [TestCase]
        public void IsUnstageOperation_CheckoutHeadDashDash()
        {
            UnstageCommandParser.IsUnstageOperation(
                "checkout",
                new[] { "pre-command", "checkout", "HEAD", "--", "file.txt" })
                .ShouldBeTrue();
        }

        [TestCase]
        public void IsUnstageOperation_CheckoutNoDashDash()
        {
            UnstageCommandParser.IsUnstageOperation(
                "checkout",
                new[] { "pre-command", "checkout", "HEAD", "file.txt" })
                .ShouldBeFalse();
        }

        [TestCase]
        public void IsUnstageOperation_CheckoutBranchName()
        {
            UnstageCommandParser.IsUnstageOperation(
                "checkout",
                new[] { "pre-command", "checkout", "my-branch" })
                .ShouldBeFalse();
        }

        [TestCase]
        public void IsUnstageOperation_OtherCommand()
        {
            UnstageCommandParser.IsUnstageOperation(
                "status",
                new[] { "pre-command", "status" })
                .ShouldBeFalse();
        }

        // ── GetRestorePathspec: inline pathspecs ────────────────────────

        [TestCase]
        public void GetRestorePathspec_RestoreStagedAllFiles()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "." });
            result.Failed.ShouldBeFalse();
            result.InlinePathspecs.ShouldEqual(".");
            result.PathspecFromFile.ShouldBeNull();
        }

        [TestCase]
        public void GetRestorePathspec_RestoreStagedSpecificFiles()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "a.txt", "b.txt" });
            result.Failed.ShouldBeFalse();
            result.InlinePathspecs.ShouldEqual("a.txt\0b.txt");
        }

        [TestCase]
        public void GetRestorePathspec_RestoreStagedNoPathspec()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged" });
            result.Failed.ShouldBeFalse();
            result.InlinePathspecs.ShouldEqual(string.Empty);
            result.PathspecFromFile.ShouldBeNull();
        }

        [TestCase]
        public void GetRestorePathspec_RestoreSkipsSourceFlag()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "--source", "HEAD~1", "file.txt" });
            result.InlinePathspecs.ShouldEqual("file.txt");
        }

        [TestCase]
        public void GetRestorePathspec_RestoreSkipsSourceEqualsFlag()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "--source=HEAD~1", "file.txt" });
            result.InlinePathspecs.ShouldEqual("file.txt");
        }

        [TestCase]
        public void GetRestorePathspec_RestoreSkipsShortSourceFlag()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "-s", "HEAD~1", "file.txt" });
            result.InlinePathspecs.ShouldEqual("file.txt");
        }

        [TestCase]
        public void GetRestorePathspec_RestorePathsAfterDashDash()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "--", "a.txt", "b.txt" });
            result.InlinePathspecs.ShouldEqual("a.txt\0b.txt");
        }

        [TestCase]
        public void GetRestorePathspec_RestoreSkipsGitPid()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "--git-pid=1234", "file.txt" });
            result.InlinePathspecs.ShouldEqual("file.txt");
        }

        // ── Checkout tree-ish stripping ────────────────────────────────

        [TestCase]
        public void GetRestorePathspec_CheckoutStripsTreeish()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "checkout",
                new[] { "pre-command", "checkout", "HEAD", "--", "foo.txt" });
            result.InlinePathspecs.ShouldEqual("foo.txt");
        }

        [TestCase]
        public void GetRestorePathspec_CheckoutStripsTreeishMultiplePaths()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "checkout",
                new[] { "pre-command", "checkout", "HEAD", "--", "a.txt", "b.txt" });
            result.InlinePathspecs.ShouldEqual("a.txt\0b.txt");
        }

        [TestCase]
        public void GetRestorePathspec_CheckoutNoPaths()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "checkout",
                new[] { "pre-command", "checkout", "HEAD", "--" });
            result.InlinePathspecs.ShouldEqual(string.Empty);
        }

        [TestCase]
        public void GetRestorePathspec_CheckoutTreeishNotIncludedAsPaths()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "checkout",
                new[] { "pre-command", "checkout", "HEAD", "--", "file.txt" });
            result.InlinePathspecs.ShouldNotContain(false, "HEAD");
        }

        // ── --pathspec-from-file forwarding ───────────────────────────

        [TestCase]
        public void GetRestorePathspec_PathspecFromFileEqualsForm()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "--pathspec-from-file=list.txt" });
            result.Failed.ShouldBeFalse();
            result.PathspecFromFile.ShouldEqual("list.txt");
            result.PathspecFileNul.ShouldBeFalse();
        }

        [TestCase]
        public void GetRestorePathspec_PathspecFromFileSeparateArg()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "--pathspec-from-file", "list.txt" });
            result.Failed.ShouldBeFalse();
            result.PathspecFromFile.ShouldEqual("list.txt");
        }

        [TestCase]
        public void GetRestorePathspec_PathspecFileNulSetsFlag()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "--pathspec-from-file=list.txt", "--pathspec-file-nul" });
            result.Failed.ShouldBeFalse();
            result.PathspecFromFile.ShouldEqual("list.txt");
            result.PathspecFileNul.ShouldBeTrue();
        }

        [TestCase]
        public void GetRestorePathspec_PathspecFromFileStdinFails()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "--pathspec-from-file=-" });
            result.Failed.ShouldBeTrue();
        }

        [TestCase]
        public void GetRestorePathspec_CheckoutPathspecFromFile()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "checkout",
                new[] { "pre-command", "checkout", "HEAD", "--pathspec-from-file=list.txt", "--" });
            result.Failed.ShouldBeFalse();
            result.PathspecFromFile.ShouldEqual("list.txt");
        }

        [TestCase]
        public void GetRestorePathspec_PathspecFileNulAloneIsIgnored()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "--pathspec-file-nul", "file.txt" });
            result.InlinePathspecs.ShouldEqual("file.txt");
            result.PathspecFromFile.ShouldBeNull();
        }

        [TestCase]
        public void GetRestorePathspec_PathspecFromFileWithInlinePaths()
        {
            UnstageCommandParser.PathspecResult result = UnstageCommandParser.GetRestorePathspec(
                "restore",
                new[] { "pre-command", "restore", "--staged", "--pathspec-from-file=list.txt", "extra.txt" });
            result.Failed.ShouldBeFalse();
            result.PathspecFromFile.ShouldEqual("list.txt");
            result.InlinePathspecs.ShouldEqual("extra.txt");
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Maintenance/GitMaintenanceQueueTests.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Maintenance;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using NUnit.Framework;
using System.Collections.Generic;
using System.Threading;

namespace GVFS.UnitTests.Maintenance
{
    [TestFixture]
    public class GitMaintenanceQueueTests
    {
        private int maxWaitTime = 500;
        private ReadyFileSystem fileSystem;
        private GVFSEnlistment enlistment;
        private GVFSContext context;
        private GitObjects gitObjects;

        [TestCase]
        public void GitMaintenanceQueueEnlistmentRootReady()
        {
            this.TestSetup();

            GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context);
            queue.EnlistmentRootReady().ShouldBeTrue();

            this.fileSystem.Paths.Remove(this.enlistment.EnlistmentRoot);
            queue.EnlistmentRootReady().ShouldBeFalse();

            this.fileSystem.Paths.Remove(this.enlistment.GitObjectsRoot);
            queue.EnlistmentRootReady().ShouldBeFalse();

            this.fileSystem.Paths.Add(this.enlistment.EnlistmentRoot);
            queue.EnlistmentRootReady().ShouldBeFalse();

            this.fileSystem.Paths.Add(this.enlistment.GitObjectsRoot);
            queue.EnlistmentRootReady().ShouldBeTrue();

            queue.Stop();
        }

        [TestCase]
        public void GitMaintenanceQueueHandlesTwoJobs()
        {
            this.TestSetup();

            TestGitMaintenanceStep step1 = new TestGitMaintenanceStep(this.context);
            TestGitMaintenanceStep step2 = new TestGitMaintenanceStep(this.context);

            GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context);

            queue.TryEnqueue(step1);
            queue.TryEnqueue(step2);

            step1.EventTriggered.WaitOne(this.maxWaitTime).ShouldBeTrue();
            step2.EventTriggered.WaitOne(this.maxWaitTime).ShouldBeTrue();

            queue.Stop();

            step1.NumberOfExecutions.ShouldEqual(1);
            step2.NumberOfExecutions.ShouldEqual(1);
        }

        [TestCase]
        public void GitMaintenanceQueueStopSuceedsWhenQueueIsEmpty()
        {
            this.TestSetup();

            GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context);

            queue.Stop();

            TestGitMaintenanceStep step = new TestGitMaintenanceStep(this.context);
            queue.TryEnqueue(step).ShouldEqual(false);
        }

        [TestCase]
        public void GitMaintenanceQueueStopsJob()
        {
            this.TestSetup();

            GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context);

            // This step stops the queue after the step is started,
            // then checks if Stop() was called.
            WatchForStopStep watchForStop = new WatchForStopStep(queue, this.context);

            queue.TryEnqueue(watchForStop);
            Assert.IsTrue(watchForStop.EventTriggered.WaitOne(this.maxWaitTime));
            watchForStop.SawStopping.ShouldBeTrue();

            // Ensure we don't start a job after the Stop() call
            TestGitMaintenanceStep watchForStart = new TestGitMaintenanceStep(this.context);
            queue.TryEnqueue(watchForStart).ShouldBeFalse();

            // This only ensures the event didn't happen within maxWaitTime
            Assert.IsFalse(watchForStart.EventTriggered.WaitOne(this.maxWaitTime));

            queue.Stop();
        }

        private void TestSetup()
        {
            ITracer tracer = new MockTracer();
            this.enlistment = new MockGVFSEnlistment();

            // We need to have the EnlistmentRoot and GitObjectsRoot available for jobs to run
            this.fileSystem = new ReadyFileSystem(new string[]
            {
                this.enlistment.EnlistmentRoot,
                this.enlistment.GitObjectsRoot
            });

            this.context = new GVFSContext(tracer, this.fileSystem, null, this.enlistment);
            this.gitObjects = new MockPhysicalGitObjects(tracer, this.fileSystem, this.enlistment, null);
        }

        public class ReadyFileSystem : PhysicalFileSystem
        {
            public ReadyFileSystem(IEnumerable paths)
            {
                this.Paths = new HashSet(paths);
            }

            public HashSet Paths { get; }

            public override bool DirectoryExists(string path)
            {
                return this.Paths.Contains(path);
            }
        }

        public class TestGitMaintenanceStep : GitMaintenanceStep
        {
            public TestGitMaintenanceStep(GVFSContext context)
                : base(context, requireObjectCacheLock: true)
            {
                this.EventTriggered = new ManualResetEvent(initialState: false);
            }

            public ManualResetEvent EventTriggered { get; set; }
            public int NumberOfExecutions { get; set; }

            public override string Area => "TestGitMaintenanceStep";

            protected override void PerformMaintenance()
            {
                this.NumberOfExecutions++;
                this.EventTriggered.Set();
            }
        }

        private class WatchForStopStep : GitMaintenanceStep
        {
            public WatchForStopStep(GitMaintenanceQueue queue, GVFSContext context)
                : base(context, requireObjectCacheLock: true)
            {
                this.Queue = queue;
                this.EventTriggered = new ManualResetEvent(false);
            }

            public GitMaintenanceQueue Queue { get; set; }

            public bool SawStopping { get; private set; }

            public ManualResetEvent EventTriggered { get; private set; }

            public override string Area => "WatchForStopStep";

            protected override void PerformMaintenance()
            {
                this.Queue.Stop();

                this.SawStopping = this.Stopping;

                this.EventTriggered.Set();
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Maintenance/GitMaintenanceStepTests.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Maintenance;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using NUnit.Framework;

namespace GVFS.UnitTests.Maintenance
{
    [TestFixture]
    public class GitMaintenanceStepTests
    {
        private GVFSContext context;

        public enum WhenToStop
        {
            Never,
            BeforeGitCommand,
            DuringGitCommand
        }

        [TestCase]
        public void GitMaintenanceStepRunsGitAction()
        {
            this.TestSetup();

            CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.Never);
            step.Execute();

            step.SawWorkInvoked.ShouldBeTrue();
            step.SawEndOfMethod.ShouldBeTrue();
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void GitMaintenanceStepSkipsGitActionAfterStop()
        {
            this.TestSetup();

            CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.Never);

            step.Stop();
            step.Execute();

            step.SawWorkInvoked.ShouldBeFalse();
            step.SawEndOfMethod.ShouldBeFalse();
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void GitMaintenanceStepSkipsRunGitCommandAfterStop()
        {
            this.TestSetup();

            CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.BeforeGitCommand);

            step.Execute();

            step.SawWorkInvoked.ShouldBeFalse();
            step.SawEndOfMethod.ShouldBeFalse();
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void GitMaintenanceStepThrowsIfStoppedDuringGitCommand()
        {
            this.TestSetup();
            CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.DuringGitCommand);

            step.Execute();

            step.SawWorkInvoked.ShouldBeTrue();
            step.SawEndOfMethod.ShouldBeFalse();
        }

        private void TestSetup()
        {
            ITracer tracer = new MockTracer();
            GVFSEnlistment enlistment = new MockGVFSEnlistment();
            PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, null, null));

            this.context = new GVFSContext(tracer, fileSystem, null, enlistment);
        }

        public class CheckMethodStep : GitMaintenanceStep
        {
            private WhenToStop when;

            public CheckMethodStep(GVFSContext context, WhenToStop when)
                : base(context, requireObjectCacheLock: true)
            {
                this.when = when;
            }

            public bool SawWorkInvoked { get; set; }
            public bool SawEndOfMethod { get; set; }

            public override string Area => "CheckMethodStep";

            protected override void PerformMaintenance()
            {
                if (this.when == WhenToStop.BeforeGitCommand)
                {
                    this.Stop();
                }

                this.RunGitCommand(
                    process =>
                    {
                        this.SawWorkInvoked = true;

                        if (this.when == WhenToStop.DuringGitCommand)
                        {
                            this.Stop();
                        }

                        return null;
                    },
                    nameof(this.SawWorkInvoked));

                this.SawEndOfMethod = true;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Maintenance/LooseObjectStepTests.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Maintenance;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using GVFS.UnitTests.Mock.Git;
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;

namespace GVFS.UnitTests.Maintenance
{
    [TestFixture]
    public class LooseObjectStepTests
    {
        private const string PrunePackedCommand = "prune-packed -q";
        private string packCommand;
        private MockTracer tracer;
        private MockGitProcess gitProcess;
        private GVFSContext context;

        [TestCase]
        public void LooseObjectsIgnoreTimeRestriction()
        {
            this.TestSetup(DateTime.UtcNow);

            LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: true);
            step.Execute();

            this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(0);
            List commands = this.gitProcess.CommandsRun;
            commands.Count.ShouldEqual(2);
            commands[0].ShouldEqual(PrunePackedCommand);
            commands[1].ShouldEqual(this.packCommand);
        }

        [TestCase]
        public void LooseObjectsFailTimeRestriction()
        {
            this.TestSetup(DateTime.UtcNow);

            LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false);
            step.Execute();

            this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(1);
            List commands = this.gitProcess.CommandsRun;
            commands.Count.ShouldEqual(0);
        }

        [TestCase]
        public void LooseObjectsPassTimeRestriction()
        {
            this.TestSetup(DateTime.UtcNow.AddDays(-7));

            Mock mockChecker = new Mock();
            mockChecker.Setup(checker => checker.GetRunningGitProcessIds())
                       .Returns(Array.Empty());

            LooseObjectsStep step = new LooseObjectsStep(
                                            this.context,
                                            requireCacheLock: false,
                                            forceRun: false,
                                            gitProcessChecker: mockChecker.Object);
            step.Execute();

            mockChecker.Verify(checker => checker.GetRunningGitProcessIds(), Times.Once());

            this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(0);
            List commands = this.gitProcess.CommandsRun;
            commands.Count.ShouldEqual(2);
            commands[0].ShouldEqual(PrunePackedCommand);
            commands[1].ShouldEqual(this.packCommand);
        }

        [TestCase]
        public void LooseObjectsFailGitProcessIds()
        {
            this.TestSetup(DateTime.UtcNow.AddDays(-7));

            Mock mockChecker = new Mock();
            mockChecker.Setup(checker => checker.GetRunningGitProcessIds())
                       .Returns(new int[] { 1 });

            LooseObjectsStep step = new LooseObjectsStep(
                                            this.context,
                                            requireCacheLock: false,
                                            forceRun: false,
                                            gitProcessChecker: mockChecker.Object);
            step.Execute();

            mockChecker.Verify(checker => checker.GetRunningGitProcessIds(), Times.Once());

            this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(1);
            List commands = this.gitProcess.CommandsRun;
            commands.Count.ShouldEqual(0);
        }

        [TestCase]
        public void LooseObjectsLimitPackCount()
        {
            this.TestSetup(DateTime.UtcNow.AddDays(-7));

            // Verify with default limit
            LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false);
            step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(3);

            // Verify with limit of 2
            step.MaxLooseObjectsInPack = 2;
            step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(2);
        }

        [TestCase]
        public void SkipInvalidLooseObjects()
        {
            this.TestSetup(DateTime.UtcNow.AddDays(-7));

            // Verify with valid Objects
            LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false);
            step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(3);
            this.tracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.RelatedWarningEvents.Count.ShouldEqual(0);

            // Write an ObjectId file with an invalid name
            this.context.FileSystem.WriteAllText(Path.Combine(this.context.Enlistment.GitObjectsRoot, "AA", "NOT_A_SHA"), string.Empty);

            // Verify it wasn't added and a warning exists
            step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(3);
            this.tracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.RelatedWarningEvents.Count.ShouldEqual(1);
        }

        [TestCase]
        public void LooseObjectsCount()
        {
            this.TestSetup(DateTime.UtcNow.AddDays(-7));

            LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false);
            step.CountLooseObjects(out int count, out long size);

            count.ShouldEqual(3);
            size.ShouldEqual("one".Length + "two".Length + "three".Length);
        }

        [TestCase]
        public void LooseObjectId()
        {
            this.TestSetup(DateTime.UtcNow.AddDays(-7));

            LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false);
            string directoryName = "AB";
            string fileName = "830bb79cd4fadb2e73e780e452dc71db909001";
            step.TryGetLooseObjectId(
                directoryName,
                Path.Combine(this.context.Enlistment.GitObjectsRoot, directoryName, fileName),
                out string objectId).ShouldBeTrue();
            objectId.ShouldEqual(directoryName + fileName);

            directoryName = "AB";
            fileName = "BAD_FILE_NAME";
            step.TryGetLooseObjectId(
                directoryName,
                Path.Combine(this.context.Enlistment.GitObjectsRoot, directoryName, fileName),
                out objectId).ShouldBeFalse();
        }

        [TestCase]
        public void LooseObjectFileName()
        {
            this.TestSetup(DateTime.UtcNow);
            LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false);

            step.GetLooseObjectFileName("0123456789012345678901234567890123456789")
                .ShouldEqual(Path.Combine(this.context.Enlistment.GitObjectsRoot, "01", "23456789012345678901234567890123456789"));
        }

        private void TestSetup(DateTime lastRun)
        {
            string lastRunTime = EpochConverter.ToUnixEpochSeconds(lastRun).ToString();

            // Create GitProcess
            this.gitProcess = new MockGitProcess();
            this.gitProcess.SetExpectedCommandResult(
                PrunePackedCommand,
                () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));

            // Create enlistment using git process
            GVFSEnlistment enlistment = new MockGVFSEnlistment(this.gitProcess);

            string packPrefix = Path.Combine(enlistment.GitPackRoot, "from-loose");
            this.packCommand = $"pack-objects {packPrefix} --non-empty --window=0 --depth=0 -q";

            this.gitProcess.SetExpectedCommandResult(
                this.packCommand,
                () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));

            // Create a last run time file
            MockFile timeFile = new MockFile(Path.Combine(enlistment.GitObjectsRoot, "info", LooseObjectsStep.LooseObjectsLastRunFileName), lastRunTime);

            // Create info directory to hold last run time file
            MockDirectory infoRoot = new MockDirectory(Path.Combine(enlistment.GitObjectsRoot, "info"), null, new List() { timeFile });

            // Create Hex Folder 1 with 1 File
            MockDirectory hex1 = new MockDirectory(
                Path.Combine(enlistment.GitObjectsRoot, "AA"),
                null,
                new List()
                {
                     new MockFile(Path.Combine(enlistment.GitObjectsRoot, "AA", "1156f4f2b850673090c285289ea8475d629fe1"), "one")
                });

            // Create Hex Folder 2 with 2 Files
            MockDirectory hex2 = new MockDirectory(
                Path.Combine(enlistment.GitObjectsRoot, "F1"),
                null,
                new List()
                {
                     new MockFile(Path.Combine(enlistment.GitObjectsRoot, "F1", "1156f4f2b850673090c285289ea8475d629fe2"), "two"),
                     new MockFile(Path.Combine(enlistment.GitObjectsRoot, "F1", "1156f4f2b850673090c285289ea8475d629fe3"), "three")
                });

            // Create NonHex Folder with 4 Files
            MockDirectory nonhex = new MockDirectory(
                Path.Combine(enlistment.GitObjectsRoot, "ZZ"),
                null,
                new List()
                {
                     new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe4"), "4"),
                     new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe5"), "5"),
                     new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe6"), "6"),
                     new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe7"), "7")
                });

            MockDirectory pack = new MockDirectory(
                enlistment.GitPackRoot,
                null,
                new List());

            // Create git objects directory
            MockDirectory gitObjectsRoot = new MockDirectory(enlistment.GitObjectsRoot, new List() { infoRoot, hex1, hex2, nonhex, pack }, null);

            // Add object directory to file System
            List directories = new List() { gitObjectsRoot };
            PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, directories, null));

            // Create and return Context
            this.tracer = new MockTracer();
            this.context = new GVFSContext(this.tracer, fileSystem, repository: null, enlistment: enlistment);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Maintenance/PackfileMaintenanceStepTests.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Maintenance;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using GVFS.UnitTests.Mock.Git;
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;

namespace GVFS.UnitTests.Maintenance
{
    [TestFixture]
    public class PackfileMaintenanceStepTests
    {
        private const string StaleIdxName = "pack-stale.idx";
        private const string KeepName = "pack-3.keep";
        private MockTracer tracer;
        private MockGitProcess gitProcess;
        private GVFSContext context;

        private string ExpireCommand => $"multi-pack-index expire --object-dir=\"{this.context.Enlistment.GitObjectsRoot}\" --no-progress";
        private string VerifyCommand => $"-c core.multiPackIndex=true multi-pack-index verify --object-dir=\"{this.context.Enlistment.GitObjectsRoot}\" --no-progress";
        private string WriteCommand => $"-c core.multiPackIndex=true multi-pack-index write --object-dir=\"{this.context.Enlistment.GitObjectsRoot}\" --no-progress";
        private string RepackCommand => $"-c pack.threads=1 -c repack.packKeptObjects=true multi-pack-index repack --object-dir=\"{this.context.Enlistment.GitObjectsRoot}\" --batch-size=2g --no-progress";

        [TestCase]
        public void PackfileMaintenanceIgnoreTimeRestriction()
        {
            this.TestSetup(DateTime.UtcNow);

            PackfileMaintenanceStep step = new PackfileMaintenanceStep(this.context, requireObjectCacheLock: false, forceRun: true);
            step.Execute();

            this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(0);
            List commands = this.gitProcess.CommandsRun;
            commands.Count.ShouldEqual(5);
            commands[0].ShouldEqual(this.WriteCommand);
            commands[1].ShouldEqual(this.ExpireCommand);
            commands[2].ShouldEqual(this.VerifyCommand);
            commands[3].ShouldEqual(this.RepackCommand);
            commands[4].ShouldEqual(this.VerifyCommand);
        }

        [TestCase]
        public void PackfileMaintenanceFailTimeRestriction()
        {
            this.TestSetup(DateTime.UtcNow);

            PackfileMaintenanceStep step = new PackfileMaintenanceStep(this.context, requireObjectCacheLock: false, forceRun: false);
            step.Execute();

            this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(1);
            List commands = this.gitProcess.CommandsRun;
            commands.Count.ShouldEqual(0);
        }

        [TestCase]
        public void PackfileMaintenancePassTimeRestriction()
        {
            this.TestSetup(DateTime.UtcNow.AddDays(-1));

            Mock mockChecker = new Mock();
            mockChecker.Setup(checker => checker.GetRunningGitProcessIds())
                       .Returns(Array.Empty());

            PackfileMaintenanceStep step = new PackfileMaintenanceStep(
                                                    this.context,
                                                    requireObjectCacheLock: false,
                                                    forceRun: false,
                                                    gitProcessChecker: mockChecker.Object);

            step.Execute();

            mockChecker.Verify(checker => checker.GetRunningGitProcessIds(), Times.Once());

            this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(0);
            List commands = this.gitProcess.CommandsRun;
            commands.Count.ShouldEqual(5);
            commands[0].ShouldEqual(this.WriteCommand);
            commands[1].ShouldEqual(this.ExpireCommand);
            commands[2].ShouldEqual(this.VerifyCommand);
            commands[3].ShouldEqual(this.RepackCommand);
            commands[4].ShouldEqual(this.VerifyCommand);
        }

        [TestCase]
        public void PackfileMaintenanceFailGitProcessIds()
        {
            this.TestSetup(DateTime.UtcNow.AddDays(-1));

            Mock mockChecker = new Mock();
            mockChecker.Setup(checker => checker.GetRunningGitProcessIds())
                       .Returns(new int[] { 1 });

            PackfileMaintenanceStep step = new PackfileMaintenanceStep(
                                                    this.context,
                                                    requireObjectCacheLock: false,
                                                    forceRun: false,
                                                    gitProcessChecker: mockChecker.Object);

            step.Execute();

            mockChecker.Verify(checker => checker.GetRunningGitProcessIds(), Times.Once());

            this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(1);
            List commands = this.gitProcess.CommandsRun;
            commands.Count.ShouldEqual(0);
        }

        [TestCase]
        public void PackfileMaintenanceRewriteOnBadVerify()
        {
            this.TestSetup(DateTime.UtcNow, failOnVerify: true);

            this.gitProcess.SetExpectedCommandResult(
                this.WriteCommand,
                () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));

            PackfileMaintenanceStep step = new PackfileMaintenanceStep(this.context, requireObjectCacheLock: false, forceRun: true);
            step.Execute();

            this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(2);

            List commands = this.gitProcess.CommandsRun;
            commands.Count.ShouldEqual(7);
            commands[0].ShouldEqual(this.WriteCommand);
            commands[1].ShouldEqual(this.ExpireCommand);
            commands[2].ShouldEqual(this.VerifyCommand);
            commands[3].ShouldEqual(this.WriteCommand);
            commands[4].ShouldEqual(this.RepackCommand);
            commands[5].ShouldEqual(this.VerifyCommand);
            commands[6].ShouldEqual(this.WriteCommand);
        }

        [TestCase]
        public void CountPackFiles()
        {
            this.TestSetup(DateTime.UtcNow);

            PackfileMaintenanceStep step = new PackfileMaintenanceStep(this.context, requireObjectCacheLock: false, forceRun: true);

            step.GetPackFilesInfo(out int count, out long size, out bool hasKeep);
            count.ShouldEqual(3);
            size.ShouldEqual(11);
            hasKeep.ShouldEqual(true);

            this.context.FileSystem.DeleteFile(Path.Combine(this.context.Enlistment.GitPackRoot, KeepName));

            step.GetPackFilesInfo(out count, out size, out hasKeep);
            count.ShouldEqual(3);
            size.ShouldEqual(11);
            hasKeep.ShouldEqual(false);
        }

        [TestCase]
        public void CleanStaleIdxFiles()
        {
            this.TestSetup(DateTime.UtcNow);

            PackfileMaintenanceStep step = new PackfileMaintenanceStep(this.context, requireObjectCacheLock: false, forceRun: true);

            List staleIdx = step.CleanStaleIdxFiles(out int numDeletionBlocked);

            staleIdx.Count.ShouldEqual(1);
            staleIdx[0].ShouldEqual(StaleIdxName);

            this.context
                .FileSystem
                .FileExists(Path.Combine(this.context.Enlistment.GitPackRoot, StaleIdxName))
                .ShouldBeFalse();
        }

        private void TestSetup(DateTime lastRun, bool failOnVerify = false)
        {
            string lastRunTime = EpochConverter.ToUnixEpochSeconds(lastRun).ToString();

            this.gitProcess = new MockGitProcess();

            // Create enlistment using git process
            GVFSEnlistment enlistment = new MockGVFSEnlistment(this.gitProcess);

            // Create a last run time file
            MockFile timeFile = new MockFile(Path.Combine(enlistment.GitObjectsRoot, "info", PackfileMaintenanceStep.PackfileLastRunFileName), lastRunTime);

            // Create info directory to hold last run time file
            MockDirectory info = new MockDirectory(
                Path.Combine(enlistment.GitObjectsRoot, "info"),
                null,
                new List() { timeFile });

            // Create pack info
            MockDirectory pack = new MockDirectory(
                enlistment.GitPackRoot,
                null,
                new List()
                {
                    new MockFile(Path.Combine(enlistment.GitPackRoot, "pack-1.pack"), "one"),
                    new MockFile(Path.Combine(enlistment.GitPackRoot, "pack-1.idx"), "1"),
                    new MockFile(Path.Combine(enlistment.GitPackRoot, "pack-2.pack"), "two"),
                    new MockFile(Path.Combine(enlistment.GitPackRoot, "pack-2.idx"), "2"),
                    new MockFile(Path.Combine(enlistment.GitPackRoot, "pack-3.pack"), "three"),
                    new MockFile(Path.Combine(enlistment.GitPackRoot, "pack-3.idx"), "3"),
                    new MockFile(Path.Combine(enlistment.GitPackRoot, KeepName), string.Empty),
                    new MockFile(Path.Combine(enlistment.GitPackRoot, StaleIdxName), "4"),
                });

            // Create git objects directory
            MockDirectory gitObjectsRoot = new MockDirectory(enlistment.GitObjectsRoot, new List() { info, pack }, null);

            // Add object directory to file System
            List directories = new List() { gitObjectsRoot };
            PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, directories, null));

            MockGitRepo repository = new MockGitRepo(this.tracer, enlistment, fileSystem);

            // Create and return Context
            this.tracer = new MockTracer();
            this.context = new GVFSContext(this.tracer, fileSystem, repository, enlistment);

            this.gitProcess.SetExpectedCommandResult(
                this.WriteCommand,
                () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));
            this.gitProcess.SetExpectedCommandResult(
                this.ExpireCommand,
                () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));
            this.gitProcess.SetExpectedCommandResult(
                this.VerifyCommand,
                () => new GitProcess.Result(string.Empty, string.Empty, failOnVerify ? GitProcess.Result.GenericFailureCode : GitProcess.Result.SuccessCode));
            this.gitProcess.SetExpectedCommandResult(
                this.RepackCommand,
                () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Maintenance/PostFetchStepTests.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Maintenance;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;
using System.Collections.Generic;

namespace GVFS.UnitTests.Maintenance
{
    [TestFixture]
    public class PostFetchStepTests
    {
        private MockTracer tracer;
        private MockGitProcess gitProcess;
        private GVFSContext context;

        private string CommitGraphWriteCommand => $"commit-graph write --stdin-packs --split --size-multiple=4 --expire-time={GitProcess.ExpireTimeDateString} --object-dir \"{this.context.Enlistment.GitObjectsRoot}\"";
        private string CommitGraphVerifyCommand => $"commit-graph verify --shallow --object-dir \"{this.context.Enlistment.GitObjectsRoot}\"";

        [TestCase]
        public void DontWriteGraphOnEmptyPacks()
        {
            this.TestSetup();

            PostFetchStep step = new PostFetchStep(this.context, new List());
            step.Execute();

            this.tracer.RelatedInfoEvents.Count.ShouldEqual(1);

            List commands = this.gitProcess.CommandsRun;
            commands.Count.ShouldEqual(0);
        }

        [TestCase]
        public void WriteGraphWithPacks()
        {
            this.TestSetup();

            this.gitProcess.SetExpectedCommandResult(
                this.CommitGraphWriteCommand,
                () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));
            this.gitProcess.SetExpectedCommandResult(
                this.CommitGraphVerifyCommand,
                () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));

            PostFetchStep step = new PostFetchStep(this.context, new List() { "pack" }, requireObjectCacheLock: false);
            step.Execute();

            this.tracer.RelatedInfoEvents.Count.ShouldEqual(0);

            List commands = this.gitProcess.CommandsRun;

            commands.Count.ShouldEqual(2);
            commands[0].ShouldEqual(this.CommitGraphWriteCommand);
            commands[1].ShouldEqual(this.CommitGraphVerifyCommand);
        }

        [TestCase]
        public void RewriteCommitGraphOnBadVerify()
        {
            this.TestSetup();

            this.gitProcess.SetExpectedCommandResult(
                this.CommitGraphWriteCommand,
                () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));
            this.gitProcess.SetExpectedCommandResult(
                this.CommitGraphVerifyCommand,
                () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode));

            PostFetchStep step = new PostFetchStep(this.context, new List() { "pack" }, requireObjectCacheLock: false);
            step.Execute();

            this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0);
            this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(1);

            List commands = this.gitProcess.CommandsRun;
            commands.Count.ShouldEqual(3);
            commands[0].ShouldEqual(this.CommitGraphWriteCommand);
            commands[1].ShouldEqual(this.CommitGraphVerifyCommand);
            commands[2].ShouldEqual(this.CommitGraphWriteCommand);
        }

        private void TestSetup()
        {
            this.gitProcess = new MockGitProcess();

            // Create enlistment using git process
            GVFSEnlistment enlistment = new MockGVFSEnlistment(this.gitProcess);

            PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, null, null));

            // Create and return Context
            this.tracer = new MockTracer();
            this.context = new GVFSContext(this.tracer, fileSystem, repository: null, enlistment: enlistment);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Common/MockFileBasedLock.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Tracing;
using System;

namespace GVFS.UnitTests.Mock.Common
{
    public class MockFileBasedLock : FileBasedLock
    {
        public MockFileBasedLock(
            PhysicalFileSystem fileSystem,
            ITracer tracer,
            string lockPath)
            : base(fileSystem, tracer, lockPath)
        {
        }

        public override bool TryAcquireLock(out Exception lockException)
        {
            lockException = null;
            return true;
        }

        public override void Dispose()
        {
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Common/MockGVFSEnlistment.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.UnitTests.Mock.Git;
using System.IO;

namespace GVFS.UnitTests.Mock.Common
{
    public class MockGVFSEnlistment : GVFSEnlistment
    {
        private MockGitProcess gitProcess;

        public MockGVFSEnlistment()
            : base(Path.Combine("mock:", "path"), "mock://repoUrl", Path.Combine("mock:", "git"), authentication: null)
        {
            this.GitObjectsRoot = Path.Combine("mock:", "path", ".git", "objects");
            this.LocalObjectsRoot = this.GitObjectsRoot;
            this.GitPackRoot = Path.Combine("mock:", "path", ".git", "objects", "pack");
        }

        public MockGVFSEnlistment(string enlistmentRoot, string repoUrl, string gitBinPath, MockGitProcess gitProcess)
            : base(enlistmentRoot, repoUrl, gitBinPath, authentication: null)
        {
            this.gitProcess = gitProcess;
        }

        public MockGVFSEnlistment(MockGitProcess gitProcess)
            : this()
        {
            this.gitProcess = gitProcess;
        }

        public override string GitObjectsRoot { get; protected set; }

        public override string LocalObjectsRoot { get; protected set; }

        public override string GitPackRoot { get; protected set; }

        public override GitProcess CreateGitProcess()
        {
            return this.gitProcess ?? new MockGitProcess();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Common/MockGitStatusCache.cs
================================================
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;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Common/MockLocalGVFSConfig.cs
================================================
using GVFS.Common;
using System.Collections.Generic;

namespace GVFS.UnitTests.Mock.Common
{
    public class MockLocalGVFSConfig : LocalGVFSConfig
    {
        public MockLocalGVFSConfig()
        {
            this.Settings = new Dictionary();
        }

        private Dictionary Settings { get; set; }

        public override bool TryGetAllConfig(out Dictionary allConfig, out string error)
        {
            allConfig = new Dictionary(this.Settings);
            error = null;

            return true;
        }

        public override bool TryGetConfig(
            string name,
            out string value,
            out string error)
        {
            error = null;

            this.Settings.TryGetValue(name, out value);
            return true;
        }

        public override bool TrySetConfig(
            string name,
            string value,
            out string error)
        {
            error = null;
            this.Settings[name] = value;

            return true;
        }

        public override bool TryRemoveConfig(string name, out string error)
        {
            error = null;
            this.Settings.Remove(name);

            return true;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Common/MockLocalGVFSConfigBuilder.cs
================================================
using GVFS.Common;
using System.Collections.Generic;

namespace GVFS.UnitTests.Mock.Common
{
    public class MockLocalGVFSConfigBuilder
    {
        private string defaultRing;
        private string defaultUpgradeFeedUrl;
        private string defaultUpgradeFeedPackageName;
        private string defaultOrgServerUrl;

        private Dictionary entries;

        public MockLocalGVFSConfigBuilder(
            string defaultRing,
            string defaultUpgradeFeedUrl,
            string defaultUpgradeFeedPackageName,
            string defaultOrgServerUrl)
        {
            this.defaultRing = defaultRing;
            this.defaultUpgradeFeedUrl = defaultUpgradeFeedUrl;
            this.defaultUpgradeFeedPackageName = defaultUpgradeFeedPackageName;
            this.defaultOrgServerUrl = defaultOrgServerUrl;
            this.entries = new Dictionary();
        }

        public MockLocalGVFSConfigBuilder WithUpgradeRing(string value = null)
        {
            return this.With(GVFSConstants.LocalGVFSConfig.UpgradeRing, value ?? this.defaultRing);
        }

        public MockLocalGVFSConfigBuilder WithNoUpgradeRing()
        {
            return this.WithNo(GVFSConstants.LocalGVFSConfig.UpgradeRing);
        }

        public MockLocalGVFSConfigBuilder WithUpgradeFeedPackageName(string value = null)
        {
            return this.With(GVFSConstants.LocalGVFSConfig.UpgradeFeedPackageName, value ?? this.defaultUpgradeFeedPackageName);
        }

        public MockLocalGVFSConfigBuilder WithNoUpgradeFeedPackageName()
        {
            return this.WithNo(GVFSConstants.LocalGVFSConfig.UpgradeFeedPackageName);
        }

        public MockLocalGVFSConfigBuilder WithUpgradeFeedUrl(string value = null)
        {
            return this.With(GVFSConstants.LocalGVFSConfig.UpgradeFeedUrl, value ?? this.defaultUpgradeFeedUrl);
        }

        public MockLocalGVFSConfigBuilder WithNoUpgradeFeedUrl()
        {
            return this.WithNo(GVFSConstants.LocalGVFSConfig.UpgradeFeedUrl);
        }

        public MockLocalGVFSConfigBuilder WithOrgInfoServerUrl(string value = null)
        {
            return this.With(GVFSConstants.LocalGVFSConfig.OrgInfoServerUrl, value ?? this.defaultUpgradeFeedUrl);
        }

        public MockLocalGVFSConfigBuilder WithNoOrgInfoServerUrl()
        {
            return this.WithNo(GVFSConstants.LocalGVFSConfig.OrgInfoServerUrl);
        }

        public MockLocalGVFSConfig Build()
        {
            MockLocalGVFSConfig gvfsConfig = new MockLocalGVFSConfig();
            foreach (KeyValuePair kvp in this.entries)
            {
                gvfsConfig.TrySetConfig(kvp.Key, kvp.Value, out _);
            }

            return gvfsConfig;
        }

        private MockLocalGVFSConfigBuilder With(string key, string value)
        {
            this.entries.Add(key, value);
            return this;
        }

        private MockLocalGVFSConfigBuilder WithNo(string key)
        {
            this.entries.Remove(key);
            return this;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Common/MockPhysicalGitObjects.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Http;
using GVFS.Common.Tracing;
using System.Diagnostics;
using System.IO;

namespace GVFS.UnitTests.Mock.Common
{
    public class MockPhysicalGitObjects : GitObjects
    {
        public MockPhysicalGitObjects(ITracer tracer, PhysicalFileSystem fileSystem, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor)
            : base(tracer, enlistment, objectRequestor, fileSystem)
        {
        }

        public override string WriteLooseObject(Stream responseStream, string sha, bool overwriteExisting, byte[] sharedBuf = null)
        {
            using (StreamReader reader = new StreamReader(responseStream))
            {
                // Return "file contents" as "file name". Weird, but proves we got the right thing.
                return reader.ReadToEnd();
            }
        }

        public override string WriteTempPackFile(Stream stream)
        {
            Debug.Assert(stream != null, "WriteTempPackFile should not receive a null stream");

            using (stream)
            using (StreamReader reader = new StreamReader(stream))
            {
                // Return "file contents" as "file name". Weird, but proves we got the right thing.
                return reader.ReadToEnd();
            }
        }

        public override GitProcess.Result IndexTempPackFile(string tempPackPath, GitProcess gitProcess = null)
        {
            return new GitProcess.Result(string.Empty, "TestFailure", GitProcess.Result.GenericFailureCode);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.UnitTests.Mock.FileSystem;
using GVFS.UnitTests.Mock.Git;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Runtime.InteropServices;

namespace GVFS.UnitTests.Mock.Common
{
    public class MockPlatform : GVFSPlatform
    {
        public MockPlatform() : base(underConstruction: new UnderConstructionFlags())
        {
        }

        public string MockCurrentUser { get; set; }

        public override IKernelDriver KernelDriver => throw new NotSupportedException();

        public override IGitInstallation GitInstallation { get; } = new MockGitInstallation();

        public override IDiskLayoutUpgradeData DiskLayoutUpgrade => throw new NotSupportedException();

        public override IPlatformFileSystem FileSystem { get; } = new MockPlatformFileSystem();

        public override string Name { get => "Mock"; }

        public override string GVFSConfigPath { get => Path.Combine("mock:", LocalGVFSConfig.FileName); }

        public override bool SupportsSystemInstallLog
        {
            get
            {
                return false;
            }
        }

        public override GVFSPlatformConstants Constants { get; } = new MockPlatformConstants();

        public HashSet ActiveProcesses { get; } = new HashSet();

        public override void ConfigureVisualStudio(string gitBinPath, ITracer tracer)
        {
            throw new NotSupportedException();
        }

        public override bool TryGetGVFSHooksVersion(out string hooksVersion, out string error)
        {
            throw new NotSupportedException();
        }

        public override bool TryInstallGitCommandHooks(GVFSContext context, string executingDirectory, string hookName, string commandHookPath, out string errorMessage)
        {
            throw new NotSupportedException();
        }

        public override string GetNamedPipeName(string enlistmentRoot)
        {
            return "GVFS_Mock_PipeName";
        }

        public override string GetGVFSServiceNamedPipeName(string serviceName)
        {
            return Path.Combine("GVFS_Mock_ServicePipeName", serviceName);
        }

        public override NamedPipeServerStream CreatePipeByName(string pipeName)
        {
            throw new NotSupportedException();
        }

        public override string GetCurrentUser()
        {
            return this.MockCurrentUser;
        }

        public override string GetUserIdFromLoginSessionId(int sessionId, ITracer tracer)
        {
            return sessionId.ToString();
        }

        public override string GetOSVersionInformation()
        {
            throw new NotSupportedException();
        }

        public override string GetSecureDataRootForGVFS()
        {
            return "mock:\\dataRoot";
        }

        public override string GetSecureDataRootForGVFSComponent(string componentName)
        {
            return Path.Combine(this.GetSecureDataRootForGVFS(), componentName);
        }

        public override string GetCommonAppDataRootForGVFS()
        {
            return this.GetSecureDataRootForGVFS();
        }

        public override string GetLogsDirectoryForGVFSComponent(string componentName)
        {
            return Path.Combine(
                this.GetCommonAppDataRootForGVFS(),
                componentName,
                "Logs");
        }

        public override Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly)
        {
            return new Dictionary();
        }

        public override string GetSystemInstallerLogPath()
        {
            return "MockPath";
        }

        public override bool IsConsoleOutputRedirectedToFile()
        {
            throw new NotSupportedException();
        }

        public override bool IsElevated()
        {
            throw new NotSupportedException();
        }

        public override bool IsProcessActive(int processId)
        {
            return this.ActiveProcesses.Contains(processId);
        }

        public override void IsServiceInstalledAndRunning(string name, out bool installed, out bool running)
        {
            throw new NotSupportedException();
        }

        public override bool TryGetGVFSEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage)
        {
            throw new NotSupportedException();
        }

        public override bool TryGetDefaultLocalCacheRoot(string enlistmentRoot, out string localCacheRoot, out string localCacheRootError)
        {
            throw new NotImplementedException();
        }

        public override void StartBackgroundVFS4GProcess(ITracer tracer, string programName, string[] args)
        {
            throw new NotSupportedException();
        }

        public override void PrepareProcessToRunInBackground()
        {
            throw new NotSupportedException();
        }

        public override bool IsGitStatusCacheSupported()
        {
            return true;
        }

        public override FileBasedLock CreateFileBasedLock(PhysicalFileSystem fileSystem, ITracer tracer, string lockPath)
        {
            return new MockFileBasedLock(fileSystem, tracer, lockPath);
        }

        public override bool TryKillProcessTree(int processId, out int exitCode, out string error)
        {
            error = null;
            exitCode = 0;
            return true;
        }

        public override bool TryCopyPanicLogs(string copyToDir, out string error)
        {
            error = null;
            return true;
        }

        public class MockPlatformConstants : GVFSPlatformConstants
        {
            public override string ExecutableExtension
            {
                get { return ".mockexe"; }
            }

            public override string InstallerExtension
            {
                get { return ".mockexe"; }
            }

            public override string WorkingDirectoryBackingRootPath
            {
                get { return GVFSConstants.WorkingDirectoryRootName; }
            }

            public override string DotGVFSRoot
            {
                get { return ".mockvfsforgit"; }
            }

            public override string GVFSBinDirectoryPath
            {
                get { return Path.Combine("MockProgramFiles", this.GVFSBinDirectoryName); }
            }

            public override string GVFSBinDirectoryName
            {
                get { return "MockGVFS"; }
            }

            public override string GVFSExecutableName
            {
                get { return "MockGVFS" + this.ExecutableExtension; }
            }

            public override HashSet UpgradeBlockingProcesses
            {
                get { return new HashSet(this.PathComparer) { "GVFS", "GVFS.Mount", "git", "wish", "bash" }; }
            }

            public override bool SupportsUpgradeWhileRunning => false;

            public override int MaxPipePathLength => 250;

            public override string UpgradeInstallAdviceMessage
            {
                get { return "MockUpgradeInstallAdvice"; }
            }

            public override string UpgradeConfirmCommandMessage
            {
                get { return "MockUpgradeConfirmCommand"; }
            }

            public override string StartServiceCommandMessage
            {
                get { return "MockStartServiceCommand"; }
            }

            public override string RunUpdateMessage
            {
                get { return "MockRunUpdateMessage"; }
            }

            public override bool CaseSensitiveFileSystem => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Common/MockTracer.cs
================================================
using GVFS.Common.Tracing;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading;

namespace GVFS.UnitTests.Mock.Common
{
    public class MockTracer : ITracer
    {
        private AutoResetEvent waitEvent;

        public MockTracer()
        {
            this.waitEvent = new AutoResetEvent(false);
            this.RelatedInfoEvents = new List();
            this.RelatedWarningEvents = new List();
            this.RelatedErrorEvents = new List();
        }

        public MockTracer StartActivityTracer { get; private set; }
        public string WaitRelatedEventName { get; set; }

        public List RelatedInfoEvents { get; }
        public List RelatedWarningEvents { get; }
        public List RelatedErrorEvents { get; }

        public void WaitForRelatedEvent()
        {
            this.waitEvent.WaitOne();
        }

        public void RelatedEvent(EventLevel error, string eventName, EventMetadata metadata)
        {
            if (eventName == this.WaitRelatedEventName)
            {
                this.waitEvent.Set();
            }
        }

        public void RelatedEvent(EventLevel error, string eventName, EventMetadata metadata, Keywords keyword)
        {
            if (eventName == this.WaitRelatedEventName)
            {
                this.waitEvent.Set();
            }
        }

        public void RelatedInfo(string message)
        {
            this.RelatedInfoEvents.Add(message);
        }

        public void RelatedInfo(EventMetadata metadata, string message)
        {
            metadata[TracingConstants.MessageKey.InfoMessage] = message;
            this.RelatedInfoEvents.Add(JsonConvert.SerializeObject(metadata));
        }

        public void RelatedInfo(string format, params object[] args)
        {
            this.RelatedInfo(string.Format(format, args));
        }

        public void RelatedWarning(EventMetadata metadata, string message)
        {
            if (metadata != null)
            {
                metadata[TracingConstants.MessageKey.WarningMessage] = message;
                this.RelatedWarningEvents.Add(JsonConvert.SerializeObject(metadata));
            }
            else if (message != null)
            {
                this.RelatedWarning(message);
            }
        }

        public void RelatedWarning(EventMetadata metadata, string message, Keywords keyword)
        {
            this.RelatedWarning(metadata, message);
        }

        public void RelatedWarning(string message)
        {
            this.RelatedWarningEvents.Add(message);
        }

        public void RelatedWarning(string format, params object[] args)
        {
            this.RelatedWarningEvents.Add(string.Format(format, args));
        }

        public void RelatedError(EventMetadata metadata, string message)
        {
            metadata[TracingConstants.MessageKey.ErrorMessage] = message;
            this.RelatedErrorEvents.Add(JsonConvert.SerializeObject(metadata));
        }

        public void RelatedError(EventMetadata metadata, string message, Keywords keyword)
        {
            this.RelatedError(metadata, message);
        }

        public void RelatedError(string message)
        {
            this.RelatedErrorEvents.Add(message);
        }

        public void RelatedError(string format, params object[] args)
        {
            this.RelatedErrorEvents.Add(string.Format(format, args));
        }

        public ITracer StartActivity(string activityName, EventLevel level)
        {
            return this.StartActivity(activityName, level, metadata: null);
        }

        public ITracer StartActivity(string activityName, EventLevel level, EventMetadata metadata)
        {
            return this.StartActivity(activityName, level, Keywords.None, metadata);
        }

        public ITracer StartActivity(string activityName, EventLevel level, Keywords startStopKeywords, EventMetadata metadata)
        {
            this.StartActivityTracer = this.StartActivityTracer ?? new MockTracer();
            return this.StartActivityTracer;
        }

        public TimeSpan Stop(EventMetadata metadata)
        {
            return TimeSpan.Zero;
        }

        public void SetGitCommandSessionId(string sessionId)
        {
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.waitEvent != null)
                {
                    this.waitEvent.Dispose();
                    this.waitEvent = null;
                }
            }
        }
    }
}

================================================
FILE: GVFS/GVFS.UnitTests/Mock/Common/Tracing/MockListener.cs
================================================
using GVFS.Common.Tracing;
using System.Collections.Generic;

namespace GVFS.UnitTests.Mock.Common.Tracing
{
    public class MockListener : EventListener
    {
        public MockListener(EventLevel maxVerbosity, Keywords keywordFilter)
            : base(maxVerbosity, keywordFilter, null)
        {
        }

        public List EventNamesRead { get; set; } = new List();

        protected override void RecordMessageInternal(TraceEventMessage message)
        {
            this.EventNamesRead.Add(message.EventName);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/FileSystem/ConfigurableFileSystem.cs
================================================
using GVFS.Common.FileSystem;
using GVFS.Tests.Should;
using System.Collections.Generic;
using System.IO;

namespace GVFS.UnitTests.Mock.FileSystem
{
    public class ConfigurableFileSystem : PhysicalFileSystem
    {
        public ConfigurableFileSystem()
        {
            this.ExpectedFiles = new Dictionary();
            this.ExpectedDirectories = new HashSet();
        }

        public Dictionary ExpectedFiles { get; }
        public HashSet ExpectedDirectories { get; }

        public override void CreateDirectory(string path)
        {
        }

        public override void MoveAndOverwriteFile(string sourceFileName, string destinationFilename)
        {
            ReusableMemoryStream source;
            this.ExpectedFiles.TryGetValue(sourceFileName, out source).ShouldEqual(true, "Source file does not exist: " + sourceFileName);
            this.ExpectedFiles.ContainsKey(destinationFilename).ShouldEqual(true, "MoveAndOverwriteFile expects the destination file to exist: " + destinationFilename);

            this.ExpectedFiles.Remove(sourceFileName);
            this.ExpectedFiles[destinationFilename] = source;
        }

        public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk)
        {
            ReusableMemoryStream stream;
            this.ExpectedFiles.TryGetValue(path, out stream).ShouldEqual(true, "Unexpected access of file: " + path);
            return stream;
        }

        public override bool FileExists(string path)
        {
            return this.ExpectedFiles.ContainsKey(path);
        }

        public override bool DirectoryExists(string path)
        {
            return this.ExpectedDirectories.Contains(path);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/FileSystem/MockDirectory.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Tests.Should;
using System;
using System.Collections.Generic;
using System.IO;

namespace GVFS.UnitTests.Mock.FileSystem
{
    public class MockDirectory
    {
        public MockDirectory(string fullName, IEnumerable folders, IEnumerable files)
        {
            this.FullName = fullName;
            this.Name = Path.GetFileName(this.FullName);

            this.Directories = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
            this.Files = new Dictionary(StringComparer.InvariantCultureIgnoreCase);

            if (folders != null)
            {
                foreach (MockDirectory folder in folders)
                {
                    this.Directories[folder.FullName] = folder;
                }
            }

            if (files != null)
            {
                foreach (MockFile file in files)
                {
                    this.Files[file.FullName] = file;
                }
            }

            this.FileProperties = FileProperties.DefaultDirectory;
        }

        public string FullName { get; private set; }
        public string Name { get; private set; }
        public Dictionary Directories { get; private set; }
        public Dictionary Files { get; private set; }
        public FileProperties FileProperties { get; set; }

        public MockFile FindFile(string path)
        {
            MockFile file;
            if (this.Files.TryGetValue(path, out file))
            {
                return file;
            }

            foreach (MockDirectory directory in this.Directories.Values)
            {
                file = directory.FindFile(path);
                if (file != null)
                {
                    return file;
                }
            }

            return null;
        }

        public void AddOrOverwriteFile(MockFile file, string path)
        {
            string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar));
            MockDirectory parentDirectory = this.FindDirectory(parentPath);

            if (parentDirectory == null)
            {
                throw new IOException();
            }

            MockFile existingFileAtPath = parentDirectory.FindFile(path);

            if (existingFileAtPath != null)
            {
                parentDirectory.Files.Remove(path);
            }

            parentDirectory.Files.Add(file.FullName, file);
        }

        public void AddFile(MockFile file, string path)
        {
            string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar));
            MockDirectory parentDirectory = this.FindDirectory(parentPath);

            if (parentDirectory == null)
            {
                throw new IOException();
            }

            MockFile existingFileAtPath = parentDirectory.FindFile(path);
            existingFileAtPath.ShouldBeNull();

            parentDirectory.Files.Add(file.FullName, file);
        }

        public void RemoveFile(string path)
        {
            MockFile file;
            if (this.Files.TryGetValue(path, out file))
            {
                this.Files.Remove(path);
                return;
            }

            foreach (MockDirectory directory in this.Directories.Values)
            {
                file = directory.FindFile(path);
                if (file != null)
                {
                    directory.RemoveFile(path);
                    return;
                }
            }
        }

        public MockDirectory FindDirectory(string path)
        {
            if (path.Equals(this.FullName, StringComparison.InvariantCultureIgnoreCase))
            {
                return this;
            }

            MockDirectory foundDirectory;
            if (this.Directories.TryGetValue(path, out foundDirectory))
            {
                return foundDirectory;
            }

            foreach (MockDirectory subDirectory in this.Directories.Values)
            {
                foundDirectory = subDirectory.FindDirectory(path);
                if (foundDirectory != null)
                {
                    return foundDirectory;
                }
            }

            return null;
        }

        public MockFile CreateFile(string path)
        {
            return this.CreateFile(path, string.Empty);
        }

        public MockFile CreateFile(string path, string contents, bool createDirectories = false)
        {
            string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar));
            MockDirectory parentDirectory = this.FindDirectory(parentPath);
            if (createDirectories)
            {
                if (parentDirectory == null)
                {
                    parentDirectory = this.CreateDirectory(parentPath);
                }
            }
            else
            {
                parentDirectory.ShouldNotBeNull();
            }

            MockFile newFile = new MockFile(path, contents);
            parentDirectory.Files.Add(newFile.FullName, newFile);

            return newFile;
        }

        public MockDirectory CreateDirectory(string path)
        {
            int lastSlashIdx = path.LastIndexOf(Path.DirectorySeparatorChar);

            if (lastSlashIdx <= 0)
            {
                return this;
            }

            string parentPath = path.Substring(0, lastSlashIdx);
            MockDirectory parentDirectory = this.FindDirectory(parentPath);
            if (parentDirectory == null)
            {
                parentDirectory = this.CreateDirectory(parentPath);
            }

            MockDirectory newDirectory;
            if (!parentDirectory.Directories.TryGetValue(path, out newDirectory))
            {
                newDirectory = new MockDirectory(path, null, null);
                parentDirectory.Directories.Add(newDirectory.FullName, newDirectory);
            }

            return newDirectory;
        }

        public void DeleteDirectory(string path)
        {
            if (path.Equals(this.FullName, StringComparison.InvariantCultureIgnoreCase))
            {
                throw new NotSupportedException();
            }

            MockDirectory foundDirectory;
            if (this.Directories.TryGetValue(path, out foundDirectory))
            {
                this.Directories.Remove(path);
            }
            else
            {
                foreach (MockDirectory subDirectory in this.Directories.Values)
                {
                    foundDirectory = subDirectory.FindDirectory(path);
                    if (foundDirectory != null)
                    {
                        subDirectory.DeleteDirectory(path);
                        return;
                    }
                }
            }
        }

        public void MoveDirectory(string sourcePath, string targetPath)
        {
            MockDirectory sourceDirectory;
            MockDirectory sourceDirectoryParent;
            this.TryGetDirectoryAndParent(sourcePath, out sourceDirectory, out sourceDirectoryParent).ShouldEqual(true);

            int endPathIndex = targetPath.LastIndexOf(Path.DirectorySeparatorChar);
            string targetDirectoryPath = targetPath.Substring(0, endPathIndex);

            MockDirectory targetDirectory = this.FindDirectory(targetDirectoryPath);
            targetDirectory.ShouldNotBeNull();

            sourceDirectoryParent.RemoveDirectory(sourceDirectory);

            sourceDirectory.FullName = targetPath;

            targetDirectory.AddDirectory(sourceDirectory);
        }

        public void RemoveDirectory(MockDirectory directory)
        {
            this.Directories.ContainsKey(directory.FullName).ShouldEqual(true);
            this.Directories.Remove(directory.FullName);
        }

        private void AddDirectory(MockDirectory directory)
        {
            if (this.Directories.ContainsKey(directory.FullName))
            {
                MockDirectory oldDirectory = this.Directories[directory.FullName];
                foreach (MockFile newFile in directory.Files.Values)
                {
                    newFile.FullName = Path.Combine(oldDirectory.FullName, newFile.Name);
                    oldDirectory.AddOrOverwriteFile(newFile, newFile.FullName);
                }

                foreach (MockDirectory newDirectory in directory.Directories.Values)
                {
                    newDirectory.FullName = Path.Combine(oldDirectory.FullName, newDirectory.Name);
                    this.AddDirectory(newDirectory);
                }
            }
            else
            {
                this.Directories.Add(directory.FullName, directory);
            }
        }

        private bool TryGetDirectoryAndParent(string path, out MockDirectory directory, out MockDirectory parentDirectory)
        {
            if (this.Directories.TryGetValue(path, out directory))
            {
                parentDirectory = this;
                return true;
            }
            else
            {
                string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar));
                parentDirectory = this.FindDirectory(parentPath);
                if (parentDirectory != null)
                {
                    foreach (MockDirectory subDirectory in this.Directories.Values)
                    {
                        directory = subDirectory.FindDirectory(path);
                        if (directory != null)
                        {
                            return true;
                        }
                    }
                }
            }

            directory = null;
            parentDirectory = null;
            return false;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/FileSystem/MockFile.cs
================================================
using GVFS.Common.FileSystem;
using System;
using System.IO;

namespace GVFS.UnitTests.Mock.FileSystem
{
    public class MockFile
    {
        private ReusableMemoryStream contentStream;
        private FileProperties fileProperties;

        public MockFile(string fullName, string contents)
        {
            this.FullName = fullName;
            this.Name = Path.GetFileName(this.FullName);

            this.FileProperties = FileProperties.DefaultFile;

            this.contentStream = new ReusableMemoryStream(contents);
        }

        public MockFile(string fullName, byte[] contents)
        {
            this.FullName = fullName;
            this.Name = Path.GetFileName(this.FullName);

            this.FileProperties = FileProperties.DefaultFile;

            this.contentStream = new ReusableMemoryStream(contents);
        }

        public event Action Changed;

        public string FullName { get; set; }
        public string Name { get; set; }
        public FileProperties FileProperties
        {
            get
            {
                // The underlying content stream is the correct/true source of the file length
                // Create a new copy of the properties to make sure the length is set correctly.
                FileProperties newProperties = new FileProperties(
                    this.fileProperties.FileAttributes,
                    this.fileProperties.CreationTimeUTC,
                    this.fileProperties.LastAccessTimeUTC,
                    this.fileProperties.LastWriteTimeUTC,
                    this.contentStream.Length);

                this.fileProperties = newProperties;
                return this.fileProperties;
            }

            set
            {
                this.fileProperties = value;
                if (this.Changed != null)
                {
                    this.Changed();
                }
            }
        }

        public Stream GetContentStream()
        {
            this.contentStream.Position = 0;
            return this.contentStream;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/FileSystem/MockFileSystem.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

namespace GVFS.UnitTests.Mock.FileSystem
{
    public class MockFileSystem : PhysicalFileSystem
    {
        public MockFileSystem(MockDirectory rootDirectory)
        {
            this.RootDirectory = rootDirectory;
            this.DeleteNonExistentFileThrowsException = true;
            this.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed = true;
        }

        public MockDirectory RootDirectory { get; private set; }

        public bool DeleteFileThrowsException { get; set; }
        public Exception ExceptionThrownByCreateDirectory { get; set; }

        public bool TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed { 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 void DeleteDirectory(string path, bool recursive = true, bool ignoreDirectoryDeleteExceptions = false)
        {
            if (!recursive)
            {
                throw new NotImplementedException();
            }

            this.RootDirectory.DeleteDirectory(path);
        }

        public override bool FileExists(string path)
        {
            return this.RootDirectory.FindFile(path) != null;
        }

        public override bool DirectoryExists(string path)
        {
            return this.RootDirectory.FindDirectory(path) != null;
        }

        public override void CopyFile(string sourcePath, string destinationPath, bool overwrite)
        {
            throw new NotImplementedException();
        }

        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);
        }

        public override void MoveAndOverwriteFile(string sourcePath, string destinationPath)
        {
            if (sourcePath == null || destinationPath == null)
            {
                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)
            {
                this.RootDirectory.RemoveFile(destinationPath);
            }

            this.WriteAllText(destinationPath, this.ReadAllText(sourcePath));
            this.RootDirectory.RemoveFile(sourcePath);
        }

        public override bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage)
        {
            normalizedPath = path;
            errorMessage = null;
            return true;
        }

        public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk)
        {
            MockFile file = this.RootDirectory.FindFile(path);
            if (fileMode == FileMode.Create)
            {
                if (file != null)
                {
                    this.RootDirectory.RemoveFile(path);
                }

                return this.CreateAndOpenFileStream(path);
            }

            if (fileMode == FileMode.OpenOrCreate)
            {
                if (file == null)
                {
                    return this.CreateAndOpenFileStream(path);
                }
            }
            else
            {
                file.ShouldNotBeNull();
            }

            return file.GetContentStream();
        }

        public override void FlushFileBuffers(string path)
        {
            throw new NotImplementedException();
        }

        public override void WriteAllText(string path, string contents)
        {
            MockFile file = new MockFile(path, contents);
            this.RootDirectory.AddOrOverwriteFile(file, path);
        }

        public override string ReadAllText(string path)
        {
            MockFile file = this.RootDirectory.FindFile(path);

            using (StreamReader reader = new StreamReader(file.GetContentStream()))
            {
                return reader.ReadToEnd();
            }
        }

        public override byte[] ReadAllBytes(string path)
        {
            MockFile file = this.RootDirectory.FindFile(path);

            using (Stream s = file.GetContentStream())
            {
                int count = (int)s.Length;

                int pos = 0;
                byte[] result = new byte[count];
                while (count > 0)
                {
                    int n = s.Read(result, pos, count);
                    if (n == 0)
                    {
                        throw new IOException("Unexpected end of stream");
                    }

                    pos += n;
                    count -= n;
                }

                return result;
            }
        }

        public override IEnumerable ReadLines(string path)
        {
            MockFile file = this.RootDirectory.FindFile(path);
            using (StreamReader reader = new StreamReader(file.GetContentStream()))
            {
                while (!reader.EndOfStream)
                {
                    yield return reader.ReadLine();
                }
            }
        }

        public override void CreateDirectory(string path)
        {
            if (this.ExceptionThrownByCreateDirectory != null)
            {
                throw this.ExceptionThrownByCreateDirectory;
            }

            this.RootDirectory.CreateDirectory(path);
        }

        public override bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error)
        {
            throw new NotImplementedException();
        }

        public override bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error)
        {
            error = null;

            if (this.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed)
            {
                // TryCreateOrUpdateDirectoryToAdminModifyPermissions is typically called for paths in C:\ProgramData\GVFS,
                // if it's called for one of those paths remap the paths to be inside the mock: root
                string mockDirectoryPath = directoryPath;
                string gvfsProgramData = @"C:\ProgramData\GVFS";
                if (directoryPath.StartsWith(gvfsProgramData, GVFSPlatform.Instance.Constants.PathComparison))
                {
                    mockDirectoryPath = mockDirectoryPath.Substring(gvfsProgramData.Length);
                    mockDirectoryPath = "mock:" + mockDirectoryPath;
                }

                this.RootDirectory.CreateDirectory(mockDirectoryPath);
                return true;
            }

            return false;
        }

        public override IEnumerable ItemsInDirectory(string path)
        {
            MockDirectory directory = this.RootDirectory.FindDirectory(path);
            directory.ShouldNotBeNull();

            foreach (MockDirectory subDirectory in directory.Directories.Values)
            {
                yield return new DirectoryItemInfo()
                {
                    Name = subDirectory.Name,
                    FullName = subDirectory.FullName,
                    IsDirectory = true
                };
            }

            foreach (MockFile file in directory.Files.Values)
            {
                yield return new DirectoryItemInfo()
                {
                    FullName = file.FullName,
                    Name = file.Name,
                    IsDirectory = false,
                    Length = file.FileProperties.Length
                };
            }
        }

        public override IEnumerable EnumerateDirectories(string path)
        {
            MockDirectory directory = this.RootDirectory.FindDirectory(path);
            directory.ShouldNotBeNull();

            if (directory != null)
            {
                foreach (MockDirectory subDirectory in directory.Directories.Values)
                {
                    yield return subDirectory.FullName;
                }
            }
        }

        public override FileProperties GetFileProperties(string path)
        {
            MockFile file = this.RootDirectory.FindFile(path);
            if (file != null)
            {
                return file.FileProperties;
            }
            else
            {
                return FileProperties.DefaultFile;
            }
        }

        public override FileAttributes GetAttributes(string path)
        {
            return FileAttributes.Normal;
        }

        public override void SetAttributes(string path, FileAttributes fileAttributes)
        {
        }

        public override void MoveFile(string sourcePath, string targetPath)
        {
            if (this.AllowMoveFile)
            {
                return;
            }
            else
            {
                throw new NotImplementedException();
            }
        }

        public override string[] GetFiles(string directoryPath, string mask)
        {
            if (!mask.Equals("*"))
            {
                throw new NotImplementedException();
            }

            MockDirectory directory = this.RootDirectory.FindDirectory(directoryPath);
            directory.ShouldNotBeNull();

            List files = new List();
            foreach (MockFile file in directory.Files.Values)
            {
                files.Add(file.FullName);
            }

            return files.ToArray();
        }

        public override FileVersionInfo GetVersionInfo(string path)
        {
            throw new NotImplementedException();
        }

        public override bool FileVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2)
        {
            throw new NotImplementedException();
        }

        public override bool ProductVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2)
        {
            throw new NotImplementedException();
        }

        private Stream CreateAndOpenFileStream(string path)
        {
            MockFile file = this.RootDirectory.CreateFile(path);
            file.ShouldNotBeNull();

            return this.OpenFileStream(path, FileMode.Open, (FileAccess)NativeMethods.FileAccess.FILE_GENERIC_READ, FileShare.Read, callFlushFileBuffers: false);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/FileSystem/MockFileSystemCallbacks.cs
================================================
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Common.Git;
using GVFS.Virtualization;
using GVFS.Virtualization.Background;
using GVFS.Virtualization.BlobSize;
using GVFS.Virtualization.FileSystem;
using GVFS.Virtualization.Projection;

namespace GVFS.UnitTests.Mock.FileSystem
{
    public class MockFileSystemCallbacks : FileSystemCallbacks
    {
        public MockFileSystemCallbacks(
            GVFSContext context,
            GVFSGitObjects gitObjects,
            RepoMetadata repoMetadata,
            BlobSizes blobSizes,
            GitIndexProjection gitIndexProjection,
            BackgroundFileSystemTaskRunner backgroundFileSystemTaskRunner,
            FileSystemVirtualizer fileSystemVirtualizer,
            IPlaceholderCollection placeholderDatabase,
            ISparseCollection sparseCollection)
            : base(context, gitObjects, repoMetadata, blobSizes, gitIndexProjection, backgroundFileSystemTaskRunner, fileSystemVirtualizer, placeholderDatabase, sparseCollection)
        {
        }

        public int OnFileRenamedCallCount { get; set; }
        public int OnFolderRenamedCallCount { get; set; }
        public int OnIndexFileChangeCallCount { get; set; }
        public int OnLogsHeadChangeCallCount { get; set; }

        public override void OnFileRenamed(string oldRelativePath, string newRelativePath)
        {
            this.OnFileRenamedCallCount++;
        }

        public override void OnFolderRenamed(string oldRelativePath, string newRelativePath)
        {
            this.OnFolderRenamedCallCount++;
        }

        public override void OnIndexFileChange()
        {
            this.OnIndexFileChangeCallCount++;
        }

        public override void OnLogsHeadChange()
        {
            this.OnLogsHeadChangeCallCount++;
        }

        public void ResetCalls()
        {
            this.OnFileRenamedCallCount = 0;
            this.OnIndexFileChangeCallCount = 0;
            this.OnLogsHeadChangeCallCount = 0;
            this.OnFolderRenamedCallCount = 0;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/FileSystem/MockFileSystemWithCallbacks.cs
================================================
using GVFS.Common.FileSystem;
using System;
using System.IO;

namespace GVFS.UnitTests.Mock.FileSystem
{
    public class MockFileSystemWithCallbacks : PhysicalFileSystem
    {
        public Func OnFileExists { get; set; }

        public Func OnOpenFileStream { get; set; }

        public Action OnMoveFile { get; set; }

        public override FileProperties GetFileProperties(string path)
        {
            throw new InvalidOperationException("GetFileProperties has not been implemented.");
        }

        public override bool FileExists(string path)
        {
            if (this.OnFileExists == null)
            {
                throw new InvalidOperationException("OnFileExists should be set if it is expected to be called.");
            }

            return this.OnFileExists(path);
        }

        public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk)
        {
            if (this.OnOpenFileStream == null)
            {
                throw new InvalidOperationException("OnOpenFileStream should be set if it is expected to be called.");
            }

            return this.OnOpenFileStream(path, fileMode, fileAccess);
        }

        public override void WriteAllText(string path, string contents)
        {
        }

        public override string ReadAllText(string path)
        {
            throw new InvalidOperationException("ReadAllText has not been implemented.");
        }

        public override void DeleteFile(string path)
        {
        }

        public override void DeleteDirectory(string path, bool recursive = true, bool ignoreDirectoryDeleteExceptions = false)
        {
            throw new InvalidOperationException("DeleteDirectory has not been implemented.");
        }

        public override void CreateDirectory(string path)
        {
        }

        public override FileAttributes GetAttributes(string path)
        {
            return FileAttributes.Normal;
        }

        public override void SetAttributes(string path, FileAttributes fileAttributes)
        {
        }

        public override void MoveFile(string sourcePath, string targetPath)
        {
            if (this.OnMoveFile == null)
            {
                throw new InvalidOperationException("OnMoveFile should be set if it is expected to be called.");
            }

            this.OnMoveFile(sourcePath, targetPath);
        }

        public override string[] GetFiles(string directoryPath, string mask)
        {
            throw new NotImplementedException();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/FileSystem/MockPlatformFileSystem.cs
================================================
using GVFS.Common.FileSystem;
using GVFS.Common.Tracing;
using System;

namespace GVFS.UnitTests.Mock.FileSystem
{
    public class MockPlatformFileSystem : IPlatformFileSystem
    {
        public bool SupportsFileMode { get; } = true;

        public void FlushFileBuffers(string path)
        {
            throw new NotSupportedException();
        }

        public void MoveAndOverwriteFile(string sourceFileName, string destinationFilename)
        {
            throw new NotSupportedException();
        }

        public void ChangeMode(string path, ushort mode)
        {
            throw new NotSupportedException();
        }

        public bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage)
        {
            errorMessage = null;
            normalizedPath = path;
            return true;
        }

        public void SetDirectoryLastWriteTime(string path, DateTime lastWriteTime, out bool directoryExists)
        {
            throw new NotSupportedException();
        }

        public bool HydrateFile(string fileName, byte[] buffer)
        {
            throw new NotSupportedException();
        }

        public bool IsExecutable(string fileName)
        {
            throw new NotSupportedException();
        }

        public bool IsSocket(string fileName)
        {
            throw new NotSupportedException();
        }

        public bool TryCreateDirectoryAccessibleByAuthUsers(string directoryPath, out string error, ITracer tracer = null)
        {
            throw new NotSupportedException();
        }

        public bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error)
        {
            throw new NotSupportedException();
        }

        public bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error)
        {
            throw new NotSupportedException();
        }

        public bool IsFileSystemSupported(string path, out string error)
        {
            error = null;
            return true;
        }

        public void EnsureDirectoryIsOwnedByCurrentUser(string workingDirectoryRoot)
        {
            throw new NotSupportedException();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Git/MockBatchHttpGitObjects.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Http;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;

namespace GVFS.UnitTests.Mock.Git
{
    public class MockBatchHttpGitObjects : GitObjectsHttpRequestor
    {
        private Func objectResolver;

        public MockBatchHttpGitObjects(ITracer tracer, Enlistment enlistment, Func objectResolver)
            : base(tracer, enlistment, new MockCacheServerInfo(), new RetryConfig())
        {
            this.objectResolver = objectResolver;
        }

        public override List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public override GitRefs QueryInfoRefs(string branch)
        {
            throw new NotImplementedException();
        }

        public override RetryWrapper.InvocationResult TryDownloadObjects(
            Func> objectIdGenerator,
            Func.CallbackResult> onSuccess,
            Action.ErrorEventArgs> onFailure,
            bool preferBatchedLooseObjects)
        {
            return this.TryDownloadObjects(objectIdGenerator(), onSuccess, onFailure, preferBatchedLooseObjects);
        }

        public override RetryWrapper.InvocationResult TryDownloadObjects(
            IEnumerable objectIds,
            Func.CallbackResult> onSuccess,
            Action.ErrorEventArgs> onFailure,
            bool preferBatchedLooseObjects)
        {
            return this.StreamObjects(objectIds, onSuccess, onFailure);
        }

        private RetryWrapper.InvocationResult StreamObjects(
            IEnumerable objectIds,
            Func.CallbackResult> onSuccess,
            Action.ErrorEventArgs> onFailure)
        {
            for (int i = 0; i < this.RetryConfig.MaxAttempts; ++i)
            {
                try
                {
                    using (ReusableMemoryStream mem = new ReusableMemoryStream(string.Empty))
                    using (BinaryWriter writer = new BinaryWriter(mem))
                    {
                        writer.Write(new byte[] { (byte)'G', (byte)'V', (byte)'F', (byte)'S', (byte)' ', 1 });

                        foreach (string objectId in objectIds)
                        {
                            string contents = this.objectResolver(objectId);
                            if (!string.IsNullOrEmpty(contents))
                            {
                                writer.Write(this.SHA1BytesFromString(objectId));
                                byte[] bytes = Encoding.UTF8.GetBytes(contents);
                                writer.Write((long)bytes.Length);
                                writer.Write(bytes);
                            }
                            else
                            {
                                writer.Write(new byte[20]);
                                writer.Write(0L);
                            }
                        }

                        writer.Write(new byte[20]);
                        writer.Flush();
                        mem.Seek(0, SeekOrigin.Begin);

                        using (GitEndPointResponseData response = new GitEndPointResponseData(
                            HttpStatusCode.OK,
                            GVFSConstants.MediaTypes.CustomLooseObjectsMediaType,
                            mem,
                            message: null,
                            onResponseDisposed: null))
                        {
                            RetryWrapper.CallbackResult result = onSuccess(1, response);
                            return new RetryWrapper.InvocationResult(1, true, result.Result);
                        }
                    }
                }
                catch
                {
                    continue;
                }
            }

            return new RetryWrapper.InvocationResult(this.RetryConfig.MaxAttempts, null);
        }

        private byte[] SHA1BytesFromString(string s)
        {
            s.Length.ShouldEqual(40);

            byte[] output = new byte[20];
            for (int x = 0; x < s.Length; x += 2)
            {
                output[x / 2] = Convert.ToByte(s.Substring(x, 2), 16);
            }

            return output;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Git/MockGVFSGitObjects.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Http;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace GVFS.UnitTests.Mock.Git
{
    public class MockGVFSGitObjects : GVFSGitObjects
    {
        public const uint DefaultFileLength = 100;
        private GVFSContext context;

        public MockGVFSGitObjects(GVFSContext context, GitObjectsHttpRequestor httpGitObjects)
            : base(context, httpGitObjects)
        {
            this.context = context;
        }

        public bool CancelTryCopyBlobContentStream { get; set; }
        public uint FileLength { get; set; } = DefaultFileLength;

        public override bool TryDownloadCommit(string objectSha)
        {
            RetryWrapper.InvocationResult result = this.GitObjectRequestor.TryDownloadObjects(
                new[] { objectSha },
                onSuccess: (tryCount, response) =>
                {
                    // Add the contents to the mock repo
                    ((MockGitRepo)this.Context.Repository).AddBlob(objectSha, "DownloadedFile", response.RetryableReadToEnd());

                    return new RetryWrapper.CallbackResult(new GitObjectsHttpRequestor.GitObjectTaskResult(true));
                },
                onFailure: null,
                preferBatchedLooseObjects: false);

            return result.Succeeded && result.Result.Success;
        }

        public override bool TryCopyBlobContentStream(
            string sha,
            CancellationToken cancellationToken,
            RequestSource requestSource,
            Action writeAction)
        {
            if (this.CancelTryCopyBlobContentStream)
            {
                throw new OperationCanceledException();
            }

            writeAction(
                new MemoryStream(new byte[this.FileLength]),
                this.FileLength);

            return true;
        }

        public override string[] ReadPackFileNames(string packFolderPath, string prefixFilter = "")
        {
            return Array.Empty();
        }

        public override GitProcess.Result IndexPackFile(string packfilePath, GitProcess process)
        {
            return new GitProcess.Result("mocked", null, 0);
        }

        public override void DeleteStaleTempPrefetchPackAndIdxs()
        {
        }

        public override bool TryDownloadPrefetchPacks(GitProcess gitProcess, long latestTimestamp, bool trustPackIndexes, out List packIndexes)
        {
            packIndexes = new List();
            return true;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Git/MockGitInstallation.cs
================================================
using GVFS.Common.Git;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GVFS.UnitTests.Mock.Git
{
    public class MockGitInstallation : IGitInstallation
    {
        public bool GitExists(string gitBinPath)
        {
            return false;
        }

        public string GetInstalledGitBinPath()
        {
            return null;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Git/MockGitProcess.cs
================================================
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace GVFS.UnitTests.Mock.Git
{
    public class MockGitProcess : GitProcess
    {
        private List expectedCommandInfos = new List();

        public MockGitProcess()
            : base(new MockGVFSEnlistment())
        {
            this.CommandsRun = new List();
            this.StoredCredentials = new Dictionary(StringComparer.OrdinalIgnoreCase);
            this.CredentialApprovals = new Dictionary>();
            this.CredentialRejections = new Dictionary>();
        }

        public List CommandsRun { get; }
        public bool ShouldFail { get; set; }
        public Dictionary StoredCredentials { get; }
        public Dictionary> CredentialApprovals { get; }
        public Dictionary> CredentialRejections { get; }

        public void SetExpectedCommandResult(string command, Func result, bool matchPrefix = false)
        {
            CommandInfo commandInfo = new CommandInfo(command, result, matchPrefix);
            this.expectedCommandInfos.Add(commandInfo);
        }

        public override bool TryStoreCredential(ITracer tracer, string repoUrl, string username, string password, out string error)
        {
            Credential credential = new Credential(username, password);

            // Record the approval request for this credential
            List acceptedCredentials;
            if (!this.CredentialApprovals.TryGetValue(repoUrl, out acceptedCredentials))
            {
                acceptedCredentials = new List();
                this.CredentialApprovals[repoUrl] = acceptedCredentials;
            }

            acceptedCredentials.Add(credential);

            // Store the credential
            this.StoredCredentials[repoUrl] = credential;

            return base.TryStoreCredential(tracer, repoUrl, username, password, out error);
        }

        public override bool TryDeleteCredential(ITracer tracer, string repoUrl, string username, string password, out string error)
        {
            Credential credential = new Credential(username, password);

            // Record the rejection request for this credential
            List rejectedCredentials;
            if (!this.CredentialRejections.TryGetValue(repoUrl, out rejectedCredentials))
            {
                rejectedCredentials = new List();
                this.CredentialRejections[repoUrl] = rejectedCredentials;
            }

            rejectedCredentials.Add(credential);

            // Erase the credential
            this.StoredCredentials.Remove(repoUrl);

            return base.TryDeleteCredential(tracer, repoUrl, username, password, out error);
        }

        protected override Result InvokeGitImpl(
            string command,
            string workingDirectory,
            string dotGitDirectory,
            bool useReadObjectHook,
            Action writeStdIn,
            Action parseStdOutLine,
            int timeoutMs,
            string gitObjectsDirectory = null,
            bool usePrecommandHook = true)
        {
            this.CommandsRun.Add(command);

            if (this.ShouldFail)
            {
                return new Result(string.Empty, string.Empty, Result.GenericFailureCode);
            }

            Func commandMatchFunction =
                (CommandInfo commandInfo) =>
                {
                    if (commandInfo.MatchPrefix)
                    {
                        return command.StartsWith(commandInfo.Command);
                    }
                    else
                    {
                        return string.Equals(command, commandInfo.Command, StringComparison.Ordinal);
                    }
                };

            CommandInfo matchedCommand = this.expectedCommandInfos.Last(commandMatchFunction);
            matchedCommand.ShouldNotBeNull("Unexpected command: " + command);

            var result = matchedCommand.Result();
            if (parseStdOutLine != null && !string.IsNullOrEmpty(result.Output))
            {
                using (StringReader reader = new StringReader(result.Output))
                {
                    string line;
                    while ((line = reader.ReadLine()) != null)
                    {
                        parseStdOutLine(line);
                    }
                }
                /* Future: result.Output should be set to null in this case */
            }
            return result;
        }

        public class Credential
        {
            public Credential(string username, string password)
            {
                this.Username = username;
                this.Password = password;
            }

            public string Username { get; }
            public string Password { get; }

            public string BasicAuthString
            {
                get => Convert.ToBase64String(Encoding.ASCII.GetBytes(this.Username + ":" + this.Password));
            }
        }

        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; }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Git/MockGitRepo.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using System;
using System.Collections.Generic;

namespace GVFS.UnitTests.Mock.Git
{
    public class MockGitRepo : GitRepo
    {
        private Dictionary objects = new Dictionary();
        private string rootSha;

        public MockGitRepo(ITracer tracer, Enlistment enlistment, PhysicalFileSystem fileSystem)
            : base(tracer)
        {
            this.rootSha = Guid.NewGuid().ToString();
            this.AddTree(this.rootSha, ".");
        }

        /// 
        /// Adds an unparented tree to the "repo"
        /// 
        public void AddTree(string sha, string name, params string[] childShas)
        {
            MockGitObject newObj = new MockGitObject(sha, name, false);
            newObj.ChildShas.AddRange(childShas);
            this.objects.Add(sha, newObj);
        }

        /// 
        /// Adds an unparented blob to the "repo"
        /// 
        public void AddBlob(string sha, string name, string contents)
        {
            MockGitObject newObj = new MockGitObject(sha, name, true);
            newObj.Content = contents;
            this.objects.Add(sha, newObj);
        }

        /// 
        /// Adds a child sha to an existing tree
        /// 
        public void AddChildBySha(string treeSha, string childSha)
        {
            MockGitObject treeObj = this.GetTree(treeSha);
            treeObj.ChildShas.Add(childSha);
        }

        /// 
        /// Adds an parented blob to the "repo"
        /// 
        public string AddChildBlob(string parentSha, string childName, string childContent)
        {
            string newSha = Guid.NewGuid().ToString();
            this.AddBlob(newSha, childName, childContent);
            this.AddChildBySha(parentSha, newSha);
            return newSha;
        }

        /// 
        /// Adds an parented tree to the "repo"
        /// 
        public string AddChildTree(string parentSha, string name, params string[] childShas)
        {
            string newSha = Guid.NewGuid().ToString();
            this.AddTree(newSha, name, childShas);
            this.AddChildBySha(parentSha, newSha);
            return newSha;
        }

        public string GetHeadTreeSha()
        {
            return this.rootSha;
        }

        public override bool TryGetBlobLength(string blobSha, out long size)
        {
            MockGitObject obj;
            if (this.objects.TryGetValue(blobSha, out obj))
            {
                obj.IsBlob.ShouldEqual(true);
                size = obj.Content.Length;
                return true;
            }

            size = 0;
            return false;
        }

        private MockGitObject GetTree(string treeSha)
        {
            this.objects.ContainsKey(treeSha).ShouldEqual(true);
            MockGitObject obj = this.objects[treeSha];
            obj.IsBlob.ShouldEqual(false);
            return obj;
        }

        private class MockGitObject
        {
            public MockGitObject(string sha, string name, bool isBlob)
            {
                this.Sha = sha;
                this.Name = name;
                this.IsBlob = isBlob;
                this.ChildShas = new List();
            }

            public string Sha { get; private set; }
            public string Name { get; set; }
            public bool IsBlob { get; set; }
            public List ChildShas { get; set; }
            public string Content { get; set; }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Git/MockHttpGitObjects.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Http;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;

namespace GVFS.UnitTests.Mock.Git
{
    public class MockHttpGitObjects : GitObjectsHttpRequestor
    {
        private Dictionary shaLengths = new Dictionary(StringComparer.OrdinalIgnoreCase);
        private Dictionary shaContents = new Dictionary(StringComparer.OrdinalIgnoreCase);

        public MockHttpGitObjects(ITracer tracer, Enlistment enlistment)
            : base(tracer, enlistment, new MockCacheServerInfo(), new RetryConfig())
        {
        }

        public void AddShaLength(string sha, long length)
        {
            this.shaLengths.Add(sha, length);
        }

        public void AddBlobContent(string sha, string content)
        {
            this.shaContents.Add(sha, content);
        }

        public void AddShaLengths(IEnumerable> shaLengthPairs)
        {
            foreach (KeyValuePair kvp in shaLengthPairs)
            {
                this.AddShaLength(kvp.Key, kvp.Value);
            }
        }

        public override List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken)
        {
            return objectIds.Select(oid => new GitObjectSize(oid, this.QueryForFileSize(oid))).ToList();
        }

        public override GitRefs QueryInfoRefs(string branch)
        {
            throw new NotImplementedException();
        }

        public override RetryWrapper.InvocationResult TryDownloadObjects(
            Func> objectIdGenerator,
            Func.CallbackResult> onSuccess,
            Action.ErrorEventArgs> onFailure,
            bool preferBatchedLooseObjects)
        {
            return this.TryDownloadObjects(objectIdGenerator(), onSuccess, onFailure, preferBatchedLooseObjects);
        }

        public override RetryWrapper.InvocationResult TryDownloadObjects(
            IEnumerable objectIds,
            Func.CallbackResult> onSuccess,
            Action.ErrorEventArgs> onFailure,
            bool preferBatchedLooseObjects)
        {
            // When working within the mocks, we do not support multiple objects.
            // PhysicalGitObjects should be overridden to serialize the calls.
            objectIds.Count().ShouldEqual(1);
            string objectId = objectIds.First();
            return this.GetSingleObject(objectId, onSuccess, onFailure);
        }

        private RetryWrapper.InvocationResult GetSingleObject(
            string objectId,
            Func.CallbackResult> onSuccess,
            Action.ErrorEventArgs> onFailure)
        {
            if (this.shaContents.ContainsKey(objectId))
            {
                using (GitEndPointResponseData response = new GitEndPointResponseData(
                    HttpStatusCode.OK,
                    GVFSConstants.MediaTypes.LooseObjectMediaType,
                    new ReusableMemoryStream(this.shaContents[objectId]),
                    message: null,
                    onResponseDisposed: null))
                {
                    RetryWrapper.CallbackResult result = onSuccess(1, response);
                    return new RetryWrapper.InvocationResult(1, true, result.Result);
                }
            }

            if (onFailure != null)
            {
                onFailure(new RetryWrapper.ErrorEventArgs(new Exception("Could not find mock object: " + objectId), 1, false));
            }

            return new RetryWrapper.InvocationResult(1, new Exception("Mock failure in TryDownloadObjectsAsync"));
        }

        private long QueryForFileSize(string objectId)
        {
            this.shaLengths.ContainsKey(objectId).ShouldEqual(true);
            return this.shaLengths[objectId];
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Git/MockLibGit2Repo.cs
================================================
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using System;
using System.IO;

namespace GVFS.UnitTests.Mock.Git
{
    public class MockLibGit2Repo : LibGit2Repo
    {
        public MockLibGit2Repo(ITracer tracer)
            : base()
        {
        }

        public override bool CommitAndRootTreeExists(string commitish, out string treeSha)
        {
            treeSha = string.Empty;
            return false;
        }

        public override bool ObjectExists(string sha)
        {
            return false;
        }

        public override bool TryCopyBlob(string sha, Action writeAction)
        {
            throw new NotSupportedException();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/MockCacheServerInfo.cs
================================================
using GVFS.Common.Http;

namespace GVFS.UnitTests.Mock
{
    public class MockCacheServerInfo : CacheServerInfo
    {
        public MockCacheServerInfo() : base("https://mock", "mock")
        {
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/MockTextWriter.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace GVFS.UnitTests.Mock.Upgrader
{
    public class MockTextWriter : TextWriter
    {
        private StringBuilder stringBuilder;

        public MockTextWriter() : base()
        {
            this.AllLines = new List();
            this.stringBuilder = new StringBuilder();
        }

        public List AllLines { get; private set; }

        public override Encoding Encoding
        {
            get { return Encoding.Default; }
        }

        public override void Write(char value)
        {
            if (value.Equals('\r'))
            {
                return;
            }

            if (value.Equals('\n'))
            {
                this.AllLines.Add(this.stringBuilder.ToString());
                this.stringBuilder.Clear();
                return;
            }

            this.stringBuilder.Append(value);
        }

        public bool ContainsLine(string line)
        {
            return this.AllLines.Exists(x => x.Equals(line, StringComparison.Ordinal));
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/ReusableMemoryStream.cs
================================================
using System;
using System.IO;
using System.Text;

namespace GVFS.UnitTests.Mock
{
    public class ReusableMemoryStream : Stream
    {
        private byte[] contents;
        private long length;
        private long position;

        public ReusableMemoryStream(string initialContents)
        {
            this.contents = Encoding.UTF8.GetBytes(initialContents);
            this.length = this.contents.Length;
        }

        public ReusableMemoryStream(byte[] initialContents)
        {
            this.contents = initialContents;
            this.length = initialContents.Length;
        }

        public bool TruncateWrites { get; set; }

        public override bool CanRead
        {
            get { return true; }
        }

        public override bool CanSeek
        {
            get { return true; }
        }

        public override bool CanWrite
        {
            get { return true; }
        }

        public override long Length
        {
            get { return this.length; }
        }

        public override long Position
        {
            get { return this.position; }
            set { this.position = value; }
        }

        public override void Flush()
        {
            // noop
        }

        public string ReadAsString()
        {
            return Encoding.UTF8.GetString(this.contents, 0, (int)this.length);
        }

        public string ReadAt(long position, long length)
        {
            long lastPosition = this.Position;

            this.Position = position;

            byte[] bytes = new byte[length];
            this.Read(bytes, 0, (int)length);

            this.Position = lastPosition;

            return Encoding.UTF8.GetString(bytes);
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            int actualCount = Math.Min((int)(this.length - this.position), count);
            Array.Copy(this.contents, this.Position, buffer, offset, actualCount);
            this.Position += actualCount;

            return actualCount;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            if (origin == SeekOrigin.Begin)
            {
                this.position = offset;
            }
            else if (origin == SeekOrigin.End)
            {
                this.position = this.length - offset;
            }
            else
            {
                this.position += offset;
            }

            if (this.position > this.length)
            {
                this.position = this.length - 1;
            }

            return this.position;
        }

        public override void SetLength(long value)
        {
            while (value > this.contents.Length)
            {
                if (this.contents.Length == 0)
                {
                    this.contents = new byte[1024];
                }
                else
                {
                    Array.Resize(ref this.contents, this.contents.Length * 2);
                }
            }

            this.length = value;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (this.position + count > this.contents.Length)
            {
                this.SetLength(this.position + count);
            }

            if (this.TruncateWrites)
            {
                count /= 2;
            }

            Array.Copy(buffer, offset, this.contents, this.position, count);
            this.position += count;
            if (this.position > this.length)
            {
                this.length = this.position;
            }

            if (this.TruncateWrites)
            {
                throw new IOException("Could not complete write");
            }
        }

        protected override void Dispose(bool disposing)
        {
            // This method is a noop besides resetting the position.
            // The byte[] in this class is the source of truth for the contents that this
            // stream is providing, so we can't dispose it here.
            this.position = 0;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Virtualization/Background/MockBackgroundTaskManager.cs
================================================
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();
        }

        public List BackgroundTasks { get; private set; }

        public override bool IsEmpty => this.BackgroundTasks.Count == 0;

        public override int Count
        {
            get
            {
                return this.BackgroundTasks.Count;
            }
        }

        public override void SetCallbacks(
            Func preCallback,
            Func callback,
            Func postCallback)
        {
            this.preCallback = preCallback;
            this.callback = callback;
            this.postCallback = postCallback;
        }

        public override void Start()
        {
        }

        public override void Enqueue(FileSystemTask backgroundTask)
        {
            this.BackgroundTasks.Add(backgroundTask);
        }

        public override void Shutdown()
        {
        }

        public void ProcessTasks()
        {
            this.preCallback();

            foreach (FileSystemTask task in this.BackgroundTasks)
            {
                this.callback(task);
            }

            this.postCallback();
            this.BackgroundTasks.Clear();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Virtualization/BlobSize/MockBlobSizesDatabase.cs
================================================
using GVFS.Common.Git;
using GVFS.UnitTests.Mock.Common;
using GVFS.Virtualization.BlobSize;
using System;

namespace GVFS.UnitTests.Mock.Virtualization.BlobSize
{
    public class MockBlobSizes : BlobSizes
    {
        public MockBlobSizes()
            : base("mock:\\blobSizeDatabase", fileSystem: null, tracer: new MockTracer())
        {
        }

        public override void Initialize()
        {
        }

        public override void Shutdown()
        {
        }

        public override BlobSizesConnection CreateConnection()
        {
            return new MockBlobSizesConnection(this);
        }

        public override void AddSize(Sha1Id sha, long length)
        {
            throw new NotSupportedException("SaveValue has not been implemented yet.");
        }

        public override void Flush()
        {
            throw new NotSupportedException("Flush has not been implemented yet.");
        }

        public class MockBlobSizesConnection : BlobSizesConnection
        {
            public MockBlobSizesConnection(MockBlobSizes mockBlobSizesDatabase)
                : base(mockBlobSizesDatabase)
            {
            }

            public override bool TryGetSize(Sha1Id sha, out long length)
            {
                throw new NotSupportedException("TryGetSize has not been implemented yet.");
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Virtualization/FileSystem/MockFileSystemVirtualizer.cs
================================================
using System;
using System.IO;
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Virtualization.FileSystem;

namespace GVFS.UnitTests.Mock.Virtualization.FileSystem
{
    public class MockFileSystemVirtualizer : FileSystemVirtualizer
    {
        public MockFileSystemVirtualizer(GVFSContext context, GVFSGitObjects gvfsGitObjects)
            : base(context, gvfsGitObjects)
        {
        }

        public override FileSystemResult ClearNegativePathCache(out uint totalEntryCount)
        {
            totalEntryCount = 0;
            return new FileSystemResult(FSResult.Ok, rawResult: 0);
        }

        public override FileSystemResult DeleteFile(string relativePath, UpdatePlaceholderType updateFlags, out UpdateFailureReason failureReason)
        {
            throw new NotImplementedException();
        }

        public override void Stop()
        {
        }

        public override FileSystemResult WritePlaceholderFile(string relativePath, long endOfFile, string sha)
        {
            throw new NotImplementedException();
        }

        public override FileSystemResult WritePlaceholderDirectory(string relativePath)
        {
            throw new NotImplementedException();
        }

        public override FileSystemResult UpdatePlaceholderIfNeeded(string relativePath, DateTime creationTime, DateTime lastAccessTime, DateTime lastWriteTime, DateTime changeTime, FileAttributes fileAttributes, long endOfFile, string shaContentId, UpdatePlaceholderType updateFlags, out UpdateFailureReason failureReason)
        {
            throw new NotImplementedException();
        }

        public override FileSystemResult DehydrateFolder(string relativePath)
        {
            throw new NotImplementedException();
        }

        public override bool TryStart(out string error)
        {
            error = null;
            return true;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Mock/Virtualization/Projection/MockGitIndexProjection.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Virtualization.Background;
using GVFS.Virtualization.BlobSize;
using GVFS.Virtualization.Projection;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace GVFS.UnitTests.Mock.Virtualization.Projection
{
    public class MockGitIndexProjection : GitIndexProjection
    {
        private ConcurrentHashSet projectedFiles;

        private ManualResetEvent unblockGetProjectedItems;
        private ManualResetEvent waitForGetProjectedItems;

        private ManualResetEvent unblockIsPathProjected;
        private ManualResetEvent waitForIsPathProjected;

        private ManualResetEvent unblockGetProjectedFileInfo;
        private ManualResetEvent waitForGetProjectedFileInfo;

        private AutoResetEvent placeholderCreated;

        public MockGitIndexProjection(IEnumerable projectedFiles)
        {
            this.projectedFiles = new ConcurrentHashSet();
            foreach (string entry in projectedFiles)
            {
                this.projectedFiles.Add(entry);
            }

            this.PlaceholdersCreated = new ConcurrentHashSet();
            this.ExpandedFolders = new ConcurrentHashSet();
            this.MockFileTypesAndModes = new ConcurrentDictionary();
            this.SparseEntries = new ConcurrentHashSet();

            this.unblockGetProjectedItems = new ManualResetEvent(true);
            this.waitForGetProjectedItems = new ManualResetEvent(true);

            this.unblockIsPathProjected = new ManualResetEvent(true);
            this.waitForIsPathProjected = new ManualResetEvent(true);

            this.unblockGetProjectedFileInfo = new ManualResetEvent(true);
            this.waitForGetProjectedFileInfo = new ManualResetEvent(true);

            this.placeholderCreated = new AutoResetEvent(false);
        }

        public bool EnumerationInMemory { get; set; }

        public ConcurrentHashSet PlaceholdersCreated { get; }

        public ConcurrentHashSet ExpandedFolders { get; }

        public ConcurrentDictionary MockFileTypesAndModes { get; }

        public ConcurrentHashSet SparseEntries { get; }

        public bool ThrowOperationCanceledExceptionOnProjectionRequest { get; set; }

        public bool ProjectionParseComplete { get; set; }

        public PathSparseState GetFolderPathSparseStateValue { get; set; } = PathSparseState.Included;
        public bool TryAddSparseFolderReturnValue { get; set; } = true;

        public override bool IsProjectionParseComplete()
        {
            return this.ProjectionParseComplete;
        }

        public override PathSparseState GetFolderPathSparseState(string virtualPath)
        {
            return this.GetFolderPathSparseStateValue;
        }

        public override bool TryAddSparseFolder(string virtualPath)
        {
            if (this.TryAddSparseFolderReturnValue)
            {
                this.SparseEntries.Add(virtualPath);
            }

            return this.TryAddSparseFolderReturnValue;
        }

        public void BlockGetProjectedItems(bool willWaitForRequest)
        {
            if (willWaitForRequest)
            {
                this.waitForGetProjectedItems.Reset();
            }

            this.unblockGetProjectedItems.Reset();
        }

        public void UnblockGetProjectedItems()
        {
            this.unblockGetProjectedItems.Set();
        }

        public void WaitForGetProjectedItems()
        {
            this.waitForIsPathProjected.WaitOne();
        }

        public override FileSystemTaskResult OpenIndexForRead()
        {
            return FileSystemTaskResult.Success;
        }

        public void BlockIsPathProjected(bool willWaitForRequest)
        {
            if (willWaitForRequest)
            {
                this.waitForIsPathProjected.Reset();
            }

            this.unblockIsPathProjected.Reset();
        }

        public void UnblockIsPathProjected()
        {
            this.unblockIsPathProjected.Set();
        }

        public void WaitForIsPathProjected()
        {
            this.waitForIsPathProjected.WaitOne();
        }

        public void BlockGetProjectedFileInfo(bool willWaitForRequest)
        {
            if (willWaitForRequest)
            {
                this.waitForGetProjectedFileInfo.Reset();
            }

            this.unblockGetProjectedFileInfo.Reset();
        }

        public void UnblockGetProjectedFileInfo()
        {
            this.unblockGetProjectedFileInfo.Set();
        }

        public void WaitForGetProjectedFileInfo()
        {
            this.waitForGetProjectedFileInfo.WaitOne();
        }

        public void WaitForPlaceholderCreate()
        {
            this.placeholderCreated.WaitOne();
        }

        public override void Initialize(BackgroundFileSystemTaskRunner backgroundQueue)
        {
        }

        public override void Shutdown()
        {
        }

        public override void InvalidateProjection()
        {
        }

        public override bool TryGetProjectedItemsFromMemory(string folderPath, out List projectedItems)
        {
            if (this.EnumerationInMemory)
            {
                projectedItems = this.projectedFiles.Select(name => new ProjectedFileInfo(name, size: 0, isFolder: false, sha: new Sha1Id(1, 1, 1))).ToList();
                return true;
            }

            projectedItems = null;
            return false;
        }

        public override void GetFileTypeAndMode(string path, out FileType fileType, out ushort fileMode)
        {
            fileType = FileType.Invalid;
            fileMode = 0;

            ushort mockFileTypeAndMode;
            if (this.MockFileTypesAndModes.TryGetValue(path, out mockFileTypeAndMode))
            {
                FileTypeAndMode typeAndMode = new FileTypeAndMode(mockFileTypeAndMode);
                fileType = typeAndMode.Type;
                fileMode = typeAndMode.Mode;
            }
        }

        public override List GetProjectedItems(
            CancellationToken cancellationToken,
            BlobSizes.BlobSizesConnection blobSizesConnection,
            string folderPath)
        {
            this.waitForGetProjectedItems.Set();

            if (this.ThrowOperationCanceledExceptionOnProjectionRequest)
            {
                throw new OperationCanceledException();
            }

            this.unblockGetProjectedItems.WaitOne();
            return this.projectedFiles.Select(name => new ProjectedFileInfo(name, size: 0, isFolder: false, sha: new Sha1Id(1, 1, 1))).ToList();
        }

        public override bool IsPathProjected(string virtualPath, out string fileName, out bool isFolder)
        {
            this.waitForIsPathProjected.Set();
            this.unblockIsPathProjected.WaitOne();

            if (this.projectedFiles.Contains(virtualPath))
            {
                isFolder = false;
                string parentKey;
                this.GetChildNameAndParentKey(virtualPath, out fileName, out parentKey);
                return true;
            }

            fileName = string.Empty;
            isFolder = false;
            return false;
        }

        public override ProjectedFileInfo GetProjectedFileInfo(
            CancellationToken cancellationToken,
            BlobSizes.BlobSizesConnection blobSizesConnection,
            string virtualPath,
            out string parentFolderPath)
        {
            this.waitForGetProjectedFileInfo.Set();

            if (this.ThrowOperationCanceledExceptionOnProjectionRequest)
            {
                throw new OperationCanceledException();
            }

            this.unblockGetProjectedFileInfo.WaitOne();

            if (this.projectedFiles.Contains(virtualPath))
            {
                string childName;
                string parentKey;
                this.GetChildNameAndParentKey(virtualPath, out childName, out parentKey);
                parentFolderPath = parentKey;
                return new ProjectedFileInfo(childName, size: 0, isFolder: false, sha: new Sha1Id(1, 1, 1));
            }

            parentFolderPath = null;
            return null;
        }

        public override void OnPlaceholderFolderExpanded(string relativePath)
        {
            this.ExpandedFolders.Add(relativePath);
        }

        public override void OnPlaceholderFileCreated(string virtualPath, string sha)
        {
            this.PlaceholdersCreated.Add(virtualPath);
            this.placeholderCreated.Set();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.unblockGetProjectedItems != null)
                {
                    this.unblockGetProjectedItems.Dispose();
                    this.unblockGetProjectedItems = null;
                }

                if (this.waitForGetProjectedItems != null)
                {
                    this.waitForGetProjectedItems.Dispose();
                    this.waitForGetProjectedItems = null;
                }

                if (this.unblockIsPathProjected != null)
                {
                    this.unblockIsPathProjected.Dispose();
                    this.unblockIsPathProjected = null;
                }

                if (this.waitForIsPathProjected != null)
                {
                    this.waitForIsPathProjected.Dispose();
                    this.waitForIsPathProjected = null;
                }

                if (this.unblockGetProjectedFileInfo != null)
                {
                    this.unblockGetProjectedFileInfo.Dispose();
                    this.unblockGetProjectedFileInfo = null;
                }

                if (this.waitForGetProjectedFileInfo != null)
                {
                    this.waitForGetProjectedFileInfo.Dispose();
                    this.waitForGetProjectedFileInfo = null;
                }

                if (this.placeholderCreated != null)
                {
                    this.placeholderCreated.Dispose();
                    this.placeholderCreated = null;
                }
            }

            base.Dispose(disposing);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Prefetch/BatchObjectDownloadStageTests.cs
================================================
using GVFS.Common.Prefetch.Pipeline;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;
using System;
using System.Collections.Concurrent;
using System.Threading;

namespace GVFS.UnitTests.Prefetch
{
    [TestFixture]
    public class BatchObjectDownloadStageTests
    {
        private const int MaxParallel = 1;
        private const int ChunkSize = 2;

        // This test confirms that if two objects are downloaded at the same time and the second
        // object's download fails, the first object should not be downloaded again
        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void OnlyRequestsObjectsNotDownloaded()
        {
            string obj1Sha = new string('1', 40);
            string obj2Sha = new string('2', 40);

            BlockingCollection input = new BlockingCollection();
            input.Add(obj1Sha);
            input.Add(obj2Sha);
            input.CompleteAdding();

            int obj1Count = 0;
            int obj2Count = 0;

            Func objectResolver = (oid) =>
            {
                if (oid.Equals(obj1Sha))
                {
                    obj1Count++;
                    return "Object1Contents";
                }

                if (oid.Equals(obj2Sha) && obj2Count++ == 1)
                {
                    return "Object2Contents";
                }

                return null;
            };

            BlockingCollection output = new BlockingCollection();
            MockTracer tracer = new MockTracer();
            MockGVFSEnlistment enlistment = new MockGVFSEnlistment();
            MockBatchHttpGitObjects httpObjects = new MockBatchHttpGitObjects(tracer, enlistment, objectResolver);

            BatchObjectDownloadStage dut = new BatchObjectDownloadStage(
                MaxParallel,
                ChunkSize,
                input,
                output,
                tracer,
                enlistment,
                httpObjects,
                new MockPhysicalGitObjects(tracer, null, enlistment, httpObjects));

            dut.Start();
            dut.WaitForCompletion();

            input.Count.ShouldEqual(0);
            output.Count.ShouldEqual(2);
            output.Take().ShouldEqual(obj1Sha);
            output.Take().ShouldEqual(obj2Sha);
            obj1Count.ShouldEqual(1);
            obj2Count.ShouldEqual(2);
        }

        [TestCase]
        public void DoesNotExitEarlyIfInputTakesLongerThanChunkSizeToGetFirstBlob()
        {
            BlockingCollection input = new BlockingCollection();
            string objSha = new string('1', 40);

            int objCount = 0;

            Func objectResolver = (oid) =>
            {
                if (oid.Equals(objSha))
                {
                    objCount++;
                    return "Object1Contents";
                }

                return null;
            };

            BlockingCollection output = new BlockingCollection();
            MockTracer tracer = new MockTracer();
            MockGVFSEnlistment enlistment = new MockGVFSEnlistment();
            MockBatchHttpGitObjects httpObjects = new MockBatchHttpGitObjects(tracer, enlistment, objectResolver);

            BatchObjectDownloadStage dut = new BatchObjectDownloadStage(
                MaxParallel,
                1,
                input,
                output,
                tracer,
                enlistment,
                httpObjects,
                new MockPhysicalGitObjects(tracer, null, enlistment, httpObjects));

            dut.Start();
            Thread.Sleep(TimeSpan.FromMilliseconds(110));
            input.Add(objSha);
            input.CompleteAdding();
            dut.WaitForCompletion();
            objCount.ShouldEqual(1);
        }
    }
}

================================================
FILE: GVFS/GVFS.UnitTests/Prefetch/BlobPrefetcherTests.cs
================================================
using GVFS.Common.Prefetch;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.FileSystem;
using NUnit.Framework;
using System.IO;

namespace GVFS.UnitTests.Prefetch
{
    [TestFixture]
    public class BlobPrefetcherTests
    {
        [TestCase]
        public void AppendToNewlineSeparatedFileTests()
        {
            MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(Path.Combine("mock:", "GVFS", "UnitTests", "Repo"), null, null));

            // Validate can write to a file that doesn't exist.
            string testFileName = Path.Combine("mock:", "GVFS", "UnitTests", "Repo", "appendTests");
            BlobPrefetcher.AppendToNewlineSeparatedFile(fileSystem, testFileName, "expected content line 1");
            fileSystem.ReadAllText(testFileName).ShouldEqual("expected content line 1\n");

            // Validate that if the file doesn't end in a newline it gets a newline added.
            fileSystem.WriteAllText(testFileName, "existing content");
            BlobPrefetcher.AppendToNewlineSeparatedFile(fileSystem, testFileName, "expected line 2");
            fileSystem.ReadAllText(testFileName).ShouldEqual("existing content\nexpected line 2\n");

            // Validate that if the file ends in a newline, we don't end up with two newlines
            fileSystem.WriteAllText(testFileName, "existing content\n");
            BlobPrefetcher.AppendToNewlineSeparatedFile(fileSystem, testFileName, "expected line 2");
            fileSystem.ReadAllText(testFileName).ShouldEqual("existing content\nexpected line 2\n");
        }
    }
}

================================================
FILE: GVFS/GVFS.UnitTests/Prefetch/DiffHelperTests.cs
================================================
using GVFS.Common.Git;
using GVFS.Common.Prefetch.Git;
using GVFS.Tests;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace GVFS.UnitTests.Prefetch
{
    [TestFixtureSource(typeof(DataSources), nameof(DataSources.AllBools))]
    public class DiffHelperTests
    {
        public DiffHelperTests(bool symLinkSupport)
        {
            this.IncludeSymLinks = symLinkSupport;
        }

        public bool IncludeSymLinks { get; set; }

        // Make two commits. The first should look like this:
        // recursiveDelete
        // recursiveDelete/subfolder
        // recursiveDelete/subfolder/childFile.txt
        // fileToBecomeFolder
        // fileToDelete.txt
        // fileToEdit.txt
        // fileToRename.txt
        // fileToRenameEdit.txt
        // folderToBeFile
        // folderToBeFile/childFile.txt
        // folderToDelete
        // folderToDelete/childFile.txt
        // folderToEdit
        // folderToEdit/childFile.txt
        // folderToRename
        // folderToRename/childFile.txt
        // symLinkToBeCreated.txt
        //
        // The second should follow the action indicated by the file/folder name:
        // eg. recursiveDelete should run "rmdir /s/q recursiveDelete"
        // eg. folderToBeFile should be deleted and replaced with a file of the same name
        // Note that each childFile.txt should have unique contents, but is only a placeholder to force git to add a folder.
        //
        // Then to generate the diffs, run:
        // git diff-tree -r -t Head~1 Head > forward.txt
        // git diff-tree -r -t Head Head ~1 > backward.txt
        [TestCase]
        public void CanParseDiffForwards()
        {
            MockTracer tracer = new MockTracer();
            DiffHelper diffForwards = new DiffHelper(tracer, new MockGVFSEnlistment(), new List(), new List(), includeSymLinks: this.IncludeSymLinks);
            diffForwards.ParseDiffFile(GetDataPath("forward.txt"));

            // File added, file edited, file renamed, folder => file, edit-rename file, SymLink added (if applicable)
            // Children of: Add folder, Renamed folder, edited folder, file => folder
            diffForwards.RequiredBlobs.Count.ShouldEqual(diffForwards.ShouldIncludeSymLinks ? 10 : 9);

            diffForwards.FileAddOperations.ContainsKey("3bd509d373734a9f9685d6a73ba73324f72931e3").ShouldEqual(diffForwards.ShouldIncludeSymLinks);

            // File deleted, folder deleted, file > folder, edit-rename
            diffForwards.FileDeleteOperations.Count.ShouldEqual(4);

            // Includes children of: Recursive delete folder, deleted folder, renamed folder, and folder => file
            diffForwards.TotalFileDeletes.ShouldEqual(8);

            // Folder created, folder edited, folder deleted, folder renamed (add + delete),
            // folder => file, file => folder, recursive delete (top-level only)
            diffForwards.DirectoryOperations.Count.ShouldEqual(8);

            // Should also include the deleted folder of recursive delete
            diffForwards.TotalDirectoryOperations.ShouldEqual(9);
        }

        // Parses Diff B => A
        [TestCase]
        public void CanParseBackwardsDiff()
        {
            MockTracer tracer = new MockTracer();
            DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockGVFSEnlistment(), new List(), new List(), includeSymLinks: this.IncludeSymLinks);
            diffBackwards.ParseDiffFile(GetDataPath("backward.txt"));

            // File > folder, deleted file, edited file, renamed file, rename-edit file
            // Children of file > folder, renamed folder, deleted folder, recursive delete file, edited folder
            diffBackwards.RequiredBlobs.Count.ShouldEqual(10);

            // File added, folder > file, moved folder, added folder
            diffBackwards.FileDeleteOperations.Count.ShouldEqual(6);

            // Also includes, the children of: Folder added, folder renamed, file => folder
            diffBackwards.TotalFileDeletes.ShouldEqual(9);

            // Folder created, folder edited, folder deleted, folder renamed (add + delete),
            // folder => file, file => folder, recursive delete (include subfolder)
            diffBackwards.TotalDirectoryOperations.ShouldEqual(9);
        }

        // Delete a folder with two sub folders each with a single file
        // Readd it with a different casing and same contents
        [TestCase]
        [Category(CategoryConstants.CaseInsensitiveFileSystemOnly)]
        public void ParsesCaseChangesAsAdds()
        {
            MockTracer tracer = new MockTracer();
            DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockGVFSEnlistment(), new List(), new List(), includeSymLinks: this.IncludeSymLinks);
            diffBackwards.ParseDiffFile(GetDataPath("caseChange.txt"));

            diffBackwards.RequiredBlobs.Count.ShouldEqual(2);
            diffBackwards.FileAddOperations.Sum(list => list.Value.Count).ShouldEqual(2);

            diffBackwards.FileDeleteOperations.Count.ShouldEqual(0);
            diffBackwards.TotalFileDeletes.ShouldEqual(0);

            diffBackwards.DirectoryOperations.ShouldNotContain(entry => entry.Operation == DiffTreeResult.Operations.Delete);
            diffBackwards.TotalDirectoryOperations.ShouldEqual(3);
        }

        // Delete a folder with two sub folders each with a single file
        // Readd it with a different casing and same contents
        [TestCase]
        [Category(CategoryConstants.CaseSensitiveFileSystemOnly)]
        public void ParsesCaseChangesAsRenames()
        {
            MockTracer tracer = new MockTracer();
            DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockGVFSEnlistment(), new List(), new List(), includeSymLinks: this.IncludeSymLinks);
            diffBackwards.ParseDiffFile(GetDataPath("caseChange.txt"));

            diffBackwards.RequiredBlobs.Count.ShouldEqual(2);
            diffBackwards.FileAddOperations.Sum(list => list.Value.Count).ShouldEqual(2);

            diffBackwards.FileDeleteOperations.Count.ShouldEqual(0);
            diffBackwards.TotalFileDeletes.ShouldEqual(2);

            diffBackwards.DirectoryOperations.ShouldContain(entry => entry.Operation == DiffTreeResult.Operations.Add);
            diffBackwards.DirectoryOperations.ShouldContain(entry => entry.Operation == DiffTreeResult.Operations.Delete);
            diffBackwards.TotalDirectoryOperations.ShouldEqual(6);
        }

        [TestCase]
        public void DetectsFailuresInDiffTree()
        {
            MockTracer tracer = new MockTracer();
            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 Mock.Common.MockGVFSEnlistment(), gitProcess, new List(), new List(), includeSymLinks: this.IncludeSymLinks);
            diffBackwards.PerformDiff("sha1", "sha2");
            diffBackwards.HasFailures.ShouldEqual(true);
        }

        [TestCase]
        public void DetectsFailuresInLsTree()
        {
            MockTracer tracer = new MockTracer();
            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 Mock.Common.MockGVFSEnlistment(), gitProcess, new List(), new List(), includeSymLinks: this.IncludeSymLinks);
            diffBackwards.PerformDiff(null, "sha1");
            diffBackwards.HasFailures.ShouldEqual(true);
        }

        private static string GetDataPath(string fileName)
        {
            string workingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            return Path.Combine(workingDirectory, "Data", fileName);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Prefetch/DiffTreeResultTests.cs
================================================
using GVFS.Common.Git;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using NUnit.Framework;
using System;
using System.IO;

namespace GVFS.UnitTests.Prefetch
{
    [TestFixture]
    public class DiffTreeResultTests
    {
        private const string TestSha1 = "0ee459db639f34c3064f56845acbc7df0d528e81";
        private const string Test2Sha1 = "2052fbe2ce5b081db3e3b9ffdebe9b0258d14cce";
        private const string EmptySha1 = "0000000000000000000000000000000000000000";

        private const string TestTreePath1 = "Test/GVFS";
        private const string TestTreePath2 = "Test/directory with blob and spaces";
        private const string TestBlobPath1 = "Test/file with spaces.txt";
        private const string TestBlobPath2 = "Test/file with tree and spaces.txt";

        private static readonly string MissingColonLineFromDiffTree = $"040000 040000 {TestSha1} {Test2Sha1} M\t{TestTreePath1}";
        private static readonly string TooManyFieldsLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1} M BadData\t{TestTreePath1}";
        private static readonly string NotEnoughFieldsLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1}\t{TestTreePath1}";
        private static readonly string TwoPathLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1} M\t{TestTreePath1}\t{TestBlobPath1}";
        private static readonly string ModifyTreeLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1} M\t{TestTreePath1}";
        private static readonly string DeleteTreeLineFromDiffTree = $":040000 000000 {TestSha1} {EmptySha1} D\t{TestTreePath1}";
        private static readonly string AddTreeLineFromDiffTree = $":000000 040000 {EmptySha1} {Test2Sha1} A\t{TestTreePath1}";
        private static readonly string ModifyBlobLineFromDiffTree = $":100644 100644 {TestSha1} {Test2Sha1} M\t{TestBlobPath1}";
        private static readonly string DeleteBlobLineFromDiffTree = $":100755 000000 {TestSha1} {EmptySha1} D\t{TestBlobPath1}";
        private static readonly string DeleteBlobLineFromDiffTree2 = $":100644 000000 {TestSha1} {EmptySha1} D\t{TestBlobPath1}";
        private static readonly string AddBlobLineFromDiffTree = $":000000 100644 {EmptySha1} {Test2Sha1} A\t{TestBlobPath1}";

        private static readonly string BlobLineFromLsTree = $"100644 blob {TestSha1}\t{TestTreePath1}";
        private static readonly string BlobLineWithTreePathFromLsTree = $"100644 blob {TestSha1}\t{TestBlobPath2}";
        private static readonly string TreeLineFromLsTree = $"040000 tree {TestSha1}\t{TestTreePath1}";
        private static readonly string TreeLineWithBlobPathFromLsTree = $"040000 tree {TestSha1}\t{TestTreePath2}";
        private static readonly string InvalidLineFromLsTree = $"040000 bad {TestSha1}\t{TestTreePath1}";
        private static readonly string SymLinkLineFromLsTree = $"120000 blob {TestSha1}\t{TestTreePath1}";

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ParseFromDiffTreeLine_NullLine()
        {
            Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(null));
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ParseFromDiffTreeLine_EmptyLine()
        {
            Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(string.Empty));
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ParseFromDiffTreeLine_EmptyRepo()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Modify,
                SourceIsDirectory = true,
                TargetIsDirectory = true,
                TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar,
                SourceSha = TestSha1,
                TargetSha = Test2Sha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(ModifyTreeLineFromDiffTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ParseFromLsTreeLine_NullLine()
        {
            Assert.Throws(() => DiffTreeResult.ParseFromLsTreeLine(null));
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ParseFromLsTreeLine_EmptyLine()
        {
            Assert.Throws(() => DiffTreeResult.ParseFromLsTreeLine(string.Empty));
        }

        [TestCase]
        public void ParseFromLsTreeLine_EmptyRepoRoot()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Add,
                SourceIsDirectory = false,
                TargetIsDirectory = false,
                TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar),
                SourceSha = null,
                TargetSha = TestSha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(BlobLineFromLsTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromLsTreeLine_BlobLine()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Add,
                SourceIsDirectory = false,
                TargetIsDirectory = false,
                TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar),
                SourceSha = null,
                TargetSha = TestSha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(BlobLineFromLsTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromLsTreeLine_TreeLine()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Add,
                SourceIsDirectory = false,
                TargetIsDirectory = true,
                TargetPath = CreateTreePath(TestTreePath1),
                SourceSha = null,
                TargetSha = null
            };

            DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(TreeLineFromLsTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromLsTreeLine_InvalidLine()
        {
            DiffTreeResult.ParseFromLsTreeLine(InvalidLineFromLsTree).ShouldBeNull();
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ParseFromDiffTreeLine_NoColonLine()
        {
            Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(MissingColonLineFromDiffTree));
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ParseFromDiffTreeLine_TooManyFieldsLine()
        {
            Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(TooManyFieldsLineFromDiffTree));
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void ParseFromDiffTreeLine_NotEnoughFieldsLine()
        {
            Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(NotEnoughFieldsLineFromDiffTree));
        }

        [TestCase]

        [Category(CategoryConstants.ExceptionExpected)]
        public void ParseFromDiffTreeLine_TwoPathLine()
        {
            Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(TwoPathLineFromDiffTree));
        }

        [TestCase]
        public void ParseFromDiffTreeLine_ModifyTreeLine()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Modify,
                SourceIsDirectory = true,
                TargetIsDirectory = true,
                TargetPath = CreateTreePath(TestTreePath1),
                SourceSha = TestSha1,
                TargetSha = Test2Sha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(ModifyTreeLineFromDiffTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromDiffTreeLine_DeleteTreeLine()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Delete,
                SourceIsDirectory = true,
                TargetIsDirectory = false,
                TargetPath = CreateTreePath(TestTreePath1),
                SourceSha = TestSha1,
                TargetSha = EmptySha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(DeleteTreeLineFromDiffTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromDiffTreeLine_AddTreeLine()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Add,
                SourceIsDirectory = false,
                TargetIsDirectory = true,
                TargetPath = CreateTreePath(TestTreePath1),
                SourceSha = EmptySha1,
                TargetSha = Test2Sha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(AddTreeLineFromDiffTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromDiffTreeLine_AddBlobLine()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Add,
                SourceIsDirectory = false,
                TargetIsDirectory = false,
                TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar),
                SourceSha = EmptySha1,
                TargetSha = Test2Sha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(AddBlobLineFromDiffTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromDiffTreeLine_DeleteBlobLine()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Delete,
                SourceIsDirectory = false,
                TargetIsDirectory = false,
                TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar),
                SourceSha = TestSha1,
                TargetSha = EmptySha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(DeleteBlobLineFromDiffTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromDiffTreeLine_DeleteBlobLine2()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Delete,
                SourceIsDirectory = false,
                TargetIsDirectory = false,
                TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar),
                SourceSha = TestSha1,
                TargetSha = EmptySha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(DeleteBlobLineFromDiffTree2);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromDiffTreeLine_ModifyBlobLine()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Modify,
                SourceIsDirectory = false,
                TargetIsDirectory = false,
                TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar),
                SourceSha = TestSha1,
                TargetSha = Test2Sha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(ModifyBlobLineFromDiffTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromLsTreeLine_SymLinkLine()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Add,
                SourceIsDirectory = false,
                TargetIsDirectory = false,
                TargetIsSymLink = true,
                TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar),
                SourceSha = null,
                TargetSha = TestSha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(SymLinkLineFromLsTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromDiffTreeLine_TreeLineWithBlobPath()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Add,
                SourceIsDirectory = false,
                TargetIsDirectory = true,
                TargetPath = CreateTreePath(TestTreePath2),
                SourceSha = null,
                TargetSha = null
            };

            DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(TreeLineWithBlobPathFromLsTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase]
        public void ParseFromDiffTreeLine_BlobLineWithTreePath()
        {
            DiffTreeResult expected = new DiffTreeResult()
            {
                Operation = DiffTreeResult.Operations.Add,
                SourceIsDirectory = false,
                TargetIsDirectory = false,
                TargetPath = TestBlobPath2.Replace('/', Path.DirectorySeparatorChar),
                SourceSha = null,
                TargetSha = TestSha1
            };

            DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(BlobLineWithTreePathFromLsTree);
            this.ValidateDiffTreeResult(expected, result);
        }

        [TestCase("040000 tree 73b881d52b607b0f3e9e620d36f556d3d233a11d\tGVFS", DiffTreeResult.TreeMarker, true)]
        [TestCase("040000 tree 73b881d52b607b0f3e9e620d36f556d3d233a11d\tGVFS", DiffTreeResult.BlobMarker, false)]
        [TestCase("100644 blob 44c5f5cba4b29d31c2ad06eed51ea02af76c27c0\tReadme.md", DiffTreeResult.BlobMarker, true)]
        [TestCase("100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts / BuildGVFSForMac.sh", DiffTreeResult.BlobMarker, true)]
        [TestCase("100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts / BuildGVFSForMac.sh", DiffTreeResult.BlobMarker, true)]
        [TestCase("100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts / tree file.txt", DiffTreeResult.TreeMarker, false)]
        [TestCase("100755 ", DiffTreeResult.TreeMarker, false)]
        public void TestGetIndexOfTypeMarker(string line, string typeMarker, bool expectedResult)
        {
            DiffTreeResult.IsLsTreeLineOfType(line, typeMarker).ShouldEqual(expectedResult);
        }

        private static string CreateTreePath(string testPath)
        {
            return testPath.Replace('/', Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
        }

        private void ValidateDiffTreeResult(DiffTreeResult expected, DiffTreeResult actual)
        {
            actual.Operation.ShouldEqual(expected.Operation, $"{nameof(DiffTreeResult)}.{nameof(actual.Operation)}");
            actual.SourceIsDirectory.ShouldEqual(expected.SourceIsDirectory, $"{nameof(DiffTreeResult)}.{nameof(actual.SourceIsDirectory)}");
            actual.TargetIsDirectory.ShouldEqual(expected.TargetIsDirectory, $"{nameof(DiffTreeResult)}.{nameof(actual.TargetIsDirectory)}");
            actual.TargetPath.ShouldEqual(expected.TargetPath, $"{nameof(DiffTreeResult)}.{nameof(actual.TargetPath)}");
            actual.SourceSha.ShouldEqual(expected.SourceSha, $"{nameof(DiffTreeResult)}.{nameof(actual.SourceSha)}");
            actual.TargetSha.ShouldEqual(expected.TargetSha, $"{nameof(DiffTreeResult)}.{nameof(actual.TargetSha)}");
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Prefetch/PrefetchPacksDeserializerTests.cs
================================================
using GVFS.Common.NetworkStreams;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace GVFS.UnitTests.Prefetch
{
    [TestFixture]
    public class PrefetchPacksDeserializerTests
    {
        private static readonly byte[] PrefetchPackExpectedHeader
            = new byte[]
            {
                (byte)'G', (byte)'P', (byte)'R', (byte)'E', (byte)' ',
                1 // Version
            };

        [TestCase]
        public void PrefetchPacksDeserializer_No_Packs_Succeeds()
        {
            this.RunPrefetchPacksDeserializerTest(0, false);
        }

        [TestCase]
        public void PrefetchPacksDeserializer_Single_Pack_With_Index_Receives_Both()
        {
            this.RunPrefetchPacksDeserializerTest(1, true);
        }

        [TestCase]
        public void PrefetchPacksDeserializer_Single_Pack_Without_Index_Receives_Only_Pack()
        {
            this.RunPrefetchPacksDeserializerTest(1, false);
        }

        [TestCase]
        public void PrefetchPacksDeserializer_Multiple_Packs_With_Indexes()
        {
            this.RunPrefetchPacksDeserializerTest(10, true);
        }

        [TestCase]
        public void PrefetchPacksDeserializer_Multiple_Packs_Without_Indexes()
        {
            this.RunPrefetchPacksDeserializerTest(10, false);
        }

        /// 
        /// A deterministic way to create somewhat unique packs
        /// 
        private static byte[] PackForTimestamp(long timestamp)
        {
            unchecked
            {
                Random rand = new Random((int)timestamp);
                byte[] data = new byte[100];
                rand.NextBytes(data);
                return data;
            }
        }

        /// 
        /// A deterministic way to create somewhat unique indexes
        /// 
        private static byte[] IndexForTimestamp(long timestamp)
        {
            unchecked
            {
                Random rand = new Random((int)-timestamp);
                byte[] data = new byte[50];
                rand.NextBytes(data);
                return data;
            }
        }

        /// 
        /// Implementation of the PrefetchPack spec to generate data for tests
        /// 
        private void WriteToSpecs(Stream stream, long[] packTimestamps, bool withIndexes)
        {
            // Header
            stream.Write(PrefetchPackExpectedHeader, 0, PrefetchPackExpectedHeader.Length);

            // PackCount
            stream.Write(BitConverter.GetBytes((ushort)packTimestamps.Length), 0, 2);

            for (int i = 0; i < packTimestamps.Length; i++)
            {
                byte[] packContents = PackForTimestamp(packTimestamps[i]);
                byte[] indexContents = IndexForTimestamp(packTimestamps[i]);

                // Pack Header
                // Timestamp
                stream.Write(BitConverter.GetBytes(packTimestamps[i]), 0, 8);

                // Pack length
                stream.Write(BitConverter.GetBytes((long)packContents.Length), 0, 8);

                // Pack index length
                if (withIndexes)
                {
                    stream.Write(BitConverter.GetBytes((long)indexContents.Length), 0, 8);
                }
                else
                {
                    stream.Write(BitConverter.GetBytes(-1L), 0, 8);
                }

                // Pack data
                stream.Write(packContents, 0, packContents.Length);

                if (withIndexes)
                {
                    stream.Write(indexContents, 0, indexContents.Length);
                }
            }
        }

        private void RunPrefetchPacksDeserializerTest(int packCount, bool withIndexes)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                long[] packTimestamps = Enumerable.Range(0, packCount).Select(x => (long)x).ToArray();

                // Write the data to the memory stream.
                this.WriteToSpecs(ms, packTimestamps, withIndexes);
                ms.Position = 0;

                Dictionary>> receivedPacksAndIndexes = new Dictionary>>();

                foreach (PrefetchPacksDeserializer.PackAndIndex pack in new PrefetchPacksDeserializer(ms).EnumeratePacks())
                {
                    List> packsAndIndexesByUniqueId;
                    if (!receivedPacksAndIndexes.TryGetValue(pack.UniqueId, out packsAndIndexesByUniqueId))
                    {
                        packsAndIndexesByUniqueId = new List>();
                        receivedPacksAndIndexes.Add(pack.UniqueId, packsAndIndexesByUniqueId);
                    }

                    using (MemoryStream packContent = new MemoryStream())
                    using (MemoryStream idxContent = new MemoryStream())
                    {
                        pack.PackStream.CopyTo(packContent);
                        byte[] packData = packContent.ToArray();
                        packData.ShouldMatchInOrder(PackForTimestamp(pack.Timestamp));
                        packsAndIndexesByUniqueId.Add(Tuple.Create("pack", pack.Timestamp));

                        if (pack.IndexStream != null)
                        {
                            pack.IndexStream.CopyTo(idxContent);
                            byte[] idxData = idxContent.ToArray();
                            idxData.ShouldMatchInOrder(IndexForTimestamp(pack.Timestamp));
                            packsAndIndexesByUniqueId.Add(Tuple.Create("idx", pack.Timestamp));
                        }
                    }
                }

                receivedPacksAndIndexes.Count.ShouldEqual(packCount, "UniqueId count");

                foreach (List> groupedByUniqueId in receivedPacksAndIndexes.Values)
                {
                    if (withIndexes)
                    {
                        groupedByUniqueId.Count.ShouldEqual(2, "Both Pack and Index for UniqueId");

                        // Should only contain 1 index file
                        groupedByUniqueId.ShouldContainSingle(x => x.Item1 == "idx");
                    }

                    // should only contain 1 pack file
                    groupedByUniqueId.ShouldContainSingle(x => x.Item1 == "pack");

                    groupedByUniqueId.Select(x => x.Item2).Distinct().Count().ShouldEqual(1, "Same timestamps for a uniqueId");
                }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Prefetch/PrefetchTracingTests.cs
================================================
using GVFS.Common.Prefetch.Pipeline;
using GVFS.Common.Prefetch.Pipeline.Data;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.Git;
using NUnit.Framework;
using System.Collections.Concurrent;

namespace GVFS.UnitTests.Prefetch
{
    [TestFixture]
    public class PrefetchTracingTests
    {
        private const string FakeSha = "fakesha";
        private const string FakeShaContents = "fakeshacontents";

        [TestCase]
        public void ErrorsForBatchObjectDownloadJob()
        {
            using (ITracer tracer = CreateTracer())
            {
                MockGVFSEnlistment enlistment = new MockGVFSEnlistment();
                MockHttpGitObjects httpGitObjects = new MockHttpGitObjects(tracer, enlistment);
                MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, httpGitObjects);

                BlockingCollection input = new BlockingCollection();
                input.Add(FakeSha);
                input.CompleteAdding();

                BatchObjectDownloadStage dut = new BatchObjectDownloadStage(1, 1, input, new BlockingCollection(), tracer, enlistment, httpGitObjects, gitObjects);
                dut.Start();
                dut.WaitForCompletion();

                string sha;
                input.TryTake(out sha).ShouldEqual(false);

                IndexPackRequest request;
                dut.AvailablePacks.TryTake(out request).ShouldEqual(false);
            }
        }

        [TestCase]
        public void SuccessForBatchObjectDownloadJob()
        {
            using (ITracer tracer = CreateTracer())
            {
                MockGVFSEnlistment enlistment = new MockGVFSEnlistment();
                MockHttpGitObjects httpGitObjects = new MockHttpGitObjects(tracer, enlistment);
                httpGitObjects.AddBlobContent(FakeSha, FakeShaContents);
                MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, httpGitObjects);

                BlockingCollection input = new BlockingCollection();
                input.Add(FakeSha);
                input.CompleteAdding();

                BatchObjectDownloadStage dut = new BatchObjectDownloadStage(1, 1, input, new BlockingCollection(), tracer, enlistment, httpGitObjects, gitObjects);
                dut.Start();
                dut.WaitForCompletion();

                string sha;
                input.TryTake(out sha).ShouldEqual(false);
                dut.AvailablePacks.Count.ShouldEqual(0);

                dut.AvailableObjects.Count.ShouldEqual(1);
                string output = dut.AvailableObjects.Take();
                output.ShouldEqual(FakeSha);
            }
        }

        [TestCase]
        public void ErrorsForIndexPackFile()
        {
            using (ITracer tracer = CreateTracer())
            {
                MockGVFSEnlistment enlistment = new MockGVFSEnlistment();
                MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, null);

                BlockingCollection input = new BlockingCollection();
                BlobDownloadRequest downloadRequest = new BlobDownloadRequest(new string[] { FakeSha });
                input.Add(new IndexPackRequest("mock:\\path\\packFileName", downloadRequest));
                input.CompleteAdding();

                IndexPackStage dut = new IndexPackStage(1, input, new BlockingCollection(), tracer, gitObjects);
                dut.Start();
                dut.WaitForCompletion();
            }
        }

        private static ITracer CreateTracer()
        {
            return new MockTracer();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Program.cs
================================================
using GVFS.Tests;
using GVFS.UnitTests.Category;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace GVFS.UnitTests
{
    public class Program
    {
        public static void Main(string[] args)
        {
            NUnitRunner runner = new NUnitRunner(args);
            runner.AddGlobalSetupIfNeeded("GVFS.UnitTests.Setup");

            List excludeCategories = new List();

            if (Debugger.IsAttached)
            {
                excludeCategories.Add(CategoryConstants.ExceptionExpected);
            }

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                excludeCategories.Add(CategoryConstants.CaseInsensitiveFileSystemOnly);
            }
            else
            {
                excludeCategories.Add(CategoryConstants.CaseSensitiveFileSystemOnly);
            }

            Environment.ExitCode = runner.RunTests(includeCategories: null, excludeCategories: excludeCategories);

            if (Debugger.IsAttached)
            {
                Console.WriteLine("Tests completed. Press Enter to exit.");
                Console.ReadLine();
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Readme.md
================================================
# GVFS Unit Tests

## Unit Test Projects

### GVFS.UnitTests

* Targets .NET Core
* Contains all unit tests that are .NET Standard compliant

### GVFS.UnitTests.Windows

* Targets .NET Framework
* Contains all unit tests that depend on .NET Framework assemblies
* Has links (in the `NetCore` folder) to all of the unit tests in GVFS.UnitTests 

GVFS.UnitTests.Windows links in all of the tests from GVFS.UnitTests to ensure that they pass on both the .NET Core and .Net Framework platforms.

## Running Unit Tests

**Option 1: `Scripts\RunUnitTests.bat`**

`RunUnitTests.bat` will run both GVFS.UnitTests and GVFS.UnitTests.Windows

**Option 2: Run individual projects from Visual Studio**

GVFS.UnitTests and GVFS.UnitTests.Windows can both be run from Visual Studio.  Simply set either as the StartUp project and run them from the IDE.

## Adding New Tests

### GVFS.UnitTests or GVFS.UnitTests.Windows?

Whenever possible new unit tests should be added to GVFS.UnitTests. If the new tests are for a .NET Framework assembly (e.g. `GVFS.Platform.Windows`) 
then they will need to be added to GVFS.UnitTests.Windows.

### Adding New Test Files

When adding new test files, keep the following in mind:

* New test files that are added to GVFS.UnitTests will not appear in the `NetCore` folder of GVFS.UnitTests.Windows until the GVFS solution is reloaded.
* New test files that are meant to be run on both .NET platforms should be added to the **GVFS.UnitTests** project.


================================================
FILE: GVFS/GVFS.UnitTests/Service/RepoRegistryTests.cs
================================================
using GVFS.Service;
using GVFS.Service.Handlers;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace GVFS.UnitTests.Service
{
    [TestFixture]
    public class RepoRegistryTests
    {
        private Mock mockRepoMounter;
        private Mock mockNotificationHandler;

         [SetUp]
        public void Setup()
        {
            this.mockRepoMounter = new Mock(MockBehavior.Strict);
            this.mockNotificationHandler = new Mock(MockBehavior.Strict);
        }

         [TearDown]
        public void TearDown()
        {
            this.mockRepoMounter.VerifyAll();
            this.mockNotificationHandler.VerifyAll();
        }

        [TestCase]
        public void TryRegisterRepo_EmptyRegistry()
        {
            string dataLocation = Path.Combine("mock:", "registryDataFolder");

            MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null));
            RepoRegistry registry = new RepoRegistry(
                new MockTracer(),
                fileSystem,
                dataLocation,
                this.mockRepoMounter.Object,
                this.mockNotificationHandler.Object);

            string repoRoot = Path.Combine("c:", "test");
            string ownerSID = Guid.NewGuid().ToString();

            string errorMessage;
            registry.TryRegisterRepo(repoRoot, ownerSID, out errorMessage).ShouldEqual(true);

            Dictionary verifiableRegistry = registry.ReadRegistry();
            verifiableRegistry.Count.ShouldEqual(1);
            this.VerifyRepo(verifiableRegistry[repoRoot], ownerSID, expectedIsActive: true);
        }

        [TestCase]
        public void ReadRegistry_Upgrade_ExistingVersion1()
        {
            string dataLocation = Path.Combine("mock:", "registryDataFolder");
            MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null));

            string repo1 = Path.Combine("mock:", "code", "repo1");
            string repo2 = Path.Combine("mock:", "code", "repo2");

            // Create a version 1 registry file
            fileSystem.WriteAllText(
                Path.Combine(dataLocation, RepoRegistry.RegistryName),
$@"1
{{""EnlistmentRoot"":""{repo1.Replace("\\", "\\\\")}"",""IsActive"":false}}
{{""EnlistmentRoot"":""{repo2.Replace("\\", "\\\\")}"",""IsActive"":true}}
");

            RepoRegistry registry = new RepoRegistry(
                new MockTracer(),
                fileSystem,
                dataLocation,
                this.mockRepoMounter.Object,
                this.mockNotificationHandler.Object);
            registry.Upgrade();

            Dictionary repos = registry.ReadRegistry();
            repos.Count.ShouldEqual(2);

            this.VerifyRepo(repos[repo1], expectedOwnerSID: null, expectedIsActive: false);
            this.VerifyRepo(repos[repo2], expectedOwnerSID: null, expectedIsActive: true);
        }

        [TestCase]
        public void ReadRegistry_Upgrade_NoRegistry()
        {
            string dataLocation = Path.Combine("mock:", "registryDataFolder");
            MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null));
            RepoRegistry registry = new RepoRegistry(
                new MockTracer(),
                fileSystem,
                dataLocation,
                this.mockRepoMounter.Object,
                this.mockNotificationHandler.Object);
            registry.Upgrade();

            Dictionary repos = registry.ReadRegistry();
            repos.Count.ShouldEqual(0);
        }

        [TestCase]
        public void TryGetActiveRepos_BeforeAndAfterActivateAndDeactivate()
        {
            string dataLocation = Path.Combine("mock:", "registryDataFolder");
            MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null));
            RepoRegistry registry = new RepoRegistry(
                new MockTracer(),
                fileSystem,
                dataLocation,
                this.mockRepoMounter.Object,
                this.mockNotificationHandler.Object);

            string repo1Root = Path.Combine("mock:", "test", "repo1");
            string owner1SID = Guid.NewGuid().ToString();
            string repo2Root = Path.Combine("mock:", "test", "repo2");
            string owner2SID = Guid.NewGuid().ToString();
            string repo3Root = Path.Combine("mock:", "test", "repo3");
            string owner3SID = Guid.NewGuid().ToString();

            // Register all 3 repos
            string errorMessage;
            registry.TryRegisterRepo(repo1Root, owner1SID, out errorMessage).ShouldEqual(true);
            registry.TryRegisterRepo(repo2Root, owner2SID, out errorMessage).ShouldEqual(true);
            registry.TryRegisterRepo(repo3Root, owner3SID, out errorMessage).ShouldEqual(true);

            // Confirm all 3 active
            List activeRepos;
            registry.TryGetActiveRepos(out activeRepos, out errorMessage);
            activeRepos.Count.ShouldEqual(3);
            this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo1Root)), owner1SID, expectedIsActive: true);
            this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo2Root)), owner2SID, expectedIsActive: true);
            this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo3Root)), owner3SID, expectedIsActive: true);

            // Deactive repo 2
            registry.TryDeactivateRepo(repo2Root, out errorMessage).ShouldEqual(true);

            // Confirm repos 1 and 3 still active
            registry.TryGetActiveRepos(out activeRepos, out errorMessage);
            activeRepos.Count.ShouldEqual(2);
            this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo1Root)), owner1SID, expectedIsActive: true);
            this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo3Root)), owner3SID, expectedIsActive: true);

            // Activate repo 2
            registry.TryRegisterRepo(repo2Root, owner2SID, out errorMessage).ShouldEqual(true);

            // Confirm all 3 active
            registry.TryGetActiveRepos(out activeRepos, out errorMessage);
            activeRepos.Count.ShouldEqual(3);
            this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo1Root)), owner1SID, expectedIsActive: true);
            this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo2Root)), owner2SID, expectedIsActive: true);
            this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo3Root)), owner3SID, expectedIsActive: true);
        }

        [TestCase]
        public void TryDeactivateRepo()
        {
            string dataLocation = Path.Combine("mock:", "registryDataFolder");
            MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null));
            RepoRegistry registry = new RepoRegistry(
                new MockTracer(),
                fileSystem,
                dataLocation,
                this.mockRepoMounter.Object,
                this.mockNotificationHandler.Object);

            string repo1Root = Path.Combine("mock:", "test", "repo1");
            string owner1SID = Guid.NewGuid().ToString();
            string errorMessage;
            registry.TryRegisterRepo(repo1Root, owner1SID, out errorMessage).ShouldEqual(true);

            List activeRepos;
            registry.TryGetActiveRepos(out activeRepos, out errorMessage);
            activeRepos.Count.ShouldEqual(1);
            this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo1Root)), owner1SID, expectedIsActive: true);

            // Deactivate repo
            registry.TryDeactivateRepo(repo1Root, out errorMessage).ShouldEqual(true);
            registry.TryGetActiveRepos(out activeRepos, out errorMessage);
            activeRepos.Count.ShouldEqual(0);

            // Deactivate repo again (no-op)
            registry.TryDeactivateRepo(repo1Root, out errorMessage).ShouldEqual(true);
            registry.TryGetActiveRepos(out activeRepos, out errorMessage);
            activeRepos.Count.ShouldEqual(0);

            // Repo should still be in the registry
            Dictionary verifiableRegistry = registry.ReadRegistry();
            verifiableRegistry.Count.ShouldEqual(1);
            this.VerifyRepo(verifiableRegistry[repo1Root], owner1SID, expectedIsActive: false);

            // Deactivate non-existent repo should fail
            string nonExistantPath = Path.Combine("mock:", "test", "doesNotExist");
            registry.TryDeactivateRepo(nonExistantPath, out errorMessage).ShouldEqual(false);
            errorMessage.ShouldContain("Attempted to deactivate non-existent repo");
        }

        [TestCase]
        public void TraceStatus()
        {
            string dataLocation = Path.Combine("mock:", "registryDataFolder");
            MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null));
            MockTracer tracer = new MockTracer();
            RepoRegistry registry = new RepoRegistry(
                tracer,
                fileSystem,
                dataLocation,
                this.mockRepoMounter.Object,
                this.mockNotificationHandler.Object);

            string repo1Root = Path.Combine("mock:", "test", "repo1");
            string owner1SID = Guid.NewGuid().ToString();
            string repo2Root = Path.Combine("mock:", "test", "repo2");
            string owner2SID = Guid.NewGuid().ToString();
            string repo3Root = Path.Combine("mock:", "test", "repo3");
            string owner3SID = Guid.NewGuid().ToString();

            string errorMessage;
            registry.TryRegisterRepo(repo1Root, owner1SID, out errorMessage).ShouldEqual(true);
            registry.TryRegisterRepo(repo2Root, owner2SID, out errorMessage).ShouldEqual(true);
            registry.TryRegisterRepo(repo3Root, owner3SID, out errorMessage).ShouldEqual(true);
            registry.TryDeactivateRepo(repo2Root, out errorMessage).ShouldEqual(true);

            registry.TraceStatus();

            Dictionary repos = registry.ReadRegistry();
            repos.Count.ShouldEqual(3);
            foreach (KeyValuePair kvp in repos)
            {
                tracer.RelatedInfoEvents.SingleOrDefault(message => message.Equals(kvp.Value.ToString())).ShouldNotBeNull();
            }
        }

        private void VerifyRepo(RepoRegistration repo, string expectedOwnerSID, bool expectedIsActive)
        {
            repo.ShouldNotBeNull();
            repo.OwnerSID.ShouldEqual(expectedOwnerSID);
            repo.IsActive.ShouldEqual(expectedIsActive);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Setup.cs
================================================
using GVFS.Common;
using GVFS.UnitTests.Mock.Common;
using NUnit.Framework;

namespace GVFS.UnitTests
{
    [SetUpFixture]
    public class Setup
    {
        [OneTimeSetUp]
        public void SetUp()
        {
            GVFSPlatform.Register(new MockPlatform());
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Tracing/EventListenerTests.cs
================================================
using System;
using GVFS.Common.Tracing;
using Moq;
using NUnit.Framework;

namespace GVFS.UnitTests.Tracing
{
    [TestFixture]
    public class EventListenerTests
    {
        [TestCase]
        public void EventListener_RecordMessage_ExceptionThrownInternally_RaisesFailureEventWithErrorMessage()
        {
            string expectedErrorMessage = $"test error message unique={Guid.NewGuid():N}";

            Mock eventSink = new Mock();

            TraceEventMessage message = new TraceEventMessage { Level = EventLevel.Error, Keywords = Keywords.None };
            TestEventListener listener = new TestEventListener(EventLevel.Informational, Keywords.Any, eventSink.Object)
            {
                RecordMessageInternalCallback = _ => throw new Exception(expectedErrorMessage)
            };

            listener.RecordMessage(message);

            eventSink.Verify(
                x => x.OnListenerFailure(listener, It.Is(msg => msg.Contains(expectedErrorMessage))),
                times: Times.Once);
        }

        private class TestEventListener : EventListener
        {
            public TestEventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink)
                : base(maxVerbosity, keywordFilter, eventSink)
            {
            }

            public Action RecordMessageInternalCallback { get; set; }

            protected override void RecordMessageInternal(TraceEventMessage message)
            {
                this.RecordMessageInternalCallback?.Invoke(message);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Tracing/QueuedPipeStringWriterTests.cs
================================================
using System;
using System.IO.Pipes;
using System.Threading;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using Moq;
using NUnit.Framework;

namespace GVFS.UnitTests.Tracing
{
    [TestFixture]
    public class QueuedPipeStringWriterTests
    {
        [TestCase]
        public void Stop_RaisesStateStopped()
        {
            // Capture event invocations
            Mock eventSink = new Mock();

            // createPipeFunc returns `null` since the test will never enqueue any messaged to write
            QueuedPipeStringWriter writer = new QueuedPipeStringWriter(
                () => null,
                eventSink.Object);

            // Try to write some dummy data
            writer.Start();
            writer.Stop();

            eventSink.Verify(
                x => x.OnStateChanged(writer, QueuedPipeStringWriterState.Stopped, null),
                Times.Once);
        }

        [TestCase]
        public void MissingPipe_RaisesStateFailing()
        {
            const string inputMessage = "FooBar";

            // Capture event invocations
            Mock eventSink = new Mock();

            QueuedPipeStringWriter writer = new QueuedPipeStringWriter(
                () => throw new Exception("Failing pipe connection"),
                eventSink.Object);

            // Try to write some dummy data
            writer.Start();
            bool queueOk = writer.TryEnqueue(inputMessage);
            writer.Stop();

            queueOk.ShouldBeTrue();
            eventSink.Verify(
                x => x.OnStateChanged(writer, QueuedPipeStringWriterState.Failing, It.IsAny()),
                Times.Once);
            eventSink.Verify(
                x => x.OnStateChanged(writer, QueuedPipeStringWriterState.Stopped, It.IsAny()),
                Times.Once);
        }

        [TestCase]
        public void GoodPipe_WritesDataAndRaisesStateHealthy()
        {
            const string inputMessage = "FooBar";
            byte[] expectedData =
            {
                0x46, 0x6F, 0x6F, 0x42, 0x61, 0x72, (byte)'\n', // "FooBar\n"
                0x46, 0x6F, 0x6F, 0x42, 0x61, 0x72, (byte)'\n', // "FooBar\n"
                0x46, 0x6F, 0x6F, 0x42, 0x61, 0x72, (byte)'\n', // "FooBar\n"
            };

            string pipeName = Guid.NewGuid().ToString("N");

            // Capture event invocations
            Mock eventSink = new Mock();

            QueuedPipeStringWriter writer = new QueuedPipeStringWriter(
                () => new NamedPipeClientStream(".", pipeName, PipeDirection.Out),
                eventSink.Object);

            using (TestPipeReaderWorker pipeWorker = new TestPipeReaderWorker(pipeName, PipeTransmissionMode.Byte))
            {
                // Start the pipe reader worker first and wait until the pipe server has been stood-up
                // before starting the pipe writer/enqueuing messages because the writer does not wait
                // for the pipe to be ready to accept (it returns and drops messages immediately).
                pipeWorker.Start();
                pipeWorker.WaitForReadyToAccept();
                writer.Start();

                // Try to write some dummy data
                bool queueOk1 = writer.TryEnqueue(inputMessage);
                bool queueOk2 = writer.TryEnqueue(inputMessage);
                bool queueOk3 = writer.TryEnqueue(inputMessage);

                // Wait until we've received all the sent messages before shuting down
                // the pipe worker thread.
                pipeWorker.WaitForRecievedBytes(count: expectedData.Length);
                pipeWorker.Stop();
                writer.Stop();

                queueOk1.ShouldBeTrue();
                queueOk2.ShouldBeTrue();
                queueOk3.ShouldBeTrue();

                byte[] actualData = pipeWorker.GetReceivedDataSnapshot();
                CollectionAssert.AreEqual(expectedData, actualData);

                // Should only receive one 'healthy' state change per successfully written message
                eventSink.Verify(
                    x => x.OnStateChanged(writer, QueuedPipeStringWriterState.Healthy, It.IsAny()),
                    Times.Once);
                eventSink.Verify(
                    x => x.OnStateChanged(writer, QueuedPipeStringWriterState.Stopped, It.IsAny()),
                    Times.Once);
            }
        }

        private class TestPipeReaderWorker : IDisposable
        {
            private readonly string pipeName;
            private readonly PipeTransmissionMode transmissionMode;
            private readonly AutoResetEvent receivedData = new AutoResetEvent(initialState: false);
            private readonly ManualResetEvent readyToAccept = new ManualResetEvent(initialState: false);
            private readonly ManualResetEvent shutdownEvent = new ManualResetEvent(initialState: false);

            private int bufferLength = 0;
            private byte[] buffer = new byte[16*1024];
            private object bufferLock = new object();
            private Thread thread;
            private bool isRunning;
            private bool isDisposed;

            public TestPipeReaderWorker(string pipeName, PipeTransmissionMode transmissionMode)
            {
                this.pipeName = pipeName;
                this.transmissionMode = transmissionMode;
            }

            public void Start()
            {
                if (!this.isRunning)
                {
                    this.isRunning = true;
                    this.thread = new Thread(this.ThreadProc)
                    {
                        Name = nameof(TestPipeReaderWorker),
                        IsBackground = true
                    };
                    this.thread.Start();
                }
            }

            public void WaitForReadyToAccept()
            {
                this.readyToAccept.WaitOne();
            }

            public void WaitForRecievedBytes(int count)
            {
                if (!this.isRunning)
                {
                    throw new InvalidOperationException("Worker has been stopped so will never receieve new data");
                }

                int length;

                while (true)
                {
                    // Since the buffer can only grow (and we only care about waiting for a minimum length), we
                    // don't care that the length could increase after we've released the lock.
                    lock (this.bufferLock)
                    {
                        length = this.bufferLength;
                    }

                    if (length >= count)
                    {
                        break;
                    }

                    // Wait for more data (the buffer will grow)
                    this.receivedData.WaitOne();
                }
            }

            public byte[] GetReceivedDataSnapshot()
            {
                if (this.isRunning)
                {
                    throw new InvalidOperationException("Should stop the test pipe worker first");
                }

                if (this.isDisposed)
                {
                    throw new ObjectDisposedException($"{nameof(TestPipeReaderWorker)}");
                }

                byte[] snapshot;

                lock (this.bufferLock)
                {
                    snapshot = new byte[this.bufferLength];
                    Array.Copy(this.buffer, snapshot, snapshot.Length);
                }

                return snapshot;
            }

            public void Stop()
            {
                if (this.isRunning)
                {
                    this.isRunning = false;
                    this.shutdownEvent.Set();
                    this.thread.Join();
                }
            }

            public void Dispose()
            {
                if (this.isDisposed)
                {
                    return;
                }

                this.Stop();
                this.isDisposed = true;
            }

            private void ThreadProc()
            {
                using (NamedPipeServerStream pipe = new NamedPipeServerStream(this.pipeName, PipeDirection.In, -1, this.transmissionMode, PipeOptions.Asynchronous))
                {
                    // Signal that the pipe has been created and we're ready to accept clients
                    this.readyToAccept.Set();

                    pipe.WaitForConnection();

                    while (this.isRunning)
                    {
                        byte[] readBuffer = new byte[1024];

                        IAsyncResult asyncResult = pipe.BeginRead(readBuffer, offset: 0, count: readBuffer.Length, callback: null, state: null);

                        // Wait for a read operation to complete, or until we're told to shutdown
                        WaitHandle.WaitAny(new[] { asyncResult.AsyncWaitHandle, this.shutdownEvent });

                        if (this.isRunning)
                        {
                            // Complete the read
                            int nr = pipe.EndRead(asyncResult);
                            if (nr > 0)
                            {
                                // We actually read some data so append this to the main buffer
                                lock (this.bufferLock)
                                {
                                    Array.Copy(readBuffer, 0, this.buffer, this.bufferLength, nr);
                                    this.bufferLength += nr;
                                }

                                this.receivedData.Set();
                            }
                            else
                            {
                                // We got here because the pipe has been closed.
                                // If we've been asked to shutdown we will break on the next while-loop evaluation.
                            }
                        }
                    }
                }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Tracing/TelemetryDaemonEventListenerTests.cs
================================================
using System.Collections.Generic;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using Newtonsoft.Json;
using NUnit.Framework;

namespace GVFS.UnitTests.Tracing
{
    [TestFixture]
    public class TelemetryDaemonEventListenerTests
    {
        [TestCase]
        public void TraceMessageDataIsCorrectFormat()
        {
            const string vfsVersion = "test-vfsVersion";
            const string providerName = "test-ProviderName";
            const string eventName = "test-eventName";
            const EventLevel level = EventLevel.Error;
            const EventOpcode opcode = EventOpcode.Start;
            const string enlistmentId = "test-enlistmentId";
            const string mountId = "test-mountId";
            const string gitCommandSessionId = "test_sessionId";
            const string payload = "test-payload";

            Dictionary expectedDict = new Dictionary
            {
                ["version"] = vfsVersion,
                ["providerName"] = providerName,
                ["eventName"] = eventName,
                ["eventLevel"] = (int)level,
                ["eventOpcode"] = (int)opcode,
                ["payload"] = new Dictionary
                {
                    ["enlistmentId"] = enlistmentId,
                    ["mountId"] = mountId,
                    ["gitCommandSessionId"] = gitCommandSessionId,
                    ["json"] = payload,
                },
            };

            TelemetryDaemonEventListener.PipeMessage message = new TelemetryDaemonEventListener.PipeMessage
            {
                Version = vfsVersion,
                ProviderName = providerName,
                EventName = eventName,
                EventLevel = level,
                EventOpcode = opcode,
                Payload = new TelemetryDaemonEventListener.PipeMessage.PipeMessagePayload
                {
                    EnlistmentId = enlistmentId,
                    MountId = mountId,
                    GitCommandSessionId = gitCommandSessionId,
                    Json = payload
                },
            };

            string messageJson = message.ToJson();

            Dictionary actualDict = JsonConvert.DeserializeObject>(messageJson);

            actualDict.Count.ShouldEqual(expectedDict.Count);
            actualDict["version"].ShouldEqual(expectedDict["version"]);
            actualDict["providerName"].ShouldEqual(expectedDict["providerName"]);
            actualDict["eventName"].ShouldEqual(expectedDict["eventName"]);
            actualDict["eventLevel"].ShouldEqual(expectedDict["eventLevel"]);
            actualDict["eventOpcode"].ShouldEqual(expectedDict["eventOpcode"]);

            Dictionary expectedPayloadDict = (Dictionary)expectedDict["payload"];
            Dictionary actualPayloadDict = JsonConvert.DeserializeObject>(actualDict["payload"].ToString());
            actualPayloadDict.Count.ShouldEqual(expectedPayloadDict.Count);
            actualPayloadDict["enlistmentId"].ShouldEqual(expectedPayloadDict["enlistmentId"]);
            actualPayloadDict["mountId"].ShouldEqual(expectedPayloadDict["mountId"]);
            actualPayloadDict["gitCommandSessionId"].ShouldEqual(expectedPayloadDict["gitCommandSessionId"]);
            actualPayloadDict["json"].ShouldEqual(expectedPayloadDict["json"]);
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Virtual/CommonRepoSetup.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using GVFS.UnitTests.Mock.Git;
using GVFS.Virtualization.FileSystem;
using System;
using System.IO;

namespace GVFS.UnitTests.Virtual
{
    public class CommonRepoSetup : IDisposable
    {
        public static readonly byte[] DefaultContentId = FileSystemVirtualizer.ConvertShaToContentId("0123456789012345678901234567890123456789");

        public CommonRepoSetup()
        {
            MockTracer tracer = new MockTracer();

            string enlistmentRoot = Path.Combine("mock:", "GVFS", "UnitTests", "Repo");
            GVFSEnlistment enlistment = new GVFSEnlistment(enlistmentRoot, "fake://repoUrl", "fake://gitBinPath", authentication: null);
            enlistment.InitializeCachePathsFromKey("fake:\\gvfsSharedCache", "fakeCacheKey");

            this.GitParentPath = enlistment.WorkingDirectoryRoot;
            this.GVFSMetadataPath = enlistment.DotGVFSRoot;

            MockDirectory enlistmentDirectory = new MockDirectory(
                enlistmentRoot,
                new MockDirectory[]
                {
                    new MockDirectory(this.GitParentPath, folders: null, files: null),
                },
                null);
            enlistmentDirectory.CreateFile(Path.Combine(this.GitParentPath, ".git", "config"), ".git config Contents", createDirectories: true);
            enlistmentDirectory.CreateFile(Path.Combine(this.GitParentPath, ".git", "HEAD"), ".git HEAD Contents", createDirectories: true);
            enlistmentDirectory.CreateFile(Path.Combine(this.GitParentPath, ".git", "logs", "HEAD"), "HEAD Contents", createDirectories: true);
            enlistmentDirectory.CreateFile(Path.Combine(this.GitParentPath, ".git", "info", "always_exclude"), "always_exclude Contents", createDirectories: true);
            enlistmentDirectory.CreateDirectory(enlistment.GitPackRoot);

            this.FileSystem = new MockFileSystem(enlistmentDirectory);
            this.Repository = new MockGitRepo(
                tracer,
                enlistment,
                this.FileSystem);
            CreateStandardGitTree(this.Repository);

            this.Context = new GVFSContext(tracer, this.FileSystem, this.Repository, enlistment);

            this.HttpObjects = new MockHttpGitObjects(tracer, enlistment);
            this.GitObjects = new MockGVFSGitObjects(this.Context, this.HttpObjects);
        }

        public GVFSContext Context { get; private set; }

        public string GitParentPath { get; private set; }

        public string GVFSMetadataPath { get; private set; }
        public GVFSGitObjects GitObjects { get; private set; }

        public MockGitRepo Repository { get; private set; }
        public MockHttpGitObjects HttpObjects { get; private set; }
        public MockFileSystem FileSystem { get; private set; }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.Context != null)
                {
                    this.Context.Dispose();
                    this.Context = null;
                }

                if (this.HttpObjects != null)
                {
                    this.HttpObjects.Dispose();
                    this.HttpObjects = null;
                }
            }
        }

        private static void CreateStandardGitTree(MockGitRepo repository)
        {
            string rootSha = repository.GetHeadTreeSha();

            string atreeSha = repository.AddChildTree(rootSha, "A");
            repository.AddChildBlob(atreeSha, "A.1.txt", "A.1 in GitTree");
            repository.AddChildBlob(atreeSha, "A.2.txt", "A.2 in GitTree");

            string btreeSha = repository.AddChildTree(rootSha, "B");
            repository.AddChildBlob(btreeSha, "B.1.txt", "B.1 in GitTree");

            string dupContentSha = repository.AddChildTree(rootSha, "DupContent");
            repository.AddChildBlob(dupContentSha, "dup1.txt", "This is some duplicate content");
            repository.AddChildBlob(dupContentSha, "dup2.txt", "This is some duplicate content");

            string dupTreeSha = repository.AddChildTree(rootSha, "DupTree");
            repository.AddChildBlob(dupTreeSha, "B.1.txt", "B.1 in GitTree");

            repository.AddChildBlob(rootSha, "C.txt", "C in GitTree");
        }
    }
}

================================================
FILE: GVFS/GVFS.UnitTests/Virtual/FileSystemVirtualizerTester.cs
================================================
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.FileSystem;
using GVFS.UnitTests.Mock.Virtualization.Background;
using GVFS.UnitTests.Mock.Virtualization.BlobSize;
using GVFS.UnitTests.Mock.Virtualization.Projection;
using GVFS.Virtualization.Background;
using GVFS.Virtualization.FileSystem;
using Moq;
using System;

namespace GVFS.UnitTests.Virtual
{
    public abstract class FileSystemVirtualizerTester : IDisposable
    {
        public const int NumberOfWorkThreads = 1;

        public FileSystemVirtualizerTester(CommonRepoSetup repo)
            : this(repo, new[] { "test.txt" })
        {
        }

        public FileSystemVirtualizerTester(CommonRepoSetup repo, string[] projectedFiles)
        {
            this.Repo = repo;
            this.MockPlaceholderDb = new Mock(MockBehavior.Strict);
            this.MockPlaceholderDb.Setup(x => x.GetCount()).Returns(1);
            this.MockSparseDb = new Mock(MockBehavior.Strict);
            this.BackgroundTaskRunner = new MockBackgroundFileSystemTaskRunner();
            this.GitIndexProjection = new MockGitIndexProjection(projectedFiles);
            this.Virtualizer = this.CreateVirtualizer(repo);
            this.FileSystemCallbacks = new MockFileSystemCallbacks(
                repo.Context,
                repo.GitObjects,
                RepoMetadata.Instance,
                new MockBlobSizes(),
                this.GitIndexProjection,
                this.BackgroundTaskRunner,
                this.Virtualizer,
                this.MockPlaceholderDb.Object,
                this.MockSparseDb.Object);

            this.FileSystemCallbacks.TryStart(out string error).ShouldEqual(true);
        }

        public CommonRepoSetup Repo { get; }
        public Mock MockPlaceholderDb { get; }
        public Mock MockSparseDb { get; }

        public MockBackgroundFileSystemTaskRunner BackgroundTaskRunner { get; }
        public MockGitIndexProjection GitIndexProjection { get; }
        public FileSystemVirtualizer Virtualizer { get; }
        public MockFileSystemCallbacks FileSystemCallbacks { get; }

        public virtual void Dispose()
        {
            this.FileSystemCallbacks?.Stop();
            this.MockPlaceholderDb.VerifyAll();
            this.MockSparseDb.VerifyAll();
            this.FileSystemCallbacks?.Dispose();
            this.Virtualizer?.Dispose();
            this.GitIndexProjection?.Dispose();
            this.BackgroundTaskRunner?.Dispose();
        }

        public void BackgroundTaskShouldBeScheduled(string expectedPath, FileSystemTask.OperationType expectedOperationType)
        {
            this.BackgroundTaskRunner.Count.ShouldEqual(1);
            this.BackgroundTaskRunner.BackgroundTasks[0].Operation.ShouldEqual(expectedOperationType);
            this.BackgroundTaskRunner.BackgroundTasks[0].VirtualPath.ShouldEqual(expectedPath);
        }

        protected abstract FileSystemVirtualizer CreateVirtualizer(CommonRepoSetup repo);
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Virtual/TestsWithCommonRepo.cs
================================================
using GVFS.Common;
using GVFS.UnitTests.Mock.Common;
using NUnit.Framework;

namespace GVFS.UnitTests.Virtual
{
    [TestFixture]
    public abstract class TestsWithCommonRepo
    {
        protected CommonRepoSetup Repo { get; private set; }

        [SetUp]
        public virtual void TestSetup()
        {
            this.Repo = new CommonRepoSetup();

            string error;
            RepoMetadata.TryInitialize(
                new MockTracer(),
                this.Repo.Context.FileSystem,
                this.Repo.Context.Enlistment.DotGVFSRoot,
                out error);
        }

        [TearDown]
        public virtual void TestTearDown()
        {
            if (this.Repo != null)
            {
                this.Repo.Dispose();
            }

            RepoMetadata.Shutdown();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Virtualization/FileSystemCallbacksTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.Virtualization.Background;
using GVFS.UnitTests.Mock.Virtualization.BlobSize;
using GVFS.UnitTests.Mock.Virtualization.FileSystem;
using GVFS.UnitTests.Mock.Virtualization.Projection;
using GVFS.UnitTests.Virtual;
using GVFS.Virtualization;
using GVFS.Virtualization.Background;
using Moq;
using Newtonsoft.Json;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;

namespace GVFS.UnitTests.Virtualization
{
    [TestFixture]
    public class FileSystemCallbacksTests : TestsWithCommonRepo
    {
        [TestCase]
        public void EmptyStringIsNotInsideDotGitPath()
        {
            FileSystemCallbacks.IsPathInsideDotGit(string.Empty).ShouldEqual(false);
        }

        [TestCase]
        public void IsPathInsideDotGitIsTrueForDotGitPath()
        {
            FileSystemCallbacks.IsPathInsideDotGit(@".git" + Path.DirectorySeparatorChar).ShouldEqual(true);
            FileSystemCallbacks.IsPathInsideDotGit(Path.Combine(".git", "test_file.txt")).ShouldEqual(true);
            FileSystemCallbacks.IsPathInsideDotGit(Path.Combine(".git", "test_folder", "test_file.txt")).ShouldEqual(true);
        }

        [TestCase]
        [Category(CategoryConstants.CaseInsensitiveFileSystemOnly)]
        public void IsPathInsideDotGitIsTrueForDifferentCaseDotGitPath()
        {
            FileSystemCallbacks.IsPathInsideDotGit(@".GIT" + Path.DirectorySeparatorChar).ShouldEqual(true);
            FileSystemCallbacks.IsPathInsideDotGit(Path.Combine(".GIT", "test_file.txt")).ShouldEqual(true);
            FileSystemCallbacks.IsPathInsideDotGit(Path.Combine(".GIT", "test_folder", "test_file.txt")).ShouldEqual(true);
        }

        [TestCase]
        public void IsPathInsideDotGitIsFalseForNonDotGitPath()
        {
            FileSystemCallbacks.IsPathInsideDotGit(@".git").ShouldEqual(false);
            FileSystemCallbacks.IsPathInsideDotGit(@".GIT").ShouldEqual(false);
            FileSystemCallbacks.IsPathInsideDotGit(@".gitattributes").ShouldEqual(false);
            FileSystemCallbacks.IsPathInsideDotGit(@".gitignore").ShouldEqual(false);
            FileSystemCallbacks.IsPathInsideDotGit(@".gitsubfolder\").ShouldEqual(false);
            FileSystemCallbacks.IsPathInsideDotGit(@".gitsubfolder\test_file.txt").ShouldEqual(false);
            FileSystemCallbacks.IsPathInsideDotGit(@"test_file.txt").ShouldEqual(false);
            FileSystemCallbacks.IsPathInsideDotGit(@"test_folder\test_file.txt").ShouldEqual(false);
        }

        [TestCase]
        public void BackgroundOperationCountMatchesBackgroundFileSystemTaskRunner()
        {
            Mock mockPlaceholderDb = new Mock(MockBehavior.Strict);
            mockPlaceholderDb.Setup(x => x.GetCount()).Returns(1);
            Mock mockSparseDb = new Mock(MockBehavior.Strict);
            mockSparseDb.Setup(x => x.GetAll()).Returns(new HashSet());
            using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner())
            using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks(
                this.Repo.Context,
                this.Repo.GitObjects,
                RepoMetadata.Instance,
                new MockBlobSizes(),
                gitIndexProjection: null,
                backgroundFileSystemTaskRunner: backgroundTaskRunner,
                fileSystemVirtualizer: null,
                placeholderDatabase: mockPlaceholderDb.Object,
                sparseCollection: mockSparseDb.Object))
            {
                fileSystemCallbacks.BackgroundOperationCount.ShouldEqual(backgroundTaskRunner.Count);

                fileSystemCallbacks.OnFileConvertedToFull("Path1.txt");
                fileSystemCallbacks.OnFileConvertedToFull("Path2.txt");
                backgroundTaskRunner.Count.ShouldEqual(2);
                fileSystemCallbacks.BackgroundOperationCount.ShouldEqual(backgroundTaskRunner.Count);
            }

            mockPlaceholderDb.VerifyAll();
            mockSparseDb.VerifyAll();
        }

        [TestCase]
        public void GetMetadataForHeartBeatDoesNotChangeEventLevelWhenNoPlaceholderHaveBeenCreated()
        {
            Mock mockPlaceholderDb = new Mock(MockBehavior.Strict);
            mockPlaceholderDb.Setup(x => x.GetCount()).Returns(0);
            mockPlaceholderDb.Setup(x => x.GetFilePlaceholdersCount()).Returns(() => 0);
            mockPlaceholderDb.Setup(x => x.GetFolderPlaceholdersCount()).Returns(() => 0);
            Mock mockSparseDb = new Mock(MockBehavior.Strict);
            mockSparseDb.Setup(x => x.GetAll()).Returns(new HashSet());
            using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner())
            using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks(
                this.Repo.Context,
                this.Repo.GitObjects,
                RepoMetadata.Instance,
                new MockBlobSizes(),
                gitIndexProjection: null,
                backgroundFileSystemTaskRunner: backgroundTaskRunner,
                fileSystemVirtualizer: null,
                placeholderDatabase: mockPlaceholderDb.Object,
                sparseCollection: mockSparseDb.Object))
            {
                EventMetadata metadata = fileSystemCallbacks.GetAndResetHeartBeatMetadata(out bool writeToLogFile);
                EventLevel eventLevel = writeToLogFile ? EventLevel.Informational : EventLevel.Verbose;
                eventLevel.ShouldEqual(EventLevel.Verbose);

                // "ModifiedPathsCount" should be 1 because ".gitattributes" is always present
                metadata.ShouldContain("ModifiedPathsCount", 1);
                metadata.ShouldContain("FilePlaceholderCount", 0);
                metadata.ShouldContain(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId);
            }

            mockPlaceholderDb.VerifyAll();
            mockSparseDb.VerifyAll();
        }

        [TestCase]
        public void GetMetadataForHeartBeatDoesSetsEventLevelToInformationalWhenPlaceholdersHaveBeenCreated()
        {
            Mock mockPlaceholderDb = new Mock(MockBehavior.Strict);
            int filePlaceholderCount = 0;
            int folderPlaceholderCount = 0;
            mockPlaceholderDb.Setup(x => x.GetCount()).Returns(() => filePlaceholderCount + folderPlaceholderCount);
            mockPlaceholderDb.Setup(x => x.AddFile("test.txt", "1111122222333334444455555666667777788888")).Callback(() => ++filePlaceholderCount);
            mockPlaceholderDb.Setup(x => x.AddFile("test.txt", "2222233333444445555566666777778888899999")).Callback(() => ++filePlaceholderCount);
            mockPlaceholderDb.Setup(x => x.AddFile("test.txt", "3333344444555556666677777888889999900000")).Callback(() => ++filePlaceholderCount);
            mockPlaceholderDb.Setup(x => x.AddPartialFolder("foo", null)).Callback(() => ++folderPlaceholderCount);
            mockPlaceholderDb.Setup(x => x.GetFilePlaceholdersCount()).Returns(() => filePlaceholderCount);
            mockPlaceholderDb.Setup(x => x.GetFolderPlaceholdersCount()).Returns(() => folderPlaceholderCount);
            Mock mockSparseDb = new Mock(MockBehavior.Strict);
            mockSparseDb.Setup(x => x.GetAll()).Returns(new HashSet());
            using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner())
            using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks(
                this.Repo.Context,
                this.Repo.GitObjects,
                RepoMetadata.Instance,
                new MockBlobSizes(),
                gitIndexProjection: null,
                backgroundFileSystemTaskRunner: backgroundTaskRunner,
                fileSystemVirtualizer: null,
                placeholderDatabase: mockPlaceholderDb.Object,
                sparseCollection: mockSparseDb.Object))
            {
                fileSystemCallbacks.OnPlaceholderFileCreated("test.txt", "1111122222333334444455555666667777788888", "GVFS.UnitTests.exe");

                EventMetadata metadata = fileSystemCallbacks.GetAndResetHeartBeatMetadata(out bool writeToLogFile);
                EventLevel eventLevel = writeToLogFile ? EventLevel.Informational : EventLevel.Verbose;
                eventLevel.ShouldEqual(EventLevel.Informational);

                // "ModifiedPathsCount" should be 1 because ".gitattributes" is always present
                metadata.Count.ShouldEqual(8);
                metadata.ContainsKey("FilePlaceholderCreation").ShouldBeTrue();
                metadata.TryGetValue("FilePlaceholderCreation", out object fileNestedMetadata);
                JsonConvert.SerializeObject(fileNestedMetadata).ShouldContain("\"ProcessName1\":\"GVFS.UnitTests.exe\"");
                JsonConvert.SerializeObject(fileNestedMetadata).ShouldContain("\"ProcessCount1\":1");
                metadata.ShouldContain("ModifiedPathsCount", 1);
                metadata.ShouldContain("FilePlaceholderCount", 1);
                metadata.ShouldContain("FolderPlaceholderCount", 0);
                metadata.ShouldContain(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId);
                metadata.ContainsKey("PhysicalDiskInfo").ShouldBeTrue();

                // Create more placeholders
                fileSystemCallbacks.OnPlaceholderFileCreated("test.txt", "2222233333444445555566666777778888899999", "GVFS.UnitTests.exe2");
                fileSystemCallbacks.OnPlaceholderFileCreated("test.txt", "3333344444555556666677777888889999900000", "GVFS.UnitTests.exe2");
                fileSystemCallbacks.OnPlaceholderFolderCreated("foo", "GVFS.UnitTests.exe2");

                // Hydrate a file
                fileSystemCallbacks.OnPlaceholderFileHydrated("GVFS.UnitTests.exe2");

                metadata = fileSystemCallbacks.GetAndResetHeartBeatMetadata(out bool writeToLogFile2);
                eventLevel = writeToLogFile2 ? EventLevel.Informational : EventLevel.Verbose;
                eventLevel.ShouldEqual(EventLevel.Informational);

                metadata.Count.ShouldEqual(8);

                // Only processes that have created placeholders since the last heartbeat should be named
                metadata.ContainsKey("FilePlaceholderCreation").ShouldBeTrue();
                metadata.TryGetValue("FilePlaceholderCreation", out object fileNestedMetadata2);
                JsonConvert.SerializeObject(fileNestedMetadata2).ShouldContain("\"ProcessName1\":\"GVFS.UnitTests.exe2\"");
                JsonConvert.SerializeObject(fileNestedMetadata2).ShouldContain("\"ProcessCount1\":2");
                metadata.ContainsKey("FolderPlaceholderCreation").ShouldBeTrue();
                metadata.TryGetValue("FolderPlaceholderCreation", out object folderNestedMetadata2);
                JsonConvert.SerializeObject(folderNestedMetadata2).ShouldContain("\"ProcessName1\":\"GVFS.UnitTests.exe2\"");
                JsonConvert.SerializeObject(folderNestedMetadata2).ShouldContain("\"ProcessCount1\":1");
                metadata.ContainsKey("FilePlaceholdersHydrated").ShouldBeTrue();
                metadata.TryGetValue("FilePlaceholdersHydrated", out object hydrationNestedMetadata2);
                JsonConvert.SerializeObject(hydrationNestedMetadata2).ShouldContain("\"ProcessName1\":\"GVFS.UnitTests.exe2\"");
                JsonConvert.SerializeObject(hydrationNestedMetadata2).ShouldContain("\"ProcessCount1\":1");
                metadata.ShouldContain("ModifiedPathsCount", 1);
                metadata.ShouldContain("FilePlaceholderCount", 3);
                metadata.ShouldContain("FolderPlaceholderCount", 1);
                metadata.ShouldContain(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId);
                metadata.ContainsKey("PhysicalDiskInfo").ShouldBeTrue();
            }

            mockPlaceholderDb.VerifyAll();
            mockSparseDb.VerifyAll();
        }

        [TestCase]
        public void IsReadyForExternalAcquireLockRequests()
        {
            Mock mockPlaceholderDb = new Mock(MockBehavior.Strict);
            mockPlaceholderDb.Setup(x => x.GetCount()).Returns(1);
            Mock mockSparseDb = new Mock(MockBehavior.Strict);
            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 (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks(
                this.Repo.Context,
                this.Repo.GitObjects,
                RepoMetadata.Instance,
                new MockBlobSizes(),
                gitIndexProjection: gitIndexProjection,
                backgroundFileSystemTaskRunner: backgroundTaskRunner,
                fileSystemVirtualizer: fileSystemVirtualizer,
                placeholderDatabase: mockPlaceholderDb.Object,
                sparseCollection: mockSparseDb.Object))
            {
                string denyMessage;
                fileSystemCallbacks.IsReadyForExternalAcquireLockRequests(
                    new NamedPipeMessages.LockData(
                        pid: 0,
                        isElevated: false,
                        checkAvailabilityOnly: false,
                        parsedCommand: "git dummy-command",
                        gitCommandSessionId: "123"),
                    out denyMessage).ShouldBeFalse();
                denyMessage.ShouldEqual("Waiting for GVFS to parse index and update placeholder files");

                string error;
                fileSystemCallbacks.TryStart(out error).ShouldBeTrue();
                gitIndexProjection.ProjectionParseComplete = false;
                fileSystemCallbacks.IsReadyForExternalAcquireLockRequests(
                    new NamedPipeMessages.LockData(
                        pid: 0,
                        isElevated: false,
                        checkAvailabilityOnly: false,
                        parsedCommand: "git dummy-command",
                        gitCommandSessionId: "123"),
                    out denyMessage).ShouldBeFalse();
                denyMessage.ShouldEqual("Waiting for GVFS to parse index and update placeholder files");

                // Put something on the background queue
                fileSystemCallbacks.OnFileCreated("NewFilePath.txt");
                backgroundTaskRunner.Count.ShouldEqual(1);
                fileSystemCallbacks.IsReadyForExternalAcquireLockRequests(
                    new NamedPipeMessages.LockData(
                        pid: 0,
                        isElevated: false,
                        checkAvailabilityOnly: false,
                        parsedCommand: "git dummy-command",
                        gitCommandSessionId: "123"),
                    out denyMessage).ShouldBeFalse();
                denyMessage.ShouldEqual("Waiting for GVFS to release the lock");

                backgroundTaskRunner.BackgroundTasks.Clear();
                gitIndexProjection.ProjectionParseComplete = true;
                fileSystemCallbacks.IsReadyForExternalAcquireLockRequests(
                    new NamedPipeMessages.LockData(
                        pid: 0,
                        isElevated: false,
                        checkAvailabilityOnly: false,
                        parsedCommand: "git dummy-command",
                        gitCommandSessionId: "123"),
                    out denyMessage).ShouldBeTrue();
                denyMessage.ShouldEqual("Waiting for GVFS to release the lock");

                fileSystemCallbacks.Stop();
            }

            mockPlaceholderDb.VerifyAll();
            mockSparseDb.VerifyAll();
        }

        [TestCase]
        public void FileAndFolderCallbacksScheduleBackgroundTasks()
        {
            Mock mockPlaceholderDb = new Mock(MockBehavior.Strict);
            mockPlaceholderDb.Setup(x => x.GetCount()).Returns(1);
            Mock mockSparseDb = new Mock(MockBehavior.Strict);
            mockSparseDb.Setup(x => x.GetAll()).Returns(new HashSet());
            using (MockBackgroundFileSystemTaskRunner backgroundTaskRunner = new MockBackgroundFileSystemTaskRunner())
            using (FileSystemCallbacks fileSystemCallbacks = new FileSystemCallbacks(
                this.Repo.Context,
                this.Repo.GitObjects,
                RepoMetadata.Instance,
                new MockBlobSizes(),
                gitIndexProjection: null,
                backgroundFileSystemTaskRunner: backgroundTaskRunner,
                fileSystemVirtualizer: null,
                placeholderDatabase: mockPlaceholderDb.Object,
                sparseCollection: mockSparseDb.Object))
            {
                this.CallbackSchedulesBackgroundTask(
                    backgroundTaskRunner,
                    (path) => fileSystemCallbacks.OnFileConvertedToFull(path),
                    "OnFileConvertedToFull.txt",
                    FileSystemTask.OperationType.OnFileConvertedToFull);

                this.CallbackSchedulesBackgroundTask(
                    backgroundTaskRunner,
                    (path) => fileSystemCallbacks.OnFileCreated(path),
                    "OnFileCreated.txt",
                    FileSystemTask.OperationType.OnFileCreated);

                this.CallbackSchedulesBackgroundTask(
                    backgroundTaskRunner,
                    (path) => fileSystemCallbacks.OnFileDeleted(path),
                    "OnFileDeleted.txt",
                    FileSystemTask.OperationType.OnFileDeleted);

                this.CallbackSchedulesBackgroundTask(
                    backgroundTaskRunner,
                    (path) => fileSystemCallbacks.OnFileOverwritten(path),
                    "OnFileOverwritten.txt",
                    FileSystemTask.OperationType.OnFileOverwritten);

                this.CallbackSchedulesBackgroundTask(
                    backgroundTaskRunner,
                    (oldPath, newPath) => fileSystemCallbacks.OnFileRenamed(oldPath, newPath),
                    "OnFileRenamed.txt",
                    "OnFileRenamed2.txt",
                    FileSystemTask.OperationType.OnFileRenamed);

                this.CallbackSchedulesBackgroundTask(
                    backgroundTaskRunner,
                    (existingPath, newLink) => fileSystemCallbacks.OnFileHardLinkCreated(newLink, existingPath),
                    string.Empty,
                    "OnFileHardLinkCreated.txt",
                    FileSystemTask.OperationType.OnFileHardLinkCreated);

                this.CallbackSchedulesBackgroundTask(
                    backgroundTaskRunner,
                    (path) => fileSystemCallbacks.OnFileSuperseded(path),
                    "OnFileSuperseded.txt",
                    FileSystemTask.OperationType.OnFileSuperseded);

                this.CallbackSchedulesBackgroundTask(
                    backgroundTaskRunner,
                    (path) => fileSystemCallbacks.OnFolderCreated(path, out _),
                    "OnFolderCreated.txt",
                    FileSystemTask.OperationType.OnFolderCreated);

                this.CallbackSchedulesBackgroundTask(
                    backgroundTaskRunner,
                    (path) => fileSystemCallbacks.OnFolderDeleted(path),
                    "OnFolderDeleted.txt",
                    FileSystemTask.OperationType.OnFolderDeleted);

                this.CallbackSchedulesBackgroundTask(
                    backgroundTaskRunner,
                    (oldPath, newPath) => fileSystemCallbacks.OnFolderRenamed(oldPath, newPath),
                    "OnFolderRenamed.txt",
                    "OnFolderRenamed2.txt",
                    FileSystemTask.OperationType.OnFolderRenamed);
            }

            mockPlaceholderDb.VerifyAll();
            mockSparseDb.VerifyAll();
        }

        [TestCase]
        public void TestFileSystemOperationsInvalidateStatusCache()
        {
            Mock mockPlaceholderDb = new Mock(MockBehavior.Strict);
            mockPlaceholderDb.Setup(x => x.GetCount()).Returns(1);
            Mock mockSparseDb = new Mock(MockBehavior.Strict);
            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,
                placeholderDatabase: mockPlaceholderDb.Object,
                sparseCollection: mockSparseDb.Object,
                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, x => fileSystemCallbacks.OnFolderCreated(x, out _), "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);
            }

            mockPlaceholderDb.VerifyAll();
            mockSparseDb.VerifyAll();
        }

        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,
            string path,
            FileSystemTask.OperationType operationType)
        {
            callback(path);
            backgroundTaskRunner.Count.ShouldEqual(1);
            backgroundTaskRunner.BackgroundTasks[0].Operation.ShouldEqual(operationType);
            backgroundTaskRunner.BackgroundTasks[0].VirtualPath.ShouldEqual(path);
            backgroundTaskRunner.BackgroundTasks.Clear();
        }

        private void CallbackSchedulesBackgroundTask(
            MockBackgroundFileSystemTaskRunner backgroundTaskRunner,
            Action callback,
            string oldPath,
            string newPath,
            FileSystemTask.OperationType operationType)
        {
            callback(oldPath, newPath);
            backgroundTaskRunner.Count.ShouldEqual(1);
            backgroundTaskRunner.BackgroundTasks[0].Operation.ShouldEqual(operationType);
            backgroundTaskRunner.BackgroundTasks[0].OldVirtualPath.ShouldEqual(oldPath);
            backgroundTaskRunner.BackgroundTasks[0].VirtualPath.ShouldEqual(newPath);
            backgroundTaskRunner.BackgroundTasks.Clear();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Virtualization/Projection/GitIndexEntryTests.cs
================================================
using GVFS.Tests;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using NUnit.Framework;
using System;
using System.IO;
using System.Text;
using static GVFS.Virtualization.Projection.GitIndexProjection;

namespace GVFS.UnitTests.Virtualization.Git
{
    [TestFixtureSource(typeof(DataSources), nameof(DataSources.AllBools))]
    public class GitIndexEntryTests
    {
        private const int DefaultIndexEntryCount = 10;
        private bool buildingNewProjection;

        public GitIndexEntryTests(bool buildingNewProjection)
        {
            this.buildingNewProjection = buildingNewProjection;
        }

        [OneTimeSetUp]
        public void Setup()
        {
            LazyUTF8String.InitializePools(new MockTracer(), DefaultIndexEntryCount);
        }

        [TestCase]
        public void TopLevelPath()
        {
            string[] pathParts = new[] { ".gitignore" };
            GitIndexEntry indexEntry = this.SetupIndexEntry(".gitignore");
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);
        }

        [TestCase]
        public void TwoLevelPath()
        {
            string[] pathParts = new[] { "folder", "file.txt" };
            GitIndexEntry indexEntry = this.SetupIndexEntry(string.Join("/", pathParts));
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);
        }

        [TestCase]
        public void ReplaceFileName()
        {
            string[] pathParts = new[] { "folder", "file.txt" };
            GitIndexEntry indexEntry = this.SetupIndexEntry(string.Join("/", pathParts));
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);

            string[] pathParts2 = new[] { "folder", "newfile.txt" };
            this.ParsePathForIndexEntry(indexEntry, string.Join("/", pathParts2), replaceIndex: 7);
            this.TestPathParts(indexEntry, pathParts2, hasSameParent: true);
        }

        [TestCase]
        public void ReplaceNonASCIIFileName()
        {
            string[] pathParts = new[] { "توبر", "مارسأغ", "FCIBBinaries.kml" };
            string path = string.Join("/", pathParts);
            GitIndexEntry indexEntry = this.SetupIndexEntry(path);
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);

            string[] pathParts2 = new[] { "توبر", "مارسأغ", "FCIBBinaries.txt" };
            this.ParsePathForIndexEntry(indexEntry, string.Join("/", pathParts2), replaceIndex: Encoding.UTF8.GetByteCount(path) - 3);
            this.TestPathParts(indexEntry, pathParts2, hasSameParent: true);
        }

        [TestCase]
        public void ReplaceFileNameShorter()
        {
            string[] pathParts = new[] { "MergedComponents", "InstrumentedBinCatalogs", "dirs" };
            GitIndexEntry indexEntry = this.SetupIndexEntry(string.Join("/", pathParts));
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);

            string[] pathParts2 = new[] { "MergedComponents", "InstrumentedBinCatalogs", "pgi", "sources.dep" };
            this.ParsePathForIndexEntry(indexEntry, string.Join("/", pathParts2), replaceIndex: 41);
            this.TestPathParts(indexEntry, pathParts2, hasSameParent: false);
        }

        [TestCase]
        public void TestComponentsWithSimilarNames()
        {
            string[] pathParts = new[] { "MergedComponents", "SDK", "FCIBBinaries.kml" };
            string path = string.Join("/", pathParts);
            GitIndexEntry indexEntry = this.SetupIndexEntry(path);
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);

            string[] pathParts2 = new[] { "MergedComponents", "SDK", "FCIBBinaries", "TH2Legacy", "amd64", "mdmerge.exe" };
            this.ParsePathForIndexEntry(indexEntry, string.Join("/", pathParts2), replaceIndex: path.Length - 4);
            this.TestPathParts(indexEntry, pathParts2, hasSameParent: false);
        }

        [TestCase]
        public void TestComponentsWithSimilarNonASCIINames()
        {
            string[] pathParts = new[] { "توبر", "مارسأغ", "FCIBBinaries.kml" };
            string path = string.Join("/", pathParts);
            GitIndexEntry indexEntry = this.SetupIndexEntry(path);
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);

            string[] pathParts2 = new[] { "توبر", "مارسأغ", "FCIBBinaries", "TH2Legacy", "amd64", "mdmerge.exe" };
            this.ParsePathForIndexEntry(indexEntry, string.Join("/", pathParts2), replaceIndex: Encoding.UTF8.GetByteCount(path) - 4);
            this.TestPathParts(indexEntry, pathParts2, hasSameParent: false);
        }

        [TestCase]
        public void AddFolder()
        {
            string[] pathParts = new[] { "folder", "file.txt" };
            GitIndexEntry indexEntry = this.SetupIndexEntry(string.Join("/", pathParts));
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);

            string[] pathParts2 = new[] { "folder", "folder2", "file.txt" };
            this.ParsePathForIndexEntry(indexEntry, string.Join("/", pathParts2), replaceIndex: 8);
            this.TestPathParts(indexEntry, pathParts2, hasSameParent: false);
        }

        [TestCase]
        public void RemoveFolder()
        {
            string[] pathParts = new[] { "folder", "folder2", "file.txt" };
            GitIndexEntry indexEntry = this.SetupIndexEntry(string.Join("/", pathParts));
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);

            string[] pathParts2 = new[] { "folder", "file.txt" };
            this.ParsePathForIndexEntry(indexEntry, string.Join("/", pathParts2), replaceIndex: 8);
            this.TestPathParts(indexEntry, pathParts2, hasSameParent: false);
        }

        [TestCase]
        public void NewSimilarRootFolder()
        {
            string[] pathParts = new[] { "folder", "file.txt" };
            GitIndexEntry indexEntry = this.SetupIndexEntry(string.Join("/", pathParts));
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);

            string[] pathParts2 = new[] { "folder1", "file.txt" };
            this.ParsePathForIndexEntry(indexEntry, string.Join("/", pathParts2), replaceIndex: 6);
            this.TestPathParts(indexEntry, pathParts2, hasSameParent: false);
        }

        [TestCase]
        public void ReplaceFullPath()
        {
            string[] pathParts = new[] { "folder", "file.txt" };
            GitIndexEntry indexEntry = this.SetupIndexEntry(string.Join("/", pathParts));
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);

            string[] pathParts2 = new[] { "another", "one", "new.txt" };
            this.ParsePathForIndexEntry(indexEntry, string.Join("/", pathParts2), replaceIndex: 0);
            this.TestPathParts(indexEntry, pathParts2, hasSameParent: false);
        }

        [TestCase]
        public void ClearLastParent()
        {
            string[] pathParts = new[] { "folder", "one", "file.txt" };
            GitIndexEntry indexEntry = this.SetupIndexEntry(string.Join("/", pathParts));
            this.TestPathParts(indexEntry, pathParts, hasSameParent: false);

            string[] pathParts2 = new[] { "folder", "one", "newfile.txt" };
            this.ParsePathForIndexEntry(indexEntry, string.Join("/", pathParts2), replaceIndex: 11);
            this.TestPathParts(indexEntry, pathParts2, hasSameParent: true);

            if (this.buildingNewProjection)
            {
                indexEntry.BuildingProjection_LastParent = new FolderData();
                indexEntry.ClearLastParent();
                indexEntry.BuildingProjection_HasSameParentAsLastEntry.ShouldBeFalse();
                indexEntry.BuildingProjection_LastParent.ShouldBeNull();
            }
        }

        private GitIndexEntry SetupIndexEntry(string path)
        {
            GitIndexEntry indexEntry = new GitIndexEntry(this.buildingNewProjection);
            this.ParsePathForIndexEntry(indexEntry, path, replaceIndex: 0);
            return indexEntry;
        }

        private void ParsePathForIndexEntry(GitIndexEntry indexEntry, string path, int replaceIndex)
        {
            byte[] pathBuffer = Encoding.UTF8.GetBytes(path);
            Buffer.BlockCopy(pathBuffer, 0, indexEntry.PathBuffer, 0, pathBuffer.Length);
            indexEntry.PathLength = pathBuffer.Length;
            indexEntry.ReplaceIndex = replaceIndex;

            if (this.buildingNewProjection)
            {
                indexEntry.BuildingProjection_ParsePath();
            }
            else
            {
                indexEntry.BackgroundTask_ParsePath();
            }
        }

        private void TestPathParts(GitIndexEntry indexEntry, string[] pathParts, bool hasSameParent)
        {
            if (this.buildingNewProjection)
            {
                indexEntry.BuildingProjection_HasSameParentAsLastEntry.ShouldEqual(hasSameParent, nameof(indexEntry.BuildingProjection_HasSameParentAsLastEntry));
                indexEntry.BuildingProjection_NumParts.ShouldEqual(pathParts.Length, nameof(indexEntry.BuildingProjection_NumParts));
            }

            for (int i = 0; i < pathParts.Length; i++)
            {
                if (this.buildingNewProjection)
                {
                    indexEntry.BuildingProjection_PathParts[i].ShouldNotBeNull();
                    indexEntry.BuildingProjection_PathParts[i].GetString().ShouldEqual(pathParts[i]);
                }
            }

            if (this.buildingNewProjection)
            {
                indexEntry.BuildingProjection_GetChildName().GetString().ShouldEqual(pathParts[pathParts.Length - 1]);
                indexEntry.BuildingProjection_GetGitRelativePath().ShouldEqual(string.Join("/", pathParts));
            }
            else
            {
                indexEntry.BackgroundTask_GetPlatformRelativePath().ShouldEqual(string.Join(Path.DirectorySeparatorChar.ToString(), pathParts));
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Virtualization/Projection/LazyUTF8StringTests.cs
================================================
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using NUnit.Framework;
using System.Text;
using static GVFS.Virtualization.Projection.GitIndexProjection;

namespace GVFS.UnitTests.Virtualization.Git
{
    [TestFixture]
    public class LazyUTF8StringTests
    {
        private const int DefaultIndexEntryCount = 10;

        private unsafe delegate void RunUsingPointer(byte* buffer);

        [OneTimeSetUp]
        public void Setup()
        {
            LazyUTF8String.InitializePools(new MockTracer(), DefaultIndexEntryCount);
        }

        [SetUp]
        public void TestSetup()
        {
            LazyUTF8String.ResetPool(new MockTracer(), DefaultIndexEntryCount);
        }

        [TestCase]
        public unsafe void GetString()
        {
            UseASCIIBytePointer(
                "folderonefile.txt",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    firstFolder.GetString().ShouldEqual("folder");
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 6, 3);
                    secondFolder.GetString().ShouldEqual("one");
                    LazyUTF8String file = LazyUTF8String.FromByteArray(bufferPtr + 9, 8);
                    file.GetString().ShouldEqual("file.txt");
                });
        }

        [TestCase]
        public unsafe void GetString_NonASCII()
        {
            UseUTF8BytePointer(
                "folderoneريلٌأكتوبرfile.txt",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    firstFolder.GetString().ShouldEqual("folder");
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 6, 3);
                    secondFolder.GetString().ShouldEqual("one");
                    LazyUTF8String utf8 = LazyUTF8String.FromByteArray(bufferPtr + 9, 20);
                    utf8.GetString().ShouldEqual("ريلٌأكتوبر");
                    LazyUTF8String file = LazyUTF8String.FromByteArray(bufferPtr + 29, 8);
                    file.GetString().ShouldEqual("file.txt");
                });
        }

        [TestCase]
        public unsafe void CaseInsensitiveEquals_SameName_EqualsTrue()
        {
            UseASCIIBytePointer(
                "folderonefile.txtfolder",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 6);
                    firstFolder.CaseInsensitiveEquals(secondFolder).ShouldBeTrue(nameof(firstFolder.CaseInsensitiveEquals));
                });
        }

        [TestCase]
        public unsafe void CaseSensitiveEquals_SameName_EqualsTrue()
        {
            UseASCIIBytePointer(
                "folderonefile.txtfolder",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 6);
                    firstFolder.CaseSensitiveEquals(secondFolder).ShouldBeTrue(nameof(firstFolder.CaseSensitiveEquals));
                });
        }

        [TestCase]
        public unsafe void CaseInsensitiveEquals_SameNameDifferentCase1_EqualsTrue()
        {
            UseASCIIBytePointer(
                "folderonefile.txtFolder",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 6);
                    firstFolder.CaseInsensitiveEquals(secondFolder).ShouldBeTrue(nameof(firstFolder.CaseInsensitiveEquals));
                });
        }

        [TestCase]
        public unsafe void CaseSensitiveEquals_SameNameDifferentCase1_EqualsFalse()
        {
            UseASCIIBytePointer(
                "folderonefile.txtFolder",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 6);
                    firstFolder.CaseSensitiveEquals(secondFolder).ShouldBeFalse(nameof(firstFolder.CaseSensitiveEquals));
                });
        }

        [TestCase]
        public unsafe void CaseInsensitiveEquals_SameNameDifferentCase2_EqualsTrue()
        {
            UseASCIIBytePointer(
                "FOlderonefile.txtFolder",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 6);
                    firstFolder.CaseInsensitiveEquals(secondFolder).ShouldBeTrue(nameof(firstFolder.CaseInsensitiveEquals));
                });
        }

        [TestCase]
        public unsafe void CaseSensitiveEquals_SameNameDifferentCase2_EqualsFalse()
        {
            UseASCIIBytePointer(
                "FOlderonefile.txtFolder",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 6);
                    firstFolder.CaseSensitiveEquals(secondFolder).ShouldBeFalse(nameof(firstFolder.CaseSensitiveEquals));
                });
        }

        [TestCase]
        public unsafe void CaseInsensitiveEquals_OneNameLongerEqualsFalse()
        {
            UseASCIIBytePointer(
                "folderonefile.txtFolderTest",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 10);
                    firstFolder.CaseInsensitiveEquals(secondFolder).ShouldBeFalse(nameof(firstFolder.CaseInsensitiveEquals));
                });
        }

        [TestCase]
        public unsafe void CaseSensitiveEquals_OneNameLongerEqualsFalse()
        {
            UseASCIIBytePointer(
                "folderonefile.txtfoldertest",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 10);
                    firstFolder.CaseSensitiveEquals(secondFolder).ShouldBeFalse(nameof(firstFolder.CaseSensitiveEquals));
                });
        }

        [TestCase]
        public unsafe void CaseInsensitiveEquals_OneNameShorterEqualsFalse()
        {
            UseASCIIBytePointer(
                "folderonefile.txtFold",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 4);
                    firstFolder.CaseInsensitiveEquals(secondFolder).ShouldBeFalse(nameof(firstFolder.CaseInsensitiveEquals));
                });
        }

        [TestCase]
        public unsafe void CaseSensitiveEquals_OneNameShorterEqualsFalse()
        {
            UseASCIIBytePointer(
                "folderonefile.txtfold",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 4);
                    firstFolder.CaseSensitiveEquals(secondFolder).ShouldBeFalse(nameof(firstFolder.CaseSensitiveEquals));
                });
        }

        [TestCase]
        public unsafe void Compare_EqualsZero()
        {
            UseASCIIBytePointer(
                "folderonefile.txtfolder",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 6);
                    firstFolder.CaseInsensitiveCompare(secondFolder).ShouldEqual(0, nameof(firstFolder.CaseInsensitiveCompare));
                    firstFolder.CaseSensitiveCompare(secondFolder).ShouldEqual(0, nameof(firstFolder.CaseSensitiveCompare));
                });
        }

        [TestCase]
        public unsafe void Compare_EqualsLessThanZero()
        {
            UseASCIIBytePointer(
                "folderonefile.txtfolders",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 7);
                    firstFolder.CaseInsensitiveCompare(secondFolder).ShouldBeAtMost(-1, nameof(firstFolder.CaseInsensitiveCompare));
                    firstFolder.CaseSensitiveCompare(secondFolder).ShouldBeAtMost(-1, nameof(firstFolder.CaseSensitiveCompare));
                });
        }

        [TestCase]
        public unsafe void Compare_EqualsLessThanZero2()
        {
            UseASCIIBytePointer(
                "folderDKfile.txtSDKfolders",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 6, 2);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 16, 3);
                    firstFolder.CaseInsensitiveCompare(secondFolder).ShouldBeAtMost(-1, nameof(firstFolder.CaseInsensitiveCompare));
                    firstFolder.CaseSensitiveCompare(secondFolder).ShouldBeAtMost(-1, nameof(firstFolder.CaseSensitiveCompare));
                });
        }

        [TestCase]
        public unsafe void Compare_EqualsGreaterThanZero()
        {
            UseASCIIBytePointer(
                "folderonefile.txtfold",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 0, 6);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 4);
                    firstFolder.CaseInsensitiveCompare(secondFolder).ShouldBeAtLeast(1, nameof(firstFolder.CaseInsensitiveCompare));
                    firstFolder.CaseSensitiveCompare(secondFolder).ShouldBeAtLeast(1, nameof(firstFolder.CaseSensitiveCompare));
                });
        }

        [TestCase]
        public unsafe void Compare_EqualsGreaterThanZero2()
        {
            UseASCIIBytePointer(
                "folderSDKfile.txtDKfolders",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 6, 3);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 2);
                    firstFolder.CaseInsensitiveCompare(secondFolder).ShouldBeAtLeast(1, nameof(firstFolder.CaseInsensitiveCompare));
                    firstFolder.CaseSensitiveCompare(secondFolder).ShouldBeAtLeast(1, nameof(firstFolder.CaseSensitiveCompare));
                });
        }

        [TestCase]
        public unsafe void PoolSizeCheck()
        {
            UseASCIIBytePointer(
                "folderSDKfile.txtDKfolders",
                bufferPtr =>
                {
                    int bytePoolSizeBeforeFreePool = LazyUTF8String.BytePoolSize();
                    int stringPoolSizeBeforeFreePool = LazyUTF8String.StringPoolSize();
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 6, 3);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 2);
                    CheckPoolSizes(bytePoolSizeBeforeFreePool, stringPoolSizeBeforeFreePool);
                });
        }

        [TestCase]
        public unsafe void FreePool_KeepsPoolSize()
        {
            UseASCIIBytePointer(
                "folderSDKfile.txtDKfolders",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 6, 3);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 2);
                    int bytePoolSizeBeforeFreePool = LazyUTF8String.BytePoolSize();
                    int stringPoolSizeBeforeFreePool = LazyUTF8String.StringPoolSize();
                    LazyUTF8String.FreePool();
                    CheckPoolSizes(bytePoolSizeBeforeFreePool, stringPoolSizeBeforeFreePool);
                });
        }

        [TestCase]
        public unsafe void ShrinkPool_DecreasesPoolSize()
        {
            LazyUTF8String.ResetPool(new MockTracer(), DefaultIndexEntryCount);
            string fileAndFolderNames = "folderSDKfile.txtDKfolders";
            UseASCIIBytePointer(
                fileAndFolderNames,
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 6, 3);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 2);
                    LazyUTF8String.ShrinkPool();
                    CheckPoolSizes(expectedBytePoolSize: 6, expectedStringPoolSize: 2);
                });
        }

        [TestCase]
        public unsafe void ExpandAfterShrinkPool_AllocatesDefault()
        {
            LazyUTF8String.ResetPool(new MockTracer(), DefaultIndexEntryCount);
            string fileAndFolderNames = "folderSDKfile.txtDKfolders";
            UseASCIIBytePointer(
                fileAndFolderNames,
                bufferPtr =>
                {
                    int initialBytePoolSize = LazyUTF8String.BytePoolSize();
                    int initialStringPoolSize = LazyUTF8String.StringPoolSize();

                    LazyUTF8String.FromByteArray(bufferPtr + 6, 3);
                    LazyUTF8String.FromByteArray(bufferPtr + 17, 2);
                    LazyUTF8String.ShrinkPool();
                    CheckPoolSizes(expectedBytePoolSize: 6, expectedStringPoolSize: 2);
                    LazyUTF8String.FreePool();
                    CheckPoolSizes(expectedBytePoolSize: 6, expectedStringPoolSize: 2);
                    LazyUTF8String.ShrinkPool();
                    CheckPoolSizes(expectedBytePoolSize: 0, expectedStringPoolSize: 0);
                    LazyUTF8String.FromByteArray(bufferPtr + 6, 3);
                    CheckPoolSizes(expectedBytePoolSize: initialBytePoolSize, expectedStringPoolSize: initialStringPoolSize);
                    LazyUTF8String.ShrinkPool();
                    CheckPoolSizes(expectedBytePoolSize: 3, expectedStringPoolSize: 1);
                });
        }

        [TestCase]
        public unsafe void PoolSizeIncreasesAfterShrinking()
        {
            LazyUTF8String.ResetPool(new MockTracer(), DefaultIndexEntryCount);
            string fileAndFolderNames = "folderSDKfile.txtDKfolders";
            UseASCIIBytePointer(
                fileAndFolderNames,
                bufferPtr =>
                {
                    int initialBytePoolSize = LazyUTF8String.BytePoolSize();
                    int initialStringPoolSize = LazyUTF8String.StringPoolSize();
                    LazyUTF8String.FromByteArray(bufferPtr + 6, 3);
                    LazyUTF8String.FromByteArray(bufferPtr + 17, 2);
                    LazyUTF8String.ShrinkPool();
                    CheckPoolSizes(expectedBytePoolSize: 6, expectedStringPoolSize: 2);
                    LazyUTF8String.FromByteArray(bufferPtr + 6, 3);
                    LazyUTF8String.FromByteArray(bufferPtr + 17, 2);
                    LazyUTF8String.FromByteArray(bufferPtr, 6);

                    CheckPoolSizes(expectedBytePoolSize: initialBytePoolSize, expectedStringPoolSize: initialStringPoolSize);
                });
        }

        [TestCase]
        public unsafe void NonASCIICharacters_Compare()
        {
            UseUTF8BytePointer(
                "folderSDKfile.txtريلٌأكتوبرDKfolders",
                bufferPtr =>
                {
                    LazyUTF8String firstFolder = LazyUTF8String.FromByteArray(bufferPtr + 6, 3);
                    LazyUTF8String secondFolder = LazyUTF8String.FromByteArray(bufferPtr + 17, 20);
                    firstFolder.CaseInsensitiveCompare(secondFolder).ShouldBeAtMost(-1, nameof(firstFolder.CaseInsensitiveCompare));
                    firstFolder.CaseSensitiveCompare(secondFolder).ShouldBeAtMost(-1, nameof(firstFolder.CaseSensitiveCompare));
                });
        }

        [TestCase]
        public void MinimumPoolSize()
        {
            LazyUTF8String.ResetPool(new MockTracer(), 0);

            LazyUTF8String.FreePool();
            LazyUTF8String.BytePoolSize().ShouldBeAtLeast(1);

            LazyUTF8String.InitializePools(new MockTracer(), 0);
            LazyUTF8String.BytePoolSize().ShouldBeAtLeast(1);
        }

        private static void CheckPoolSizes(int expectedBytePoolSize, int expectedStringPoolSize)
        {
            LazyUTF8String.BytePoolSize().ShouldEqual(expectedBytePoolSize, $"{nameof(LazyUTF8String.BytePoolSize)} should be {expectedBytePoolSize}");
            LazyUTF8String.StringPoolSize().ShouldEqual(expectedStringPoolSize, $"{nameof(LazyUTF8String.StringPoolSize)} should be {expectedStringPoolSize}");
        }

        private static unsafe void UseUTF8BytePointer(string fileAndFolderNames, RunUsingPointer action)
        {
            byte[] buffer = Encoding.UTF8.GetBytes(fileAndFolderNames);
            fixed (byte* bufferPtr = buffer)
            {
                action(bufferPtr);
            }
        }

        private static unsafe void UseASCIIBytePointer(string fileAndFolderNames, RunUsingPointer action)
        {
            byte[] buffer = Encoding.ASCII.GetBytes(fileAndFolderNames);
            fixed (byte* bufferPtr = buffer)
            {
                action(bufferPtr);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Virtualization/Projection/ObjectPoolTests.cs
================================================
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Common;
using GVFS.Virtualization.Projection;
using NUnit.Framework;
using System;

namespace GVFS.UnitTests.Virtualization
{
    [TestFixture]
    public class ObjectPoolTests
    {
        private const int DefaultObjectsToUse = 101;
        private const int AllocationSize = 100;
        private static readonly int PoolSizeAfterUsingDefault = Convert.ToInt32(AllocationSize * 1.15);
        private static readonly int DefaultShrinkSize = PoolSizeAfterUsingDefault;
        private static readonly int PoolSizeAfterExpandingAfterShrinking = Convert.ToInt32(AllocationSize * 1.15 * 1.15);

        [TestCase]
        public void TestGettingObjects()
        {
            CreateExpandedPool();
        }

        [TestCase]
        public void ShrinkKeepsUsedObjectsPlusPercent()
        {
            GitIndexProjection.ObjectPool pool = new GitIndexProjection.ObjectPool(new MockTracer(), AllocationSize, objectCreator: () => new object());
            UseObjectsInPool(pool, 20);
            pool.Size.ShouldEqual(AllocationSize);
            pool.Shrink();
            pool.Size.ShouldEqual(Convert.ToInt32(20 * 1.1));
        }

        [TestCase]
        public void FreeToZeroAllocatesMinimumSizeNextGet()
        {
            GitIndexProjection.ObjectPool pool = new GitIndexProjection.ObjectPool(new MockTracer(), AllocationSize, objectCreator: () => new object());
            pool.FreeAll();
            pool.Shrink();
            pool.Size.ShouldEqual(0);
            UseObjectsInPool(pool, 1);
            pool.Size.ShouldEqual(AllocationSize);
        }

        [TestCase]
        public void FreeKeepsPoolSize()
        {
            GitIndexProjection.ObjectPool pool = CreateExpandedPool();
            pool.FreeAll();
            pool.Size.ShouldEqual(DefaultShrinkSize);
            UseObjectsInPool(pool, DefaultObjectsToUse);
            UseObjectsInPool(pool, 15);
            pool.Size.ShouldEqual(PoolSizeAfterExpandingAfterShrinking);
            pool.FreeAll();
            pool.Size.ShouldEqual(PoolSizeAfterExpandingAfterShrinking);
        }

        private static GitIndexProjection.ObjectPool CreateExpandedPool()
        {
            GitIndexProjection.ObjectPool pool = new GitIndexProjection.ObjectPool(new MockTracer(), AllocationSize, objectCreator: () => new object());
            UseObjectsInPool(pool, DefaultObjectsToUse);
            pool.Size.ShouldEqual(PoolSizeAfterUsingDefault);
            return pool;
        }

        private static void UseObjectsInPool(GitIndexProjection.ObjectPool pool, int count)
        {
            for (int i = 0; i < count; i++)
            {
                pool.GetNew().ShouldNotBeNull();
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Virtualization/Projection/SortedFolderEntriesTests.cs
================================================
using GVFS.Common;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
using static GVFS.Virtualization.Projection.GitIndexProjection;

namespace GVFS.UnitTests.Virtualization.Git
{
    [TestFixture]
    public class SortedFolderEntriesTests
    {
        private const int DefaultIndexEntryCount = 100;
        private static string[] defaultFiles = new string[]
        {
            "_test",
            "zero",
            "a",
            "{file}",
            "(1)",
            "file.txt",
            "01",
        };

        private static string[] caseDifferingFiles = new string[]
        {
            "file1.txt",
            "File1.txt",
        };

        private static string[] defaultFolders = new string[]
        {
            "zf",
            "af",
            "01f",
            "{folder}",
            "_f",
            "(1f)",
            "folder",
        };

        private static string[] caseDifferingFolders = new string[]
        {
            "folder1",
            "Folder1",
        };

        [OneTimeSetUp]
        public void Setup()
        {
            LazyUTF8String.InitializePools(new MockTracer(), DefaultIndexEntryCount);
            SortedFolderEntries.InitializePools(new MockTracer(), DefaultIndexEntryCount);
        }

        [SetUp]
        public void TestSetup()
        {
            LazyUTF8String.ResetPool(new MockTracer(), DefaultIndexEntryCount);
            SortedFolderEntries.ResetPool(new MockTracer(), DefaultIndexEntryCount);
        }

        [TestCase]
        public void EmptyFolderEntries_NotFound()
        {
            SortedFolderEntries sfe = new SortedFolderEntries();
            LazyUTF8String findName = ConstructLazyUTF8String("Anything");
            sfe.TryGetValue(findName, out FolderEntryData folderEntryData).ShouldBeFalse();
            folderEntryData.ShouldBeNull();
        }

        [TestCase]
        public void EntryNotFound()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String findName = ConstructLazyUTF8String("Anything");
            sfe.TryGetValue(findName, out FolderEntryData folderEntryData).ShouldBeFalse();
            folderEntryData.ShouldBeNull();
        }

        [TestCase]
        public void EntryFoundMatchingCase()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String findName = ConstructLazyUTF8String("folder");
            sfe.TryGetValue(findName, out FolderEntryData folderEntryData).ShouldBeTrue();
            folderEntryData.ShouldNotBeNull();
        }

        [TestCase]
        [Category(CategoryConstants.CaseInsensitiveFileSystemOnly)]
        public void EntryFoundDifferentCase()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String findName = ConstructLazyUTF8String("Folder");
            sfe.TryGetValue(findName, out FolderEntryData folderEntryData).ShouldBeTrue();
            folderEntryData.ShouldNotBeNull();
        }

        [TestCase]
        [Category(CategoryConstants.CaseSensitiveFileSystemOnly)]
        public void EntryNotFoundDifferentCase()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String findName = ConstructLazyUTF8String("Folder");
            sfe.TryGetValue(findName, out FolderEntryData folderEntryData).ShouldBeFalse();
            folderEntryData.ShouldBeNull();
        }

        [TestCase]
        public void AddItemAtEnd()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String name = ConstructLazyUTF8String("{{shouldbeattheend");
            sfe.AddFile(name, new byte[20]);
            sfe[GetDefaultEntriesLength()].Name.ShouldEqual(name, "Item added at incorrect index.");
        }

        [TestCase]
        public void AddItemAtTheBeginning()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String name = ConstructLazyUTF8String("((shouldbeatthestart");
            sfe.AddFile(name, new byte[20]);
            sfe[0].Name.ShouldEqual(name, "Item added at incorrect index.");
        }

        [TestCase]
        [Category(CategoryConstants.CaseInsensitiveFileSystemOnly)]
        public void ValidateCaseInsensitiveOrderOfDefaultEntries()
        {
            List allEntries = new List(defaultFiles);
            allEntries.AddRange(defaultFolders);
            allEntries.Sort(CaseInsensitiveStringCompare);
            SortedFolderEntries sfe = SetupDefaultEntries();
            sfe.Count.ShouldEqual(14);
            for (int i = 0; i < allEntries.Count; i++)
            {
                sfe[i].Name.GetString().ShouldEqual(allEntries[i]);
            }
        }

        [TestCase]
        [Category(CategoryConstants.CaseSensitiveFileSystemOnly)]
        public void ValidateCaseSensitiveOrderOfDefaultEntries()
        {
            List allEntries = new List(defaultFiles);
            allEntries.AddRange(defaultFolders);
            allEntries.AddRange(caseDifferingFiles);
            allEntries.AddRange(caseDifferingFolders);
            allEntries.Sort(CaseSensitiveStringCompare);
            SortedFolderEntries sfe = SetupDefaultEntries();
            sfe.Count.ShouldEqual(18);
            for (int i = 0; i < allEntries.Count; i++)
            {
                sfe[i].Name.GetString().ShouldEqual(allEntries[i]);
            }
        }

        [TestCase]
        public void FoldersShouldBeIncludedWhenSparseFolderDataIsEmpty()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String name = ConstructLazyUTF8String("IsIncludedFalse");
            sfe.GetOrAddFolder(new[] { name }, partIndex: 0, parentIsIncluded: false, rootSparseFolderData: new SparseFolderData());
            ValidateFolder(sfe, name, isIncludedValue: true);
        }

        [TestCase]
        public void AddFolderWhereParentIncludedIsFalseAndIncluded()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String name = ConstructLazyUTF8String("Child");
            SparseFolderData sparseFolderData = new SparseFolderData();
            sparseFolderData.Children.Add("Child", new SparseFolderData());
            sfe.GetOrAddFolder(new[] { name }, partIndex: 0, parentIsIncluded: false, rootSparseFolderData: sparseFolderData);
            ValidateFolder(sfe, name, isIncludedValue: false);
        }

        [TestCase]
        public void AddFolderWhereParentIncludedIsTrueAndChildIsIncluded()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String name = ConstructLazyUTF8String("Child");
            SparseFolderData sparseFolderData = new SparseFolderData();
            sparseFolderData.Children.Add("Child", new SparseFolderData());
            sfe.GetOrAddFolder(new[] { name }, partIndex: 0, parentIsIncluded: true, rootSparseFolderData: sparseFolderData);
            ValidateFolder(sfe, name, isIncludedValue: true);
        }

        [TestCase]
        public void AddFolderWhereParentIncludedIsTrueAndChildIsNotIncluded()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String name = ConstructLazyUTF8String("ChildNotIncluded");
            SparseFolderData sparseFolderData = new SparseFolderData();
            sparseFolderData.Children.Add("Child", new SparseFolderData());
            sfe.GetOrAddFolder(new[] { name }, partIndex: 0, parentIsIncluded: true, rootSparseFolderData: sparseFolderData);
            ValidateFolder(sfe, name, isIncludedValue: false);
        }

        [TestCase]
        public void AddFolderWhereParentIsRecursive()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String name = ConstructLazyUTF8String("Child");
            LazyUTF8String name2 = ConstructLazyUTF8String("GrandChild");
            SparseFolderData sparseFolderData = new SparseFolderData() { IsRecursive = true };
            sparseFolderData.Children.Add("Child", new SparseFolderData());
            sfe.GetOrAddFolder(new[] { name, name2 }, partIndex: 1, parentIsIncluded: true, rootSparseFolderData: sparseFolderData);
            ValidateFolder(sfe, name2, isIncludedValue: true);
        }

        [TestCase]
        public void AddFolderBelowTopLevelNotIncluded()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String name = ConstructLazyUTF8String("Child");
            LazyUTF8String name2 = ConstructLazyUTF8String("GrandChild");
            SparseFolderData sparseFolderData = new SparseFolderData();
            sparseFolderData.Children.Add("Child", new SparseFolderData());
            sfe.GetOrAddFolder(new[] { name, name2 }, partIndex: 1, parentIsIncluded: true, rootSparseFolderData: sparseFolderData);
            ValidateFolder(sfe, name2, isIncludedValue: false);
        }

        [TestCase]
        public void AddFolderBelowTopLevelIsIncluded()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            LazyUTF8String name = ConstructLazyUTF8String("Child");
            LazyUTF8String name2 = ConstructLazyUTF8String("GrandChild");
            SparseFolderData sparseFolderData = new SparseFolderData();
            SparseFolderData childSparseFolderData = new SparseFolderData();
            childSparseFolderData.Children.Add("GrandChild", new SparseFolderData());
            sparseFolderData.Children.Add("Child", childSparseFolderData);
            sfe.GetOrAddFolder(new[] { name, name2 }, partIndex: 1, parentIsIncluded: true, rootSparseFolderData: sparseFolderData);
            ValidateFolder(sfe, name2, isIncludedValue: true);
        }

        [TestCase]
        public void Clear()
        {
            SortedFolderEntries sfe = SetupDefaultEntries();
            sfe.Clear();
            sfe.Count.ShouldEqual(0);
        }

        [TestCase]
        public void SmallEntries()
        {
            SortedFolderEntries.FreePool();

            SortedFolderEntries.InitializePools(new MockTracer(), indexEntryCount: 0);
            SortedFolderEntries.FilePoolSize().ShouldBeAtLeast(1);
            SortedFolderEntries.FolderPoolSize().ShouldBeAtLeast(1);

            SortedFolderEntries.ResetPool(new MockTracer(), indexEntryCount: 0);
            SortedFolderEntries.FilePoolSize().ShouldBeAtLeast(1);
            SortedFolderEntries.FolderPoolSize().ShouldBeAtLeast(1);
        }

        private static int CaseInsensitiveStringCompare(string x, string y)
        {
            return string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
        }

        private static int CaseSensitiveStringCompare(string x, string y)
        {
            return string.Compare(x, y, StringComparison.Ordinal);
        }

        private static SortedFolderEntries SetupDefaultEntries()
        {
            SortedFolderEntries sfe = new SortedFolderEntries();
            AddFiles(sfe, defaultFiles);
            AddFolders(sfe, defaultFolders);
            if (GVFSPlatform.Instance.Constants.CaseSensitiveFileSystem)
            {
                AddFiles(sfe, caseDifferingFiles);
                AddFolders(sfe, caseDifferingFolders);
            }

            sfe.Count.ShouldEqual(GetDefaultEntriesLength());
            return sfe;
        }

        private static int GetDefaultEntriesLength()
        {
            int length = defaultFiles.Length + defaultFolders.Length;
            if (GVFSPlatform.Instance.Constants.CaseSensitiveFileSystem)
            {
                length += caseDifferingFiles.Length + caseDifferingFolders.Length;
            }

            return length;
        }

        private static unsafe LazyUTF8String ConstructLazyUTF8String(string name)
        {
            byte[] buffer = Encoding.ASCII.GetBytes(name);
            fixed (byte* bufferPtr = buffer)
            {
                return LazyUTF8String.FromByteArray(bufferPtr, name.Length);
            }
        }

        private static void AddFiles(SortedFolderEntries entries, params string[] names)
        {
            for (int i = 0; i < names.Length; i++)
            {
                LazyUTF8String entryString = ConstructLazyUTF8String(names[i]);
                entries.AddFile(entryString, new byte[20]);
                entries.TryGetValue(entryString, out FolderEntryData folderEntryData).ShouldBeTrue();
                folderEntryData.ShouldNotBeNull();
                folderEntryData.IsFolder.ShouldBeFalse();
            }
        }

        private static void AddFolders(SortedFolderEntries entries, params string[] names)
        {
            for (int i = 0; i < names.Length; i++)
            {
                LazyUTF8String entryString = ConstructLazyUTF8String(names[i]);
                entries.GetOrAddFolder(new[] { entryString }, partIndex: 0, parentIsIncluded: true, rootSparseFolderData: new SparseFolderData());
                ValidateFolder(entries, entryString, isIncludedValue: true);
            }
        }

        private static void ValidateFolder(SortedFolderEntries entries, LazyUTF8String entryToValidate, bool isIncludedValue)
        {
            entries.TryGetValue(entryToValidate, out FolderEntryData folderEntryData).ShouldBeTrue();
            folderEntryData.ShouldNotBeNull();
            folderEntryData.IsFolder.ShouldBeTrue();

            FolderData folderData = folderEntryData as FolderData;
            folderData.ShouldNotBeNull();
            folderData.IsIncluded.ShouldEqual(isIncludedValue, "IsIncluded does not match expected value.");
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Windows/CommandLine/SparseVerbTests.cs
================================================
using GVFS.CommandLine;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;

// These tests are in GVFS.UnitTests.Windows because they rely on GVFS.Windows
// which is a .NET Framework project and only supported on Windows
namespace GVFS.UnitTests.Windows.Windows.CommandLine
{
    [TestFixture]
    public class SparseVerbTests
    {
        private const char StatusPathSeparatorToken = '\0';

        private static readonly HashSet EmptySparseSet = new HashSet();

        [TestCase]
        public void GetNextGitPathGetsPaths()
        {
            string testStatusOutput = $"a.txt{StatusPathSeparatorToken}";
            ConfirmGitPathsParsed(testStatusOutput, new List() { "a.txt" });

            testStatusOutput = $"a.txt{StatusPathSeparatorToken}b.txt{StatusPathSeparatorToken}c.txt{StatusPathSeparatorToken}";
            ConfirmGitPathsParsed(testStatusOutput, new List() { "a.txt", "b.txt", "c.txt" });

            testStatusOutput = $"a.txt{StatusPathSeparatorToken}d/b.txt{StatusPathSeparatorToken}d/c.txt{StatusPathSeparatorToken}";
            ConfirmGitPathsParsed(testStatusOutput, new List() { "a.txt", "d/b.txt", "d/c.txt" });
        }

        [TestCase]
        public void PathCoveredBySparseFolders_RootPaths()
        {
            List testPaths = new List()
            {
                "a.txt",
                "b.txt",
                "c.txt"
            };

            ConfirmAllPathsCovered(testPaths, EmptySparseSet);
            ConfirmAllPathsCovered(testPaths, new HashSet(StringComparer.OrdinalIgnoreCase) { "A" });
            ConfirmAllPathsCovered(testPaths, new HashSet(StringComparer.OrdinalIgnoreCase) { "A", @"B\C" });
        }

        [TestCase]
        public void PathCoveredBySparseFolders_RecursivelyCoveredPaths()
        {
            List testPaths = new List()
            {
                "A/a.txt",
                "A/B/B.txt"
            };

            HashSet singleFolderSparseSet = new HashSet(StringComparer.OrdinalIgnoreCase) { "A" };
            HashSet twoFolderSparseSet = new HashSet(StringComparer.OrdinalIgnoreCase) { "A", @"B\C" };

            ConfirmAllPathsCovered(testPaths, singleFolderSparseSet);
            ConfirmAllPathsCovered(testPaths, twoFolderSparseSet);

            // Root entries should always be covered
            testPaths.Add("d.txt");
            testPaths.Add("e.txt");
            ConfirmAllPathsCovered(testPaths, singleFolderSparseSet);
            ConfirmAllPathsCovered(testPaths, twoFolderSparseSet);

            testPaths.Add("B/C/e.txt");
            testPaths.Add("B/C/F/g.txt");
            ConfirmAllPathsCovered(testPaths, twoFolderSparseSet);
        }

        [TestCase]
        public void PathCoveredBySparseFolders_NonRecursivelyCoveredPaths()
        {
            List testPaths = new List()
            {
                "A/B/B.txt",
                "A/C.txt",
                "A/D/E/C.txt"
            };

            ConfirmAllPathsCovered(
                testPaths,
                new HashSet(StringComparer.OrdinalIgnoreCase)
                {
                    @"A\B\C\D",
                    @"A\D\E\F\G"
                });

            // Root entries should always be covered
            testPaths.Add("d.txt");
            testPaths.Add("e.txt");

            ConfirmAllPathsCovered(
                testPaths,
                new HashSet(StringComparer.OrdinalIgnoreCase)
                {
                    @"A\B\C\D",
                    @"A\D\E\F\G"
                });
        }

        [TestCase]
        public void PathCoveredBySparseFolders_PathsNotCovered()
        {
            List testPaths = new List()
            {
                "A/B/B.txt",
                "A/D/E/C.txt"
            };

            ConfirmAllPathsNotCovered(testPaths, EmptySparseSet);
            ConfirmAllPathsNotCovered(testPaths, new HashSet(StringComparer.OrdinalIgnoreCase) { @"A\C" });
            ConfirmAllPathsNotCovered(testPaths, new HashSet(StringComparer.OrdinalIgnoreCase) { "B" });
            ConfirmAllPathsNotCovered(testPaths, new HashSet(StringComparer.OrdinalIgnoreCase) { "B", @"C\D" });
        }

        private static void ConfirmAllPathsCovered(List paths, HashSet sparseSet)
        {
            CheckIfPathsCovered(paths, sparseSet, shouldBeCovered: true);
        }

        private static void ConfirmAllPathsNotCovered(List paths, HashSet sparseSet)
        {
            CheckIfPathsCovered(paths, sparseSet, shouldBeCovered: false);
        }

        private static void ConfirmGitPathsParsed(string paths, List expectedPaths)
        {
            int index = 0;
            int listIndex = 0;
            while (index < paths.Length - 1)
            {
                int nextSeparatorIndex = paths.IndexOf(StatusPathSeparatorToken, index);
                string expectedGitPath = expectedPaths[listIndex];
                SparseVerb.GetNextGitPath(ref index, paths).ShouldEqual(expectedGitPath);
                index.ShouldEqual(nextSeparatorIndex + 1);
                ++listIndex;
            }
        }

        private static void CheckIfPathsCovered(List paths, HashSet sparseSet, bool shouldBeCovered)
        {
            foreach (string path in paths)
            {
                SparseVerb.PathCoveredBySparseFolders(path, sparseSet).ShouldEqual(shouldBeCovered);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Windows/Mock/MockVirtualizationInstance.cs
================================================
using GVFS.Common;
using GVFS.UnitTests.Windows.Windows.Mock;
using Microsoft.Windows.ProjFS;
using System;
using System.IO;
using System.Threading;

namespace GVFS.UnitTests.Windows.Mock
{
    public class MockVirtualizationInstance : IVirtualizationInstance, IDisposable
    {
        private AutoResetEvent commandCompleted;
        private AutoResetEvent placeholderCreated;
        private ManualResetEvent unblockCreateWriteBuffer;
        private ManualResetEvent waitForCreateWriteBuffer;

        private volatile HResult completionResult;
        private volatile HResult writeFileReturnResult;

        public MockVirtualizationInstance()
        {
            this.commandCompleted = new AutoResetEvent(false);
            this.placeholderCreated = new AutoResetEvent(false);
            this.CreatedPlaceholders = new ConcurrentHashSet();

            this.unblockCreateWriteBuffer = new ManualResetEvent(true);
            this.waitForCreateWriteBuffer = new ManualResetEvent(true);

            this.WriteFileReturnResult = HResult.Ok;
        }

        public ConcurrentHashSet CreatedPlaceholders { get; private set; }

        public CancelCommandCallback OnCancelCommand { get; set; }

        public IRequiredCallbacks requiredCallbacks { get; set; }
        public NotifyFileOpenedCallback OnNotifyFileOpened { get; set; }
        public NotifyNewFileCreatedCallback OnNotifyNewFileCreated { get; set; }
        public NotifyFileOverwrittenCallback OnNotifyFileOverwritten { get; set; }
        public NotifyFileHandleClosedNoModificationCallback OnNotifyFileHandleClosedNoModification { get; set; }
        public NotifyFileHandleClosedFileModifiedOrDeletedCallback OnNotifyFileHandleClosedFileModifiedOrDeleted { get; set; }
        public NotifyFilePreConvertToFullCallback OnNotifyFilePreConvertToFull { get; set; }
        public NotifyFileRenamedCallback OnNotifyFileRenamed { get; set; }
        public NotifyHardlinkCreatedCallback OnNotifyHardlinkCreated { get; set; }
        public NotifyPreDeleteCallback OnNotifyPreDelete { get; set; }
        public NotifyPreRenameCallback OnNotifyPreRename { get; set; }
        public NotifyPreCreateHardlinkCallback OnNotifyPreCreateHardlink { get; set; }
        public QueryFileNameCallback OnQueryFileName { get; set; }

        public HResult WriteFileReturnResult
        {
            get { return this.writeFileReturnResult; }
            set { this.writeFileReturnResult = value; }
        }

        public uint NegativePathCacheCount { get; set; }

        public HResult DeleteFileResult { get; set; }
        public UpdateFailureCause DeleteFileUpdateFailureCause { get; set; }

        public HResult UpdateFileIfNeededResult { get; set; }
        public UpdateFailureCause UpdateFileIfNeededFailureCase { get; set; }

        public HResult StartVirtualizing(IRequiredCallbacks requiredCallbacks)
        {
            this.requiredCallbacks = requiredCallbacks;
            return HResult.Ok;
        }

        public void StopVirtualizing()
        {
        }

        public HResult DetachDriver()
        {
            return HResult.Ok;
        }

        public HResult ClearNegativePathCache(out uint totalEntryNumber)
        {
            totalEntryNumber = this.NegativePathCacheCount;
            this.NegativePathCacheCount = 0;
            return HResult.Ok;
        }

        public HResult DeleteFile(string relativePath, UpdateType updateFlags, out UpdateFailureCause failureReason)
        {
            failureReason = this.DeleteFileUpdateFailureCause;
            return this.DeleteFileResult;
        }

        public HResult UpdateFileIfNeeded(string relativePath, DateTime creationTime, DateTime lastAccessTime, DateTime lastWriteTime, DateTime changeTime, FileAttributes fileAttributes, long endOfFile, byte[] contentId, byte[] providerId, UpdateType updateFlags, out UpdateFailureCause failureReason)
        {
            failureReason = this.UpdateFileIfNeededFailureCase;
            return this.UpdateFileIfNeededResult;
        }

        public HResult CreatePlaceholderAsHardlink(string destinationFileName, string hardLinkTarget)
        {
            throw new NotImplementedException();
        }

        public HResult MarkDirectoryAsPlaceholder(string targetDirectoryPath, byte[] contentId, byte[] providerId)
        {
            throw new NotImplementedException();
        }

        public HResult WritePlaceholderInfo(
            string relativePath,
            DateTime creationTime,
            DateTime lastAccessTime,
            DateTime lastWriteTime,
            DateTime changeTime,
            FileAttributes fileAttributes,
            long endOfFile,
            bool isDirectory,
            byte[] contentId,
            byte[] epochId)
        {
            this.CreatedPlaceholders.Add(relativePath);
            this.placeholderCreated.Set();
            return HResult.Ok;
        }

        public HResult WaitForCompletionStatus()
        {
            this.commandCompleted.WaitOne();
            return this.completionResult;
        }

        public void WaitForPlaceholderCreate()
        {
            this.placeholderCreated.WaitOne();
        }

        public void BlockCreateWriteBuffer(bool willWaitForRequest)
        {
            if (willWaitForRequest)
            {
                this.waitForCreateWriteBuffer.Reset();
            }

            this.unblockCreateWriteBuffer.Reset();
        }

        public void UnblockCreateWriteBuffer()
        {
            this.unblockCreateWriteBuffer.Set();
        }

        public void WaitForCreateWriteBuffer()
        {
            this.waitForCreateWriteBuffer.WaitOne();
        }

        public HResult CompleteCommand(int commandId, NotificationType newNotificationMask)
        {
            throw new NotImplementedException();
        }

        public HResult CompleteCommand(int commandId, IDirectoryEnumerationResults results)
        {
            throw new NotImplementedException();
        }

        public HResult CompleteCommand(int commandId, HResult completionResult)
        {
            this.completionResult = completionResult;
            this.commandCompleted.Set();
            return HResult.Ok;
        }

        public HResult CompleteCommand(int commandId)
        {
            throw new NotImplementedException();
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        public HResult WriteFileData(Guid dataStreamId, IWriteBuffer buffer, ulong byteOffset, uint length)
        {
            return this.WriteFileReturnResult;
        }

        public IWriteBuffer CreateWriteBuffer(ulong byteOffset, uint length, out ulong alignedByteOffset, out uint alignedLength)
        {
            throw new NotImplementedException();
        }

        public IWriteBuffer CreateWriteBuffer(uint desiredBufferSize)
        {
            this.waitForCreateWriteBuffer.Set();
            this.unblockCreateWriteBuffer.WaitOne();

            return new MockWriteBuffer(desiredBufferSize);
        }

        protected void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.commandCompleted != null)
                {
                    this.commandCompleted.Dispose();
                    this.commandCompleted = null;
                }

                if (this.placeholderCreated != null)
                {
                    this.placeholderCreated.Dispose();
                    this.placeholderCreated = null;
                }

                if (this.unblockCreateWriteBuffer != null)
                {
                    this.unblockCreateWriteBuffer.Dispose();
                    this.unblockCreateWriteBuffer = null;
                }

                if (this.waitForCreateWriteBuffer != null)
                {
                    this.waitForCreateWriteBuffer.Dispose();
                    this.waitForCreateWriteBuffer = null;
                }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Windows/Mock/MockWriteBuffer.cs
================================================
using Microsoft.Windows.ProjFS;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace GVFS.UnitTests.Windows.Windows.Mock
{
    public class MockWriteBuffer : IWriteBuffer
    {
        private IntPtr memIntPtr;
        private bool disposed = false;

        public MockWriteBuffer(long bufferSize)
        {
            unsafe
            {
                this.Length = bufferSize;
                this.memIntPtr = Marshal.AllocHGlobal(unchecked((int)this.Length));
                byte* memBytePtr = (byte*)this.memIntPtr.ToPointer();
                this.Stream = new UnmanagedMemoryStream(memBytePtr, this.Length, this.Length, FileAccess.Write);
            }
        }

        ~MockWriteBuffer()
        {
            this.Dispose(false);
        }

        public IntPtr Pointer => this.memIntPtr;

        public UnmanagedMemoryStream Stream
        {
            get;
            set;
        }

        public long Length
        {
            get;
            set;
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(true);
        }

        protected void Dispose(bool disposing)
        {
            if (this.disposed)
            {
                return;
            }

            if (disposing)
            {
                if (this.Stream != null)
                {
                    this.Stream.Dispose();
                    this.Stream = null;
                }
            }

            if (this.memIntPtr != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(this.memIntPtr);
                this.memIntPtr = IntPtr.Zero;
            }

            this.disposed = true;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Windows/Mock/WindowsFileSystemVirtualizerTester.cs
================================================
using GVFS.Platform.Windows;
using GVFS.Tests.Should;
using GVFS.UnitTests.Mock.Git;
using GVFS.UnitTests.Virtual;
using GVFS.Virtualization.FileSystem;
using Microsoft.Windows.ProjFS;
using System;

namespace GVFS.UnitTests.Windows.Mock
{
    public class WindowsFileSystemVirtualizerTester : FileSystemVirtualizerTester
    {
        public WindowsFileSystemVirtualizerTester(CommonRepoSetup repo)
            : base(repo)
        {
        }

        public WindowsFileSystemVirtualizerTester(CommonRepoSetup repo, string[] projectedFiles)
            : base(repo, projectedFiles)
        {
        }

        public MockVirtualizationInstance MockVirtualization { get; private set; }
        public WindowsFileSystemVirtualizer WindowsVirtualizer { get; private set; }

        public void InvokeGetFileDataCallback(HResult expectedResult = HResult.Pending, byte[] providerId = null, ulong byteOffset = 0)
        {
            if (providerId == null)
            {
                providerId = WindowsFileSystemVirtualizer.PlaceholderVersionId;
            }

            this.MockVirtualization.requiredCallbacks.GetFileDataCallback(
                commandId: 1,
                relativePath: "test.txt",
                byteOffset: byteOffset,
                length: MockGVFSGitObjects.DefaultFileLength,
                dataStreamId: Guid.NewGuid(),
                contentId: CommonRepoSetup.DefaultContentId,
                providerId: providerId,
                triggeringProcessId: 2,
                triggeringProcessImageFileName: "UnitTest").ShouldEqual(expectedResult);
        }

        protected override FileSystemVirtualizer CreateVirtualizer(CommonRepoSetup repo)
        {
            this.MockVirtualization = new MockVirtualizationInstance();
            this.WindowsVirtualizer = new WindowsFileSystemVirtualizer(repo.Context, repo.GitObjects, this.MockVirtualization, FileSystemVirtualizerTester.NumberOfWorkThreads);
            return this.WindowsVirtualizer;
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Windows/Platform/ProjFSFilterTests.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Platform.Windows;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using Moq;
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;

namespace GVFS.UnitTests.Windows.Platform
{
    [TestFixture]
    public class ProjFSFilterTests
    {
        private const string System32DriversRoot = @"%SystemRoot%\System32\drivers";
        private const string PrjFltDriverName = "prjflt.sys";
        private const string ProjFSNativeLibFileName = "ProjectedFSLib.dll";

        private readonly string system32NativeLibPath = Path.Combine(Environment.SystemDirectory, ProjFSNativeLibFileName);
        private readonly string nonInboxNativeLibInstallPath = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), ProjFSNativeLibFileName);
        private readonly string packagedNativeLibPath = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), "ProjFS", ProjFSNativeLibFileName);

        private readonly string packagedDriverPath = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), "Filter", PrjFltDriverName);
        private readonly string system32DriverPath = Path.Combine(Environment.ExpandEnvironmentVariables(System32DriversRoot), PrjFltDriverName);

        // .NET doesn't allow us to create custom FileVersionInfos, and so use the version for our assembly and mock
        // the version comparison methods
        private readonly FileVersionInfo dummyVersionInfo = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location);

        private Mock mockFileSystem;
        private MockTracer mockTracer;

        [SetUp]
        public void Setup()
        {
            this.mockFileSystem = new Mock(MockBehavior.Strict);
            this.mockTracer = new MockTracer();
        }

        [TearDown]
        public void TearDown()
        {
            this.mockFileSystem.VerifyAll();
        }

        [TestCase]
        public void IsNativeLibInstalled_ReturnsTrueWhenLibInSystem32()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32NativeLibPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.nonInboxNativeLibInstallPath)).Returns(false);
            ProjFSFilter.IsNativeLibInstalled(this.mockTracer, this.mockFileSystem.Object).ShouldBeTrue();
        }

        [TestCase]
        public void IsNativeLibInstalled_ReturnsTrueWhenLibInNonInboxInstallLocation()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32NativeLibPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.nonInboxNativeLibInstallPath)).Returns(true);
            ProjFSFilter.IsNativeLibInstalled(this.mockTracer, this.mockFileSystem.Object).ShouldBeTrue();
        }

        [TestCase]
        public void IsNativeLibInstalled_ReturnsFalseWhenNativeLibraryDoesNotExistInAnyInstallLocation()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(It.IsAny())).Returns(false);
            ProjFSFilter.IsNativeLibInstalled(this.mockTracer, this.mockFileSystem.Object).ShouldBeFalse();
        }

        [TestCase]
        public void TryCopyNativeLibIfDriverVersionsMatch_ReturnsFalseWhenLibInSystem32()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32NativeLibPath)).Returns(true);
            ProjFSFilter.TryCopyNativeLibIfDriverVersionsMatch(this.mockTracer, this.mockFileSystem.Object, out string _).ShouldBeFalse();
        }

        [TestCase]
        public void TryCopyNativeLibIfDriverVersionsMatch_ReturnsFalseWhenLibAtNonInboxInstallLocation()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32NativeLibPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.nonInboxNativeLibInstallPath)).Returns(true);
            ProjFSFilter.TryCopyNativeLibIfDriverVersionsMatch(this.mockTracer, this.mockFileSystem.Object, out string _).ShouldBeFalse();
        }

        [TestCase]
        public void TryCopyNativeLibIfDriverVersionsMatch_ReturnsFalseWhenLibMissingFromPackagedLocation()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32NativeLibPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.nonInboxNativeLibInstallPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedNativeLibPath)).Returns(false);
            ProjFSFilter.TryCopyNativeLibIfDriverVersionsMatch(this.mockTracer, this.mockFileSystem.Object, out string _).ShouldBeFalse();
        }

        [TestCase]
        public void TryCopyNativeLibIfDriverVersionsMatch_ReturnsFalseWhenDriverMissingFromPackagedLocation()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32NativeLibPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.nonInboxNativeLibInstallPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedNativeLibPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedDriverPath)).Returns(false);
            ProjFSFilter.TryCopyNativeLibIfDriverVersionsMatch(this.mockTracer, this.mockFileSystem.Object, out string _).ShouldBeFalse();
        }

        [TestCase]
        public void TryCopyNativeLibIfDriverVersionsMatch_ReturnsFalseWhenDriverMissingFromSystem32()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32NativeLibPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.nonInboxNativeLibInstallPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedNativeLibPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedDriverPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32DriverPath)).Returns(false);
            ProjFSFilter.TryCopyNativeLibIfDriverVersionsMatch(this.mockTracer, this.mockFileSystem.Object, out string _).ShouldBeFalse();
        }

        [TestCase]
        public void TryCopyNativeLibIfDriverVersionsMatch_ReturnsFalseWhenFileVersionDoesNotMatch()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32NativeLibPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.nonInboxNativeLibInstallPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedNativeLibPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedDriverPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32DriverPath)).Returns(true);

            this.mockFileSystem.Setup(fileSystem => fileSystem.GetVersionInfo(this.packagedDriverPath)).Returns(this.dummyVersionInfo);
            this.mockFileSystem.Setup(fileSystem => fileSystem.GetVersionInfo(this.system32DriverPath)).Returns(this.dummyVersionInfo);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileVersionsMatch(this.dummyVersionInfo, this.dummyVersionInfo)).Returns(false);
            ProjFSFilter.TryCopyNativeLibIfDriverVersionsMatch(this.mockTracer, this.mockFileSystem.Object, out string _).ShouldBeFalse();
        }

        [TestCase]
        public void TryCopyNativeLibIfDriverVersionsMatch_ReturnsFalseWhenProductVersionDoesNotMatch()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32NativeLibPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.nonInboxNativeLibInstallPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedNativeLibPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedDriverPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32DriverPath)).Returns(true);

            this.mockFileSystem.Setup(fileSystem => fileSystem.GetVersionInfo(this.packagedDriverPath)).Returns(this.dummyVersionInfo);
            this.mockFileSystem.Setup(fileSystem => fileSystem.GetVersionInfo(this.system32DriverPath)).Returns(this.dummyVersionInfo);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileVersionsMatch(this.dummyVersionInfo, this.dummyVersionInfo)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.ProductVersionsMatch(this.dummyVersionInfo, this.dummyVersionInfo)).Returns(false);
            ProjFSFilter.TryCopyNativeLibIfDriverVersionsMatch(this.mockTracer, this.mockFileSystem.Object, out string _).ShouldBeFalse();
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void TryCopyNativeLibIfDriverVersionsMatch_ReturnsFalseWhenCopyingNativeLibFails()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32NativeLibPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.nonInboxNativeLibInstallPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedNativeLibPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedDriverPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32DriverPath)).Returns(true);

            this.mockFileSystem.Setup(fileSystem => fileSystem.GetVersionInfo(this.packagedDriverPath)).Returns(this.dummyVersionInfo);
            this.mockFileSystem.Setup(fileSystem => fileSystem.GetVersionInfo(this.system32DriverPath)).Returns(this.dummyVersionInfo);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileVersionsMatch(this.dummyVersionInfo, this.dummyVersionInfo)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.ProductVersionsMatch(this.dummyVersionInfo, this.dummyVersionInfo)).Returns(true);

            this.mockFileSystem.Setup(fileSystem => fileSystem.CopyFile(this.packagedNativeLibPath, this.nonInboxNativeLibInstallPath, true)).Throws(new IOException());
            ProjFSFilter.TryCopyNativeLibIfDriverVersionsMatch(this.mockTracer, this.mockFileSystem.Object, out string _).ShouldBeFalse();
        }

        [TestCase]
        public void TryCopyNativeLibIfDriverVersionsMatch_ReturnsTrueOnSuccess()
        {
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32NativeLibPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.nonInboxNativeLibInstallPath)).Returns(false);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedNativeLibPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.packagedDriverPath)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.system32DriverPath)).Returns(true);

            this.mockFileSystem.Setup(fileSystem => fileSystem.GetVersionInfo(this.packagedDriverPath)).Returns(this.dummyVersionInfo);
            this.mockFileSystem.Setup(fileSystem => fileSystem.GetVersionInfo(this.system32DriverPath)).Returns(this.dummyVersionInfo);
            this.mockFileSystem.Setup(fileSystem => fileSystem.FileVersionsMatch(this.dummyVersionInfo, this.dummyVersionInfo)).Returns(true);
            this.mockFileSystem.Setup(fileSystem => fileSystem.ProductVersionsMatch(this.dummyVersionInfo, this.dummyVersionInfo)).Returns(true);

            this.mockFileSystem.Setup(fileSystem => fileSystem.CopyFile(this.packagedNativeLibPath, this.nonInboxNativeLibInstallPath, true));
            this.mockFileSystem.Setup(fileSystem => fileSystem.FlushFileBuffers(this.nonInboxNativeLibInstallPath));
            ProjFSFilter.TryCopyNativeLibIfDriverVersionsMatch(this.mockTracer, this.mockFileSystem.Object, out string _).ShouldBeTrue();
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Windows/Virtualization/ActiveEnumerationTests.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Platform.Windows;
using GVFS.Tests.Should;
using GVFS.Virtualization.Projection;
using Microsoft.Windows.ProjFS;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;

namespace GVFS.UnitTests.Windows.Virtualization
{
    [TestFixtureSource(TestRunners)]
    public class ActiveEnumerationTests
    {
        public const string TestRunners = "Runners";

        private static object[] patternMatchers =
        {
            new object[] { new PatternMatcherWrapper(Utils.IsFileNameMatch) },
            new object[] { new PatternMatcherWrapper(WindowsFileSystemVirtualizer.InternalFileNameMatchesFilter) },
        };

        public ActiveEnumerationTests(PatternMatcherWrapper wrapper)
        {
            ActiveEnumeration.SetWildcardPatternMatcher(wrapper.Matcher);
        }

        public static object[] Runners
        {
            get { return patternMatchers; }
        }

        [TestCase]
        public void EnumerationHandlesEmptyList()
        {
            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]
        public void EnumerateSingleEntryList()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1))
            };

            ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
        }

        [TestCase]
        public void EnumerateMultipleEntries()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1)),
                new ProjectedFileInfo("B", size: 0, isFolder:true, sha: Sha1Id.None),
                new ProjectedFileInfo("c", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 2)),
                new ProjectedFileInfo("D.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 3)),
                new ProjectedFileInfo("E.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 4)),
                new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
            };

            ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
        }

        [TestCase]
        public void EnumerateSingleEntryListWithEmptyFilter()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1))
            };

            // Test empty string ("") filter
            ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString(string.Empty).ShouldEqual(true);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);

            // Test null filter
            activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString(null).ShouldEqual(true);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
        }

        [TestCase]
        public void EnumerateSingleEntryListWithWildcardFilter()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1))
            };

            ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString("*").ShouldEqual(true);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);

            activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString("?").ShouldEqual(true);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);

            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.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]
        public void EnumerateSingleEntryListWithMatchingFilter()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1))
            };

            ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString("a").ShouldEqual(true);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);

            activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString("A").ShouldEqual(true);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
        }

        [TestCase]
        public void EnumerateSingleEntryListWithNonMatchingFilter()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1))
            };

            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]
        public void CannotSetMoreThanOneFilter()
        {
            string 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]
        public void EnumerateMultipleEntryListWithEmptyFilter()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1)),
                new ProjectedFileInfo("B", size: 0, isFolder:true, sha: Sha1Id.None),
                new ProjectedFileInfo("c", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 2)),
                new ProjectedFileInfo("D.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 3)),
                new ProjectedFileInfo("E.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 4)),
                new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
            };

            // Test empty string ("") filter
            ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString(string.Empty).ShouldEqual(true);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);

            // Test null filter
            activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString(null).ShouldEqual(true);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);
        }

        [TestCase]
        public void EnumerateMultipleEntryListWithWildcardFilter()
        {
            List entries = new List()
            {
                new ProjectedFileInfo(".txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1)),
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 2)),
                new ProjectedFileInfo("B", size: 0, isFolder:true, sha: Sha1Id.None),
                new ProjectedFileInfo("c", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 3)),
                new ProjectedFileInfo("D.", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 4)),
                new ProjectedFileInfo("D.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
                new ProjectedFileInfo("E..log", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 6)),
                new ProjectedFileInfo("E.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 7)),
                new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 8)),
            };

            ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString("*").ShouldEqual(true);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries);

            activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString("*.*").ShouldEqual(true);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Contains(".")));

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

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

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

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

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

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

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

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

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

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

        [TestCase]
        public void EnumerateMultipleEntryListWithMatchingFilter()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1)),
                new ProjectedFileInfo("B", size: 0, isFolder:true, sha: Sha1Id.None),
                new ProjectedFileInfo("c", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 2)),
                new ProjectedFileInfo("D.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 3)),
                new ProjectedFileInfo("E.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 4)),
                new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
            };

            ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString("E.bat").ShouldEqual(true);
            this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name == "E.bat"));

            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]
        public void EnumerateMultipleEntryListWithNonMatchingFilter()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1)),
                new ProjectedFileInfo("B", size: 0, isFolder:true, sha: Sha1Id.None),
                new ProjectedFileInfo("c", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 2)),
                new ProjectedFileInfo("D.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 3)),
                new ProjectedFileInfo("E.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 4)),
                new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
            };

            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]
        public void SettingFilterAdvancesEnumeratorToMatchingEntry()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1)),
                new ProjectedFileInfo("B", size: 0, isFolder:true, sha: Sha1Id.None),
                new ProjectedFileInfo("c", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 2)),
                new ProjectedFileInfo("D.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 3)),
                new ProjectedFileInfo("E.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 4)),
                new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
            };

            ActiveEnumeration activeEnumeration = CreateActiveEnumeration(entries);
            activeEnumeration.TrySaveFilterString("D.txt").ShouldEqual(true);
            activeEnumeration.IsCurrentValid.ShouldEqual(true);
            activeEnumeration.Current.Name.ShouldEqual("D.txt");
        }

        [TestCase]
        public void RestartingScanWithFilterAdvancesEnumeratorToNewMatchingEntry()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("a", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1)),
                new ProjectedFileInfo("B", size: 0, isFolder:true, sha: Sha1Id.None),
                new ProjectedFileInfo("c", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 2)),
                new ProjectedFileInfo("D.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 3)),
                new ProjectedFileInfo("E.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 4)),
                new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 5)),
            };

            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");
        }

        [TestCase]
        public void RestartingScanWithFilterAdvancesEnumeratorToFirstMatchingEntry()
        {
            List entries = new List()
            {
                new ProjectedFileInfo("C.TXT", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 1)),
                new ProjectedFileInfo("D.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 2)),
                new ProjectedFileInfo("E.txt", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 3)),
                new ProjectedFileInfo("E.bat", size: 0, isFolder:false, sha: new Sha1Id(1, 1, 4)),
            };

            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]);
            }
            else
            {
                activeEnumeration.IsCurrentValid.ShouldEqual(false);
                activeEnumeration.Current.ShouldBeNull();
            }

            return activeEnumeration;
        }

        private void ValidateActiveEnumeratorReturnsAllEntries(ActiveEnumeration activeEnumeration, IEnumerable entries)
        {
            activeEnumeration.IsCurrentValid.ShouldEqual(true);

            // activeEnumeration should iterate over each entry in entries
            foreach (ProjectedFileInfo entry in entries)
            {
                activeEnumeration.IsCurrentValid.ShouldEqual(true);
                activeEnumeration.Current.ShouldBeSameAs(entry);
                activeEnumeration.MoveNext();
            }

            // 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
        {
            public PatternMatcherWrapper(ActiveEnumeration.FileNamePatternMatcher matcher)
            {
                this.Matcher = matcher;
            }

            public ActiveEnumeration.FileNamePatternMatcher Matcher { get; }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Windows/Virtualization/PatternMatcherTests.cs
================================================
using GVFS.Platform.Windows;
using GVFS.Tests.Should;
using Microsoft.Windows.ProjFS;
using NUnit.Framework;

namespace GVFS.UnitTests.Windows.Virtualization
{
    [TestFixture]
    public class PatternMatcherTests
    {
        private const char DOSStar = '<';
        private const char DOSQm = '>';
        private const char DOSDot = '"';

        [TestCase]
        public void EmptyPatternShouldMatch()
        {
            PatternShouldMatch(null, "Test");
            PatternShouldMatch(string.Empty, "Test");
        }

        [TestCase]
        public void EmptyNameDoesNotMatch()
        {
            PatternShouldNotMatch("Test", null);
            PatternShouldNotMatch("Test", string.Empty);
            PatternShouldNotMatch(null, null);
            PatternShouldNotMatch(string.Empty, string.Empty);
        }

        [TestCase]
        public void IdenticalStringsMatch()
        {
            PatternShouldMatch("Test", "Test");
            PatternShouldMatch("ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
        }

        [TestCase]
        public void MatchingIsCaseInsensitive()
        {
            PatternShouldMatch("Test", "TEST");
            PatternShouldMatch("TEST", "Test");
            PatternShouldMatch("ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.TXT");
            PatternShouldMatch("ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.TXT", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
        }

        [TestCase]
        public void WildCardSearchMatchesEverything()
        {
            PatternShouldMatch("*", "Test");
            PatternShouldNotMatch("*.*", "Test");
            PatternShouldMatch("*", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
            PatternShouldMatch("*.*", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
        }

        [TestCase]
        public void TestLeadingStarPattern()
        {
            PatternShouldMatch("*est", "Test");
            PatternShouldMatch("*EST", "Test");
            PatternShouldMatch("*txt", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
            PatternShouldMatch("*.TXT", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
        }

        [TestCase]
        public void TestLeadingDosStarPattern()
        {
            PatternShouldMatch(DOSStar + "est", "Test");
            PatternShouldMatch(DOSStar + "EST", "Test");
            PatternShouldMatch(DOSStar + "txt", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
            PatternShouldMatch(DOSStar + "TXT", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
        }

        [TestCase]
        public void TestTrailingDosQmPattern()
        {
            PatternShouldMatch("Test" + DOSQm, "Test");
            PatternShouldMatch("TEST" + DOSQm, "Test");
            PatternShouldMatch("ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt" + DOSQm, "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
        }

        [TestCase]
        public void TestQuestionMarkPattern()
        {
            PatternShouldNotMatch("???", "Test");
            PatternShouldMatch("????", "Test");
            PatternShouldNotMatch("?????", "Test");
        }

        [TestCase]
        public void TestMixedQuestionMarkPattern()
        {
            PatternShouldMatch("T?st", "Test");
            PatternShouldMatch("T?ST", "Test");
            PatternShouldNotMatch("T??ST", "Test");
        }

        [TestCase]
        public void TestMixedStarPattern()
        {
            PatternShouldMatch("T*est", "Test");
            PatternShouldMatch("T*t", "Test");
            PatternShouldMatch("T*T", "Test");
            PatternShouldMatch("ر*يلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
        }

        [TestCase]
        public void TestMixedStarAndQuestionMarkPattern()
        {
            PatternShouldNotMatch("T*?est", "Test");
            PatternShouldMatch("T*?t", "Test");
            PatternShouldMatch("T*?", "Test");
            PatternShouldMatch("t*?", "Test");
            PatternShouldMatch("ر*يلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.?xt", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
        }

        [TestCase]
        public void TestDosStarPattern()
        {
            PatternShouldMatch("T" + DOSStar, "Test");
            PatternShouldMatch("t" + DOSStar + "txt", "Test.txt");
            PatternShouldMatch("ر*يلٌأكتوبرû" + DOSStar + "TXT", "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
        }

        [TestCase]
        public void TestDosDotPattern()
        {
            PatternShouldMatch("Test" + DOSDot, "Test");
            PatternShouldMatch("Test" + DOSDot, "Test.");
            PatternShouldNotMatch("Test" + DOSDot, "Test.txt");
            PatternShouldMatch("ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt" + DOSDot, "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt");
            PatternShouldMatch("ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt" + DOSDot, "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt.");
            PatternShouldNotMatch("ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt" + DOSDot, "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt.temp");
        }

        [TestCase]
        public void TestDosQmPattern()
        {
            PatternShouldNotMatch(string.Concat(DOSQm, DOSQm, DOSQm), "Test");
            PatternShouldMatch(string.Concat(DOSQm, DOSQm, DOSQm, DOSQm), "Test");
            PatternShouldMatch(string.Concat(DOSQm, DOSQm, DOSQm, DOSQm, DOSQm), "Test");

            PatternShouldNotMatch(string.Concat("Te", DOSQm), "Test");
            PatternShouldMatch(string.Concat("TE", DOSQm, DOSQm), "Test");
            PatternShouldMatch(string.Concat("te", DOSQm, DOSQm, DOSQm), "Test");
        }

        private static void PatternShouldMatch(string filter, string name)
        {
            PatternMatcher.StrictMatchPattern(filter, name).ShouldBeTrue();
            Utils.IsFileNameMatch(name, filter).ShouldBeTrue();
        }

        private static void PatternShouldNotMatch(string filter, string name)
        {
            PatternMatcher.StrictMatchPattern(filter, name).ShouldBeFalse();
            Utils.IsFileNameMatch(name, filter).ShouldBeFalse();
        }
    }
}

================================================
FILE: GVFS/GVFS.UnitTests/Windows/Virtualization/WindowsFileSystemVirtualizerTests.cs
================================================
using GVFS.Platform.Windows;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.Git;
using GVFS.UnitTests.Virtual;
using GVFS.UnitTests.Windows.Mock;
using GVFS.Virtualization.FileSystem;
using Microsoft.Windows.ProjFS;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace GVFS.UnitTests.Windows.Virtualization
{
    [TestFixture]
    public class WindowsFileSystemVirtualizerTests : TestsWithCommonRepo
    {
        private const uint TriggeringProcessId = 1;
        private const string TriggeringProcessImageFileName = "UnitTests";

        private static readonly Dictionary MappedHResults = new Dictionary()
        {
            { HResult.Ok, FSResult.Ok },
            { HResult.DirNotEmpty, FSResult.DirectoryNotEmpty },
            { HResult.FileNotFound, FSResult.FileOrPathNotFound },
            { HResult.PathNotFound, FSResult.FileOrPathNotFound },
            { (HResult)HResultExtensions.HResultFromNtStatus.IoReparseTagNotHandled, FSResult.IoReparseTagNotHandled },
            { HResult.VirtualizationInvalidOp, FSResult.VirtualizationInvalidOperation },
        };

        private static int numWorkThreads = 1;

        [TestCase]
        public void HResultToFSResultMapsHResults()
        {
            foreach (HResult result in Enum.GetValues(typeof(HResult)))
            {
                if (MappedHResults.ContainsKey(result))
                {
                    WindowsFileSystemVirtualizer.HResultToFSResult(result).ShouldEqual(MappedHResults[result]);
                }
                else
                {
                    WindowsFileSystemVirtualizer.HResultToFSResult(result).ShouldEqual(FSResult.IOError);
                }
            }
        }

        [TestCase]
        public void ClearNegativePathCache()
        {
            const uint InitialNegativePathCacheCount = 7;
            using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance())
            using (WindowsFileSystemVirtualizer virtualizer = new WindowsFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization, numWorkThreads))
            {
                mockVirtualization.NegativePathCacheCount = InitialNegativePathCacheCount;

                uint totalEntryCount;
                virtualizer.ClearNegativePathCache(out totalEntryCount).ShouldEqual(new FileSystemResult(FSResult.Ok, (int)HResult.Ok));
                totalEntryCount.ShouldEqual(InitialNegativePathCacheCount);
            }
        }

        [TestCase]
        public void DeleteFile()
        {
            using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance())
            using (WindowsFileSystemVirtualizer virtualizer = new WindowsFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization, numWorkThreads))
            {
                UpdateFailureReason failureReason = UpdateFailureReason.NoFailure;

                mockVirtualization.DeleteFileResult = HResult.Ok;
                mockVirtualization.DeleteFileUpdateFailureCause = UpdateFailureCause.NoFailure;
                virtualizer
                    .DeleteFile("test.txt", UpdatePlaceholderType.AllowReadOnly, out failureReason)
                    .ShouldEqual(new FileSystemResult(FSResult.Ok, (int)mockVirtualization.DeleteFileResult));
                failureReason.ShouldEqual((UpdateFailureReason)mockVirtualization.DeleteFileUpdateFailureCause);

                mockVirtualization.DeleteFileResult = HResult.FileNotFound;
                mockVirtualization.DeleteFileUpdateFailureCause = UpdateFailureCause.NoFailure;
                virtualizer
                    .DeleteFile("test.txt", UpdatePlaceholderType.AllowReadOnly, out failureReason)
                    .ShouldEqual(new FileSystemResult(FSResult.FileOrPathNotFound, (int)mockVirtualization.DeleteFileResult));
                failureReason.ShouldEqual((UpdateFailureReason)mockVirtualization.DeleteFileUpdateFailureCause);

                mockVirtualization.DeleteFileResult = HResult.VirtualizationInvalidOp;
                mockVirtualization.DeleteFileUpdateFailureCause = UpdateFailureCause.DirtyData;
                virtualizer
                    .DeleteFile("test.txt", UpdatePlaceholderType.AllowReadOnly, out failureReason)
                    .ShouldEqual(new FileSystemResult(FSResult.VirtualizationInvalidOperation, (int)mockVirtualization.DeleteFileResult));
                failureReason.ShouldEqual((UpdateFailureReason)mockVirtualization.DeleteFileUpdateFailureCause);
            }
        }

        [TestCase]
        public void UpdatePlaceholderIfNeeded()
        {
            using (MockVirtualizationInstance mockVirtualization = new MockVirtualizationInstance())
            using (WindowsFileSystemVirtualizer virtualizer = new WindowsFileSystemVirtualizer(this.Repo.Context, this.Repo.GitObjects, mockVirtualization, numWorkThreads))
            {
                UpdateFailureReason failureReason = UpdateFailureReason.NoFailure;

                mockVirtualization.UpdateFileIfNeededResult = HResult.Ok;
                mockVirtualization.UpdateFileIfNeededFailureCase = UpdateFailureCause.NoFailure;
                virtualizer
                    .UpdatePlaceholderIfNeeded(
                        "test.txt",
                        DateTime.Now,
                        DateTime.Now,
                        DateTime.Now,
                        DateTime.Now,
                        0,
                        15,
                        string.Empty,
                        UpdatePlaceholderType.AllowReadOnly,
                        out failureReason)
                    .ShouldEqual(new FileSystemResult(FSResult.Ok, (int)mockVirtualization.UpdateFileIfNeededResult));
                failureReason.ShouldEqual((UpdateFailureReason)mockVirtualization.UpdateFileIfNeededFailureCase);

                mockVirtualization.UpdateFileIfNeededResult = HResult.FileNotFound;
                mockVirtualization.UpdateFileIfNeededFailureCase = UpdateFailureCause.NoFailure;
                virtualizer
                    .UpdatePlaceholderIfNeeded(
                        "test.txt",
                        DateTime.Now,
                        DateTime.Now,
                        DateTime.Now,
                        DateTime.Now,
                        0,
                        15,
                        string.Empty,
                        UpdatePlaceholderType.AllowReadOnly,
                        out failureReason)
                    .ShouldEqual(new FileSystemResult(FSResult.FileOrPathNotFound, (int)mockVirtualization.UpdateFileIfNeededResult));
                failureReason.ShouldEqual((UpdateFailureReason)mockVirtualization.UpdateFileIfNeededFailureCase);

                mockVirtualization.UpdateFileIfNeededResult = HResult.VirtualizationInvalidOp;
                mockVirtualization.UpdateFileIfNeededFailureCase = UpdateFailureCause.DirtyData;
                virtualizer
                    .UpdatePlaceholderIfNeeded(
                        "test.txt",
                        DateTime.Now,
                        DateTime.Now,
                        DateTime.Now,
                        DateTime.Now,
                        0,
                        15,
                        string.Empty,
                        UpdatePlaceholderType.AllowReadOnly,
                        out failureReason)
                    .ShouldEqual(new FileSystemResult(FSResult.VirtualizationInvalidOperation, (int)mockVirtualization.UpdateFileIfNeededResult));
                failureReason.ShouldEqual((UpdateFailureReason)mockVirtualization.UpdateFileIfNeededFailureCase);
            }
        }

        [TestCase]
        public void OnStartDirectoryEnumerationReturnsPendingWhenResultsNotInMemory()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                Guid enumerationGuid = Guid.NewGuid();
                tester.GitIndexProjection.EnumerationInMemory = false;
                tester.MockVirtualization.requiredCallbacks.StartDirectoryEnumerationCallback(1, enumerationGuid, "test", TriggeringProcessId, TriggeringProcessImageFileName).ShouldEqual(HResult.Pending);
                tester.MockVirtualization.WaitForCompletionStatus().ShouldEqual(HResult.Ok);
                tester.MockVirtualization.requiredCallbacks.EndDirectoryEnumerationCallback(enumerationGuid).ShouldEqual(HResult.Ok);
            }
        }

        [TestCase]
        public void OnStartDirectoryEnumerationReturnsSuccessWhenResultsInMemory()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo, new[] { "test" }))
            {
                Guid enumerationGuid = Guid.NewGuid();
                tester.GitIndexProjection.EnumerationInMemory = true;
                tester.MockVirtualization.requiredCallbacks.StartDirectoryEnumerationCallback(1, enumerationGuid, "test", TriggeringProcessId, TriggeringProcessImageFileName).ShouldEqual(HResult.Ok);
                tester.MockVirtualization.requiredCallbacks.EndDirectoryEnumerationCallback(enumerationGuid).ShouldEqual(HResult.Ok);
            }
        }

        [TestCase]
        public void GetPlaceholderInformationHandlerPathNotProjected()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                tester.MockVirtualization.requiredCallbacks.GetPlaceholderInfoCallback(1, "doesNotExist", TriggeringProcessId, TriggeringProcessImageFileName).ShouldEqual(HResult.FileNotFound);
            }
        }

        [TestCase]
        public void GetPlaceholderInformationHandlerPathProjected()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                tester.MockVirtualization.requiredCallbacks.GetPlaceholderInfoCallback(1, "test.txt", TriggeringProcessId, TriggeringProcessImageFileName).ShouldEqual(HResult.Pending);
                tester.MockVirtualization.WaitForCompletionStatus().ShouldEqual(HResult.Ok);
                tester.MockVirtualization.CreatedPlaceholders.ShouldContain(entry => entry == "test.txt");
                tester.GitIndexProjection.PlaceholdersCreated.ShouldContain(entry => entry == "test.txt");
            }
        }

        [TestCase]
        public void GetPlaceholderInformationHandlerCancelledBeforeSchedulingAsync()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                tester.GitIndexProjection.BlockIsPathProjected(willWaitForRequest: true);

                Task.Run(() =>
                {
                    // Wait for OnGetPlaceholderInformation to call IsPathProjected and then while it's blocked there
                    // call OnCancelCommand
                    tester.GitIndexProjection.WaitForIsPathProjected();
                    tester.MockVirtualization.OnCancelCommand(1);
                    tester.GitIndexProjection.UnblockIsPathProjected();
                });

                tester.MockVirtualization.requiredCallbacks.GetPlaceholderInfoCallback(1, "test.txt", TriggeringProcessId, TriggeringProcessImageFileName).ShouldEqual(HResult.Pending);

                // Cancelling before GetPlaceholderInformation has registered the command results in placeholders being created
                tester.MockVirtualization.WaitForPlaceholderCreate();
                tester.GitIndexProjection.WaitForPlaceholderCreate();
                tester.MockVirtualization.CreatedPlaceholders.ShouldContain(entry => entry == "test.txt");
                tester.GitIndexProjection.PlaceholdersCreated.ShouldContain(entry => entry == "test.txt");
            }
        }

        [TestCase]
        public void GetPlaceholderInformationHandlerCancelledDuringAsyncCallback()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                tester.GitIndexProjection.BlockGetProjectedFileInfo(willWaitForRequest: true);
                tester.MockVirtualization.requiredCallbacks.GetPlaceholderInfoCallback(1, "test.txt", TriggeringProcessId, TriggeringProcessImageFileName).ShouldEqual(HResult.Pending);
                tester.GitIndexProjection.WaitForGetProjectedFileInfo();
                tester.MockVirtualization.OnCancelCommand(1);
                tester.GitIndexProjection.UnblockGetProjectedFileInfo();

                // Cancelling in the middle of GetPlaceholderInformation still allows it to create placeholders when the cancellation does not
                // interrupt network requests
                tester.MockVirtualization.WaitForPlaceholderCreate();
                tester.GitIndexProjection.WaitForPlaceholderCreate();
                tester.MockVirtualization.CreatedPlaceholders.ShouldContain(entry => entry == "test.txt");
                tester.GitIndexProjection.PlaceholdersCreated.ShouldContain(entry => entry == "test.txt");
            }
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void GetPlaceholderInformationHandlerCancelledDuringNetworkRequest()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                MockTracer mockTracker = this.Repo.Context.Tracer as MockTracer;
                mockTracker.WaitRelatedEventName = "GetPlaceholderInformationAsyncHandler_GetProjectedFileInfo_Cancelled";
                tester.GitIndexProjection.ThrowOperationCanceledExceptionOnProjectionRequest = true;
                tester.MockVirtualization.requiredCallbacks.GetPlaceholderInfoCallback(1, "test.txt", TriggeringProcessId, TriggeringProcessImageFileName).ShouldEqual(HResult.Pending);

                // Cancelling in the middle of GetPlaceholderInformation in the middle of a network request should not result in placeholder
                // getting created
                mockTracker.WaitForRelatedEvent();
                tester.MockVirtualization.CreatedPlaceholders.ShouldNotContain(entry => entry == "test.txt");
                tester.GitIndexProjection.PlaceholdersCreated.ShouldNotContain(entry => entry == "test.txt");
            }
        }

        [TestCase]
        public void OnGetFileStreamReturnsInternalErrorWhenOffsetNonZero()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                tester.InvokeGetFileDataCallback(expectedResult: HResult.InternalError, byteOffset: 10);
            }
        }

        [TestCase]
        public void OnGetFileStreamReturnsInternalErrorWhenPlaceholderVersionDoesNotMatchExpected()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                byte[] epochId = new byte[] { FileSystemVirtualizer.PlaceholderVersion + 1 };
                tester.InvokeGetFileDataCallback(expectedResult: HResult.InternalError, providerId: epochId);
            }
        }

        [TestCase]
        public void MoveFileIntoDotGitDirectory()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                NotificationType notificationType = NotificationType.UseExistingMask;
                tester.MockVirtualization.OnNotifyFileRenamed(
                        "test.txt",
                        Path.Combine(".git", "test.txt"),
                        isDirectory: false,
                        triggeringProcessId: TriggeringProcessId,
                        triggeringProcessImageFileName: TriggeringProcessImageFileName,
                        notificationMask: out notificationType);
                notificationType.ShouldEqual(NotificationType.UseExistingMask);
                tester.FileSystemCallbacks.OnIndexFileChangeCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.OnLogsHeadChangeCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.OnFileRenamedCallCount.ShouldEqual(1);
                tester.FileSystemCallbacks.OnFolderRenamedCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.ResetCalls();

                // We don't expect something to rename something from outside the .gitdir to the .git\index, but this
                // verifies that we behave as expected in case that happens
                tester.MockVirtualization.OnNotifyFileRenamed(
                        "test.txt",
                        Path.Combine(".git", "index"),
                        isDirectory: false,
                        triggeringProcessId: TriggeringProcessId,
                        triggeringProcessImageFileName: TriggeringProcessImageFileName,
                        notificationMask: out notificationType);
                notificationType.ShouldEqual(NotificationType.UseExistingMask);
                tester.FileSystemCallbacks.OnIndexFileChangeCallCount.ShouldEqual(1);
                tester.FileSystemCallbacks.OnLogsHeadChangeCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.OnFileRenamedCallCount.ShouldEqual(1);
                tester.FileSystemCallbacks.OnFolderRenamedCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.ResetCalls();

                // We don't expect something to rename something from outside the .gitdir to the .git\logs\HEAD, but this
                // verifies that we behave as expected in case that happens
                tester.MockVirtualization.OnNotifyFileRenamed(
                        "test.txt",
                        Path.Combine(".git", "logs\\HEAD"),
                        isDirectory: false,
                        triggeringProcessId: TriggeringProcessId,
                        triggeringProcessImageFileName: TriggeringProcessImageFileName,
                        notificationMask: out notificationType);
                notificationType.ShouldEqual(NotificationType.UseExistingMask);
                tester.FileSystemCallbacks.OnIndexFileChangeCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.OnLogsHeadChangeCallCount.ShouldEqual(1);
                tester.FileSystemCallbacks.OnFileRenamedCallCount.ShouldEqual(1);
                tester.FileSystemCallbacks.OnFolderRenamedCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.ResetCalls();
            }
        }

        [TestCase]
        public void MoveFileFromDotGitToSrc()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                NotificationType notificationType = NotificationType.UseExistingMask;
                tester.MockVirtualization.OnNotifyFileRenamed(
                        Path.Combine(".git", "test.txt"),
                        "test2.txt",
                        isDirectory: false,
                        triggeringProcessId: TriggeringProcessId,
                        triggeringProcessImageFileName: TriggeringProcessImageFileName,
                        notificationMask: out notificationType);
                notificationType.ShouldEqual(NotificationType.UseExistingMask);
                tester.FileSystemCallbacks.OnIndexFileChangeCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.OnLogsHeadChangeCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.OnFileRenamedCallCount.ShouldEqual(1);
                tester.FileSystemCallbacks.OnFolderRenamedCallCount.ShouldEqual(0);
            }
        }

        [TestCase]
        public void MoveFile()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                NotificationType notificationType = NotificationType.UseExistingMask;
                tester.MockVirtualization.OnNotifyFileRenamed(
                    "test.txt",
                    "test2.txt",
                    isDirectory: false,
                    triggeringProcessId: TriggeringProcessId,
                    triggeringProcessImageFileName: TriggeringProcessImageFileName,
                    notificationMask: out notificationType);
                notificationType.ShouldEqual(NotificationType.UseExistingMask);
                tester.FileSystemCallbacks.OnIndexFileChangeCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.OnLogsHeadChangeCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.OnFileRenamedCallCount.ShouldEqual(1);
                tester.FileSystemCallbacks.OnFolderRenamedCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.ResetCalls();

                tester.MockVirtualization.OnNotifyFileRenamed(
                    "test_folder_src",
                    "test_folder_dst",
                    isDirectory: true,
                    triggeringProcessId: TriggeringProcessId,
                    triggeringProcessImageFileName: TriggeringProcessImageFileName,
                    notificationMask: out notificationType);
                notificationType.ShouldEqual(NotificationType.UseExistingMask);
                tester.FileSystemCallbacks.OnIndexFileChangeCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.OnLogsHeadChangeCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.OnFileRenamedCallCount.ShouldEqual(0);
                tester.FileSystemCallbacks.OnFolderRenamedCallCount.ShouldEqual(1);
            }
        }

        [TestCase]
        public void OnGetFileStreamReturnsPendingAndCompletesWithSuccessWhenNoFailures()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                tester.MockVirtualization.WriteFileReturnResult = HResult.Ok;

                tester.InvokeGetFileDataCallback(expectedResult: HResult.Pending);

                tester.MockVirtualization.WaitForCompletionStatus().ShouldEqual(HResult.Ok);
            }
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void OnGetFileStreamHandlesTryCopyBlobContentStreamThrowingOperationCanceled()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                MockTracer mockTracker = this.Repo.Context.Tracer as MockTracer;
                mockTracker.WaitRelatedEventName = "GetFileStreamHandlerAsyncHandler_OperationCancelled";
                MockGVFSGitObjects mockGVFSGitObjects = this.Repo.GitObjects as MockGVFSGitObjects;
                mockGVFSGitObjects.CancelTryCopyBlobContentStream = true;

                tester.InvokeGetFileDataCallback(expectedResult: HResult.Pending);

                mockTracker.WaitForRelatedEvent();
            }
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void OnGetFileStreamHandlesCancellationDuringWriteAction()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                MockTracer mockTracker = this.Repo.Context.Tracer as MockTracer;
                mockTracker.WaitRelatedEventName = "GetFileStreamHandlerAsyncHandler_OperationCancelled";

                tester.MockVirtualization.BlockCreateWriteBuffer(willWaitForRequest: true);
                tester.InvokeGetFileDataCallback(expectedResult: HResult.Pending);

                tester.MockVirtualization.WaitForCreateWriteBuffer();
                tester.MockVirtualization.OnCancelCommand(1);
                tester.MockVirtualization.UnblockCreateWriteBuffer();
                mockTracker.WaitForRelatedEvent();
            }
        }

        [TestCase]
        public void OnGetFileStreamHandlesWriteFailure()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                tester.MockVirtualization.WriteFileReturnResult = HResult.InternalError;
                tester.InvokeGetFileDataCallback(expectedResult: HResult.Pending);

                HResult result = tester.MockVirtualization.WaitForCompletionStatus();
                result.ShouldEqual(tester.MockVirtualization.WriteFileReturnResult);
            }
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void OnGetFileStreamHandlesHResultHandleResult()
        {
            using (WindowsFileSystemVirtualizerTester tester = new WindowsFileSystemVirtualizerTester(this.Repo))
            {
                tester.MockVirtualization.WriteFileReturnResult = HResult.Handle;
                tester.InvokeGetFileDataCallback(expectedResult: HResult.Pending);

                HResult result = tester.MockVirtualization.WaitForCompletionStatus();
                result.ShouldEqual(tester.MockVirtualization.WriteFileReturnResult);
                MockTracer mockTracker = this.Repo.Context.Tracer as MockTracer;
                mockTracker.RelatedErrorEvents.ShouldBeEmpty();
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.UnitTests/Windows/WindowsFileBasedLockTests.cs
================================================
using GVFS.Common;
using GVFS.Platform.Windows;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using NUnit.Framework;
using System;
using System.IO;

namespace GVFS.UnitTests.Windows
{
    [TestFixture]
    public class WindowsFileBasedLockTests
    {
        [TestCase]
        public void CreateLockWhenDirectoryMissing()
        {
            string parentPath = Path.Combine("mock:", "path", "to");
            string lockPath = Path.Combine(parentPath, "lock");
            MockTracer tracer = new MockTracer();
            FileBasedLockFileSystem fs = new FileBasedLockFileSystem();
            FileBasedLock fileBasedLock = new WindowsFileBasedLock(fs, tracer, lockPath);

            fileBasedLock.TryAcquireLock().ShouldBeTrue();
            fs.CreateDirectoryPath.ShouldNotBeNull();
            fs.CreateDirectoryPath.ShouldEqual(parentPath);
        }

        [TestCase]
        [Category(CategoryConstants.ExceptionExpected)]
        public void AttemptToAcquireLockWhenAlreadyLocked()
        {
            string parentPath = Path.Combine("mock:", "path", "to");
            string lockPath = Path.Combine(parentPath, "lock");
            MockTracer tracer = new MockTracer();
            FileBasedLockFileSystem fs = new FileBasedLockFileSystem();
            FileBasedLock fileBasedLock = new WindowsFileBasedLock(fs, tracer, lockPath);

            fileBasedLock.TryAcquireLock().ShouldBeTrue();
            Assert.Throws(() => fileBasedLock.TryAcquireLock());
        }

        private class FileBasedLockFileSystem : ConfigurableFileSystem
        {
            public string CreateDirectoryPath { get; set; }

            public override void CreateDirectory(string path)
            {
                this.CreateDirectoryPath = path;
            }

            public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk)
            {
                return new MemoryStream();
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.VirtualFileSystemHook/GVFS.VirtualFileSystemHook.vcxproj
================================================


  
    
      Debug
      x64
    
    
      Release
      x64
    
  
  
    {2D23AB54-541F-4ABC-8DCA-08C199E97ABB}
    Win32Proj
    GVFSVirtualFileSystemHook
    10.0
    GVFS.VirtualFileSystemHook
    GVFS.VirtualFileSystemHook
  
  
  
    Application
    true
    v143
    MultiByte
  
  
    Application
    false
    v143
    true
    MultiByte
  
  
  
  
  
  
  
    
  
  
    
  
  
  
    true
  
  
    false
  
  
    
      Use
      Level4
      Disabled
      _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0\ucrt;..\GVFS.NativeHooks.Common;%(AdditionalIncludeDirectories)
      /Zc:__cplusplus
      MultiThreadedDebug
    
    
      Console
      true
      C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64;%(AdditionalLibraryDirectories)
    
    
      $(IntDir)\$(MSBuildProjectName).log
    
    
    
    
      $(GeneratedIncludePath)
    
  
  
    
      Level4
      Use
      MaxSpeed
      true
      true
      NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0\ucrt;..\GVFS.NativeHooks.Common;%(AdditionalIncludeDirectories)
      /Zc:__cplusplus
      MultiThreaded
    
    
      Console
      true
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64;%(AdditionalLibraryDirectories)
    
    
      $(IntDir)\$(MSBuildProjectName).log
    
    
    
    
      $(GeneratedIncludePath)
    
  
  
    
    
    
    
  
  
    
    
    
      Create
      Create
    
  
  
    
  
  
  
  


================================================
FILE: GVFS/GVFS.VirtualFileSystemHook/GVFS.VirtualFileSystemHook.vcxproj.filters
================================================


  
    
      {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
      cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
    
    
      {93995380-89BD-4b04-88EB-625FBE52EBFB}
      h;hh;hpp;hxx;hm;inl;inc;xsd
    
    
      {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
      rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
    
    
      {dc184179-b81b-462c-bc71-f6735e699448}
    
    
      {19cb5377-2e7a-49d0-b976-a200efb400f4}
    
  
  
    
      Header Files
    
    
      Header Files
    
    
      Header Files
    
    
      Shared Header Files
    
  
  
    
      Source Files
    
    
      Source Files
    
    
      Shared Source Files
    
  
  
    
      Resource Files
    
  


================================================
FILE: GVFS/GVFS.VirtualFileSystemHook/main.cpp
================================================
#include "stdafx.h"
#include "common.h"

enum VirtualFileSystemErrorReturnCode
{
	ErrorVirtualFileSystemProtocol = ReturnCode::LastError + 1,
};

const int PIPE_BUFFER_SIZE = 1024;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        die(VirtualFileSystemErrorReturnCode::ErrorVirtualFileSystemProtocol, "Invalid arguments");
    }

    if (strcmp(argv[1], "1"))
    {
        die(VirtualFileSystemErrorReturnCode::ErrorVirtualFileSystemProtocol, "Bad version");
    }

    DisableCRLFTranslationOnStdPipes();

    PATH_STRING pipeName(GetGVFSPipeName(argv[0]));
    PIPE_HANDLE pipeHandle = CreatePipeToGVFS(pipeName);

    // Construct projection request message
    unsigned long bytesWritten;
    unsigned long messageLength = 6;
    int error = 0;
    bool success = WriteToPipe(
        pipeHandle,
        "MPL|1\x3",
        messageLength,
        &bytesWritten,
        &error);

    if (!success || bytesWritten != messageLength)
    {
        die(ReturnCode::PipeWriteFailed, "Failed to write to pipe (%d)\n", error);
    }

    // Allow for 1 extra character in case we need to
    // null terminate the message, and the message
    // is PIPE_BUFFER_SIZE chars long.
    char message[PIPE_BUFFER_SIZE + 1];
    unsigned long bytesRead;
    int lastError;
    bool finishedReading = false;
    bool firstRead = true;

    do
    {
        char *pMessage = &message[0];

        success = ReadFromPipe(
            pipeHandle,
            message,
            PIPE_BUFFER_SIZE,
            &bytesRead,
            &lastError);

        if (!success)
        {
            break;
        }

        messageLength = bytesRead;

        if (firstRead)
        {
            firstRead = false;
            if (message[0] != 'S')
            {
                message[bytesRead] = 0;
                die(ReturnCode::PipeReadFailed, "Read response from pipe failed (%s)\n", message);
            }

            pMessage += 2;
            messageLength -= 2;
        }

        if (*(pMessage + messageLength - 1) == '\x3')
        {
            finishedReading = true;
            messageLength -= 1;
        }

        fwrite(pMessage, 1, messageLength, stdout);

    } while (success && !finishedReading);

    if (!success)
    {
        die(ReturnCode::PipeReadFailed, "Read response from pipe failed (%d)\n", lastError);
    }

    return 0;
}



================================================
FILE: GVFS/GVFS.VirtualFileSystemHook/resource.h
================================================
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Version.rc

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        101
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1001
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif


================================================
FILE: GVFS/GVFS.VirtualFileSystemHook/stdafx.cpp
================================================
// stdafx.cpp : source file that includes just the standard includes
// GVFS.VirtualFileSystemHook.pch will be the pre-compiled header
// stdafx.obj will contain the pre-compiled type information

#include "stdafx.h"

// TODO: reference any additional headers you need in STDAFX.H
// and not in this file


================================================
FILE: GVFS/GVFS.VirtualFileSystemHook/stdafx.h
================================================
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//

#pragma once

#ifdef _WIN32
#include "targetver.h"
#include 
#endif

#include 
#include 



// TODO: reference additional headers your program requires here


================================================
FILE: GVFS/GVFS.VirtualFileSystemHook/targetver.h
================================================
#pragma once

// Including SDKDDKVer.h defines the highest available Windows platform.

// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.

#include 


================================================
FILE: GVFS/GVFS.Virtualization/Background/BackgroundFileSystemTaskRunner.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Tracing;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace GVFS.Virtualization.Background
{
    public class BackgroundFileSystemTaskRunner : IDisposable
    {
        private const int ActionRetryDelayMS = 50;
        private const int RetryFailuresLogThreshold = 200;
        private const int LogUpdateTaskThreshold = 25000;
        private static readonly string EtwArea = nameof(BackgroundFileSystemTaskRunner);

        private FileSystemTaskQueue backgroundTasks;
        private AutoResetEvent wakeUpThread;
        private Task backgroundThread;
        private bool isStopping;

        private GVFSContext context;

        // TODO 656051: Replace these callbacks with an interface
        private Func preCallback;
        private Func callback;
        private Func postCallback;

        public BackgroundFileSystemTaskRunner(
            GVFSContext context,
            Func preCallback,
            Func callback,
            Func postCallback,
            string databasePath)
        {
            this.context = context;
            this.preCallback = preCallback;
            this.callback = callback;
            this.postCallback = postCallback;

            string error;
            if (!FileSystemTaskQueue.TryCreate(
                this.context.Tracer,
                databasePath,
                new PhysicalFileSystem(),
                out this.backgroundTasks,
                out error))
            {
                string message = "Failed to create new background tasks folder: " + error;
                context.Tracer.RelatedError(message);
                throw new InvalidRepoException(message);
            }

            this.wakeUpThread = new AutoResetEvent(true);
        }

        // For Unit Testing
        protected BackgroundFileSystemTaskRunner()
        {
        }

        private enum AcquireGVFSLockResult
        {
            LockAcquired,
            ShuttingDown
        }

        public virtual bool IsEmpty
        {
            get { return this.backgroundTasks.IsEmpty; }
        }

        /// 
        /// Gets the count of tasks in the background queue
        /// 
        /// 
        /// This is an expensive call on .net core and you should avoid calling in performance critical paths.
        /// Use the IsEmpty property when checking if the queue has any items instead of Count.
        /// 
        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.IsEmpty)
            {
                this.wakeUpThread.Set();
            }
        }

        public virtual void Enqueue(FileSystemTask backgroundTask)
        {
            this.backgroundTasks.EnqueueAndFlush(backgroundTask);

            if (!this.isStopping)
            {
                this.wakeUpThread.Set();
            }
        }

        public virtual void Shutdown()
        {
            this.isStopping = true;
            this.wakeUpThread.Set();
            this.backgroundThread.Wait();
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.backgroundThread != null)
                {
                    this.backgroundThread.Dispose();
                    this.backgroundThread = null;
                }
                if (this.backgroundTasks != null)
                {
                    this.backgroundTasks.Dispose();
                    this.backgroundTasks = null;
                }
            }
        }

        private AcquireGVFSLockResult WaitToAcquireGVFSLock()
        {
            int attempts = 0;
            while (!this.context.Repository.GVFSLock.TryAcquireLockForGVFS())
            {
                if (this.isStopping)
                {
                    return AcquireGVFSLockResult.ShuttingDown;
                }

                ++attempts;
                if (attempts > RetryFailuresLogThreshold)
                {
                    this.context.Tracer.RelatedWarning($"{nameof(this.WaitToAcquireGVFSLock)}: {nameof(BackgroundFileSystemTaskRunner)} unable to acquire lock, retrying");
                    attempts = 0;
                }

                Thread.Sleep(ActionRetryDelayMS);
            }

            return AcquireGVFSLockResult.LockAcquired;
        }

        private void ProcessBackgroundTasks()
        {
            FileSystemTask backgroundTask;

            while (true)
            {
                AcquireGVFSLockResult acquireLockResult = AcquireGVFSLockResult.ShuttingDown;

                try
                {
                    this.wakeUpThread.WaitOne();

                    if (this.isStopping)
                    {
                        return;
                    }

                    acquireLockResult = this.WaitToAcquireGVFSLock();
                    switch (acquireLockResult)
                    {
                        case AcquireGVFSLockResult.LockAcquired:
                            break;
                        case AcquireGVFSLockResult.ShuttingDown:
                            return;
                        default:
                            this.LogErrorAndExit("Invalid " + nameof(AcquireGVFSLockResult) + " result");
                            return;
                    }

                    this.RunCallbackUntilSuccess(this.preCallback, "PreCallback");

                    int tasksProcessed = 0;
                    while (this.backgroundTasks.TryPeek(out backgroundTask))
                    {
                        if (tasksProcessed % LogUpdateTaskThreshold == 0 &&
                            (tasksProcessed >= LogUpdateTaskThreshold || this.backgroundTasks.Count >= LogUpdateTaskThreshold))
                        {
                            this.LogTaskProcessingStatus(tasksProcessed);
                        }

                        if (this.isStopping)
                        {
                            // If we are stopping, then ProjFS has already been shut down
                            // Some of the queued background tasks may require ProjFS, and so it is unsafe to
                            // proceed.  GVFS will resume any queued tasks next time it is mounted
                            return;
                        }

                        FileSystemTaskResult callbackResult = this.callback(backgroundTask);
                        switch (callbackResult)
                        {
                            case FileSystemTaskResult.Success:
                                this.backgroundTasks.DequeueAndFlush(backgroundTask);
                                ++tasksProcessed;
                                break;

                            case FileSystemTaskResult.RetryableError:
                                if (!this.isStopping)
                                {
                                    Thread.Sleep(ActionRetryDelayMS);
                                }

                                break;

                            case FileSystemTaskResult.FatalError:
                                this.LogErrorAndExit("Callback encountered fatal error, exiting process");
                                break;

                            default:
                                this.LogErrorAndExit("Invalid background operation result");
                                break;
                        }
                    }

                    if (tasksProcessed >= LogUpdateTaskThreshold)
                    {
                        EventMetadata metadata = new EventMetadata();
                        metadata.Add("Area", EtwArea);
                        metadata.Add("TasksProcessed", tasksProcessed);
                        metadata.Add(TracingConstants.MessageKey.InfoMessage, "Processing background tasks complete");
                        this.context.Tracer.RelatedEvent(EventLevel.Informational, "TaskProcessingStatus", metadata);
                    }

                    if (this.isStopping)
                    {
                        return;
                    }
                }
                catch (Exception e)
                {
                    this.LogErrorAndExit($"{nameof(this.ProcessBackgroundTasks)} caught unhandled exception, exiting process", e);
                }
                finally
                {
                    this.PerformPostTaskProcessing(acquireLockResult);
                }
            }
        }

        private void PerformPostTaskProcessing(AcquireGVFSLockResult acquireLockResult)
        {
            try
            {
                if (acquireLockResult == AcquireGVFSLockResult.LockAcquired)
                {
                    this.RunCallbackUntilSuccess(this.postCallback, "PostCallback");
                    if (this.backgroundTasks.IsEmpty)
                    {
                        this.context.Repository.GVFSLock.ReleaseLockHeldByGVFS();
                    }
                }
            }
            catch (Exception e)
            {
                this.LogErrorAndExit($"{nameof(this.ProcessBackgroundTasks)} caught unhandled exception in {nameof(this.PerformPostTaskProcessing)}, exiting process", e);
            }
        }

        private void RunCallbackUntilSuccess(Func callback, string errorHeader)
        {
            int attempts = 0;
            while (true)
            {
                FileSystemTaskResult callbackResult = callback();
                switch (callbackResult)
                {
                    case FileSystemTaskResult.Success:
                        return;

                    case FileSystemTaskResult.RetryableError:
                        if (this.isStopping)
                        {
                            return;
                        }

                        ++attempts;
                        if (attempts > RetryFailuresLogThreshold)
                        {
                            this.context.Tracer.RelatedWarning("RunCallbackUntilSuccess(" + errorHeader + "): callback failed, retrying");
                            attempts = 0;
                        }

                        Thread.Sleep(ActionRetryDelayMS);
                        break;

                    case FileSystemTaskResult.FatalError:
                        this.LogErrorAndExit(errorHeader + " encountered fatal error, exiting process");
                        return;

                    default:
                        this.LogErrorAndExit(errorHeader + " result could not be found");
                        return;
                }
            }
        }

        private void LogErrorAndExit(string message, Exception e = null)
        {
            EventMetadata metadata = new EventMetadata();
            metadata.Add("Area", EtwArea);
            if (e != null)
            {
                metadata.Add("Exception", e.ToString());
            }

            this.context.Tracer.RelatedError(metadata, message);
            Environment.Exit(1);
        }

        private void LogTaskProcessingStatus(int tasksProcessed)
        {
            EventMetadata metadata = new EventMetadata();
            metadata.Add("BackgroundOperations", EtwArea);
            metadata.Add("TasksProcessed", tasksProcessed);
            metadata.Add("TasksRemaining", this.backgroundTasks.Count);
            this.context.Tracer.RelatedEvent(EventLevel.Informational, "TaskProcessingStatus", metadata);
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Background/FileSystemTask.cs
================================================
using Newtonsoft.Json;

namespace GVFS.Virtualization.Background
{
    public struct FileSystemTask
    {
        public FileSystemTask(OperationType operation, string virtualPath, string oldVirtualPath)
        {
            this.Operation = operation;
            this.VirtualPath = virtualPath;
            this.OldVirtualPath = oldVirtualPath;
        }

        public enum OperationType
        {
            Invalid = 0,

            OnFileCreated,
            OnFileRenamed,
            OnFileDeleted,
            OnFileOverwritten,
            OnFileSuperseded,
            OnFileConvertedToFull,
            OnFailedPlaceholderDelete,
            OnFailedPlaceholderUpdate,
            OnFolderCreated,
            OnFolderRenamed,
            OnFolderDeleted,
            OnFolderFirstWrite,
            OnIndexWriteRequiringModifiedPathsValidation,
            OnPlaceholderCreationsBlockedForGit,
            OnFileHardLinkCreated,
            OnFilePreDelete,
            OnFolderPreDelete,
            OnFileSymLinkCreated,
            OnFailedFileHydration,
        }

        public OperationType Operation { get; }

        public string VirtualPath { get; }
        public string OldVirtualPath { get; }

        public static FileSystemTask OnFileCreated(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFileCreated, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFileRenamed(string oldVirtualPath, string newVirtualPath)
        {
            return new FileSystemTask(OperationType.OnFileRenamed, newVirtualPath, oldVirtualPath);
        }

        public static FileSystemTask OnFileHardLinkCreated(string newLinkRelativePath, string existingRelativePath)
        {
            return new FileSystemTask(OperationType.OnFileHardLinkCreated, newLinkRelativePath, oldVirtualPath: existingRelativePath);
        }

        public static FileSystemTask OnFileSymLinkCreated(string newLinkRelativePath)
        {
            return new FileSystemTask(OperationType.OnFileSymLinkCreated, newLinkRelativePath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFileDeleted(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFileDeleted, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFilePreDelete(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFilePreDelete, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFileOverwritten(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFileOverwritten, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFileSuperseded(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFileSuperseded, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFileConvertedToFull(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFileConvertedToFull, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFailedPlaceholderDelete(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFailedPlaceholderDelete, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFailedPlaceholderUpdate(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFailedPlaceholderUpdate, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFailedFileHydration(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFailedFileHydration, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFolderCreated(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFolderCreated, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFolderRenamed(string oldVirtualPath, string newVirtualPath)
        {
            return new FileSystemTask(OperationType.OnFolderRenamed, newVirtualPath, oldVirtualPath);
        }

        public static FileSystemTask OnFolderDeleted(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFolderDeleted, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnFolderPreDelete(string virtualPath)
        {
            return new FileSystemTask(OperationType.OnFolderPreDelete, virtualPath, oldVirtualPath: null);
        }

        public static FileSystemTask OnIndexWriteRequiringModifiedPathsValidation()
        {
            return new FileSystemTask(OperationType.OnIndexWriteRequiringModifiedPathsValidation, virtualPath: null, oldVirtualPath: null);
        }

        public static FileSystemTask OnPlaceholderCreationsBlockedForGit()
        {
            return new FileSystemTask(OperationType.OnPlaceholderCreationsBlockedForGit, virtualPath: null, oldVirtualPath: null);
        }

        public override string ToString()
        {
            return JsonConvert.SerializeObject(this);
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Background/FileSystemTaskQueue.cs
================================================
using GVFS.Common;
using GVFS.Common.FileSystem;
using GVFS.Common.Tracing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;

namespace GVFS.Virtualization.Background
{
    public class FileSystemTaskQueue : FileBasedCollection
    {
        private const string ValueTerminator = "\0";
        private const char ValueTerminatorChar = '\0';

        private readonly ConcurrentQueue> data = new ConcurrentQueue>();

        private long entryCounter = 0;

        private FileSystemTaskQueue(ITracer tracer, PhysicalFileSystem fileSystem, string dataFilePath)
            : base(tracer, fileSystem, dataFilePath, collectionAppendsDirectlyToFile: true)
        {
        }

        public bool IsEmpty
        {
            get { return this.data.IsEmpty; }
        }

        /// 
        /// Gets the count of tasks in the queue
        /// 
        /// 
        /// This is an expensive call on .net core and you should avoid calling in performance critical paths.
        /// Use the IsEmpty property when checking if the queue has any items instead of Count.
        /// 
        public int Count
        {
            get { return this.data.Count; }
        }

        public static bool TryCreate(ITracer tracer, string dataDirectory, PhysicalFileSystem fileSystem, out FileSystemTaskQueue output, out string error)
        {
            output = new FileSystemTaskQueue(tracer, fileSystem, dataDirectory);
            if (!output.TryLoadFromDisk(
                output.TryParseAddLine,
                output.TryParseRemoveLine,
                output.AddParsedEntry,
                out error))
            {
                output = null;
                return false;
            }

            return true;
        }

        public void EnqueueAndFlush(FileSystemTask value)
        {
            try
            {
                KeyValuePair kvp = new KeyValuePair(
                    Interlocked.Increment(ref this.entryCounter),
                    value);

                this.WriteAddEntry(
                    kvp.Key + ValueTerminator + this.Serialize(kvp.Value),
                    () => this.data.Enqueue(kvp));
            }
            catch (Exception e)
            {
                throw new FileBasedCollectionException(e);
            }
        }

        public void DequeueAndFlush(FileSystemTask expectedValue)
        {
            try
            {
                KeyValuePair kvp;
                if (this.data.TryDequeue(out kvp))
                {
                    if (!expectedValue.Equals(kvp.Value))
                    {
                        throw new InvalidOperationException(string.Format("Dequeued value is expected to be the same as input value. Expected: '{0}' Actual: '{1}'", expectedValue, kvp.Value));
                    }

                    this.WriteRemoveEntry(kvp.Key.ToString());

                    this.DeleteDataFileIfCondition(() => this.data.IsEmpty);
                }
                else
                {
                    throw new InvalidOperationException(string.Format("Dequeued value is expected to be the same as input value. Expected: '{0}' Actual: 'None. List is empty.'", expectedValue));
                }
            }
            catch (Exception e)
            {
                throw new FileBasedCollectionException(e);
            }
        }

        public bool TryPeek(out FileSystemTask value)
        {
            try
            {
                KeyValuePair kvp;
                if (this.data.TryPeek(out kvp))
                {
                    value = kvp.Value;
                    return true;
                }

                value = default(FileSystemTask);
                return false;
            }
            catch (Exception e)
            {
                throw new FileBasedCollectionException(e);
            }
        }

        private bool TryParseAddLine(string line, out long key, out FileSystemTask value, out string error)
        {
            // Expected: \0
            int idx = line.IndexOf(ValueTerminator, StringComparison.Ordinal);
            if (idx < 0)
            {
                key = 0;
                value = default(FileSystemTask);
                error = "Add line missing ID terminator: " + line;
                return false;
            }

            if (!long.TryParse(line.Substring(0, idx), out key))
            {
                value = default(FileSystemTask);
                error = "Could not parse ID for add line: " + line;
                return false;
            }

            if (!this.TryDeserialize(line.Substring(idx + 1), out value))
            {
                value = default(FileSystemTask);
                error = $"Could not parse {nameof(FileSystemTask)} for add line: " + line;
                return false;
            }

            error = null;
            return true;
        }

        private string Serialize(FileSystemTask input)
        {
            return ((int)input.Operation) + ValueTerminator + input.VirtualPath + ValueTerminator + input.OldVirtualPath;
        }

        private bool TryDeserialize(string line, out FileSystemTask value)
        {
            // Expected: \0\0
            string[] parts = line.Split(ValueTerminatorChar);
            if (parts.Length != 3)
            {
                value = default(FileSystemTask);
                return false;
            }

            FileSystemTask.OperationType operationType;
            if (!Enum.TryParse(parts[0], out operationType))
            {
                value = default(FileSystemTask);
                return false;
            }

            value = new FileSystemTask(
                operationType,
                parts[1],
                parts[2]);

            return true;
        }

        private bool TryParseRemoveLine(string line, out long key, out string error)
        {
            if (!long.TryParse(line, out key))
            {
                error = "Could not parse ID for remove line: " + line;
                return false;
            }

            error = null;
            return true;
        }

        private void AddParsedEntry(long key, FileSystemTask value)
        {
            this.data.Enqueue(new KeyValuePair(key, value));
            if (this.entryCounter < key + 1)
            {
                this.entryCounter = key;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Background/FileSystemTaskResult.cs
================================================
namespace GVFS.Virtualization.Background
{
    public enum FileSystemTaskResult
    {
        Invalid = 0,

        Success,
        RetryableError,
        FatalError
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/BlobSize/BlobSizes.cs
================================================
using GVFS.Common.Database;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace GVFS.Virtualization.BlobSize
{
    public class BlobSizes : IDisposable
    {
        public const string DatabaseName = "BlobSizes.sql";

        private const string EtwArea = nameof(BlobSizes);
        private const int SaveSizesRetryDelayMS = 50;

        private readonly string databasePath;
        private readonly string sqliteConnectionString;

        private ITracer tracer;
        private PhysicalFileSystem fileSystem;

        private Thread flushDataThread;
        private AutoResetEvent wakeUpFlushThread;
        private bool isStopping;
        private ConcurrentQueue queuedSizes;

        public BlobSizes(string blobSizesRoot, PhysicalFileSystem fileSystem, ITracer tracer)
        {
            this.databasePath = Path.Combine(blobSizesRoot, DatabaseName);
            this.fileSystem = fileSystem;
            this.tracer = tracer;
            this.wakeUpFlushThread = new AutoResetEvent(false);
            this.queuedSizes = new ConcurrentQueue();
            this.sqliteConnectionString = SqliteDatabase.CreateConnectionString(this.databasePath);
        }

        /// 
        /// Create a connection to BlobSizes that can be used for saving and retrieving blob sizes
        /// 
        /// BlobSizesConnection
        /// BlobSizesConnection are thread-specific
        public virtual BlobSizesConnection CreateConnection()
        {
            return new BlobSizesConnection(this, this.sqliteConnectionString);
        }

        public virtual void Initialize()
        {
            string folderPath = Path.GetDirectoryName(this.databasePath);
            this.fileSystem.CreateDirectory(folderPath);

            using (SqliteConnection connection = new SqliteConnection(this.sqliteConnectionString))
            {
                connection.Open();

                using (SqliteCommand pragmaWalCommand = connection.CreateCommand())
                {
                    // Advantages of using WAL ("Write-Ahead Log")
                    // 1. WAL is significantly faster in most scenarios.
                    // 2. WAL provides more concurrency as readers do not block writers and a writer does not block readers.
                    //    Reading and writing can proceed concurrently.
                    // 3. Disk I/O operations tends to be more sequential using WAL.
                    // 4. WAL uses many fewer fsync() operations and is thus less vulnerable to problems on systems
                    //    where the fsync() system call is broken.
                    // http://www.sqlite.org/wal.html
                    pragmaWalCommand.CommandText = $"PRAGMA journal_mode=WAL;";
                    pragmaWalCommand.ExecuteNonQuery();
                }

                using (SqliteCommand pragmaCacheSizeCommand = connection.CreateCommand())
                {
                    // If the argument N is negative, then the number of cache pages is adjusted to use approximately abs(N*1024) bytes of memory
                    // -40000 => 40,000 * 1024 bytes => ~39MB
                    pragmaCacheSizeCommand.CommandText = $"PRAGMA cache_size=-40000;";
                    pragmaCacheSizeCommand.ExecuteNonQuery();
                }

                EventMetadata databaseMetadata = this.CreateEventMetadata();

                using (SqliteCommand userVersionCommand = connection.CreateCommand())
                {
                    // The user_version pragma will to get or set the value of the user-version integer at offset 60 in the database header.
                    // The user-version is an integer that is available to applications to use however they want. SQLite makes no use of the user-version itself.
                    // https://sqlite.org/pragma.html#pragma_user_version
                    userVersionCommand.CommandText = $"PRAGMA user_version;";

                    object userVersion = userVersionCommand.ExecuteScalar();

                    if (userVersion == null || Convert.ToInt64(userVersion) < 1)
                    {
                        userVersionCommand.CommandText = $"PRAGMA user_version=1;";
                        userVersionCommand.ExecuteNonQuery();
                        this.tracer.RelatedInfo($"{nameof(BlobSize)}.{nameof(this.Initialize)}: setting user_version to 1");
                    }
                    else
                    {
                        databaseMetadata.Add("user_version", Convert.ToInt64(userVersion));
                    }
                }

                using (SqliteCommand pragmaSynchronousCommand = connection.CreateCommand())
                {
                    // GVFS uses the default value (FULL) to reduce the risks of corruption
                    // http://www.sqlite.org/pragma.html#pragma_synchronous
                    // (Note: This call is to retrieve the value of 'synchronous' and log it)
                    pragmaSynchronousCommand.CommandText = $"PRAGMA synchronous;";
                    object synchronous = pragmaSynchronousCommand.ExecuteScalar();
                    if (synchronous != null)
                    {
                        databaseMetadata.Add("synchronous", Convert.ToInt64(synchronous));
                    }
                }

                this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(BlobSize)}_{nameof(this.Initialize)}_db_settings", databaseMetadata);

                using (SqliteCommand createTableCommand = connection.CreateCommand())
                {
                    // Use a BLOB for sha rather than a string to reduce the size of the database
                    createTableCommand.CommandText = @"CREATE TABLE IF NOT EXISTS [BlobSizes] (sha BLOB, size INT, PRIMARY KEY (sha));";
                    createTableCommand.ExecuteNonQuery();
                }
            }

            this.flushDataThread = new Thread(this.FlushDbThreadMain);
            this.flushDataThread.IsBackground = true;
            this.flushDataThread.Start();
        }

        public virtual void Shutdown()
        {
            this.isStopping = true;
            this.wakeUpFlushThread.Set();
            this.flushDataThread.Join();
        }

        public virtual void AddSize(Sha1Id sha, long size)
        {
            this.queuedSizes.Enqueue(new BlobSize(sha, size));
        }

        public virtual void Flush()
        {
            this.wakeUpFlushThread.Set();
        }

        public void Dispose()
        {
            if (this.wakeUpFlushThread != null)
            {
                this.wakeUpFlushThread.Dispose();
                this.wakeUpFlushThread = null;
            }
        }

        private void FlushDbThreadMain()
        {
            try
            {
                int errorCode;
                string error;
                ulong failCount;

                using (BlobSizesDatabaseWriter sizeWriter = new BlobSizesDatabaseWriter(this.sqliteConnectionString))
                {
                    sizeWriter.Initialize();

                    while (true)
                    {
                        this.wakeUpFlushThread.WaitOne();

                        failCount = 0;

                        while (!sizeWriter.TryAddSizes(this.queuedSizes, out errorCode, out error) && !this.isStopping)
                        {
                            ++failCount;
                            if (failCount % 200UL == 1)
                            {
                                EventMetadata metadata = this.CreateEventMetadata();
                                metadata.Add(nameof(errorCode), errorCode);
                                metadata.Add(nameof(error), error);
                                metadata.Add(nameof(failCount), failCount);
                                this.tracer.RelatedWarning(metadata, $"{nameof(this.flushDataThread)}: {nameof(BlobSizesDatabaseWriter.TryAddSizes)} failed");
                            }

                            Thread.Sleep(SaveSizesRetryDelayMS);
                        }

                        if (this.isStopping)
                        {
                            return;
                        }
                        else if (failCount > 1)
                        {
                            EventMetadata metadata = this.CreateEventMetadata();
                            metadata.Add(nameof(failCount), failCount);
                            this.tracer.RelatedEvent(
                                EventLevel.Informational,
                                $"{nameof(this.FlushDbThreadMain)}_{nameof(BlobSizesDatabaseWriter.TryAddSizes)}_SucceededAfterFailing",
                                metadata);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                this.LogErrorAndExit("FlushDbThreadMain caught unhandled exception, exiting process", e);
            }
        }

        private void LogErrorAndExit(string message, Exception e = null)
        {
            EventMetadata metadata = this.CreateEventMetadata(e);
            this.tracer.RelatedError(metadata, message);
            Environment.Exit(1);
        }

        private EventMetadata CreateEventMetadata(Exception e = null)
        {
            EventMetadata metadata = new EventMetadata();
            metadata.Add("Area", EtwArea);
            if (e != null)
            {
                metadata.Add("Exception", e.ToString());
            }

            return metadata;
        }

        public class BlobSizesConnection : IDisposable
        {
            private string connectionString;

            // Keep connection and command alive for the duration of BlobSizesConnection so that
            // the prepared SQLite statement can be reused
            private SqliteConnection connection;
            private SqliteCommand querySizeCommand;
            private SqliteParameter shaParam;

            private byte[] shaBuffer;

            public BlobSizesConnection(BlobSizes blobSizes)
            {
                // For unit testing
                this.BlobSizesDatabase = blobSizes;
            }

            public BlobSizesConnection(BlobSizes blobSizes, string connectionString)
            {
                this.BlobSizesDatabase = blobSizes;
                this.connectionString = connectionString;
                this.shaBuffer = new byte[20];

                try
                {
                    this.connection = new SqliteConnection(this.connectionString);
                    this.connection.Open();

                    using (SqliteCommand pragmaReadUncommittedCommand = this.connection.CreateCommand())
                    {
                        // A database connection in read-uncommitted mode does not attempt to obtain read-locks
                        // before reading from database tables as described above. This can lead to inconsistent
                        // query results if another database connection modifies a table while it is being read,
                        // but it also means that a read-transaction opened by a connection in read-uncommitted
                        // mode can neither block nor be blocked by any other connection
                        // http://www.sqlite.org/pragma.html#pragma_read_uncommitted
                        pragmaReadUncommittedCommand.CommandText = $"PRAGMA read_uncommitted=1;";
                        pragmaReadUncommittedCommand.ExecuteNonQuery();
                    }

                    this.querySizeCommand = this.connection.CreateCommand();

                    this.shaParam = this.querySizeCommand.CreateParameter();
                    this.shaParam.ParameterName = "@sha";

                    this.querySizeCommand.CommandText = "SELECT size FROM BlobSizes WHERE sha = (@sha);";
                    this.querySizeCommand.Parameters.Add(this.shaParam);
                    this.querySizeCommand.Prepare();
                }
                catch (Exception e)
                {
                    if (this.querySizeCommand != null)
                    {
                        this.querySizeCommand.Dispose();
                        this.querySizeCommand = null;
                    }

                    if (this.connection != null)
                    {
                        this.connection.Dispose();
                        this.connection = null;
                    }

                    throw new BlobSizesException(e);
                }
            }

            public BlobSizes BlobSizesDatabase { get; }

            public virtual bool TryGetSize(Sha1Id sha, out long length)
            {
                try
                {
                    length = -1;

                    sha.ToBuffer(this.shaBuffer);
                    this.shaParam.Value = this.shaBuffer;

                    using (SqliteDataReader reader = this.querySizeCommand.ExecuteReader())
                    {
                        if (reader.Read())
                        {
                            length = reader.GetInt64(0);
                            return true;
                        }
                    }
                }
                catch (Exception e)
                {
                    throw new BlobSizesException(e);
                }

                return false;
            }

            public void Dispose()
            {
                if (this.querySizeCommand != null)
                {
                    this.querySizeCommand.Dispose();
                    this.querySizeCommand = null;
                }

                if (this.connection != null)
                {
                    this.connection.Dispose();
                    this.connection = null;
                }
            }
        }

        private class BlobSize
        {
            public BlobSize(Sha1Id sha, long size)
            {
                this.Sha = sha;
                this.Size = size;
            }

            public Sha1Id Sha { get; }
            public long Size { get; }
        }

        private class BlobSizesDatabaseWriter : IDisposable
        {
            private string connectionString;

            private SqliteConnection connection;
            private SqliteCommand addCommand;
            private SqliteParameter shaParam;
            private SqliteParameter sizeParam;

            private byte[] shaBuffer;

            public BlobSizesDatabaseWriter(string connectionString)
            {
                this.connectionString = connectionString;
                this.shaBuffer = new byte[20];
            }

            public void Initialize()
            {
                this.connection = new SqliteConnection(this.connectionString);
                this.connection.Open();

                this.addCommand = this.connection.CreateCommand();

                this.shaParam = this.addCommand.CreateParameter();
                this.shaParam.ParameterName = "@sha";

                this.sizeParam = this.addCommand.CreateParameter();
                this.sizeParam.ParameterName = "@size";

                this.addCommand.CommandText = $"INSERT OR IGNORE INTO BlobSizes (sha, size) VALUES (@sha, @size);";
                this.addCommand.Parameters.Add(this.shaParam);
                this.addCommand.Parameters.Add(this.sizeParam);

                this.addCommand.Prepare();
            }

            public bool TryAddSizes(ConcurrentQueue sizes, out int errorCode, out string error)
            {
                errorCode = 0;
                error = null;

                try
                {
                    using (SqliteTransaction insertTransaction = this.connection.BeginTransaction())
                    {
                        this.addCommand.Transaction = insertTransaction;

                        BlobSize blobSize;
                        while (sizes.TryDequeue(out blobSize))
                        {
                            blobSize.Sha.ToBuffer(this.shaBuffer);
                            this.shaParam.Value = this.shaBuffer;
                            this.sizeParam.Value = blobSize.Size;
                            this.addCommand.ExecuteNonQuery();
                        }

                        insertTransaction.Commit();
                    }
                }
                catch (SqliteException e)
                {
                    errorCode = e.SqliteErrorCode;
                    error = e.Message;
                    return false;
                }

                return true;
            }

            public void Dispose()
            {
                if (this.addCommand != null)
                {
                    this.addCommand.Dispose();
                    this.connection = null;
                }

                if (this.connection != null)
                {
                    this.connection.Dispose();
                    this.connection = null;
                }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/BlobSize/BlobSizesException.cs
================================================
using System;

namespace GVFS.Virtualization.BlobSize
{
    public class BlobSizesException : Exception
    {
        public BlobSizesException(Exception innerException)
            : base(innerException.Message, innerException)
        {
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/FileSystem/FSResult.cs
================================================
namespace GVFS.Virtualization.FileSystem
{
    public enum FSResult
    {
        Invalid = 0,
        Ok,
        IOError,
        DirectoryNotEmpty,
        FileOrPathNotFound,
        IoReparseTagNotHandled,
        VirtualizationInvalidOperation,
        GenericProjFSError,
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/FileSystem/FileSystemResult.cs
================================================
namespace GVFS.Virtualization.FileSystem
{
    public struct FileSystemResult
    {
        public FileSystemResult(FSResult result, int rawResult)
        {
            this.Result = result;
            this.RawResult = rawResult;
        }

        public FSResult Result { get; }

        /// 
        /// Underlying result. The value of RawResult varies based on the operating system.
        /// 
        public int RawResult { get; }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.Virtualization.BlobSize;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Text;
using System.Threading;

namespace GVFS.Virtualization.FileSystem
{
    public abstract class FileSystemVirtualizer : IDisposable
    {
        public const byte PlaceholderVersion = 1;

        protected static readonly byte[] FolderContentId = Encoding.Unicode.GetBytes(GVFSConstants.AllZeroSha);

        protected static readonly GitCommandLineParser.Verbs CanCreatePlaceholderVerbs =
            GitCommandLineParser.Verbs.AddOrStage | GitCommandLineParser.Verbs.Move | GitCommandLineParser.Verbs.Status;

        private BlockingCollection fileAndNetworkRequests;
        private Thread[] fileAndNetworkWorkerThreads;
        private int numWorkerThreads;

        protected FileSystemVirtualizer(GVFSContext context, GVFSGitObjects gvfsGitObjects)
            : this(context, gvfsGitObjects, FileSystemVirtualizer.DefaultNumWorkerThreads)
        {
        }

        protected FileSystemVirtualizer(GVFSContext context, GVFSGitObjects gvfsGitObjects, int numWorkerThreads)
        {
            if (numWorkerThreads <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(numWorkerThreads), numWorkerThreads, "Number of threads must be greater than 0");
            }

            this.Context = context;
            this.GitObjects = gvfsGitObjects;
            this.fileAndNetworkRequests = new BlockingCollection();

            this.numWorkerThreads = numWorkerThreads;
        }

        protected static int DefaultNumWorkerThreads
        {
            get
            {
                return Environment.ProcessorCount;
            }
        }

        protected GVFSContext Context { get; private set; }
        protected GVFSGitObjects GitObjects { get; private set; }
        protected FileSystemCallbacks FileSystemCallbacks { get; private set; }
        protected virtual string EtwArea
        {
            get
            {
                return nameof(FileSystemVirtualizer);
            }
        }

        public static byte[] ConvertShaToContentId(string sha)
        {
            return Encoding.Unicode.GetBytes(sha);
        }

        public void Initialize(FileSystemCallbacks fileSystemCallbacks)
        {
            this.FileSystemCallbacks = fileSystemCallbacks;

            this.fileAndNetworkWorkerThreads = new Thread[this.numWorkerThreads];
            for (int i = 0; i < this.fileAndNetworkWorkerThreads.Length; ++i)
            {
                this.fileAndNetworkWorkerThreads[i] = new Thread(this.ExecuteFileOrNetworkRequest);
                this.fileAndNetworkWorkerThreads[i].IsBackground = true;
                this.fileAndNetworkWorkerThreads[i].Start();
            }
        }

        public void PrepareToStop()
        {
            this.fileAndNetworkRequests.CompleteAdding();
            foreach (Thread t in this.fileAndNetworkWorkerThreads)
            {
                t.Join();
            }
        }

        public abstract void Stop();

        public abstract FileSystemResult ClearNegativePathCache(out uint totalEntryCount);

        public abstract FileSystemResult DeleteFile(string relativePath, UpdatePlaceholderType updateFlags, out UpdateFailureReason failureReason);

        public abstract FileSystemResult WritePlaceholderFile(string relativePath, long endOfFile, string sha);
        public abstract FileSystemResult WritePlaceholderDirectory(string relativePath);

        public abstract FileSystemResult UpdatePlaceholderIfNeeded(
            string relativePath,
            DateTime creationTime,
            DateTime lastAccessTime,
            DateTime lastWriteTime,
            DateTime changeTime,
            FileAttributes fileAttributes,
            long endOfFile,
            string shaContentId,
            UpdatePlaceholderType updateFlags,
            out UpdateFailureReason failureReason);

        public abstract FileSystemResult DehydrateFolder(string relativePath);

        public void Dispose()
        {
            if (this.fileAndNetworkRequests != null)
            {
                this.fileAndNetworkRequests.Dispose();
                this.fileAndNetworkRequests = null;
            }
        }

        public abstract bool TryStart(out string error);

        protected static string GetShaFromContentId(byte[] contentId)
        {
            return Encoding.Unicode.GetString(contentId, 0, GVFSConstants.ShaStringLength * sizeof(char));
        }

        protected static byte GetPlaceholderVersionFromProviderId(byte[] providerId)
        {
            return providerId[0];
        }

        /// 
        /// If a git-status or git-add is running, we don't want to fail placeholder creation because users will
        /// want to be able to run those commands during long running builds. Allow lock acquisition to be deferred
        /// until background thread actually needs it.
        ///
        /// git-mv is also allowed to defer since it needs to create the files it moves.
        /// 
        protected bool CanCreatePlaceholder()
        {
            GitCommandLineParser gitCommand = new GitCommandLineParser(this.Context.Repository.GVFSLock.GetLockedGitCommand());
            return
                !gitCommand.IsValidGitCommand ||
                gitCommand.IsVerb(FileSystemVirtualizer.CanCreatePlaceholderVerbs);
        }

        protected bool IsSpecialGitFile(string fileName)
        {
            return
                fileName.Equals(GVFSConstants.SpecialGitFiles.GitAttributes, GVFSPlatform.Instance.Constants.PathComparison) ||
                fileName.Equals(GVFSConstants.SpecialGitFiles.GitIgnore, GVFSPlatform.Instance.Constants.PathComparison);
        }

        protected void OnDotGitFileOrFolderChanged(string relativePath)
        {
            if (relativePath.Equals(GVFSConstants.DotGit.Index, GVFSPlatform.Instance.Constants.PathComparison))
            {
                this.FileSystemCallbacks.OnIndexFileChange();
            }
            else if (relativePath.Equals(GVFSConstants.DotGit.Logs.Head, GVFSPlatform.Instance.Constants.PathComparison))
            {
                this.FileSystemCallbacks.OnLogsHeadChange();
            }
            else if (IsPathHeadOrLocalBranch(relativePath))
            {
                this.FileSystemCallbacks.OnHeadOrRefChanged();
            }
            else if (relativePath.Equals(GVFSConstants.DotGit.Info.ExcludePath, GVFSPlatform.Instance.Constants.PathComparison))
            {
                this.FileSystemCallbacks.OnExcludeFileChanged();
            }
        }

        protected void OnDotGitFileOrFolderDeleted(string relativePath)
        {
            if (IsPathHeadOrLocalBranch(relativePath))
            {
                this.FileSystemCallbacks.OnHeadOrRefChanged();
            }
            else if (relativePath.Equals(GVFSConstants.DotGit.Info.ExcludePath, GVFSPlatform.Instance.Constants.PathComparison))
            {
                this.FileSystemCallbacks.OnExcludeFileChanged();
            }
        }

        protected void OnWorkingDirectoryFileOrFolderDeleteNotification(string relativePath, bool isDirectory, bool isPreDelete)
        {
            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)
                {
                    if (isPreDelete)
                    {
                        this.FileSystemCallbacks.OnFolderPreDelete(relativePath);
                    }
                    else
                    {
                        this.FileSystemCallbacks.OnFolderDeleted(relativePath);
                    }
                }
                else
                {
                    // During a git command if it deletes a folder we need to track that as a tombstone
                    // So that we can delete them if the projection changes. This will be a no-op on platforms
                    // that don't override OnPossibleTombstoneFolderCreated
                    this.OnPossibleTombstoneFolderCreated(relativePath);
                }
            }
            else
            {
                if (isPreDelete)
                {
                    this.FileSystemCallbacks.OnFilePreDelete(relativePath);
                }
                else
                {
                    this.FileSystemCallbacks.OnFileDeleted(relativePath);
                }
            }

            this.FileSystemCallbacks.InvalidateGitStatusCache();
        }

        // This method defaults to a no-op and is overridden in the platform specific
        // FileSystemVirtualizer derived classes that support tombstones
        protected virtual void OnPossibleTombstoneFolderCreated(string relativePath)
        {
        }

        protected void OnFileRenamed(string relativeSourcePath, string relativeDestinationPath, bool isDirectory)
        {
            try
            {
                bool srcPathInDotGit = FileSystemCallbacks.IsPathInsideDotGit(relativeSourcePath);
                bool dstPathInDotGit = FileSystemCallbacks.IsPathInsideDotGit(relativeDestinationPath);

                if (dstPathInDotGit)
                {
                    this.OnDotGitFileOrFolderChanged(relativeDestinationPath);
                }

                if (!(srcPathInDotGit && dstPathInDotGit))
                {
                    if (isDirectory)
                    {
                        this.FileSystemCallbacks.OnFolderRenamed(relativeSourcePath, relativeDestinationPath);
                    }
                    else
                    {
                        this.FileSystemCallbacks.OnFileRenamed(relativeSourcePath, relativeDestinationPath);
                    }
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(relativeSourcePath, e);
                metadata.Add("destinationPath", relativeDestinationPath);
                metadata.Add("isDirectory", isDirectory);
                this.LogUnhandledExceptionAndExit(nameof(this.OnFileRenamed), metadata);
            }
        }

        protected void OnHardLinkCreated(string relativeExistingFilePath, string relativeNewLinkPath)
        {
            try
            {
                bool newLinkPathInDotGit = FileSystemCallbacks.IsPathInsideDotGit(relativeNewLinkPath);
                bool existingFilePathInDotGit = FileSystemCallbacks.IsPathInsideDotGit(relativeExistingFilePath);

                if (newLinkPathInDotGit)
                {
                    this.OnDotGitFileOrFolderChanged(relativeNewLinkPath);
                }

                if (!(newLinkPathInDotGit && existingFilePathInDotGit))
                {
                     this.FileSystemCallbacks.OnFileHardLinkCreated(relativeNewLinkPath, relativeExistingFilePath);
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(relativeNewLinkPath, e);
                metadata.Add(nameof(relativeExistingFilePath), relativeExistingFilePath);
                this.LogUnhandledExceptionAndExit(nameof(this.OnHardLinkCreated), metadata);
            }
        }

        protected void OnFilePreConvertToFull(string relativePath)
        {
            try
            {
                bool isFolder;
                string fileName;
                bool isPathProjected = this.FileSystemCallbacks.GitIndexProjection.IsPathProjected(relativePath, out fileName, out isFolder);
                if (isPathProjected)
                {
                    this.FileSystemCallbacks.OnFileConvertedToFull(relativePath);
                }
            }
            catch (Exception e)
            {
                this.LogUnhandledExceptionAndExit(nameof(this.OnFilePreConvertToFull), this.CreateEventMetadata(relativePath, e));
            }
        }

        protected EventMetadata CreateEventMetadata(
            Guid enumerationId,
            string relativePath = null,
            Exception exception = null)
        {
            EventMetadata metadata = this.CreateEventMetadata(relativePath, exception);
            metadata.Add("enumerationId", enumerationId);
            return metadata;
        }

        protected EventMetadata CreateEventMetadata(
            string relativePath = null,
            Exception exception = null)
        {
            EventMetadata metadata = new EventMetadata();
            metadata.Add("Area", this.EtwArea);

            if (relativePath != null)
            {
                metadata.Add(nameof(relativePath), relativePath);
            }

            if (exception != null)
            {
                metadata.Add("Exception", exception.ToString());
            }

            return metadata;
        }

        protected bool TryScheduleFileOrNetworkRequest(FileOrNetworkRequest request, out Exception exception)
        {
            exception = null;

            try
            {
                this.fileAndNetworkRequests.Add(request);
                return true;
            }
            catch (InvalidOperationException e)
            {
                // Attempted to call Add after CompleteAdding has been called
                exception = e;
            }

            return false;
        }

        protected void LogUnhandledExceptionAndExit(string methodName, EventMetadata metadata)
        {
            this.Context.Tracer.RelatedError(metadata, methodName + " caught unhandled exception, exiting process");
            Environment.Exit(1);
        }

        private static bool IsPathHeadOrLocalBranch(string relativePath)
        {
            if (!relativePath.EndsWith(GVFSConstants.DotGit.LockExtension, GVFSPlatform.Instance.Constants.PathComparison) &&
                (relativePath.Equals(GVFSConstants.DotGit.Head, GVFSPlatform.Instance.Constants.PathComparison) ||
                relativePath.StartsWith(GVFSConstants.DotGit.Refs.Heads.RootFolder, GVFSPlatform.Instance.Constants.PathComparison)))
            {
                return true;
            }

            return false;
        }

        private void ExecuteFileOrNetworkRequest()
        {
            try
            {
                using (BlobSizes.BlobSizesConnection blobSizesConnection = this.FileSystemCallbacks.BlobSizes.CreateConnection())
                {
                    FileOrNetworkRequest request;
                    while (this.fileAndNetworkRequests.TryTake(out request, Timeout.Infinite))
                    {
                        try
                        {
                            request.Work(blobSizesConnection);
                        }
                        catch (Exception e)
                        {
                            EventMetadata metadata = this.CreateEventMetadata(relativePath: null, exception: e);
                            this.LogUnhandledExceptionAndExit($"{nameof(this.ExecuteFileOrNetworkRequest)}_Work", metadata);
                        }

                        try
                        {
                            request.Cleanup();
                        }
                        catch (Exception e)
                        {
                            EventMetadata metadata = this.CreateEventMetadata(relativePath: null, exception: e);
                            this.LogUnhandledExceptionAndExit($"{nameof(this.ExecuteFileOrNetworkRequest)}_Cleanup", metadata);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                EventMetadata metadata = this.CreateEventMetadata(relativePath: null, exception: e);
                this.LogUnhandledExceptionAndExit($"{nameof(this.ExecuteFileOrNetworkRequest)}", metadata);
            }
        }

        /// 
        /// Requests from the file system that require file and\or network access (and hence
        /// should be executed asynchronously).
        /// 
        protected class FileOrNetworkRequest
        {
            /// 
            /// FileOrNetworkRequest constructor
            /// 
            /// Action that requires file and\or network access
            /// Cleanup action to take after performing work
            public FileOrNetworkRequest(Action work, Action cleanup)
            {
                this.Work = work;
                this.Cleanup = cleanup;
            }

            public Action Work { get; }
            public Action Cleanup { get; }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/FileSystem/UpdateFailureReason.cs
================================================
using System;

namespace GVFS.Virtualization.FileSystem
{
    [Flags]
    public enum UpdateFailureReason : uint
    {
        // These values are identical to ProjFS.UpdateFailureCause to allow for easier casting
        NoFailure = 0,
        DirtyMetadata = 1,
        DirtyData = 2,
        Tombstone = 4,
        ReadOnly = 8
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/FileSystem/UpdatePlaceholderType.cs
================================================
using System;

namespace GVFS.Virtualization.FileSystem
{
    [Flags]
    public enum UpdatePlaceholderType : uint
    {
        // These values are identical to ProjFS.UpdateType to allow for easier casting
        AllowDirtyMetadata = 1,
        AllowDirtyData = 2,
        AllowTombstone = 4,
        AllowReadOnly = 32
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/FileSystemCallbacks.cs
================================================
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Virtualization.Background;
using GVFS.Virtualization.BlobSize;
using GVFS.Virtualization.FileSystem;
using GVFS.Virtualization.Projection;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;

namespace GVFS.Virtualization
{
    public class FileSystemCallbacks : IDisposable, IHeartBeatMetadataProvider
    {
        private const string EtwArea = nameof(FileSystemCallbacks);
        private const int NumberOfRetriesCheckingForDeleted = 10;
        private const int MillisecondsToSleepBeforeCheckingForDeleted = 1;

        private static readonly GitCommandLineParser.Verbs LeavesProjectionUnchangedVerbs =
            GitCommandLineParser.Verbs.AddOrStage |
            GitCommandLineParser.Verbs.Commit |
            GitCommandLineParser.Verbs.Status |
            GitCommandLineParser.Verbs.UpdateIndex;

        private readonly string logsHeadPath;

        private GVFSContext context;
        private IPlaceholderCollection placeholderDatabase;
        private ModifiedPathsDatabase modifiedPaths;
        private ConcurrentHashSet newlyCreatedFileAndFolderPaths;
        private ConcurrentDictionary filePlaceHolderCreationCount;
        private ConcurrentDictionary folderPlaceHolderCreationCount;
        private ConcurrentDictionary fileHydrationCount;
        private BackgroundFileSystemTaskRunner backgroundFileSystemTaskRunner;
        private FileSystemVirtualizer fileSystemVirtualizer;
        private FileProperties logsHeadFileProperties;

        private GitStatusCache gitStatusCache;
        private bool enableGitStatusCache;

        public FileSystemCallbacks(
            GVFSContext context,
            GVFSGitObjects gitObjects,
            RepoMetadata repoMetadata,
            BlobSizes blobSizes,
            GitIndexProjection gitIndexProjection,
            BackgroundFileSystemTaskRunner backgroundFileSystemTaskRunner,
            FileSystemVirtualizer fileSystemVirtualizer,
            IPlaceholderCollection placeholderDatabase,
            ISparseCollection sparseCollection,
            GitStatusCache gitStatusCache = null)
        {
            this.logsHeadFileProperties = null;

            this.context = context;
            this.fileSystemVirtualizer = fileSystemVirtualizer;

            this.filePlaceHolderCreationCount = new ConcurrentDictionary(GVFSPlatform.Instance.Constants.PathComparer);
            this.folderPlaceHolderCreationCount = new ConcurrentDictionary(GVFSPlatform.Instance.Constants.PathComparer);
            this.fileHydrationCount = new ConcurrentDictionary(GVFSPlatform.Instance.Constants.PathComparer);
            this.newlyCreatedFileAndFolderPaths = new ConcurrentHashSet(GVFSPlatform.Instance.Constants.PathComparer);

            string error;
            if (!ModifiedPathsDatabase.TryLoadOrCreate(
                this.context.Tracer,
                Path.Combine(this.context.Enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.ModifiedPaths),
                this.context.FileSystem,
                out this.modifiedPaths,
                out error))
            {
                throw new InvalidRepoException(error);
            }

            this.BlobSizes = blobSizes ?? new BlobSizes(context.Enlistment.BlobSizesRoot, context.FileSystem, context.Tracer);
            this.BlobSizes.Initialize();

            this.placeholderDatabase = placeholderDatabase;
            this.GitIndexProjection = gitIndexProjection ?? new GitIndexProjection(
                context,
                gitObjects,
                this.BlobSizes,
                repoMetadata,
                fileSystemVirtualizer,
                this.placeholderDatabase,
                sparseCollection,
                this.modifiedPaths);

            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.gitStatusCache.SetProjectedFolderCountProvider(
                () => this.GitIndexProjection.GetProjectedFolderCount());

            this.logsHeadPath = Path.Combine(this.context.Enlistment.DotGitRoot, GVFSConstants.DotGit.Logs.HeadRelativePath);

            EventMetadata metadata = new EventMetadata();
            metadata.Add("placeholders.Count", this.placeholderDatabase.GetCount());
            metadata.Add("background.Count", this.backgroundFileSystemTaskRunner.Count);
            metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(FileSystemCallbacks)} created");
            this.context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(FileSystemCallbacks)}_Constructor", metadata);
        }

        public IProfilerOnlyIndexProjection GitIndexProjectionProfiler
        {
            get { return this.GitIndexProjection; }
        }

        /// 
        /// Gets the count of tasks in the background operation queue
        /// 
        /// 
        /// This is an expensive call on .net core and you should avoid calling
        /// in performance critical paths.
        /// 
        public int BackgroundOperationCount
        {
            get { return this.backgroundFileSystemTaskRunner.Count; }
        }

        public BlobSizes BlobSizes { get; private set; }

        public GitIndexProjection GitIndexProjection { get; private set; }

        /// 
        /// Returns true for paths that begin with ".git\" (regardless of case)
        /// 
        public static bool IsPathInsideDotGit(string relativePath)
        {
            return relativePath.StartsWith(GVFSConstants.DotGit.Root + Path.DirectorySeparatorChar, GVFSPlatform.Instance.Constants.PathComparison);
        }

        public bool TryStart(out string error)
        {
            this.fileSystemVirtualizer.Initialize(this);
            this.modifiedPaths.RemoveEntriesWithParentFolderEntry(this.context.Tracer);
            this.modifiedPaths.WriteAllEntriesAndFlush();

            this.GitIndexProjection.Initialize(this.backgroundFileSystemTaskRunner);

            if (this.enableGitStatusCache)
            {
                this.gitStatusCache.Initialize();
            }

            this.backgroundFileSystemTaskRunner.Start();

            if (!this.fileSystemVirtualizer.TryStart(out error))
            {
                return false;
            }

            return true;
        }

        public void Stop()
        {
            // Shutdown the GitStatusCache before other
            // components that it depends on.
            this.gitStatusCache.Shutdown();

            this.fileSystemVirtualizer.PrepareToStop();
            this.backgroundFileSystemTaskRunner.Shutdown();
            this.GitIndexProjection.Shutdown();
            this.BlobSizes.Shutdown();
            this.fileSystemVirtualizer.Stop();
        }

        public void Dispose()
        {
            if (this.BlobSizes != null)
            {
                this.BlobSizes.Dispose();
                this.BlobSizes = null;
            }

            if (this.fileSystemVirtualizer != null)
            {
                this.fileSystemVirtualizer.Dispose();
                this.fileSystemVirtualizer = null;
            }

            if (this.GitIndexProjection != null)
            {
                this.GitIndexProjection.Dispose();
                this.GitIndexProjection = null;
            }

            if (this.modifiedPaths != null)
            {
                this.modifiedPaths.Dispose();
                this.modifiedPaths = null;
            }

            if (this.gitStatusCache != null)
            {
                this.gitStatusCache.Dispose();
                this.gitStatusCache = null;
            }

            if (this.backgroundFileSystemTaskRunner != null)
            {
                this.backgroundFileSystemTaskRunner.Dispose();
                this.backgroundFileSystemTaskRunner = null;
            }
        }

        public bool IsReadyForExternalAcquireLockRequests(NamedPipeMessages.LockData requester, out string denyMessage)
        {
            if (!this.backgroundFileSystemTaskRunner.IsEmpty)
            {
                denyMessage = "Waiting for GVFS to release the lock";
                return false;
            }

            if (!this.GitIndexProjection.IsProjectionParseComplete())
            {
                denyMessage = "Waiting for GVFS to parse index and update placeholder files";
                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
            // appropriate error message in case that happens
            denyMessage = "Waiting for GVFS to release the lock";

            return true;
        }

        public EventMetadata GetAndResetHeartBeatMetadata(out bool logToFile)
        {
            logToFile = false;
            EventMetadata metadata = new EventMetadata();

            metadata.Add(
                "FilePlaceholderCreation",
                this.GetProcessInteractionData(this.GetAndResetProcessCountMetadata(ref this.filePlaceHolderCreationCount), ref logToFile));
            metadata.Add(
                "FolderPlaceholderCreation",
                this.GetProcessInteractionData(this.GetAndResetProcessCountMetadata(ref this.folderPlaceHolderCreationCount), ref logToFile));
            metadata.Add(
                "FilePlaceholdersHydrated",
                this.GetProcessInteractionData(this.GetAndResetProcessCountMetadata(ref this.fileHydrationCount), ref logToFile));

            metadata.Add("ModifiedPathsCount", this.modifiedPaths.Count);
            metadata.Add("FilePlaceholderCount", this.placeholderDatabase.GetFilePlaceholdersCount());
            metadata.Add("FolderPlaceholderCount", this.placeholderDatabase.GetFolderPlaceholdersCount());

            if (this.gitStatusCache.WriteTelemetryandReset(metadata))
            {
                logToFile = true;
            }

            metadata.Add(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId);
            metadata.Add(
                "PhysicalDiskInfo",
                GVFSPlatform.Instance.GetPhysicalDiskInfo(
                    this.context.Enlistment.WorkingDirectoryBackingRoot,
                    sizeStatsOnly: true));

            return metadata;
        }

        public bool TryDehydrateFolder(string relativePath, out string errorMessage)
        {
            List removedPlaceholders = null;
            List removedModifiedPaths = null;
            errorMessage = string.Empty;

            try
            {
                relativePath = GVFSDatabase.NormalizePath(relativePath);
                removedPlaceholders = this.placeholderDatabase.RemoveAllEntriesForFolder(relativePath);
                removedModifiedPaths = this.modifiedPaths.RemoveAllEntriesForFolder(relativePath);
                FileSystemResult result = this.fileSystemVirtualizer.DehydrateFolder(relativePath);
                if (result.Result != FSResult.Ok)
                {
                    errorMessage = $"{nameof(this.TryDehydrateFolder)} failed with {result.Result}";
                    this.context.Tracer.RelatedError(errorMessage);
                }
            }
            catch (Exception ex)
            {
                errorMessage = $"{nameof(this.TryDehydrateFolder)} threw an exception - {ex.Message}";
                EventMetadata metadata = this.CreateEventMetadata(relativePath, ex);
                this.context.Tracer.RelatedError(metadata, errorMessage);
            }

            if (!string.IsNullOrEmpty(errorMessage))
            {
                if (removedPlaceholders != null)
                {
                    foreach (IPlaceholderData data in removedPlaceholders)
                    {
                        try
                        {
                            this.placeholderDatabase.AddPlaceholderData(data);
                        }
                        catch (Exception ex)
                        {
                            EventMetadata metadata = this.CreateEventMetadata(data.Path, ex);
                            this.context.Tracer.RelatedError(metadata, $"{nameof(FileSystemCallbacks)}.{nameof(this.TryDehydrateFolder)} failed to add '{data.Path}' back into PlaceholderDatabase");
                        }
                    }
                }

                if (removedModifiedPaths != null)
                {
                    foreach (string modifiedPath in removedModifiedPaths)
                    {
                        if (!this.modifiedPaths.TryAdd(modifiedPath, isFolder: modifiedPath.EndsWith(GVFSConstants.GitPathSeparatorString), isRetryable: out bool isRetryable))
                        {
                            this.context.Tracer.RelatedError($"{nameof(FileSystemCallbacks)}.{nameof(this.TryDehydrateFolder)}: failed to add '{modifiedPath}' back into ModifiedPaths");
                        }
                    }
                }
            }

            return string.IsNullOrEmpty(errorMessage);
        }

        public void ForceIndexProjectionUpdate(bool invalidateProjection, bool invalidateModifiedPaths)
        {
            this.InvalidateState(invalidateProjection, invalidateModifiedPaths);
            this.GitIndexProjection.WaitForProjectionUpdate();
        }

        public NamedPipeMessages.ReleaseLock.Response TryReleaseExternalLock(int pid)
        {
            return this.GitIndexProjection.TryReleaseExternalLock(pid);
        }

        public IEnumerable GetAllModifiedPaths()
        {
            return this.modifiedPaths.GetAllModifiedPaths();
        }

        /// 
        /// Checks whether the given folder path, or any of its parent folders,
        /// is in the ModifiedPaths database. Used to determine if git/user has
        /// taken ownership of a directory tree.
        /// 
        public bool IsPathOrParentInModifiedPaths(string path, bool isFolder)
        {
            return this.modifiedPaths.Contains(path, isFolder) ||
                   this.modifiedPaths.ContainsParentFolder(path, out _);
        }

        /// 
        /// Finds index entries that are staged (differ from HEAD) matching the given
        /// pathspec, and adds them to ModifiedPaths. This prepares for an unstage operation
        /// (e.g., restore --staged) by ensuring git will clear skip-worktree for these
        /// entries so it can detect their working tree state correctly.
        /// Files that were added (not in HEAD) are also written to disk from the git object
        /// store as full files, so they persist after projection changes.
        /// 
        /// 
        /// IPC message body. Formats:
        ///   null/empty           — all staged files
        ///   "path1\0path2"       — inline pathspecs (null-separated)
        ///   "\nF\n{filepath}"    — --pathspec-from-file (forwarded to git)
        ///   "\nFZ\n{filepath}"   — --pathspec-from-file with --pathspec-file-nul
        /// File-reference bodies may include inline pathspecs after a 4th \n field.
        /// 
        /// Number of paths added to ModifiedPaths.
        /// True if all operations succeeded, false if any failed.
        public bool AddStagedFilesToModifiedPaths(string messageBody, out int addedCount)
        {
            addedCount = 0;
            bool success = true;

            // Use a dedicated GitProcess instance to avoid serialization with other
            // concurrent pipe message handlers that may also be running git commands.
            GitProcess gitProcess = new GitProcess(this.context.Enlistment);

            // Parse message body to extract pathspec arguments for git diff --cached
            string[] pathspecs = null;
            string pathspecFromFile = null;
            bool pathspecFileNul = false;

            if (!string.IsNullOrEmpty(messageBody))
            {
                if (messageBody.StartsWith("\n"))
                {
                    // File-reference format: "\n{F|FZ}\n[\n]"
                    string[] fields = messageBody.Split(new[] { '\n' }, 4, StringSplitOptions.None);
                    if (fields.Length >= 3)
                    {
                        pathspecFileNul = fields[1] == "FZ";
                        pathspecFromFile = fields[2];

                        if (fields.Length >= 4 && !string.IsNullOrEmpty(fields[3]))
                        {
                            pathspecs = fields[3].Split('\0');
                        }
                    }
                }
                else
                {
                    pathspecs = messageBody.Split('\0');
                }
            }

            // Query all staged files in one call using --name-status -z.
            // Output format: "A\0path1\0M\0path2\0D\0path3\0"
            GitProcess.Result result = gitProcess.DiffCachedNameStatus(pathspecs, pathspecFromFile, pathspecFileNul);
            if (result.ExitCodeIsSuccess && !string.IsNullOrEmpty(result.Output))
            {
                string[] parts = result.Output.Split(new[] { '\0' }, StringSplitOptions.RemoveEmptyEntries);
                List addedFilePaths = new List();

                // Parts alternate: status, path, status, path, ...
                for (int i = 0; i + 1 < parts.Length; i += 2)
                {
                    string status = parts[i];
                    string gitPath = parts[i + 1];

                    if (string.IsNullOrEmpty(gitPath))
                    {
                        continue;
                    }

                    string platformPath = gitPath.Replace(GVFSConstants.GitPathSeparator, Path.DirectorySeparatorChar);
                    if (this.modifiedPaths.TryAdd(platformPath, isFolder: false, isRetryable: out _))
                    {
                        addedCount++;
                    }

                    // Added files (in index but not in HEAD) are ProjFS placeholders that
                    // would vanish when the projection reverts to HEAD. Collect them for
                    // hydration below.
                    if (status.StartsWith("A"))
                    {
                        addedFilePaths.Add(gitPath);
                    }
                }

                // Write added files from the git object store to disk as full files
                // so they persist across projection changes. Batched into as few git
                // process invocations as possible.
                if (addedFilePaths.Count > 0)
                {
                    if (!this.WriteStagedFilesToWorkingDirectory(gitProcess, addedFilePaths))
                    {
                        success = false;
                    }
                }
            }
            else if (!result.ExitCodeIsSuccess)
            {
                EventMetadata metadata = new EventMetadata();
                metadata.Add("ExitCode", result.ExitCode);
                metadata.Add("Errors", result.Errors ?? string.Empty);
                this.context.Tracer.RelatedError(
                    metadata,
                    nameof(this.AddStagedFilesToModifiedPaths) + ": git diff --cached failed");
                success = false;
            }

            return success;
        }

        /// 
        /// Writes the staged (index) versions of files to the working directory as
        /// full files, bypassing ProjFS. Uses "git checkout-index --force" with
        /// batched paths to minimize process invocations.
        /// Returns true if all batches succeeded, false if any failed.
        /// 
        private bool WriteStagedFilesToWorkingDirectory(GitProcess gitProcess, List gitPaths)
        {
            bool allSucceeded = true;
            try
            {
                List results = gitProcess.CheckoutIndexForFiles(gitPaths);
                foreach (GitProcess.Result result in results)
                {
                    if (!result.ExitCodeIsSuccess)
                    {
                        allSucceeded = false;
                        EventMetadata metadata = new EventMetadata();
                        metadata.Add("pathCount", gitPaths.Count);
                        metadata.Add("error", result.Errors);
                        this.context.Tracer.RelatedWarning(
                            metadata,
                            nameof(this.WriteStagedFilesToWorkingDirectory) + ": git checkout-index failed");
                    }
                }
            }
            catch (Exception e)
            {
                allSucceeded = false;
                EventMetadata metadata = new EventMetadata();
                metadata.Add("pathCount", gitPaths.Count);
                metadata.Add("Exception", e.ToString());
                this.context.Tracer.RelatedWarning(metadata, nameof(this.WriteStagedFilesToWorkingDirectory) + ": Failed to write files");
            }

            return allSucceeded;
        }

        public virtual void OnIndexFileChange()
        {
            string lockedGitCommand = this.context.Repository.GVFSLock.GetLockedGitCommand();
            GitCommandLineParser gitCommand = new GitCommandLineParser(lockedGitCommand);
            if (!gitCommand.IsValidGitCommand)
            {
                // Something wrote to the index without holding the GVFS lock, so we invalidate the projection
                this.InvalidateState(invalidateProjection: true, invalidateModifiedPaths: false);

                // But this isn't something we expect to see, so log a warning
                EventMetadata metadata = new EventMetadata
                {
                    { "Area", EtwArea },
                    { TracingConstants.MessageKey.WarningMessage, "Index modified without git holding GVFS lock" },
                };

                this.context.Tracer.RelatedEvent(EventLevel.Warning, $"{nameof(this.OnIndexFileChange)}_NoLock", metadata);
            }
        }

        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.IsEmpty)
            {
                this.gitStatusCache.RefreshAsynchronously();
            }
        }

        public EnlistmentHydrationSummary GetCachedHydrationSummary()
        {
            return this.gitStatusCache.GetCachedHydrationSummary();
        }

        public int GetProjectedFolderCount()
        {
            return this.GitIndexProjection.GetProjectedFolderCount();
        }

        public virtual void OnLogsHeadChange()
        {
            // Don't open the .git\logs\HEAD file here to check its attributes as we're in a callback for the .git folder
            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.AddToNewlyCreatedList(relativePath, isFolder: false);
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileCreated(relativePath));
        }

        public void OnFileOverwritten(string relativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileOverwritten(relativePath));
        }

        public void OnFileSuperseded(string relativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileSuperseded(relativePath));
        }

        public void OnFileConvertedToFull(string relativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileConvertedToFull(relativePath));
        }

        public void OnFailedFileHydration(string relativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFailedFileHydration(relativePath));
        }

        public virtual void OnFileRenamed(string oldRelativePath, string newRelativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileRenamed(oldRelativePath, newRelativePath));
        }

        public virtual void OnFileHardLinkCreated(string newLinkRelativePath, string existingRelativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileHardLinkCreated(newLinkRelativePath, existingRelativePath));
        }

        public virtual void OnFileSymLinkCreated(string newLinkRelativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileSymLinkCreated(newLinkRelativePath));
        }

        public void OnFileDeleted(string relativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileDeleted(relativePath));
        }

        public void OnFilePreDelete(string relativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFilePreDelete(relativePath));
        }

        /// 
        /// Called to indicate a folder was created
        /// 
        /// The relative path to the newly created folder
        /// 
        /// true when the folder is successfully added to the sparse list because it is in the projection but currently excluded.
        /// false when the folder was not excluded or there was a failure adding to the sparse list.
        /// 
        public void OnFolderCreated(string relativePath, out bool sparseFoldersUpdated)
        {
            sparseFoldersUpdated = false;
            GitIndexProjection.PathSparseState pathProjectionState = this.GitIndexProjection.GetFolderPathSparseState(relativePath);
            if (pathProjectionState == GitIndexProjection.PathSparseState.Excluded)
            {
                if (this.GitIndexProjection.TryAddSparseFolder(relativePath))
                {
                    sparseFoldersUpdated = true;
                    return;
                }
            }

            this.AddToNewlyCreatedList(relativePath, isFolder: true);
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderCreated(relativePath));
        }

        public virtual void OnFolderRenamed(string oldRelativePath, string newRelativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderRenamed(oldRelativePath, newRelativePath));
        }

        public void OnFolderDeleted(string relativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderDeleted(relativePath));
        }

        public void OnPossibleTombstoneFolderCreated(string relativePath)
        {
            this.GitIndexProjection.OnPossibleTombstoneFolderCreated(relativePath);
        }

        public void OnFolderPreDelete(string relativePath)
        {
            this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderPreDelete(relativePath));
        }

        public void OnPlaceholderFileCreated(string relativePath, string sha, string triggeringProcessImageFileName)
        {
            this.GitIndexProjection.OnPlaceholderFileCreated(relativePath, sha);

            // Note: Because OnPlaceholderFileCreated is not synchronized on all platforms it is possible that GVFS will double count
            // the creation of file placeholders if multiple requests for the same file are received at the same time on different
            // threads.
            this.filePlaceHolderCreationCount.AddOrUpdate(
                triggeringProcessImageFileName,
                (imageName) => { return new PlaceHolderCreateCounter(); },
                (key, oldCount) => { oldCount.Increment(); return oldCount; });
        }

        public void OnPlaceholderCreateBlockedForGit()
        {
            this.GitIndexProjection.OnPlaceholderCreateBlockedForGit();
        }

        public void OnPlaceholderFolderCreated(string relativePath, string triggeringProcessImageFileName)
        {
            this.GitIndexProjection.OnPlaceholderFolderCreated(relativePath);

            this.folderPlaceHolderCreationCount.AddOrUpdate(
                triggeringProcessImageFileName,
                (imageName) => { return new PlaceHolderCreateCounter(); },
                (key, oldCount) => { oldCount.Increment(); return oldCount; });
        }

        public void OnPlaceholderFolderExpanded(string relativePath)
        {
            this.GitIndexProjection.OnPlaceholderFolderExpanded(relativePath);
        }

        public void OnPlaceholderFileHydrated(string triggeringProcessImageFileName)
        {
            this.fileHydrationCount.AddOrUpdate(
                triggeringProcessImageFileName,
                (imageName) => { return new PlaceHolderCreateCounter(); },
                (key, oldCount) => { oldCount.Increment(); return oldCount; });
        }

        public FileProperties GetLogsHeadFileProperties()
        {
            // Use a temporary FileProperties in case another thread sets this.logsHeadFileProperties before this
            // method returns
            FileProperties properties = this.logsHeadFileProperties;
            if (properties == null)
            {
                try
                {
                    properties = this.context.FileSystem.GetFileProperties(this.logsHeadPath);
                    this.logsHeadFileProperties = properties;
                }
                catch (Exception e)
                {
                    EventMetadata metadata = this.CreateEventMetadata(relativePath: null, exception: e);
                    this.context.Tracer.RelatedWarning(metadata, "GetLogsHeadFileProperties: Exception thrown from GetFileProperties", Keywords.Telemetry);

                    properties = FileProperties.DefaultFile;

                    // Leave logsHeadFileProperties null to indicate that it is still needs to be refreshed
                    this.logsHeadFileProperties = null;
                }
            }

            return properties;
        }

        private static bool CheckConditionWithRetry(Func predicate, int retries, int millisecondsToSleep)
        {
            bool result = predicate();
            while (!result && retries > 0)
            {
                Thread.Sleep(millisecondsToSleep);
                result = predicate();
                --retries;
            }

            return result;
        }

        private EventMetadata GetProcessInteractionData(ConcurrentDictionary collectedData, ref bool logToFile)
        {
            EventMetadata metadata = new EventMetadata();

            if (collectedData.Count > 0)
            {
                int count = 0;
                foreach (KeyValuePair processCount in
                collectedData.OrderByDescending((KeyValuePair kvp) => kvp.Value.Count).Take(10))
                {
                    ++count;
                    metadata.Add("ProcessName" + count, processCount.Key);
                    metadata.Add("ProcessCount" + count, processCount.Value.Count);
                }

                logToFile = true;
            }

            return metadata;
        }

        // Captures the current state of dictionary, and resets it
        // This approach is optimal for our use case to preserve all entries while avoiding additional locking
        private ConcurrentDictionary GetAndResetProcessCountMetadata(ref ConcurrentDictionary collectedData)
        {
            ConcurrentDictionary localData = collectedData;
            collectedData = new ConcurrentDictionary(GVFSPlatform.Instance.Constants.PathComparer);
            return localData;
        }

        private void InvalidateState(bool invalidateProjection, bool invalidateModifiedPaths)
        {
            if (invalidateProjection)
            {
                this.GitIndexProjection.InvalidateProjection();
            }

            if (invalidateModifiedPaths)
            {
                this.GitIndexProjection.InvalidateModifiedFiles();
                this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnIndexWriteRequiringModifiedPathsValidation());
            }

            this.InvalidateGitStatusCache();
            this.newlyCreatedFileAndFolderPaths.Clear();
        }

        private bool GitCommandLeavesProjectionUnchanged(GitCommandLineParser gitCommand)
        {
            return
                gitCommand.IsVerb(LeavesProjectionUnchangedVerbs) ||
                gitCommand.IsResetSoftOrMixed() ||
                gitCommand.IsCheckoutWithFilePaths();
        }

        private bool GitCommandRequiresModifiedPathValidationAfterIndexChange(GitCommandLineParser gitCommand)
        {
            return
                gitCommand.IsVerb(GitCommandLineParser.Verbs.UpdateIndex) ||
                gitCommand.IsResetMixed();
        }

        private FileSystemTaskResult PreBackgroundOperation()
        {
            return this.GitIndexProjection.OpenIndexForRead();
        }

        private FileSystemTaskResult ExecuteBackgroundOperation(FileSystemTask gitUpdate)
        {
            EventMetadata metadata = new EventMetadata();

            FileSystemTaskResult result;

            switch (gitUpdate.Operation)
            {
                case FileSystemTask.OperationType.OnFileCreated:
                case FileSystemTask.OperationType.OnFailedPlaceholderDelete:
                case FileSystemTask.OperationType.OnFileSymLinkCreated:
                    metadata.Add("virtualPath", gitUpdate.VirtualPath);
                    result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.VirtualPath);
                    break;

                case FileSystemTask.OperationType.OnFileHardLinkCreated:
                    metadata.Add("virtualPath", gitUpdate.VirtualPath);
                    metadata.Add("oldVirtualPath", gitUpdate.OldVirtualPath);
                    result = FileSystemTaskResult.Success;
                    if (!string.IsNullOrEmpty(gitUpdate.OldVirtualPath) && !IsPathInsideDotGit(gitUpdate.OldVirtualPath))
                    {
                        result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.OldVirtualPath);
                    }

                    if ((result == FileSystemTaskResult.Success) &&
                        !string.IsNullOrEmpty(gitUpdate.VirtualPath) && !IsPathInsideDotGit(gitUpdate.VirtualPath))
                    {
                        result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.VirtualPath);
                    }

                    break;

                case FileSystemTask.OperationType.OnFileRenamed:
                    metadata.Add("oldVirtualPath", gitUpdate.OldVirtualPath);
                    metadata.Add("virtualPath", gitUpdate.VirtualPath);
                    result = FileSystemTaskResult.Success;
                    if (!string.IsNullOrEmpty(gitUpdate.OldVirtualPath) && !IsPathInsideDotGit(gitUpdate.OldVirtualPath))
                    {
                        if (this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.OldVirtualPath))
                        {
                            result = this.TryRemoveModifiedPath(gitUpdate.OldVirtualPath, isFolder: false);
                        }
                        else
                        {
                            result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.OldVirtualPath);
                        }
                    }

                    if (result == FileSystemTaskResult.Success &&
                        !string.IsNullOrEmpty(gitUpdate.VirtualPath) &&
                        !IsPathInsideDotGit(gitUpdate.VirtualPath))
                    {
                        result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.VirtualPath);
                    }

                    break;

                case FileSystemTask.OperationType.OnFilePreDelete:
                    // This code assumes that the current implementations of FileSystemVirtualizer will call either
                    // the PreDelete or the Delete not both so if a new implementation starts calling both
                    // this will need to be cleaned up to not duplicate the work that is being done.
                    metadata.Add("virtualPath", gitUpdate.VirtualPath);
                    if (this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.VirtualPath))
                    {
                        string fullPathToFile = Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, gitUpdate.VirtualPath);

                        // Because this is a predelete message the file could still be on disk when we make this check
                        // so we retry for a limited time before deciding the delete didn't happen
                        bool fileDeleted = CheckConditionWithRetry(() => !this.context.FileSystem.FileExists(fullPathToFile), NumberOfRetriesCheckingForDeleted, MillisecondsToSleepBeforeCheckingForDeleted);
                        if (fileDeleted)
                        {
                            result = this.TryRemoveModifiedPath(gitUpdate.VirtualPath, isFolder: false);
                        }
                        else
                        {
                            result = FileSystemTaskResult.Success;
                        }
                    }
                    else
                    {
                        result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.VirtualPath);
                    }

                    break;

                case FileSystemTask.OperationType.OnFileDeleted:
                    // This code assumes that the current implementations of FileSystemVirtualizer will call either
                    // the PreDelete or the Delete not both so if a new implementation starts calling both
                    // this will need to be cleaned up to not duplicate the work that is being done.
                    metadata.Add("virtualPath", gitUpdate.VirtualPath);
                    if (this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.VirtualPath))
                    {
                        result = this.TryRemoveModifiedPath(gitUpdate.VirtualPath, isFolder: false);
                    }
                    else
                    {
                        result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.VirtualPath);
                    }

                    break;

                case FileSystemTask.OperationType.OnFileOverwritten:
                case FileSystemTask.OperationType.OnFileSuperseded:
                case FileSystemTask.OperationType.OnFileConvertedToFull:
                case FileSystemTask.OperationType.OnFailedPlaceholderUpdate:
                case FileSystemTask.OperationType.OnFailedFileHydration:
                    metadata.Add("virtualPath", gitUpdate.VirtualPath);
                    result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.VirtualPath);
                    break;

                case FileSystemTask.OperationType.OnFolderCreated:
                    metadata.Add("virtualPath", gitUpdate.VirtualPath);
                    result = this.TryAddModifiedPath(gitUpdate.VirtualPath, isFolder: true);

                    break;

                case FileSystemTask.OperationType.OnFolderRenamed:
                    result = FileSystemTaskResult.Success;
                    metadata.Add("oldVirtualPath", gitUpdate.OldVirtualPath);
                    metadata.Add("virtualPath", gitUpdate.VirtualPath);

                    if (!string.IsNullOrEmpty(gitUpdate.OldVirtualPath) &&
                        this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.OldVirtualPath))
                    {
                        result = this.TryRemoveModifiedPath(gitUpdate.OldVirtualPath, isFolder: true);
                    }

                    // An empty destination path means the folder was renamed to somewhere outside of the repo
                    // Note that only full folders can be moved\renamed, and so there will already be a recursive
                    // sparse-checkout entry for the virtualPath of the folder being moved (meaning that no
                    // additional work is needed for any files\folders inside the folder being moved)
                    if (result == FileSystemTaskResult.Success && !string.IsNullOrEmpty(gitUpdate.VirtualPath))
                    {
                        this.AddToNewlyCreatedList(gitUpdate.VirtualPath, isFolder: true);
                        result = this.TryAddModifiedPath(gitUpdate.VirtualPath, isFolder: true);
                        if (result == FileSystemTaskResult.Success)
                        {
                            Queue relativeFolderPaths = new Queue();
                            relativeFolderPaths.Enqueue(gitUpdate.VirtualPath);

                            // Remove old paths from modified paths if in the newly created list
                            while (relativeFolderPaths.Count > 0)
                            {
                                string folderPath = relativeFolderPaths.Dequeue();
                                if (result == FileSystemTaskResult.Success)
                                {
                                    try
                                    {
                                        foreach (DirectoryItemInfo itemInfo in this.context.FileSystem.ItemsInDirectory(Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, folderPath)))
                                        {
                                            string itemVirtualPath = Path.Combine(folderPath, itemInfo.Name);
                                            string oldItemVirtualPath = gitUpdate.OldVirtualPath + itemVirtualPath.Substring(gitUpdate.VirtualPath.Length);

                                            this.AddToNewlyCreatedList(itemVirtualPath, isFolder: itemInfo.IsDirectory);
                                            if (this.newlyCreatedFileAndFolderPaths.Contains(oldItemVirtualPath))
                                            {
                                                result = this.TryRemoveModifiedPath(oldItemVirtualPath, isFolder: itemInfo.IsDirectory);
                                            }

                                            if (itemInfo.IsDirectory)
                                            {
                                                relativeFolderPaths.Enqueue(itemVirtualPath);
                                            }
                                        }
                                    }
                                    catch (DirectoryNotFoundException)
                                    {
                                        // DirectoryNotFoundException can occur when the renamed folder (or one of its children) is
                                        // deleted prior to the background thread running
                                        EventMetadata exceptionMetadata = new EventMetadata();
                                        exceptionMetadata.Add("Area", "ExecuteBackgroundOperation");
                                        exceptionMetadata.Add("Operation", gitUpdate.Operation.ToString());
                                        exceptionMetadata.Add("oldVirtualPath", gitUpdate.OldVirtualPath);
                                        exceptionMetadata.Add("virtualPath", gitUpdate.VirtualPath);
                                        exceptionMetadata.Add(TracingConstants.MessageKey.InfoMessage, "DirectoryNotFoundException while traversing folder path");
                                        exceptionMetadata.Add("folderPath", folderPath);
                                        this.context.Tracer.RelatedEvent(EventLevel.Informational, "DirectoryNotFoundWhileUpdatingModifiedPaths", exceptionMetadata);
                                    }
                                    catch (IOException e)
                                    {
                                        metadata.Add("Details", "IOException while traversing folder path");
                                        metadata.Add("folderPath", folderPath);
                                        metadata.Add("Exception", e.ToString());
                                        result = FileSystemTaskResult.RetryableError;
                                        break;
                                    }
                                    catch (UnauthorizedAccessException e)
                                    {
                                        metadata.Add("Details", "UnauthorizedAccessException while traversing folder path");
                                        metadata.Add("folderPath", folderPath);
                                        metadata.Add("Exception", e.ToString());
                                        result = FileSystemTaskResult.RetryableError;
                                        break;
                                    }
                                }
                                else
                                {
                                    break;
                                }
                            }
                        }
                    }

                    break;

                case FileSystemTask.OperationType.OnFolderPreDelete:
                    // This code assumes that the current implementations of FileSystemVirtualizer will call either
                    // the PreDelete or the Delete not both so if a new implementation starts calling both
                    // this will need to be cleaned up to not duplicate the work that is being done.
                    metadata.Add("virtualPath", gitUpdate.VirtualPath);
                    if (this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.VirtualPath))
                    {
                        string fullPathToFolder = Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, gitUpdate.VirtualPath);

                        // Because this is a predelete message the file could still be on disk when we make this check
                        // so we retry for a limited time before deciding the delete didn't happen
                        bool folderDeleted = CheckConditionWithRetry(() => !this.context.FileSystem.DirectoryExists(fullPathToFolder), NumberOfRetriesCheckingForDeleted, MillisecondsToSleepBeforeCheckingForDeleted);
                        if (folderDeleted)
                        {
                            result = this.TryRemoveModifiedPath(gitUpdate.VirtualPath, isFolder: true);
                        }
                        else
                        {
                            result = FileSystemTaskResult.Success;
                        }
                    }
                    else
                    {
                        result = this.TryAddModifiedPath(gitUpdate.VirtualPath, isFolder: true);
                    }

                    break;

                case FileSystemTask.OperationType.OnFolderDeleted:
                    // This code assumes that the current implementations of FileSystemVirtualizer will call either
                    // the PreDelete or the Delete not both so if a new implementation starts calling both
                    // this will need to be cleaned up to not duplicate the work that is being done.
                    metadata.Add("virtualPath", gitUpdate.VirtualPath);
                    if (this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.VirtualPath))
                    {
                        result = this.TryRemoveModifiedPath(gitUpdate.VirtualPath, isFolder: true);
                    }
                    else
                    {
                        result = this.TryAddModifiedPath(gitUpdate.VirtualPath, isFolder: true);
                    }

                    break;

                case FileSystemTask.OperationType.OnFolderFirstWrite:
                    result = FileSystemTaskResult.Success;
                    break;

                case FileSystemTask.OperationType.OnIndexWriteRequiringModifiedPathsValidation:
                    result = this.GitIndexProjection.AddMissingModifiedFiles();
                    break;

                case FileSystemTask.OperationType.OnPlaceholderCreationsBlockedForGit:
                    this.GitIndexProjection.ClearNegativePathCacheIfPollutedByGit();
                    result = FileSystemTaskResult.Success;
                    break;

                default:
                    throw new InvalidOperationException("Invalid background operation");
            }

            if (result != FileSystemTaskResult.Success)
            {
                metadata.Add("Area", "ExecuteBackgroundOperation");
                metadata.Add("Operation", gitUpdate.Operation.ToString());
                metadata.Add(TracingConstants.MessageKey.WarningMessage, "Background operation failed");
                metadata.Add(nameof(result), result.ToString());
                this.context.Tracer.RelatedEvent(EventLevel.Warning, "FailedBackgroundOperation", metadata);
            }

            return result;
        }

        private void AddToNewlyCreatedList(string virtualPath, bool isFolder)
        {
            if (!this.modifiedPaths.Contains(virtualPath, isFolder))
            {
                this.newlyCreatedFileAndFolderPaths.Add(virtualPath);
            }
        }

        private FileSystemTaskResult TryRemoveModifiedPath(string virtualPath, bool isFolder)
        {
            if (!this.modifiedPaths.TryRemove(virtualPath, isFolder, out bool isRetryable))
            {
                return isRetryable ? FileSystemTaskResult.RetryableError : FileSystemTaskResult.FatalError;
            }

            this.newlyCreatedFileAndFolderPaths.TryRemove(virtualPath);

            this.InvalidateGitStatusCache();
            return FileSystemTaskResult.Success;
        }

        private FileSystemTaskResult TryAddModifiedPath(string virtualPath, bool isFolder)
        {
            if (!this.modifiedPaths.TryAdd(virtualPath, isFolder, out bool isRetryable))
            {
                return isRetryable ? FileSystemTaskResult.RetryableError : FileSystemTaskResult.FatalError;
            }

            this.InvalidateGitStatusCache();
            return FileSystemTaskResult.Success;
        }

        private FileSystemTaskResult AddModifiedPathAndRemoveFromPlaceholderList(string virtualPath)
        {
            FileSystemTaskResult result = this.TryAddModifiedPath(virtualPath, isFolder: false);
            if (result != FileSystemTaskResult.Success)
            {
                return result;
            }

            bool isFolder;
            string fileName;

            // We don't want to fill the placeholder list with deletes for files that are
            // not in the projection so we make sure it is in the projection before removing.
            if (this.GitIndexProjection.IsPathProjected(virtualPath, out fileName, out isFolder))
            {
                this.GitIndexProjection.RemoveFromPlaceholderList(virtualPath);
            }

            return result;
        }

        private FileSystemTaskResult PostBackgroundOperation()
        {
            this.modifiedPaths.WriteAllEntriesAndFlush();
            this.gitStatusCache.RefreshAsynchronously();
            return this.GitIndexProjection.CloseIndex();
        }

        private EventMetadata CreateEventMetadata(
            string relativePath = null,
            Exception exception = null)
        {
            EventMetadata metadata = new EventMetadata();
            metadata.Add("Area", EtwArea);

            if (relativePath != null)
            {
                metadata.Add(nameof(relativePath), relativePath);
            }

            if (exception != null)
            {
                metadata.Add("Exception", exception.ToString());
            }

            return metadata;
        }

        private class PlaceHolderCreateCounter
        {
            private long count;

            public PlaceHolderCreateCounter()
            {
                this.count = 1;
            }

            public long Count
            {
                get { return this.count; }
            }

            public void Increment()
            {
                Interlocked.Increment(ref this.count);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/GVFS.Virtualization.csproj
================================================


  
    net471
    true
  

  
    
  

  
    
    
  




================================================
FILE: GVFS/GVFS.Virtualization/InternalsVisibleTo.cs
================================================
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("GVFS.UnitTests")]


================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FileData.cs
================================================
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.Virtualization.BlobSize;
using System.Collections.Generic;

namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection
    {
        internal class FileData : FolderEntryData
        {
            // Special values that can be stored in Size
            // Use the Size field rather than additional fields to save on memory
            private const long MinValidSize = 0;
            private const long InvalidSize = -1;

            private ulong shaBytes1through8;
            private ulong shaBytes9Through16;
            private uint shaBytes17Through20;

            public override bool IsFolder => false;

            public long Size { get; set; }
            public Sha1Id Sha
            {
                get
                {
                    return new Sha1Id(this.shaBytes1through8, this.shaBytes9Through16, this.shaBytes17Through20);
                }
            }

            public bool IsSizeSet()
            {
                return this.Size >= MinValidSize;
            }

            public string ConvertShaToString()
            {
                return this.Sha.ToString();
            }

            public void ResetData(LazyUTF8String name, byte[] shaBytes)
            {
                this.Name = name;
                this.Size = InvalidSize;
                Sha1Id.ShaBufferToParts(shaBytes, out this.shaBytes1through8, out this.shaBytes9Through16, out this.shaBytes17Through20);
            }

            public bool TryPopulateSizeLocally(
                ITracer tracer,
                GVFSGitObjects gitObjects,
                BlobSizes.BlobSizesConnection blobSizesConnection,
                Dictionary availableSizes,
                out string missingSha)
            {
                missingSha = null;
                long blobLength = 0;

                Sha1Id sha1Id = new Sha1Id(this.shaBytes1through8, this.shaBytes9Through16, this.shaBytes17Through20);
                string shaString = null;

                if (availableSizes != null)
                {
                    shaString = this.ConvertShaToString();
                    if (availableSizes.TryGetValue(shaString, out blobLength))
                    {
                        this.Size = blobLength;
                        return true;
                    }
                }

                try
                {
                    if (blobSizesConnection.TryGetSize(sha1Id, out blobLength))
                    {
                        this.Size = blobLength;
                        return true;
                    }
                }
                catch (BlobSizesException e)
                {
                    EventMetadata metadata = CreateEventMetadata(e);
                    missingSha = this.ConvertShaToString();
                    metadata.Add(nameof(missingSha), missingSha);
                    tracer.RelatedWarning(metadata, $"{nameof(this.TryPopulateSizeLocally)}: Exception while trying to get file size", Keywords.Telemetry);
                }

                if (missingSha == null)
                {
                    missingSha = (shaString == null) ? this.ConvertShaToString() : shaString;
                }

                if (gitObjects.TryGetBlobSizeLocally(missingSha, out blobLength))
                {
                    this.Size = blobLength;

                    // There is no flush for this value because it's already local, so there's little loss if it doesn't get persisted
                    // But it's faster to wait for some remote call to batch this value into a different flush
                    blobSizesConnection.BlobSizesDatabase.AddSize(sha1Id, blobLength);
                    return true;
                }

                return false;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FileTypeAndMode.cs
================================================
using System;
namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection
    {
        internal struct FileTypeAndMode
        {
            // Bitmasks for extracting file type and mode from the ushort stored in the index
            private const ushort FileTypeMask = 0xF000;
            private const ushort FileModeMask = 0x1FF;

            // Values used in the index file to indicate the type of the file
            private const ushort RegularFileIndexEntry = 0x8000;
            private const ushort SymLinkFileIndexEntry = 0xA000;
            private const ushort GitLinkFileIndexEntry = 0xE000;

            public FileTypeAndMode(ushort typeAndModeInIndexFormat)
            {
                switch (typeAndModeInIndexFormat & FileTypeMask)
                {
                    case RegularFileIndexEntry:
                        this.Type = FileType.Regular;
                        break;
                    case SymLinkFileIndexEntry:
                        this.Type = FileType.SymLink;
                        break;
                    case GitLinkFileIndexEntry:
                        this.Type = FileType.GitLink;
                        break;
                    default:
                        this.Type = FileType.Invalid;
                        break;
                }

                this.Mode = (ushort)(typeAndModeInIndexFormat & FileModeMask);
            }

            public FileType Type { get; }
            public ushort Mode { get; }

            public string GetModeAsOctalString()
            {
                return Convert.ToString(this.Mode, 8);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FolderData.cs
================================================
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.Virtualization.BlobSize;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;

namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection
    {
        internal class FolderData : FolderEntryData
        {
            public override bool IsFolder => true;

            public SortedFolderEntries ChildEntries { get; private set; }
            public bool ChildrenHaveSizes { get; private set; }
            public bool IsIncluded { get; set; } = true;

            public int GetRecursiveFolderCount()
            {
                int count = 0;
                Stack stack = new Stack();
                stack.Push(this);

                while (stack.Count > 0)
                {
                    FolderData current = stack.Pop();
                    for (int i = 0; i < current.ChildEntries.Count; i++)
                    {
                        FolderData childFolder = current.ChildEntries[i] as FolderData;
                        if (childFolder != null)
                        {
                            count++;
                            stack.Push(childFolder);
                        }
                    }
                }

                return count;
            }

            public void ResetData(LazyUTF8String name, bool isIncluded)
            {
                this.Name = name;
                this.ChildrenHaveSizes = false;
                this.IsIncluded = isIncluded;
                if (this.ChildEntries == null)
                {
                    this.ChildEntries = new SortedFolderEntries();
                }

                this.ChildEntries.Clear();
            }

            public FileData AddChildFile(LazyUTF8String name, byte[] shaBytes)
            {
                return this.ChildEntries.AddFile(name, shaBytes);
            }

            public void Include()
            {
                this.IsIncluded = true;
                for (int i = 0; i < this.ChildEntries.Count; i++)
                {
                    if (this.ChildEntries[i].IsFolder)
                    {
                        FolderData folderData = (FolderData)this.ChildEntries[i];
                        folderData.Include();
                    }
                }
            }

            public string HashedChildrenNamesSha()
            {
                using (HashAlgorithm hash = SHA1.Create()) // CodeQL [SM02196] SHA-1 is acceptable here because this is Git's hashing algorithm, not used for cryptographic purposes
                {
                    for (int i = 0; i < this.ChildEntries.Count; i++)
                    {
                        FolderEntryData entry = this.ChildEntries[i];

                        // Name only
                        byte[] bytes = Encoding.UTF8.GetBytes(entry.Name.ToString());
                        hash.TransformBlock(bytes, 0, bytes.Length, null, 0);
                    }

                    hash.TransformFinalBlock(new byte[0], 0, 0);
                    return SHA1Util.HexStringFromBytes(hash.Hash);
                }
            }

            public void PopulateSizes(
                ITracer tracer,
                GVFSGitObjects gitObjects,
                BlobSizes.BlobSizesConnection blobSizesConnection,
                Dictionary availableSizes,
                CancellationToken cancellationToken)
            {
                if (this.ChildrenHaveSizes)
                {
                    return;
                }

                HashSet missingShas;
                List childrenMissingSizes;
                this.PopulateSizesLocally(tracer, gitObjects, blobSizesConnection, availableSizes, out missingShas, out childrenMissingSizes);

                lock (this)
                {
                    // Check ChildrenHaveSizes again in case another
                    // thread has already done the work of setting the sizes
                    if (this.ChildrenHaveSizes)
                    {
                        return;
                    }

                    this.PopulateSizesFromRemote(
                        tracer,
                        gitObjects,
                        blobSizesConnection,
                        missingShas,
                        childrenMissingSizes,
                        cancellationToken);
                }
            }

            /// 
            /// Populates the sizes of child entries in the folder using locally available data
            /// 
            private void PopulateSizesLocally(
                ITracer tracer,
                GVFSGitObjects gitObjects,
                BlobSizes.BlobSizesConnection blobSizesConnection,
                Dictionary availableSizes,
                out HashSet missingShas,
                out List childrenMissingSizes)
            {
                if (this.ChildrenHaveSizes)
                {
                    missingShas = null;
                    childrenMissingSizes = null;
                    return;
                }

                missingShas = new HashSet();
                childrenMissingSizes = new List();
                for (int i = 0; i < this.ChildEntries.Count; i++)
                {
                    FileData childEntry = this.ChildEntries[i] as FileData;
                    if (childEntry != null)
                    {
                        string sha;
                        if (!childEntry.TryPopulateSizeLocally(tracer, gitObjects, blobSizesConnection, availableSizes, out sha))
                        {
                            childrenMissingSizes.Add(new FileMissingSize(childEntry, sha));
                            missingShas.Add(sha);
                        }
                    }
                }

                if (childrenMissingSizes.Count == 0)
                {
                    this.ChildrenHaveSizes = true;
                }
            }

            /// 
            /// Populate sizes using size data from the remote
            /// 
            /// Set of object shas whose sizes should be downloaded from the remote.  This set should contains all the distinct SHAs from
            /// in childrenMissingSizes.  PopulateSizesLocally can be used to generate this set
            /// List of child entries whose sizes should be downloaded from the remote.  PopulateSizesLocally
            /// can be used to generate this list
            private void PopulateSizesFromRemote(
                ITracer tracer,
                GVFSGitObjects gitObjects,
                BlobSizes.BlobSizesConnection blobSizesConnection,
                HashSet missingShas,
                List childrenMissingSizes,
                CancellationToken cancellationToken)
            {
                if (childrenMissingSizes != null && childrenMissingSizes.Count > 0)
                {
                    Dictionary objectLengths = gitObjects.GetFileSizes(missingShas, cancellationToken).ToDictionary(s => s.Id, s => s.Size, StringComparer.OrdinalIgnoreCase);
                    foreach (FileMissingSize childNeedingSize in childrenMissingSizes)
                    {
                        long blobLength = 0;
                        if (objectLengths.TryGetValue(childNeedingSize.Sha, out blobLength))
                        {
                            childNeedingSize.Data.Size = blobLength;
                            blobSizesConnection.BlobSizesDatabase.AddSize(
                                childNeedingSize.Data.Sha,
                                blobLength);
                        }
                        else
                        {
                            EventMetadata metadata = CreateEventMetadata();
                            metadata.Add("SHA", childNeedingSize.Sha);
                            tracer.RelatedError(metadata, "PopulateMissingSizesFromRemote: Failed to download size for child entry", Keywords.Network);
                            throw new SizesUnavailableException("Failed to download size for " + childNeedingSize.Sha);
                        }
                    }

                    blobSizesConnection.BlobSizesDatabase.Flush();
                }

                this.ChildrenHaveSizes = true;
            }

            // Wrapper for FileData that allows for caching string SHAs
            protected class FileMissingSize
            {
                public FileMissingSize(FileData fileData, string sha)
                {
                    this.Data = fileData;
                    this.Sha = sha;
                }

                public FileData Data { get; }

                public string Sha { get; }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FolderEntryData.cs
================================================
using GVFS.Common.Tracing;
using System;

namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection
    {
        internal abstract class FolderEntryData
        {
            public LazyUTF8String Name { get; protected set; }
            public abstract bool IsFolder { get; }

            protected static EventMetadata CreateEventMetadata(Exception e = null)
            {
                EventMetadata metadata = new EventMetadata();
                metadata.Add("Area", nameof(FolderEntryData));
                if (e != null)
                {
                    metadata.Add("Exception", e.ToString());
                }

                return metadata;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.GitIndexEntry.cs
================================================
using GVFS.Common;
using System;
using System.Linq;
using System.Text;

namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection
    {
        /// 
        /// Data for an entry in the git index
        /// 
        /// 
        /// GitIndexEntry should not be used for storing projection data. It's designed for
        /// temporary storage of a single entry from the index.
        /// 
        internal class GitIndexEntry
        {
            private const int MaxPathBufferSize = 4096;
            private const int MaxParts = MaxPathBufferSize / 2;
            private const byte PathSeparatorCode = 0x2F;

            private int buildingProjectionPreviousFinalSeparatorIndex = int.MaxValue;

            // Only used when buildingNewProjection is false
            private string backgroundTaskRelativePath;
            private StringBuilder backgroundTaskRelativePathBuilder;

            public GitIndexEntry(bool buildingNewProjection)
            {
                if (buildingNewProjection)
                {
                    this.BuildingProjection_PathParts = new LazyUTF8String[MaxParts];
                }
                else
                {
                    this.backgroundTaskRelativePathBuilder = new StringBuilder(MaxPathBufferSize);
                }
            }

            public byte[] Sha { get; } = new byte[20];
            public bool SkipWorktree { get; set; }
            public FileTypeAndMode TypeAndMode { get; set; }
            public GitIndexParser.MergeStage MergeState { get; set; }
            public int ReplaceIndex { get; set; }

            /// 
            /// Number of bytes for the path in the PathBuffer
            /// 
            public int PathLength { get; set; }
            public byte[] PathBuffer { get; } = new byte[MaxPathBufferSize];
            public FolderData BuildingProjection_LastParent { get; set; }

            // Only used when buildingNewProjection is true
            public LazyUTF8String[] BuildingProjection_PathParts
            {
                get; private set;
            }

            public int BuildingProjection_NumParts
            {
                get; private set;
            }

            public bool BuildingProjection_HasSameParentAsLastEntry
            {
               get; private set;
            }

            /// 
            /// Parses the path using LazyUTF8Strings. It should only be called when building a new projection.
            /// 
            public unsafe void BuildingProjection_ParsePath()
            {
                this.PathBuffer[this.PathLength] = 0;

                // The index of that path part that is after the path separator
                int currentPartStartIndex = 0;

                // The index to start looking for the next path separator
                // Because the previous final separator is stored and we know where the previous path will be replaced
                // the code can use the previous final separator to start looking from that point instead of having to
                // run through the entire path to break it apart
                /* Example:
                 * Previous path = folder/where/previous/separator/is/used/file.txt
                 * This path     = folder/where/previous/separator/is/used/file2.txt
                 *                                                        ^    ^
                 *                         this.previousFinalSeparatorIndex    |
                 *                                                             this.ReplaceIndex
                 *
                 *   folder/where/previous/separator/is/used/file2.txt
                 *                                           ^^
                 *                       currentPartStartIndex|
                 *                                            forLoopStartIndex
                 */
                int forLoopStartIndex = 0;

                fixed (byte* pathPtr = this.PathBuffer)
                {
                    if (this.buildingProjectionPreviousFinalSeparatorIndex < this.ReplaceIndex &&
                        !this.RangeContains(pathPtr + this.ReplaceIndex, this.PathLength - this.ReplaceIndex, PathSeparatorCode))
                    {
                        // Only need to parse the last part, because the rest of the string is unchanged

                        // The logical thing to do would be to start the for loop at previousFinalSeparatorIndex+1, but two
                        // repeated / characters would make an invalid path, so we'll assume that git would not have stored that path
                        forLoopStartIndex = this.buildingProjectionPreviousFinalSeparatorIndex + 2;

                        // we still do need to start the current part's index at the correct spot, so subtract one for that
                        currentPartStartIndex = forLoopStartIndex - 1;

                        this.BuildingProjection_NumParts--;

                        this.BuildingProjection_HasSameParentAsLastEntry = true;
                    }
                    else
                    {
                        this.BuildingProjection_NumParts = 0;
                        this.ClearLastParent();
                    }

                    int partIndex = this.BuildingProjection_NumParts;

                    byte* forLoopPtr = pathPtr + forLoopStartIndex;
                    for (int i = forLoopStartIndex; i < this.PathLength + 1; i++)
                    {
                        if (*forLoopPtr == PathSeparatorCode)
                        {
                            this.BuildingProjection_PathParts[partIndex] = LazyUTF8String.FromByteArray(pathPtr + currentPartStartIndex, i - currentPartStartIndex);

                            partIndex++;
                            currentPartStartIndex = i + 1;

                            this.BuildingProjection_NumParts++;
                            this.buildingProjectionPreviousFinalSeparatorIndex = i;
                        }

                        ++forLoopPtr;
                    }

                    // We unrolled the final part calculation to after the loop, to avoid having to do a 0-byte check inside the for loop
                    this.BuildingProjection_PathParts[partIndex] = LazyUTF8String.FromByteArray(pathPtr + currentPartStartIndex, this.PathLength - currentPartStartIndex);

                    this.BuildingProjection_NumParts++;
                }
            }

            /// 
            /// Parses the path from the index as platform-specific relative path.
            /// It should only be called when running a background task.
            /// 
            public unsafe void BackgroundTask_ParsePath()
            {
                this.PathBuffer[this.PathLength] = 0;

                // The index in the buffer at which to start parsing
                int loopStartIndex = 0;

                // backgroundTaskRelativePathBuilder is reset to empty if the last index entry contained non-ASCII character(s)
                // Only start from ReplaceIndex if the last entry contained *only* ASCII characters
                if (this.backgroundTaskRelativePathBuilder.Length > 0)
                {
                    loopStartIndex = this.ReplaceIndex;
                    this.backgroundTaskRelativePathBuilder.Length = this.ReplaceIndex;
                }

                fixed (byte* pathPtr = this.PathBuffer)
                {
                    byte* bufferPtrForLoop = pathPtr + loopStartIndex;
                    while (loopStartIndex < this.PathLength)
                    {
                        if (*bufferPtrForLoop <= 127)
                        {
                            if (*bufferPtrForLoop == PathSeparatorCode)
                            {
                                this.backgroundTaskRelativePathBuilder.Append(GVFSPlatform.GVFSPlatformConstants.PathSeparator);
                            }
                            else
                            {
                                this.backgroundTaskRelativePathBuilder.Append((char)(*bufferPtrForLoop));
                            }
                        }
                        else
                        {
                            // The string has non-ASCII characters in it, fall back to full parsing
                            this.backgroundTaskRelativePath = Encoding.UTF8.GetString(pathPtr, this.PathLength);
                            if (GVFSConstants.GitPathSeparator != GVFSPlatform.GVFSPlatformConstants.PathSeparator)
                            {
                                this.backgroundTaskRelativePath = this.backgroundTaskRelativePath.Replace(GVFSConstants.GitPathSeparator, GVFSPlatform.GVFSPlatformConstants.PathSeparator);
                            }

                            // Record that this entry had non-ASCII characters by clearing backgroundTaskRelativePathBuilder
                            this.backgroundTaskRelativePathBuilder.Clear();

                            return;
                        }

                        ++bufferPtrForLoop;
                        ++loopStartIndex;
                    }

                    this.backgroundTaskRelativePath = this.backgroundTaskRelativePathBuilder.ToString();
                }
            }

            /// 
            /// Clear last parent data that's tracked when GitIndexEntry is used for building a new projection.
            /// No-op when GitIndexEntry is used for background tasks.
            /// 
            public void ClearLastParent()
            {
                this.buildingProjectionPreviousFinalSeparatorIndex = int.MaxValue;
                this.BuildingProjection_HasSameParentAsLastEntry = false;
                this.BuildingProjection_LastParent = null;
            }

            public LazyUTF8String BuildingProjection_GetChildName()
            {
                return this.BuildingProjection_PathParts[this.BuildingProjection_NumParts - 1];
            }

            public string BuildingProjection_GetGitRelativePath()
            {
                return string.Join(GVFSConstants.GitPathSeparatorString, this.BuildingProjection_PathParts.Take(this.BuildingProjection_NumParts).Select(x => x.GetString()));
            }

            public string BackgroundTask_GetPlatformRelativePath()
            {
                return this.backgroundTaskRelativePath;
            }

            private unsafe bool RangeContains(byte* bufferPtr, int count, byte value)
            {
                byte* indexPtr = bufferPtr;
                while (indexPtr - bufferPtr < count)
                {
                    if (*indexPtr == value)
                    {
                        return true;
                    }

                    ++indexPtr;
                }

                return false;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.GitIndexParser.cs
================================================
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Common.Tracing;
using GVFS.Virtualization.Background;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection
    {
        internal partial class GitIndexParser
        {
            public const int PageSize = 512 * 1024;

            private const ushort ExtendedBit = 0x4000;
            private const ushort SkipWorktreeBit = 0x4000;

            private Stream indexStream;
            private byte[] page;
            private int nextByteIndex;

            private GitIndexProjection projection;

            /// 
            /// A single GitIndexEntry instance used for parsing all entries in the index when building the projection
            /// 
            private GitIndexEntry resuableProjectionBuildingIndexEntry = new GitIndexEntry(buildingNewProjection: true);

            /// 
            /// A single GitIndexEntry instance used by the background task thread for parsing all entries in the index
            /// 
            private GitIndexEntry resuableBackgroundTaskThreadIndexEntry = new GitIndexEntry(buildingNewProjection: false);

            public GitIndexParser(GitIndexProjection projection)
            {
                this.projection = projection;
                this.page = new byte[PageSize];
            }

            public enum MergeStage : byte
            {
                NoConflicts = 0,
                CommonAncestor = 1,
                Yours = 2,
                Theirs = 3
            }

            public static void ValidateIndex(ITracer tracer, Stream indexStream)
            {
                GitIndexParser indexParser = new GitIndexParser(null);
                FileSystemTaskResult result = indexParser.ParseIndex(tracer, indexStream, indexParser.resuableProjectionBuildingIndexEntry, ValidateIndexEntry);

                if (result != FileSystemTaskResult.Success)
                {
                    // ValidateIndex should always result in FileSystemTaskResult.Success (or a thrown exception)
                    throw new InvalidOperationException($"{nameof(ValidateIndex)} failed: {result.ToString()}");
                }
            }

            /// 
            /// Count unique directories in the index by scanning entry paths for separators.
            /// Uses the existing index parser to read entries, avoiding a custom index parser.
            /// 
            public static int CountIndexFolders(ITracer tracer, Stream indexStream)
            {
                HashSet dirs = new HashSet(StringComparer.OrdinalIgnoreCase);
                GitIndexParser indexParser = new GitIndexParser(null);

                FileSystemTaskResult result = indexParser.ParseIndex(
                    tracer,
                    indexStream,
                    indexParser.resuableProjectionBuildingIndexEntry,
                    entry =>
                    {
                        // Match the same filter as AddIndexEntryToProjection so the
                        // fallback folder count agrees with the mounted projection.
                        if (!((entry.MergeState != MergeStage.CommonAncestor && entry.SkipWorktree) || entry.MergeState == MergeStage.Yours))
                        {
                            return FileSystemTaskResult.Success;
                        }

                        // Extract unique parent directories from the raw path buffer
                        string path = Encoding.UTF8.GetString(entry.PathBuffer, 0, entry.PathLength);
                        int lastSlash = path.LastIndexOf('/');
                        while (lastSlash > 0)
                        {
                            string dir = path.Substring(0, lastSlash);
                            if (!dirs.Add(dir))
                            {
                                break;
                            }

                            lastSlash = dir.LastIndexOf('/');
                        }

                        return FileSystemTaskResult.Success;
                    });

                if (result != FileSystemTaskResult.Success)
                {
                    throw new InvalidOperationException($"{nameof(CountIndexFolders)} failed: {result}");
                }

                return dirs.Count;
            }

            public void RebuildProjection(ITracer tracer, Stream indexStream)
            {
                if (this.projection == null)
                {
                    throw new InvalidOperationException($"{nameof(this.projection)} cannot be null when calling {nameof(this.RebuildProjection)}");
                }

                this.projection.ClearProjectionCaches();
                FileSystemTaskResult result = this.ParseIndex(
                    tracer,
                    indexStream,
                    this.resuableProjectionBuildingIndexEntry,
                    this.AddIndexEntryToProjection);

                if (result != FileSystemTaskResult.Success)
                {
                    // RebuildProjection should always result in FileSystemTaskResult.Success (or a thrown exception)
                    throw new InvalidOperationException($"{nameof(this.RebuildProjection)}: {nameof(GitIndexParser.ParseIndex)} failed to {nameof(this.AddIndexEntryToProjection)}");
                }
            }

            public FileSystemTaskResult AddMissingModifiedFilesAndRemoveThemFromPlaceholderList(
                ITracer tracer,
                Stream indexStream)
            {
                if (this.projection == null)
                {
                    throw new InvalidOperationException($"{nameof(this.projection)} cannot be null when calling {nameof(this.AddMissingModifiedFilesAndRemoveThemFromPlaceholderList)}");
                }

                HashSet filePlaceholders = this.projection.placeholderDatabase.GetAllFilePaths();

                tracer.RelatedEvent(
                    EventLevel.Informational,
                    $"{nameof(this.AddMissingModifiedFilesAndRemoveThemFromPlaceholderList)}_FilePlaceholderCount",
                    new EventMetadata
                    {
                        { "FilePlaceholderCount", filePlaceholders.Count }
                    });

                FileSystemTaskResult result = this.ParseIndex(
                    tracer,
                    indexStream,
                    this.resuableBackgroundTaskThreadIndexEntry,
                    (data) => this.AddEntryToModifiedPathsAndRemoveFromPlaceholdersIfNeeded(data, filePlaceholders));

                if (result != FileSystemTaskResult.Success)
                {
                    return result;
                }

                // Any paths that were not found in the index need to be added to ModifiedPaths
                // and removed from the placeholder list
                foreach (string path in filePlaceholders)
                {
                    result = this.projection.AddModifiedPath(path);
                    if (result != FileSystemTaskResult.Success)
                    {
                        return result;
                    }

                    this.projection.RemoveFromPlaceholderList(path);
                }

                return FileSystemTaskResult.Success;
            }

            private static FileSystemTaskResult ValidateIndexEntry(GitIndexEntry data)
            {
                if (data.PathLength <= 0 || data.PathBuffer[0] == 0)
                {
                    throw new InvalidDataException("Zero-length path found in index");
                }

                return FileSystemTaskResult.Success;
            }

            private FileSystemTaskResult AddIndexEntryToProjection(GitIndexEntry data)
            {
                // Never want to project the common ancestor even if the skip worktree bit is on
                if ((data.MergeState != MergeStage.CommonAncestor && data.SkipWorktree) || data.MergeState == MergeStage.Yours)
                {
                    data.BuildingProjection_ParsePath();
                    this.projection.AddItemFromIndexEntry(data);
                }
                else
                {
                    data.ClearLastParent();
                }

                return FileSystemTaskResult.Success;
            }

            /// 
            /// Adjusts the modifed paths and placeholders list for an index entry.
            /// 
            /// Index entry
            /// 
            /// Dictionary of file placeholders.  AddEntryToModifiedPathsAndRemoveFromPlaceholdersIfNeeded will
            /// remove enties from filePlaceholders as they are found in the index.  After
            /// AddEntryToModifiedPathsAndRemoveFromPlaceholdersIfNeeded is called for all entries in the index
            /// filePlaceholders will contain only those placeholders that are not in the index.
            /// 
            private FileSystemTaskResult AddEntryToModifiedPathsAndRemoveFromPlaceholdersIfNeeded(
                GitIndexEntry gitIndexEntry,
                HashSet filePlaceholders)
            {
                gitIndexEntry.BackgroundTask_ParsePath();
                string placeholderRelativePath = gitIndexEntry.BackgroundTask_GetPlatformRelativePath();

                FileSystemTaskResult result = FileSystemTaskResult.Success;

                if (!gitIndexEntry.SkipWorktree)
                {
                    // A git command (e.g. 'git reset --mixed') may have cleared a file's skip worktree bit without
                    // triggering an update to the projection. If git cleared the skip-worktree bit then git will
                    // be responsible for updating the file and we need to:
                    //    - Ensure this file is in GVFS's modified files database
                    //    - Remove this path from the placeholders list (if present)
                    result = this.projection.AddModifiedPath(placeholderRelativePath);

                    if (result == FileSystemTaskResult.Success)
                    {
                        if (filePlaceholders.Remove(placeholderRelativePath))
                        {
                            this.projection.RemoveFromPlaceholderList(placeholderRelativePath);
                        }
                    }
                }
                else
                {
                    filePlaceholders.Remove(placeholderRelativePath);
                }

                return result;
            }

            /// 
            /// Takes an action on a GitIndexEntry using the index in indexStream
            /// 
            /// Stream for reading a git index file
            /// Action to take on each GitIndexEntry from the index
            /// 
            /// FileSystemTaskResult indicating success or failure of the specified action
            /// 
            /// 
            /// Only the AddToModifiedFiles method because it updates the modified paths file can result
            /// in TryIndexAction returning a FileSystemTaskResult other than Success.  All other actions result in success (or an exception in the
            /// case of a corrupt index)
            /// 
            private FileSystemTaskResult ParseIndex(
                ITracer tracer,
                Stream indexStream,
                GitIndexEntry resuableParsedIndexEntry,
                Func entryAction)
            {
                this.indexStream = indexStream;
                this.indexStream.Position = 0;
                this.ReadNextPage();

                if (this.page[0] != 'D' ||
                    this.page[1] != 'I' ||
                    this.page[2] != 'R' ||
                    this.page[3] != 'C')
                {
                    throw new InvalidDataException("Incorrect magic signature for index: " + string.Join(string.Empty, this.page.Take(4).Select(c => (char)c)));
                }

                this.Skip(4);
                uint indexVersion = this.ReadFromIndexHeader();
                if (indexVersion != 4)
                {
                    throw new InvalidDataException("Unsupported index version: " + indexVersion);
                }

                uint entryCount = this.ReadFromIndexHeader();

                // Don't want to flood the logs on large indexes so only log every 500ms
                const int LoggingTicksThreshold = 500;
                int nextLogTicks = Environment.TickCount + LoggingTicksThreshold;

                SortedFolderEntries.InitializePools(tracer, entryCount);
                LazyUTF8String.InitializePools(tracer, entryCount);

                resuableParsedIndexEntry.ClearLastParent();
                int previousPathLength = 0;

                bool parseMode = GVFSPlatform.Instance.FileSystem.SupportsFileMode;
                FileSystemTaskResult result = FileSystemTaskResult.Success;
                for (int i = 0; i < entryCount; i++)
                {
                    if (parseMode)
                    {
                        this.Skip(26);

                        // 4-bit object type
                        //     valid values in binary are 1000(regular file), 1010(symbolic link) and 1110(gitlink)
                        // 3-bit unused
                        // 9-bit unix permission. Only 0755 and 0644 are valid for regular files. (Legacy repos can also contain 664)
                        //     Symbolic links and gitlinks have value 0 in this field.
                        ushort indexFormatTypeAndMode = this.ReadUInt16();

                        FileTypeAndMode typeAndMode = new FileTypeAndMode(indexFormatTypeAndMode);

                        switch (typeAndMode.Type)
                        {
                            case FileType.Regular:
                                if (typeAndMode.Mode != FileMode755 &&
                                    typeAndMode.Mode != FileMode644 &&
                                    typeAndMode.Mode != FileMode664)
                                {
                                    throw new InvalidDataException($"Invalid file mode {typeAndMode.GetModeAsOctalString()} found for regular file in index");
                                }

                                break;

                            case FileType.SymLink:
                            case FileType.GitLink:
                                if (typeAndMode.Mode != 0)
                                {
                                    throw new InvalidDataException($"Invalid file mode {typeAndMode.GetModeAsOctalString()} found for link file({typeAndMode.Type:X}) in index");
                                }

                                break;

                            default:
                                throw new InvalidDataException($"Invalid file type {typeAndMode.Type:X} found in index");
                        }

                        resuableParsedIndexEntry.TypeAndMode = typeAndMode;

                        this.Skip(12);
                    }
                    else
                    {
                        this.Skip(40);
                    }

                    this.ReadSha(resuableParsedIndexEntry);

                    ushort flags = this.ReadUInt16();
                    if (flags == 0)
                    {
                        throw new InvalidDataException("Invalid flags found in index");
                    }

                    resuableParsedIndexEntry.MergeState = (MergeStage)((flags >> 12) & 3);
                    bool isExtended = (flags & ExtendedBit) == ExtendedBit;
                    resuableParsedIndexEntry.PathLength = (ushort)(flags & 0xFFF);

                    resuableParsedIndexEntry.SkipWorktree = false;
                    if (isExtended)
                    {
                        ushort extendedFlags = this.ReadUInt16();
                        resuableParsedIndexEntry.SkipWorktree = (extendedFlags & SkipWorktreeBit) == SkipWorktreeBit;
                    }

                    int replaceLength = this.ReadReplaceLength();
                    resuableParsedIndexEntry.ReplaceIndex = previousPathLength - replaceLength;
                    int bytesToRead = resuableParsedIndexEntry.PathLength - resuableParsedIndexEntry.ReplaceIndex + 1;
                    this.ReadPath(resuableParsedIndexEntry, resuableParsedIndexEntry.ReplaceIndex, bytesToRead);
                    previousPathLength = resuableParsedIndexEntry.PathLength;

                    result = entryAction.Invoke(resuableParsedIndexEntry);
                    if (result != FileSystemTaskResult.Success)
                    {
                        return result;
                    }

                    int curTicks = Environment.TickCount;
                    if (curTicks - nextLogTicks > 0)
                    {
                        tracer.RelatedInfo($"{i}/{entryCount} index entries parsed.");
                        nextLogTicks = curTicks + LoggingTicksThreshold;
                    }
                }

                tracer.RelatedInfo($"Finished parsing {entryCount} index entries.");
                return result;
            }

            private void ReadNextPage()
            {
                this.indexStream.Read(this.page, 0, PageSize);
                this.nextByteIndex = 0;
            }

            private int ReadReplaceLength()
            {
                int headerByte = this.ReadByte();
                int offset = headerByte & 0x7f;

                // Terminate the loop when the high bit is no longer set.
                for (int i = 0; (headerByte & 0x80) != 0; i++)
                {
                    headerByte = this.ReadByte();
                    if (headerByte < 0)
                    {
                        throw new EndOfStreamException("Unexpected end of stream while reading git index.");
                    }

                    offset += 1;
                    offset = (offset << 7) + (headerByte & 0x7f);
                }

                return offset;
            }

            private void ReadSha(GitIndexEntry indexEntryData)
            {
                if (this.nextByteIndex + 20 <= PageSize)
                {
                    Buffer.BlockCopy(this.page, this.nextByteIndex, indexEntryData.Sha, 0, 20);
                    this.Skip(20);
                }
                else
                {
                    int availableBytes = PageSize - this.nextByteIndex;
                    int remainingBytes = 20 - availableBytes;

                    if (availableBytes > 0)
                    {
                        Buffer.BlockCopy(this.page, this.nextByteIndex, indexEntryData.Sha, 0, availableBytes);
                    }

                    this.ReadNextPage();
                    Buffer.BlockCopy(this.page, this.nextByteIndex, indexEntryData.Sha, availableBytes, remainingBytes);
                    this.Skip(remainingBytes);
                }
            }

            private void ReadPath(GitIndexEntry indexEntryData, int replaceIndex, int byteCount)
            {
                if (this.nextByteIndex + byteCount <= PageSize)
                {
                    Buffer.BlockCopy(this.page, this.nextByteIndex, indexEntryData.PathBuffer, replaceIndex, byteCount);
                    this.Skip(byteCount);
                }
                else
                {
                    int availableBytes = PageSize - this.nextByteIndex;
                    int remainingBytes = byteCount - availableBytes;

                    if (availableBytes != 0)
                    {
                        this.ReadPath(indexEntryData, replaceIndex, availableBytes);
                    }

                    this.ReadNextPage();
                    this.ReadPath(indexEntryData, replaceIndex + availableBytes, remainingBytes);
                }
            }

            private uint ReadFromIndexHeader()
            {
                // This code should only get called for parsing the header, so we don't need to worry about wrapping around a page
                uint result = (uint)
                    (this.page[this.nextByteIndex] << 24 |
                    this.page[this.nextByteIndex + 1] << 16 |
                    this.page[this.nextByteIndex + 2] << 8 |
                    this.page[this.nextByteIndex + 3]);
                this.Skip(4);
                return result;
            }

            private ushort ReadUInt16()
            {
                if (this.nextByteIndex + 2 <= PageSize)
                {
                    ushort result = (ushort)
                        (this.page[this.nextByteIndex] << 8 |
                        this.page[this.nextByteIndex + 1]);
                    this.Skip(2);

                    return result;
                }
                else
                {
                    return (ushort)(this.ReadByte() << 8 | this.ReadByte());
                }
            }

            private byte ReadByte()
            {
                if (this.nextByteIndex < PageSize)
                {
                    byte result = this.page[this.nextByteIndex];

                    this.nextByteIndex++;

                    return result;
                }
                else
                {
                    this.ReadNextPage();
                    return this.ReadByte();
                }
            }

            private void Skip(int byteCount)
            {
                if (this.nextByteIndex + byteCount <= PageSize)
                {
                    this.nextByteIndex += byteCount;
                }
                else
                {
                    int availableBytes = PageSize - this.nextByteIndex;
                    int remainingBytes = byteCount - availableBytes;

                    this.ReadNextPage();
                    this.Skip(remainingBytes);
                }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.LazyUTF8String.cs
================================================
using GVFS.Common.Tracing;
using System;
using System.Runtime.InteropServices;
using System.Text;

namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection
    {
        internal class LazyUTF8String
        {
            private static ObjectPool stringPool;
            private static BytePool bytePool;

            private int startIndex;
            private int length;

            private string utf16string;

            public LazyUTF8String()
            {
            }

            public LazyUTF8String(string value)
            {
                this.utf16string = value;
                this.startIndex = -1;
                this.length = -1;
            }

            public static void InitializePools(ITracer tracer, uint indexEntryCount)
            {
                if (stringPool == null)
                {
                    stringPool = new ObjectPool(tracer, Convert.ToInt32(indexEntryCount * PoolAllocationMultipliers.StringPool), objectCreator: () => new LazyUTF8String());
                }

                if (bytePool == null)
                {
                    bytePool = new BytePool(tracer, indexEntryCount);
                }
            }

            public static void ResetPool(ITracer tracer, uint indexEntryCount)
            {
                stringPool = new ObjectPool(tracer, Convert.ToInt32(indexEntryCount * PoolAllocationMultipliers.StringPool), objectCreator: () => new LazyUTF8String());
                if (bytePool != null)
                {
                    bytePool.UnpinPool();
                }

                bytePool = new BytePool(tracer, indexEntryCount);
            }

            public static void FreePool()
            {
                if (bytePool != null)
                {
                    bytePool.FreeAll();
                }

                if (stringPool != null)
                {
                    stringPool.FreeAll();
                }
            }

            public static int StringPoolSize()
            {
                return stringPool.Size;
            }

            public static int BytePoolSize()
            {
                return bytePool.Size;
            }

            public static void ShrinkPool()
            {
                bytePool.Shrink();
                stringPool.Shrink();
            }

            public static unsafe LazyUTF8String FromByteArray(byte* bufferPtr, int length)
            {
                bytePool.MakeFreeSpace(length);
                LazyUTF8String lazyString = stringPool.GetNew();

                byte* poolPtrForLoop = bytePool.RawPointer + bytePool.FreeIndex;
                byte* bufferPtrForLoop = bufferPtr;
                int index = 0;

                while (index < length)
                {
                    if (*bufferPtrForLoop <= 127)
                    {
                        *poolPtrForLoop = *bufferPtrForLoop;
                    }
                    else
                    {
                        // The string has non-ASCII characters in it, fall back to full parsing
                        lazyString.SetToString(Encoding.UTF8.GetString(bufferPtr, length));
                        return lazyString;
                    }

                    ++poolPtrForLoop;
                    ++bufferPtrForLoop;
                    ++index;
                }

                lazyString.ResetState(bytePool.FreeIndex, length);
                bytePool.AdvanceFreeIndex(length);
                return lazyString;
            }

            public unsafe int Compare(LazyUTF8String other, bool caseSensitive)
            {
                // If we've already converted to a .NET String, use their implementation because it's likely to contain
                // extended characters, which we're not set up to handle below
                if (this.utf16string != null ||
                    other.utf16string != null)
                {
                    return string.Compare(this.GetString(), other.GetString(), caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
                }

                // We now know that both strings are ASCII, because if they had extended characters they would
                // have already been created as string objects

                int minLength = this.length <= other.length ? this.length : other.length;

                byte* thisPtr = bytePool.RawPointer + this.startIndex;
                byte* otherPtr = bytePool.RawPointer + other.startIndex;
                int count = 0;

                // Case-sensitive comparison; always returns and never proceeds to case-insensitive comparison
                if (caseSensitive)
                {
                    while (count < minLength)
                    {
                        if (*thisPtr != *otherPtr)
                        {
                            byte thisC = *thisPtr;
                            byte otherC = *otherPtr;
                            return thisC - otherC;
                        }

                        ++thisPtr;
                        ++otherPtr;
                        ++count;
                    }

                    return this.length - other.length;
                }

                // Case-insensitive comparison
                while (count < minLength)
                {
                    if (*thisPtr != *otherPtr)
                    {
                        byte thisC = *thisPtr;
                        byte otherC = *otherPtr;

                        // The more intuitive approach to checking IsLower() is to do two comparisons to see if c is within the range 'a'-'z'.
                        // However since byte is unsigned, we can rely on underflow to satisfy both conditions with one comparison.
                        //      if c < 'a', (c - 'a') will underflow and become a large positive number, hence > ('z' - 'a')
                        //      if c > 'z', (c - 'a') will naturally be > ('z' - 'a')
                        //      else the condition is satisfied and we know it is lower-case

                        // Note: We only want to do the ToUpper calculation if one char is lower-case and the other char is not.
                        // If they are both lower-case, they can be safely compared as is.

                        //// if (thisC.IsLower())
                        if ((byte)(thisC - 'a') <= 'z' - 'a')
                        {
                            //// if (!otherC.IsLower())
                            if ((byte)(otherC - 'a') > 'z' - 'a')
                            {
                                //// thisC = thisC.ToUpper();
                                thisC -= 'a' - 'A';
                            }
                        }
                        else
                        {
                            //// else, we know !thisC.IsLower()

                            //// if (otherC.IsLower())
                            if ((byte)(otherC - 'a') <= 'z' - 'a')
                            {
                                //// otherC = otherC.ToUpper();
                                otherC -= 'a' - 'A';
                            }
                        }

                        if (thisC != otherC)
                        {
                            return thisC - otherC;
                        }
                    }

                    ++thisPtr;
                    ++otherPtr;
                    ++count;
                }

                return this.length - other.length;
            }

            public unsafe int CaseInsensitiveCompare(LazyUTF8String other)
            {
                return this.Compare(other, caseSensitive: false);
            }

            public unsafe int CaseSensitiveCompare(LazyUTF8String other)
            {
                return this.Compare(other, caseSensitive: true);
            }

            public bool CaseInsensitiveEquals(LazyUTF8String other)
            {
                return this.CaseInsensitiveCompare(other) == 0;
            }

            public bool CaseSensitiveEquals(LazyUTF8String other)
            {
                return this.CaseSensitiveCompare(other) == 0;
            }

            public unsafe string GetString()
            {
                if (this.utf16string == null)
                {
                    // Confirmed earlier that the bytes are all ASCII
                    this.utf16string = Encoding.ASCII.GetString(bytePool.RawPointer + this.startIndex, this.length);
                }

                return this.utf16string;
            }

            private void SetToString(string value)
            {
                this.utf16string = value;

                this.startIndex = -1;
                this.length = -1;
            }

            private void ResetState(int startIndex, int length)
            {
                this.startIndex = startIndex;
                this.length = length;

                this.utf16string = null;
            }

            private class BytePool : ObjectPool
            {
                private GCHandle poolHandle;

                public BytePool(ITracer tracer, uint indexEntryCount)
                    : base(tracer, Convert.ToInt32(indexEntryCount * PoolAllocationMultipliers.BytePool), null)
                {
                }

                public unsafe byte* RawPointer { get; private set; }

                public void MakeFreeSpace(int count)
                {
                    if (this.FreeIndex + count > this.Pool.Length)
                    {
                        this.ExpandPool();
                    }
                }

                public void AdvanceFreeIndex(int length)
                {
                    this.FreeIndex += length;
                }

                public override unsafe void UnpinPool()
                {
                    if (this.poolHandle.IsAllocated)
                    {
                        this.poolHandle.Free();
                        this.RawPointer = (byte*)IntPtr.Zero.ToPointer();
                    }
                }

                protected override unsafe void PinPool()
                {
                    this.poolHandle = GCHandle.Alloc(this.Pool, GCHandleType.Pinned);
                    this.RawPointer = (byte*)this.poolHandle.AddrOfPinnedObject().ToPointer();
                }
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.ObjectPool.cs
================================================
using GVFS.Common.Tracing;
using System;

namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection
    {
        /// 
        /// This class is used to keep an array of objects that can be used.
        /// The size of the array is dynamically increased as objects get used.
        /// The size can be shrunk to eliminate having too many object allocated
        /// This class is not thread safe and is intended to only be used when parsing the git index
        /// which is currently single threaded.
        /// 
        /// The type of object to be stored in the array pool
        internal class ObjectPool
        {
            private const int MinPoolSize = 100;
            private int allocationSize;
            private T[] pool;
            private int freeIndex;
            private Func objectCreator;
            private ITracer tracer;

            public ObjectPool(ITracer tracer, int allocationSize, Func objectCreator)
            {
                if (allocationSize < MinPoolSize)
                {
                    allocationSize = MinPoolSize;
                }

                this.tracer = tracer;
                this.objectCreator = objectCreator;
                this.allocationSize = allocationSize;
                this.pool = new T[0];
                this.ResizePool(allocationSize);
                this.AllocateObjects(startIndex: 0);
            }

            public int ObjectsUsed
            {
                get { return this.freeIndex; }
            }

            public int Size
            {
                get { return this.pool.Length; }
            }

            public int FreeIndex
            {
                get { return this.freeIndex; }
                protected set { this.freeIndex = value; }
            }

            protected T[] Pool
            {
                get { return this.pool; }
            }

            public T GetNew()
            {
                this.EnsureRoomInPool();
                return this.pool[this.freeIndex++];
            }

            public void FreeAll()
            {
                this.freeIndex = 0;
            }

            public void Shrink()
            {
                using (ITracer tracer = this.tracer.StartActivity("ShrinkPool", EventLevel.Informational))
                {
                    EventMetadata metadata = new EventMetadata();
                    metadata.Add("Area", EtwArea);
                    metadata.Add("PoolType", typeof(T).Name);
                    metadata.Add("CurrentSize", this.pool.Length);

                    bool didShrink = false;

                    // Keep extra objects so we don't have to expand on the very next GetNew() call
                    // and make sure that the shrink will reclaim at least a percentage of the objects
                    int shrinkToSize = Convert.ToInt32(this.freeIndex * PoolAllocationMultipliers.ShrinkExtraObjects);
                    if (this.pool.Length * PoolAllocationMultipliers.ShrinkMinPoolSize > shrinkToSize)
                    {
                        this.ResizePool(shrinkToSize);
                        didShrink = true;
                    }

                    metadata.Add(nameof(didShrink), didShrink);
                    metadata.Add(nameof(shrinkToSize), shrinkToSize);

                    tracer.Stop(metadata);
                }
            }

            public virtual void UnpinPool()
            {
            }

            protected void ExpandPool()
            {
                using (ITracer tracer = this.tracer.StartActivity("ExpandPool", EventLevel.Informational))
                {
                    EventMetadata metadata = new EventMetadata();
                    metadata.Add("Area", EtwArea);
                    metadata.Add("PoolType", typeof(T).Name);

                    int previousSize = this.pool.Length;

                    // The values for shrinking and expanding are currently set to prevent the pool from shrinking after expanding
                    //
                    // Example using
                    // ExpandPoolNewObjects = 0.15
                    // ShrinkExtraObjects = 1.1
                    // ShrinkMinPoolSize = 0.9
                    //
                    // Pool at 1000 will get expanded to 1150 and if only one new object is used the free index will be 1001
                    //
                    // The shrink code will check
                    // 1001 * 1.1 = 1101 - shrinkToSize
                    // 1150 * 0.9 = 1035 - shrink threshold
                    // 1035 > 1101 - do not shrink the pool
                    int newObjects = Convert.ToInt32(previousSize * PoolAllocationMultipliers.ExpandPoolNewObjects);

                    // If the previous size of the pool was a lot smaller than what was first allocated and
                    // set as the allocation size, just expand back up to the originally set allocation size
                    if (previousSize * (1 + PoolAllocationMultipliers.ExpandPoolNewObjects) < this.allocationSize)
                    {
                        newObjects = this.allocationSize - previousSize;
                    }

                    this.ResizePool(previousSize + newObjects);

                    this.AllocateObjects(previousSize);

                    metadata.Add("PreviousSize", previousSize);
                    metadata.Add("NewSize", this.pool.Length);
                    tracer.Stop(metadata);
                }
            }

            protected virtual void PinPool()
            {
            }

            private void EnsureRoomInPool()
            {
                if (this.freeIndex >= this.pool.Length)
                {
                    this.ExpandPool();
                }
            }

            private void ResizePool(int newSize)
            {
                this.UnpinPool();
                Array.Resize(ref this.pool, newSize);
                this.PinPool();
            }

            private void AllocateObjects(int startIndex)
            {
                if (this.objectCreator != null)
                {
                    for (int i = startIndex; i < this.pool.Length; i++)
                    {
                        this.pool[i] = this.objectCreator();
                    }
                }
            }
        }
    }
}

================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.PoolAllocationMultipliers.cs
================================================
namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection
    {
        /// 
        /// Multipliers for allocating the various pools while parsing the index.
        /// These numbers come from looking at the allocations needed for various repos
        /// 
        private static class PoolAllocationMultipliers
        {
            public const double FolderDataPool = 0.17;
            public const double FileDataPool = 1.1;
            public const double StringPool = 2.4;
            public const double BytePool = 30;
            public const double ExpandPoolNewObjects = 0.15;

            // Keep 10% extra objects so we don't have to expand on the very next GetNew() call
            public const double ShrinkExtraObjects = 1.1;

            // Make sure that the shrink will reclaim at least 10% of the objects
            public const double ShrinkMinPoolSize = 0.9;
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.SortedFolderEntries.cs
================================================
using GVFS.Common;
using GVFS.Common.Tracing;
using System;
using System.Collections.Generic;

namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection
    {
        /// 
        /// This class stores the list of FolderEntryData objects for a FolderData ChildEntries in sorted order.
        /// The entries can be either FolderData objects or FileData objects in the sortedEntries list.
        /// 
        internal class SortedFolderEntries
        {
            private static ObjectPool folderPool;
            private static ObjectPool filePool;

            private List sortedEntries;

            public SortedFolderEntries()
            {
                this.sortedEntries = new List();
            }

            public int Count
            {
                get { return this.sortedEntries.Count; }
            }

            public FolderEntryData this[int index]
            {
                get
                {
                   return this.sortedEntries[index];
                }
            }

            public static void InitializePools(ITracer tracer, uint indexEntryCount)
            {
                if (folderPool == null)
                {
                    folderPool = new ObjectPool(tracer, Convert.ToInt32(indexEntryCount * PoolAllocationMultipliers.FolderDataPool), () => new FolderData());
                }

                if (filePool == null)
                {
                    filePool = new ObjectPool(tracer, Convert.ToInt32(indexEntryCount * PoolAllocationMultipliers.FileDataPool), () => new FileData());
                }
            }

            public static void ResetPool(ITracer tracer, uint indexEntryCount)
            {
                folderPool = new ObjectPool(tracer, Convert.ToInt32(indexEntryCount * PoolAllocationMultipliers.FolderDataPool), () => new FolderData());
                filePool = new ObjectPool(tracer, Convert.ToInt32(indexEntryCount * PoolAllocationMultipliers.FileDataPool), () => new FileData());
            }

            public static void FreePool()
            {
                if (folderPool != null)
                {
                    folderPool.FreeAll();
                }

                if (filePool != null)
                {
                    filePool.FreeAll();
                }
            }

            public static void ShrinkPool()
            {
                folderPool.Shrink();
                filePool.Shrink();
            }

            public static int FolderPoolSize()
            {
                return folderPool.Size;
            }

            public static int FilePoolSize()
            {
                return filePool.Size;
            }

            public void Clear()
            {
                this.sortedEntries.Clear();
            }

            public FileData AddFile(LazyUTF8String name, byte[] shaBytes)
            {
                int insertionIndex = this.GetInsertionIndex(name);
                return this.InsertFile(name, shaBytes, insertionIndex);
            }

            public FolderData GetOrAddFolder(
                LazyUTF8String[] pathParts,
                int partIndex,
                bool parentIsIncluded,
                SparseFolderData rootSparseFolderData)
            {
                int index = this.GetSortedEntriesIndexOfName(pathParts[partIndex]);
                if (index >= 0)
                {
                    return (FolderData)this.sortedEntries[index];
                }

                bool isIncluded = true;
                if (rootSparseFolderData.Children.Count > 0)
                {
                    if (parentIsIncluded)
                    {
                        // Need to check if this child folder should be included
                        SparseFolderData folderData = rootSparseFolderData;
                        for (int i = 0; i <= partIndex; i++)
                        {
                            if (folderData.IsRecursive)
                            {
                                break;
                            }

                            string childFolderName = pathParts[i].GetString();
                            if (!folderData.Children.ContainsKey(childFolderName))
                            {
                                isIncluded = false;
                                break;
                            }
                            else
                            {
                                folderData = folderData.Children[childFolderName];
                            }
                        }
                    }
                    else
                    {
                        isIncluded = false;
                    }
                }

                return this.InsertFolder(pathParts[partIndex], ~index, isIncluded: isIncluded);
            }

            public bool TryGetValue(LazyUTF8String name, out FolderEntryData value)
            {
                int index = this.GetSortedEntriesIndexOfName(name);
                if (index >= 0)
                {
                    value = this.sortedEntries[index];
                    return true;
                }

                value = null;
                return false;
            }

            private int GetInsertionIndex(LazyUTF8String name)
            {
                int insertionIndex = 0;
                if (this.sortedEntries.Count != 0)
                {
                    insertionIndex = this.GetSortedEntriesIndexOfName(name);
                    if (insertionIndex >= 0)
                    {
                        throw new InvalidOperationException($"All entries should be unique, non-unique entry: {name.GetString()}");
                    }

                    // When the name is not found the returned value is the bitwise complement of
                    // where the name should be inserted to keep the sortedEntries in sorted order
                    insertionIndex = ~insertionIndex;
                }

                return insertionIndex;
            }

            private FolderData InsertFolder(LazyUTF8String name, int insertionIndex, bool isIncluded)
            {
                FolderData data = folderPool.GetNew();
                data.ResetData(name, isIncluded);
                this.sortedEntries.Insert(insertionIndex, data);
                return data;
            }

            private FileData InsertFile(LazyUTF8String name, byte[] shaBytes, int insertionIndex)
            {
                FileData data = filePool.GetNew();
                data.ResetData(name, shaBytes);
                this.sortedEntries.Insert(insertionIndex, data);
                return data;
            }

            /// 
            /// Get the index of the name in the sorted folder entries list
            /// 
            /// The name to search for in the entries
            /// 
            /// The zero based index of the entry if found;
            /// otherwise, a negative number that is the bitwise complement of the index of the next element that is larger than item or,
            /// if there is no larger element, the bitwise complement of this.entries.ObjectsUsed.
            /// 
            private int GetSortedEntriesIndexOfName(LazyUTF8String name)
            {
                if (this.sortedEntries.Count == 0)
                {
                    return -1;
                }

                // Insertions are almost always at the end, because the inputs are pre-sorted by git.
                // We only have to insert at a different spot where Windows/Mac and git disagree on the sort order;
                // on Linux we use a case-sensitive comparsion, which we expect to align with git.
                bool caseSensitive = GVFSPlatform.Instance.Constants.CaseSensitiveFileSystem;
                int compareResult = this.sortedEntries[this.sortedEntries.Count - 1].Name.Compare(name, caseSensitive);
                if (compareResult == 0)
                {
                    return this.sortedEntries.Count - 1;
                }
                else if (compareResult < 0)
                {
                    return ~this.sortedEntries.Count;
                }

                int left = 0;
                int right = this.sortedEntries.Count - 2;

                while (right - left > 2)
                {
                    int middle = left + ((right - left) / 2);
                    int comparison = this.sortedEntries[middle].Name.Compare(name, caseSensitive);

                    if (comparison == 0)
                    {
                        return middle;
                    }

                    if (comparison < 0)
                    {
                        left = middle + 1;
                    }
                    else
                    {
                        right = middle - 1;
                    }
                }

                for (int i = right; i >= left; i--)
                {
                    compareResult = this.sortedEntries[i].Name.Compare(name, caseSensitive);
                    if (compareResult == 0)
                    {
                        return i;
                    }
                    else if (compareResult < 0)
                    {
                        return ~(i + 1);
                    }
                }

                return ~left;
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.SparseFolder.cs
================================================
using GVFS.Common;
using System;
using System.Collections.Generic;

namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection
    {
        /// 
        /// This class is used to represent what has been added to sparse set of folders
        /// It is build in the RefreshSparseFolders method in the GitIndexProjection
        /// The last folder in the sparse entry will be marked with IsRecursive = true
        /// This is ONLY what is in the sparse folder set and NOT what is on disk or in
        /// the index for a folder
        /// 
        /// 
        /// For sparse folder entries of:
        /// foo/example
        /// other
        ///
        /// The SparseFolderData would be:
        /// root
        /// Children:
        /// |- foo (IsRecursive = false, Depth = 0)
        /// |  Children:
        /// |  |- example (IsRecursive = true, Depth = 1)
        /// |
        /// |- other (IsRecursive = true, Depth = 0)
        /// 
        internal class SparseFolderData
        {
            public SparseFolderData()
            {
                this.Children = new Dictionary(GVFSPlatform.Instance.Constants.PathComparer);
            }

            public bool IsRecursive { get; set; }
            public int Depth { get; set; }
            public Dictionary Children { get; }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/GitIndexProjection.cs
================================================
using GVFS.Common;
using GVFS.Common.Database;
using GVFS.Common.Git;
using GVFS.Common.Http;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Virtualization.Background;
using GVFS.Virtualization.BlobSize;
using GVFS.Virtualization.FileSystem;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace GVFS.Virtualization.Projection
{
    public partial class GitIndexProjection : IDisposable, IProfilerOnlyIndexProjection
    {
        public const string ProjectionIndexBackupName = "GVFS_projection";

        public static readonly ushort FileMode755 = Convert.ToUInt16("755", 8);
        public static readonly ushort FileMode664 = Convert.ToUInt16("664", 8);
        public static readonly ushort FileMode644 = Convert.ToUInt16("644", 8);

        private const int IndexFileStreamBufferSize = 512 * 1024;

        private const UpdatePlaceholderType FolderPlaceholderDeleteFlags =
            UpdatePlaceholderType.AllowDirtyMetadata |
            UpdatePlaceholderType.AllowReadOnly |
            UpdatePlaceholderType.AllowTombstone;

        private const UpdatePlaceholderType FilePlaceholderUpdateFlags =
            UpdatePlaceholderType.AllowDirtyMetadata |
            UpdatePlaceholderType.AllowReadOnly;

        private const string EtwArea = "GitIndexProjection";

        private GVFSContext context;
        private RepoMetadata repoMetadata;
        private FileSystemVirtualizer fileSystemVirtualizer;
        private ModifiedPathsDatabase modifiedPaths;

        private FolderData rootFolderData = new FolderData();
        private GitIndexParser indexParser;

        // Cache of folder paths (in Windows format) to folder data
        private ConcurrentDictionary projectionFolderCache = new ConcurrentDictionary(GVFSPlatform.Instance.Constants.PathComparer);

        // nonDefaultFileTypesAndModes is only populated when the platform supports file mode
        // On platforms that support file modes, file paths that are not in nonDefaultFileTypesAndModes are regular files with mode 644
        private Dictionary nonDefaultFileTypesAndModes = new Dictionary(GVFSPlatform.Instance.Constants.PathComparer);

        private BlobSizes blobSizes;
        private IPlaceholderCollection placeholderDatabase;
        private ISparseCollection sparseCollection;
        private SparseFolderData rootSparseFolder;
        private GVFSGitObjects gitObjects;
        private BackgroundFileSystemTaskRunner backgroundFileSystemTaskRunner;
        private ReaderWriterLockSlim projectionReadWriteLock;
        private ManualResetEventSlim projectionParseComplete;

        private volatile bool projectionInvalid;

        // Number of times that the negative path cache has (potentially) been updated by GVFS preventing
        // git from creating a placeholder (since the last time the cache was cleared)
        private int negativePathCacheUpdatedForGitCount;

        // modifiedFilesInvalid: If true, a change to the index that did not trigger a new projection
        // has been made and GVFS has not yet validated that all entries whose skip-worktree bit is
        // cleared are in the ModifiedFilesDatabase
        private volatile bool modifiedFilesInvalid;

        private ConcurrentHashSet updatePlaceholderFailures;
        private ConcurrentHashSet deletePlaceholderFailures;

        private string projectionIndexBackupPath;
        private string indexPath;

        private FileStream indexFileStream;

        private AutoResetEvent wakeUpIndexParsingThread;
        private Task indexParsingThread;
        private bool isStopping;
        private bool updateUsnJournal;

        public GitIndexProjection(
            GVFSContext context,
            GVFSGitObjects gitObjects,
            BlobSizes blobSizes,
            RepoMetadata repoMetadata,
            FileSystemVirtualizer fileSystemVirtualizer,
            IPlaceholderCollection placeholderDatabase,
            ISparseCollection sparseCollection,
            ModifiedPathsDatabase modifiedPaths)
        {
            this.context = context;
            this.gitObjects = gitObjects;
            this.blobSizes = blobSizes;
            this.repoMetadata = repoMetadata;
            this.fileSystemVirtualizer = fileSystemVirtualizer;
            this.indexParser = new GitIndexParser(this);

            this.projectionReadWriteLock = new ReaderWriterLockSlim();
            this.projectionParseComplete = new ManualResetEventSlim(initialState: false);
            this.wakeUpIndexParsingThread = new AutoResetEvent(initialState: false);
            this.projectionIndexBackupPath = Path.Combine(this.context.Enlistment.DotGVFSRoot, ProjectionIndexBackupName);
            this.indexPath = this.context.Enlistment.GitIndexPath;
            this.placeholderDatabase = placeholderDatabase;
            this.sparseCollection = sparseCollection;
            this.modifiedPaths = modifiedPaths;
            this.rootSparseFolder = new SparseFolderData();
            this.ClearProjectionCaches();

            LocalGVFSConfig config = new LocalGVFSConfig();
            if (config.TryGetConfig(GVFSConstants.LocalGVFSConfig.USNJournalUpdates, out string value, out string error))
            {
                bool.TryParse(value, out this.updateUsnJournal);
            }
        }

        // For Unit Testing
        protected GitIndexProjection()
        {
        }

        public enum FileType : short
        {
            Invalid,

            Regular,
            SymLink,
            GitLink,
        }

        public enum PathSparseState
        {
            NotFound,
            Included,
            Excluded,
        }

        public static void ReadIndex(ITracer tracer, string indexPath)
        {
            using (FileStream indexStream = new FileStream(indexPath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, IndexFileStreamBufferSize))
            {
                GitIndexParser.ValidateIndex(tracer, indexStream);
            }
        }

        /// 
        /// Force the index file to be parsed and a new projection collection to be built.
        /// This method should only be used to measure index parsing performance.
        /// 
        void IProfilerOnlyIndexProjection.ForceRebuildProjection()
        {
            this.CopyIndexFileAndBuildProjection();
        }

        /// 
        /// Force the index file to be parsed to add missing paths to the modified paths database.
        /// This method should only be used to measure index parsing performance.
        /// 
        void IProfilerOnlyIndexProjection.ForceAddMissingModifiedPaths(ITracer tracer)
        {
            using (FileStream indexStream = new FileStream(this.indexPath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, IndexFileStreamBufferSize))
            {
                // Not checking the FileSystemTaskResult here because this is only for profiling
                this.indexParser.AddMissingModifiedFilesAndRemoveThemFromPlaceholderList(tracer, indexStream);
            }
        }

        public void BuildProjectionFromPath(ITracer tracer, string indexPath)
        {
            using (FileStream indexStream = new FileStream(indexPath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, IndexFileStreamBufferSize))
            {
                this.indexParser.RebuildProjection(tracer, indexStream);
            }
        }

        public virtual void Initialize(BackgroundFileSystemTaskRunner backgroundFileSystemTaskRunner)
        {
            if (!File.Exists(this.indexPath))
            {
                string message = "GVFS requires the .git\\index to exist";
                EventMetadata metadata = CreateEventMetadata();
                this.context.Tracer.RelatedError(metadata, message);
                throw new FileNotFoundException(message);
            }

            this.backgroundFileSystemTaskRunner = backgroundFileSystemTaskRunner;

            this.projectionReadWriteLock.EnterWriteLock();
            try
            {
                this.projectionInvalid = this.repoMetadata.GetProjectionInvalid();

                if (!this.context.FileSystem.FileExists(this.projectionIndexBackupPath) || this.projectionInvalid)
                {
                    this.CopyIndexFileAndBuildProjection();
                }
                else
                {
                    this.BuildProjection();
                }
            }
            finally
            {
                this.projectionReadWriteLock.ExitWriteLock();
            }

            this.ClearUpdatePlaceholderErrors();
            if (this.repoMetadata.GetPlaceholdersNeedUpdate())
            {
                this.UpdatePlaceholders();
            }

            // If somehow something invalidated the projection while we were initializing, the parsing thread will
            // pick it up and parse again
            if (!this.projectionInvalid)
            {
                this.projectionParseComplete.Set();
            }

            this.indexParsingThread = Task.Factory.StartNew(this.ParseIndexThreadMain, TaskCreationOptions.LongRunning);
        }

        public virtual void Shutdown()
        {
            this.isStopping = true;
            this.wakeUpIndexParsingThread.Set();
            this.indexParsingThread.Wait();
        }

        public void WaitForProjectionUpdate()
        {
            this.projectionParseComplete.Wait();
        }

        public NamedPipeMessages.ReleaseLock.Response TryReleaseExternalLock(int pid)
        {
            NamedPipeMessages.LockData externalHolder = this.context.Repository.GVFSLock.GetExternalHolder();
            if (externalHolder != null &&
                externalHolder.PID == pid)
            {
                // We MUST NOT release the lock until all processing has been completed, so that once
                // control returns to the user, the projection is in a consistent state

                this.context.Tracer.RelatedEvent(EventLevel.Informational, "ReleaseExternalLockRequested", null);
                this.context.Repository.GVFSLock.Stats.RecordReleaseExternalLockRequested();

                this.ClearNegativePathCacheIfPollutedByGit();

                ConcurrentHashSet updateFailures = this.updatePlaceholderFailures;
                ConcurrentHashSet deleteFailures = this.deletePlaceholderFailures;
                this.ClearUpdatePlaceholderErrors();

                if (this.context.Repository.GVFSLock.ReleaseLockHeldByExternalProcess(pid))
                {
                    if (updateFailures.Count > 0 || deleteFailures.Count > 0)
                    {
                        return new NamedPipeMessages.ReleaseLock.Response(
                            NamedPipeMessages.ReleaseLock.SuccessResult,
                            new NamedPipeMessages.ReleaseLock.ReleaseLockData(
                                new List(updateFailures),
                                new List(deleteFailures)));
                    }

                    return new NamedPipeMessages.ReleaseLock.Response(NamedPipeMessages.ReleaseLock.SuccessResult);
                }
            }

            EventMetadata metadata = new EventMetadata();
            metadata.Add("LockStatus", this.context.Repository.GVFSLock.GetStatus());
            metadata.Add("ReleaseCallerPID", pid);

            try
            {
                using (Process process = Process.GetProcessById(pid))
                {
                    metadata.Add("ReleaseCallerName", process.ProcessName);
                    metadata.Add("ReleaseCallerArgs", process.StartInfo.Arguments);
                }
            }
            catch (Exception)
            {
            }

            if (externalHolder != null)
            {
                metadata.Add("ExternalHolder", externalHolder.ParsedCommand);
                metadata.Add("ExternalHolderPID", externalHolder.PID);
            }
            else
            {
                metadata.Add("ExternalHolder", string.Empty);
            }

            this.context.Tracer.RelatedError(metadata, "GitIndexProjection: Received a release request from a process that does not own the lock");
            return new NamedPipeMessages.ReleaseLock.Response(NamedPipeMessages.ReleaseLock.FailureResult);
        }

        public virtual bool IsProjectionParseComplete()
        {
            return this.projectionParseComplete.IsSet;
        }

        /// 
        /// Get the total number of directories in the projection.
        /// This is computed from the in-memory tree built during index parsing,
        /// so it is essentially free (no I/O, no process spawn).
        /// 
        public virtual int GetProjectedFolderCount()
        {
            this.projectionReadWriteLock.EnterReadLock();
            try
            {
                return this.rootFolderData.GetRecursiveFolderCount();
            }
            finally
            {
                this.projectionReadWriteLock.ExitReadLock();
            }
        }

        /// 
        /// Count unique directories by parsing the index file directly.
        /// This is a fallback for when the in-memory projection is not available
        /// (e.g., when running gvfs health --status without a mount process).
        /// 
        public static int CountIndexFolders(ITracer tracer, string indexPath)
        {
            using (FileStream indexStream = new FileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                return CountIndexFolders(tracer, indexStream);
            }
        }

        /// 
        /// Count unique directories by parsing an index stream.
        /// 
        public static int CountIndexFolders(ITracer tracer, Stream indexStream)
        {
            return GitIndexParser.CountIndexFolders(tracer, indexStream);
        }

        public virtual void InvalidateProjection()
        {
            this.context.Tracer.RelatedEvent(EventLevel.Informational, "InvalidateProjection", null);

            this.projectionParseComplete.Reset();

            try
            {
                // Because the projection is now invalid, attempt to delete the projection file.  If this delete fails
                // replacing the projection will be handled by the parsing thread
                this.context.FileSystem.DeleteFile(this.projectionIndexBackupPath);
            }
            catch (Exception e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.InvalidateProjection) + ": Failed to delete GVFS_Projection file");
                this.context.Tracer.RelatedEvent(EventLevel.Informational, nameof(this.InvalidateProjection) + "_FailedToDeleteProjection", metadata);
            }

            this.SetProjectionAndPlaceholdersAsInvalid();
            this.wakeUpIndexParsingThread.Set();
        }

        public void InvalidateModifiedFiles()
        {
            this.context.Tracer.RelatedEvent(EventLevel.Informational, "ModifiedFilesInvalid", null);
            this.modifiedFilesInvalid = true;
        }

        public void OnPlaceholderCreateBlockedForGit()
        {
            int count = Interlocked.Increment(ref this.negativePathCacheUpdatedForGitCount);
            if (count == 1)
            {
                // If placeholder creation is blocked multiple times, only queue a single background task
                this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnPlaceholderCreationsBlockedForGit());
            }
        }

        public void ClearNegativePathCacheIfPollutedByGit()
        {
            int count = Interlocked.Exchange(ref this.negativePathCacheUpdatedForGitCount, 0);
            if (count > 0)
            {
                this.ClearNegativePathCache();
            }
        }

        public void OnPlaceholderFolderCreated(string virtualPath)
        {
            string sha = null;
            if (this.updateUsnJournal && this.TryGetFolderDataFromTreeUsingPath(virtualPath, out FolderData folderData))
            {
                sha = folderData.HashedChildrenNamesSha();
            }

            this.placeholderDatabase.AddPartialFolder(virtualPath, sha);
        }

        public void OnPossibleTombstoneFolderCreated(string virtualPath)
        {
            this.placeholderDatabase.AddPossibleTombstoneFolder(virtualPath);
        }

        public virtual void OnPlaceholderFolderExpanded(string relativePath)
        {
            this.placeholderDatabase.AddExpandedFolder(relativePath);
        }

        public virtual void OnPlaceholderFileCreated(string virtualPath, string sha)
        {
            this.placeholderDatabase.AddFile(virtualPath, sha);
        }

        public virtual bool TryGetProjectedItemsFromMemory(string folderPath, out List projectedItems)
        {
            projectedItems = null;

            this.projectionReadWriteLock.EnterReadLock();

            try
            {
                FolderData folderData;
                if (this.TryGetOrAddFolderDataFromCache(folderPath, out folderData))
                {
                    if (folderData.ChildrenHaveSizes)
                    {
                        projectedItems = ConvertToProjectedFileInfos(folderData.ChildEntries);
                        return true;
                    }
                }

                return false;
            }
            finally
            {
                this.projectionReadWriteLock.ExitReadLock();
            }
        }

        public virtual void GetFileTypeAndMode(string filePath, out FileType fileType, out ushort fileMode)
        {
            if (!GVFSPlatform.Instance.FileSystem.SupportsFileMode)
            {
                throw new InvalidOperationException($"{nameof(this.GetFileTypeAndMode)} is only supported on GVFSPlatforms that support file mode");
            }

            fileType = FileType.Regular;
            fileMode = FileMode644;

            this.projectionReadWriteLock.EnterReadLock();

            try
            {
                FileTypeAndMode fileTypeAndMode;
                if (this.nonDefaultFileTypesAndModes.TryGetValue(filePath, out fileTypeAndMode))
                {
                    fileType = fileTypeAndMode.Type;
                    fileMode = fileTypeAndMode.Mode;
                }
            }
            finally
            {
                this.projectionReadWriteLock.ExitReadLock();
            }
        }

        /// 
        /// Gets the projected items within the specified folder.
        /// 
        /// Cancellation token
        /// 
        /// BlobSizes database connection, if null file sizes will not be populated
        /// 
        /// Path of the folder relative to the repo's root
        public virtual List GetProjectedItems(
            CancellationToken cancellationToken,
            BlobSizes.BlobSizesConnection blobSizesConnection,
            string folderPath)
        {
            this.projectionReadWriteLock.EnterReadLock();

            try
            {
                FolderData folderData;
                if (this.TryGetOrAddFolderDataFromCache(folderPath, out folderData))
                {
                    if (blobSizesConnection != null)
                    {
                        folderData.PopulateSizes(
                            this.context.Tracer,
                            this.gitObjects,
                            blobSizesConnection,
                            availableSizes: null,
                            cancellationToken: cancellationToken);
                    }

                    return ConvertToProjectedFileInfos(folderData.ChildEntries);
                }

                return new List();
            }
            finally
            {
                this.projectionReadWriteLock.ExitReadLock();
            }
        }

        public virtual PathSparseState GetFolderPathSparseState(string virtualPath)
        {
            // Have to use a call that will get excluded entries in order to return the Excluded state
            // Excluded folders are not in the cache and GetProjectedFolderEntryData will not return them
            if (this.TryGetFolderDataFromTreeUsingPath(virtualPath, out FolderData folderData))
            {
                return folderData.IsIncluded ? PathSparseState.Included : PathSparseState.Excluded;
            }

            return PathSparseState.NotFound;
        }

        public virtual bool TryAddSparseFolder(string virtualPath)
        {
            try
            {
                // Have to use a call that will get excluded entries in order to return the Excluded state
                // Excluded folders are not in the cache and GetProjectedFolderEntryData will not return them
                if (this.TryGetFolderDataFromTreeUsingPath(virtualPath, out FolderData folderData) &&
                    !folderData.IsIncluded)
                {
                    folderData.Include();
                    this.sparseCollection.Add(virtualPath);
                    return true;
                }

                return false;
            }
            catch (GVFSDatabaseException ex)
            {
                this.context.Tracer.RelatedWarning($"{nameof(this.TryAddSparseFolder)} failed for {virtualPath}.  {ex.ToString()}");
                return false;
            }
        }

        public virtual bool IsPathProjected(string virtualPath, out string fileName, out bool isFolder)
        {
            isFolder = false;
            string parentKey;
            this.GetChildNameAndParentKey(virtualPath, out fileName, out parentKey);

            // GetProjectedFolderEntryData returns a null FolderEntryData when the path's parent folder IsIncluded is false
            FolderEntryData data = this.GetProjectedFolderEntryData(
                blobSizesConnection: null,
                childName: fileName,
                parentKey: parentKey);

            if (data != null)
            {
                isFolder = data.IsFolder;
                return true;
            }

            return false;
        }

        public virtual ProjectedFileInfo GetProjectedFileInfo(
            CancellationToken cancellationToken,
            BlobSizes.BlobSizesConnection blobSizesConnection,
            string virtualPath,
            out string parentFolderPath)
        {
            string childName;
            string parentKey;
            this.GetChildNameAndParentKey(virtualPath, out childName, out parentKey);
            parentFolderPath = parentKey;
            string gitCasedChildName;

            // GetProjectedFolderEntryData returns a null FolderEntryData when the path's parent folder IsIncluded is false
            FolderEntryData data = this.GetProjectedFolderEntryData(
                cancellationToken,
                blobSizesConnection,
                availableSizes: null,
                childName: childName,
                parentKey: parentKey,
                gitCasedChildName: out gitCasedChildName);

            if (data != null)
            {
                if (data.IsFolder)
                {
                    return new ProjectedFileInfo(gitCasedChildName, size: 0, isFolder: true, sha: Sha1Id.None);
                }
                else
                {
                    FileData fileData = (FileData)data;
                    return new ProjectedFileInfo(gitCasedChildName, fileData.Size, isFolder: false, sha: fileData.Sha);
                }
            }

            return null;
        }

        public virtual FileSystemTaskResult OpenIndexForRead()
        {
            if (!File.Exists(this.indexPath))
            {
                EventMetadata metadata = CreateEventMetadata();
                this.context.Tracer.RelatedError(metadata, "AcquireIndexLockAndOpenForWrites: Can't open the index because it doesn't exist");

                return FileSystemTaskResult.FatalError;
            }

            this.projectionParseComplete.Wait();

            FileSystemTaskResult result = FileSystemTaskResult.FatalError;
            try
            {
                this.indexFileStream = new FileStream(this.indexPath, FileMode.Open, FileAccess.Read, FileShare.Read, IndexFileStreamBufferSize);
                result = FileSystemTaskResult.Success;
            }
            catch (IOException e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                this.context.Tracer.RelatedWarning(metadata, "IOException in AcquireIndexLockAndOpenForWrites (Retryable)");
                result = FileSystemTaskResult.RetryableError;
            }
            catch (Exception e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                this.context.Tracer.RelatedError(metadata, "Exception in AcquireIndexLockAndOpenForWrites (FatalError)");
                result = FileSystemTaskResult.FatalError;
            }

            return result;
        }

        public FileSystemTaskResult CloseIndex()
        {
            if (this.indexFileStream != null)
            {
                this.indexFileStream.Dispose();
                this.indexFileStream = null;
            }

            return FileSystemTaskResult.Success;
        }

        public FileSystemTaskResult AddMissingModifiedFiles()
        {
            try
            {
                if (this.modifiedFilesInvalid)
                {
                    using (ITracer activity = this.context.Tracer.StartActivity(
                        nameof(this.indexParser.AddMissingModifiedFilesAndRemoveThemFromPlaceholderList),
                        EventLevel.Informational))
                    {
                        FileSystemTaskResult result = this.indexParser.AddMissingModifiedFilesAndRemoveThemFromPlaceholderList(
                            activity,
                            this.indexFileStream);

                        if (result == FileSystemTaskResult.Success)
                        {
                            this.modifiedFilesInvalid = false;
                        }

                        return result;
                    }
                }
            }
            catch (IOException e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                this.context.Tracer.RelatedWarning(metadata, "IOException in " + nameof(this.AddMissingModifiedFiles) + " (Retryable)");

                return FileSystemTaskResult.RetryableError;
            }
            catch (Exception e)
            {
                EventMetadata metadata = CreateEventMetadata(e);
                this.context.Tracer.RelatedError(metadata, "Exception in " + nameof(this.AddMissingModifiedFiles) + " (FatalError)");

                return FileSystemTaskResult.FatalError;
            }

            return FileSystemTaskResult.Success;
        }

        public void RemoveFromPlaceholderList(string fileOrFolderPath)
        {
            this.placeholderDatabase.Remove(fileOrFolderPath);
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.projectionReadWriteLock != null)
                {
                    this.projectionReadWriteLock.Dispose();
                    this.projectionReadWriteLock = null;
                }

                if (this.projectionParseComplete != null)
                {
                    this.projectionParseComplete.Dispose();
                    this.projectionParseComplete = null;
                }

                if (this.wakeUpIndexParsingThread != null)
                {
                    this.wakeUpIndexParsingThread.Dispose();
                    this.wakeUpIndexParsingThread = null;
                }

                if (this.indexParsingThread != null)
                {
                    this.indexParsingThread.Dispose();
                    this.indexParsingThread = null;
                }
            }
        }

        protected void GetChildNameAndParentKey(string virtualPath, out string childName, out string parentKey)
        {
            parentKey = string.Empty;

            int separatorIndex = virtualPath.LastIndexOf(Path.DirectorySeparatorChar);
            if (separatorIndex < 0)
            {
                childName = virtualPath;
                return;
            }

            childName = virtualPath.Substring(separatorIndex + 1);
            parentKey = virtualPath.Substring(0, separatorIndex);
        }

        private static EventMetadata CreateEventMetadata(Exception e = null)
        {
            EventMetadata metadata = new EventMetadata();
            metadata.Add("Area", EtwArea);
            if (e != null)
            {
                metadata.Add("Exception", e.ToString());
            }

            return metadata;
        }

        private static List ConvertToProjectedFileInfos(SortedFolderEntries sortedFolderEntries)
        {
            List childItems = new List(sortedFolderEntries.Count);
            FolderEntryData childEntry;
            for (int i = 0; i < sortedFolderEntries.Count; i++)
            {
                childEntry = sortedFolderEntries[i];

                if (childEntry.IsFolder)
                {
                    FolderData folderData = (FolderData)childEntry;
                    if (folderData.IsIncluded)
                    {
                        childItems.Add(new ProjectedFileInfo(childEntry.Name.GetString(), size: 0, isFolder: true, sha: Sha1Id.None));
                    }
                }
                else
                {
                    FileData fileData = (FileData)childEntry;
                    childItems.Add(new ProjectedFileInfo(fileData.Name.GetString(), fileData.Size, isFolder: false, sha: fileData.Sha));
                }
            }

            return childItems;
        }

        private void AddItemFromIndexEntry(GitIndexEntry indexEntry)
        {
            if (indexEntry.BuildingProjection_HasSameParentAsLastEntry)
            {
                indexEntry.BuildingProjection_LastParent.AddChildFile(indexEntry.BuildingProjection_GetChildName(), indexEntry.Sha);
            }
            else
            {
                if (indexEntry.BuildingProjection_NumParts == 1)
                {
                    indexEntry.BuildingProjection_LastParent = this.rootFolderData;
                    indexEntry.BuildingProjection_LastParent.AddChildFile(indexEntry.BuildingProjection_GetChildName(), indexEntry.Sha);
                }
                else
                {
                    indexEntry.BuildingProjection_LastParent = this.AddFileToTree(indexEntry);
                }
            }

            if (GVFSPlatform.Instance.FileSystem.SupportsFileMode)
            {
                if (indexEntry.TypeAndMode.Type != FileType.Regular ||
                    indexEntry.TypeAndMode.Mode != FileMode644)
                {
                    this.nonDefaultFileTypesAndModes.Add(indexEntry.BuildingProjection_GetGitRelativePath(), indexEntry.TypeAndMode);
                }
            }
        }

        private FileSystemTaskResult AddModifiedPath(string path)
        {
            bool wasAdded = this.modifiedPaths.TryAdd(path, isFolder: false, isRetryable: out bool isRetryable);
            if (!wasAdded)
            {
                return isRetryable ? FileSystemTaskResult.RetryableError : FileSystemTaskResult.FatalError;
            }

            return FileSystemTaskResult.Success;
        }

        private void ClearProjectionCaches()
        {
            SortedFolderEntries.FreePool();
            LazyUTF8String.FreePool();
            this.projectionFolderCache.Clear();
            this.nonDefaultFileTypesAndModes.Clear();
            this.RefreshSparseFolders();
            this.rootFolderData.ResetData(new LazyUTF8String(""), isIncluded: true);
        }

        private void RefreshSparseFolders()
        {
            this.rootSparseFolder.Children.Clear();
            if (this.sparseCollection != null)
            {
                Dictionary parentFolder = this.rootSparseFolder.Children;
                foreach (string directoryPath in this.sparseCollection.GetAll())
                {
                    string[] folders = directoryPath.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
                    for (int i = 0; i < folders.Length; i++)
                    {
                        SparseFolderData folderData;
                        if (!parentFolder.ContainsKey(folders[i]))
                        {
                            folderData = new SparseFolderData();
                            folderData.Depth = i;
                            parentFolder.Add(folders[i], folderData);
                        }
                        else
                        {
                            folderData = parentFolder[folders[i]];
                        }

                        if (!folderData.IsRecursive)
                        {
                            // The last folder needs to be recursive
                            folderData.IsRecursive = (i == folders.Length - 1);
                        }

                        parentFolder = folderData.Children;
                    }

                    parentFolder = this.rootSparseFolder.Children;
                }
            }
        }

        private bool TryGetSha(string childName, string parentKey, out string sha)
        {
            sha = string.Empty;

            // GetProjectedFolderEntryData returns a null FolderEntryData when the path's parent folder IsIncluded is false
            FileData data = this.GetProjectedFolderEntryData(
                blobSizesConnection: null,
                childName: childName,
                parentKey: parentKey) as FileData;

            if (data != null && !data.IsFolder)
            {
                sha = data.ConvertShaToString();
                return true;
            }

            return false;
        }

        private void SetProjectionInvalid(bool isInvalid)
        {
            this.projectionInvalid = isInvalid;
            this.repoMetadata.SetProjectionInvalid(isInvalid);
        }

        private void SetProjectionAndPlaceholdersAsInvalid()
        {
            this.projectionInvalid = true;
            this.repoMetadata.SetProjectionInvalidAndPlaceholdersNeedUpdate();
        }

        private string GetParentKey(string gitPath, out int pathSeparatorIndex)
        {
            string parentKey = string.Empty;
            pathSeparatorIndex = gitPath.LastIndexOf(GVFSConstants.GitPathSeparator);
            if (pathSeparatorIndex >= 0)
            {
                parentKey = gitPath.Substring(0, pathSeparatorIndex);
            }

            return parentKey;
        }

        private string GetChildName(string gitPath, int pathSeparatorIndex)
        {
            if (pathSeparatorIndex < 0)
            {
                return gitPath;
            }

            return gitPath.Substring(pathSeparatorIndex + 1);
        }

        /// 
        /// Add FolderData and FileData objects to the tree needed for the current index entry
        /// 
        /// GitIndexEntry used to create the child's path
        /// The FolderData for childData's parent
        /// This method will create and add any intermediate FolderDatas that are
        /// required but not already in the tree.  For example, if the tree was completely empty
        /// and AddFileToTree was called for the path \A\B\C.txt:
        ///
        ///    pathParts -> { "A", "B", "C.txt"}
        ///
        ///    AddFileToTree would create new FolderData entries in the tree for "A" and "B"
        ///    and return the FolderData entry for "B"
        /// 
        private FolderData AddFileToTree(GitIndexEntry indexEntry)
        {
            FolderData parentFolder = this.rootFolderData;
            for (int pathIndex = 0; pathIndex < indexEntry.BuildingProjection_NumParts - 1; ++pathIndex)
            {
                if (parentFolder == null)
                {
                    string parentFolderName;
                    if (pathIndex > 0)
                    {
                        parentFolderName = indexEntry.BuildingProjection_PathParts[pathIndex - 1].GetString();
                    }
                    else
                    {
                        parentFolderName = this.rootFolderData.Name.GetString();
                    }

                    string gitPath = indexEntry.BuildingProjection_GetGitRelativePath();

                    EventMetadata metadata = CreateEventMetadata();
                    metadata.Add("gitPath", gitPath);
                    metadata.Add("parentFolder", parentFolderName);
                    this.context.Tracer.RelatedError(metadata, "AddFileToTree: Found a file where a folder was expected");

                    throw new InvalidDataException("Found a file (" + parentFolderName + ") where a folder was expected: " + gitPath);
                }

                parentFolder = parentFolder.ChildEntries.GetOrAddFolder(indexEntry.BuildingProjection_PathParts, pathIndex, parentFolder.IsIncluded, this.rootSparseFolder);
            }

            parentFolder.AddChildFile(indexEntry.BuildingProjection_PathParts[indexEntry.BuildingProjection_NumParts - 1], indexEntry.Sha);

            return parentFolder;
        }

        private FolderEntryData GetProjectedFolderEntryData(
            CancellationToken cancellationToken,
            BlobSizes.BlobSizesConnection blobSizesConnection,
            Dictionary availableSizes,
            string childName,
            string parentKey,
            out string gitCasedChildName)
        {
            this.projectionReadWriteLock.EnterReadLock();
            try
            {
                FolderData parentFolderData;
                if (this.TryGetOrAddFolderDataFromCache(parentKey, out parentFolderData))
                {
                    LazyUTF8String child = new LazyUTF8String(childName);
                    FolderEntryData childData;
                    if (parentFolderData.ChildEntries.TryGetValue(child, out childData) && (!childData.IsFolder || ((FolderData)childData).IsIncluded))
                    {
                        gitCasedChildName = childData.Name.GetString();

                        if (blobSizesConnection != null && !childData.IsFolder)
                        {
                            FileData fileData = (FileData)childData;
                            if (!fileData.IsSizeSet() && !fileData.TryPopulateSizeLocally(this.context.Tracer, this.gitObjects, blobSizesConnection, availableSizes, out string _))
                            {
                                Stopwatch queryTime = Stopwatch.StartNew();
                                parentFolderData.PopulateSizes(this.context.Tracer, this.gitObjects, blobSizesConnection, availableSizes, cancellationToken);
                                this.context.Repository.GVFSLock.Stats.RecordSizeQuery(queryTime.ElapsedMilliseconds);
                            }
                        }

                        return childData;
                    }
                }

                gitCasedChildName = string.Empty;
                return null;
            }
            finally
            {
                this.projectionReadWriteLock.ExitReadLock();
            }
        }

        /// 
        /// Get the FolderEntryData for the specified child name and parent key.
        /// 
        /// 
        /// BlobSizesConnection used to lookup the size for the FolderEntryData.  If null, size will not be populated.
        /// 
        /// Child name (i.e. file name)
        /// Parent key (parent folder path)
        /// 
        /// FolderEntryData for the specified childName and parentKey or null if no FolderEntryData exists for them in the projection.
        /// This will not return entries where IsIncluded is false
        /// 
        ///  can be used for getting child name and parent key from a file path
        private FolderEntryData GetProjectedFolderEntryData(
            BlobSizes.BlobSizesConnection blobSizesConnection,
            string childName,
            string parentKey)
        {
            string casedChildName;
            return this.GetProjectedFolderEntryData(
                CancellationToken.None,
                blobSizesConnection,
                availableSizes: null,
                childName: childName,
                parentKey: parentKey,
                gitCasedChildName: out casedChildName);
        }

        /// 
        /// Try to get the FolderData for the specified folder path from the projectionFolderCache
        /// cache.  If the  folder is not already in projectionFolderCache, search for it in the tree and
        /// then add it to projectionData
        /// 
        /// True if the folder could be found, and false otherwise
        private bool TryGetOrAddFolderDataFromCache(
            string folderPath,
            out FolderData folderData)
        {
            if (!this.projectionFolderCache.TryGetValue(folderPath, out folderData))
            {
                if (!this.TryGetFolderDataFromTreeUsingPath(folderPath, out folderData) ||
                    !folderData.IsIncluded)
                {
                    folderData = null;
                    return false;
                }

                this.projectionFolderCache.TryAdd(folderPath, folderData);
            }

            return true;
        }

        /// 
        /// Takes a path and gets the FolderData object for that path if it exists and is a folder
        /// 
        /// The path to the folder to lookup
        /// out paramenter - the FolderData to return if found
        /// true if the FolderData was found and set in the out parameter otherwise false
        private bool TryGetFolderDataFromTreeUsingPath(string folderPath, out FolderData folderData)
        {
            folderData = null;
            LazyUTF8String[] pathParts = folderPath
                .Split(new char[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)
                .Select(x => new LazyUTF8String(x))
                .ToArray();

            FolderEntryData data;
            if (!this.TryGetFolderEntryDataFromTree(pathParts, folderEntryData: out data))
            {
                return false;
            }

            if (data.IsFolder)
            {
                folderData = (FolderData)data;
                return true;
            }
            else
            {
                EventMetadata metadata = CreateEventMetadata();
                metadata.Add("folderPath", folderPath);
                metadata.Add(TracingConstants.MessageKey.InfoMessage, "Found file at path");
                this.context.Tracer.RelatedEvent(
                    EventLevel.Informational,
                    $"{nameof(this.TryGetFolderDataFromTreeUsingPath)}_FileAtPath",
                    metadata);

                return false;
            }
        }

        /// 
        /// Finds the FolderEntryData for the path provided will find the FolderEntryData specified in pathParts.
        /// 
        /// Path
        /// Out: FolderEntryData for pathParts
        /// True if the specified path could be found in the tree, and false otherwise
        private bool TryGetFolderEntryDataFromTree(LazyUTF8String[] pathParts, out FolderEntryData folderEntryData)
        {
            folderEntryData = null;
            int depth = pathParts.Length;
            FolderEntryData currentEntry = this.rootFolderData;
            for (int pathIndex = 0; pathIndex < depth; ++pathIndex)
            {
                if (!currentEntry.IsFolder)
                {
                    return false;
                }

                FolderData folderData = (FolderData)currentEntry;
                if (!folderData.ChildEntries.TryGetValue(pathParts[pathIndex], out currentEntry))
                {
                    return false;
                }
            }

            folderEntryData = currentEntry;
            return folderEntryData != null;
        }

        private void ParseIndexThreadMain()
        {
            try
            {
                while (true)
                {
                    this.wakeUpIndexParsingThread.WaitOne();

                    if (this.isStopping)
                    {
                        return;
                    }

                    Stopwatch stopwatch = Stopwatch.StartNew();
                    this.projectionReadWriteLock.EnterWriteLock();

                    // Record if the projection needed to be updated to ensure that placeholders and the negative cache
                    // are only updated when required (i.e. only updated when the projection was updated)
                    bool updatedProjection = this.projectionInvalid;

                    try
                    {
                        while (this.projectionInvalid)
                        {
                            try
                            {
                                this.CopyIndexFileAndBuildProjection();
                            }
                            catch (Win32Exception e)
                            {
                                this.SetProjectionAndPlaceholdersAsInvalid();

                                EventMetadata metadata = CreateEventMetadata(e);
                                this.context.Tracer.RelatedWarning(metadata, "Win32Exception when reparsing index for projection");
                            }
                            catch (IOException e)
                            {
                                this.SetProjectionAndPlaceholdersAsInvalid();

                                EventMetadata metadata = CreateEventMetadata(e);
                                this.context.Tracer.RelatedWarning(metadata, "IOException when reparsing index for projection");
                            }
                            catch (UnauthorizedAccessException e)
                            {
                                this.SetProjectionAndPlaceholdersAsInvalid();

                                EventMetadata metadata = CreateEventMetadata(e);
                                this.context.Tracer.RelatedWarning(metadata, "UnauthorizedAccessException when reparsing index for projection");
                            }

                            if (this.isStopping)
                            {
                                return;
                            }
                        }
                    }
                    finally
                    {
                        this.projectionReadWriteLock.ExitWriteLock();
                    }

                    stopwatch.Stop();
                    this.context.Repository.GVFSLock.Stats.RecordProjectionWriteLockHeld(stopwatch.ElapsedMilliseconds);

                    if (this.isStopping)
                    {
                        return;
                    }

                    // Avoid unnecessary updates by checking if the projection actually changed.  Some git commands (e.g. cherry-pick)
                    // update the index multiple times which can result in the outer 'while (true)' loop executing twice.  This happens
                    // because this.wakeUpThread is set again after this thread has woken up but before it's done any processing (because it's
                    // still waiting for the git command to complete).  If the projection is still valid during the second execution of
                    // the loop there's no need to clear the negative cache or update placeholders a second time.
                    if (updatedProjection)
                    {
                        this.ClearNegativePathCache();
                        this.UpdatePlaceholders();
                    }

                    this.projectionParseComplete.Set();

                    if (this.isStopping)
                    {
                        return;
                    }
                }
            }
            catch (Exception e)
            {
                this.LogErrorAndExit("ParseIndexThreadMain caught unhandled exception, exiting process", e);
            }
        }

        private void ClearNegativePathCache()
        {
            uint totalEntryCount;
            FileSystemResult clearCacheResult = this.fileSystemVirtualizer.ClearNegativePathCache(out totalEntryCount);
            int gitCount = Interlocked.Exchange(ref this.negativePathCacheUpdatedForGitCount, 0);

            EventMetadata clearCacheMetadata = CreateEventMetadata();
            clearCacheMetadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.ClearNegativePathCache)}: Cleared negative path cache");
            clearCacheMetadata.Add(nameof(totalEntryCount), totalEntryCount);
            clearCacheMetadata.Add("negativePathCacheUpdatedForGitCount", gitCount);
            clearCacheMetadata.Add("clearCacheResult.Result", clearCacheResult.ToString());
            clearCacheMetadata.Add("clearCacheResult.RawResult", clearCacheResult.RawResult);
            this.context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.ClearNegativePathCache)}_ClearedCache", clearCacheMetadata);

            if (clearCacheResult.Result != FSResult.Ok)
            {
                this.LogErrorAndExit("ClearNegativePathCache failed, exiting process. ClearNegativePathCache result: " + clearCacheResult.ToString());
            }
        }

        private void ClearUpdatePlaceholderErrors()
        {
            this.updatePlaceholderFailures = new ConcurrentHashSet();
            this.deletePlaceholderFailures = new ConcurrentHashSet();
        }

        private void UpdatePlaceholders()
        {
            Stopwatch stopwatch = new Stopwatch();
            List placeholderFilesListCopy;
            List placeholderFoldersListCopy;
            this.placeholderDatabase.GetAllEntries(out placeholderFilesListCopy, out placeholderFoldersListCopy);

            EventMetadata metadata = new EventMetadata();
            metadata.Add("File placeholder count", placeholderFilesListCopy.Count);
            metadata.Add("Folder placeholders count", placeholderFoldersListCopy.Count);

            using (ITracer activity = this.context.Tracer.StartActivity("UpdatePlaceholders", EventLevel.Informational, metadata))
            {
                // folderPlaceholdersToKeep always contains the empty path so as to avoid unnecessary attempts
                // to remove the repository's root folder.
                ConcurrentHashSet folderPlaceholdersToKeep = new ConcurrentHashSet(GVFSPlatform.Instance.Constants.PathComparer);
                folderPlaceholdersToKeep.Add(string.Empty);

                stopwatch.Restart();
                this.MultiThreadedPlaceholderUpdatesAndDeletes(placeholderFilesListCopy, folderPlaceholdersToKeep);
                stopwatch.Stop();

                long millisecondsUpdatingFilePlaceholders = stopwatch.ElapsedMilliseconds;

                stopwatch.Restart();
                this.blobSizes.Flush();

                int deleteFolderPlaceholderAttempted = 0;
                int folderPlaceholdersDeleted = 0;
                int folderPlaceholdersPathNotFound = 0;
                int folderPlaceholdersShaUpdate = 0;

                // A hash of the placeholders is only required if the platform expands directories
                // This is using a in memory HashSet for speed in processing
                // ~1 million placeholders was taking over 10 seconds to check for existence where the
                // HashSet was only taking a a few milliseconds
                HashSet existingPlaceholders = null;
                if (GVFSPlatform.Instance.KernelDriver.EnumerationExpandsDirectories)
                {
                    // Since only the file placeholders have been processed we can still use the folder placeholder list
                    // that was returned by GetAllEntries but we need to get the file paths that are now in the database.
                    // This is to avoid the extra time and processing to get all the placeholders when there are many
                    // folder placeholders and only a few file placeholders.
                    IEnumerable allPlaceholders = placeholderFoldersListCopy
                        .Select(x => x.Path)
                        .Union(this.placeholderDatabase.GetAllFilePaths());
                    existingPlaceholders = new HashSet(allPlaceholders, GVFSPlatform.Instance.Constants.PathComparer);
                }

                // Order the folders in decscending order so that we walk the tree from bottom up.
                // Traversing the folders in this order:
                //  1. Ensures child folders are deleted before their parents
                //  2. Ensures that folders that have been deleted by git (but are still in the projection) are found before their
                //     parent folder is re-expanded (only applies on platforms where EnumerationExpandsDirectories is true)
                foreach (IPlaceholderData folderPlaceholder in placeholderFoldersListCopy.OrderByDescending(x => x.Path))
                {
                    bool keepFolder = true;
                    if (!folderPlaceholdersToKeep.Contains(folderPlaceholder.Path))
                    {
                        bool isProjected = this.IsPathProjected(folderPlaceholder.Path, out string fileName, out bool isFolder);

                        // Check the projection for the folder to determine if the folder needs to be deleted
                        // The delete will be attempted if one of the following is true
                        // 1. not in the projection anymore
                        // 2. in the projection but is not a folder in the projection
                        // 3. Folder placeholder is a possible tombstone
                        if (!isProjected ||
                            !isFolder ||
                            folderPlaceholder.IsPossibleTombstoneFolder)
                        {
                            FSResult result = this.RemoveFolderPlaceholderIfEmpty(folderPlaceholder);
                            if (result == FSResult.Ok)
                            {
                                ++folderPlaceholdersDeleted;
                                keepFolder = false;
                            }
                            else if (result == FSResult.FileOrPathNotFound)
                            {
                                ++folderPlaceholdersPathNotFound;
                                keepFolder = false;
                            }

                            ++deleteFolderPlaceholderAttempted;
                        }

                        if (keepFolder)
                        {
                            this.AddParentFoldersToListToKeep(folderPlaceholder.Path, folderPlaceholdersToKeep);
                        }
                    }

                    if (keepFolder)
                    {
                        if (this.updateUsnJournal && this.TryGetFolderDataFromTreeUsingPath(folderPlaceholder.Path, out FolderData folderData))
                        {
                            string newFolderSha = folderData.HashedChildrenNamesSha();

                            if (folderPlaceholder.Sha != newFolderSha)
                            {
                                ++folderPlaceholdersShaUpdate;

                                // Write and delete a file so USN journal will have the folder as being changed
                                string tempFilePath = Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, folderPlaceholder.Path, ".vfs_usn_folder_update.tmp");
                                if (this.context.FileSystem.TryWriteAllText(tempFilePath, "TEMP FILE FOR USN FOLDER MODIFICATION"))
                                {
                                    this.context.FileSystem.DeleteFile(tempFilePath);
                                }

                                folderPlaceholder.Sha = newFolderSha;
                                this.placeholderDatabase.AddPlaceholderData(folderPlaceholder);
                            }
                        }

                        // Remove folder placeholders before re-expansion to ensure that projection changes that convert a folder to a file work
                        // properly
                        if (GVFSPlatform.Instance.KernelDriver.EnumerationExpandsDirectories && folderPlaceholder.IsExpandedFolder)
                        {
                            this.ReExpandFolder(folderPlaceholder.Path, existingPlaceholders);
                        }
                    }
                    else
                    {
                        existingPlaceholders?.Remove(folderPlaceholder.Path);
                        this.placeholderDatabase.Remove(folderPlaceholder.Path);
                    }
                }

                stopwatch.Stop();
                long millisecondsUpdatingFolderPlaceholders = stopwatch.ElapsedMilliseconds;

                stopwatch.Restart();

                this.repoMetadata.SetPlaceholdersNeedUpdate(false);

                stopwatch.Stop();
                long millisecondsWriteAndFlush = stopwatch.ElapsedMilliseconds;

                TimeSpan duration = activity.Stop(null);
                this.context.Repository.GVFSLock.Stats.RecordUpdatePlaceholders(
                    (long)duration.TotalMilliseconds,
                    millisecondsUpdatingFilePlaceholders,
                    millisecondsUpdatingFolderPlaceholders,
                    millisecondsWriteAndFlush,
                    deleteFolderPlaceholderAttempted,
                    folderPlaceholdersDeleted,
                    folderPlaceholdersPathNotFound,
                    folderPlaceholdersShaUpdate);
            }
        }

        private void MultiThreadedPlaceholderUpdatesAndDeletes(
            List placeholderList,
            ConcurrentHashSet folderPlaceholdersToKeep)
        {
            int minItemsPerThread = 10;
            int numThreads = Math.Max(8, Environment.ProcessorCount);
            numThreads = Math.Min(numThreads, placeholderList.Count / minItemsPerThread);
            numThreads = Math.Max(numThreads, 1);

            if (numThreads > 1)
            {
                Thread[] processThreads = new Thread[numThreads];
                int itemsPerThread = placeholderList.Count / numThreads;

                for (int i = 0; i < numThreads; i++)
                {
                    int start = i * itemsPerThread;
                    int end = (i + 1) == numThreads ? placeholderList.Count : (i + 1) * itemsPerThread;

                    processThreads[i] = new Thread(
                        () =>
                        {
                            // We have a top-level try\catch for any unhandled exceptions thrown in the newly created thread
                            try
                            {
                                this.UpdateAndDeletePlaceholdersThreadCallback(placeholderList, start, end, folderPlaceholdersToKeep);
                            }
                            catch (Exception e)
                            {
                                this.LogErrorAndExit(nameof(this.MultiThreadedPlaceholderUpdatesAndDeletes) + " background thread caught unhandled exception, exiting process", e);
                            }
                        });

                    processThreads[i].Start();
                }

                for (int i = 0; i < processThreads.Length; i++)
                {
                    processThreads[i].Join();
                }
            }
            else
            {
                this.UpdateAndDeletePlaceholdersThreadCallback(placeholderList, 0, placeholderList.Count, folderPlaceholdersToKeep);
            }
        }

        private void UpdateAndDeletePlaceholdersThreadCallback(
            List placeholderList,
            int start,
            int end,
            ConcurrentHashSet folderPlaceholdersToKeep)
        {
            if (GVFSPlatform.Instance.KernelDriver.EmptyPlaceholdersRequireFileSize)
            {
                using (BlobSizes.BlobSizesConnection blobSizesConnection = this.blobSizes.CreateConnection())
                {
                    Dictionary availableSizes = new Dictionary();

                    this.BatchPopulateMissingSizesFromRemote(blobSizesConnection, placeholderList, start, end, availableSizes);

                    for (int j = start; j < end; ++j)
                    {
                        this.UpdateOrDeleteFilePlaceholder(
                            blobSizesConnection,
                            placeholderList[j],
                            folderPlaceholdersToKeep,
                            availableSizes);
                    }
                }
            }
            else
            {
                for (int j = start; j < end; ++j)
                {
                    this.UpdateOrDeleteFilePlaceholder(
                        blobSizesConnection: null,
                        placeholder: placeholderList[j],
                        folderPlaceholdersToKeep: folderPlaceholdersToKeep,
                        availableSizes: null);
                }
            }
        }

        private void BatchPopulateMissingSizesFromRemote(
            BlobSizes.BlobSizesConnection blobSizesConnection,
            List placeholderList,
            int start,
            int end,
            Dictionary availableSizes)
        {
            int maxObjectsInHTTPRequest = 2000;

            for (int index = start; index < end; index += maxObjectsInHTTPRequest)
            {
                int count = Math.Min(maxObjectsInHTTPRequest, end - index);
                IEnumerable nextBatch = this.GetShasWithoutSizeAndNeedingUpdate(blobSizesConnection, availableSizes, placeholderList, index, index + count);

                if (nextBatch.Any())
                {
                    Stopwatch queryTime = Stopwatch.StartNew();
                    List fileSizes = this.gitObjects.GetFileSizes(nextBatch, CancellationToken.None);
                    this.context.Repository.GVFSLock.Stats.RecordSizeQuery(queryTime.ElapsedMilliseconds);

                    foreach (GitObjectsHttpRequestor.GitObjectSize downloadedSize in fileSizes)
                    {
                        string downloadedSizeId = downloadedSize.Id.ToUpper();
                        Sha1Id sha1Id = new Sha1Id(downloadedSizeId);
                        blobSizesConnection.BlobSizesDatabase.AddSize(sha1Id, downloadedSize.Size);
                        availableSizes[downloadedSizeId] = downloadedSize.Size;
                    }
                }
            }
        }

        private IEnumerable GetShasWithoutSizeAndNeedingUpdate(BlobSizes.BlobSizesConnection blobSizesConnection, Dictionary availableSizes, List placeholders, int start, int end)
        {
            for (int index = start; index < end; index++)
            {
                string projectedSha = this.GetNewProjectedShaForPlaceholder(placeholders[index].Path);

                if (!string.IsNullOrEmpty(projectedSha))
                {
                    long blobSize = 0;
                    string shaOnDisk = placeholders[index].Sha;

                    if (shaOnDisk.Equals(projectedSha))
                    {
                        continue;
                    }

                    if (blobSizesConnection.TryGetSize(new Sha1Id(projectedSha), out blobSize))
                    {
                        availableSizes[projectedSha] = blobSize;
                        continue;
                    }

                    if (this.gitObjects.TryGetBlobSizeLocally(projectedSha, out blobSize))
                    {
                        availableSizes[projectedSha] = blobSize;
                        continue;
                    }

                    yield return projectedSha;
                }
            }
        }

        private string GetNewProjectedShaForPlaceholder(string path)
        {
            string childName;
            string parentKey;
            this.GetChildNameAndParentKey(path, out childName, out parentKey);

            string projectedSha;
            if (this.TryGetSha(childName, parentKey, out projectedSha))
            {
                return projectedSha;
            }

            return null;
        }

        private void ReExpandFolder(
            string relativeFolderPath,
            HashSet existingPlaceholders)
        {
            bool foundFolder = this.TryGetOrAddFolderDataFromCache(relativeFolderPath, out FolderData folderData);
            if (!foundFolder)
            {
                // Folder is no longer in the projection
                existingPlaceholders.Remove(relativeFolderPath);
                this.placeholderDatabase.Remove(relativeFolderPath);
                return;
            }

            if (GVFSPlatform.Instance.KernelDriver.EmptyPlaceholdersRequireFileSize)
            {
                using (BlobSizes.BlobSizesConnection blobSizesConnection = this.blobSizes.CreateConnection())
                {
                    folderData.PopulateSizes(
                    this.context.Tracer,
                    this.gitObjects,
                    blobSizesConnection,
                    availableSizes: null,
                    cancellationToken: CancellationToken.None);
                }
            }

            for (int i = 0; i < folderData.ChildEntries.Count; i++)
            {
                FolderEntryData childEntry = folderData.ChildEntries[i];
                if (!childEntry.IsFolder || ((FolderData)childEntry).IsIncluded)
                {
                    string childRelativePath;
                    if (relativeFolderPath.Length == 0)
                    {
                        childRelativePath = childEntry.Name.GetString();
                    }
                    else
                    {
                        childRelativePath = relativeFolderPath + Path.DirectorySeparatorChar + childEntry.Name.GetString();
                    }

                    if (!existingPlaceholders.Contains(childRelativePath))
                    {
                        FileSystemResult result;
                        if (childEntry.IsFolder)
                        {
                            result = this.fileSystemVirtualizer.WritePlaceholderDirectory(childRelativePath);
                            if (result.Result == FSResult.Ok)
                            {
                                this.placeholderDatabase.AddPartialFolder(childRelativePath, sha: null);
                            }
                            else if (result.Result == FSResult.IOError)
                            {
                                // When running in sparse mode there could be directories that were left on disk but are not in the
                                // modified paths or in the placeholder list because they were not part of the projection
                                // At this point they need to be re-expanded because they are on disk and part of the projection
                                if (this.context.FileSystem.DirectoryExists(Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, childRelativePath)))
                                {
                                    this.ReExpandFolder(childRelativePath, existingPlaceholders);
                                    this.placeholderDatabase.AddExpandedFolder(childRelativePath);
                                }
                            }
                        }
                        else
                        {
                            FileData childFileData = childEntry as FileData;
                            string fileSha = childFileData.Sha.ToString();
                            result = this.fileSystemVirtualizer.WritePlaceholderFile(childRelativePath, childFileData.Size, fileSha);
                            if (result.Result == FSResult.Ok)
                            {
                                this.placeholderDatabase.AddFile(childRelativePath, fileSha);
                            }
                        }

                        switch (result.Result)
                        {
                            case FSResult.Ok:
                                break;

                            case FSResult.FileOrPathNotFound:
                                // Git command must have removed the folder being re-expanded (relativeFolderPath)
                                // Remove the folder from existingFolderPlaceholders so that its parent will create
                                // it again (when it's re-expanded)
                                existingPlaceholders.Remove(relativeFolderPath);
                                this.placeholderDatabase.Remove(relativeFolderPath);
                                return;

                            default:
                                // TODO(#245): Handle failures of WritePlaceholderDirectory and WritePlaceholderFile
                                break;
                        }
                    }
                }
            }
        }

        /// 
        /// Removes the folder placeholder from disk if it's empty.
        /// 
        ///  Result of trying to delete the PlaceHolder
        /// 
        private FSResult RemoveFolderPlaceholderIfEmpty(IPlaceholderData placeholder)
        {
            UpdateFailureReason failureReason = UpdateFailureReason.NoFailure;
            FileSystemResult result = this.fileSystemVirtualizer.DeleteFile(placeholder.Path, FolderPlaceholderDeleteFlags, out failureReason);
            switch (result.Result)
            {
                case FSResult.Ok:
                case FSResult.FileOrPathNotFound:
                case FSResult.DirectoryNotEmpty:
                    return result.Result;

                default:
                    EventMetadata metadata = CreateEventMetadata();
                    metadata.Add("Folder Path", placeholder.Path);
                    metadata.Add("result.Result", result.Result.ToString());
                    metadata.Add("result.RawResult", result.RawResult);
                    metadata.Add("UpdateFailureCause", failureReason.ToString());
                    this.context.Tracer.RelatedEvent(EventLevel.Informational, nameof(this.RemoveFolderPlaceholderIfEmpty) + "_DeleteFileFailure", metadata);
                    return result.Result;
            }
        }

        private void UpdateOrDeleteFilePlaceholder(
            BlobSizes.BlobSizesConnection blobSizesConnection,
            IPlaceholderData placeholder,
            ConcurrentHashSet folderPlaceholdersToKeep,
            Dictionary availableSizes)
        {
            string childName;
            string parentKey;
            this.GetChildNameAndParentKey(placeholder.Path, out childName, out parentKey);

            string projectedSha;
            if (!this.TryGetSha(childName, parentKey, out projectedSha))
            {
                UpdateFailureReason failureReason = UpdateFailureReason.NoFailure;
                FileSystemResult result = this.fileSystemVirtualizer.DeleteFile(placeholder.Path, FilePlaceholderUpdateFlags, out failureReason);
                this.ProcessGvUpdateDeletePlaceholderResult(
                    placeholder,
                    string.Empty,
                    result,
                    failureReason,
                    parentKey,
                    folderPlaceholdersToKeep,
                    deleteOperation: true);
            }
            else
            {
                string onDiskSha = placeholder.Sha;
                if (!onDiskSha.Equals(projectedSha))
                {
                    DateTime now = DateTime.UtcNow;
                    UpdateFailureReason failureReason = UpdateFailureReason.NoFailure;
                    FileSystemResult result;

                    try
                    {
                        FileData data = (FileData)this.GetProjectedFolderEntryData(CancellationToken.None, blobSizesConnection, availableSizes, childName, parentKey, out string _);
                        result = this.fileSystemVirtualizer.UpdatePlaceholderIfNeeded(
                            placeholder.Path,
                            creationTime: now,
                            lastAccessTime: now,
                            lastWriteTime: now,
                            changeTime: now,
                            fileAttributes: FileAttributes.Archive,
                            endOfFile: data.Size,
                            shaContentId: projectedSha,
                            updateFlags: FilePlaceholderUpdateFlags,
                            failureReason: out failureReason);
                    }
                    catch (Exception e)
                    {
                        result = new FileSystemResult(FSResult.IOError, rawResult: -1);

                        EventMetadata metadata = CreateEventMetadata(e);
                        metadata.Add("virtualPath", placeholder.Path);
                        this.context.Tracer.RelatedWarning(metadata, "UpdateOrDeletePlaceholder: Exception while trying to update placeholder");
                    }

                    this.ProcessGvUpdateDeletePlaceholderResult(
                        placeholder,
                        projectedSha,
                        result,
                        failureReason,
                        parentKey,
                        folderPlaceholdersToKeep,
                        deleteOperation: false);
                }
                else
                {
                    this.AddParentFoldersToListToKeep(parentKey, folderPlaceholdersToKeep);
                }
            }
        }

        private void ProcessGvUpdateDeletePlaceholderResult(
            IPlaceholderData placeholder,
            string projectedSha,
            FileSystemResult result,
            UpdateFailureReason failureReason,
            string parentKey,
            ConcurrentHashSet folderPlaceholdersToKeep,
            bool deleteOperation)
        {
            EventMetadata metadata;
            switch (result.Result)
            {
                case FSResult.Ok:
                    if (!deleteOperation)
                    {
                        this.placeholderDatabase.AddFile(placeholder.Path, projectedSha);
                        this.AddParentFoldersToListToKeep(parentKey, folderPlaceholdersToKeep);
                        return;
                    }

                    break;

                case FSResult.IoReparseTagNotHandled:
                    // Attempted to update\delete a file that has a non-ProjFS reparse point

                    this.ScheduleBackgroundTaskForFailedUpdateDeletePlaceholder(placeholder, deleteOperation);
                    this.AddParentFoldersToListToKeep(parentKey, folderPlaceholdersToKeep);

                    metadata = CreateEventMetadata();
                    metadata.Add("deleteOperation", deleteOperation);
                    metadata.Add("virtualPath", placeholder.Path);
                    metadata.Add("result.Result", result.ToString());
                    metadata.Add("result.RawResult", result.RawResult);
                    metadata.Add(TracingConstants.MessageKey.InfoMessage, "UpdateOrDeletePlaceholder: StatusIoReparseTagNotHandled");
                    this.context.Tracer.RelatedEvent(EventLevel.Informational, "UpdatePlaceholders_StatusIoReparseTagNotHandled", metadata);

                    break;

                case FSResult.GenericProjFSError when failureReason == UpdateFailureReason.DirtyData:
                case FSResult.VirtualizationInvalidOperation:
                    // GVFS attempted to update\delete a file that is no longer partial.
                    // This can occur if:
                    //
                    //    - A file is converted from partial to full (or tombstone) while a git command is running.
                    //      Any tasks scheduled during the git command to update the placeholder list have not yet
                    //      completed at this point.
                    //
                    //    - A placeholder file was converted to full without being in the index.  This can happen if 'git update-index --remove'
                    //      is used to remove a file from the index before converting the file to full.  Because a skip-worktree bit
                    //      is not cleared when this file file is converted to full, FileSystemCallbacks assumes that there is no placeholder
                    //      that needs to be removed removed from the list
                    //
                    //    - When there is a merge conflict the conflicting file will get the skip worktree bit removed. In some cases git
                    //      will not make any changes to the file in the working directory. In order to handle this case we have to check
                    //      the merge stage in the index and add that entry to the placeholder list. In doing so the files that git did
                    //      update in the working directory will be full files but we will have a placeholder entry for them as well.

                    // There have been reports of FileSystemVirtualizationInvalidOperation getting hit without a corresponding background
                    // task having been scheduled (to add the file to the modified paths).
                    // Schedule OnFailedPlaceholderUpdate\OnFailedPlaceholderDelete to be sure that Git starts managing this
                    // file.  Currently the only known way that this can happen is deleting a partial file and putting a full
                    // file in its place while GVFS is unmounted.
                    this.ScheduleBackgroundTaskForFailedUpdateDeletePlaceholder(placeholder, deleteOperation);
                    this.AddParentFoldersToListToKeep(parentKey, folderPlaceholdersToKeep);

                    metadata = CreateEventMetadata();
                    metadata.Add("deleteOperation", deleteOperation);
                    metadata.Add("virtualPath", placeholder.Path);
                    metadata.Add("result.Result", result.ToString());
                    metadata.Add("result.RawResult", result.RawResult);
                    metadata.Add("failureReason", failureReason.ToString());
                    metadata.Add("backgroundCount", this.backgroundFileSystemTaskRunner.Count);
                    metadata.Add(TracingConstants.MessageKey.InfoMessage, "UpdateOrDeletePlaceholder: attempted an invalid operation");
                    this.context.Tracer.RelatedEvent(EventLevel.Informational, "UpdatePlaceholders_InvalidOperation", metadata);

                    break;

                case FSResult.FileOrPathNotFound:
                    break;

                default:
                    {
                        string gitPath;
                        this.AddFileToUpdateDeletePlaceholderFailureReport(deleteOperation, placeholder, out gitPath);
                        this.ScheduleBackgroundTaskForFailedUpdateDeletePlaceholder(placeholder, deleteOperation);
                        this.AddParentFoldersToListToKeep(parentKey, folderPlaceholdersToKeep);

                        metadata = CreateEventMetadata();
                        metadata.Add("deleteOperation", deleteOperation);
                        metadata.Add("virtualPath", placeholder.Path);
                        metadata.Add("gitPath", gitPath);
                        metadata.Add("result.Result", result.ToString());
                        metadata.Add("result.RawResult", result.RawResult);
                        metadata.Add("failureReason", failureReason.ToString());
                        this.context.Tracer.RelatedWarning(metadata, "UpdateOrDeletePlaceholder: did not succeed");
                    }

                    break;
            }

            this.placeholderDatabase.Remove(placeholder.Path);
        }

        private void AddParentFoldersToListToKeep(string parentKey, ConcurrentHashSet folderPlaceholdersToKeep)
        {
            string folder = parentKey;
            while (!string.IsNullOrEmpty(folder))
            {
                folderPlaceholdersToKeep.Add(folder);
                this.GetChildNameAndParentKey(folder, out string _, out string parentFolder);
                folder = parentFolder;
            }
        }

        private void AddFileToUpdateDeletePlaceholderFailureReport(
            bool deleteOperation,
            IPlaceholderData placeholder,
            out string gitPath)
        {
            gitPath = placeholder.Path.TrimStart(Path.DirectorySeparatorChar).Replace(Path.DirectorySeparatorChar, GVFSConstants.GitPathSeparator);
            if (deleteOperation)
            {
                this.deletePlaceholderFailures.Add(gitPath);
            }
            else
            {
                this.updatePlaceholderFailures.Add(gitPath);
            }
        }

        private void ScheduleBackgroundTaskForFailedUpdateDeletePlaceholder(IPlaceholderData placeholder, bool deleteOperation)
        {
            if (deleteOperation)
            {
                this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFailedPlaceholderDelete(placeholder.Path));
            }
            else
            {
                this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFailedPlaceholderUpdate(placeholder.Path));
            }
        }

        private void LogErrorAndExit(string message, Exception e = null)
        {
            EventMetadata metadata = CreateEventMetadata(e);
            this.context.Tracer.RelatedError(metadata, message);
            Environment.Exit(1);
        }

        private void CopyIndexFileAndBuildProjection()
        {
            this.context.FileSystem.CopyFile(this.indexPath, this.projectionIndexBackupPath, overwrite: true);
            this.BuildProjection();
        }

        private void BuildProjection()
        {
            this.SetProjectionInvalid(false);

            using (ITracer tracer = this.context.Tracer.StartActivity("ParseGitIndex", EventLevel.Informational))
            {
                using (FileStream indexStream = new FileStream(this.projectionIndexBackupPath, FileMode.Open, FileAccess.Read, FileShare.Read, IndexFileStreamBufferSize))
                {
                    try
                    {
                        this.indexParser.RebuildProjection(tracer, indexStream);
                    }
                    catch (Exception e)
                    {
                        EventMetadata metadata = CreateEventMetadata(e);
                        this.context.Tracer.RelatedWarning(metadata, $"{nameof(this.BuildProjection)}: Exception thrown by {nameof(GitIndexParser.RebuildProjection)}");

                        this.SetProjectionInvalid(true);
                        throw;
                    }
                }

                SortedFolderEntries.ShrinkPool();
                LazyUTF8String.ShrinkPool();

                EventMetadata poolMetadata = CreateEventMetadata();
                poolMetadata.Add($"{nameof(SortedFolderEntries)}_{nameof(SortedFolderEntries.FolderPoolSize)}", SortedFolderEntries.FolderPoolSize());
                poolMetadata.Add($"{nameof(SortedFolderEntries)}_{nameof(SortedFolderEntries.FilePoolSize)}", SortedFolderEntries.FilePoolSize());
                poolMetadata.Add($"{nameof(LazyUTF8String)}_{nameof(LazyUTF8String.StringPoolSize)}", LazyUTF8String.StringPoolSize());
                poolMetadata.Add($"{nameof(LazyUTF8String)}_{nameof(LazyUTF8String.BytePoolSize)}", LazyUTF8String.BytePoolSize());
                TimeSpan duration = tracer.Stop(poolMetadata);
                this.context.Repository.GVFSLock.Stats.RecordParseGitIndex((long)duration.TotalMilliseconds);
            }
        }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/IProfilerOnlyIndexProjection.cs
================================================
using GVFS.Common.Tracing;

namespace GVFS.Virtualization.Projection
{
    /// 
    /// Interface used for performace profiling GitIndexProjection.  This interface
    /// allows performance tests to force GitIndexProjection to parse the index on demand so
    /// that index parsing can be measured and profiled.
    /// 
    public interface IProfilerOnlyIndexProjection
    {
        void ForceRebuildProjection();
        void ForceAddMissingModifiedPaths(ITracer tracer);
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/ProjectedFileInfo.cs
================================================
using GVFS.Common.Git;

namespace GVFS.Virtualization.Projection
{
    public class ProjectedFileInfo
    {
        public ProjectedFileInfo(string name, long size, bool isFolder, Sha1Id sha)
        {
            this.Name = name;
            this.Size = size;
            this.IsFolder = isFolder;
            this.Sha = sha;
        }

        public string Name { get; }
        public long Size { get; }
        public bool IsFolder { get; }

        public Sha1Id Sha { get; }
    }
}


================================================
FILE: GVFS/GVFS.Virtualization/Projection/Readme.md
================================================
# GitIndexProjection

## Overview

This document is to help give developers a better understanding of the `GitIndexProjection` class and associated classes and the design and architectural decisions that went into it. In simplest terms the purpose of the `GitIndexProjection` class is to parse the `.git/index` file and build an in-memory tree representation of the directories and files that are used when a file system request comes from the virtual file system driver.  GVFS.Mount.exe keeps an instance of this class in-memory for the lifetime of the process.  This helps VFSForGit quickly return file system operations such as enumeration or on-demand hydration. VFSForGit uses the [skip worktree bit](https://git-scm.com/docs/git-update-index#_skip_worktree_bit) to know what to include in the projection data and what files git will be keeping up to date.  Currently VFSForGit only supports using [version 4 of the index](https://git-scm.com/docs/git-update-index#Documentation/git-update-index.txt---index-versionltngt).  Details on the index format and version 4 can be found [here](https://github.com/microsoft/git/blob/031fd4b93b8182761948aa348565118955f48307/Documentation/technical/index-format.txt).

This code was designed for incredibly large repositories (over 3 million files and 500K folders), there are multiple internal classes that are used to help with the prioritized objectives of:

1. Keep git commands functioning correctly.
2. Keep end-to-end time as short as possible.
3. Keep the memory footprint as small as possible.

Some things used to acheive these are:

1. Use `unsafe` code and `fixed` pointers for speed.
2. Keep object pools so that the overhead of allocating is a one-time up-front cost.
3. Keep all folder and files names in a byte array with an index and length to avoid converting them all to .NET strings.
4. Multiple threads and sychronization.

### Processes

These are some of the processes that use the `GitIndexProjection`.

#### Enumeration

Enumeration is tracked on a per call basis with a `Guid` and an `ActiveEnumeration` so that multiple enumerations can run and be restarted without affecting each other.

1. Request comes to start a directory enumeration via the callback `IRequiredCallbacks.StartDirectoryEnumerationCallback`
2. Take a projection read lock
3. Try to get the projected items for the folder from the cache
4. If not in cache, get projected items from the tree and add folder data to the cache
5. Convert projeted items to `ProjectedFileInfo` objects
6. Release the read lock

#### File Placeholder

1. Request comes to get placeholder information via the callback `IRequiredCallbacks.GetPlaceholderInfoCallback`
2. If the path is in the projection and placeholders can get created
3. Take a projection read lock
4. Try to get the projected item for the parent folder from the cache
5. Try get the child item from the parent folder data child entries
6. Populate the size if not set
7. Release the read lock

#### File Data

1. Request comes for file data via the callback `IRequiredCallbacks.GetFileDataCallback`
2. Get the SHA1 from the contentId
3. Get the BLOB data looking in the following places in this order
   1. Check in the loose objects
   2. Use LibGit2 to try and get the object
   3. Try to download object from server and save to the loose objects
4. Write BLOB content using the `IWriteBuffer` returned by a call to the virtualization instance's `CreateWriteBuffer` method.

#### git command

1. User runs git command
2. git invokes pre-command hook
   1. Check for valid command
   2. Obtain GVFS Lock if needed by using the named pipe message of "AquireLock" (`NamedPipeMessages.AcquireLock.AcquireRequest`).
   3. If command is fetch or pull, run prefetch of commits
3. git invokes virtual-filesystem hook
   1. Requests the list of modified paths from the GVFS.Mount process using the named pipe message "MPL" (`NamedPipeMessages.ModifiedPaths.ListRequest`).
4. git reads the index setting the skip-worktree bit based when path is not in the list of modified paths
5. When git needs to read an object it checks in this order
   1. pack files
   2. loose objects
   3. if enabled gvfs-helper to try and download via the gvfs protocol
   4. retry pack files
   5. if enabled use the read-object hook to have GVFS.Mount.exe download the object using the named pipe message "DLO" (`NamedPipeMessages.DownloadObject.DownloadRequest`).
   6. if enabled check promisor remote
6. If git changes the index it will write out the new index and the invoke the post-index-change hook using the named pipe message "PICN" (`NamedPipeMessages.PostIndexChanged.NotificationRequest`). This will wait for the hook to return before continuing.  This is important because the hook is when the projection is updated and needs to be complete before git continues or it may see the wrong projection.
   1. Invalidate the projection state
   2. This wakes up the index parsing thread
   3. Once the parsing and updating of placeholders is complete the hook returns
7. git invokes post-command hook using the named pipe message "ReleaseLock" (`NamedPipeMessages.ReleaseLock.Request`).
   1. Release the GVFS Lock if needed

## Internal Classes

### `FileTypeAndMode`

Class only used for file systems that support file mode since that is in the git index and is needed when the file is created on disk.

### `PoolAllocationMultipliers`

Class used to hold the multipliers that are applied to the various pools in the code.  These numbers come from running with various sized repos and determining what was best for keeping the pools at reasonable sizes.

### `ObjectPool`

Class that is a generic pool of some type of object that will dynamically grow or can be shrunk to free objects when too many get allocated.  All objects for the pool are created at the time the pool is expanded.  The `LazyUTF8String.BytePool` is a specialized pool to allow the use of a pointer into the allocated `byte[]`.

### `FolderEntryData`

Abstract base class for data about an item that is in a folder.  Contains the name and a flag for whether the entry is a folder.  `FolderData` and `FileData` are the derived classes for this class.

### `FolderData`

Class containing the data about a folder in the projection.  Includes the child entries as a `SortedFolderEntries` object, a flag to indicate the children's sizes have been populated, and a flag to indicate if the folder should be included in the projection (This is when using sparse mode).

### `FileData`

Class containing the data about a file in the projection.  Includes the size and the SHA1.  The SHA1 is stored as 2 `ulong` and an `uint` for performance and memory usage.

### `LazyUTF8String`

Class used to keep track of the string from the index that is in the `BytePool` and converts from the `BytePool` to a `string` on when needed by either calling the `GetString` method or `Compare` when one string is not all ASCII.

### `SortedFolderEntries`

Class used to keep the list entries for a folder, either `FolderData` or `FileData` objects) in sorted order.  This class keeps the static pool of both `FolderData` and `FileData` objects for reuse.

Couple of things to note:

1. This is using the `Compare` method of the `LazyUTF8String` class for a performance optimization since most of the time the paths in the index are ASCII and the code can do byte by byte comparison and __not__ have to convert to a `string` object and then compare which is a performance and memory hit.

2. When getting the index of the name in the sorted entries it will return the bitwise complement of the index where the item should be inserted.  This was done to avoid making one call to determine if the name exists and a second call to get the index for insertion.

### `SparseFolderData`

Class used to keep the sparse folder information.  It contains a flag for whether the folder should be recursed into for projection, the depth of the folder, and the children in a name, data `Dictionary`.

When sparse mode is enable this data is used to determine which folders should be included in the projection.  A root instance (`rootSparseFolder`) is kept in the `GitIndexProjection` which is not recursive and only files in the root folder are being projected when there aren't any other sparse folders.  When sparse folders are added via the `SparseVerb`, the children of the root instance are inserted or removed accordingly.  

For example when `gvfs sparse --set foo/bar/example;other` runs, there will be 2 sparse folders, `foo/bar/example` and `other`.

```
`rootSparseFolder` in the `GitIndexProjection` would have:
Children:
|- foo (IsRecursive = false, Depth = 0)
|  Children:
|  |- bar (IsRecursive = false, Depth = 1)
      Children:
|     |- example (IsRecursive = true, Depth = 2)
|
|- other (IsRecursive = true, Depth = 0)
```

This will cause the root folder to have files and folders for `foo` and `other`.  `foo` will only have the `bar` folder and all its files, but no other folders will be projected.  The `foo/bar` folder will only have the `example` folder and all its files, but no other folders will be projected. The `foo/bar/example` and `other` folders will have all child files and folders projected recursively.

### `GitIndexEntry`

Class used to store the data from the index about a single entry.  There is only one instance of this class used during index parsing and it is reused for each index entry.  The reason for this is that version 4 of the git index has the [path prefix compressed](https://github.com/microsoft/git/blob/f5992bb185757a1654ce31424611b4d05bda3400/Documentation/technical/index-format.txt#L116) and the previous path is needed to create the path for the current entry.  The code in this class is heavily optimized to make parsing the index and the paths as fast as possible.

### `GitIndexParser`

Class that is responsible for parsing the git index based of version 4.  Please see [`index-format.txt`](https://github.com/microsoft/git/blob/f5992bb185757a1654ce31424611b4d05bda3400/Documentation/technical/index-format.txt) for detailed information of this format.  This is used to both validate the index and build the projection.  It currently ignores all index extensions and is only for getting the paths and building the tree using the `FolderData` and `FileData` classes. The index is read in chunks of 512K which gave the best performance.

## Other classes

### `ProjectedFileInfo`

Class used to hold the data that is used by `FileSystemVirtualizer` when enumerating or creating placeholders.

## `GitIndexProjection`

Class used to hold the projection data and keep it up to date. This code uses and can be called from multiple threads.  It is using `ReaderWriterLockSlim` to synchronize access to the projection and ResetEvents for waiting and notification of events. There are caches for a variety of objects that are used.

### Initialization

Found in the `Initialize` method and does the following:

1. Take a projection write lock
2. Build the projection
3. Release the write lock
4. Update placeholders if needed
5. Start the index parsing thread

### Index parsing thread

There is a thread started when the class is initialized that waits to be woken up to parse the index. Events are used to indicate when the parsing is complete to make sure that the projection is in a good state before using it.

When woken the parsing thread will:

1. Check if it needs to stop
2. Take the projection write lock
3. Copy the index file and rebuild the projection while projection is invalid
4. Release projection write lock
5. If the projection was updated clear the negative path cache and update placeholders
6. Set event indicating projection parsing is complete

## GVFS.PerfProfiling project

This project is used to specifically test the memory and performance of parsing the index and building the projection.  There are three tests that can be ran: `ValidateIndex`, `RebuildProjection`, and `ValidateModifiedPaths`.  The `IProfilerOnlyIndexProjection` interface is used to expose the methods for use in this project only.  Options can be used to limit which tests run.  Each test runs 11 times skipping the first run and getting the average of the last 10.  Memory is tracked and displayed as well to make sure it stays consistent.


================================================
FILE: GVFS/GVFS.Virtualization/Projection/SizesUnavailableException.cs
================================================
using System;

namespace GVFS.Virtualization.Projection
{
    public class SizesUnavailableException : Exception
    {
        public SizesUnavailableException(string message)
            : base(message)
        {
        }
    }
}


================================================
FILE: GVFS/GitHooksLoader/GitHooksLoader.cpp
================================================
// GitHooksLoader.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include 
#include 

int ExecuteHook(const std::wstring &applicationName, wchar_t *hookName, int argc, WCHAR *argv[]);

int wmain(int argc, WCHAR *argv[])
{
	LARGE_INTEGER tickFrequency = { 0 };
	LARGE_INTEGER startTime = { 0 }, endTime = { 0 };
    bool perfTraceEnabled = false;

    size_t requiredCount = 0;
    if (getenv_s(&requiredCount, NULL, 0, "GITHOOKSLOADER_PERFTRACE") != 0)
    {
        requiredCount = 0;
    }

    if (requiredCount != 0)
    {
        // Only enable tracing if we have access to a high res perf counter.
        if (QueryPerformanceFrequency(&tickFrequency) != 0)
        {
            perfTraceEnabled = true;
        }
    }

    if (argc < 2)
    {
        fwprintf(stderr, L"Usage: %s  []\n", argv[0]);
        exit(1);
    }

    wchar_t hookName[_MAX_FNAME];
    errno_t err = _wsplitpath_s(argv[0], NULL, 0, NULL, 0, hookName, _MAX_FNAME, NULL, 0);
    if (err != 0)
    {
        fwprintf(stderr, L"Error splitting the path. Error code %d.\n", err);
        exit(2);
    }
    
    std::wstring executingLoader = std::wstring(argv[0]);
    size_t exePartStart = executingLoader.rfind(L".exe");

    if (exePartStart != std::wstring::npos)
    {
        executingLoader.resize(exePartStart);
    }

    std::wifstream hooksList(executingLoader + L".hooks");
    int numHooksExecuted = 0;
    for (std::wstring hookApplication; std::getline(hooksList, hookApplication); )
    {
        // Skip comments and empty lines.
        if (hookApplication.empty() || hookApplication.at(0) == '#')
        {
            continue;
        }

        numHooksExecuted++;

        if (perfTraceEnabled)
        {
            QueryPerformanceCounter(&startTime);
        }

        int hookExitCode = ExecuteHook(hookApplication, hookName, argc, argv);
        if (0 != hookExitCode)
        {
            return hookExitCode;
        }

        if (perfTraceEnabled)
        {
            double elapsedTime;
            QueryPerformanceCounter(&endTime);
            elapsedTime = (endTime.QuadPart - startTime.QuadPart) * 1000.0 / tickFrequency.QuadPart;
            fwprintf(stdout, L"%s: %s = %.2f milliseconds\n", executingLoader.c_str(), hookApplication.c_str(), elapsedTime);
        }
    }

    if (0 == numHooksExecuted)
    {
        fwprintf(stderr, L"No hooks found to execute\n");
        exit(5);
    }

    return 0;
}

int ExecuteHook(const std::wstring &applicationName, wchar_t *hookName, int argc, WCHAR *argv[])
{
    wchar_t expandedPath[MAX_PATH + 1];
    DWORD length = ExpandEnvironmentStrings(applicationName.c_str(), expandedPath, MAX_PATH);
    if (length == 0 || length > MAX_PATH)
    {
        fwprintf(stderr, L"Unable to expand '%s'", applicationName.c_str());
        exit(6);
    }
    
    std::wstring commandLine = std::wstring(expandedPath) + L" " + hookName;
    for (int x = 1; x < argc; x++)
    {
        commandLine += L" " + std::wstring(argv[x]);
    }
    
    // Start the child process. 
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    DWORD creationFlags = 0;
    HANDLE consoleHandle;

    /* If we have a console, use it (ie allow default behavior)*/
    if ((consoleHandle = CreateFile(L"CONOUT$", GENERIC_WRITE,
        FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL, NULL)) !=
        INVALID_HANDLE_VALUE)
        CloseHandle(consoleHandle);
    else {
        /* Otherwise, forward stdout/err in case they were redirected,
         * but do not allow creating a window.*/
        si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
        si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
        /* Git disallows stdin from hooks */
        si.dwFlags = STARTF_USESTDHANDLES;

        creationFlags |= CREATE_NO_WINDOW;
    }

    ZeroMemory(&pi, sizeof(pi));

    /* The child process will inherit ErrorMode from this process.
     * SEM_FAILCRITICALERRORS will prevent the .NET runtime from
     * creating a dialog box for critical errors - in particular
     * if antivirus has locked the machine.config file.
     * Disabling the dialog box lets the child process (typically GVFS.Hooks.exe)
     * continue trying to run, and if it still needs machine.config then it
     * can handle the exception at that time (whereas the dialog box would
     * hang the app until clicked, and is not handleable by our code).
     */
    UINT previousErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS);

    if (!CreateProcess(
        NULL,           // Application name
        const_cast(commandLine.c_str()),
        NULL,           // Process handle not inheritable
        NULL,           // Thread handle not inheritable
        TRUE,           // Set handle inheritance to TRUE
        creationFlags , // Process creation flags
        NULL,           // Use parent's environment block
        NULL,           // Use parent's starting directory 
        &si,            // Pointer to STARTUPINFO structure
        &pi)            // Pointer to PROCESS_INFORMATION structure
        )
    {
        fwprintf(stderr, L"Could not execute '%s'. CreateProcess error (%d).\n", applicationName.c_str(), GetLastError());
        SetErrorMode(previousErrorMode);
        exit(3);
    }
    SetErrorMode(previousErrorMode);

    // Wait until child process exits.
    WaitForSingleObject(pi.hProcess, INFINITE);

    // Get process exit code to pass along
    DWORD exitCode;
    if (!GetExitCodeProcess(pi.hProcess, &exitCode))
    {
        fwprintf(stderr, L"GetExitCodeProcess failed (%d).\n", GetLastError());
        exit(4);
    }

    // Close process and thread handles. 
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    return (int)exitCode;
}

================================================
FILE: GVFS/GitHooksLoader/GitHooksLoader.vcxproj
================================================


  
    
      Debug
      x64
    
    
      Release
      x64
    
  
  
    {798DE293-6EDA-4DC4-9395-BE7A71C563E3}
    Win32Proj
    GitHooksLoader
    10.0
  
  
  
    Application
    true
    v143
    Unicode
  
  
    Application
    false
    v143
    true
    Unicode
  
  
  
  
  
  
  
    
  
  
    
  
  
  
    true
  
  
    false
  
  
    
      Use
      Level4
      Disabled
      _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0\ucrt;%(AdditionalIncludeDirectories)
      MultiThreadedDebug
    
    
      Console
      true
      C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64;%(AdditionalLibraryDirectories)
    
    
      $(IntDir)\$(MSBuildProjectName).log
    
    
      $(GeneratedIncludePath)
    
  
  
    
      Level4
      Use
      MaxSpeed
      true
      true
      NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0\ucrt;%(AdditionalIncludeDirectories)
      MultiThreaded
    
    
      Console
      true
      true
      true
      C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64;%(AdditionalLibraryDirectories)
    
    
      $(IntDir)\$(MSBuildProjectName).log
    
    
      $(GeneratedIncludePath)
    
  
  
    
    
    
  
  
    
    
      Create
      Create
    
  
  
    
  
  
  
  


================================================
FILE: GVFS/GitHooksLoader/GitHooksLoader.vcxproj.filters
================================================


  
    
      {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
      cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
    
    
      {93995380-89BD-4b04-88EB-625FBE52EBFB}
      h;hh;hpp;hxx;hm;inl;inc;xsd
    
    
      {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
      rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
    
  
  
    
      Header Files
    
    
      Header Files
    
    
      Header Files
    
  
  
    
      Source Files
    
    
      Source Files
    
  
  
    
      Resource Files
    
  


================================================
FILE: GVFS/GitHooksLoader/stdafx.cpp
================================================
// stdafx.cpp : source file that includes just the standard includes
// GitHooksLoader.pch will be the pre-compiled header
// stdafx.obj will contain the pre-compiled type information

#include "stdafx.h"

// TODO: reference any additional headers you need in STDAFX.H
// and not in this file


================================================
FILE: GVFS/GitHooksLoader/stdafx.h
================================================
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//

#pragma once

#include "targetver.h"
#include 
#include 



================================================
FILE: GVFS/GitHooksLoader/targetver.h
================================================
#pragma once

// Including SDKDDKVer.h defines the highest available Windows platform.

// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.

#include 


================================================
FILE: GVFS.sln
================================================

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastFetch", "GVFS\FastFetch\FastFetch.csproj", "{642D14C3-0332-4C95-8EE0-0EAC54CBF918}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS", "GVFS\GVFS\GVFS.csproj", "{DADCDF10-E38D-432E-9684-CE029DEE1D07}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.Common", "GVFS\GVFS.Common\GVFS.Common.csproj", "{77C8EC7B-4166-4F01-81C4-D9AB924021C0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.FunctionalTests", "GVFS\GVFS.FunctionalTests\GVFS.FunctionalTests.csproj", "{963F33D0-09EE-42CB-9E5A-37A4F4F1BFAB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.FunctionalTests.LockHolder", "GVFS\GVFS.FunctionalTests.LockHolder\GVFS.FunctionalTests.LockHolder.csproj", "{B26985C3-250A-4805-AA97-AD0604331AC7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.Hooks", "GVFS\GVFS.Hooks\GVFS.Hooks.csproj", "{EDB4A40E-CFC9-486A-BDC5-AB2951FD8EDC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.Mount", "GVFS\GVFS.Mount\GVFS.Mount.csproj", "{F96089C2-6D09-4349-B65D-9CCA6160C6A5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.MSBuild", "GVFS\GVFS.MSBuild\GVFS.MSBuild.csproj", "{39361E20-C7D3-43E5-A90E-5135457EABC0}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GVFS.NativeTests", "GVFS\GVFS.NativeTests\GVFS.NativeTests.vcxproj", "{3771C555-B5C1-45E2-B8B7-2CEF1619CDC5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.PerfProfiling", "GVFS\GVFS.PerfProfiling\GVFS.PerfProfiling.csproj", "{26B5D74F-972B-4B54-98C3-15958616E56D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.Platform.Windows", "GVFS\GVFS.Platform.Windows\GVFS.Platform.Windows.csproj", "{41A25DAD-698D-47AB-8BB1-7E622FE6FAAC}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GVFS.PostIndexChangedHook", "GVFS\GVFS.PostIndexChangedHook\GVFS.PostIndexChangedHook.vcxproj", "{24D161E9-D1F0-4299-BBD3-5D940BEDD535}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GVFS.ReadObjectHook", "GVFS\GVFS.ReadObjectHook\GVFS.ReadObjectHook.vcxproj", "{5A6656D5-81C7-472C-9DC8-32D071CB2258}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.Service", "GVFS\GVFS.Service\GVFS.Service.csproj", "{5E236AF3-31D7-4313-A129-F080FF058283}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.Tests", "GVFS\GVFS.Tests\GVFS.Tests.csproj", "{FE70E0D6-B0A6-421D-AA12-F28F822F09A0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.UnitTests", "GVFS\GVFS.UnitTests\GVFS.UnitTests.csproj", "{1A46C414-7F39-4EF0-B216-A88033D18678}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GVFS.VirtualFileSystemHook", "GVFS\GVFS.VirtualFileSystemHook\GVFS.VirtualFileSystemHook.vcxproj", "{2D23AB54-541F-4ABC-8DCA-08C199E97ABB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.Virtualization", "GVFS\GVFS.Virtualization\GVFS.Virtualization.csproj", "{EC90AF5D-E018-4248-85D6-9DB1898D710E}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GitHooksLoader", "GVFS\GitHooksLoader\GitHooksLoader.vcxproj", "{798DE293-6EDA-4DC4-9395-BE7A71C563E3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.Payload", "GVFS\GVFS.Payload\GVFS.Payload.csproj", "{A40DD1DC-2D35-4215-9FA0-3990FB7182FD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GVFS.Installers", "GVFS\GVFS.Installers\GVFS.Installers.csproj", "{258FEAC0-5E2D-408A-9652-9E9653219F3B}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|x64 = Debug|x64
		Release|x64 = Release|x64
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{642D14C3-0332-4C95-8EE0-0EAC54CBF918}.Debug|x64.ActiveCfg = Debug|Any CPU
		{642D14C3-0332-4C95-8EE0-0EAC54CBF918}.Debug|x64.Build.0 = Debug|Any CPU
		{642D14C3-0332-4C95-8EE0-0EAC54CBF918}.Release|x64.ActiveCfg = Release|Any CPU
		{642D14C3-0332-4C95-8EE0-0EAC54CBF918}.Release|x64.Build.0 = Release|Any CPU
		{DADCDF10-E38D-432E-9684-CE029DEE1D07}.Debug|x64.ActiveCfg = Debug|Any CPU
		{DADCDF10-E38D-432E-9684-CE029DEE1D07}.Debug|x64.Build.0 = Debug|Any CPU
		{DADCDF10-E38D-432E-9684-CE029DEE1D07}.Release|x64.ActiveCfg = Release|Any CPU
		{DADCDF10-E38D-432E-9684-CE029DEE1D07}.Release|x64.Build.0 = Release|Any CPU
		{77C8EC7B-4166-4F01-81C4-D9AB924021C0}.Debug|x64.ActiveCfg = Debug|Any CPU
		{77C8EC7B-4166-4F01-81C4-D9AB924021C0}.Debug|x64.Build.0 = Debug|Any CPU
		{77C8EC7B-4166-4F01-81C4-D9AB924021C0}.Release|x64.ActiveCfg = Release|Any CPU
		{77C8EC7B-4166-4F01-81C4-D9AB924021C0}.Release|x64.Build.0 = Release|Any CPU
		{963F33D0-09EE-42CB-9E5A-37A4F4F1BFAB}.Debug|x64.ActiveCfg = Debug|Any CPU
		{963F33D0-09EE-42CB-9E5A-37A4F4F1BFAB}.Debug|x64.Build.0 = Debug|Any CPU
		{963F33D0-09EE-42CB-9E5A-37A4F4F1BFAB}.Release|x64.ActiveCfg = Release|Any CPU
		{963F33D0-09EE-42CB-9E5A-37A4F4F1BFAB}.Release|x64.Build.0 = Release|Any CPU
		{B26985C3-250A-4805-AA97-AD0604331AC7}.Debug|x64.ActiveCfg = Debug|Any CPU
		{B26985C3-250A-4805-AA97-AD0604331AC7}.Debug|x64.Build.0 = Debug|Any CPU
		{B26985C3-250A-4805-AA97-AD0604331AC7}.Release|x64.ActiveCfg = Release|Any CPU
		{B26985C3-250A-4805-AA97-AD0604331AC7}.Release|x64.Build.0 = Release|Any CPU
		{EDB4A40E-CFC9-486A-BDC5-AB2951FD8EDC}.Debug|x64.ActiveCfg = Debug|Any CPU
		{EDB4A40E-CFC9-486A-BDC5-AB2951FD8EDC}.Debug|x64.Build.0 = Debug|Any CPU
		{EDB4A40E-CFC9-486A-BDC5-AB2951FD8EDC}.Release|x64.ActiveCfg = Release|Any CPU
		{EDB4A40E-CFC9-486A-BDC5-AB2951FD8EDC}.Release|x64.Build.0 = Release|Any CPU
		{F96089C2-6D09-4349-B65D-9CCA6160C6A5}.Debug|x64.ActiveCfg = Debug|Any CPU
		{F96089C2-6D09-4349-B65D-9CCA6160C6A5}.Debug|x64.Build.0 = Debug|Any CPU
		{F96089C2-6D09-4349-B65D-9CCA6160C6A5}.Release|x64.ActiveCfg = Release|Any CPU
		{F96089C2-6D09-4349-B65D-9CCA6160C6A5}.Release|x64.Build.0 = Release|Any CPU
		{39361E20-C7D3-43E5-A90E-5135457EABC0}.Debug|x64.ActiveCfg = Debug|Any CPU
		{39361E20-C7D3-43E5-A90E-5135457EABC0}.Debug|x64.Build.0 = Debug|Any CPU
		{39361E20-C7D3-43E5-A90E-5135457EABC0}.Release|x64.ActiveCfg = Release|Any CPU
		{39361E20-C7D3-43E5-A90E-5135457EABC0}.Release|x64.Build.0 = Release|Any CPU
		{3771C555-B5C1-45E2-B8B7-2CEF1619CDC5}.Debug|x64.ActiveCfg = Debug|x64
		{3771C555-B5C1-45E2-B8B7-2CEF1619CDC5}.Debug|x64.Build.0 = Debug|x64
		{3771C555-B5C1-45E2-B8B7-2CEF1619CDC5}.Release|x64.ActiveCfg = Release|x64
		{3771C555-B5C1-45E2-B8B7-2CEF1619CDC5}.Release|x64.Build.0 = Release|x64
		{26B5D74F-972B-4B54-98C3-15958616E56D}.Debug|x64.ActiveCfg = Debug|Any CPU
		{26B5D74F-972B-4B54-98C3-15958616E56D}.Debug|x64.Build.0 = Debug|Any CPU
		{26B5D74F-972B-4B54-98C3-15958616E56D}.Release|x64.ActiveCfg = Release|Any CPU
		{26B5D74F-972B-4B54-98C3-15958616E56D}.Release|x64.Build.0 = Release|Any CPU
		{41A25DAD-698D-47AB-8BB1-7E622FE6FAAC}.Debug|x64.ActiveCfg = Debug|Any CPU
		{41A25DAD-698D-47AB-8BB1-7E622FE6FAAC}.Debug|x64.Build.0 = Debug|Any CPU
		{41A25DAD-698D-47AB-8BB1-7E622FE6FAAC}.Release|x64.ActiveCfg = Release|Any CPU
		{41A25DAD-698D-47AB-8BB1-7E622FE6FAAC}.Release|x64.Build.0 = Release|Any CPU
		{24D161E9-D1F0-4299-BBD3-5D940BEDD535}.Debug|x64.ActiveCfg = Debug|x64
		{24D161E9-D1F0-4299-BBD3-5D940BEDD535}.Debug|x64.Build.0 = Debug|x64
		{24D161E9-D1F0-4299-BBD3-5D940BEDD535}.Release|x64.ActiveCfg = Release|x64
		{24D161E9-D1F0-4299-BBD3-5D940BEDD535}.Release|x64.Build.0 = Release|x64
		{5A6656D5-81C7-472C-9DC8-32D071CB2258}.Debug|x64.ActiveCfg = Debug|x64
		{5A6656D5-81C7-472C-9DC8-32D071CB2258}.Debug|x64.Build.0 = Debug|x64
		{5A6656D5-81C7-472C-9DC8-32D071CB2258}.Release|x64.ActiveCfg = Release|x64
		{5A6656D5-81C7-472C-9DC8-32D071CB2258}.Release|x64.Build.0 = Release|x64
		{5E236AF3-31D7-4313-A129-F080FF058283}.Debug|x64.ActiveCfg = Debug|Any CPU
		{5E236AF3-31D7-4313-A129-F080FF058283}.Debug|x64.Build.0 = Debug|Any CPU
		{5E236AF3-31D7-4313-A129-F080FF058283}.Release|x64.ActiveCfg = Release|Any CPU
		{5E236AF3-31D7-4313-A129-F080FF058283}.Release|x64.Build.0 = Release|Any CPU
		{FE70E0D6-B0A6-421D-AA12-F28F822F09A0}.Debug|x64.ActiveCfg = Debug|Any CPU
		{FE70E0D6-B0A6-421D-AA12-F28F822F09A0}.Debug|x64.Build.0 = Debug|Any CPU
		{FE70E0D6-B0A6-421D-AA12-F28F822F09A0}.Release|x64.ActiveCfg = Release|Any CPU
		{FE70E0D6-B0A6-421D-AA12-F28F822F09A0}.Release|x64.Build.0 = Release|Any CPU
		{1A46C414-7F39-4EF0-B216-A88033D18678}.Debug|x64.ActiveCfg = Debug|Any CPU
		{1A46C414-7F39-4EF0-B216-A88033D18678}.Debug|x64.Build.0 = Debug|Any CPU
		{1A46C414-7F39-4EF0-B216-A88033D18678}.Release|x64.ActiveCfg = Release|Any CPU
		{1A46C414-7F39-4EF0-B216-A88033D18678}.Release|x64.Build.0 = Release|Any CPU
		{2D23AB54-541F-4ABC-8DCA-08C199E97ABB}.Debug|x64.ActiveCfg = Debug|x64
		{2D23AB54-541F-4ABC-8DCA-08C199E97ABB}.Debug|x64.Build.0 = Debug|x64
		{2D23AB54-541F-4ABC-8DCA-08C199E97ABB}.Release|x64.ActiveCfg = Release|x64
		{2D23AB54-541F-4ABC-8DCA-08C199E97ABB}.Release|x64.Build.0 = Release|x64
		{EC90AF5D-E018-4248-85D6-9DB1898D710E}.Debug|x64.ActiveCfg = Debug|Any CPU
		{EC90AF5D-E018-4248-85D6-9DB1898D710E}.Debug|x64.Build.0 = Debug|Any CPU
		{EC90AF5D-E018-4248-85D6-9DB1898D710E}.Release|x64.ActiveCfg = Release|Any CPU
		{EC90AF5D-E018-4248-85D6-9DB1898D710E}.Release|x64.Build.0 = Release|Any CPU
		{798DE293-6EDA-4DC4-9395-BE7A71C563E3}.Debug|x64.ActiveCfg = Debug|x64
		{798DE293-6EDA-4DC4-9395-BE7A71C563E3}.Debug|x64.Build.0 = Debug|x64
		{798DE293-6EDA-4DC4-9395-BE7A71C563E3}.Release|x64.ActiveCfg = Release|x64
		{798DE293-6EDA-4DC4-9395-BE7A71C563E3}.Release|x64.Build.0 = Release|x64
		{A40DD1DC-2D35-4215-9FA0-3990FB7182FD}.Debug|x64.ActiveCfg = Debug|Any CPU
		{A40DD1DC-2D35-4215-9FA0-3990FB7182FD}.Debug|x64.Build.0 = Debug|Any CPU
		{A40DD1DC-2D35-4215-9FA0-3990FB7182FD}.Release|x64.ActiveCfg = Release|Any CPU
		{A40DD1DC-2D35-4215-9FA0-3990FB7182FD}.Release|x64.Build.0 = Release|Any CPU
		{258FEAC0-5E2D-408A-9652-9E9653219F3B}.Debug|x64.ActiveCfg = Debug|Any CPU
		{258FEAC0-5E2D-408A-9652-9E9653219F3B}.Debug|x64.Build.0 = Debug|Any CPU
		{258FEAC0-5E2D-408A-9652-9E9653219F3B}.Release|x64.ActiveCfg = Release|Any CPU
		{258FEAC0-5E2D-408A-9652-9E9653219F3B}.Release|x64.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {C506C09B-011F-491F-9D17-D0E2BA0B3467}
	EndGlobalSection
EndGlobal


================================================
FILE: GvFlt_EULA.md
================================================
# MICROSOFT SOFTWARE LICENSE TERMS
## Microsoft GvFlt

These license terms are an agreement between you and Microsoft Corporation (or one of its affiliates). They apply to the software named above and any Microsoft services or software updates (except to the extent such services or updates are accompanied by new or additional terms, in which case those different terms apply prospectively and do not alter your or Microsoft’s rights relating to pre-updated software or services). IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW. BY USING THE SOFTWARE, YOU ACCEPT THESE TERMS.
1. **INSTALLATION AND USE RIGHTS.** You may install and use any number of copies of the software on your devices, solely for use with Microsoft Git Virtual File System (GVFS) and otherwise for your internal business purposes. You may not use the software in a live operating environment unless Microsoft permits you to do so under another agreement.
2. **PRE-RELEASE SOFTWARE.** The software is a pre-release version. It may not operate correctly. It may be different from the commercially released version.
3. **FEEDBACK.** If you give feedback about the software to Microsoft, you give to Microsoft, without charge, the right to use, share and commercialize your feedback in any way and for any purpose. You will not give feedback that is subject to a license that requires Microsoft to license its software or documentation to third parties because Microsoft includes your feedback in them. These rights survive this agreement.
4. **DATA COLLECTION.** The software may collect information about you and your use of the software and send that to Microsoft. Microsoft may use this information to provide services and improve Microsoft’s products and services. Your opt-out rights, if any, are described in the product documentation. Some features in the software may enable collection of data from users of your applications that access or use the software. If you use these features to enable data collection in your applications, you must comply with applicable law, including getting any required user consent, and maintain a prominent privacy policy that accurately informs users about how you use, collect, and share their data. You can learn more about Microsoft’s data collection and use in the product documentation and the Microsoft Privacy Statement at https://go.microsoft.com/fwlink/?LinkId=521839. You agree to comply with all applicable provisions of the Microsoft Privacy Statement.
5. **SCOPE OF LICENSE.** The software is licensed, not sold. Microsoft reserves all other rights. Unless applicable law gives you more rights despite this limitation, you will not (and have no right to):
    1. work around any technical limitations in the software that only allow you to use it in certain ways;
    2. reverse engineer, decompile or disassemble the software;
    3. remove, minimize, block, or modify any notices of Microsoft or its suppliers in the software;
    4. use the software for commercial, non-profit, or revenue-generating activities;
    5. use the software in any way that is against the law or to create or propagate malware; or
    6. share, publish, distribute, or lend the software, provide the software as a stand-alone hosted solution for others to use, or transfer the software or this agreement to any third party.
6. **EXPORT RESTRICTIONS.** You must comply with all domestic and international export laws and regulations that apply to the software, which include restrictions on destinations, end users, and end use. For further information on export restrictions, visit http://aka.ms/exporting.
7. **SUPPORT SERVICES.** Microsoft is not obligated under this agreement to provide any support services for the software. Any support provided is “as is”, “with all faults”, and without warranty of any kind.
8. **UPDATES.** The software may periodically check for updates, and download and install them for you. You may obtain updates only from Microsoft or authorized sources. Microsoft may need to update your system to provide you with updates. You agree to receive these automatic updates without any additional notice. Updates may not include or support all existing software features, services, or peripheral devices.
9. **ENTIRE AGREEMENT.** This agreement, and any other terms Microsoft may provide for supplements, updates, or third-party applications, is the entire agreement for the software.
10. **APPLICABLE LAW AND PLACE TO RESOLVE DISPUTES.** If you acquired the software in the United States or Canada, the laws of the state or province where you live (or, if a business, where your principal place of business is located) govern the interpretation of this agreement, claims for its breach, and all other claims (including consumer protection, unfair competition, and tort claims), regardless of conflict of laws principles. If you acquired the software in any other country, its laws apply. If U.S. federal jurisdiction exists, you and Microsoft consent to exclusive jurisdiction and venue in the federal court in King County, Washington for all disputes heard in court. If not, you and Microsoft consent to exclusive jurisdiction and venue in the Superior Court of King County, Washington for all disputes heard in court.
11. **CONSUMER RIGHTS; REGIONAL VARIATIONS.** This agreement describes certain legal rights. You may have other rights, including consumer rights, under the laws of your state, province, or country. Separate and apart from your relationship with Microsoft, you may also have rights with respect to the party from which you acquired the software. This agreement does not change those other rights if the laws of your state, province, or country do not permit it to do so. For example, if you acquired the software in one of the below regions, or mandatory country law applies, then the following provisions apply to you:
    1. **Australia.** You have statutory guarantees under the Australian Consumer Law and nothing in this agreement is intended to affect those rights.
    2. **Canada.** If you acquired this software in Canada, you may stop receiving updates by turning off the automatic update feature, disconnecting your device from the Internet (if and when you re-connect to the Internet, however, the software will resume checking for and installing updates), or uninstalling the software. The product documentation, if any, may also specify how to turn off updates for your specific device or software.
    3. **Germany and Austria.**
        1. Warranty. The properly licensed software will perform substantially as described in any Microsoft materials that accompany the software. However, Microsoft gives no contractual guarantee in relation to the licensed software.
        2. Limitation of Liability. In case of intentional conduct, gross negligence, claims based on the Product Liability Act, as well as, in case of death or personal or physical injury, Microsoft is liable according to the statutory law.

        Subject to the foregoing clause 11.3.2, Microsoft will only be liable for slight negligence if Microsoft is in breach of such material contractual obligations, the fulfillment of which facilitate the due performance of this agreement, the breach of which would endanger the purpose of this agreement and the compliance with which a party may constantly trust in (so-called "cardinal obligations"). In other cases of slight negligence, Microsoft will not be liable for slight negligence.

12. **DISCLAIMER OF WARRANTY.** THE SOFTWARE IS LICENSED “AS IS.” YOU BEAR THE RISK OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES, OR CONDITIONS. TO THE EXTENT PERMITTED UNDER APPLICABLE LAWS, MICROSOFT EXCLUDES ALL IMPLIED WARRANTIES, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
13.	**LIMITATION ON AND EXCLUSION OF DAMAGES.** IF YOU HAVE ANY BASIS FOR RECOVERING DAMAGES DESPITE THE PRECEDING DISCLAIMER OF WARRANTY, YOU CAN RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES.

**This limitation applies to (a) anything related to the software, services, content (including code) on third party Internet sites, or third party applications; and (b) claims for breach of contract, warranty, guarantee, or condition; strict liability, negligence, or other tort; or any other claim; in each case to the extent permitted by applicable law.**

**It also applies even if Microsoft knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your state, province, or country may not allow the exclusion or limitation of incidental, consequential, or other damages.**

**Please note: As this software is distributed in Canada, some of the clauses in this agreement are provided below in French.**

**Remarque: Ce logiciel étant distribué au Canada, certaines des clauses dans ce contrat sont fournies ci-dessous en français.**

**EXONÉRATION DE GARANTIE. Le logiciel visé par une licence est offert « tel quel ». Toute utilisation de ce logiciel est à votre seule risque et péril. Microsoft n’accorde aucune autre garantie expresse. Vous pouvez bénéficier de droits additionnels en vertu du droit local sur la protection des consommateurs, que ce contrat ne peut modifier. La ou elles sont permises par le droit locale, les garanties implicites de qualité marchande, d’adéquation à un usage particulier et d’absence de contrefaçon sont exclues.**

**LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES DOMMAGES. Vous pouvez obtenir de Microsoft et de ses fournisseurs une indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US. Vous ne pouvez prétendre à aucune indemnisation pour les autres dommages, y compris les dommages spéciaux, indirects ou accessoires et pertes de bénéfices.
Cette limitation concerne:**

* **tout ce qui est relié au logiciel, aux services ou au contenu (y compris le code) figurant sur des sites Internet tiers ou dans des programmes tiers; et**
* **les réclamations au titre de violation de contrat ou de garantie, ou au titre de responsabilité stricte, de négligence ou d’une autre faute dans la limite autorisée par la loi en vigueur.**

**Elle s’applique également, même si Microsoft connaissait ou devrait connaître l’éventualité d’un tel dommage. Si votre pays n’autorise pas l’exclusion ou la limitation de responsabilité pour les dommages indirects, accessoires ou de quelque nature que ce soit, il se peut que la limitation ou l’exclusion ci-dessus ne s’appliquera pas à votre égard.**

**EFFET JURIDIQUE. Le présent contrat décrit certains droits juridiques. Vous pourriez avoir d’autres droits prévus par les lois de votre pays. Le présent contrat ne modifie pas les droits que vous confèrent les lois de votre pays si celles-ci ne le permettent pas.**


================================================
FILE: License.md
================================================
    MIT License

    Copyright (c) Microsoft Corporation. All rights reserved.

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE

================================================
FILE: Protocol.md
================================================
# The GVFS Protocol (v1)

The GVFS network protocol consists of four operations on three endpoints. In summary:
* `GET /gvfs/objects/{objectId}`
  * Provides a single object in loose-object format
* `POST /gvfs/objects`
  * Provides one or more objects in packfile or streaming loose object format
* `GET /gvfs/prefetch[?lastPackTimestamp={secondsSinceEpoch}]`
  * Provides one or more packfiles of non-blobs and optionally packfile indexes in a streaming format
* `POST /gvfs/sizes`
  * Provides the uncompressed, undeltified size of one or more objects
* `GET /gvfs/config`
  * Provides server-set client configuration options

# `GET /gvfs/objects/{objectId}`
Will return a single object in compressed loose object format, which can be directly
written to `.git/xx/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy` if desired. The request/response looks
similar to the "Dumb Protocol" as described [here](https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols).

# `POST /gvfs/objects`
Will return multiple objects, possibly more than the client requested based on request parameters.

The request consists of a JSON body with the following format:
```
{
    "objectIds" : [ {JSON array of SHA-1 object IDs, as strings} ],
    "commitDepth" : {positive integer}
}
```

For example,
```
{
    "objectIds" : [
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    ],
    "commitDepth" : 1
}
```

## `Accept: application/x-git-packfile` (the default)

If
* An `Accept` header of  `application/x-git-packfile` is specified, or 
* No `Accept` header is specified

then a git packfile, indexable via `index-pack`, will be returned to the client.

If `objectIds` includes a `commit`, then all `tree`s recursively referenced by that commit are also returned. 
If any other object type is requested (`tree`, `blob`, or `tag`), then only that object will be returned.

`commitDepth` - if the requested object is a `commit`, all parents up to `n` levels deep will be returned, along
with all their trees as previously described. Does not include any `blob`s.

## `Accept: application/x-gvfs-loose-objects`

**NOTE**: This format is currently only supposed by the cache server, not by VSTS.

To enable scenarios where multiple objects are required, but less overhead would be incurred by using pre-existing
loose objects (e.g. on a caching proxy), an alternative, packfile-like response format that contains loose objects 
is also supported.

To receive objects in this format, the client **MUST** supply an `Accept` header of `application/x-gvfs-loose-objects` 
to the `POST /gvfs/objects` endpoint. Otherwise, the response format will be `application/x-git-packfile`.

This format will **NOT** perform any `commit` to `tree` expansion, and will return an error if a `commitDepth`
greater than `1` is supplied. Said another way, this `Accept`/return type has no concept of "implicitly-requested"
objects.

### Version 1
* Integers are signed and little-endian, unless otherwise specified
* Byte offset 0 is the first byte in the file
* Index offset 0 is the first byte in the first element of an array
* `num_objects` represents the variable number of objects in the file/response

```
Count            Size (bytes)    Chunk Description

HEADER
                +-------------------------------------------------------------------------------+
1               |          5 | UTF-8 encoded 'GVFS '                                            |
                |          1 | Unsigned byte version number. Currently, 1.                      |
                +-------------------------------------------------------------------------------+

OBJECT CONTENT
                +-------------------------------------------------------------------------------+
num_objects     |         20 | SHA-1 ID of the object.                                          |
                |          8 | Signed-long length of the object.                                |
                |   variable | Compressed, raw loose object content.                            |
                +-------------------------------------------------------------------------------+

TRAILER
                +-------------------------------------------------------------------------------+
1               |         20 | Zero bytes                                                       |
                +-------------------------------------------------------------------------------+
```

# `GET /gvfs/prefetch[?lastPackTimestamp={secondsSinceEpoch}]`

To enable the reuse of already-existing packfiles and indexes, a custom format for transmitting these files
is supported. The `prefetch` endpoint will return one or more packfiles of **non-blob** objects.  

If the optional `lastPackTimestamp` query parameter is supplied, only packs created by the server
after the specific Unix epoch time (approximately, ±10 minutes or so) will be returned. Generally, these packs 
will contain only objects introduced to the repository after that UTC-based timestamp, but will not contain
**all** objects introduced after that timestamp.

A media-type of `application/x-gvfs-timestamped-packfiles-indexes` will be returned from this endpoint.

## Response format

* Integers are signed and little-endian, unless otherwise specified
* Byte offset 0 is the first byte in the file
* Index offset 0 is the first byte in the first element of an array
* `num_packs` represents the variable number of packs in the file/response

### Version 1

```
Count            Size (bytes)    Chunk Description

HEADER
                +-------------------------------------------------------------------------------+
1               |          5 | UTF-8 encoded 'GPRE '                                            |
                |          1 | Unsigned byte version number. Currently, 1.                      |
                +-------------------------------------------------------------------------------+

CONTENT

                +-------------------------------------------------------------------------------+
1               |          2 | Unsigned short number of packs. `num_packs`.                     |
                +-------------------------------------------------------------------------------+

                +-------------------------------------------------------------------------------+
num_packs       |          8 | Signed-long pack timestamp in seconds since UTC epoch.           |
                |          8 | Signed-long length of the pack.                                  |
                |          8 | Signed-long length of the pack index. -1 indicates no index.     |
                |   variable | Pack contents.                                                   |
                |   variable | Pack index contents.                                             |
                +-------------------------------------------------------------------------------+
```

Packs **MUST** be sent in increasing `timestamp` order. In the case of a failed connection, this allows the 
client to keep the packs it received successfully and "resume" by sending the highest completed timestamp.

# `POST /gvfs/sizes`
Will return the uncompressed, undeltified length of the requested objects in JSON format.

The request consists of a JSON body with the following format:
```
[ {JSON array of SHA-1 object IDs, as strings} ]
```

For example, a request of:
```
[
    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
]
```

will result in a a response like:
```
[
    {
        "Id" : "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "Size" : 123
    },
    {
        "Id" : "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
        "Size" : 456
    }
]
```

# `GET /gvfs/config`
This optional endpoint will return all server-set GVFS client configuration options. It currently
provides:

* A set of allowed GVFS client version ranges, in order to block older clients from running in 
certain scenarios. For example, a data corruption bug may be found and encouraging clients to 
avoid that version is desirable.
* A list of available cache servers, each describing their url and default-ness with a friendly name
that users can use to inform which cache server to use. Note that the names "None" and "User Defined" 
are reserved by GVFS. Any caches with these names may cause undefined behavior in the GVFS client.

An example response is provided below. Note that the `null` `"Max"` value is only allowed for the last
(or greatest) range, since it logically excludes greater version numbers from having an effect.
```
{
	"AllowedGvfsClientVersions": [{
		"Max": {
			"Major": 0,
			"Minor": 4,
			"Build": 0,
			"Revision": 0
		},
		"Min": {
			"Major": 0,
			"Minor": 2,
			"Build": 0,
			"Revision": 0
		}
	}, {
		"Max": {
			"Major": 0,
			"Minor": 5,
			"Build": 0,
			"Revision": 0
		},
		"Min": {
			"Major": 0,
			"Minor": 4,
			"Build": 17009,
			"Revision": 1
		}
	}, {
		"Max": null,
		"Min": {
			"Major": 0,
			"Minor": 5,
			"Build": 16326,
			"Revision": 1
		}
	}],
	"CacheServers": [{
		"Url": "https://redmond-cache-machine/repo-id",
		"Name": "Redmond",
		"GlobalDefault": true
	}, {
		"Url": "https://dublin-cache-machine/repo-id",
		"Name": "Dublin",
		"GlobalDefault": false
	}]
}
```


================================================
FILE: Readme.md
================================================
# VFS for Git

**Notice:** With the release of VFS for Git 2.32, VFS for Git is in maintenance mode. Only required updates as a reaction to critical security vulnerabilities will prompt a release.

|Branch|Unit Tests|Functional Tests|Large Repo Perf|Large Repo Build|
|:--:|:--:|:--:|:--:|:--:|
|**master**|[![Build status](https://dev.azure.com/gvfs/ci/_apis/build/status/CI%20-%20Windows?branchName=master)](https://dev.azure.com/gvfs/ci/_build/latest?definitionId=7&branchName=master)|[![Build status](https://dev.azure.com/gvfs/ci/_apis/build/status/CI%20-%20Windows%20-%20Full%20Functional%20Tests?branchName=master)](https://dev.azure.com/gvfs/ci/_build/latest?definitionId=6&branchName=master)|[![Build status](https://dev.azure.com/mseng/AzureDevOps/_apis/build/status/GVFS/GitHub%20VFSForGit%20Large%20Repo%20Perf%20Tests?branchName=master)](https://dev.azure.com/mseng/AzureDevOps/_build/latest?definitionId=7179&branchName=master)|[![Build status](https://dev.azure.com/mseng/AzureDevOps/_apis/build/status/GVFS/GitHub%20VFSForGit%20Large%20Repo%20Build?branchName=master)](https://dev.azure.com/mseng/AzureDevOps/_build/latest?definitionId=7180&branchName=master)|
|**shipped**|[![Build status](https://dev.azure.com/gvfs/ci/_apis/build/status/CI%20-%20Windows?branchName=releases%2Fshipped)](https://dev.azure.com/gvfs/ci/_build/latest?definitionId=7&branchName=releases%2Fshipped)|[![Build status](https://dev.azure.com/gvfs/ci/_apis/build/status/CI%20-%20Windows%20-%20Full%20Functional%20Tests?branchName=releases%2Fshipped)](https://dev.azure.com/gvfs/ci/_build/latest?definitionId=6&branchName=releases%2Fshipped)|[![Build status](https://dev.azure.com/mseng/AzureDevOps/_apis/build/status/GVFS/GitHub%20VFSForGit%20Large%20Repo%20Perf%20Tests?branchName=releases%2Fshipped)](https://dev.azure.com/mseng/AzureDevOps/_build/latest?definitionId=7179&branchName=releases%2Fshipped)|[![Build status](https://dev.azure.com/mseng/AzureDevOps/_apis/build/status/GVFS/GitHub%20VFSForGit%20Large%20Repo%20Build?branchName=releases%2Fshipped)](https://dev.azure.com/mseng/AzureDevOps/_build/latest?definitionId=7180&branchName=releases%2Fshipped)|

## What is VFS for Git?

VFS stands for Virtual File System. VFS for Git virtualizes the file system
beneath your Git repository so that Git and all tools see what appears to be a
regular working directory, but VFS for Git only downloads objects as they
are needed. VFS for Git also manages the files that Git will consider, to
ensure that Git operations such as `status`, `checkout`, etc., can be as quick
as possible because they will only consider the files that the user has
accessed, not all files in the repository.

Note: for new deployments, we strongly recommend you consider
[Scalar](https://github.com/microsoft/scalar) instead of VFS for Git. By
combining the lessons from operating VFS for Git at scale with new developments
in Git, Scalar offers a clearer path forward for all large monorepos.

## Installing VFS for Git

VFS for Git requires Windows 10 Anniversary Update (Windows 10 version 1607) or later.

To install, use [`winget`](https://github.com/microsoft/winget-cli) to install the
[`microsoft/git` fork of Git](https://github.com/microsoft/git) and VFS for Git
using:

```
winget install --id Microsoft.Git
winget install --id Microsoft.VFSforGit
```

You will need to continue using the `microsoft/git` version of Git, and it
will notify you when new versions are available.


## Building VFS for Git

If you'd like to build your own VFS for Git Windows installer:
* Install Visual Studio 2022 Community Edition or higher (https://www.visualstudio.com/downloads/).
  * Include the following workloads:
    * .NET desktop development
    * Desktop development with C++
    * .NET Core cross-platform development
  * Include the following additional components:
    * .NET Core runtime
    * Windows 10 or 11 SDK (10.0+)
* Install the .NET Core 8 SDK (https://www.microsoft.com/net/download/dotnet-core/8)
* Install [`nuget.exe`](https://www.nuget.org/downloads)
* Create a folder to clone into, e.g. `C:\Repos\VFSForGit`
* Clone this repo into the `src` subfolder, e.g. `C:\Repos\VFSForGit\src`
* Run `\src\Scripts\BuildGVFSForWindows.bat`
* You can also build in Visual Studio by opening `src\GVFS.sln` (do not upgrade any projects) and building. However, the very first
build will fail, and the second and subsequent builds will succeed. This is because the build requires a prebuild code generation step.
For details, see the build script in the previous step.

Visual Studio 2022 will [automatically prompt you to install these dependencies](https://devblogs.microsoft.com/setup/configure-visual-studio-across-your-organization-with-vsconfig/) when you open the solution. The .vsconfig file that is present in the root of the repository specifies all required components.

The installer can now be found at `C:\Repos\VFSForGit\BuildOutput\GVFS.Installer.Windows\bin\x64\[Debug|Release]\SetupGVFS..exe`

## Trying out VFS for Git

* VFS for Git requires a Git service that supports the
  [GVFS protocol](Protocol.md). For example, you can create a repo in
  [Azure DevOps](https://azure.microsoft.com/services/devops/), and push
  some contents to it. There are two constraints:
  * Your repo must not enable any clean/smudge filters
  * Your repo must have a `.gitattributes` file in the root that includes the line `* -text`
* `gvfs clone `
  * Please choose the **Clone with HTTPS** option in the `Clone Repository` dialog in Azure Repos, not **Clone with SSH**.
* `cd \src`
* Run Git commands as you normally would
* `gvfs unmount` when done

## Note on naming

This project was formerly known as GVFS (Git Virtual File System). You may occasionally
see collateral, including code and protocol names, which refer to the previous name.

# Licenses

The VFS for Git source code in this repo is available under the MIT license.
See [License.md](License.md).

VFS for Git relies on the PrjFlt filter driver, formerly known as the GvFlt
filter driver, available as a prerelease NuGet package.


================================================
FILE: SECURITY.md
================================================


## Security

Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).

If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.

## Reporting Security Issues

**Please do not report security vulnerabilities through public GitHub issues.**

Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).

If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com).  If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).

You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 

Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:

  * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
  * Full paths of source file(s) related to the manifestation of the issue
  * The location of the affected source code (tag/branch/commit or direct URL)
  * Any special configuration required to reproduce the issue
  * Step-by-step instructions to reproduce the issue
  * Proof-of-concept or exploit code (if possible)
  * Impact of the issue, including how an attacker might exploit the issue

This information will help us triage your report more quickly.

If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.

## Preferred Languages

We prefer all communications to be in English.

## Policy

Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).




================================================
FILE: ThirdPartyNotices.txt
================================================
microsoft-VFSForGit

THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
Do Not Translate or Localize

This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise.


1.     dtruss from Apple's dtrace distribution version 284.200.15 (https://opensource.apple.com/source/dtrace/dtrace-284.200.15/DTTk/)


dtrace NOTICES AND INFORMATION BEGIN HERE
=========================================
COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0


      1. Definitions.

            1.1. “Contributor” means each individual or entity that
            creates or contributes to the creation of Modifications.

            1.2. “Contributor Version” means the combination of the
            Original Software, prior Modifications used by a
            Contributor (if any), and the Modifications made by that
            particular Contributor.

            1.3. “Covered Software” means (a) the Original Software, or
            (b) Modifications, or (c) the combination of files
            containing Original Software with files containing
            Modifications, in each case including portions thereof.

            1.4. “Executable” means the Covered Software in any form
            other than Source Code. 

            1.5. “Initial Developer” means the individual or entity
            that first makes Original Software available under this
            License. 
            
            1.6. “Larger Work” means a work which combines Covered
            Software or portions thereof with code not governed by the
            terms of this License.

            1.7. “License” means this document.

            1.8. “Licensable” means having the right to grant, to the
            maximum extent possible, whether at the time of the initial
            grant or subsequently acquired, any and all of the rights
            conveyed herein.
            
            1.9. “Modifications” means the Source Code and Executable
            form of any of the following: 

                  A. Any file that results from an addition to,
                  deletion from or modification of the contents of a
                  file containing Original Software or previous
                  Modifications; 

                  B. Any new file that contains any part of the
                  Original Software or previous Modification; or 

                  C. Any new file that is contributed or otherwise made
                  available under the terms of this License.

            1.10. “Original Software” means the Source Code and
            Executable form of computer software code that is
            originally released under this License. 

            1.11. “Patent Claims” means any patent claim(s), now owned
            or hereafter acquired, including without limitation,
            method, process, and apparatus claims, in any patent
            Licensable by grantor. 

            1.12. “Source Code” means (a) the common form of computer
            software code in which modifications are made and (b)
            associated documentation included in or with such code.

            1.13. “You” (or “Your”) means an individual or a legal
            entity exercising rights under, and complying with all of
            the terms of, this License. For legal entities, “You”
            includes any entity which controls, is controlled by, or is
            under common control with You. For purposes of this
            definition, “control” means (a) the power, direct or
            indirect, to cause the direction or management of such
            entity, whether by contract or otherwise, or (b) ownership
            of more than fifty percent (50%) of the outstanding shares
            or beneficial ownership of such entity.

      2. License Grants. 

            2.1. The Initial Developer Grant.

            Conditioned upon Your compliance with Section 3.1 below and
            subject to third party intellectual property claims, the
            Initial Developer hereby grants You a world-wide,
            royalty-free, non-exclusive license: 

                  (a) under intellectual property rights (other than
                  patent or trademark) Licensable by Initial Developer,
                  to use, reproduce, modify, display, perform,
                  sublicense and distribute the Original Software (or
                  portions thereof), with or without Modifications,
                  and/or as part of a Larger Work; and 

                  (b) under Patent Claims infringed by the making,
                  using or selling of Original Software, to make, have
                  made, use, practice, sell, and offer for sale, and/or
                  otherwise dispose of the Original Software (or
                  portions thereof). 

                  (c) The licenses granted in Sections 2.1(a) and (b)
                  are effective on the date Initial Developer first
                  distributes or otherwise makes the Original Software
                  available to a third party under the terms of this
                  License. 

                  (d) Notwithstanding Section 2.1(b) above, no patent
                  license is granted: (1) for code that You delete from
                  the Original Software, or (2) for infringements
                  caused by: (i) the modification of the Original
                  Software, or (ii) the combination of the Original
                  Software with other software or devices. 

            2.2. Contributor Grant.

            Conditioned upon Your compliance with Section 3.1 below and
            subject to third party intellectual property claims, each
            Contributor hereby grants You a world-wide, royalty-free,
            non-exclusive license:

                  (a) under intellectual property rights (other than
                  patent or trademark) Licensable by Contributor to
                  use, reproduce, modify, display, perform, sublicense
                  and distribute the Modifications created by such
                  Contributor (or portions thereof), either on an
                  unmodified basis, with other Modifications, as
                  Covered Software and/or as part of a Larger Work; and
                  

                  (b) under Patent Claims infringed by the making,
                  using, or selling of Modifications made by that
                  Contributor either alone and/or in combination with
                  its Contributor Version (or portions of such
                  combination), to make, use, sell, offer for sale,
                  have made, and/or otherwise dispose of: (1)
                  Modifications made by that Contributor (or portions
                  thereof); and (2) the combination of Modifications
                  made by that Contributor with its Contributor Version
                  (or portions of such combination). 

                  (c) The licenses granted in Sections 2.2(a) and
                  2.2(b) are effective on the date Contributor first
                  distributes or otherwise makes the Modifications
                  available to a third party. 

                  (d) Notwithstanding Section 2.2(b) above, no patent
                  license is granted: (1) for any code that Contributor
                  has deleted from the Contributor Version; (2) for
                  infringements caused by: (i) third party
                  modifications of Contributor Version, or (ii) the
                  combination of Modifications made by that Contributor
                  with other software (except as part of the
                  Contributor Version) or other devices; or (3) under
                  Patent Claims infringed by Covered Software in the
                  absence of Modifications made by that Contributor. 

      3. Distribution Obligations.

            3.1. Availability of Source Code.

            Any Covered Software that You distribute or otherwise make
            available in Executable form must also be made available in
            Source Code form and that Source Code form must be
            distributed only under the terms of this License. You must
            include a copy of this License with every copy of the
            Source Code form of the Covered Software You distribute or
            otherwise make available. You must inform recipients of any
            such Covered Software in Executable form as to how they can
            obtain such Covered Software in Source Code form in a
            reasonable manner on or through a medium customarily used
            for software exchange.

            3.2. Modifications.

            The Modifications that You create or to which You
            contribute are governed by the terms of this License. You
            represent that You believe Your Modifications are Your
            original creation(s) and/or You have sufficient rights to
            grant the rights conveyed by this License.

            3.3. Required Notices.

            You must include a notice in each of Your Modifications
            that identifies You as the Contributor of the Modification.
            You may not remove or alter any copyright, patent or
            trademark notices contained within the Covered Software, or
            any notices of licensing or any descriptive text giving
            attribution to any Contributor or the Initial Developer.

            3.4. Application of Additional Terms.

            You may not offer or impose any terms on any Covered
            Software in Source Code form that alters or restricts the
            applicable version of this License or the recipients’
            rights hereunder. You may choose to offer, and to charge a
            fee for, warranty, support, indemnity or liability
            obligations to one or more recipients of Covered Software.
            However, you may do so only on Your own behalf, and not on
            behalf of the Initial Developer or any Contributor. You
            must make it absolutely clear that any such warranty,
            support, indemnity or liability obligation is offered by
            You alone, and You hereby agree to indemnify the Initial
            Developer and every Contributor for any liability incurred
            by the Initial Developer or such Contributor as a result of
            warranty, support, indemnity or liability terms You offer.
          

            3.5. Distribution of Executable Versions.

            You may distribute the Executable form of the Covered
            Software under the terms of this License or under the terms
            of a license of Your choice, which may contain terms
            different from this License, provided that You are in
            compliance with the terms of this License and that the
            license for the Executable form does not attempt to limit
            or alter the recipient’s rights in the Source Code form
            from the rights set forth in this License. If You
            distribute the Covered Software in Executable form under a
            different license, You must make it absolutely clear that
            any terms which differ from this License are offered by You
            alone, not by the Initial Developer or Contributor. You
            hereby agree to indemnify the Initial Developer and every
            Contributor for any liability incurred by the Initial
            Developer or such Contributor as a result of any such terms
            You offer.

            3.6. Larger Works.

            You may create a Larger Work by combining Covered Software
            with other code not governed by the terms of this License
            and distribute the Larger Work as a single product. In such
            a case, You must make sure the requirements of this License
            are fulfilled for the Covered Software. 
            
      4. Versions of the License. 

            4.1. New Versions.

            Sun Microsystems, Inc. is the initial license steward and
            may publish revised and/or new versions of this License
            from time to time. Each version will be given a
            distinguishing version number. Except as provided in
            Section 4.3, no one other than the license steward has the
            right to modify this License. 

            4.2. Effect of New Versions.

            You may always continue to use, distribute or otherwise
            make the Covered Software available under the terms of the
            version of the License under which You originally received
            the Covered Software. If the Initial Developer includes a
            notice in the Original Software prohibiting it from being
            distributed or otherwise made available under any
            subsequent version of the License, You must distribute and
            make the Covered Software available under the terms of the
            version of the License under which You originally received
            the Covered Software. Otherwise, You may also choose to
            use, distribute or otherwise make the Covered Software
            available under the terms of any subsequent version of the
            License published by the license steward. 

            4.3. Modified Versions.

            When You are an Initial Developer and You want to create a
            new license for Your Original Software, You may create and
            use a modified version of this License if You: (a) rename
            the license and remove any references to the name of the
            license steward (except to note that the license differs
            from this License); and (b) otherwise make it clear that
            the license contains terms which differ from this License.
            

      5. DISCLAIMER OF WARRANTY.

      COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN “AS IS”
      BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
      INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED
      SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR
      PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND
      PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY
      COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE
      INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF
      ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF
      WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
      ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS
      DISCLAIMER. 

      6. TERMINATION. 

            6.1. This License and the rights granted hereunder will
            terminate automatically if You fail to comply with terms
            herein and fail to cure such breach within 30 days of
            becoming aware of the breach. Provisions which, by their
            nature, must remain in effect beyond the termination of
            this License shall survive.

            6.2. If You assert a patent infringement claim (excluding
            declaratory judgment actions) against Initial Developer or
            a Contributor (the Initial Developer or Contributor against
            whom You assert such claim is referred to as “Participant”)
            alleging that the Participant Software (meaning the
            Contributor Version where the Participant is a Contributor
            or the Original Software where the Participant is the
            Initial Developer) directly or indirectly infringes any
            patent, then any and all rights granted directly or
            indirectly to You by such Participant, the Initial
            Developer (if the Initial Developer is not the Participant)
            and all Contributors under Sections 2.1 and/or 2.2 of this
            License shall, upon 60 days notice from Participant
            terminate prospectively and automatically at the expiration
            of such 60 day notice period, unless if within such 60 day
            period You withdraw Your claim with respect to the
            Participant Software against such Participant either
            unilaterally or pursuant to a written agreement with
            Participant.

            6.3. In the event of termination under Sections 6.1 or 6.2
            above, all end user licenses that have been validly granted
            by You or any distributor hereunder prior to termination
            (excluding licenses granted to You by any distributor)
            shall survive termination.

      7. LIMITATION OF LIABILITY.

      UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
      (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE
      INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF
      COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE
      LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR
      CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT
      LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK
      STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
      COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
      INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
      LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL
      INJURY RESULTING FROM SUCH PARTY’S NEGLIGENCE TO THE EXTENT
      APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO
      NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR
      CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT
      APPLY TO YOU.

      8. U.S. GOVERNMENT END USERS.

      The Covered Software is a “commercial item,” as that term is
      defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of “commercial
      computer software” (as that term is defined at 48 C.F.R. §
      252.227-7014(a)(1)) and “commercial computer software
      documentation” as such terms are used in 48 C.F.R. 12.212 (Sept.
      1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1
      through 227.7202-4 (June 1995), all U.S. Government End Users
      acquire Covered Software with only those rights set forth herein.
      This U.S. Government Rights clause is in lieu of, and supersedes,
      any other FAR, DFAR, or other clause or provision that addresses
      Government rights in computer software under this License.

      9. MISCELLANEOUS.

      This License represents the complete agreement concerning subject
      matter hereof. If any provision of this License is held to be
      unenforceable, such provision shall be reformed only to the
      extent necessary to make it enforceable. This License shall be
      governed by the law of the jurisdiction specified in a notice
      contained within the Original Software (except to the extent
      applicable law, if any, provides otherwise), excluding such
      jurisdiction’s conflict-of-law provisions. Any litigation
      relating to this License shall be subject to the jurisdiction of
      the courts located in the jurisdiction and venue specified in a
      notice contained within the Original Software, with the losing
      party responsible for costs, including, without limitation, court
      costs and reasonable attorneys’ fees and expenses. The
      application of the United Nations Convention on Contracts for the
      International Sale of Goods is expressly excluded. Any law or
      regulation which provides that the language of a contract shall
      be construed against the drafter shall not apply to this License.
      You agree that You alone are responsible for compliance with the
      United States export administration regulations (and the export
      control laws and regulation of any other countries) when You use,
      distribute or otherwise make available any Covered Software.

      10. RESPONSIBILITY FOR CLAIMS.

      As between Initial Developer and the Contributors, each party is
      responsible for claims and damages arising, directly or
      indirectly, out of its utilization of rights under this License
      and You agree to work with Initial Developer and Contributors to
      distribute such responsibility on an equitable basis. Nothing
      herein is intended or shall be deemed to constitute any admission
      of liability.


================================================
FILE: Version.props
================================================


  
    
    0.2.173.2

    
    v2.31.0.vfs.0.1
  




================================================
FILE: docs/faq.md
================================================
Frequently Asked Questions
==========================

Here are some questions that users often have with VFS for Git, but are
unrelated to [troubleshooting issues](troubleshooting.md).

### Why does `gvfs clone` create a `/src` folder?

VFS for Git integrates with ProjFS to keep track of changes under this `src` folder.
Any activity in this folder is assumed to be important to Git operations. By
creating the `src` folder, we are making it easy for your build system to
create output folders outside the `src` directory. We commonly see systems
create folders for build outputs and package downloads. VFS for Git creates
these folders during its builds.

Your build system may create build artifacts such as `.obj` or `.lib` files
next to your source code. These are commonly "hidden" from Git using
`.gitignore` files. Having such artifacts in your source tree creates
additional work for Git because it needs to look at these files and match them
against the `.gitignore` patterns.

By following the pattern VFS for Git tries to establish and placing your build
intermediates and outputs parallel with the `src` folder and not inside it,
you can help optimize Git command performance for developers in the repository
by limiting the number of files Git needs to consider for many common
operations.

### Why the name change?

This project was formerly known as GVFS (Git Virtual File System). It is
undergoing a rename to VFS for Git. While the rename is in progress, the
code, protocol, built executables, and releases may still refer to the old
GVFS name. See https://github.com/Microsoft/VFSForGit/projects/4 for the
latest status of the rename effort.

### Why only Windows? Wasn't there a macOS version coming?

We were working hard to deliver a macOS version, and there are still many
remnants of that effort in the codebase. We were heavily dependent upon the
macOS KAUTH kernel extensions to provide equivalent functionality of ProjFS on
Windows. Unfortunately, [Apple deprecated this feature in Catalina](https://developer.apple.com/support/kernel-extensions/).
All of the recommended alternatives were either not appropriate for our
scenario or were not fast enough.

We transitioned our large repository strategy to focus on using
[`git sparse-checkout`](https://github.blog/2020-01-17-bring-your-monorepo-down-to-size-with-sparse-checkout/)
instead of filesystem virtualization. We then forked the VFS for Git
codebase to create [Scalar](https://github.com/microsoft/scalar). Those
investments in a cross-platform tool paid off since Scalar could launch
quickly.

### Why are you abandoning VFS for Git?

We will continue supporting VFS for Git as long as there is a need for it.
Through our experience, we have found that it is appropriate for only a very
small number of extremely large repos. For instance, the Windows OS repository
will depend on VFS for Git for the foreseeable future, and we will continue
supporting them. This includes updating VFS for Git with new versions of Git.

The basic issue with VFS for Git is that users who don’t know exactly what’s
going on will frequently get into bad states, such as populating too much of
their working directory or not properly enabling the ProjFS feature (this is
a larger problem for users using older versions of Windows). The Windows OS
team developed a lot of tribal knowledge for how to avoid known issues with
VFS for Git.

We prefer users adopting Scalar because that is where we are investing most
of our engineering efforts. The system is simpler and has a better “offramp”
onto core Git, if one decides that they want to get there. We are also
investing in making the Git client do more of the heavy lifting and reducing
how much is really a “Scalar” feature and how much is just a Git feature.


================================================
FILE: docs/getting-started.md
================================================
Getting Started
===============

Repository Requirements
-----------------------

VFS for Git will work with any Git service that supports the
[GVFS protocol](/Protocol.md). For example, you can create a repo in
[Azure DevOps](https://azure.microsoft.com/services/devops/), and push
some contents to it. There are two constraints:

  * Your repo must not enable any clean/smudge filters
  * Your repo must have a `.gitattributes` file in the root that includes
    the line `* -text`


Cloning 
-------

The `clone` verb creates a local enlistment of a remote repository using the
[GVFS protocol](https://github.com/microsoft/VFSForGit/blob/master/Protocol.md).

```
gvfs clone [options]  []
```

Create a local copy of the repository at ``. If specified, create the ``
directory and place the repository there. Otherwise, the last section of the ``
will be used for ``. At the end, the repo is located at `/src`.

### Options

These options allow a user to customize their initial enlistment.

* `--cache-server-url=`: If specified, set the intended cache server to
  the specified ``. All object queries will use the GVFS protocol to this
  `` instead of the origin remote. If the remote supplies a list of
  cache servers via the `/gvfs/config` endpoint, then the `clone` command
  will select a nearby cache server from that list.

* `--branch=`: Specify the branch to checkout after clone.

* `--local-cache-path=`: Use this option to override the path for the
  local VFS for Git cache. If not specified, then a default path inside
  `:\.gvfsCache\` is used. The default cache path is recommended so
  multiple clones of the same remote repository share objects on the
  same device.

### Advanced Options

The options below are not intended for use by a typical user. These are
usually used by build machines to create a temporary enlistment that
operates on a single commit.

* `--single-branch`: Use this option to only download metadata for the branch
  that will be checked out. This is helpful for build machines that target
  a remote with many branches. Any `git fetch` commands after the clone will
  still ask for all branches.

* `--no-prefetch`: Use this option to not prefetch commits after clone. This
  is not recommended for anyone planning to use their clone for history
  traversal. Use of this option will make commands like `git log` or
  `git pull` extremely slow and is therefore not recommended.

Mounting and Unmounting
-----------------------

Before running Git commands in your VFS for Git enlistment or reading
files and folders inside the enlistment, a `GVFS.Mount` process must be
running to manage the virtual file system projection.

A mount process is started by a successful `gvfs clone`, and the
enlistment is registered with `GVFS.Service` to auto-mount in the future.

The `gvfs status` command checks to see if a mount process is currently
running for the current enlistment.

The `gvfs mount` command will start a new mount process and register the
enlistment for auto-mount in the future.

The `gvfs unmount` command will safely shut down the mount process and
unregister the enlistment for auto-mount.


Removing a VFS for Git Clone
----------------------------

Since a VFS for Git clone has a running `GVFS.Mount` process to track the
Git index and watch updates from the ProjFS filesystem driver, you must
first run `gvfs unmount` before deleting your repository. This will also
remove the repository from the auto-mount feature of `GVFS.Service`.

If you have deleted the enlistment or its `.gvfs` folder, then you will
likely see alerts saying "Failed to auto-mount at path `X`". To remove
this enlistment from the auto-mount feature, remove the appropriate line
from the `C:\ProgramData\GVFS\GVFS.Service\repo-registry` file.


================================================
FILE: docs/index.md
================================================
VFS for Git: Virtualized File System for Git
============================================

VFS stands for Virtual File System. VFS for Git virtualizes the file system
beneath your Git repository so that Git and all tools see what appears to be a
regular working directory, but VFS for Git only downloads objects as they
are needed. VFS for Git also manages the files that Git will consider, to
ensure that Git operations such as `status`, `checkout`, etc., can be as quick
as possible because they will only consider the files that the user has
accessed, not all files in the repository.

Installing
----------

* VFS for Git requires Windows 10 Anniversary Update (Windows 10 version 1607) or later
* Run the latest VFS for Git and Git for Windows installers from https://github.com/Microsoft/VFSForGit/releases

Documentation
-------------

* [Getting Started](getting-started.md): Get started with VFS for Git.
  Includes `gvfs clone`.

* [Troubleshooting](troubleshooting.md):
  Collect diagnostic information or update custom settings. Includes
  `gvfs diagnose`, `gvfs config`, `gvfs upgrade`, and `gvfs cache-server`.

* [Frequently Asked Questions](faq.md)


================================================
FILE: docs/troubleshooting.md
================================================
Troubleshooting
===============

Deleting a VFS for Git repo
---------------------------

You must follow these steps to delete a VFS for Git repository.

If you have attempted deletion before un-mounting jump to 
[Recovering from an attempt to delete without un-mounting](#Recovering-from-an-attempt-to-delete-without-un-mounting).


1. Un-mount the repo.

    Since a VFS for Git clone has a running `GVFS.Mount` process to track the
    Git index and watch updates from the ProjFS filesystem driver, you must
    first run `gvfs unmount` before deleting your repository. This will also
    remove the repository from the auto-mount feature of `GVFS.Service`.

    Make sure the current working directory of your shell is not in the VFS for Git 
    repo and that no other processes are using files in it. For example:

    ```
    C:\Users\you\big_repo\src\> cd ..\..
    C:\Users\you\> gvfs unmount big_repo
    ```

1. Clean up the remaining folder. (Do not try to delete the repo before it is un mounted.)

    Once un-mounted you can fully clean up the old repo by deleteing it. 
    Following the example from above:

    ```
    C:\Users\you\> rmdir /S /Q big_repo
    ```

### Recovering from an attempt to delete without un-mounting

  If you have attempted to delete the repo or its `.gvfs` folder, then you will
  likely see alerts saying "Failed to auto-mount at path `X`".
  
  1. Manually remove this repo from the auto-mount feature, remove the appropriate line
  from the `C:\ProgramData\GVFS\GVFS.Service\repo-registry` file.

  1. Ensure there is no currently running mount process for the repo.
      1. Open Task Manager.
      1. Go the `Details` tab.
      1. Right click in the header row (on `Name` for instance) to choose `Select Columns`.
      1. Check the `Command line` column which will show the full command line for each process.
      1. Look for a `GVFS.Mount.exe` process that has your repo in question in the command line arguments.
      1. If you find said process, right click and choose `End Task` to end it.

  1. Proceed with removing the directory as described above to clean up the folder 
     with `rmdir /S /Q REPO`.

Upgrade
-------

The `GVFS.Service` process checks for new versions of VFS for Git daily and
will prompt you for upgrade using a notification. To check manually, run
`gvfs upgrade` to see if an upgrade is available. Run `gvfs upgrade --confirm`
to actually perform the upgrade, if you wish.

### Upgrade fails with

**Symptom:** `gvfs upgrade` fails with the following error:

> ERROR: Could not launch upgrade tool. File copy error - The specified path, file name, or both are too long"

**Fix:** There is a known issue with VFS for Git v1.0.20112.1 where the
`gvfs upgrade` command fails with a long-path error. The root cause is a
[recursive directory copy](https://github.com/microsoft/VFSForGit/pull/1672)
that loops the contents of that directory into the copy and it never ends.
The only fix is to [manually upgrade to version v1.0.20154.3](https://github.com/microsoft/VFSForGit/releases/tag/v1.0.20154.3).

Common Issues
-------------

We are constantly improving VFS for Git to prevent known issues from
reoccurring. Please make sure you are on the latest version using `gvfs upgrade`
before raising an issue. Please also keep your version of Windows updated
as that is the only way to get updates to the ProjFS filesystem driver.

### TRY THIS FIRST: `gvfs repair`

Some known issues can get your enlistment into a bad state. Running

`gvfs repair` will detect known issues and the output will mention if the
issue is actionable or not. Then, `gvfs repair --confirm` will actually
make the changes it thinks are necessary.

If `gvfs repair` detects a problem but says it cannot fix the problem,
then that's an excellent message to include when creating an issue.

### TRY THIS NEXT: `gvfs unmount`, `gvfs mount`, or restart

Sometimes the `GVFS.Mount` process gets in a bad state and simply needs to
restart to auto-heal. Since VFS for Git also interacts directly with the
ProjFS filesystem driver, sometimes a system restart can help.

### Mismatched Git version

**Symptom:** Enlistment fails to mount with an error such as

```
Warning: Installed git version  does not match supported version of 
```

**Fix:** VFS for Git is tightly coupled to
[a custom version of Git](https://github.com/microsoft/git). This error
happens when the installed version of Git does not match the one that was
included in the VFS for Git installer. Please download and install the
matching version of Git from
[the VFS for Git releases page](https://github.com/microsoft/vfsforgit/releases).

### 404 Errors, or "The Git repository with name or identifier X does not exist..."

If your `gvfs clone ` command fails with this error, then check if you
can access `` in a web browser. If you cannot see the repository in the
web, then you do not have permissions to read that repository. These issues
cannot be resolved by the VFS for Git team and must be done by your repository
administrators.

If you _can_ see the repository in the web, then likely you have a stale
credential in your credential manager that needs updating. VFS for Git
_should_ attempt to renew your credential. If it does not, then go to
Windows Credential Manager and delete the Git credential for that URL.

### ProjFS Installation Issue

**Symptoms:**

- VFS for Git will not mount
- The mount logs (or mount CLI) have a ProjFS related error.
Examples:
   - "Service prjflt was not found on computer".
    - "Could not load file or assembly 'ProjectedFSLib.Managed.dll' or one
      of its dependencies. The specified module could not be found."
    - "Attaching the filter resulted in: 2149515283"
    - "StartVirtualizationInstance failed: 80070057(InvalidArg)"

**Fix:**

The easiest way to fix ProjFS issues is to completely remove ProjFS, and 
then rely on `GVFS.Service` to re-install (or re-enable) ProjFS.

1. If `C:\Program Files\GVFS\ProjectedFSLib.dll` is present, delete it
2. Determine if inbox ProjFS is currently enabled:

*In an Administrator Command Prompt*

```powershell -NonInteractive -NoProfile -Command "&{Get-WindowsOptionalFeature -Online -FeatureName Client-ProjFS}"```

Check the value of `State:` in the output to see if inbox ProjFS is enabled.

3. If ProjFS **is enabled**
   - Restart machine
   - Attempt to mount
   - If mount fails, manually disable ProjFS:
       - Control Panel->Programs->Turn Windows features on or off
       - Uncheck "Windows Projected File System"
      - Click OK
      - Restart machine
      - (When `GVFS.Service` starts, it will automatically re-enable the optional feature)
    - `gvfs mount` repo
    - If the mount still fails, restart one more time and try `gvfs mount` again

4. If ProjFS **is not enabled**
    - Manually remove ProjFS and GvFlt
    From an Administrator command prompt:
        - `sc stop GVFS.Service`
       -  `sc stop gvflt`
       -  `sc delete gvflt`
       - `sc stop prjflt`
      - `sc delete prjflt`
      - `del c:\Windows\System32\drivers\prjflt.sys`
      - `sc start GVFS.Service`
    - `gvfs mount` repo

If the above steps do not resolve the issue, and the user is on Windows
Server, ensure they have
[the latest version of GVFS installed](https://github.com/microsoft/vfsforgit/releases).

### Unable to Move/Rename directories inside GVFS repository.

**Symptom:**
User is not able to rename or move a partial directory inside their
VFS for Git enlistment. If rename or move is done using Windows Explorer,
the user might see an error alert with message ```Error 0x80070032: The request is not supported.```

**Fix:**
Partial directories are directories that originate from the virtual projection.
They exist on disk and still contain ProjFS reparse data. When an application
enumerates a partial directory ProjFS calls VFS for Git's enumeration callbacks.

VFS for Git does not support rename or move of partial directories inside an
enlistment. However it supports rename/move of a regular directory. User can
copy the partial directory and paste it inside the enlistment. The newly pasted
directory is a regular directory and can be renamed or moved around inside the
enlistment. The original partial directory can now be deleted.

Diagnosing Issues
-----------------

The `gvfs diagnose` command collects logs and config details for the current
repository. The resulting zip file helps root-cause issues.

When run inside your repository, creates a zip file containing several important
files for that repository. This includes:

* All log files from `gvfs` commands run in the enlistment, including
  maintenance steps.

* Log files from the `GVFS.Service`.

* Configuration files from your `.git` folder, such as the `config` file,
  `index`, `hooks`, and `refs`.

* A summary of your Git object database, including the number of loose objects
  and the names and sizes of pack-files.

As the `diagnose` command completes, it provides the path of the resulting
zip file. This zip can be sent to the support team for investigation.

Modifying Configuration Values
------------------------------

### Cache Server URL

Cache servers are a feature of the GVFS protocol to provide low-latency
access to the on-demand object requests. This modifies the `gvfs.cache-server`
setting in your local Git config file.

Run `gvfs cache-server --get` to see the current cache server.

Run `gvfs cache-server --list` to see the available cache server URLs.

Run `gvfs cache-server --set=` to set your cache server to ``.

### System-wide Config

The `gvfs config` command allows customizing some behavior.

1. Set system-wide config settings using `gvfs config  `.
2. View existing settings with `gvfs config --list`.
3. Remove an existing setting with `gvfs config --delete `.

The `usn.updateDirectories` config option, when `true`, will update the
[USN journal entries](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/fsutil-usn)
of directories when the names of subdirectories or files are modified,
even if the directory is still only in a "projected" state. This can be
particularly important when using incremental build systems such as
microsoft/BuildXL. However, there is a 10-15% performance penalty on some
Git commands when this option is enabled.

The `gvfs config` command is also used for customizing the feed used for
VFS for Git upgrades. This is so large teams can bundle a custom installer
or other tools along with VFS for Git upgrades.


================================================
FILE: global.json
================================================
{
  "msbuild-sdks": {
    "Microsoft.Build.Traversal": "2.0.19",
    "Microsoft.Build.NoTargets": "1.0.85"
  }
}


================================================
FILE: nuget.config
================================================


  
    
  
  
    
    
  
  
    
    
  



================================================
FILE: scripts/Build.bat
================================================
@ECHO OFF
CALL "%~dp0\InitializeEnvironment.bat" || EXIT /b 10
SETLOCAL
SETLOCAL EnableDelayedExpansion

IF "%~1"=="" (
    SET CONFIGURATION=Debug
) ELSE (
    SET CONFIGURATION=%1
)

IF "%~2"=="" (
    SET GVFSVERSION=0.2.173.2
) ELSE (
    SET GVFSVERSION=%2
)

IF "%~3"=="" (
    SET VERBOSITY=minimal
) ELSE (
    SET VERBOSITY=%3
)

REM If we have MSBuild on the PATH then go straight to the build phase
FOR /F "tokens=* USEBACKQ" %%F IN (`where msbuild.exe`) DO (
    SET MSBUILD_EXEC=%%F
    ECHO INFO: Found msbuild.exe at '%%F'
    GOTO :BUILD
)

:LOCATE_MSBUILD
REM Locate MSBuild via the vswhere tool
FOR /F "tokens=* USEBACKQ" %%F IN (`where nuget.exe`) DO (
    SET NUGET_EXEC=%%F
    ECHO INFO: Found nuget.exe at '%%F'
)

REM NuGet is required to be on the PATH to install vswhere
IF NOT EXIST "%NUGET_EXEC%" (
    ECHO ERROR: Could not find nuget.exe on the PATH
    EXIT /B 10
)

REM Acquire vswhere to find VS installations reliably
SET VSWHERE_VER=2.6.7
"%NUGET_EXEC%" install vswhere -Version %VSWHERE_VER% -OutputDirectory %VFS_PACKAGESDIR% || exit /b 1
SET VSWHERE_EXEC="%VFS_PACKAGESDIR%\vswhere.%VSWHERE_VER%\tools\vswhere.exe"

REM Use vswhere to find the latest VS installation with the MSBuild component
REM See https://github.com/Microsoft/vswhere/wiki/Find-MSBuild
FOR /F "tokens=* USEBACKQ" %%F IN (`%VSWHERE_EXEC% -all -prerelease -latest -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\amd64\MSBuild.exe`) DO (
    SET MSBUILD_EXEC=%%F
    ECHO INFO: Found msbuild.exe at '%%F'
)

:BUILD
IF NOT DEFINED MSBUILD_EXEC (
  ECHO ERROR: Could not locate a Visual Studio installation with required components.
  ECHO Refer to Readme.md for a list of the required Visual Studio components.
  EXIT /B 10
)

ECHO ^**********************
ECHO ^* Restoring Packages *
ECHO ^**********************
"%MSBUILD_EXEC%" "%VFS_SRCDIR%\GVFS.sln" ^
        /t:Restore ^
        /v:%VERBOSITY% ^
        /p:Configuration=%CONFIGURATION% || GOTO ERROR

ECHO ^*********************
ECHO ^* Building Solution *
ECHO ^*********************
"%MSBUILD_EXEC%" "%VFS_SRCDIR%\GVFS.sln" ^
        /t:Build ^
        /v:%VERBOSITY% ^
        /p:Configuration=%CONFIGURATION% || GOTO ERROR

GOTO :EOF

:USAGE
ECHO usage: %~n0%~x0 [^] [^] [^]
ECHO.
ECHO   configuration    Solution configuration (default: Debug).
ECHO   version          GVFS version (default: 0.2.173.2).
ECHO   verbosity        MSBuild verbosity (default: minimal).
ECHO.
EXIT 1

:ERROR
ECHO ERROR: Build failed with exit code %ERRORLEVEL%
EXIT /B %ERRORLEVEL%


================================================
FILE: scripts/CreateBuildArtifacts.bat
================================================
@ECHO OFF
CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10
SETLOCAL

IF "%~1"=="" (
    SET CONFIGURATION=Debug
) ELSE (
    SET CONFIGURATION=%1
)

IF "%~2"=="" (
    SET OUTROOT=%VFS_PUBLISHDIR%
) ELSE (
    SET OUTROOT=%2
)

IF EXIST %OUTROOT% (
  rmdir /s /q %OUTROOT%
)

ECHO ^**********************
ECHO ^* Collecting Symbols *
ECHO ^**********************
mkdir %OUTROOT%\Symbols
SET COPY_SYM_CMD="&{Get-ChildItem -Recurse -Path '%VFS_OUTDIR%' -Include *.pdb | Where-Object FullName -Match '\\bin\\.*\\?%CONFIGURATION%\\' | Copy-Item -Destination '%OUTROOT%\Symbols'}"
powershell ^
    -NoProfile ^
    -ExecutionPolicy Bypass ^
    -Command %COPY_SYM_CMD% || GOTO ERROR

ECHO ^******************************
ECHO ^* Collecting GVFS.Installers *
ECHO ^******************************
mkdir %OUTROOT%\GVFS.Installers
xcopy /S /Y ^
    %VFS_OUTDIR%\GVFS.Installers\bin\%CONFIGURATION%\win-x64 ^
    %OUTROOT%\GVFS.Installers\ || GOTO ERROR

ECHO ^************************
ECHO ^* Collecting FastFetch *
ECHO ^************************
ECHO Collecting FastFetch...
mkdir %OUTROOT%\FastFetch
xcopy /S /Y ^
    %VFS_OUTDIR%\FastFetch\bin\%CONFIGURATION%\net471\win-x64 ^
    %OUTROOT%\FastFetch\ || GOTO ERROR

ECHO ^***********************************
ECHO ^* Collecting GVFS.FunctionalTests *
ECHO ^***********************************
mkdir %OUTROOT%\GVFS.FunctionalTests
xcopy /S /Y ^
    %VFS_OUTDIR%\GVFS.FunctionalTests\bin\%CONFIGURATION%\net471\win-x64 ^
    %OUTROOT%\GVFS.FunctionalTests\ || GOTO ERROR

GOTO :EOF

:USAGE
ECHO usage: %~n0%~x0 [^] [^]
ECHO.
ECHO   configuration    Solution configuration (default: Debug).
ECHO   destination      Destination directory to copy artifacts (default: %VFS_PUBLISHDIR%).
ECHO.
EXIT 1

:ERROR
ECHO ERROR: Create build artifacts failed with exit code %ERRORLEVEL%
EXIT /B %ERRORLEVEL%


================================================
FILE: scripts/InitializeEnvironment.bat
================================================
@IF "%_echo%"=="" (ECHO OFF) ELSE (ECHO ON)

REM Set environment variables for interesting paths that scripts might need access to.
PUSHD %~dp0
SET VFS_SCRIPTSDIR=%CD%
POPD

CALL :RESOLVEPATH "%VFS_SCRIPTSDIR%\.."
SET VFS_SRCDIR=%_PARSED_PATH_%

CALL :RESOLVEPATH "%VFS_SRCDIR%\.."
SET VFS_ENLISTMENTDIR=%_PARSED_PATH_%

SET VFS_OUTDIR=%VFS_ENLISTMENTDIR%\out
SET VFS_PACKAGESDIR=%VFS_ENLISTMENTDIR%\packages
SET VFS_PUBLISHDIR=%VFS_ENLISTMENTDIR%\publish

REM Clean up
SET _PARSED_PATH_=

GOTO :EOF

:RESOLVEPATH
SET "_PARSED_PATH_=%~f1"
GOTO :EOF


================================================
FILE: scripts/RunFunctionalTests-Dev.ps1
================================================
<#
.SYNOPSIS
    Runs GVFS functional tests in dev mode (no admin, no install required).

.DESCRIPTION
    Runs GVFS.FunctionalTests.exe using build output from out\ instead of
    requiring a system-wide GVFS installation. The test harness launches a
    test service as a console process (not a Windows service), so no admin
    privileges are required.

    After the test process exits, any GVFS.Service.exe child processes it
    spawned are killed by PID. This is safe for concurrent runs — each
    invocation only cleans up its own child processes.

.PARAMETER Configuration
    Build configuration: Debug (default) or Release.

.PARAMETER ExtraArgs
    Additional arguments passed through to GVFS.FunctionalTests.exe
    (e.g. --test=GVFS.FunctionalTests.Tests.GVFSVerbTests.UnknownVerb)

.EXAMPLE
    .\RunFunctionalTests-Dev.ps1
    .\RunFunctionalTests-Dev.ps1 -Configuration Release
    .\RunFunctionalTests-Dev.ps1 -ExtraArgs "--test=GVFS.FunctionalTests.Tests.GVFSVerbTests.UnknownVerb"
    .\RunFunctionalTests-Dev.ps1 Debug --test=GVFS.FunctionalTests.Tests.EnlistmentPerFixture.WorktreeTests
#>
param(
    [string]$Configuration = "Debug",
    [Parameter(ValueFromRemainingArguments)]
    [string[]]$ExtraArgs
)

$ErrorActionPreference = "Stop"

# Resolve paths (mirrors InitializeEnvironment.bat)
$scriptsDir = $PSScriptRoot
$srcDir = Split-Path $scriptsDir -Parent
$enlistmentDir = Split-Path $srcDir -Parent
$outDir = Join-Path $enlistmentDir "out"

# Dev mode environment
$env:GVFS_FUNCTIONAL_TEST_DEV_MODE = "1"
$env:GVFS_DEV_OUT_DIR = $outDir
$env:GVFS_DEV_CONFIGURATION = $Configuration

# Derive a unique service name from the enlistment path so concurrent runs
# from different working directories don't collide on the named pipe.
$hash = [System.BitConverter]::ToString(
    [System.Security.Cryptography.SHA256]::Create().ComputeHash(
        [System.Text.Encoding]::UTF8.GetBytes($enlistmentDir.ToLowerInvariant())
    )
).Replace("-","").Substring(0,8)
$env:GVFS_TEST_SERVICE_NAME = "Test.GVFS.Service.$hash.$PID"

# Isolate test data per enlistment and run
$env:GVFS_TEST_DATA = Join-Path $env:TEMP "GVFS-FunctionalTest-$hash.$PID"
$env:GVFS_COMMON_APPDATA_ROOT = Join-Path $env:GVFS_TEST_DATA "AppData"
$env:GVFS_SECURE_DATA_ROOT = Join-Path $env:GVFS_TEST_DATA "ProgramData"

# Put build output gvfs.exe on PATH
$payloadDir = Join-Path $outDir "GVFS.Payload\bin\$Configuration\win-x64"
$env:PATH = "$payloadDir;C:\Program Files\Git\cmd;$env:PATH"

Write-Host "============================================"
Write-Host "GVFS Functional Tests - Dev Mode (no admin)"
Write-Host "============================================"
Write-Host "Configuration:       $Configuration"
Write-Host "Build output:        $outDir"
Write-Host "Test service:        $env:GVFS_TEST_SERVICE_NAME"
Write-Host "Test data:           $env:GVFS_TEST_DATA"
Write-Host ""

# Validate prerequisites
$gvfsPath = Get-Command gvfs -ErrorAction SilentlyContinue
if (-not $gvfsPath) {
    Write-Error "Unable to locate gvfs on the PATH. Has the solution been built?"
    exit 1
}
Write-Host "gvfs location:       $($gvfsPath.Source)"

$gitPath = Get-Command git -ErrorAction SilentlyContinue
if (-not $gitPath) {
    Write-Error "Unable to locate git on the PATH."
    exit 1
}
Write-Host "git location:        $($gitPath.Source)"
Write-Host ""

# Build test exe path
$testExe = Join-Path $outDir "GVFS.FunctionalTests\bin\$Configuration\net471\win-x64\GVFS.FunctionalTests.exe"
if (-not (Test-Path $testExe)) {
    Write-Error "Test executable not found: $testExe`nRun Build.bat first."
    exit 1
}

# Build arguments
$testArgs = @("/result:$(Join-Path $enlistmentDir 'TestResult.xml')")
if ($ExtraArgs) { $testArgs += $ExtraArgs }

Write-Host "Running: $testExe"
Write-Host "  Args:  $($testArgs -join ' ')"
Write-Host ""

# Start the test process and track its PID
$testProc = Start-Process -FilePath $testExe -ArgumentList $testArgs `
    -NoNewWindow -PassThru

try {
    $testProc.WaitForExit()
}
finally {
    # Kill any GVFS.Service.exe that was spawned by our test process.
    # ParentProcessId is set at creation time and doesn't change when the
    # parent exits, so this works even after GVFS.FunctionalTests.exe is gone.
    $orphans = Get-CimInstance Win32_Process -Filter `
        "Name = 'GVFS.Service.exe' AND ParentProcessId = $($testProc.Id)" `
        -ErrorAction SilentlyContinue
    foreach ($orphan in $orphans) {
        Write-Host "Cleaning up test service process (PID $($orphan.ProcessId))..."
        Stop-Process -Id $orphan.ProcessId -Force -ErrorAction SilentlyContinue
    }
}

exit $testProc.ExitCode


================================================
FILE: scripts/RunFunctionalTests.bat
================================================
@ECHO OFF
CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10

IF "%1"=="" (SET "CONFIGURATION=Debug") ELSE (SET "CONFIGURATION=%1")

REM Ensure GVFS installation is on the PATH for the Functional Tests to find
SETLOCAL
SET PATH=C:\Program Files\VFS for Git\;C:\Program Files\GVFS;C:\Program Files\Git\cmd;%PATH%

ECHO PATH = %PATH%

ECHO gvfs location:
where gvfs
IF NOT %ERRORLEVEL% == 0 (
    ECHO error: unable to locate GVFS on the PATH (has it been installed?)
)

ECHO GVFS.Service location:
where GVFS.Service
IF NOT %ERRORLEVEL% == 0 (
    ECHO error: unable to locate GVFS.Service on the PATH (has it been installed?)
)

ECHO git location:
where git
IF NOT %ERRORLEVEL% == 0 (
    ECHO error: unable to locate Git on the PATH (has it been installed?)
)

%VFS_OUTDIR%\GVFS.FunctionalTests\bin\%CONFIGURATION%\net471\win-x64\GVFS.FunctionalTests.exe /result:TestResult.xml %2 %3 %4 %5

SET error=%ERRORLEVEL%
CALL %VFS_SCRIPTSDIR%\StopAllServices.bat
EXIT /b %error%


================================================
FILE: scripts/RunUnitTests.bat
================================================
@ECHO OFF
CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10

IF "%1"=="" (SET "CONFIGURATION=Debug") ELSE (SET "CONFIGURATION=%1")

SET RESULT=0

%VFS_OUTDIR%\GVFS.UnitTests\bin\%CONFIGURATION%\net471\win-x64\GVFS.UnitTests.exe || SET RESULT=1

EXIT /b %RESULT%


================================================
FILE: scripts/StopAllServices.bat
================================================
@ECHO OFF
CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10

CALL %VFS_SCRIPTSDIR%\StopService.bat GVFS.Service
CALL %VFS_SCRIPTSDIR%\StopService.bat Test.GVFS.Service


================================================
FILE: scripts/StopService.bat
================================================
sc stop %1
verify >nul