using UnityEditor; using UnityEngine; using System; using System.Collections.Generic; using System.Linq; using System.Text; using Perforce.P4; using System.Runtime.InteropServices; using System.Reflection; using System.IO; using log4net; namespace P4Connect { /// <summary> /// Utility methods for P4Connect /// </summary> public static class Utils { static bool _caseKnown = false; static bool _caseSensitive = false; private static readonly ILog log = LogManager.GetLogger(typeof(Utils)); /// <summary> /// Remove directory wild cards from the end of a path /// </summary> /// <param name="path">Asset Path, including wild cards</param> /// <returns> Asset Paths stripped of ending wild cards </returns> public static string RemoveDirectoryWildcards(string path) { if (path.EndsWith("...")) { path = path.Substring(0, path.Length - 3); if (path.EndsWith("/")) { path = path.Substring(0, path.Length - 1); } } return path; } public static string AddDirectoryWildcards(string path) { if (! path.EndsWith("...")) { return path + "/..."; } return path; } public static bool HasDirectoryWildcards(string path) { return (path.EndsWith("...") || path.EndsWith("*")); } /// <summary> /// Returns true if running on a case sensitive file system. /// </summary> public static bool IsCaseSensitive() { // Return cached results if (_caseKnown) return _caseSensitive; // The assets directory should always exist in a project string path = Main.DataPath; bool u = System.IO.Directory.Exists(path.ToUpper()); bool l = System.IO.Directory.Exists(path.ToLower()); if (u ^ l) return true; // default to true for a new project, but don't cache the result. if (u && l){ _caseSensitive = false; } else { _caseSensitive = true; } _caseKnown = true; return _caseSensitive; } public static bool IsRunningUnity5() { if (Application.unityVersion.Trim()[0] == '5') return true; return false; } /// <summary> /// Retrieves a displayable filename from a perforce file spec /// </summary> public static string GetFilename(FileAndMeta aFileAndMeta) { return GetFilename(aFileAndMeta, Config.ShowPaths); } /// <summary> /// Retrieves a displayable filename from a perforce file spec /// </summary> public static string GetFilename(FileAndMeta aFileAndMeta, bool aShowPath) { // Base version uses whatever prefs the user set if (aFileAndMeta.File != null) return GetFilename(aFileAndMeta.File, aShowPath); else if (aFileAndMeta.Meta != null) return GetFilename(aFileAndMeta.Meta, aShowPath); else return ""; } /// <summary> /// Retrieves a displayable filename from a perforce file spec /// </summary> public static string GetFilename(Perforce.P4.FileSpec aFile) { // Base version uses whatever prefs the user set return GetFilename(aFile, Config.ShowPaths); } /// <summary> /// Retrieves a displayable filename from a perforce file spec /// </summary> public static string GetFilename(Perforce.P4.FileSpec aFile, bool aShowPath) { string filename = ""; if (aFile == null) return "null"; if (aFile.ClientPath != null) filename = aFile.ClientPath.Path; else if (aFile.DepotPath != null) filename = aFile.DepotPath.Path; else filename = aFile.LocalPath.Path; if (!filename.EndsWith("...") && !aShowPath) { // parse out the file name filename = Path.GetFileName(filename); } return UnescapeFilename(filename); } /// <summary> /// Separates the files based on their extension /// </summary> public static void FilterTextAndBinary(List<FileSpec> aFiles, List<FileSpec> aOutText, List<FileSpec> aOutBinary, List<FileSpec> aOutUnknown) { foreach (var file in aFiles) { switch (FilterTextAndBinary(file)) { case FilterTextAndBinaryResult.Binary: aOutBinary.Add(file); break; case FilterTextAndBinaryResult.Text: aOutText.Add(file); break; case FilterTextAndBinaryResult.Unknown: aOutUnknown.Add(file); break; } } } public enum FilterTextAndBinaryResult { Text, Binary, Unknown } /// <summary> /// Separates the files based on their extension /// </summary> public static FilterTextAndBinaryResult FilterTextAndBinary(FileSpec aFile) { bool defaultToBinary = false; if (EditorSettings.serializationMode == SerializationMode.ForceBinary || EditorSettings.serializationMode == SerializationMode.Mixed) { defaultToBinary = true; } string extension = System.IO.Path.GetExtension(GetFilename(aFile, false)).ToLowerInvariant(); // Certain files are forced as text! if (extension == ".meta" || extension == ".txt" || extension == ".html" || extension == ".htm" || extension == ".xml" || // extension == ".bytes" || extension == ".json" || extension == ".csv" || extension == ".yaml" || extension == ".fnt" || extension == ".js" || extension == ".cs" || extension == ".sln" || extension == ".csproj" || extension == ".compute" || extension == ".unityproj") { return FilterTextAndBinaryResult.Text; } // Others depend on the serialization setting else if (extension == ".scene" || extension == ".prefab" || extension == ".mat" || extension == ".cubemap" || extension == ".flare" || extension == ".rendertexture" || extension == ".controller" || extension == ".anim" || extension == ".overridecontroller" || extension == ".mask" || extension == ".physicmaterial" || extension == ".physicmaterial2d" || extension == ".guiskin" || extension == ".fontsettings") { if (defaultToBinary) { return FilterTextAndBinaryResult.Binary; } else { return FilterTextAndBinaryResult.Text; } } // And the rest, we leave up to perforce to determine. else { return FilterTextAndBinaryResult.Unknown; } } /// <summary> /// Escape filenames for perforce /// </summary> public static string EscapeFilename(string aFilename) { return aFilename.Replace("%", "%25").Replace("@", "%40").Replace("#", "%23").Replace("*", "%2A"); } /// <summary> /// Unescape filenames for perforce /// </summary> public static string UnescapeFilename(string aFilename) { return aFilename.Replace("%25", "%").Replace("%40", "@").Replace("%23", "#").Replace("%2A", "*"); } /// <summary> /// Compare the list of submitted files to those retrieved. /// If there are unmatched files, they are reported using aErrorMessage /// </summary> /// <param name="aSubmitted">Submitted Files</param> /// <param name="aRetrieved">Files returned from operation</param> /// <param name="aErrorMessage">error message appropriate for left-overs</param> public static void CheckForMissingFilesAndWarn(IList<FileSpec> aSubmitted, IList<FileSpec> aRetrieved, string aErrorMessage) { #if DEBUG //log.Debug("CheckForMissingFilesAndWarn in: " + Logger.FileSpecListToString(aSubmitted) + " out: " + Logger.FileSpecListToString(aRetrieved)); //log.Debug("ErrorMessage: " + aErrorMessage); #endif if (aSubmitted != null && aSubmitted.Count > 0) { // Make a copy of the submitted list List<FileSpec> remainder = new List<FileSpec>(aSubmitted.UnversionedSpecs()); // Remove special entries remainder.RemoveAll(spec => spec == null || (spec.ToString().EndsWith("..."))); //log.Debug("remainder: " + Logger.FileSpecListToString(remainder)); // Remove all the files we did get back if (aRetrieved != null) { foreach (var spec in aRetrieved.UnversionedSpecs()) { for (int i = 0; i < remainder.Count; ++i) { // Submitted is Asset Path // Retrieved is Depot Paths #if DEBUG // log.Debug("\ttry this: " + LocalPathToRelativePath(remainder[i].LocalPath.Path)); // log.Debug("\t against: " + spec.LocalPath.Path); #endif if (spec.LocalPath.Path.EndsWith(LocalPathToRelativePath(remainder[i].LocalPath.Path), !IsCaseSensitive(), null)) { //log.Debug("\t removed element : " + i.ToString()); remainder.RemoveAt(i); break; } } } } if (remainder.Count > 0) { StringBuilder builder = new StringBuilder(); builder.AppendLine(aErrorMessage); foreach (var spec in remainder) { if (spec != null) { builder.AppendLine("\t" + GetFilename(spec, true)); } } string errorMsg = builder.ToString(); #if DEBUG log.Debug("\tP4Connect - " + errorMsg); #endif EditorUtility.DisplayDialog("P4Connect - Error", errorMsg, "Ok"); } } } #region LogFiles /// <summary> /// Logs a message to the console, formatting the filename properly /// </summary> public static void LogFile(FileAndMeta aFileAndMeta, string aMsg) { if (aFileAndMeta.File != null) { string filename = GetFilename(aFileAndMeta.File); if (aFileAndMeta.Meta != null) filename += " (and meta)"; Debug.Log("P4Connect - " + string.Format(aMsg, filename)); } else { if (aFileAndMeta.Meta != null) { string filename = GetFilename(aFileAndMeta.Meta); Debug.Log("P4Connect - " + string.Format(aMsg, filename)); } else { Debug.LogError("null file AND meta passed in to LogFile()"); } } } /// <summary> /// Logs a message to the console, formatting the filename properly /// </summary> public static void LogFile(Perforce.P4.FileSpec aFile, string aMsg) { LogFile(new FileAndMeta(aFile, null), aMsg); } /// <summary> /// Logs a message to the console, formatting the filename properly /// </summary> public static void LogFiles(IList<Perforce.P4.FileSpec> aFiles, string aMsg) { if (aFiles != null) { List<FileAndMeta> fileAndMetas = aFiles.ToFileAndMetas().ToList(); LogFiles(fileAndMetas, aMsg); } } /// <summary> /// Logs a message to the console, formatting the filename properly /// </summary> public static void LogFiles(IList<FileAndMeta> aFilesAndMetas, string aMsg) { if (aFilesAndMetas != null && aFilesAndMetas.Count > 0) { if (aFilesAndMetas.Count == 1) { LogFile(aFilesAndMetas[0], aMsg); } else { StringBuilder builder = new StringBuilder(); builder.Append(aFilesAndMetas.Count.ToString()); builder.Append(" assets.\n"); foreach (var fileAndMeta in aFilesAndMetas) { if (fileAndMeta.File != null) { string filename = GetFilename(fileAndMeta.File, true); if (fileAndMeta.Meta != null) filename += " (and meta)"; builder.Append(filename); builder.Append("\n"); } else { if (fileAndMeta.Meta != null) { string filename = GetFilename(fileAndMeta.Meta, true); builder.Append(filename); builder.Append("\n"); } else { Debug.LogError("null file AND meta passed in to LogFile()"); } } } builder.Remove(builder.Length - 1, 1); Debug.Log("P4Connect - " + string.Format(aMsg, builder.ToString())); } } } /// <summary> /// Logs a message to the console, formatting the filename properly /// </summary> public static void LogFile(FileAndMeta aFileAndMeta, FileAndMeta aMoveToFileAndMeta, string aMsg, bool aShowPath) { if (aFileAndMeta.File != null && aMoveToFileAndMeta.File != null) { string filename = GetFilename(aFileAndMeta.File, aShowPath); string moveToFilename = GetFilename(aMoveToFileAndMeta.File, aShowPath); if (aFileAndMeta.Meta != null) filename += " (and meta)"; Debug.Log("P4Connect - " + string.Format(aMsg, filename, moveToFilename)); } else { if (aFileAndMeta.Meta != null && aMoveToFileAndMeta.Meta != null) { string filename = GetFilename(aFileAndMeta.Meta, aShowPath); string moveToFilename = GetFilename(aMoveToFileAndMeta.Meta, aShowPath); Debug.Log("P4Connect - " + string.Format(aMsg, filename, moveToFilename)); } else { Debug.LogError("null file AND meta passed in to LogFile()"); } } } /// <summary> /// Logs a message to the console, formatting the filename properly /// </summary> public static void LogFiles(IList<FileAndMeta> aFilesAndMetas, IList<FileAndMeta> aMoveToFilesAndMetas, string aMsg) { if (aFilesAndMetas != null && aMoveToFilesAndMetas != null && aFilesAndMetas.Count > 0 && aMoveToFilesAndMetas.Count > 0) { if (aFilesAndMetas.Count == 1 && aMoveToFilesAndMetas.Count == 1) { LogFile(aFilesAndMetas[0], aMoveToFilesAndMetas[0], aMsg); } else { StringBuilder builderFrom = new StringBuilder(); foreach (var fileAndMeta in aFilesAndMetas) { if (fileAndMeta.File != null) { string filename = GetFilename(fileAndMeta.File, true); if (fileAndMeta.Meta != null) filename += " (and meta)"; builderFrom.Append(filename); builderFrom.Append("\n"); } else { if (fileAndMeta.Meta != null) { string filename = GetFilename(fileAndMeta.Meta, true); builderFrom.Append(filename); builderFrom.Append("\n"); } else { Debug.LogError("null file AND meta passed in to LogFile()"); } } } builderFrom.Remove(builderFrom.Length - 1, 1); Debug.Log("P4Connect - " + string.Format(aMsg, aFilesAndMetas.Count.ToString(), "a new location") + "\n" + builderFrom.ToString()); } } } /// <summary> /// Logs a message to the console, formatting the filename properly /// </summary> public static void LogFiles(IList<FileSpec> aFiles, IList<FileAndMeta> aMoveToFilesAndMetas, string aMsg) { if (aFiles != null && aMoveToFilesAndMetas != null) { List<FileAndMeta> fileAndMetas = aFiles.ToFileAndMetas().ToList(); LogFiles(fileAndMetas, aMoveToFilesAndMetas, aMsg); } } /// <summary> /// Logs a message to the console, formatting the filename properly /// </summary> public static void LogFile(FileAndMeta aFileAndMeta, FileAndMeta aMoveToFileAndMeta, string aMsg) { LogFile(aFileAndMeta, aMoveToFileAndMeta, aMsg, Config.ShowPaths); } /// <summary> /// Logs a message to the console, formatting the filename properly /// </summary> public static void LogFile(Perforce.P4.FileSpec aFile, Perforce.P4.FileSpec aMoveToFile, string aMsg, bool aShowPath) { LogFile(new FileAndMeta(aFile, null), new FileAndMeta(aMoveToFile, null), aMsg, aShowPath); } /// <summary> /// Logs a message to the console, formatting the filename properly /// </summary> public static void LogFile(Perforce.P4.FileSpec aFile, Perforce.P4.FileSpec aMoveToFile, string aMsg) { LogFile(aFile, aMoveToFile, aMsg, Config.ShowPaths); } /// <summary> /// Logs a warning to the console, formatting the filename properly /// </summary> public static void LogFileWarning(Perforce.P4.FileSpec aFile, string aMsg) { Debug.LogWarning("P4Connect - " + GetFilename(aFile) + aMsg); } /// <summary> /// Logs a warning to the console, formatting the filename properly /// </summary> public static void LogFileWarning(string aFile, string aMsg) { Debug.LogWarning("P4Connect - " + aFile + aMsg); } #endregion /// <summary> /// Checks whether the filepath can be used by perforce /// </summary> public static bool IsFilePathValid(string aFilePath) { bool ret = true; char[] forbiddenChars = new char[] { '@', '#', '*', '%' }; for (int i = 0; ret && i < forbiddenChars.Length; ++i) { if (aFilePath.Contains(forbiddenChars[i])) { ret = false; } } return ret; } public static string ClientPathToLocalPath(string aClientPath) { string ClientPrefix = "//" + Config.Workspace; return aClientPath.Substring(ClientPrefix.Length + 1).Replace('/', System.IO.Path.DirectorySeparatorChar); } public static string ClientPathToFullPath(string aClientPath) { return LocalPathToFullPath(ClientPathToLocalPath(aClientPath)); } public static string DepotPathToLocalPath(PerforceConnection aConnection, FileSpec aDepotPath) { IList<FileSpec> mapping = aConnection.P4Client.GetClientFileMappings(null, aDepotPath); if (mapping != null && mapping.Count > 0) { return ClientPathToLocalPath(mapping[0].ClientPath.Path); } else { return ""; } } /// <summary> /// Convert a Depot Path to an Asset Path /// Does not call back to server for a "where". Uses Config.DepotProjectRoot matching /// </summary> /// <param name="dpath">depot path</param> /// <returns>asset path</returns> public static string DepotPathToAssetPath(string dpath) { string DepotPrefix = RemoveDirectoryWildcards(Config.DepotProjectRoot); if (dpath.StartsWith(DepotPrefix)) { return dpath.Substring(DepotPrefix.Length + 1); } log.Debug("Malformed DepotPath: " + dpath); return "Asset/" + dpath; } public static string ClientPathToAssetPath(string aClientPath) { return LocalPathToAssetPath(ClientPathToLocalPath(aClientPath)); } public static string DepotPathToFullPath(PerforceConnection aConnection, FileSpec aDepotPath) { IList<FileSpec> mapping = aConnection.P4Client.GetClientFileMappings(null, aDepotPath); if (mapping != null && mapping.Count > 0) { return ClientPathToFullPath(mapping[0].ClientPath.Path); } else { return ""; } } public static string LocalPathToRelativePath(string aLocalPath) { string result = aLocalPath; string project = Main.RootPath; if (aLocalPath.StartsWith(project, !IsCaseSensitive(), null)) { if (aLocalPath.Length > project.Length) { result = aLocalPath.Substring(project.Length); } } //Debug.Log("LocalPathToRelativePath returns " + result); return result; } public static string AssetPathToLocalPath(string aAssetPath) { return System.IO.Path.Combine(Main.RootPath, aAssetPath).Replace('/', Path.DirectorySeparatorChar); } public static string LocalPathToAssetPath(string aLocalPath) { if (aLocalPath.StartsWith(Main.RootPath, !IsCaseSensitive(), null)) { var cleanedRoot = Main.RootPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (aLocalPath.Length > cleanedRoot.Length + 1) { return aLocalPath.Substring(cleanedRoot.Length + 1).Replace(Path.DirectorySeparatorChar, '/'); } else { return aLocalPath.Replace(Path.DirectorySeparatorChar, '/'); } } else { return aLocalPath.Replace(Path.DirectorySeparatorChar, '/'); } } public static string LocalPathToClientPath(string aLocalPath) { string clientPrefix = "//" + Config.Workspace; return clientPrefix + "/" + aLocalPath.Replace(Path.DirectorySeparatorChar, '/'); } /// <summary> /// Checks an asset path to see if it is a directory /// Assumes paths ending with "..." are directories /// Assumes paths without extensions are directories /// </summary> /// <param name="aAssetPath">Asset Path to test</param> /// <returns>"true" if is directory</returns> public static bool IsDirectory(string aAssetPath) { if (aAssetPath.EndsWith("...")) return true; else { string fullPath = LocalPathToFullPath(aAssetPath); if (Directory.Exists(fullPath + Path.DirectorySeparatorChar)) return true; else if (Path.GetExtension(aAssetPath) == string.Empty) return true; else return false; } } /// <summary> /// Test an asset path to see if it contains a non empty FileName /// </summary> /// <param name="aAssetPath"></param> /// <returns></returns> public static bool IsValidFilename(string aAssetPath) { var fn = Path.GetFileNameWithoutExtension(aAssetPath); if (fn == null || fn.Length <= 0) return false; return true; } /// <summary> /// Predicate used to identify which Asset Paths which are directories /// </summary> /// <param name="aAssetPath">Asset Path to check</param> /// <returns>"true" if path is a directory</returns> public static bool IsDirectoryAsset(string aAssetPath) { string fullpath = System.IO.Path.Combine(Main.RootPath, aAssetPath).Replace('/', Path.DirectorySeparatorChar); return Directory.Exists(fullpath + Path.DirectorySeparatorChar); } /// <summary> /// Return the .meta path for a given asset /// </summary> /// <param name="aAssetPath">path to Asset</param> /// <returns>path to .meta file associated with the Asset</returns> public static string MetaFromAsset(string aAssetPath) { return aAssetPath + ".meta"; } /// <summary> /// Given a path to a .meta file, return the associated asset path /// </summary> /// <param name="aMetaPath">Path to .meta file</param> /// <returns>Path to asset</returns> public static string AssetFromMeta(string aMetaPath) { return aMetaPath.Substring(0, aMetaPath.Length - 5); } /// <summary> /// Convert an Asset path to a Full Path /// </summary> /// <param name="aAssetPath">Asset Path to convert</param> /// <returns>Full Path</returns> public static string AssetPathToFullPath(string aAssetPath) { return Path.Combine(Main.RootPath, aAssetPath.Replace('/', Path.DirectorySeparatorChar)); } public static string LocalPathToFullPath(string aLocalPath) { return AssetPathToFullPath(LocalPathToAssetPath(aLocalPath)); } public static string FullPathToAssetPath(string aFullPath) { return aFullPath.Substring(Main.RootPath.Length).Replace(System.IO.Path.DirectorySeparatorChar, '/'); } public static void GetFileAndMeta(string aName, out string aFileName, out string aMetaName) { // Is this a .meta file? if so we need to fetch the original file name from it if (IsMetaFile(aName)) { aMetaName = Utils.AssetPathToLocalPath(aName); aFileName = aMetaName.Substring(0, aMetaName.Length - 5); } // If it's not a meta file, generate the meta file name from it else { aFileName = Utils.AssetPathToLocalPath(aName); aMetaName = aFileName + ".meta"; } } /// <summary> /// Given a filename, compute the meta file name /// </summary> /// <param name="aName">asset path to get meta for</param> /// <returns>asset path to matching meta file</returns> public static string GetMetaFile(string aName) { if (IsMetaFile(aName)) return aName; return aName + ".meta"; } /// <summary> /// Given a FileState, return a string description. /// </summary> /// <param name="aState"></param> /// <returns></returns> public static string GetFileStateString(FileState aState) { string ret = "<unknown>"; switch (aState) { case FileState.None: ret = "<unknown>"; break; case FileState.InDepot: ret = "None"; break; case FileState.MarkedForEdit: ret = "Edit"; break; case FileState.MarkedForAdd: ret = "Add"; break; case FileState.MarkedForDelete: ret = "Delete"; break; case FileState.MarkedForAddMove: ret = "Move/Add"; break; case FileState.MarkedForDeleteMove: ret = "Move/Delete"; break; } return ret; } public static string GetStorageTypeString(StorageType aType) { string ret = "Other"; switch (aType) { case StorageType.Text: ret = "Text"; break; case StorageType.Binary: ret = "Binary"; break; } return ret; } /// <summary> /// Query Unity to get the currently selected objects and convert them into Asset paths. /// </summary> /// <returns>An Enumeration of asset paths for the selected objects</returns> public static IEnumerable<string> GetSelectedAssetPaths() { var objects = Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.Assets); foreach (var obj in objects) { string assetPath = AssetDatabase.GetAssetPath(obj); yield return assetPath; } yield break; } public static void LaunchDiffAgainstHaveRev(string aAsset) { if (System.IO.File.Exists(Config.DiffToolPathname)) { Engine.PerformConnectionOperation(con => { FileMetaData metaData = AssetStatusCache.GetCachedMetaDataAndUpdateDefaults(aAsset); BaseFileType baseFileType = metaData.Type.BaseType; if ((baseFileType == BaseFileType.Text) || (baseFileType == BaseFileType.Unicode) || (baseFileType == BaseFileType.UTF16)) { if (metaData.HaveRev != -1) { FileSpec headRevSpec = FileSpec.LocalSpec(Utils.AssetPathToLocalPath(aAsset), metaData.HaveRev); IList<string> contents = con.P4Depot.GetFileContents(null, headRevSpec); if (contents != null && contents.Count == 2) { // Create a temp file ad dump the content in there string haveRevPathname = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetFileName(headRevSpec.ToString())); System.IO.StreamWriter tempFile = System.IO.File.CreateText(haveRevPathname); tempFile.Write(contents[1]); tempFile.Close(); // Fetch the full pathname of the original file string localPathname = Utils.AssetPathToFullPath(aAsset); // Now run the diff tool string arguments = haveRevPathname + " " + localPathname; System.Diagnostics.Process.Start(Config.DiffToolPathname, arguments); } } else { EditorUtility.DisplayDialog("File not Synced", "You have never synced this file before and cannot compare it against your HAVE revision", "Ok"); } } }); } else { EditorUtility.DisplayDialog("No Diff Utility Set", "You must point P4Connect to your favorite Diff Utility in Edit->Perforce Settings to use this feature.", "Ok"); } } public static string GetBridgeDirectory() { return System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + System.IO.Path.DirectorySeparatorChar + (IntPtr.Size == 4 ? "x86" : "x86_64" ) + System.IO.Path.DirectorySeparatorChar; } public static string GetEditorAssetFullDirectory() { return System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + System.IO.Path.DirectorySeparatorChar; } public static string GetEditorAssetRelativeDirectory() { string fullPath = GetEditorAssetFullDirectory(); return FullPathToAssetPath(fullPath); } static string GetFileSpecPath(FileSpec aSpec) { string filePath = ""; if (aSpec.LocalPath != null) filePath = aSpec.LocalPath.Path; else if (aSpec.ClientPath != null) filePath = aSpec.ClientPath.Path; else if (aSpec.DepotPath != null) filePath = aSpec.DepotPath.Path; return filePath; } /// <summary> /// Matches up a list of meta data to the list of files it was generated from /// Uses local paths. /// <param name="aFiles"> Which files to find metadata for </param> /// <param name="aMetaData"> A list of random Metadata </param> /// <param name="aOutMeta"> One Entry per file, contains Metadata or null </param> /// </summary> public static void GetMatchingMetaData(List<FileSpec> aFiles, IList<FileMetaData> aMetaData, List<FileMetaData> aOutMeta) { if (aMetaData == null || aFiles == null || aOutMeta == null) return; //Debug.Log(Logger.FileMetaDataListToString(aMetaData)); var metaDataPaths = aMetaData.ToDictionary(g => g.LocalPath.Path.ToLowerInvariant(), g => g); //Debug.Log(Logger.FileSpecListToString(aFiles)); foreach (var file in aFiles) { if (file != null) { FileMetaData meta = null; if (! metaDataPaths.TryGetValue(GetLocalPath(file).ToLowerInvariant(), out meta)) { meta = new FileMetaData(); } aOutMeta.Add(meta); } else { //log.Debug("FileData Null!"); aOutMeta.Add(null); } } } public static bool IsSolutionProjectFile(string aPath) { return aPath.EndsWith(".csproj") || aPath.EndsWith(".sln"); } public static bool IsMetaFile(string aPath) { return (aPath.EndsWith(".meta")); } /// <summary> /// Compare two local paths to see if one contains the other /// </summary> /// <param name="aSubPath">A directory Local path</param> /// <param name="aAncestorPath">A parent directory Local path</param> /// <returns>true or false</returns> public static bool IsDirOrValidSubDirectoryOf(string aSubPath, string aAncestorPath) { if (! IsCaseSensitive()) { aSubPath = aSubPath.ToLower(); aAncestorPath = aAncestorPath.ToLower(); } if (aSubPath.StartsWith(aAncestorPath.TrimEnd(System.IO.Path.DirectorySeparatorChar, '/'), StringComparison.InvariantCultureIgnoreCase)) return true; //Debug.Log("aSubPath: |" + aSubPath + "|"); //Debug.Log("Ancestor: |" + aAncestorPath + "|"); return false; } public static string GetLocalPath(FileSpec fs) { if (fs.LocalPath != null && fs.LocalPath.Path != null) return fs.LocalPath.Path; else if (fs.ClientPath != null && fs.ClientPath.Path != null) return ClientPathToLocalPath(fs.ClientPath.Path); Debug.Log("no local or clientpath in FileSpec"); return (""); } /// <summary> /// Returns the path to use when printing the name, or looking up status info /// Directories get EffectivePaths because they are used to associate .meta files. /// </summary> /// <param name="aAssetPath">Asset Path</param> /// <returns>The effective path</returns> public static string GetEffectivePath(string aAssetPath) { if (Utils.IsDirectory(aAssetPath)) { return Utils.MetaFromAsset(Utils.RemoveDirectoryWildcards(aAssetPath)); } else { return aAssetPath; } } } // Utils class /// <summary> /// Extensions class contains extension methods for use /// </summary> public static class Extensions { private static readonly ILog log = LogManager.GetLogger(typeof(Extensions)); static readonly Revision InvalidRevision = new Revision(-1); #region FileAndOp Enumerable Extensions /// <summary> /// From a Collection of operations, remove all the entries with No operation specified /// </summary> /// <param name="faos">Collection of FileAndOperations</param> /// <returns>Filtered Collection</returns> public static IEnumerable<Engine.FilesAndOp> RemoveNoOps(this IEnumerable<Engine.FilesAndOp> faos) { foreach(var fao in faos) { if (fao.FileOp != Engine.FileOperation.None) yield return fao; } yield break; } #endregion #region Generic Enumerable Extensions /// <summary> /// An Extension to split an IEnumberable into smaller pieces. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="source"></param> /// <param name="len"></param> /// <returns></returns> public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int len) { if (len == 0) throw new ArgumentNullException(); var enumer = source.GetEnumerator(); while (enumer.MoveNext()) { yield return Take(enumer.Current, enumer, len); } } private static IEnumerable<T> Take<T>(T head, IEnumerator<T> tail, int len) { while (true) { yield return head; if (--len == 0) break; if (tail.MoveNext()) head = tail.Current; else break; } } /// <summary> /// Generic extension method that removes all null entries from a IEnumerable<typeparam name="T"></typeparam> /// </summary> public static IEnumerable<T> NonNullElements<T>(this IEnumerable<T> elements) { foreach (T fs in elements) { if (fs == null) continue; yield return fs; } yield break; } #endregion #region FileSpec Enumerable Extensions /// <summary> /// Extension method to convert a list of Filespecs into ones with valid localpaths /// </summary> /// <param name="aDepotPaths">An enumerable of </param> /// <param name="aConnection">a valid PerforceConnection</param> /// <returns></returns> public static IEnumerable<FileSpec> FixLocalPaths(this IEnumerable<FileSpec> aDepotPaths, PerforceConnection aConnection) { if (aDepotPaths == null || !aDepotPaths.Any()) { //log.Debug("no depot paths"); yield break; } var needed = aDepotPaths.UnversionedSpecs(); foreach (var entry in aDepotPaths) { if (entry.LocalPath != null) yield return entry.LocalPath; var assetPath = entry.ToAssetPath(); entry.LocalPath = new LocalPath(Utils.AssetPathToLocalPath(assetPath)); yield return entry; } //IList<FileSpec> mapping = aConnection.P4Client.GetClientFileMappings(needed.ToArray()); //if (mapping == null || mapping.Count == 0) //{ // //log.Debug("no mappings"); // yield break; //} ////log.Debug("ClientFileMappings: " + Logger.FileSpecListToString(aDepotPaths.ToList())); //foreach (var entry in mapping) //{ // //log.Debug("fix: " + entry.ToStringNullSafe()); // FileSpec val = entry; // if (entry.LocalPath == null && entry.ClientPath != null) // { // val = FileSpec.LocalSpec(Utils.ClientPathToLocalPath(entry.ClientPath.Path)); // } // yield return val; //} //yield break; } /// <summary> /// Extension to convert a FileSpec into an assetpath /// </summary> /// <param name="spec"></param> /// <returns></returns> public static string ToAssetPath(this FileSpec spec) { if (HasLocalPath(spec)) return Utils.LocalPathToAssetPath(spec.LocalPath.Path); if (HasClientPath(spec)) return Utils.ClientPathToAssetPath(spec.ClientPath.Path); // Must only have a depot path return Utils.DepotPathToAssetPath(spec.DepotPath.Path); } /// <summary> /// Extension for FileSpec which allows the creation of FileAndMeta collections from FileSpec collections /// </summary> /// <param name="specs">Filespecs to process</param> /// <returns>FileAndMetas (matched pairs when possible) </returns> public static IEnumerable<FileAndMeta> ToFileAndMetas(this IEnumerable<FileSpec> specs) { if (specs == null) yield break; FamManager fm = new FamManager(); foreach (FileSpec spec in specs) fm.Add(spec); foreach (var val in fm.Values) yield return val; } /// <summary> /// Extension for FileSpec, return only entries with valid versions /// </summary> /// <param name="specs"></param> /// <returns></returns> public static IEnumerable<FileSpec> ValidSpecs(this IEnumerable<FileSpec> specs) { return specs.Where(s => !Version.Equals(s.Version, InvalidRevision)); } /// <summary> /// From a list of filespecs, just select the ones with NonLocal Specs /// </summary> /// <param name="specs"></param> /// <returns></returns> public static IEnumerable<FileSpec> NonLocalSpecs(this IEnumerable<FileSpec> specs) { return specs.Where(s => !s.HasLocalPath()); } public static IEnumerable<FileSpec> JustLocalSpecs(this IEnumerable<FileSpec> specs) { return specs.Where(s => s.HasLocalPath()); } /// <summary> /// Create a list of Unversioned Filespecs from an existing list /// </summary> /// <param name="specs">FileSpecs to copy</param> /// <returns>FileSpecs with Versions stripped</returns> public static IEnumerable<FileSpec> UnversionedSpecs(this IEnumerable<FileSpec> specs) { foreach (var fs in specs) { yield return fs.StripVersion(); } yield break; } public static IEnumerable<string> ToLocalPaths(this IEnumerable<FileSpec> specs) { foreach (var fs in specs) { yield return fs.ToLocalPath(); } yield break; } public static IEnumerable<string> ToAssetPaths(this IEnumerable<FileSpec> specs) { foreach (var fs in specs) { yield return fs.ToAssetPath(); } yield break; } /// <summary> /// Create a HashSet from an Enumeration of Asset Paths. /// </summary> /// <param name="files">Asset Paths</param> /// <returns>Hashset containing Asset Paths</returns> public static HashSet<string> ToHashSet(this IEnumerable<string> files) { HashSet<string> contents = new HashSet<string>(files); return contents; } #endregion #region PathString Enumerable Extensions /// <summary> /// Scan a collection of asset paths, Identify directories, add Perforce wildcard "/..." /// </summary> /// <param name="paths">paths to scan</param> /// <returns>paths with directories containing wildcards</returns> public static IEnumerable<string> AddDirectoryWildcards(this IEnumerable<string> paths) { foreach( string path in paths) { if (Utils.IsDirectory(path) && (! path.EndsWith("..."))) { yield return path + "/..."; } else { yield return path; } } } /// <summary> /// Given an enumeration of asset paths, look for meta files which represent directories /// Add the directory path (with wildcards) to the enumeration /// </summary> /// <param name="paths">asset paths</param> /// <returns>asset paths with additional directory entries</returns> public static IEnumerable<string> AddDirectoriesFromMetas(this IEnumerable<string> paths) { var assets = new HashSet<string>(paths); // Look for Meta files associated with directories, if found, also add the directory with Perforce wildcards // to the asset list foreach (var f in paths) { if (Utils.IsMetaFile(f)) { var rname = Utils.AssetFromMeta(f); if (Utils.IsDirectory(rname)) { yield return Utils.AddDirectoryWildcards(rname); } } yield return f; } } /// <summary> /// Strip Directory paths from a path collection /// </summary> /// <param name="paths">path collection</param> /// <returns>Filtered path collection</returns> public static IEnumerable<string> StripDirectories(this IEnumerable<string> paths) { foreach (string path in paths) { if (! Utils.IsDirectory(path)) { yield return path; } } } /// <summary> /// Return only Directory paths from an asset path collection /// </summary> /// <param name="paths">path collection</param> /// <returns>Just the directory paths</returns> public static IEnumerable<string> JustDirectories(this IEnumerable<string> paths) { foreach (string path in paths) { if (Utils.IsDirectory(path)) { yield return path; } } } /// <summary> /// Remove paths containing Directory Wildcards from an AssetPath enumeration /// </summary> /// <param name="paths">asset Paths Enumeration</param> /// <returns>asset Paths containing no wildcards</returns> public static IEnumerable<string> RemoveWildCards(this IEnumerable<string> paths) { foreach (var path in paths) { if (!Utils.HasDirectoryWildcards(path)) yield return path; } } public static IEnumerable<string> AssetToLocalPaths(this IEnumerable<string> paths) { foreach (var path in paths) { yield return Utils.AssetPathToLocalPath(path); } } public static IEnumerable<string> LocalToAssetPaths(this IEnumerable<string> paths) { foreach (var path in paths) { yield return Utils.LocalPathToAssetPath(path); } } public static IEnumerable<string> EffectivePaths(this IEnumerable<string> paths) { foreach (string path in paths) { yield return Utils.GetEffectivePath(path); } } public static IEnumerable<FileSpec> ToDepotSpecs(this IEnumerable<string> assetPaths) { string depotRoot = Utils.RemoveDirectoryWildcards(Config.DepotProjectRoot) + "/"; foreach (string asset in assetPaths) { yield return FileSpec.DepotSpec(depotRoot + asset); } } public static IEnumerable<FileSpec> ToLocalSpecs(this IEnumerable<string> assetPaths) { string localRoot = Utils.RemoveDirectoryWildcards(Config.LocalProjectRoot) + System.IO.Path.DirectorySeparatorChar; foreach (string asset in assetPaths) { yield return FileSpec.LocalSpec(localRoot + asset); } } /// <summary> /// Given a Collection of Local Paths, Explode the list to include all children in the output Collection /// Only return the files with full paths, not directories /// </summary> /// <param name="paths"></param> /// <returns></returns> public static IEnumerable<string> ExplodeLocalPaths(this IEnumerable<string> paths) { foreach (string path in paths) { if (Utils.IsDirectory(path)) { var npath = Utils.RemoveDirectoryWildcards(path); // Get The Folder Contents var files = Directory.GetFiles(npath, "*.*", SearchOption.TopDirectoryOnly); foreach (string file in files) { yield return file; } // Recurse into subdirectories var dfiles = ExplodeLocalPaths(Directory.GetDirectories(npath)); foreach (string dfile in dfiles) { yield return dfile; } } else { yield return path; } } yield break; } /// <summary> /// Takes a list of asset paths. /// For directories in the path, recurse inside /// </summary> /// <param name="paths">list of all paths to examine.</param> /// <returns>list of all "leaf nodes" in the passed in tree.</returns> public static IEnumerable<string> ExplodeAssetPaths(this IEnumerable<string> paths) { // Debug.Log("ExplodeAssetPaths " + Logger.StringArrayToString(paths.ToArray())); foreach (string path in paths.AssetToLocalPaths().ExplodeLocalPaths().LocalToAssetPaths()) { //Debug.Log("Result Asset: " + path); yield return path; } } #endregion #region FileAndMeta Enumerable Extensions /// <summary> /// Create an enumerable of FileSpec from an enumerable of FileAndMeta /// </summary> /// <param name="fams">FileAndMeta enumerable</param> /// <returns>FileSpec enumerable</returns> public static IEnumerable<FileSpec> ToFileSpecs(this IEnumerable<FileAndMeta> fams) { foreach (var fam in fams) { if (fam.File != null) { yield return fam.File; } if (fam.Meta != null) { yield return fam.Meta; } } } #endregion #region FileMetaData Extensions public static string ToAssetPath(this FileMetaData fmd) { return Utils.ClientPathToAssetPath(fmd.ClientPath.Path); } public static IEnumerable<string> ToAssetPaths(this IEnumerable<FileMetaData> fmds) { foreach (FileMetaData fmd in fmds) { yield return ToAssetPath(fmd); } } #endregion #region FamManager /// <summary> /// Class used by ToFilesAndMetas to collect files into pairs (asset and meta) with appropriate types. /// Results are returned as a FileAndMeta collection /// </summary> class FamManager { private static readonly ILog log = LogManager.GetLogger(typeof(FamManager)); Dictionary<string, FileAndMeta> famDictionary; public FamManager() { famDictionary = new Dictionary<string, FileAndMeta>(); } /// <summary> /// Add an asset path to the Fam Manager /// </summary> /// <param name="path"></param> public void Add(FileSpec fspec) { //log.Debug("Add: " + path); fspec = fspec.StripVersion(); // remove any version information from FileSpec string path = fspec.ToString(); if (Utils.IsMetaFile(path)) { // It's a meta file, is there an asset in the list as well? string assetpath = Utils.AssetFromMeta(path); FileAndMeta existingFam; if (famDictionary.TryGetValue(assetpath, out existingFam)) { existingFam.Meta = fspec; famDictionary[assetpath] = existingFam; // update existing entry } else { // Add a new entry to the famDictionary existingFam.Meta = fspec; existingFam.File = null; famDictionary[assetpath] = existingFam; // Add new entry to famDictionary } } else { string metapath = Utils.MetaFromAsset(path); FileAndMeta existingFam; // It's not a meta file, is there a meta as well? if (famDictionary.TryGetValue(path, out existingFam)) { existingFam.File = fspec; famDictionary[path] = existingFam; // update existing entry } else { FileAndMeta fam = new FileAndMeta(); fam.File = fspec; fam.Meta = null; famDictionary[path] = fam; // Add entry to famDictionary } } } /// <summary> /// Return all famDictionary elements /// </summary> public ICollection<FileAndMeta> Values { get { return famDictionary.Values; } } } #endregion #region FileSpec Extensions /// <summary> /// Extension for FileSpec to return asset path /// </summary> /// <param name="spec">FileSpec to convert to asset path</param> /// <param name="con"> If provided, this routine could query the server to determine asset path from depot path</param> /// <returns>Asset Path, relative to project</returns> public static string ToAssetPath(this FileSpec spec, PerforceConnection con = null) { string path = ""; if (spec.LocalPath != null) path = Utils.LocalPathToAssetPath(spec.LocalPath.Path); else if (spec.ClientPath != null) path = Utils.LocalPathToAssetPath(Utils.ClientPathToLocalPath(spec.ClientPath.Path)); else if (spec.DepotPath != null && con != null) path = Utils.LocalPathToAssetPath(Utils.DepotPathToLocalPath(con, spec.DepotPath)); #if DEBUG if (path.Length == 0) Debug.Log("ToAssetPath came up EMPTY!" + spec.ToStringNullSafe()); #endif return path; } /// <summary> /// Return true if a LocalPath exists and is not empty /// </summary> /// <param name="spec"></param> /// <returns>bool</returns> public static bool HasLocalPath(this FileSpec spec) { if (spec == null || spec.LocalPath == null || spec.LocalPath.Path == null || spec.LocalPath.Path.Length == 0) return false; return true; } /// <summary> /// Return true if a ClientPath exists and is not empty /// </summary> /// <param name="spec"></param> /// <returns>bool</returns> public static bool HasClientPath(this FileSpec spec) { if (spec == null || spec.ClientPath == null || spec.ClientPath.Path == null || spec.ClientPath.Path.Length == 0) return false; return true; } /// <summary> /// Extension to convert FileSpec into a localpath /// </summary> /// <param name="spec"></param> /// <returns></returns> public static string ToLocalPath(this FileSpec spec) { if (spec.LocalPath != null) return(spec.LocalPath.Path); else if (spec.ClientPath != null) return(Utils.ClientPathToLocalPath(spec.ClientPath.Path)); #if DEBUG log.Debug("Missing Local Path"); #endif return(""); } #endregion #region AssetStatus Extensions /// <summary> /// Extension which takes a set of asset paths and makes sure the AssetStatusCache is updated with "Real" values. /// AssetStatus Updates run SYNCHRONOUSLY /// </summary> /// <param name="aAssetPaths">asset paths to synchronize</param> /// <returns>The same asset paths as originally input</returns> public static IEnumerable<string> InitializeAssetStatuses(this IEnumerable<string> aAssetPaths) { List<string> defaults = new List<string>(); foreach (var path in aAssetPaths) { if (! AssetStatusCache.GetCachedAssetStatus(path).IsInitialized()) defaults.Add(path); } if (defaults.Count > 0) { Engine.PerformConnectionOperation(con => { AssetStatusCache.GetAssetMetaData(con, defaults); // Run an fstat command synchronously, when complete it will update entry. }, false); } return aAssetPaths; } /// <summary> /// Extension which takes a set of asset paths and makes sure that each Uninitialized AssetStatusCache is updated with "Real" values. /// AssetStatuses are update ASYNCHRONOUSLY /// </summary> /// <param name="aAssetPaths">asset paths to synchronize</param> /// <returns>The same asset paths passed in</returns> public static IEnumerable<string> PrepareAssetStatuses(this IEnumerable<string> aAssetPaths) { List<string> defaults = new List<string>(); foreach (var path in aAssetPaths) { if (!AssetStatusCache.GetCachedAssetStatus(path).IsInitialized()) defaults.Add(path); } if (defaults.Count > 0) { AssetStatusCache.GetFstatsFromServer(defaults); } return aAssetPaths; } /// <summary> /// Extension which returns a collection of AssetStatuses from a collection of assetPaths. /// </summary> /// <param name="aAssetPaths">paths of assets to get status</param> /// <returns>Ienumerable of AssetStatuses</returns> public static IEnumerable<AssetStatus> GetCachedAssetStatuses(this IEnumerable<string> aAssetPaths) { foreach (var path in aAssetPaths) { yield return AssetStatusCache.GetCachedAssetStatus(path); } } /// <summary> /// Extension which returns a collection of FileMetaDatas from a collection of assetPaths. /// </summary> /// <param name="aAssetPaths">paths of assets</param> /// <returns>Ienumerable of FileMetaData</returns> public static IEnumerable<FileMetaData> GetCachedAssetMetaDatas(this IEnumerable<string> aAssetPaths) { foreach (var path in aAssetPaths) { yield return AssetStatusCache.GetCachedMetaData(path); } } #endregion } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 25222 | seandanger | "Forking branch Main of perforce-software-p4connect to seandanger-p4connect." | ||
//guest/perforce_software/p4connect/main/src/P4Connect/P4Connect/P4Connect.Utils.cs | |||||
#6 | 20275 | Norman Morse |
Update source to match 2016.2 patch 2 release Much thrashing of source during refactor. Proper caching of asset statuses, reducing fstats issued. Some bug fixes by Liz Lam Cleanup of Pending changes, rework of initialization and play/stop play transitions |
||
#5 | 16560 | Norman Morse | Fix swapped parameters to IsDirOrValidSubDirectoryOf() | ||
#4 | 16485 | Norman Morse |
Asynchronous status requests Update release notes Minor cleanup |
||
#3 | 16413 | Norman Morse | Code cleanup | ||
#2 | 16350 | Norman Morse |
Minor Code Clean Up Minor Documentation Clean Up Changed Post Processor callback to ignore directories Added option to use Server Typemap |
||
#1 | 16209 | Norman Morse | Move entire source tree into "main" branch so workshop code will act correctly. | ||
//guest/perforce_software/p4connect/src/P4Connect/P4Connect/P4Connect.Utils.cs | |||||
#14 | 15383 | Norman Morse |
Improved Diagnostics, cleaned up unnecessary log output Moved some Dialog Initialization to OnEnable() Fixed Unity 5.1.1 incompatibilities Added Operation support for In Depot Deleted files |
||
#13 | 15244 | Norman Morse |
Better Directory support in "add" "get latest" "refresh" and other commands. Improved Project Root detection Various Bug Fixes and Clean up |
||
#12 | 15146 | Norman Morse |
Rewrote Config Dialog to resize well and work both vertically and horizontally. Fixed some internal issues in file handling. Removed .bytes from default type of "text" |
||
#11 | 15079 | Norman Morse |
Rewrote AssetStatusCache to Cache AssetStatuses and FileMetaData Fixed Edge conditions on Engine Operations Change Debug output defaults. Will now Checkout files which request to be "added" but which already exist in perforce. Output P4Connect version to log on initialization. |
||
#10 | 14801 | Norman Morse |
GA.9 changes. Fixed debug message exceptions Improved Pending Changes dialog for large changesets Changed configuration to allow saving configuration with Perforce disabled. Improved restart after recompile, automatically attempts connection now unless disabled. |
||
#9 | 14193 | Norman Morse |
GA.7 release Refactor Pending Changes Resolve Submit issues. Fixed Menu entries. Handle mismatched file and meta states. |
||
#8 | 13864 | Norman Morse | Final fixes for GA.5 release. | ||
#7 | 13813 | Norman Morse |
Changed Config to call CheckProjectRoot which creates ClientProjectRoot and DepotProjectRoot Filter files in changelists to include only files under the Project root. Run fstat on files in the default change to make sure we have complete metadata |
||
#6 | 12553 | Norman Morse |
integrate from internal main Build fixes for EC. Major changes to Configuration and re-initialization code. Bug fixes |
||
#5 | 12512 | Norman Morse | Integrate from Dev branch, preparing for Beta3 release | ||
#4 | 12362 | Norman Morse |
Added Debug Logging for p4log Fixed some path comparison issues. Created a CaseSensitivity test |
||
#3 | 12135 | Norman Morse |
Integrate dev branch changes into main. This code is the basiis of the 2.7 BETA release which provides Unity 5 compatibility |
||
#2 | 11447 | Norman Morse | Updates from internal main, moved to workshop | ||
#1 | 10940 | Norman Morse |
Inital Workshop release of P4Connect. Released under BSD-2 license |