// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using EpicGames.Core; namespace EpicGames.Perforce.Managed { /// /// Utility class to efficiently track changes to a StreamTree object in memory /// public class StreamTreeBuilder { /// /// Map from name to mutable tree /// public Dictionary NameToFile { get; } /// /// Map from name to mutable tree /// public Dictionary NameToTree { get; } /// /// Map from name to mutable tree /// public Dictionary NameToTreeBuilder { get; } = new Dictionary(FileUtils.PlatformPathComparerUtf8); /// /// Tests whether the tree is empty /// public bool IsEmpty => NameToFile.Count == 0 && NameToTree.Count == 0 && NameToTreeBuilder.Count == 0; /// /// Constructor /// public StreamTreeBuilder() { NameToFile = new Dictionary(FileUtils.PlatformPathComparerUtf8); NameToTree = new Dictionary(FileUtils.PlatformPathComparerUtf8); } /// /// Copy constructor /// /// public StreamTreeBuilder(StreamTree tree) { NameToFile = new Dictionary(tree.NameToFile, FileUtils.PlatformPathComparerUtf8); NameToTree = new Dictionary(tree.NameToTree, FileUtils.PlatformPathComparerUtf8); } /// /// Encodes the current tree state and returns a reference to it /// /// Dictionary of encoded objects /// public StreamTree Encode(Func writeTree) { // Recursively serialize all the child items EncodeChildren(writeTree); // Find the common base path for all items in this tree. Dictionary basePathToCount = new Dictionary(); foreach ((Utf8String name, StreamFile file) in NameToFile) { AddBasePath(basePathToCount, file.Path, name); } foreach ((Utf8String name, StreamTreeRef tree) in NameToTree) { AddBasePath(basePathToCount, tree.Path, name); } // Create the new tree Utf8String basePath = (basePathToCount.Count == 0) ? Utf8String.Empty : basePathToCount.MaxBy(x => x.Value).Key; return new StreamTree(basePath, NameToFile, NameToTree); } /// /// Encodes a StreamTreeRef from this tree /// /// /// The new tree ref public StreamTreeRef EncodeRef(Func writeTree) { StreamTree tree = Encode(writeTree); return new StreamTreeRef(tree.Path, writeTree(tree)); } /// /// Collapses all of the builders underneath this node /// /// public void EncodeChildren(Func writeTree) { foreach ((Utf8String subTreeName, StreamTreeBuilder subTreeBuilder) in NameToTreeBuilder) { StreamTree subTree = subTreeBuilder.Encode(writeTree); if (subTree.NameToFile.Count > 0 || subTree.NameToTree.Count > 0) { IoHash hash = writeTree(subTree); NameToTree[subTreeName] = new StreamTreeRef(subTree.Path, hash); } } NameToTreeBuilder.Clear(); } /// /// Adds the base path of the given item to the count of similar items /// /// /// /// static void AddBasePath(Dictionary basePathToCount, Utf8String path, Utf8String name) { if (path.EndsWith(name) && path[^(name.Length + 1)] == '/') { Utf8String basePath = path[..^(name.Length + 1)]; basePathToCount.TryGetValue(basePath, out int count); basePathToCount[basePath] = count + 1; } } } /// /// Helper variant of StreamTreeBuilder capable of adding files to tree using both client and depot file paths /// public class DepotStreamTreeBuilder : StreamTreeBuilder { /// /// Adds a file to the tree /// public void AddFile(string clientFile, StreamFile depotFile) { if (clientFile[0] == '/') { // Strip any slash at the start clientFile = clientFile[1..]; } StreamTreeBuilder currentStreamDirectory = this; string[] pathFragments = clientFile.Split('/'); // Find stream tree builder for deepest path fragment // and skip last fragment as that is the filename for (int i = 0; i < pathFragments.Length - 1; i++) { string pathFragment = pathFragments[i]; Utf8String unescapedFragment = new Utf8String(PerforceUtils.UnescapePath(pathFragment)); if (!currentStreamDirectory.NameToTreeBuilder.TryGetValue(unescapedFragment, out StreamTreeBuilder? nextStreamDirectory)) { nextStreamDirectory = new StreamTreeBuilder(); currentStreamDirectory.NameToTreeBuilder.Add(unescapedFragment, nextStreamDirectory); } currentStreamDirectory = nextStreamDirectory; } Utf8String filename = new Utf8String(PerforceUtils.UnescapePath(pathFragments[^1])); // Last fragment is filename currentStreamDirectory.NameToFile[filename] = depotFile; } } }