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; } } }