Files
UnrealEngineUWP/Engine/Source/Programs/Shared/EpicGames.Perforce.Managed/WorkspaceDirectoryInfo.cs
ben marsh 4911ccd1a0 Change EpicGames.Perforce.Managed to use IoHash, simplifying the migration to using Jupiter for storage.
#ROBOMERGE-SOURCE: CL 16592003 in //UE5/Main/...
#ROBOMERGE-BOT: STARSHIP (Main -> Release-Engine-Test) (v828-16531559)

[CL 16592087 by ben marsh in ue5-release-engine-test branch]
2021-06-08 15:53:22 -04:00

391 lines
13 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace EpicGames.Perforce.Managed
{
/// <summary>
/// Stores the state of a directory in the workspace
/// </summary>
class WorkspaceDirectoryInfo
{
/// <summary>
/// The parent directory
/// </summary>
public WorkspaceDirectoryInfo? ParentDirectory { get; }
/// <summary>
/// Name of this directory
/// </summary>
public ReadOnlyUtf8String Name { get; }
/// <summary>
/// Digest of the matching stream directory info. This should be set to zero if the workspace is modified.
/// </summary>
public IoHash StreamDirectoryDigest { get; set; }
/// <summary>
/// Map of name to file
/// </summary>
public Dictionary<ReadOnlyUtf8String, WorkspaceFileInfo> NameToFile { get; set; }
/// <summary>
/// Map of name to subdirectory
/// </summary>
public Dictionary<ReadOnlyUtf8String, WorkspaceDirectoryInfo> NameToSubDirectory { get; set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="RootDir"></param>
public WorkspaceDirectoryInfo(DirectoryReference RootDir)
: this(null, RootDir.FullName, IoHash.Zero)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="ParentDirectory">The parent directory</param>
/// <param name="Name">Name of this directory</param>
/// <param name="Digest">The corresponding stream digest</param>
public WorkspaceDirectoryInfo(WorkspaceDirectoryInfo? ParentDirectory, ReadOnlyUtf8String Name, IoHash Digest)
{
this.ParentDirectory = ParentDirectory;
this.Name = Name;
this.StreamDirectoryDigest = Digest;
this.NameToFile = new Dictionary<ReadOnlyUtf8String, WorkspaceFileInfo>(ReadOnlyUtf8StringComparer.Ordinal);
this.NameToSubDirectory = new Dictionary<ReadOnlyUtf8String, WorkspaceDirectoryInfo>(FileUtils.PlatformPathComparerUtf8);
}
/// <summary>
/// Adds a file to the workspace
/// </summary>
/// <param name="Path">Relative path to the file, using forward slashes, and without a leading slash</param>
/// <param name="Length">Length of the file on disk</param>
/// <param name="LastModifiedTicks">Last modified time of the file</param>
/// <param name="bReadOnly">Whether the file is read only</param>
/// <param name="ContentId">Unique identifier for the server content</param>
public void AddFile(ReadOnlyUtf8String Path, long Length, long LastModifiedTicks, bool bReadOnly, FileContentId ContentId)
{
StreamDirectoryDigest = IoHash.Zero;
int Idx = Path.Span.IndexOf((byte)'/');
if (Idx == -1)
{
NameToFile[Path] = new WorkspaceFileInfo(this, Path, Length, LastModifiedTicks, bReadOnly, ContentId);
}
else
{
ReadOnlyUtf8String Name = Path.Slice(0, Idx);
WorkspaceDirectoryInfo? SubDirectory;
if (!NameToSubDirectory.TryGetValue(Name, out SubDirectory))
{
SubDirectory = new WorkspaceDirectoryInfo(this, Name, IoHash.Zero);
NameToSubDirectory[Name] = SubDirectory;
}
SubDirectory.AddFile(Path.Slice(Idx + 1), Length, LastModifiedTicks, bReadOnly, ContentId);
}
}
/// <summary>
/// Create a flat list of files in this workspace
/// </summary>
/// <returns>List of files</returns>
public List<WorkspaceFileInfo> GetFiles()
{
List<WorkspaceFileInfo> Files = new List<WorkspaceFileInfo>();
GetFilesInternal(Files);
return Files;
}
/// <summary>
/// Internal helper method for recursing through the tree to build a file list
/// </summary>
/// <param name="Files"></param>
private void GetFilesInternal(List<WorkspaceFileInfo> Files)
{
Files.AddRange(NameToFile.Values);
foreach (KeyValuePair<ReadOnlyUtf8String, WorkspaceDirectoryInfo> Pair in NameToSubDirectory)
{
Pair.Value.GetFilesInternal(Files);
}
}
/// <summary>
/// Refresh the state of the workspace on disk
/// </summary>
/// <param name="bRemoveUntracked">Whether to remove files that are not part of the stream</param>
/// <param name="FilesToDelete">Receives an array of files to delete</param>
/// <param name="DirectoriesToDelete">Recevies an array of directories to delete</param>
public void Refresh(bool bRemoveUntracked, out FileInfo[] FilesToDelete, out DirectoryInfo[] DirectoriesToDelete)
{
ConcurrentBag<FileInfo> ConcurrentFilesToDelete = new ConcurrentBag<FileInfo>();
ConcurrentBag<DirectoryInfo> ConcurrentDirectoriesToDelete = new ConcurrentBag<DirectoryInfo>();
using (ThreadPoolWorkQueue Queue = new ThreadPoolWorkQueue())
{
Queue.Enqueue(() => Refresh(new DirectoryInfo(GetFullName()), bRemoveUntracked, ConcurrentFilesToDelete, ConcurrentDirectoriesToDelete, Queue));
}
DirectoriesToDelete = ConcurrentDirectoriesToDelete.ToArray();
FilesToDelete = ConcurrentFilesToDelete.ToArray();
}
/// <summary>
/// Recursive method for querying the workspace state
/// </summary>
/// <param name="Info"></param>
/// <param name="bRemoveUntracked"></param>
/// <param name="FilesToDelete"></param>
/// <param name="DirectoriesToDelete"></param>
/// <param name="Queue"></param>
void Refresh(DirectoryInfo Info, bool bRemoveUntracked, ConcurrentBag<FileInfo> FilesToDelete, ConcurrentBag<DirectoryInfo> DirectoriesToDelete, ThreadPoolWorkQueue Queue)
{
// Recurse through subdirectories
Dictionary<ReadOnlyUtf8String, WorkspaceDirectoryInfo> NewNameToSubDirectory = new Dictionary<ReadOnlyUtf8String, WorkspaceDirectoryInfo>(NameToSubDirectory.Count, NameToSubDirectory.Comparer);
foreach (DirectoryInfo SubDirectoryInfo in Info.EnumerateDirectories())
{
WorkspaceDirectoryInfo? SubDirectory;
if (NameToSubDirectory.TryGetValue(SubDirectoryInfo.Name, out SubDirectory))
{
NewNameToSubDirectory.Add(SubDirectory.Name, SubDirectory);
Queue.Enqueue(() => SubDirectory.Refresh(SubDirectoryInfo, bRemoveUntracked, FilesToDelete, DirectoriesToDelete, Queue));
}
else if (bRemoveUntracked)
{
DirectoriesToDelete.Add(SubDirectoryInfo);
}
}
NameToSubDirectory = NewNameToSubDirectory;
// Figure out which files have changed.
Dictionary<ReadOnlyUtf8String, WorkspaceFileInfo> NewNameToFile = new Dictionary<ReadOnlyUtf8String, WorkspaceFileInfo>(NameToFile.Count, NameToFile.Comparer);
foreach (FileInfo File in Info.EnumerateFiles())
{
WorkspaceFileInfo? StagedFile;
if (NameToFile.TryGetValue(File.Name, out StagedFile))
{
if (StagedFile.MatchesAttributes(File))
{
NewNameToFile.Add(StagedFile.Name, StagedFile);
}
else
{
FilesToDelete.Add(File);
}
}
else
{
if (bRemoveUntracked)
{
FilesToDelete.Add(File);
}
}
}
// If the file state has changed, clear the directory hashes
if (NameToFile.Count != NewNameToFile.Count)
{
for (WorkspaceDirectoryInfo? Directory = this; Directory != null && Directory.StreamDirectoryDigest != IoHash.Zero; Directory = Directory.ParentDirectory)
{
Directory.StreamDirectoryDigest = IoHash.Zero;
}
}
// Update the new file list
NameToFile = NewNameToFile;
}
/// <summary>
/// Builds a list of differences from the working directory
/// </summary>
/// <returns></returns>
public string[] FindDifferences()
{
ConcurrentBag<string> Paths = new ConcurrentBag<string>();
using (ThreadPoolWorkQueue Queue = new ThreadPoolWorkQueue())
{
Queue.Enqueue(() => FindDifferences(new DirectoryInfo(GetFullName()), "/", Paths, Queue));
}
return Paths.OrderBy(x => x).ToArray();
}
/// <summary>
/// Helper method for finding differences from the working directory
/// </summary>
/// <param name="Directory"></param>
/// <param name="Path"></param>
/// <param name="Paths"></param>
/// <param name="Queue"></param>
void FindDifferences(DirectoryInfo Directory, string Path, ConcurrentBag<string> Paths, ThreadPoolWorkQueue Queue)
{
// Recurse through subdirectories
HashSet<ReadOnlyUtf8String> RemainingSubDirectoryNames = new HashSet<ReadOnlyUtf8String>(NameToSubDirectory.Keys);
foreach (DirectoryInfo SubDirectory in Directory.EnumerateDirectories())
{
WorkspaceDirectoryInfo? StagedSubDirectory;
if (NameToSubDirectory.TryGetValue(SubDirectory.Name, out StagedSubDirectory))
{
RemainingSubDirectoryNames.Remove(SubDirectory.Name);
Queue.Enqueue(() => StagedSubDirectory.FindDifferences(SubDirectory, String.Format("{0}{1}/", Path, SubDirectory.Name), Paths, Queue));
continue;
}
Paths.Add(String.Format("+{0}{1}/...", Path, SubDirectory.Name));
}
foreach (ReadOnlyUtf8String RemainingSubDirectoryName in RemainingSubDirectoryNames)
{
Paths.Add(String.Format("-{0}{1}/...", Path, RemainingSubDirectoryName));
}
// Search through files
HashSet<ReadOnlyUtf8String> RemainingFileNames = new HashSet<ReadOnlyUtf8String>(NameToFile.Keys);
foreach (FileInfo File in Directory.EnumerateFiles())
{
WorkspaceFileInfo? StagedFile;
if (!NameToFile.TryGetValue(File.Name, out StagedFile))
{
Paths.Add(String.Format("+{0}{1}", Path, File.Name));
}
else if (!StagedFile.MatchesAttributes(File))
{
Paths.Add(String.Format("!{0}{1}", Path, File.Name));
RemainingFileNames.Remove(File.Name);
}
else
{
RemainingFileNames.Remove(File.Name);
}
}
foreach (ReadOnlyUtf8String RemainingFileName in RemainingFileNames)
{
Paths.Add(String.Format("-{0}{1}", Path, RemainingFileName));
}
}
/// <summary>
/// Get the full path to this directory
/// </summary>
/// <returns></returns>
public string GetFullName()
{
StringBuilder Builder = new StringBuilder();
AppendFullPath(Builder);
return Builder.ToString();
}
/// <summary>
/// Get the path to this directory
/// </summary>
/// <returns></returns>
public DirectoryReference GetLocation()
{
return new DirectoryReference(GetFullName());
}
/// <summary>
/// Append the client path, using native directory separators, to the given string builder
/// </summary>
/// <param name="Builder"></param>
public void AppendClientPath(StringBuilder Builder)
{
if (ParentDirectory != null)
{
ParentDirectory.AppendClientPath(Builder);
Builder.Append(Name);
Builder.Append(Path.DirectorySeparatorChar);
}
}
/// <summary>
/// Append the path for this directory to the given string builder
/// </summary>
/// <param name="Builder"></param>
public void AppendFullPath(StringBuilder Builder)
{
if (ParentDirectory != null)
{
ParentDirectory.AppendFullPath(Builder);
Builder.Append(Path.DirectorySeparatorChar);
}
Builder.Append(Name);
}
/// <inheritdoc/>
public override string ToString()
{
return GetFullName();
}
}
/// <summary>
/// Extension methods for WorkspaceDirectoryInfo
/// </summary>
static class WorkspaceDirectoryInfoExtensions
{
public static void ReadWorkspaceDirectoryInfo(this MemoryReader Reader, WorkspaceDirectoryInfo DirectoryInfo, ManagedWorkspaceVersion Version)
{
if (Version < ManagedWorkspaceVersion.AddDigest)
{
DirectoryInfo.StreamDirectoryDigest = IoHash.Zero;
}
else if (Version < ManagedWorkspaceVersion.AddDigestIoHash)
{
Reader.ReadFixedLengthBytes(Sha1.Length);
DirectoryInfo.StreamDirectoryDigest = IoHash.Zero;
}
else
{
DirectoryInfo.StreamDirectoryDigest = Reader.ReadIoHash();
}
int NumFiles = Reader.ReadInt32();
for (int Idx = 0; Idx < NumFiles; Idx++)
{
WorkspaceFileInfo FileInfo = Reader.ReadWorkspaceFileInfo(DirectoryInfo);
DirectoryInfo.NameToFile.Add(FileInfo.Name, FileInfo);
}
int NumSubDirectories = Reader.ReadInt32();
for (int Idx = 0; Idx < NumSubDirectories; Idx++)
{
ReadOnlyUtf8String Name = Reader.ReadString();
WorkspaceDirectoryInfo SubDirectory = new WorkspaceDirectoryInfo(DirectoryInfo, Name, IoHash.Zero);
Reader.ReadWorkspaceDirectoryInfo(SubDirectory, Version);
DirectoryInfo.NameToSubDirectory[SubDirectory.Name] = SubDirectory;
}
}
public static void WriteWorkspaceDirectoryInfo(this MemoryWriter Writer, WorkspaceDirectoryInfo DirectoryInfo)
{
Writer.WriteIoHash(DirectoryInfo.StreamDirectoryDigest);
Writer.WriteInt32(DirectoryInfo.NameToFile.Count);
foreach (WorkspaceFileInfo File in DirectoryInfo.NameToFile.Values)
{
Writer.WriteWorkspaceFileInfo(File);
}
Writer.WriteInt32(DirectoryInfo.NameToSubDirectory.Count);
foreach (WorkspaceDirectoryInfo SubDirectory in DirectoryInfo.NameToSubDirectory.Values)
{
Writer.WriteString(SubDirectory.Name);
Writer.WriteWorkspaceDirectoryInfo(SubDirectory);
}
}
public static int GetSerializedSize(this WorkspaceDirectoryInfo DirectoryInfo)
{
return Digest<Sha1>.Length + sizeof(int) + DirectoryInfo.NameToFile.Values.Sum(x => x.GetSerializedSize()) + sizeof(int) + DirectoryInfo.NameToSubDirectory.Values.Sum(x => x.Name.GetSerializedSize() + x.GetSerializedSize());
}
}
}