private string[] GetDirectoriesOutsideSparse(string rootPath, HashSet <string> sparseFolders) { PhysicalFileSystem fileSystem = new PhysicalFileSystem(); Queue <string> foldersToEnumerate = new Queue <string>(); foldersToEnumerate.Enqueue(rootPath); List <string> foldersOutsideSparse = new List <string>(); while (foldersToEnumerate.Count > 0) { string folderToEnumerate = foldersToEnumerate.Dequeue(); foreach (string directory in fileSystem.EnumerateDirectories(folderToEnumerate)) { string enlistmentRootRelativeFolderPath = GVFSDatabase.NormalizePath(directory.Substring(rootPath.Length)); 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.ToArray()); }
public bool TryDehydrateFolder(string relativePath, out string errorMessage) { List <IPlaceholderData> removedPlaceholders = null; List <string> 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)); }
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)); }
private IEnumerable <string> 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 string[] ParseFolderList(string folders) { if (string.IsNullOrEmpty(folders)) { return(new string[0]); } else { return(folders.Split(new[] { FolderListSeparator }, StringSplitOptions.RemoveEmptyEntries) .Select(x => GVFSDatabase.NormalizePath(x)) .ToArray()); } }
/// <summary> /// Get two lists of placeholders, one containing the files and the other the directories /// Goes to the SQLite database for the placeholder lists /// </summary> /// <param name="enlistment">The current GVFS enlistment being operated on</param> /// <param name="filePlaceholders">Out parameter where the list of file placeholders will end up</param> /// <param name="folderPlaceholders">Out parameter where the list of folder placeholders will end up</param> private void GetPlaceholdersFromDatabase(GVFSEnlistment enlistment, EnlistmentPathData pathData) { List <IPlaceholderData> filePlaceholders = new List <IPlaceholderData>(); List <IPlaceholderData> folderPlaceholders = new List <IPlaceholderData>(); using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistment.EnlistmentRoot, new SqliteDatabase())) { PlaceholderTable placeholderTable = new PlaceholderTable(database); placeholderTable.GetAllEntries(out filePlaceholders, out folderPlaceholders); } pathData.PlaceholderFilePaths.AddRange(filePlaceholders.Select(placeholderData => placeholderData.Path)); pathData.PlaceholderFolderPaths.AddRange(folderPlaceholders.Select(placeholderData => placeholderData.Path)); }
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 <IPlaceholderData> 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); }
private void ListSparseFolders(string enlistmentRoot) { using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistmentRoot, new SqliteDatabase())) { SparseTable sparseTable = new SparseTable(database); HashSet <string> 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 void UnmountAndStopWorkingDirectoryCallbacks() { 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; }
private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache) { string error; if (!this.context.Enlistment.Authentication.TryInitialize(this.context.Tracer, this.context.Enlistment, out error)) { this.FailMountAndExit("Failed to obtain git credentials: " + 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), gitStatusCache: gitStatusCache)); }, "Failed to create src folder callback listener"); this.maintenanceScheduler = this.CreateOrReportAndExit(() => new GitMaintenanceScheduler(this.context, this.gitObjects), "Failed to start maintenance scheduler"); 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(); }
protected override void Execute(GVFSEnlistment enlistment) { using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, SparseVerbName)) { tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.Sparse), EventLevel.Informational, Keywords.Any); bool needToChangeProjection = false; using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistment.EnlistmentRoot, new SqliteDatabase())) { SparseTable sparseTable = new SparseTable(database); HashSet <string> directories = sparseTable.GetAll(); string[] foldersToRemove = this.ParseFolderList(this.Remove); string[] foldersToAdd = this.ParseFolderList(this.Add); if (this.List || (foldersToAdd.Length == 0 && foldersToRemove.Length == 0)) { 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); } } return; } foreach (string folder in foldersToRemove) { if (directories.Contains(folder)) { needToChangeProjection = true; break; } } if (!needToChangeProjection) { foreach (string folder in foldersToAdd) { if (!directories.Contains(folder)) { needToChangeProjection = true; break; } } } if (needToChangeProjection) { // Make sure there is a clean git status before allowing sparse set to change this.CheckGitStatus(tracer, enlistment); 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."); } } } 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."); } } }
private void TestGVFSDatabase(Action <GVFSDatabase> testCode, bool throwException = false) { MockFileSystem fileSystem = new MockFileSystem(new MockDirectory("GVFSDatabaseTests", null, null)); Mock <IDbCommand> mockCommand = new Mock <IDbCommand>(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 <IDbCommand> mockCommand2 = new Mock <IDbCommand>(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 <IDbCommand> mockCommand3 = new Mock <IDbCommand>(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 <Mock <IDbConnection> > mockConnections = new List <Mock <IDbConnection> >(); Mock <IDbConnection> mockConnection = new Mock <IDbConnection>(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 <IDbConnectionFactory> mockConnectionFactory = new Mock <IDbConnectionFactory>(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 <IDbConnection> newMockConnection = new Mock <IDbConnection>(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(); }
private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, string[] folders) { List <string> foldersToDehydrate = new List <string>(); List <string> folderErrors = new List <string>(); if (!this.ShowStatusWhileRunning( () => { if (!ModifiedPathsDatabase.TryLoadOrCreate( tracer, Path.Combine(enlistment.DotGVFSRoot, 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); if (this.fileSystem.DirectoryExists(fullPath)) { if (!this.TryIO(tracer, () => this.fileSystem.DeleteDirectory(fullPath), $"Deleting '{fullPath}'", out ioError)) { this.WriteMessage(tracer, $"Cannot {this.ActionName} folder '{folder}': removing '{folder}' failed."); this.WriteMessage(tracer, "Ensure no applications are accessing the folder and retry."); this.WriteMessage(tracer, $"More details: {ioError}"); folderErrors.Add($"{folder}\0{ioError}"); } else { foldersToDehydrate.Add(folder); } } else { this.WriteMessage(tracer, $"Cannot {this.ActionName} folder '{folder}': '{folder}' does not exist."); // Still add to foldersToDehydrate so that any placeholders or modified paths get cleaned up foldersToDehydrate.Add(folder); } } } } } return(true); }, "Cleaning up folders")) { this.ReportErrorAndExit(tracer, $"{this.ActionName} for folders failed."); } this.Mount(tracer); this.SendDehydrateMessage(tracer, enlistment, folderErrors, foldersToDehydrate.ToArray()); 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)."); } }
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 <string> directories; bool needToChangeProjection = false; using (GVFSDatabase database = new GVFSDatabase(new PhysicalFileSystem(), enlistment.EnlistmentRoot, new SqliteDatabase())) { SparseTable sparseTable = new SparseTable(database); directories = sparseTable.GetAll(); List <string> foldersToRemove = new List <string>(); List <string> foldersToAdd = new List <string>(); 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 <string> 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."); } if (this.Prune && directories.Count > 0) { this.PruneFoldersOutsideSparse(tracer, enlistment, sparseTable); } } } }