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, MarkedForEdit, MarkedForAdd, MarkedForDelete, MarkedForAddMove, MarkedForDeleteMove, } /// <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; } } /// <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; } /// <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.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) { IList<FileMetaData> dataList = GetFileMetaData(aConnection, null, aFile); return GetFileState(dataList); } /// <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 state of the passed in file /// </summary> public static FileState GetFileState(FileMetaData aMetaData) { return ParseFileAction(aMetaData.Action); } /// <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) { 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"); } } /// <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) { Changelist repList = null; try { repList = aConnection.P4Depot.CreateChangelist(changeList); } catch (System.Exception ex) { #if DEBUG log.Debug("CreateChangelist Failed", ex); #endif Debug.LogException(ex); Debug.LogWarning("P4Connect - CreateChangelist failed, open P4V and make sure your files are in the default changelist"); 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 Failed", 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()) { arPaths = StripIgnore(arPaths, connection); List<FilesAndOp> filesAndMetas = new List<FilesAndOp>(); 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; } /// <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="aDesiredOperation">The operation to perform on those</param> /// <param name="aAlreadyHappened">Whether the operations have already happened, i.e. the files have already been created or deleted</param> /// <param name="aInOutFilesAndMetas">INOUT: The list of perforce files to fill out</param> static void AddToFileAndMetaList(PerforceConnection aConnection, string[] arAssetPaths, string[] arMoveToAssetPaths, AssetOperation aDesiredOperation, List<FilesAndOp> aInOutFilesAndMetas) { // 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(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); } } // Get the meta data as a chunk and then remap them properly var filesMetaDataRaw = GetFileMetaData(aConnection, fileSpecs, null); var metasMetaDataRaw = GetFileMetaData(aConnection, metaSpecs, null); List<FileMetaData> filesMetaData = new List<FileMetaData>(); List<FileMetaData> metasMetaData = new List<FileMetaData>(); 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 } /// <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.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; } /// <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 is /// </summary> 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; } } /// <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> NonNullFileSpecs(IEnumerable<FileSpec> aFiles) { return new List<FileSpec>(aFiles.Where(f => f != null)); } /// <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, 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); } } public static IList<FileMetaData> GetFileMetaData(PerforceConnection aConnection, IList<FileSpec> aSpecs, Options aOptions) { IList<FileMetaData> ret = null; var nonNullSpecs = NonNullFileSpecs(aSpecs); if (nonNullSpecs.Count > 0) { 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); } } else { ret = new List<FileMetaData>(); } return ret; } } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 12954 | anis_sg |
Populate -o //guest/perforce_software/p4connect/... //guest/anis_sg/perforce_software/p4connect/.... |
||
//guest/perforce_software/p4connect/src/P4Connect/P4Connect/P4Connect.Engine.cs | |||||
#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 |