using UnityEditor; using UnityEngine; using System; using System.Threading; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using Perforce.P4; using log4net; namespace P4Connect { /// /// Stores and periodically updates the Perforce status of all the assets /// public class AssetStatusCache { private static readonly ILog log = LogManager.GetLogger(typeof(AssetStatusCache)); /// /// Simple asset status struct /// Stores the file state (checked out, modified, etc...) and revision state /// public struct AssetStatus { public DateTime TimeStamp; public FileState LocalState; public FileState OtherState; public DepotState DepotState; public RevisionState RevisionState; public ResolvedState ResolvedState; public StorageType StorageType; public LockState LockState; // Use this to create an AssetStatus for a local file (perforce would return null from the fstat call) public AssetStatus(string assetPath) { TimeStamp = DateTime.UtcNow; LocalState = FileState.None; OtherState = FileState.None; DepotState = DepotState.None; RevisionState = P4Connect.RevisionState.None; ResolvedState = P4Connect.ResolvedState.None; StorageType = StorageType.Text; LockState = LockState.None; } public override string ToString() { StringBuilder sb = new StringBuilder(); sb.AppendFormat("TimeStamp: {0} ", TimeStamp); sb.AppendFormat("LocalState: {0} ", LocalState); sb.AppendFormat("OtherState: {0} ", OtherState); sb.AppendFormat("DepotState: {0} ", DepotState); sb.AppendFormat("Revision: {0} ", RevisionState); sb.AppendFormat("Resolved: {0} ", ResolvedState); sb.AppendFormat("Storage: {0} ", StorageType); sb.AppendFormat("Lockstate: {0} ", LockState); return (sb.ToString()); } public bool IsOnServer() { return (LocalState != FileState.None && LocalState != FileState.MarkedForAdd); } public bool IsLocked() { return (LockState == LockState.OurLock || LockState == LockState.TheirLock); } public bool IsOpened() { return (! (LocalState == FileState.None || LocalState == FileState.InDepot)); } } /// /// This delegate is triggered when the status of an asset changes /// public delegate void OnAssetStatusChangedDelegate(PerforceConnection aConnection); public static event OnAssetStatusChangedDelegate OnAssetStatusChanged { add { if (_OnAssetStatusChanged == null) { // We had no reason to get updated before, so do it now Engine.OnOperationPerformed += OnEngineOperationPerformed; } _OnAssetStatusChanged += value; } remove { _OnAssetStatusChanged -= value; if (_OnAssetStatusChanged == null) { // We no longer have any reason to be updated Engine.OnOperationPerformed -= OnEngineOperationPerformed; } } } // The internal event that is actually registered with static event OnAssetStatusChangedDelegate _OnAssetStatusChanged; // The cache static Dictionary _Statuses; public static void Initialize() { _Statuses = new Dictionary(); } /// /// Generic method that fetches the cached asset status (checked out, out of date, etc...) /// public static AssetStatus GetAssetStatus(string aAssetPath) { AssetStatus ret = new AssetStatus(); if (_Statuses != null) { _Statuses.TryGetValue(GetEffectivePath(aAssetPath), out ret); } return ret; } /// /// Create an asset status (checked out, out of date, etc...) /// Faster on multiple files because it batches the P4 requests /// Guaranteed to have one AssetStatus per AssetPath /// public static void GetAssetData(PerforceConnection aConnection, List aAssetPaths, List aOutStatuses) { if (! Config.ValidConfiguration) return; DateTime startTimestamp = DateTime.Now; List localFiles = GetFileSpecs(aAssetPaths); GetAssetData(aConnection, localFiles, aOutStatuses); // Cache the AssetStatuses in the _Statuses dictionary for (int i = 0; i < aAssetPaths.Count; ++i) { _Statuses[GetEffectivePath(aAssetPaths[i])] = aOutStatuses[i]; } if (Config.DisplayP4Timings) { double deltaInnerTime = (DateTime.Now - startTimestamp).TotalMilliseconds; aConnection.AppendTimingInfo("GetAssetData " + deltaInnerTime.ToString() + " ms for " + aAssetPaths.Count + " files (" + aOutStatuses.Count + " retrieved)"); } } /// /// Force P4Connect to update the status of a file in its status cache /// /// List of Linux style Project Relative Paths public static void UpdateAssetData(List aAssetPaths) { if (! Config.ValidConfiguration) return; //Debug.Log("paths: " + Logger.StringArrayToString(aAssetPaths.ToArray())); List localFiles = GetFileSpecs(aAssetPaths); List statuses = new List(); //Debug.Log("Filespecs: " + Logger.FileSpecListToString(localFiles)); Engine.PerformConnectionOperation(con => { GetAssetData(con, localFiles, statuses); }); if (statuses.Count == aAssetPaths.Count) { for (int i = 0; i < aAssetPaths.Count; ++i) { _Statuses[GetEffectivePath(aAssetPaths[i])] = statuses[i]; } } } /// /// Given the meta data, figures out the P4Connect icon representation for the file /// There is only a subset of all the metadata that P4Connect actually displays /// static AssetStatus CreateAssetStatus(FileMetaData aMeta) { //Debug.Log("CreateAssetStatus: " + Logger.FileMetaDataToString(aMeta)); AssetStatus retData = new AssetStatus(); if (aMeta == null) return retData; // Fill out the struct switch (aMeta.Type.BaseType) { case BaseFileType.Text: retData.StorageType = StorageType.Text; break; case BaseFileType.Binary: retData.StorageType = StorageType.Binary; break; default: retData.StorageType = StorageType.Other; break; } retData.LocalState = Engine.ParseFileAction(aMeta.Action); if (retData.LocalState == FileState.None) { // Check to see if Perforce knows this file so we can override the localState to InDepot. if (aMeta.IsMapped || aMeta.HeadAction != FileAction.None) retData.LocalState = FileState.InDepot; } if (aMeta.OtherActions != null) { retData.OtherState = Engine.ParseFileAction(aMeta.OtherActions[0]); } if (aMeta.HaveRev >= aMeta.HeadRev) retData.RevisionState = RevisionState.HasLatest; else retData.RevisionState = RevisionState.OutOfDate; if (aMeta.Unresolved) retData.ResolvedState = ResolvedState.NeedsResolve; else retData.ResolvedState = ResolvedState.None; if (aMeta.HeadAction == FileAction.MoveDelete || aMeta.HeadAction == FileAction.Delete) retData.DepotState = DepotState.Deleted; else retData.DepotState = DepotState.None; retData.TimeStamp = DateTime.Now; retData.LockState = Engine.GetLockState(aMeta); // Debug.Log("AssetStatus: " + retData.ToString()); return retData; } /// /// This functions gets called when P4Connect does *something* to files /// In this case we update the files status /// static void OnEngineOperationPerformed(PerforceConnection aConnection, List aFilesAndMetas) { if (aFilesAndMetas.Count > 0) { List allSpecs = new List(); foreach (var fileAndMeta in aFilesAndMetas) { if (fileAndMeta.File != null) { allSpecs.Add(fileAndMeta.File); } if (fileAndMeta.Meta != null) { allSpecs.Add(fileAndMeta.Meta); } } allSpecs = allSpecs.ValidSpecs().ToList(); List allStatuses = new List(); GetAssetData(aConnection, allSpecs, allStatuses); // Cache the returned values for (int i = 0; i < allSpecs.Count; ++i) { string path = allSpecs[i].ToAssetPath(aConnection); if (! string.IsNullOrEmpty(path)) { _Statuses[GetEffectivePath(path)] = allStatuses[i]; #if DEBUG log.Debug("_Statuses[" + GetEffectivePath(path) + "] = " + allStatuses[i].ToString()); #endif } } if (_OnAssetStatusChanged != null) { _OnAssetStatusChanged(aConnection); } } } /// /// Using the Perforce connection, get a matching AssetStatus for each FileSpec /// If the metadata for a file is not in Perforce, we provide a "local" AssetStatus instead. /// /// Perforce Connection /// Files to look up /// AssetStatus List returned, one for each File static void GetAssetData(PerforceConnection aConnection, List aFiles, List aOutStatuses) { //DateTime startTimestamp = DateTime.Now; if (aFiles.Count > 0) { Options fsopts = new Options(); fsopts["-Op"] = null; // Return "real" clientpaths //Debug.Log("GetAssetData: " + Logger.FileSpecListToString(aFiles)); Dictionary dict = new Dictionary(); // Create a Dictionary from any non-null response. var mdlist = Engine.GetFileMetaData(aConnection, aFiles, fsopts); if (mdlist != null) { //Debug.Log("AssetData: " + Logger.FileMetaDataListToString(mdlist)); foreach(var md in mdlist) { dict.Add(md.LocalPath.Path, md); } } //if (Config.DisplayP4Timings) //{ // double deltaInnerTime = (DateTime.Now - matchingMetaStartTime).TotalMilliseconds; // aConnection.AppendTimingInfo("GetMatchingMetaData " + deltaInnerTime.ToString() + " ms"); //} foreach( FileSpec f in aFiles) { AssetStatus status; FileMetaData md = new FileMetaData(); if (dict.TryGetValue(f.LocalPath.Path, out md)) { status = CreateAssetStatus(md); } else { status = new AssetStatus(); } aOutStatuses.Add(status); } } //Debug.Log("GetAssetData: filecount " + aFiles.Count.ToString() + " statuscount: " + aOutStatuses.Count.ToString()); //Debug.Log("GetAssetData: content " + Logger.AssetStatusListToString(aOutStatuses)); //if (Config.DisplayP4Timings) //{ // double deltaInnerTime = (DateTime.Now - startTimestamp).TotalMilliseconds; // aConnection.AppendTimingInfo("GetAssetData " + deltaInnerTime.ToString() + " ms"); //} } /// /// Returns a proper list of file spec given a list of paths, accounting for directories /// static List GetFileSpecs(List aAssetPaths) { List localFiles = new List(); foreach (var path in aAssetPaths) { localFiles.Add(FileSpec.LocalSpec(Utils.AssetPathToLocalPath(GetEffectivePath(path)))); } return localFiles; } /// /// Returns the path to use when printing the name, or looking up status info /// static string GetEffectivePath(string aAssetPath) { if (Utils.IsDirectory(aAssetPath)) { return Utils.MetaFromAsset(aAssetPath); } else { return aAssetPath; } } } }