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 } /// <summary> /// These are the only states that P4Connect currently recognizes /// </summary> public enum FileState { None = 0, InDepot, InDepotDeleted, MarkedForEdit, MarkedForAdd, MarkedForDelete, MarkedForAddMove, MarkedForDeleteMove, IsDirectory, } /// <summary> /// Lock state of a file /// Note that a locked file can still be modified, just not submitted /// </summary> 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, } /// <summary> /// 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. /// </summary> public enum FileAndMetaType { None = 0, FileOnly, MetaOnly, FileAndMeta, } /// <summary> /// A file and its associated .meta file (either can be null) /// </summary> public struct FileAndMeta { public FileSpec File; public FileSpec Meta; public FileAndMeta(FileSpec aFile, FileSpec aMeta) { File = aFile; Meta = aMeta; } public override string ToString() { return ("file: " + File.ToStringNullSafe() + " meta: " + Meta.ToStringNullSafe()); } } /// <summary> /// An Asset and its associated .meta file (either can be null) /// Uses Asset Path strings instead of fileSpecs. /// </summary> public struct AssetAndMeta { public string File; public string Meta; public AssetAndMeta(string aFile, string aMeta) { File = aFile; Meta = aMeta; } public FileAndMeta ToFileAndMeta() { FileSpec fsFile = (File == null) ? null : FileSpec.LocalSpec(Utils.AssetPathToLocalPath(File)); FileSpec fsMeta = (Meta == null) ? null : FileSpec.LocalSpec(Utils.AssetPathToLocalPath(Meta)); return new FileAndMeta(fsFile,fsMeta); } public override string ToString() { return ("file: " + File.ToStringNullSafe() + " meta: " + Meta.ToStringNullSafe()); } } /// <summary> /// This class is the meat of P4Connect. It grabs lists of files and talks to the server to get stuff done /// </summary> public partial class Engine { public delegate void OnOperationPerformedDelegate(PerforceConnection aConnection, List<FileAndMeta> 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, } /// <summary> /// 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. /// </summary> public struct FilesAndOp { public FileSpec File; public FileSpec Meta; public FileSpec MoveToFile; public FileSpec MoveToMeta; public FileOperation FileOp; public FilesAndOp(string file, string meta, string movefile, string movemeta, FileOperation fop) { File = Meta = MoveToFile = MoveToMeta = null; if (file != null) File = FileSpec.LocalSpec(Utils.AssetPathToLocalPath(file)); if (meta != null) Meta = FileSpec.LocalSpec(Utils.AssetPathToLocalPath(meta)); if (movefile != null) MoveToFile = FileSpec.LocalSpec(Utils.AssetPathToLocalPath(movefile)); if (movemeta != null) MoveToMeta = FileSpec.LocalSpec(Utils.AssetPathToLocalPath(movemeta)); FileOp = fop; } } /// <summary> /// Stores all our potential perforce operations and for each, a list of files to perform that operation on /// </summary> class FileListOperations { private static readonly ILog log = LogManager.GetLogger(typeof(FileListOperations)); /// <summary> /// Stores a list of Perforce files (FileSpec) on which to perform one operation /// </summary> 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<FileAndMeta> _Files; List<FileAndMeta> _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<PerforceConnection, IList<FileAndMeta>, IList<FileAndMeta>, IList<FileSpec>> _Operation; static List<FileSpec> _EmptyList; static FileListOperation() { _EmptyList = new List<FileSpec>(); } /// <summary> /// Initializing constructor /// </summary> /// <param name="aPrefix"></param> /// <param name="aOperation"></param> public FileListOperation(string aDescription, Func<PerforceConnection, IList<FileAndMeta>, IList<FileAndMeta>, IList<FileSpec>> aOperation) { _Files = new List<FileAndMeta>(); _MoveToFiles = new List<FileAndMeta>(); _Description = aDescription; _Operation = aOperation; } /// <summary> /// Add a file to the list for this operation /// </summary> public void Add(FileSpec aFile, FileSpec aMoveToFile, FileSpec aMeta, FileSpec aMoveToMeta) { _Files.Add(new FileAndMeta(aFile, aMeta)); _MoveToFiles.Add(new FileAndMeta(aMoveToFile, aMoveToMeta)); } /// <summary> /// Runs the perforce operation on the files collected so far. /// </summary> public IList<FileSpec> Run(PerforceConnection aConnection) { IList<FileSpec> 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<FileOperation, FileListOperation> _FileOperations; /// <summary> /// Initializing constructor /// </summary> public FileListOperations() { _FileOperations = new Dictionary<FileOperation, FileListOperation>(); _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)); } /// <summary> /// Add a file to the list for this operation /// </summary> 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); } } /// <summary> /// Runs the perforce operation on the files collected so far. /// </summary> public List<FileSpec> 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<FileSpec> allSpecs = new List<FileSpec>(); 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<FileAndMeta> EmptyFileAndMeta; /// <summary> /// Initialize the P4Connect Engine /// </summary> public static void Initialize() { EmptyFileAndMeta = new List<FileAndMeta>(); } 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); } /// <summary> /// 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() /// </summary> public static List<FileAndMeta> CreateAsset(string arPath) { string[] filesToAdd = new string[] { arPath }; return CreateAssets(filesToAdd); } /// <summary> /// 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() /// </summary> public static List<FileAndMeta> CreateAssets(string[] arPaths) { // Creation of assets doesn't need to happen right away return PerformOperation(arPaths, null, AssetOperation.Add); } /// <summary> /// This method is called by Unity when assets are deleted /// </summary> public static List<FileAndMeta> DeleteAsset(string arPath) { // Deletion of assets doesn't need to happen right away var paths = new String[] { arPath }; return DeleteAssets(paths); } /// <summary> /// This method is called by Unity when assets are deleted /// </summary> public static List<FileAndMeta> 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); } /// <summary> /// Called to simply mark a file as modified /// </summary> public static List<FileAndMeta> CheckoutAsset(string arPath) { // Checking out assets does need to happen right away string[] filesToCheckout = new string[] { arPath }; return CheckoutAssets(filesToCheckout); } /// <summary> /// This method is called by Unity when assets are moved /// </summary> public static List<FileAndMeta> MoveAssets(string[] arPath, string[] arMoveToPath) { // Deletion of assets doesn't need to happen right away return PerformOperation(arPath, arMoveToPath, AssetOperation.Move); } /// <summary> /// /// </summary> public static List<FileAndMeta> 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); } /// <summary> /// This method is called by Unity when assets are deleted /// </summary> public static List<FileAndMeta> 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); } /// <summary> /// Called to simply revert a modified file /// </summary> public static List<FileAndMeta> RevertAsset(string arPath, bool aForce) { // Checking out assets does need to happen right away string[] filesToRevert = new string[] { arPath }; return RevertAssets(filesToRevert, aForce); } /// <summary> /// Called to simply mark a file as modified /// </summary> public static List<FileAndMeta> CheckoutAssets(string[] arPaths) { // Checking out assets does need to happen right away return PerformOperation(arPaths, null, AssetOperation.Checkout); } /// <summary> /// Called to lock files /// </summary> public static List<FileAndMeta> LockAssets(string[] arPaths) { // Checking out assets does need to happen right away return PerformOperation(arPaths, null, AssetOperation.Lock); } /// <summary> /// Called to unlock files /// </summary> public static List<FileAndMeta> UnlockAssets(string[] arPaths) { // Checking out assets does need to happen right away return PerformOperation(arPaths, null, AssetOperation.Unlock); } /// <summary> /// This method is called when the user wants to sync files /// </summary> public static List<FileAndMeta> GetLatestAssets(string[] arPath, bool aForce) { if (aForce) return PerformOperation(arPath, null, AssetOperation.ForceGetLatest); else return PerformOperation(arPath, null, AssetOperation.GetLatest); } /// <summary> /// This method is called when the user wants to sync files /// </summary> public static List<FileAndMeta> GetLatestAsset(string arPath, bool aForce) { // Checking out assets does need to happen right away string[] filesToGetLatest = new string[] { arPath }; return GetLatestAssets(filesToGetLatest, aForce); } /// <summary> /// Returns the lock state of the passed in file /// </summary> public static LockState GetLockState(string arPath, PerforceConnection aConnection) { if (Utils.IsFilePathValid(arPath)) return GetLockState(FileSpec.LocalSpec(Utils.AssetPathToLocalPath(arPath)), aConnection); else return LockState.None; } /// <summary> /// Returns the lock state of the passed in file /// </summary> public static LockState GetLockState(FileSpec aFile, PerforceConnection aConnection) { IList<FileMetaData> dataList = GetFileMetaData(aConnection, null, aFile); return GetLockState(dataList); } /// <summary> /// Returns the lock state of the passed in file /// </summary> public static LockState GetLockState(IList<FileMetaData> 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; } /// <summary> /// Returns the lock state of the passed in file /// </summary> 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; } /// <summary> /// Returns the state of the passed in file /// </summary> public static FileState GetFileState(string arPath, PerforceConnection aConnection) { if (Utils.IsFilePathValid(arPath)) return GetFileState(FileSpec.LocalSpec(Utils.AssetPathToLocalPath(arPath)), aConnection); else return FileState.None; } /// <summary> /// Returns the state of the passed in file /// </summary> public static FileState GetFileState(FileSpec aFile, PerforceConnection aConnection) { FileMetaData meta; IList<FileMetaData> dataList = GetFileMetaData(aConnection, null, aFile); if (dataList != null && dataList.Count > 0) { meta = dataList[0]; } else { meta = new FileMetaData(); } return GetOpenAction(meta); } /// <summary> /// Returns the state of the passed in file /// </summary> //public static FileState GetFileState(IList<FileMetaData> aMetaData) //{ // FileState retState = FileState.None; // if (aMetaData != null) // { // foreach (var data in aMetaData) // { // retState = ParseFileAction(data.Action); // } // } // return retState; //} /// <summary> /// Returns the "open action" from the metadata. /// </summary> public static FileState GetOpenAction(FileMetaData aMetaData) { // Debug.Log("GetOpenAction: " + aMetaData.Action.ToString() + " returns: " + ParseFileAction(aMetaData.Action)); return ParseFileAction(aMetaData.Action); } public static FileState GetHeadAction(FileMetaData aMetaData) { //Debug.Log("GetHeadAction: " + aMetaData.HeadAction.ToString() + " returns: " + ParseFileAction(aMetaData.HeadAction)); return ParseFileAction(aMetaData.HeadAction); } /// <summary> /// Returns the state of the passed in file on the server (if checked out by someone else for instance) /// </summary> public static FileState GetServerFileState(string arPath, PerforceConnection aConnection) { if (Utils.IsFilePathValid(arPath)) return GetServerFileState(FileSpec.LocalSpec(Utils.AssetPathToLocalPath(arPath)), aConnection); else return FileState.None; } /// <summary> /// Returns the state of the passed in file on the server (if checked out by someone else for instance) /// </summary> public static FileState GetServerFileState(FileSpec arFile, PerforceConnection aConnection) { FileState retState = FileState.None; IList<FileMetaData> 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; } /// <summary> /// Returns the state of the passed in file on the server (if checked out by someone else for instance) /// </summary> public static RevisionState GetRevisionState(string arPath, PerforceConnection aConnection) { if (Utils.IsFilePathValid(arPath)) return GetRevisionState(FileSpec.LocalSpec(Utils.AssetPathToLocalPath(arPath)), aConnection); else return RevisionState.None; } /// <summary> /// Returns the state of the passed in file on the server (if checked out by someone else for instance) /// </summary> public static RevisionState GetRevisionState(FileSpec aFile, PerforceConnection aConnection) { RevisionState retState = RevisionState.None; IList<FileMetaData> 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; } /// <summary> /// Performs an operation after opening a perforce connection /// </summary> public static void PerformConnectionOperation(System.Action<PerforceConnection> aConnectionOperation, bool async = false) { 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); if (!async) { LogP4Exception(ex); } } finally { connection.Dispose(); } } else { log.Error("Configuration Invalid"); } } /// <summary> /// Performs an operation after opening a perforce connection and getting the opened files /// </summary> //public static void PerformOpenConnectionOperation(System.Action<OpenedConnection> aConnectionOperation) //{ // PerformConnectionOperation(con => aConnectionOperation(new OpenedConnection(con))); //} /// <summary> /// Submits the files to the server /// </summary> public static void SubmitFiles(PerforceConnection aConnection, string aChangeListDescription, List<FileSpec> 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<FileMetaData> allFilesMeta = new List<FileMetaData>(); Utils.GetMatchingMetaData(aFiles, allFilesMetaRaw, allFilesMeta); List<FileSpec> lockedFiles = new List<FileSpec>(); 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<FileSpec> submittedFiles = new List<FileSpec>(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<FileAndMeta> allFilesAndMetas = new List<FileAndMeta>(); 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"); } } /// <summary> /// Attempts to perform the operation immediately /// </summary> static List<FileAndMeta> 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<FileAndMeta> 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()) { //List<FilesAndOp> filesAndOps = new List<FilesAndOp>(); //AddToFilesAndOpList(connection, arPaths, arMoveToPaths, aDesiredOp, filesAndOps); List<FilesAndOp> filesAndOps = AddToFilesAndOpList(connection, arPaths, arMoveToPaths, aDesiredOp); FileListOperations operations = new FileListOperations(); AddToOperations(filesAndOps, operations); List<FileSpec> fspecs = operations.Run(connection); // var not_needed = fspecs.JustLocalSpecs(); // var needed = fspecs.NonLocalSpecs().UnversionedSpecs(); // var local_fix = needed.FixLocalPaths(connection); // var res = not_needed.Union(local_fix); var res = fspecs.JustLocalSpecs().Union(fspecs.NonLocalSpecs().UnversionedSpecs().FixLocalPaths(connection)); result = res.ToFileAndMetas().ToList(); #if DEBUG //log.Debug("not_needed: " + Logger.FileSpecListToString(not_needed.ToList())); // log.Debug("needed: " + Logger.FileSpecListToString(needed.ToList())); // log.Debug("fixed: " + Logger.FileSpecListToString(local_fix.ToList())); //log.Debug("all: " + Logger.FileSpecListToString(res.ToList())); //log.Debug("allfam: " + Logger.FileAndMetaListToString(result)); #endif // Trigger events if (OnOperationPerformed != null) { OnOperationPerformed(connection, result); } } } catch (Exception ex) { EditorUtility.ClearProgressBar(); log.Error("... Exception ", ex); LogP4Exception(ex); } } // Any result returned from an operation may have it's status changed // so we should re-queue it AssetStatusCache.MarkAsDirty(result); #if DEBUG log.DebugFormat("results: {0}", Logger.FileAndMetaListToString(result)); #endif return result; } /// <summary> /// Given a list of asset file paths and operation, fills a list of perforce files and operation for each /// </summary> /// <param name="arConnection">The perforce connection to use to query current state of files on the depot</param> /// <param name="arAssetPaths">The list of files (these can be a mix and match of .meta files and regular files)</param> /// <param name="arMoveToAssetPaths">Where moved files are going</param> /// <param name="aDesiredOperation">The operation to perform</param> /// <param name="aInOutFilesAndOp">INOUT: The list of perforce files to fill out</param> static void AddToFilesAndOpList(PerforceConnection aConnection, string[] arAssetPaths, string[] arMoveToAssetPaths, AssetOperation aDesiredOperation, List<FilesAndOp> aInOutFilesAndOp) { // Build the list of files and metas List<FileSpec> fileSpecs = new List<FileSpec>(); // there may be null values in here List<FileSpec> metaSpecs = new List<FileSpec>(); // there may be null values in here List<FileSpec> moveToFileSpecs = new List<FileSpec>(); // there may be null values in here List<FileSpec> moveToMetaSpecs = new List<FileSpec>(); // there may be null values in here List<bool> areFolders = new List<bool>(); #if DEBUG log.DebugFormat("op: {0} paths: {1} to: {2} inout: {3}", aDesiredOperation.ToString(), Logger.StringArrayToString(arAssetPaths), Logger.StringArrayToString(arMoveToAssetPaths), Logger.FilesAndOpListToString(aInOutFilesAndOp)); #endif log.Debug("arAssetPaths: " + Logger.StringArrayToString(arAssetPaths)); //log.Debug("arAssetPathsNull:" + Logger.StringArrayToString(arAssetPaths.NonNullElements().ToArray())); // Set up the arrays needed to generate the operations for (int i = 0; i < arAssetPaths.Length; ++i) { string FileName = ""; string MetaName = ""; // Gets local paths for Asset and Meta Utils.GetFileAndMeta(arAssetPaths[i], out FileName, out MetaName); string MoveToFileName = ""; string MoveToMetaName = ""; if (arMoveToAssetPaths != null) { // Gets local paths for both Asset and Meta 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); bool isMeta = Utils.IsMetaFile(arAssetPaths[i]); // 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(escapedFileName)); if (MoveToFileName != "") moveToFileSpecs.Add(FileSpec.LocalSpec(System.IO.Path.Combine(escapedMoveToFileName, "..."))); else moveToFileSpecs.Add(null); // All Folders should have metas escapedMetaName = Utils.RemoveDirectoryWildcards(escapedMetaName); // Just the folder name, thank you. metaSpecs.Add(FileSpec.LocalSpec(escapedMetaName)); if (escapedMoveToMetaName != "") moveToMetaSpecs.Add(FileSpec.LocalSpec(escapedMoveToMetaName)); else moveToMetaSpecs.Add(null); } else if (!isMeta) // Asset File { fileSpecs.Add(FileSpec.LocalSpec(escapedFileName)); if (escapedMoveToFileName != "") moveToFileSpecs.Add(FileSpec.LocalSpec(escapedMoveToFileName)); else moveToFileSpecs.Add(null); if (ShouldFileHaveMetaFile(FileName)) // this includes folders { metaSpecs.Add(FileSpec.LocalSpec(escapedMetaName)); if (escapedMoveToMetaName != "") moveToMetaSpecs.Add(FileSpec.LocalSpec(escapedMoveToMetaName)); else moveToMetaSpecs.Add(null); } else { metaSpecs.Add(null); moveToMetaSpecs.Add(null); } } else // Must be Meta file { metaSpecs.Add(FileSpec.LocalSpec(escapedMetaName)); if (escapedMoveToMetaName != "") moveToMetaSpecs.Add(FileSpec.LocalSpec(escapedMoveToMetaName)); else moveToMetaSpecs.Add(null); // Since the non-meta file was not found, add it anyway fileSpecs.Add(FileSpec.LocalSpec(escapedFileName)); if (escapedMoveToFileName != "") moveToFileSpecs.Add(FileSpec.LocalSpec(escapedMoveToFileName)); else moveToFileSpecs.Add(null); } } if (arMoveToAssetPaths != null) { log.Debug("moveto: " + Logger.StringArrayToString(arMoveToAssetPaths.NonNullElements().ToArray())); } // log.Debug("allFiles: " + Logger.StringArrayToString(allFiles.ToArray())); //Get a Hashset of all the files we need to query HashSet<string> statusFiles = new HashSet<string>(fileSpecs.NonNullElements().ToLocalPaths()); statusFiles.UnionWith(moveToFileSpecs.NonNullElements().ToLocalPaths()); statusFiles.UnionWith(metaSpecs.NonNullElements().ToLocalPaths()); statusFiles.UnionWith(moveToMetaSpecs.NonNullElements().ToLocalPaths()); // This routine will check for valid asset statuses (and associated FileMetaData) for each file in the parameters. // If needed, the server will be queried var stats = AssetStatusCache.GetAssetStatusesFromPaths(aConnection, statusFiles.ToList()); #if DEBUG log.Debug("assets: " + Logger.FileSpecListToString(fileSpecs)); log.Debug("metas: " + Logger.FileSpecListToString(metaSpecs)); #endif // 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 files: {0} and {1}", fileSpecs[i].ToStringNullSafe(), metaSpecs[i].ToStringNullSafe()); #endif FileMetaData filemd = AssetStatusCache.GetMetaData(fileSpecs[i]); //log.Debug("filemd: " + Logger.FileMetaDataToString(filemd)); fileAndOp.FileOp = GetFileOperation(fileSpecs[i].ToAssetPath(aConnection), aDesiredOperation, filemd); FileOperation metaOp = FileOperation.None; // Some files don't have associated metas if (metaSpecs[i] != null) { FileMetaData metamd = AssetStatusCache.GetMetaData(metaSpecs[i]); //log.Debug("metamd: " + Logger.FileMetaDataToString(metamd)); metaOp = GetFileOperation(metaSpecs[i].ToAssetPath(aConnection), aDesiredOperation, metamd); } if (metaOp == FileOperation.None) // No Meta { fileAndOp.Meta = null; fileAndOp.MoveToMeta = null; } else 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]; aInOutFilesAndOp.Add(metaAndOp); } // Add the operation if (!areFolders[i] || fileAndOp.FileOp != FileOperation.Add) { aInOutFilesAndOp.Add(fileAndOp); } } #if DEBUG int n = 1; foreach (var fao in aInOutFilesAndOp) { log.DebugFormat("fao {0}: {1}", n.ToString(), Logger.FileAndOpToString(fao)); n++; } #endif } static public List<FilesAndOp> AddToFilesAndOpList(PerforceConnection aConnection, string[] arAssetPaths, string[] arMoveToAssetPaths, AssetOperation aDesiredOperation) { #if DEBUG log.DebugFormat("op: {0} paths: {1} to: {2}", aDesiredOperation.ToString(), Logger.StringArrayToString(arAssetPaths), Logger.StringArrayToString(arMoveToAssetPaths)); #endif Collector collector = new Collector(aDesiredOperation); for(int i = 0; i < arAssetPaths.Length; i++) { string path = arAssetPaths[i]; string move = arMoveToAssetPaths == null ? null : arMoveToAssetPaths[i]; collector.Add(path, move); } return collector.GetFilesAndOps(aConnection); } public class Collector { private static readonly ILog log = LogManager.GetLogger(typeof(Collector)); AssetOperation operation; public class FAOEntry { public string file; public string meta; public string mfile; public string mmeta; public bool is_folder; public FAOEntry() { file = meta = mfile = mmeta = null; is_folder = false; } /// <summary> /// Return a collection of all the non-null paths /// </summary> /// <returns></returns> public IEnumerable<string> paths() { if (! is_folder && this.file != null) yield return this.file; if (this.meta != null) yield return this.meta; if (! is_folder && this.mfile != null) yield return this.mfile; if (this.mmeta != null) yield return this.mmeta; yield break; } } public Dictionary<string, FAOEntry> entryDict; public Collector(AssetOperation op) { operation = op; entryDict = new Dictionary<string, FAOEntry>(); } /// <summary> /// Add an asset with it's moveto location /// </summary> /// <param name="assetPath"></param> /// <param name="moveToAssetPath"></param> public void Add(string assetPath, string moveToAssetPath) { log.Debug("add: file: " + assetPath.ToStringNullSafe() + " mfile: " + moveToAssetPath.ToStringNullSafe()); FAOEntry fao = new FAOEntry(); string FileName = ""; string MetaName = ""; Utils.GetFileAndMeta(assetPath, out FileName, out MetaName); if (Utils.IsDirectory(assetPath)) { assetPath = Utils.AddDirectoryWildcards(assetPath); fao.is_folder = true; fao.file = assetPath; fao.mfile = moveToAssetPath == null ? null : Utils.AddDirectoryWildcards(moveToAssetPath); } else if (Utils.IsMetaFile(assetPath)) { fao.meta = assetPath; fao.mmeta = moveToAssetPath; } else { fao.file = assetPath; fao.mfile = moveToAssetPath; } // Look it up FAOEntry entry; if (entryDict.TryGetValue(FileName, out entry)) { // Update the entry in the dictionary if (entry.file == null && fao.file != null) { entry.file = fao.file; } if (entry.mfile == null && fao.mfile != null) { entry.mfile = fao.mfile; } if (entry.meta == null && fao.meta != null) { entry.meta = fao.meta; } if (entry.mmeta == null && fao.mmeta != null) { entry.mmeta = fao.mmeta; } } else // Create a new entry in the dictionary { entryDict[FileName] = fao; } } /// <summary> /// Return the list of FilesAndOp to be executed by the engine /// </summary> /// <returns></returns> public List<FilesAndOp> GetFilesAndOps(PerforceConnection aConnection) { List<FilesAndOp> results = new List<FilesAndOp>(); HashSet<string> assets = new HashSet<string>(); // Check to see if any metas are missing foreach(FAOEntry entry in entryDict.Values) { // add a meta file if needed if (entry.file != null && entry.meta == null && (entry.is_folder || ShouldFileHaveMetaFile(entry.file))) { entry.meta = Utils.GetMetaFile(Utils.RemoveDirectoryWildcards(entry.file)); } // add a move to meta file if needed if (entry.mfile != null && entry.mmeta == null && (entry.is_folder || ShouldFileHaveMetaFile(entry.mfile))) { entry.mmeta = Utils.GetMetaFile(Utils.RemoveDirectoryWildcards(entry.mfile)); } // Collect all paths into the "assets" collection assets.UnionWith(entry.paths()); } List<string> tostat = assets.StripDirectories().ToList(); // This routine will check for valid asset statuses (and associated FileMetaData) for each file in the parameters. // If needed, the server will be queried var stats = AssetStatusCache.GetAssetStatusesFromPaths(aConnection, tostat); #if DEBUG //log.Debug("stats: " + Logger.AssetStatusListToString(stats)); #endif // Compute the operation for each entry foreach (FAOEntry entry in entryDict.Values) { FileOperation op = FileOperation.None; FileOperation metaop = FileOperation.None; if (entry.file != null) { FileMetaData filemd = AssetStatusCache.GetMetaData(entry.file); //log.Debug("filemd: " + Logger.FileMetaDataToString(filemd)); op = GetFileOperation(entry.file, operation, filemd); } if (entry.meta != null) { FileMetaData metamd = AssetStatusCache.GetMetaData(entry.meta); //log.Debug("metamd: " + Logger.FileMetaDataToString(metamd)); metaop = GetFileOperation(entry.meta, operation, metamd); } if (op == metaop) { // One record sharing an op results.Add(new FilesAndOp(entry.file, entry.meta, entry.mfile, entry.mmeta, op)); } else { // file and meta file have different ops, so they need different entries if (entry.file != null) { results.Add(new FilesAndOp(entry.file, null, entry.mfile, null, op)); } if (entry.meta != null) { results.Add(new FilesAndOp(null, entry.meta, null , entry.mmeta, metaop)); } } } results = results.NoNoOps().ToList(); #if DEBUG int n = 1; foreach (var fao in results) { log.DebugFormat("fao {0}: {1}", n.ToString(), Logger.FileAndOpToString(fao)); n++; } #endif return results; } } // Collector Class /// <summary> /// Adds the list of perforce files (and associated .meta if applicable) to the list of files/operations /// </summary> static void AddToOperations(List<FilesAndOp> aFileAndMetas, FileListOperations aInOutFileOperations) { foreach (FilesAndOp fileAndMeta in aFileAndMetas) { aInOutFileOperations.Add(fileAndMeta); } } /// <summary> /// Parses a metadata file action and translates it into a state for P4connect /// </summary> public static FileState ParseFileAction(FileAction aAction) { FileState outOp = FileState.None; switch (aAction) { case FileAction.None: outOp = FileState.None; break; case FileAction.Add: outOp = FileState.MarkedForAdd; break; case FileAction.Delete: 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 outOp = FileState.InDepot; break; } return outOp; } /// <summary> /// Gets the default file operation, based on the asset operation /// </summary> 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; } /// <summary> /// 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 /// </summary> /// <param name="aFile">FileSpec for file</param> /// <param name="aDesiredOperation">Desired Perforce operation</param> /// <param name="fmd">File MetaData from fstat command</param> /// <returns>File Operation to use</returns> static FileOperation GetFileOperation(string aFile, AssetOperation aDesiredOperation, FileMetaData fmd) { FileState aOpenAction = FileState.IsDirectory; FileState aHeadAction = FileState.None; LockState aCurrentLockState = LockState.None; if (! Utils.IsDirectory(aFile)) { aOpenAction = GetOpenAction(fmd); aHeadAction = GetHeadAction(fmd); aCurrentLockState = GetLockState(fmd); } FileOperation outOp = FileOperation.None; #if DEBUG // log.DebugFormat("desired: {0} file: {1} state: {2} lock: {3} ", // aDesiredOperation.ToString(), // aFile == null ? "null" : aFile.ToString(), // aCurrentAction.ToString(), aCurrentLockState); #endif // Mark Files that are in the depot if (aOpenAction == FileState.None && (fmd.IsMapped || fmd.HeadAction != FileAction.None)) aOpenAction = FileState.InDepot; // Mark Files that are deleted in their latest revision if (aOpenAction == FileState.InDepot && (fmd.HeadAction == FileAction.Delete || fmd.HeadAction == FileAction.MoveDelete)) aOpenAction = FileState.InDepotDeleted; switch (aOpenAction) { 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, 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 sync 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, Just Check it out outOp = FileOperation.Checkout; 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.InDepotDeleted: 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: 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.RevertIfUnchanged; 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; case FileState.IsDirectory: 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 = GetLockOperation(aFile, aCurrentLockState); break; case AssetOperation.Unlock: outOp = GetUnlockOperation(aFile, aCurrentLockState); break; } break; } #if DEBUG log.DebugFormat("OP: {0} <= desired: {1} file: {2} state: {3} lock: {4} ", outOp.ToString(), aDesiredOperation.ToString(), aFile.ToString(), aOpenAction.ToString(), aCurrentLockState); // log.DebugFormat("returns: {0}", outOp.ToString()); #endif return outOp; } static FileOperation GetLockOperation(string 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(string 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; } } /// <summary> /// Logs a P4 exception and display an accompanying message /// </summary> 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(); } /// <summary> /// Helper method that filters files that shouldn't have a .meta file associated with them /// </summary> /// <param name="arPath"></param> /// <returns></returns> 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; } /// <summary> /// Utility method that adds all non null files and meta files from a list of pairs /// </summary> static List<FileSpec> NonNullFilesAndMeta(IEnumerable<FileAndMeta> aFilesAndMeta) { List<FileSpec> retList = new List<FileSpec>(); 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 } /// <summary> /// Checks for wildcards and asks the user whether they want to force the operation /// </summary> public static WildcardCheck VerifyWildcards(IEnumerable<FileAndMeta> aFiles) { var allFiles = NonNullFilesAndMeta(aFiles); List<string> filesWithWildCards = new List<string>(); 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; } } /// <summary> /// Performs a Get-Latest operation, and then checks to see if there are folders that need to be deleted or conflicts /// </summary> static IList<FileSpec> PerformGetLastestAndPostChecks(PerforceConnection aConnection, List<FileSpec> aOriginals, Options aOptions) { var result = aConnection.P4Client.SyncFiles(aOriginals, aOptions); if (result != null) { Utils.LogFiles(result, "Syncing {0}"); List<FileSpec> allFolderDepotMetaFiles = new List<FileSpec>(); 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<FileMetaData> 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<FileSpec> originalsAgain = new List<FileSpec>(); foreach (FileSpec spec in aOriginals) { originalsAgain.Add(FileSpec.LocalSpec(spec.LocalPath.Path)); } // Get the local meta data IList<FileMetaData> allLocalMeta = GetFileMetaData(aConnection, originalsAgain, null); if (allLocalMeta != null) { List<FileMetaData> unresolvedFiles = new List<FileMetaData>(); 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<FileMetaData> 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); } } /// <summary> /// Query server to get FileMetaData for each element of the FileSpec list /// </summary> /// <param name="aConnection">Connection to Perforce server</param> /// <param name="aSpecs">IList of FileSpec to match</param> /// <param name="aOptions">p4 command options</param> /// <returns>null or IList of FileMetaData matching the FileSpecs</returns> public static IList<FileMetaData> GetFileMetaData(PerforceConnection aConnection, IList<FileSpec> aSpecs, Options aOptions) { IList<FileMetaData> ret = new List<FileMetaData>(); if (aSpecs != null && aSpecs.Count > 0) { // Debug.Log("GetFileMetaData: " + Logger.FileSpecListToString(aSpecs)); if (Config.DisplayP4Timings) { DateTime startTimestamp = DateTime.Now; ret = aConnection.P4Depot.GetFileMetaData(aSpecs, 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(aSpecs, aOptions); //log.Debug("metadata: " + Logger.FileMetaDataListToString(ret)); } } //Debug.Log("GetFileMetaData Returns: " + Logger.FileMetaDataListToString(ret)); // since we have fresh metadata from the server, we should also update the status cache if (ret != null) AssetStatusCache.StorePerforceMetaData(ret); return ret; } } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 18970 | rtande |
Merging //guest/perforce_software/p4connect/main/src/... to //rtande/p4connect/main/src/... |
||
//guest/perforce_software/p4connect/main/src/P4Connect/P4Connect/P4Connect.Engine.cs | |||||
#3 | 16622 | Norman Morse |
Fixed Exception in GetFileState(). Updated Release Notes for 2015.3 patch1 |
||
#2 | 16489 | Norman Morse |
Another pass at async fstats. Removed a UI update on exception |
||
#1 | 16209 | Norman Morse | Move entire source tree into "main" branch so workshop code will act correctly. | ||
//guest/perforce_software/p4connect/src/P4Connect/P4Connect/P4Connect.Engine.cs | |||||
#18 | 15383 | Norman Morse |
Improved Diagnostics, cleaned up unnecessary log output Moved some Dialog Initialization to OnEnable() Fixed Unity 5.1.1 incompatibilities Added Operation support for In Depot Deleted files |
||
#17 | 15266 | Norman Morse |
Integrated "UpdateVersion" tool to update the VersionInfo and the DLL properties with information from "Version" EC generates the Version file for us in builds. Workshop users need to generate their own Release number with two zeros (like 2015.2.0.0) which will have the last two numbers replaced with change ID. |
||
#16 | 15244 | Norman Morse |
Better Directory support in "add" "get latest" "refresh" and other commands. Improved Project Root detection Various Bug Fixes and Clean up |
||
#15 | 15146 | Norman Morse |
Rewrote Config Dialog to resize well and work both vertically and horizontally. Fixed some internal issues in file handling. Removed .bytes from default type of "text" |
||
#14 | 15079 | Norman Morse |
Rewrote AssetStatusCache to Cache AssetStatuses and FileMetaData Fixed Edge conditions on Engine Operations Change Debug output defaults. Will now Checkout files which request to be "added" but which already exist in perforce. Output P4Connect version to log on initialization. |
||
#13 | 14801 | Norman Morse |
GA.9 changes. Fixed debug message exceptions Improved Pending Changes dialog for large changesets Changed configuration to allow saving configuration with Perforce disabled. Improved restart after recompile, automatically attempts connection now unless disabled. |
||
#12 | 14193 | Norman Morse |
GA.7 release Refactor Pending Changes Resolve Submit issues. Fixed Menu entries. Handle mismatched file and meta states. |
||
#11 | 13883 | Norman Morse | Remove some debugging noise from Unity log | ||
#10 | 13864 | Norman Morse | Final fixes for GA.5 release. | ||
#9 | 13824 | Norman Morse |
Changes to have fstat return true client paths. Remove versions, fixes problems with "local" paths sneaking into results. |
||
#8 | 13692 | Norman Morse |
Fix FileSpec corruption issue Make sure that the logged in user is verified against files in the default change. Multiple users can share the same workspace and cause lots of confusion. Also changed the code in Perforce.P4.Changelist.ToString() to create new FileSpecs instead of doing implicite casts. This should fix the exception if the source FileMetaData is incomplete some how. |
||
#7 | 13595 | Norman Morse | Added fix for crash in GetLockStatus() in addition to GA.3 features | ||
#6 | 12862 | Norman Morse |
Fixed problem with an empty default change list not refresshing. Fixed crash in is_ignored Removed a lot of log output |
||
#5 | 12553 | Norman Morse |
integrate from internal main Build fixes for EC. Major changes to Configuration and re-initialization code. Bug fixes |
||
#4 | 12512 | Norman Morse | Integrate from Dev branch, preparing for Beta3 release | ||
#3 | 12362 | Norman Morse |
Added Debug Logging for p4log Fixed some path comparison issues. Created a CaseSensitivity test |
||
#2 | 12135 | Norman Morse |
Integrate dev branch changes into main. This code is the basiis of the 2.7 BETA release which provides Unity 5 compatibility |
||
#1 | 10940 | Norman Morse |
Inital Workshop release of P4Connect. Released under BSD-2 license |