using UnityEditor;
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Perforce.P4;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Text;
using log4net;
namespace P4Connect
{
public enum StorageType
{
Text = 0,
Binary,
Other
}
///
/// These are the only states that P4Connect currently recognizes
///
public enum FileState
{
None = 0,
InDepot,
MarkedForEdit,
MarkedForAdd,
MarkedForDelete,
MarkedForAddMove,
MarkedForDeleteMove,
}
///
/// Lock state of a file
/// Note that a locked file can still be modified, just not submitted
///
public enum LockState
{
None = 0,
OurLock,
TheirLock,
}
public enum DepotState
{
None = 0,
Deleted,
}
public enum RevisionState
{
None = 0,
HasLatest,
OutOfDate,
}
public enum ResolvedState
{
None = 0,
NeedsResolve,
}
// The type of operation the user is attempting, there are only a few recognized operations
public enum AssetOperation
{
None = 0,
Add,
Remove,
Checkout,
Move,
Revert,
RevertIfUnchanged,
GetLatest,
ForceGetLatest,
Lock,
Unlock,
}
///
/// Since Unity has .meta files for 'almost' every asset file, files and metas sort of go in pairs
/// BUT because they don't always, we need to keep track of what we're dealing with.
///
public enum FileAndMetaType
{
None = 0,
FileOnly,
MetaOnly,
FileAndMeta,
}
///
/// A file and its associated .meta file (either can be null)
///
public struct FileAndMeta
{
public FileSpec File;
public FileSpec Meta;
public FileAndMeta(FileSpec aFile, FileSpec aMeta)
{
File = aFile;
Meta = aMeta;
}
}
///
/// This class is the meat of P4Connect. It grabs lists of files and talks to the server to get stuff done
///
public partial class Engine
{
public delegate void OnOperationPerformedDelegate(PerforceConnection aConnection, List aFileAndMetaList);
public static event OnOperationPerformedDelegate OnOperationPerformed;
private static readonly ILog log = LogManager.GetLogger(typeof(Engine));
// The types of operations which can be performed on a Perforce file
// There are more than asset operations because the current state of the file matters.
public enum FileOperation
{
None = 0,
Add,
RevertAndAddNoOverwrite,
Checkout,
Delete,
Revert,
RevertIfUnchanged,
RevertAndDelete,
RevertAndCheckout,
RevertAndCheckoutNoOverwrite,
Move,
RevertAndMove,
RevertAndMoveNoOverwrite,
MoveToNewLocation,
GetLatest,
ForceGetLatest,
RevertAndGetLatest,
Lock,
Unlock,
}
///
/// We try to treat assets and .meta files in pair, but they may not always be,
/// and furthermore, the operations needed on each one may not be the same.
///
public struct FilesAndOp
{
public FileSpec File;
public FileSpec Meta;
public FileSpec MoveToFile;
public FileSpec MoveToMeta;
public FileOperation FileOp;
}
///
/// Stores all our potential perforce operations and for each, a list of files to perform that operation on
///
class FileListOperations
{
private static readonly ILog log = LogManager.GetLogger(typeof(FileListOperations));
///
/// Stores a list of Perforce files (FileSpec) on which to perform one operation
///
struct FileListOperation
{
public int FileCount
{
get { return _Files.Count; }
}
public string Description
{
get { return _Description; }
}
// The list of files to perform the P4 operation on
List _Files;
List _MoveToFiles;
// The description of the task
string _Description;
// The delegate that performs the operation (quicker to do it this way than to use a class hierarchy).
Func, IList, IList> _Operation;
static List _EmptyList;
static FileListOperation()
{
_EmptyList = new List();
}
///
/// Initializing constructor
///
///
///
public FileListOperation(string aDescription, Func, IList, IList> aOperation)
{
_Files = new List();
_MoveToFiles = new List();
_Description = aDescription;
_Operation = aOperation;
}
///
/// Add a file to the list for this operation
///
public void Add(FileSpec aFile, FileSpec aMoveToFile, FileSpec aMeta, FileSpec aMoveToMeta)
{
_Files.Add(new FileAndMeta(aFile, aMeta));
_MoveToFiles.Add(new FileAndMeta(aMoveToFile, aMoveToMeta));
}
///
/// Runs the perforce operation on the files collected so far.
///
public IList Run(PerforceConnection aConnection)
{
IList ret = null;
if (_Files.Any())
{
ret = _Operation(aConnection, _Files, _MoveToFiles);
_Files.Clear();
}
else
{
ret = _EmptyList;
}
return ret;
}
}
// A map of operations and list of files to perform that operation on
Dictionary _FileOperations;
///
/// Initializing constructor
///
public FileListOperations()
{
_FileOperations = new Dictionary();
_FileOperations.Add(FileOperation.RevertAndDelete,
new FileListOperation("Deleting Files", Operations.RevertAndDelete));
_FileOperations.Add(FileOperation.Delete,
new FileListOperation("Deleting Files", Operations.Delete));
_FileOperations.Add(FileOperation.Revert,
new FileListOperation("Reverting Files", Operations.Revert));
_FileOperations.Add(FileOperation.RevertIfUnchanged,
new FileListOperation("Reverting Unchanged Files", Operations.RevertIfUnchanged));
_FileOperations.Add(FileOperation.RevertAndCheckout,
new FileListOperation("Restoring Files", Operations.RevertAndCheckout));
_FileOperations.Add(FileOperation.RevertAndCheckoutNoOverwrite,
new FileListOperation("Checking out Files", Operations.RevertAndCheckoutNoOverwrite));
_FileOperations.Add(FileOperation.Add,
new FileListOperation("Adding Files", Operations.Add));
_FileOperations.Add(FileOperation.RevertAndAddNoOverwrite,
new FileListOperation("Moving Files", Operations.RevertAndCheckoutNoOverwrite));
_FileOperations.Add(FileOperation.Checkout,
new FileListOperation("Checking out Files", Operations.Checkout));
_FileOperations.Add(FileOperation.Move,
new FileListOperation("Moving Files", Operations.Move));
_FileOperations.Add(FileOperation.RevertAndMove,
new FileListOperation("Moving Files", Operations.RevertAndMove));
_FileOperations.Add(FileOperation.RevertAndMoveNoOverwrite,
new FileListOperation("Moving Files", Operations.RevertAndMoveNoOverwrite));
_FileOperations.Add(FileOperation.MoveToNewLocation,
new FileListOperation("Moving Files", Operations.MoveToNewLocation));
_FileOperations.Add(FileOperation.GetLatest,
new FileListOperation("Syncing Files", Operations.GetLatest));
_FileOperations.Add(FileOperation.ForceGetLatest,
new FileListOperation("Syncing Files", Operations.ForceGetLatest));
_FileOperations.Add(FileOperation.RevertAndGetLatest,
new FileListOperation("Syncing Files", Operations.RevertAndGetLatest));
_FileOperations.Add(FileOperation.Lock,
new FileListOperation("Locking Files", Operations.Lock));
_FileOperations.Add(FileOperation.Unlock,
new FileListOperation("Unlocking Files", Operations.Unlock));
}
///
/// Add a file to the list for this operation
///
public void Add(FilesAndOp aFileAndOp)
{
if (aFileAndOp.FileOp != FileOperation.None)
{
FileListOperation op = _FileOperations[aFileAndOp.FileOp];
op.Add(aFileAndOp.File, aFileAndOp.MoveToFile, aFileAndOp.Meta, aFileAndOp.MoveToMeta);
}
}
///
/// Runs the perforce operation on the files collected so far.
///
public List Run(PerforceConnection aConnection)
{
// Count the files
int totalFiles = 0;
foreach (var op in _FileOperations.Values)
{
totalFiles += op.FileCount;
}
// Perform all operations
int currentCount = 0;
List allSpecs = new List();
foreach (var op in _FileOperations.Values)
{
if (op.FileCount > 2)
{
EditorUtility.DisplayProgressBar("Hold on", "P4Connect - " + op.Description, (float)currentCount / (float)totalFiles);
}
currentCount += op.FileCount;
var opRes = op.Run(aConnection);
if (opRes != null)
{
allSpecs.AddRange(opRes);
}
}
if (totalFiles > 2)
{
EditorUtility.ClearProgressBar();
}
return allSpecs;
}
}
static List EmptyFileAndMeta;
///
/// Initialize the P4Connect Engine
///
public static void Initialize()
{
EmptyFileAndMeta = new List();
}
static bool check_local_ignore(string ignore_line, string path)
{
// string msg = "check_local_ignore: " + ignore_line + " == " + path;
//Debug.Log(msg);
bool caseSensitive = Utils.IsCaseSensitive();
int ignore_ll = ignore_line.Length;
if (ignore_ll == 0)
{
return false;
}
int path_ll = path.Length;
if (ignore_ll == path_ll)
{
if (0 == String.Compare(ignore_line, path, caseSensitive))
{
// Debug.Log("Ignore Line Match Ignored: " + path);
return true;
}
}
else if (ignore_ll < path_ll) // could be a subdirectory
{
if (ignore_line[ignore_ll - 1] == '/') // ignore line ends with slash (match all children)
{
if (0 == String.Compare(ignore_line, 0, path, 0, ignore_ll, caseSensitive))
{
// The path is a child of the ignore line
//Debug.Log("Child Ignored: " + path);
return true;
}
}
}
return false;
}
static bool is_ignored(string path, PerforceConnection aConnection)
{
string[] dels = new string[] { "\n", "\r" };
path = path.Trim();
// Debug.Log("is_ignored: " + path);
// Check the additional ignore list:
if (! String.IsNullOrEmpty(Config.IgnoreLines))
{
foreach (var ipath in Config.IgnoreLines.Split(dels, StringSplitOptions.RemoveEmptyEntries))
{
if (check_local_ignore(ipath.Trim(), path))
return true;
}
}
// Check if in P4IGNORE
if (aConnection.P4Connection.IsFileIgnored(path))
{
//Debug.Log("P4IGNORE Ignored: " + path);
return true;
}
return false;
}
public static string[] StripIgnore(string[] files, PerforceConnection aConnection)
{
string[] result = files.Where(path => !is_ignored(path, aConnection)).ToArray();
//log.DebugFormat("StripIgnore {0} returns {1}", Logger.StringArrayToString(files), Logger.StringArrayToString(result));
return(result);
}
///
/// This method is called by Unity when assets are created BY Unity itself
/// Note: This is not an override because Unity calls it through Invoke()
///
public static List CreateAsset(string arPath)
{
string[] filesToAdd = new string[] { arPath };
return CreateAssets(filesToAdd);
}
///
/// This method is called by Unity when assets are created BY Unity itself
/// Note: This is not an override because Unity calls it through Invoke()
///
public static List CreateAssets(string[] arPaths)
{
// Creation of assets doesn't need to happen right away
return PerformOperation(arPaths, null, AssetOperation.Add);
}
///
/// This method is called by Unity when assets are deleted
///
public static List DeleteAsset(string arPath)
{
// Deletion of assets doesn't need to happen right away
var paths = new String[] { arPath };
return DeleteAssets(paths);
}
///
/// This method is called by Unity when assets are deleted
///
public static List DeleteAssets(string[] arPath)
{
// Deletion of assets doesn't need to happen right away
return PerformOperation(arPath.Where(p => p.Length > 0).ToArray(), null, AssetOperation.Remove);
}
///
/// Called to simply mark a file as modified
///
public static List CheckoutAsset(string arPath)
{
// Checking out assets does need to happen right away
string[] filesToCheckout = new string[] { arPath };
return CheckoutAssets(filesToCheckout);
}
///
/// This method is called by Unity when assets are moved
///
public static List MoveAssets(string[] arPath, string[] arMoveToPath)
{
// Deletion of assets doesn't need to happen right away
return PerformOperation(arPath, arMoveToPath, AssetOperation.Move);
}
///
///
///
public static List MoveAsset(string arPath, string arMoveToPath)
{
// Checking out assets does need to happen right away
string[] filesToMove = new string[] { arPath };
string[] filesToMoveTo = new string[] { arMoveToPath };
return MoveAssets(filesToMove, filesToMoveTo);
}
///
/// This method is called by Unity when assets are deleted
///
public static List RevertAssets(string[] arPath, bool aForce)
{
// Deletion of assets doesn't need to happen right away
if (aForce)
return PerformOperation(arPath, null, AssetOperation.Revert);
else
return PerformOperation(arPath, null, AssetOperation.RevertIfUnchanged);
}
///
/// Called to simply revert a modified file
///
public static List RevertAsset(string arPath, bool aForce)
{
// Checking out assets does need to happen right away
string[] filesToRevert = new string[] { arPath };
return RevertAssets(filesToRevert, aForce);
}
///
/// Called to simply mark a file as modified
///
public static List CheckoutAssets(string[] arPaths)
{
// Checking out assets does need to happen right away
return PerformOperation(arPaths, null, AssetOperation.Checkout);
}
///
/// Called to lock files
///
public static List LockAssets(string[] arPaths)
{
// Checking out assets does need to happen right away
return PerformOperation(arPaths, null, AssetOperation.Lock);
}
///
/// Called to unlock files
///
public static List UnlockAssets(string[] arPaths)
{
// Checking out assets does need to happen right away
return PerformOperation(arPaths, null, AssetOperation.Unlock);
}
///
/// This method is called when the user wants to sync files
///
public static List GetLatestAssets(string[] arPath, bool aForce)
{
if (aForce)
return PerformOperation(arPath, null, AssetOperation.ForceGetLatest);
else
return PerformOperation(arPath, null, AssetOperation.GetLatest);
}
///
/// This method is called when the user wants to sync files
///
public static List GetLatestAsset(string arPath, bool aForce)
{
// Checking out assets does need to happen right away
string[] filesToGetLatest = new string[] { arPath };
return GetLatestAssets(filesToGetLatest, aForce);
}
///
/// Returns the lock state of the passed in file
///
public static LockState GetLockState(string arPath, PerforceConnection aConnection)
{
if (Utils.IsFilePathValid(arPath))
return GetLockState(FileSpec.LocalSpec(Utils.AssetPathToLocalPath(arPath)), aConnection);
else
return LockState.None;
}
///
/// Returns the lock state of the passed in file
///
public static LockState GetLockState(FileSpec aFile, PerforceConnection aConnection)
{
IList dataList = GetFileMetaData(aConnection, null, aFile);
return GetLockState(dataList);
}
///
/// Returns the lock state of the passed in file
///
public static LockState GetLockState(IList aMeta)
{
LockState retState = LockState.None;
if (aMeta != null)
{
foreach (var data in aMeta)
{
if (data.OurLock)
retState = LockState.OurLock;
else if (data.OtherLock)
retState = LockState.TheirLock;
}
}
return retState;
}
///
/// Returns the lock state of the passed in file
///
public static LockState GetLockState(FileMetaData aMeta)
{
//log.Debug("aMeta: " + Logger.ToStringNullSafe(aMeta));
LockState retState = LockState.None;
if (aMeta != null)
{
if (aMeta.OurLock)
retState = LockState.OurLock;
else if (aMeta.OtherLock)
retState = LockState.TheirLock;
}
return retState;
}
///
/// Returns the state of the passed in file
///
public static FileState GetFileState(string arPath, PerforceConnection aConnection)
{
if (Utils.IsFilePathValid(arPath))
return GetFileState(FileSpec.LocalSpec(Utils.AssetPathToLocalPath(arPath)), aConnection);
else
return FileState.None;
}
///
/// Returns the state of the passed in file
///
public static FileState GetFileState(FileSpec aFile, PerforceConnection aConnection)
{
IList dataList = GetFileMetaData(aConnection, null, aFile);
return GetFileState(dataList);
}
///
/// Returns the state of the passed in file
///
public static FileState GetFileState(IList aMetaData)
{
FileState retState = FileState.None;
if (aMetaData != null)
{
foreach (var data in aMetaData)
{
retState = ParseFileAction(data.Action);
}
}
return retState;
}
///
/// Returns the state of the passed in file
///
public static FileState GetFileState(FileMetaData aMetaData)
{
return ParseFileAction(aMetaData.Action);
}
///
/// Returns the state of the passed in file on the server (if checked out by someone else for instance)
///
public static FileState GetServerFileState(string arPath, PerforceConnection aConnection)
{
if (Utils.IsFilePathValid(arPath))
return GetServerFileState(FileSpec.LocalSpec(Utils.AssetPathToLocalPath(arPath)), aConnection);
else
return FileState.None;
}
///
/// Returns the state of the passed in file on the server (if checked out by someone else for instance)
///
public static FileState GetServerFileState(FileSpec arFile, PerforceConnection aConnection)
{
FileState retState = FileState.None;
IList dataList = GetFileMetaData(aConnection, null, arFile);
if (dataList != null)
{
foreach (var data in dataList)
{
if (data.OtherActions != null)
{
foreach (var action in data.OtherActions)
{
FileState otherState = ParseFileAction(action);
if (otherState != FileState.InDepot)
{
retState = otherState;
}
}
}
}
}
return retState;
}
///
/// Returns the state of the passed in file on the server (if checked out by someone else for instance)
///
public static RevisionState GetRevisionState(string arPath, PerforceConnection aConnection)
{
if (Utils.IsFilePathValid(arPath))
return GetRevisionState(FileSpec.LocalSpec(Utils.AssetPathToLocalPath(arPath)), aConnection);
else
return RevisionState.None;
}
///
/// Returns the state of the passed in file on the server (if checked out by someone else for instance)
///
public static RevisionState GetRevisionState(FileSpec aFile, PerforceConnection aConnection)
{
RevisionState retState = RevisionState.None;
IList dataList = GetFileMetaData(aConnection, null, aFile);
if (dataList != null)
{
foreach (var data in dataList)
{
if (data.HaveRev == data.HeadRev)
retState = RevisionState.HasLatest;
else
retState = RevisionState.OutOfDate;
}
}
return retState;
}
///
/// Performs an operation after opening a perforce connection
///
public static void PerformConnectionOperation(System.Action aConnectionOperation)
{
if (Config.ValidConfiguration)
{
PerforceConnection connection = new PerforceConnection();
try // The try block will make sure the connection is Disposed (i.e. closed)
{
// Perform the operation
aConnectionOperation(connection);
}
catch (Exception ex)
{
log.Error("Operation Error:", ex);
LogP4Exception(ex);
}
finally
{
connection.Dispose();
}
}
else
{
log.Error("Configuration Invalid");
}
}
///
/// Performs an operation after opening a perforce connection and getting the opened files
///
//public static void PerformOpenConnectionOperation(System.Action aConnectionOperation)
//{
// PerformConnectionOperation(con => aConnectionOperation(new OpenedConnection(con)));
//}
///
/// Submits the files to the server
///
public static void SubmitFiles(PerforceConnection aConnection, string aChangeListDescription, List aFiles)
{
#if DEBUG
log.DebugFormat("files: {0}", Logger.FileSpecListToString(aFiles));
#endif
// Move all the files to a new changelist
Changelist changeList = new Changelist();
changeList.Description = aChangeListDescription;
changeList.ClientId = Config.Workspace;
var allFilesMetaRaw = GetFileMetaData(aConnection, aFiles, null);
List allFilesMeta = new List();
Utils.GetMatchingMetaData(aFiles, allFilesMetaRaw, allFilesMeta);
List lockedFiles = new List();
for (int i = 0; i < aFiles.Count; i++)
{
var metaData = allFilesMeta[i];
if (GetLockState(metaData) == LockState.TheirLock)
{
lockedFiles.Add(metaData.LocalPath);
}
else
{
changeList.Files.Add(metaData);
}
}
bool cont = lockedFiles.Count == 0;
if (cont)
{
#if DEBUG
log.Debug("Changelist files: " + Logger.FileSpecListToString(aFiles));
log.Debug("Changelist has " + aFiles.Count + " files");
log.Debug("ChangeSpec is: " + changeList.ToString());
#endif
Changelist repList = null;
try
{
repList = aConnection.P4Depot.CreateChangelist(changeList);
}
catch (System.Exception ex)
{
#if DEBUG
log.Debug("CreateChangelist Exception", ex);
#endif
Debug.LogException(ex);
Debug.LogWarning("P4Connect - CreateChangelist failed, open P4V and make sure your files are in the default changelist");
if (ex is Perforce.P4.P4Exception)
{
Debug.LogWarning("Exception caused by this cmd: " + (ex as P4Exception).CmdLine);
}
cont = false;
}
if (cont)
{
Options submitFlags = new Options(SubmitFilesCmdFlags.None, -1, repList, null, null);
SubmitResults sr = null;
try
{
EditorUtility.DisplayProgressBar("Hold on", "Submitting Files", 0.5f);
sr = aConnection.P4Client.SubmitFiles(submitFlags, null);
}
catch(Exception ex)
{
// may fail, cannot submit from non-stream client
// may fail because we need to resolve
#if DEBUG
log.Error("SubmitFiles Exception", ex);
#endif
Debug.LogWarning("P4Connect - Submit failed, You may need to use P4V to resolve conflicts.\n" + ex.ToString());
cont = false;
}
finally
{
EditorUtility.ClearProgressBar();
}
if (cont)
{
List submittedFiles = new List(sr.Files.Select(srec => srec.File));
Utils.LogFiles(submittedFiles, "Submitting {0}");
if (sr.Files.Count != changeList.Files.Count)
{
Debug.LogWarning("P4Connect - Not All files were submitted");
}
}
else
{
EditorUtility.DisplayDialog("Cannot submit files...", "Submit failed3, You may need to use P4V to resolve conflicts.", "Ok");
}
// Notify that things changed either way
if (OnOperationPerformed != null)
{
List allFilesAndMetas = new List();
foreach (var file in aFiles)
{
allFilesAndMetas.Add(new FileAndMeta(file, null));
}
OnOperationPerformed(aConnection, allFilesAndMetas);
}
}
else
{
EditorUtility.DisplayDialog("Cannot submit files...", "Submit failed4, open P4V and make sure your files are in the default changelist", "Ok");
}
}
else
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("The following files are locked by someone else and cannot be submitted.");
foreach (var file in lockedFiles)
{
builder.Append("\t" + file.LocalPath.Path);
}
EditorUtility.DisplayDialog("Cannot submit locked files...", builder.ToString(), "Ok");
}
}
///
/// Attempts to perform the operation immediately
///
static List PerformOperation(string[] arPaths, string[] arMoveToPaths, AssetOperation aDesiredOp)
{
#if DEBUG
log.DebugFormat("op: {0}, paths: {1} moveto: {2}",
aDesiredOp.ToString(), Logger.StringArrayToString(arPaths), Logger.StringArrayToString(arMoveToPaths));
#endif
List result = EmptyFileAndMeta;
if (result == null)
log.Error("result is null!");
if (Config.ValidConfiguration)
{
try
{
// The using statement will make sure the connection is Disposed (i.e. closed)
using (PerforceConnection connection = new PerforceConnection())
{
arPaths = StripIgnore(arPaths, connection);
List filesAndMetas = new List();
AddToFileAndMetaList(connection, arPaths, arMoveToPaths, aDesiredOp, filesAndMetas);
FileListOperations operations = new FileListOperations();
AddToOperations(filesAndMetas, operations);
result = Utils.GetFileAndMetas(operations.Run(connection));
// Trigger events
if (OnOperationPerformed != null)
{
OnOperationPerformed(connection, result);
}
}
}
catch (Exception ex)
{
// Don't attempt to reconnect until settings change
EditorUtility.ClearProgressBar();
log.Error("... Exception ", ex);
LogP4Exception(ex);
}
}
#if DEBUG
log.DebugFormat("result: {0}", Logger.FileAndMetaListToString(result));
#endif
return result;
}
///
/// Given a list of asset file paths and operation, fills a list of perforce files and operation for each
///
/// The perforce connection to use to query current state of files on the depot
/// The list of files (these can be a mix and match of .meta files and regular files)
/// The operation to perform on those
/// Whether the operations have already happened, i.e. the files have already been created or deleted
/// INOUT: The list of perforce files to fill out
static void AddToFileAndMetaList(PerforceConnection aConnection, string[] arAssetPaths, string[] arMoveToAssetPaths, AssetOperation aDesiredOperation, List aInOutFilesAndMetas)
{
// Build the list of files and metas
List fileSpecs = new List(); // there may be null values in here
List metaSpecs = new List(); // there may be null values in here
List moveToFileSpecs = new List(); // there may be null values in here
List moveToMetaSpecs = new List(); // there may be null values in here
List areFolders = new List();
#if DEBUG
log.DebugFormat("op: {0} paths: {1} to: {2} inout: {3}",
aDesiredOperation.ToString(),
Logger.StringArrayToString(arAssetPaths),
Logger.StringArrayToString(arMoveToAssetPaths),
Logger.FilesAndOpListToString(aInOutFilesAndMetas));
#endif
for (int i = 0; i < arAssetPaths.Length; ++i)
{
string FileName = "";
string MetaName = "";
Utils.GetFileAndMeta(arAssetPaths[i], out FileName, out MetaName);
string MoveToFileName = "";
string MoveToMetaName = "";
if (arMoveToAssetPaths != null)
{
Utils.GetFileAndMeta(arMoveToAssetPaths[i], out MoveToFileName, out MoveToMetaName);
}
// If the operation hasn't happened yet, we can be more strict with our verifications
bool isFolder = Utils.IsDirectory(Utils.LocalPathToAssetPath(FileName));
areFolders.Add(isFolder);
// Escape filenames
string escapedFileName = FileName;
string escapedMetaName = MetaName;
string escapedMoveToFileName = MoveToFileName;
string escapedMoveToMetaName = MoveToMetaName;
if (isFolder)
{
// Add special spec for "all subdirs"
fileSpecs.Add(FileSpec.LocalSpec(System.IO.Path.Combine(escapedFileName, "...")));
if (MoveToFileName != "")
moveToFileSpecs.Add(FileSpec.LocalSpec(System.IO.Path.Combine(escapedMoveToFileName, "...")));
else
moveToFileSpecs.Add(null);
}
else
{
// It's a file, so queue it up
fileSpecs.Add(FileSpec.LocalSpec(escapedFileName));
if (MoveToFileName != "")
moveToFileSpecs.Add(FileSpec.LocalSpec(escapedMoveToFileName));
else
moveToFileSpecs.Add(null);
}
if (ShouldFileHaveMetaFile(FileName)) // this includes folders
{
metaSpecs.Add(FileSpec.LocalSpec(escapedMetaName));
if (MoveToMetaName != "")
moveToMetaSpecs.Add(FileSpec.LocalSpec(escapedMoveToMetaName));
else
moveToMetaSpecs.Add(null);
}
else
{
metaSpecs.Add(null);
moveToMetaSpecs.Add(null);
}
}
Options fsopts = new Options();
fsopts["-Op"] = null; // Return "real" clientpaths
// Get the meta data as a chunk and then remap them properly
var filesMetaDataRaw = GetFileMetaData(aConnection, fileSpecs, fsopts);
var metasMetaDataRaw = GetFileMetaData(aConnection, metaSpecs, fsopts);
List filesMetaData = new List();
List metasMetaData = new List();
Utils.GetMatchingMetaData(fileSpecs, filesMetaDataRaw, filesMetaData);
Utils.GetMatchingMetaData(metaSpecs, metasMetaDataRaw, metasMetaData);
// Now create the file operations
for (int i = 0; i < arAssetPaths.Length; ++i)
{
// Build the FileAndMeta data
FilesAndOp fileAndOp = new FilesAndOp();
fileAndOp.File = fileSpecs[i];
fileAndOp.MoveToFile = moveToFileSpecs[i];
#if DEBUG
log.DebugFormat("processing file: {0}", fileSpecs[i].ToString());
#endif
var fileMetaData = filesMetaData[i];
if (fileMetaData != null)
{
FileState fileState = GetFileState(fileMetaData);
LockState lockState = GetLockState(fileMetaData);
fileAndOp.FileOp = GetFileOperation(fileAndOp.File, aDesiredOperation, fileState, lockState);
}
else
{
fileAndOp.FileOp = GetDefaultFileOperation(aDesiredOperation);
}
if (metaSpecs[i] != null)
{
FileOperation metaOp = FileOperation.None;
var metaMetaData = metasMetaData[i];
if (metaMetaData != null)
{
FileState metaState = GetFileState(metaMetaData);
LockState metaLockState = GetLockState(metaMetaData);
metaOp = GetFileOperation(fileAndOp.Meta, aDesiredOperation, metaState, metaLockState);
}
else
{
metaOp = GetDefaultFileOperation(aDesiredOperation);
}
if (metaOp == fileAndOp.FileOp && (!areFolders[i] || fileAndOp.FileOp != FileOperation.Add))
{
// Lump it in with the fileOp
fileAndOp.Meta = metaSpecs[i];
fileAndOp.MoveToMeta = moveToMetaSpecs[i];
}
else
{
// Treat the meta separately
FilesAndOp metaAndOp = new FilesAndOp();
metaAndOp.FileOp = metaOp;
metaAndOp.Meta = metaSpecs[i];
metaAndOp.MoveToMeta = moveToMetaSpecs[i];
aInOutFilesAndMetas.Add(metaAndOp);
}
}
// Add the operation
if (!areFolders[i] || fileAndOp.FileOp != FileOperation.Add)
{
aInOutFilesAndMetas.Add(fileAndOp);
}
}
#if DEBUG
log.DebugFormat("results: {0} ", Logger.FilesAndOpListToString(aInOutFilesAndMetas));
#endif
}
///
/// Adds the list of perforce files (and associated .meta if applicable) to the list of files/operations
///
static void AddToOperations(List aFileAndMetas, FileListOperations aInOutFileOperations)
{
foreach (FilesAndOp fileAndMeta in aFileAndMetas)
{
aInOutFileOperations.Add(fileAndMeta);
}
}
///
/// Parses a metadata file action and translates it into a state for P4connect
///
public static FileState ParseFileAction(FileAction aAction)
{
FileState outOp = FileState.InDepot;
switch (aAction)
{
case FileAction.Add:
outOp = FileState.MarkedForAdd;
break;
case FileAction.Delete:
//outOp = FileState.InDepotDeleted;
outOp = FileState.MarkedForDelete;
break;
case FileAction.Edit:
outOp = FileState.MarkedForEdit;
break;
case FileAction.MoveAdd:
outOp = FileState.MarkedForAddMove;
break;
case FileAction.MoveDelete:
outOp = FileState.MarkedForDeleteMove;
break;
default:
//We don't really handle anything else
break;
}
return outOp;
}
///
/// Gets the default file operation, based on the asset operation
///
static FileOperation GetDefaultFileOperation(AssetOperation aDesiredOperation)
{
FileOperation outOp = FileOperation.None;
switch (aDesiredOperation)
{
case AssetOperation.None:
outOp = FileOperation.None;
break;
case AssetOperation.Add:
outOp = FileOperation.Add;
break;
case AssetOperation.Remove:
outOp = FileOperation.Delete;
break;
case AssetOperation.Checkout:
outOp = FileOperation.Checkout;
break;
case AssetOperation.Move:
outOp = FileOperation.Move;
break;
case AssetOperation.Revert:
outOp = FileOperation.Revert;
break;
case AssetOperation.RevertIfUnchanged:
outOp = FileOperation.RevertIfUnchanged;
break;
case AssetOperation.GetLatest:
outOp = FileOperation.GetLatest;
break;
case AssetOperation.ForceGetLatest:
outOp = FileOperation.ForceGetLatest;
break;
case AssetOperation.Lock:
outOp = FileOperation.Lock;
break;
case AssetOperation.Unlock:
outOp = FileOperation.Unlock;
break;
}
return outOp;
}
///
/// Determines what Perforce operation to perform on the passed in file, given what the user wants to do and what the current state of the file is
///
static FileOperation GetFileOperation(FileSpec aFile, AssetOperation aDesiredOperation, FileState aCurrentState, LockState aCurrentLockState)
{
FileOperation outOp = FileOperation.None;
#if DEBUG
log.DebugFormat("desired: {0} file: {1} state: {2} lock: {3} ",
aDesiredOperation.ToString(),
aFile == null ? "null" : aFile.ToString(),
aCurrentState.ToString(), aCurrentLockState);
#endif
switch (aCurrentState)
{
case FileState.None:
switch (aDesiredOperation)
{
case AssetOperation.None:
outOp = FileOperation.None;
break;
case AssetOperation.Add:
outOp = FileOperation.Add;
break;
case AssetOperation.Remove:
outOp = FileOperation.None;
break;
case AssetOperation.Checkout:
// trying to checkout something not in the depot, add it instead
Utils.LogFileWarning(aFile, " isn't in the depot and cannot be checked out, the file will be marked for add instead");
outOp = FileOperation.Add;
break;
case AssetOperation.Move:
// trying to move something not in the depot, add it instead
Utils.LogFileWarning(aFile, " isn't in the depot and cannot be moved, the file will be marked for add instead");
outOp = FileOperation.Add;
break;
case AssetOperation.Revert:
// trying to revert something not in the depot
Utils.LogFileWarning(aFile, " isn't in the depot and cannot be reverted");
outOp = FileOperation.None;
break;
case AssetOperation.RevertIfUnchanged:
// trying to revert something not in the depot
Utils.LogFileWarning(aFile, " isn't in the depot and cannot be reverted");
outOp = FileOperation.None;
break;
case AssetOperation.GetLatest:
// trying to revert something not in the depot
Utils.LogFileWarning(aFile, " isn't in the depot and cannot be synced");
outOp = FileOperation.None;
break;
case AssetOperation.ForceGetLatest:
// trying to revert something not in the depot
Utils.LogFileWarning(aFile, " isn't in the depot and cannot be synced");
outOp = FileOperation.None;
break;
case AssetOperation.Lock:
// trying to lock something not in the depot
Utils.LogFileWarning(aFile, " isn't in the depot and cannot be locked");
outOp = FileOperation.None;
break;
case AssetOperation.Unlock:
// trying to unlock something not in the depot
Utils.LogFileWarning(aFile, " isn't in the depot and cannot be unlocked");
outOp = FileOperation.None;
break;
}
break;
case FileState.InDepot:
switch (aDesiredOperation)
{
case AssetOperation.None:
outOp = FileOperation.None;
break;
case AssetOperation.Add:
// The file is already in the depot, nothing to do
outOp = FileOperation.None;
break;
case AssetOperation.Remove:
outOp = FileOperation.Delete;
break;
case AssetOperation.Checkout:
outOp = FileOperation.Checkout;
break;
case AssetOperation.Move:
outOp = FileOperation.Move;
break;
case AssetOperation.Revert:
outOp = FileOperation.None;
break;
case AssetOperation.RevertIfUnchanged:
outOp = FileOperation.None;
break;
case AssetOperation.GetLatest:
outOp = FileOperation.GetLatest;
break;
case AssetOperation.ForceGetLatest:
outOp = FileOperation.ForceGetLatest;
break;
case AssetOperation.Lock:
outOp = GetLockOperation(aFile, aCurrentLockState);
break;
case AssetOperation.Unlock:
outOp = GetUnlockOperation(aFile, aCurrentLockState);
break;
}
break;
case FileState.MarkedForEdit:
switch (aDesiredOperation)
{
case AssetOperation.None:
outOp = FileOperation.None;
break;
case AssetOperation.Add:
// Already in the depot and checked out, do nothing
outOp = FileOperation.None;
break;
case AssetOperation.Remove:
outOp = FileOperation.RevertAndDelete;
break;
case AssetOperation.Checkout:
outOp = FileOperation.None;
break;
case AssetOperation.Move:
// Already in the depot and checked out, just flag it for a move
outOp = FileOperation.MoveToNewLocation;
break;
case AssetOperation.Revert:
outOp = FileOperation.Revert;
break;
case AssetOperation.RevertIfUnchanged:
outOp = FileOperation.RevertIfUnchanged;
break;
case AssetOperation.GetLatest:
outOp = FileOperation.GetLatest;
break;
case AssetOperation.ForceGetLatest:
outOp = FileOperation.ForceGetLatest;
break;
case AssetOperation.Lock:
outOp = GetLockOperation(aFile, aCurrentLockState);
break;
case AssetOperation.Unlock:
outOp = GetUnlockOperation(aFile, aCurrentLockState);
break;
}
break;
case FileState.MarkedForAdd:
switch (aDesiredOperation)
{
case AssetOperation.None:
outOp = FileOperation.None;
break;
case AssetOperation.Add:
outOp = FileOperation.None;
break;
case AssetOperation.Remove:
outOp = FileOperation.Revert;
break;
case AssetOperation.Checkout:
// Already marked for add
outOp = FileOperation.None;
break;
case AssetOperation.Move:
outOp = FileOperation.RevertAndAddNoOverwrite;
break;
case AssetOperation.Revert:
outOp = FileOperation.Revert;
break;
case AssetOperation.RevertIfUnchanged:
outOp = FileOperation.None;
break;
case AssetOperation.GetLatest:
Utils.LogFileWarning(aFile, " is marked as add and cannot be synced, the file will be skipped");
outOp = FileOperation.None;
break;
case AssetOperation.ForceGetLatest:
Utils.LogFileWarning(aFile, " is marked as add and cannot be synced, the file will be skipped");
outOp = FileOperation.None;
break;
case AssetOperation.Lock:
outOp = GetLockOperation(aFile, aCurrentLockState);
break;
case AssetOperation.Unlock:
outOp = GetUnlockOperation(aFile, aCurrentLockState);
break;
}
break;
case FileState.MarkedForDelete:
switch (aDesiredOperation)
{
case AssetOperation.None:
outOp = FileOperation.None;
break;
case AssetOperation.Add:
outOp = FileOperation.RevertAndCheckoutNoOverwrite;
break;
case AssetOperation.Remove:
outOp = FileOperation.None;
break;
case AssetOperation.Checkout:
outOp = FileOperation.None;
break;
case AssetOperation.Move:
outOp = FileOperation.RevertAndMove;
break;
case AssetOperation.Revert:
outOp = FileOperation.Revert;
break;
case AssetOperation.RevertIfUnchanged:
outOp = FileOperation.None;
break;
case AssetOperation.GetLatest:
Utils.LogFileWarning(aFile, " is marked for delete, the file will be skipped");
outOp = FileOperation.None;
break;
case AssetOperation.ForceGetLatest:
Utils.LogFileWarning(aFile, " is marked for delete, the file will be reverted and then synced");
outOp = FileOperation.RevertAndGetLatest;
break;
case AssetOperation.Lock:
outOp = GetLockOperation(aFile, aCurrentLockState);
break;
case AssetOperation.Unlock:
outOp = GetUnlockOperation(aFile, aCurrentLockState);
break;
}
break;
case FileState.MarkedForAddMove:
switch (aDesiredOperation)
{
case AssetOperation.None:
outOp = FileOperation.None;
break;
case AssetOperation.Add:
outOp = FileOperation.None;
break;
case AssetOperation.Remove:
outOp = FileOperation.RevertAndDelete;
break;
case AssetOperation.Checkout:
outOp = FileOperation.None;
break;
case AssetOperation.Move:
outOp = FileOperation.MoveToNewLocation;
break;
case AssetOperation.Revert:
outOp = FileOperation.Revert;
break;
case AssetOperation.RevertIfUnchanged:
outOp = FileOperation.None;
break;
case AssetOperation.GetLatest:
Utils.LogFileWarning(aFile, " is marked for move/add, the file will be skipped");
outOp = FileOperation.None;
break;
case AssetOperation.ForceGetLatest:
Utils.LogFileWarning(aFile, " is marked for move/add, the file will be reverted and then synced");
outOp = FileOperation.RevertAndGetLatest;
break;
case AssetOperation.Lock:
outOp = GetLockOperation(aFile, aCurrentLockState);
break;
case AssetOperation.Unlock:
outOp = GetUnlockOperation(aFile, aCurrentLockState);
break;
}
break;
case FileState.MarkedForDeleteMove:
switch (aDesiredOperation)
{
case AssetOperation.None:
outOp = FileOperation.None;
break;
case AssetOperation.Add:
outOp = FileOperation.RevertAndCheckoutNoOverwrite;
break;
case AssetOperation.Remove:
outOp = FileOperation.None;
break;
case AssetOperation.Checkout:
outOp = FileOperation.None;
break;
case AssetOperation.Move:
outOp = FileOperation.RevertAndMoveNoOverwrite;
break;
case AssetOperation.Revert:
outOp = FileOperation.Revert;
break;
case AssetOperation.RevertIfUnchanged:
outOp = FileOperation.None;
break;
case AssetOperation.GetLatest:
Utils.LogFileWarning(aFile, " is marked for move/delete, the file will be skipped");
outOp = FileOperation.None;
break;
case AssetOperation.ForceGetLatest:
Utils.LogFileWarning(aFile, " is marked for move/delete, the file will be reverted and then synced");
outOp = FileOperation.RevertAndGetLatest;
break;
case AssetOperation.Lock:
outOp = GetLockOperation(aFile, aCurrentLockState);
break;
case AssetOperation.Unlock:
outOp = GetUnlockOperation(aFile, aCurrentLockState);
break;
}
break;
}
#if DEBUG
log.DebugFormat("returns: {0}", outOp.ToString());
#endif
return outOp;
}
static FileOperation GetLockOperation(FileSpec aFile, LockState aCurrentLockState)
{
if (aCurrentLockState == LockState.None)
return FileOperation.Lock;
else
{
if (aCurrentLockState == LockState.OurLock)
{
Utils.LogFileWarning(aFile, " is already locked, the file will be skipped");
}
else
{
Utils.LogFileWarning(aFile, " is locked by someone else, the file will be skipped");
}
return FileOperation.None;
}
}
static FileOperation GetUnlockOperation(FileSpec aFile, LockState aCurrentLockState)
{
if (aCurrentLockState == LockState.OurLock)
return FileOperation.Unlock;
else
{
if (aCurrentLockState == LockState.TheirLock)
{
Utils.LogFileWarning(aFile, " is not locked by you, the file will be skipped");
}
else
{
Utils.LogFileWarning(aFile, " is not locked, the file will be skipped");
}
return FileOperation.None;
}
}
///
/// Logs a P4 exception and display an accompanying message
///
static void LogP4Exception(Exception ex)
{
Debug.LogException(ex);
Debug.LogWarning("P4Connect - P4Connect encountered the following internal exception, your file changes may not be properly checked out/added/deleted.");
// Parse exception text for dialog box
EditorUtility.DisplayDialog("Perforce Exception", "P4Connect encountered the following exception:\n\n" + ex.Message, "Ok");
//+ "\nP4Connect will disable itself to prevent further problems. Please go to Edit->Perforce Settings to correct the issue.", "Ok");
// Config.NeedToCheckSettings();
}
///
/// Helper method that filters files that shouldn't have a .meta file associated with them
///
///
///
static bool ShouldFileHaveMetaFile(string arPath)
{
bool bret = true;
// Maybe this is a project settings file, those don't seem to have .meta files with them
if (arPath.Contains("ProjectSettings"))
bret = false;
if (arPath.Contains("DotSettings"))
bret = false;
if (arPath.Contains(".unityproj"))
bret = false;
if (arPath.Contains(".csproj"))
bret = false;
if (arPath.Contains(".sln"))
bret = false;
if (arPath.EndsWith(".meta"))
bret = false;
if (arPath == "Assets")
bret = false;
if (arPath == "")
bret = false;
return bret;
}
///
/// Utility method that adds all non null files and meta files from a list of pairs
///
static IEnumerable NonNullFileSpecs(IEnumerable aFiles)
{
foreach(FileSpec fs in aFiles)
{
if (fs == null)
continue;
yield return fs;
}
yield break;
}
///
/// Utility method that adds all non null files and meta files from a list of pairs
///
static List NonNullFilesAndMeta(IEnumerable aFilesAndMeta)
{
List retList = new List();
foreach (var fileAndMeta in aFilesAndMeta)
{
if (fileAndMeta.File != null)
retList.Add(fileAndMeta.File);
if (fileAndMeta.Meta != null)
retList.Add(fileAndMeta.Meta);
}
return retList;
}
public enum WildcardCheck
{
None, // No wildcards detected
Force, // Wildcards detected but user agreed to force the operation
Cancel, // Wildcards detected and user canceled
}
///
/// Checks for wildcards and asks the user whether they want to force the operation
///
public static WildcardCheck VerifyWildcards(IEnumerable aFiles)
{
var allFiles = NonNullFilesAndMeta(aFiles);
List filesWithWildCards = new List();
foreach (var file in allFiles)
{
string filename = Utils.GetFilename(file, false);
if (filename.IndexOfAny(new char[] { '*', '%', '#', '@' }) != -1)
{
filesWithWildCards.Add(filename);
}
}
bool hasWildcards = filesWithWildCards.Count > 0;
if (hasWildcards)
{
if (Config.WarnOnSpecialCharacters)
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("The following files have special characters in them, are you sure you want to add them to the depot?");
foreach (string file in filesWithWildCards)
{
builder.AppendLine("\t" + file);
}
builder.AppendLine("Note: You can disable this warning in the Perforce Settings");
if (EditorUtility.DisplayDialog("Special Characters", builder.ToString(), "Ok", "Cancel"))
{
return WildcardCheck.Force;
}
else
{
return WildcardCheck.Cancel;
}
}
else
{
return WildcardCheck.Force;
}
}
else
{
return WildcardCheck.None;
}
}
///
/// Performs a Get-Latest operation, and then checks to see if there are folders that need to be deleted or conflicts
///
static IList PerformGetLastestAndPostChecks(PerforceConnection aConnection, List aOriginals, Options aOptions)
{
var result = aConnection.P4Client.SyncFiles(aOriginals, aOptions);
if (result != null)
{
Utils.LogFiles(result, "Syncing {0}");
List allFolderDepotMetaFiles = new List();
foreach (FileSpec spec in result)
{
string depotPath = spec.DepotPath.Path;
if (depotPath.EndsWith(".meta"))
{
string assetPath = Utils.AssetFromMeta(depotPath);
if (Utils.IsDirectory(Utils.LocalPathToAssetPath(Utils.AssetFromMeta(spec.LocalPath.Path))))
{
// We found the meta of a folder, add it
allFolderDepotMetaFiles.Add(FileSpec.DepotSpec(depotPath));
}
}
}
// Get the folder meta data
if (allFolderDepotMetaFiles.Count > 0)
{
IList allFolderMetaMeta = GetFileMetaData(aConnection, allFolderDepotMetaFiles, null);
if (allFolderMetaMeta != null)
{
for (int i = 0; i < allFolderMetaMeta.Count; ++i)
{
// Check to see if the file has been deleted
if ((allFolderMetaMeta[i].HeadAction == FileAction.Delete || allFolderMetaMeta[i].HeadAction == FileAction.MoveDelete))
{
// Delete the local folder
string fullpath = Utils.DepotPathToFullPath(aConnection, new FileSpec(allFolderMetaMeta[i]));
string folderPath = Utils.AssetFromMeta(fullpath);
if (System.IO.Directory.Exists(folderPath))
{
// Check that the folder is empty of files
var files = System.IO.Directory.GetFiles(folderPath, "*.*", System.IO.SearchOption.AllDirectories);
if (files == null || files.Length == 0)
{
try
{
System.IO.Directory.Delete(folderPath, true);
}
catch (System.IO.DirectoryNotFoundException)
{
// Ignore missing folders, they may have already been deleted
}
}
else
{
Debug.LogWarning("P4Connect - " + Utils.FullPathToAssetPath(fullpath) + " was deleted, but the matching directory still contains undeleted files and so was kept");
}
}
}
}
}
}
// Check for conflicts by getting the state of original files
List originalsAgain = new List();
foreach (FileSpec spec in aOriginals)
{
originalsAgain.Add(FileSpec.LocalSpec(spec.LocalPath.Path));
}
// Get the local meta data
IList allLocalMeta = GetFileMetaData(aConnection, originalsAgain, null);
if (allLocalMeta != null)
{
List unresolvedFiles = new List();
foreach (FileMetaData meta in allLocalMeta)
{
if (meta.Unresolved)
{
unresolvedFiles.Add(meta);
}
}
if (unresolvedFiles.Count > 0)
{
System.Text.StringBuilder builder = new StringBuilder();
builder.AppendLine("The following files have unresolved conflicts as a result of the Get Latest Operation");
foreach (var meta in unresolvedFiles)
{
builder.AppendLine(meta.LocalPath.Path);
}
builder.AppendLine("You should launch P4V and resolve the conflict before continuing your work");
EditorUtility.DisplayDialog("Unresolved Files Detected", builder.ToString(), "Ok");
}
}
}
return result;
}
public static IList GetFileMetaData(PerforceConnection aConnection, Options aOptions, params FileSpec[] aSpecs)
{
if (Config.DisplayP4Timings)
{
DateTime startTimestamp = DateTime.Now;
var ret = aConnection.P4Depot.GetFileMetaData(aOptions, aSpecs);
double deltaInnerTime = (DateTime.Now - startTimestamp).TotalMilliseconds;
aConnection.AppendTimingInfo("GetFileMetaDataTime " + deltaInnerTime.ToString() + " ms for " + aSpecs.Length + " files (" + (ret != null ? ret.Count : 0).ToString() + " retrieved)");
return ret;
}
else
{
return aConnection.P4Depot.GetFileMetaData(aOptions, aSpecs);
}
}
public static IList GetFileMetaData(PerforceConnection aConnection, IList aSpecs, Options aOptions)
{
IList ret = new List();
IList nonNullSpecs = FileSpec.UnversionedSpecList(NonNullFileSpecs(aSpecs).ToList());
if (nonNullSpecs.Any())
{
if (Config.DisplayP4Timings)
{
DateTime startTimestamp = DateTime.Now;
ret = aConnection.P4Depot.GetFileMetaData(nonNullSpecs, aOptions);
double deltaInnerTime = (DateTime.Now - startTimestamp).TotalMilliseconds;
aConnection.AppendTimingInfo("GetFileMetaDataTime " + deltaInnerTime.ToString() + " ms for " + aSpecs.Count + " files (" + (ret != null ? ret.Count : 0).ToString() + " retrieved)");
}
else
{
ret = aConnection.P4Depot.GetFileMetaData(nonNullSpecs, aOptions);
Debug.Log("metadata: " + Logger.FileMetaDataListToString(ret));
}
}
return ret;
}
}
}