""" Description: Helper functions for interfacing with Perforce through our .NET RSG.SourceControl.Perforce library. Author: Jason Hayes """ __DoNotReload__ = True # Module is NOT safe to be dynamciy reloaded import re import os import types import datetime import subprocess import marshal import platform import clr import RS.Config clr.AddReference("RSG.SourceControl.Perforce") clr.AddReference("RSG.Base") from RSG.Base import Logging from RSG.SourceControl.Perforce import P4 from RSG.SourceControl.Perforce import FileState from RSG.SourceControl.Perforce import FileAction from RSG.SourceControl.Perforce import Changelist from PySide import QtGui # # Constants # # # Setup the Perforce connection to use the current project root. # Wrapped up around try catch as there is a new constructor for p4 coming try: p4 = P4() except: p4 = P4(Logging.LogFactory.ApplicationLog) p4.set_MaxLockTime(10) p4.CWD = RS.Config.Project.Path.Root try: import RS.Globals # Add callback to ensure the connection is dispose on application shut down RS.Globals.Application.OnFileExit.Add(lambda c, e: p4.Dispose()) except ImportError: pass # # Functions # # def Reconnect(): """ Disconnects and connects the perforce instance. This is to resolve network issues where the connection is invalid. This can be called after a long running process to ensure the connection did not time out. """ global p4 p4.Disconnect() p4.Connect() def User(): """ Gets the name of the user using perforce Return: string """ global p4 return p4.User def Client(): """ Gets the name of the client using perforce Return: string """ global p4 return p4.Client def Port(): """ Gets the port used by perforce Return: string """ global p4 return p4.Port def Host(): """ Gets the host for perforce Return: string """ global p4 return p4.Host def CreateConfig(directory, client=None, port=None, user=None): """ Creates a .p4config file in the supplied directory. Arguments: directory (string): The directory to create the .p4config file. Keyword Arguments: client (string): The client workspace to set. port (string): The server and port to use. Format would be: 'rsgsanp4p01:1999' user (string): The username to use. """ if not os.path.isdir(directory): os.makedirs(directory) if not port: port = Port() if not client: client = Client() if not user: user = User() try: p4config = open('{0}\\.p4config'.format(directory), 'w') p4config.write('P4CLIENT={0}\n'.format(client)) p4config.write('P4PORT={0}\n'.format(port)) p4config.write('P4USER={0}\n'.format(user)) except IOError, err: print 'Could not create the .p4config file due to the following error!\n', err finally: p4config.close() def SwitchWorkspace(cwd): """ Switches the current p4 connection to a new workspace. Arguments: cwd (string): Path to the .p4config file containing the client, workspace and server information. """ global p4 p4.Disconnect() p4 = P4() p4.CWD = cwd p4.Connect() def _AssertFileState(fileStateObj): """ Asserts if the supplied object isn't of type RSG.SourceControl.Perforce.FileState Arguments: fileStateObj (RSG.SourceControl.Perforce.FileState): The RSG.SourceControl.Perforce.FileState object to check. """ assert isinstance(fileStateObj, FileState), ('Incorrect type: ' 'Must supply a RSG.SourceControl.Perforce.FileState object!') def DoesFileExist(filename): """ Checks to see if the supplied filename exists in Perforce. Arguments: filename (string): The filename to check. Returns: True or False """ return p4.Exists(FixFilePath(filename)) def FixFilePath(filename): """ Removes characters not recognized by P4 with their escapes. Arguments: filename (string): path to file Return: string; path to file with unrecognized characters by perforce removed. """ for character, escape, regex in (("%", "%25", "%25|%23|%40"), ("#", "%23", "%23"), ("@", "%40", "%40")): if not re.search(regex, filename) and character in filename: filename = filename.replace(character, escape) # Not sure if this is needed return str(r"{}".format(filename)) def IsLatest(fileStateObj): """ Checks to see if the supplied RSG.SourceControl.Perforce.FileState object is the latest revision. Arguments: fileStateObj (RSG.SourceControl.Perforce.FileState): The RSG.SourceControl.Perforce.FileState object to check. Returns: True or False """ _AssertFileState(fileStateObj) return fileStateObj.HaveRevision == fileStateObj.HeadRevision def IsOpenForEdit(fileStateObj): """ Checks whether or not the supplied file is opened for edit by the current user. Arguments: fileStateObj (RSG.SourceControl.Perforce.FileState): The RSG.SourceControl.Perforce.FileState object to check. Returns: True or False """ _AssertFileState(fileStateObj) return fileStateObj.OpenAction == FileAction.Edit def CanOpenForEdit(fileStateObj): """ Tests whether or not a file can be opened for edit by the current user. Arguments: fileStateObj (RSG.SourceControl.Perforce.FileState): The RSG.SourceControl.Perforce.FileState object to check. Returns: True or False """ _AssertFileState(fileStateObj) return fileStateObj.Locked # TODO: Create a generic method for calling perforce methods, most of the code making calls to perforce is duplicate # TODO: code and it doesn't need to be def FixFilePaths( filenames ): """ Some file paths contain @, we need to convert it % """ if not isinstance(filenames, (list, tuple)): filenames = [filenames] newFilenames = [] for filename in filenames: filename = FixFilePath(filename) newFilenames.append( filename ) return newFilenames def SwitchPathStyle(path): if os.path.splitdrive(path)[0]: _, path = os.path.splitdrive(path)[0] return "/{}{}".format(["", "depot/"]["gta5" in path.lower()], path.replace("\\", "/")) else: return r"x:{}".format(path.replace("/", "\\")[1:]) def GetFileState(filenames): """ Runs fstat on the supplied filenames. Arguments: filenames (list[string, ect.]): The file names to test. Returns: A list of RSG.SourceControl.Perforce.FileState objects. """ global p4 p4.Connect() if type(filenames) not in (types.TupleType, types.ListType): filenames = [filenames] #Fixing file paths filenames = FixFilePaths( filenames ) fileStates = list(FileState.Create(p4, filenames)) if len(fileStates) == 1: return fileStates[0] return fileStates def GetFileInfo(localFilePath, revision, key): """ Gets information about the file from Perforce based on the revision number and key Arguments: localFilePath (string): local perforce file path to test revision (int): revision you want to check key (string): the type of information you wish to return; acceptable values are time, path, status, desc, client, change, changeType, and user. Returns: key result - could be int/string etc. """ global p4 p4.Connect() result = None filePathState = GetFileState(localFilePath) if filePathState: depotFile = filePathState.DepotFilename pathWithRevision = [str('{0}#{1}').format(str(depotFile), str(revision))] recordSet = p4.Run('changes', pathWithRevision) if recordSet.Records.Length != 0 and recordSet.Records[0].Fields.ContainsKey(key): result = recordSet.Records[0].Fields.get_Item(key) return result def GetAllMarkedDeleteDepotFiles(startingDirectory): """ Gets ALL the files marked for delete within the supplied directory. This includes fi Arguments: startingDirectory (string): directory to inspect lResourceDirectory = "//depot/gta5/art/animation/resources/...fbx" Returns: list[string, etc.]; A list of depot file names. """ global p4 cmd = "opened" args = ["-a", startingDirectory] p4Files = p4.Run(cmd, args) fileList = [record[key] for record in p4Files.Records for key in record.get_Fields().Keys if key == "depotFile" and record['action'] == 'delete'] return fileList def Add(filenames, changelistNum=None, force=False, fileType=None): """ Adds the supplied filenames to Perforce. Arguments: filenames (string): The filenames to add. Keyword Arguments: changelistNum (int): Add files to the specified changelist. force (boolean): Indicates that the file should be force added fileType (string): The file type that we want to add the file as. Should be string that matches one of the following list of fle types; http://www.perforce.com/perforce/r15.1/manuals/cmdref/file.types.html Returns: A single or list of RSG.SourceControl.Perforce.FileState objects that were added. """ global p4 p4.Connect() if type(filenames) not in (types.TupleType, types.ListType): filenames = [filenames] # IMPORTANT: p4 add should NOT escape symbols in the file paths (@, etc). # The P4 Add command accepts characters normally ignored by the other P4 commands. # Replacing the characters with their escapes would add a file to perforce with the escapes in # the filename instead of the intended character. args = [filename for filename in filenames] if fileType: args[0:0] = ['-t', fileType] if force: args[0:0] = ['-f'] if changelistNum: args[0:0] = ['-c', str(changelistNum)] recordSet = p4.Run('add', args) fileStates = list(FileState.Create(p4, recordSet)) if len(fileStates) == 1: return fileStates[0] return fileStates def Edit(filenames, changelistNum=None, fileType=None): """ Opens for edit the supplied filenames. Arguments: filenames (list[string, etc.]): The filenames to open for edit. Keyword Arguments: changelistNum (int): Open for edit the files under this changelist. fileType: The file type that we want to add the file as. Should be string that matches one of the following list of fle types; http://www.perforce.com/perforce/r15.1/manuals/cmdref/file.types.html Returns: A single or list of RSG.SourceControl.Perforce.FileState objects that were opened for edit. """ global p4 p4.Connect() if type(filenames) not in (types.TupleType, types.ListType): filenames = [filenames] filenames = FixFilePaths(filenames) # Get the filenames args = [filename for filename in filenames] # Add file type flag to the front of the args list if fileType: args[0:0] = ['-t', fileType] # Add changelist flag to the front of the args list if changelistNum: args[0:0] = ['-c', str(changelistNum)] recordSet = p4.Run('edit', args) fileStates = list(FileState.Create(p4, recordSet)) if len(fileStates) == 1: return fileStates[0] return fileStates def Sync(filenames, force=False, revision=None): """ Syncs the supplied filename(s). Arguments: filenames (list[string, etc.]): The filename(s) to sync. Keyword Arguments: force (boolean): Whether or not to force the sync. Returns: A single or list of RSG.SourceControl.Perforce.FileState objects that were synced. """ global p4 p4.Connect() if type(filenames) not in (types.TupleType, types.ListType): filenames = [filenames] filenames = FixFilePaths(filenames) args = [] if force: args.append('-f') for filename in filenames: if revision: filename = '{}#{}'.format(filename, revision) args.append(filename) print args recordSet = p4.Run('sync', args) fileStates = list(FileState.Create(p4, recordSet)) if len(fileStates) == 1: return fileStates[0] return fileStates def GetChangelist(changelistNum): """ Query a changelist. Arguments: changelistNum(int): The changelist number to query. Returns: RSG.SourceControl.Perforce.Changelist object. None if not found. """ global p4 p4.Connect() changelist = Changelist.Create(p4, [changelistNum]) if changelist.Length == 1: return changelist[0] return None def CreateChangelist(description): """ Creates a new changelist. Arguments: description (string): The changelist desription. Returns: RSG.SourceControl.Perforce.Changelist object. """ global p4 p4.Connect() changelist = p4.CreatePendingChangelist(description) return GetChangelist(changelist.Number) def GetFilesInChangelist(changelistNum): """ Query all files in a changelist. Arguments: changelistNum (int): The changelist number to query. Returns: List of files found in the changelist. """ changelist = GetChangelist(changelistNum) if changelist: return list(changelist.Files) return None def GetDescription(changelistNumber): """ Gets the description from the given changelist. The results are returned in a normal python dictionary Arguments: changelistNumber (int): changelist number Returns: dictionary """ results = p4.Run("describe", [str(changelistNumber)]) return ConvertRecordSetToDictionary(results) def GetUserData(user): """ Gets information on the user based on the perforce user name. The results are returned in a normal python dictionary Arguments: user (string): perforce username to get data from Return: dictionary """ results = p4.Run("users", [user]) return ConvertRecordSetToDictionary(results) def ConvertRecordSetToDictionary(recordSet): return {key: record[key] for record in recordSet.Records for key in record.Fields.Keys} def DeleteChangelist(changelistNum, deleteShelvedFiles=True): """ Delete a changelist. Arguments: changelistNum(int): The changelist number to delete. Keyword Arguments: deleteShelvedFiles(boolean): Delete shelved files in the changelist. Returns: P4API.P4RecordSet object. """ global p4 p4.Connect() if deleteShelvedFiles: DeleteShelvedFiles(changelistNum) return p4.Run('change', ['-d', str(changelistNum)]) def RevertChangelist(changelistNum, deleteShelvedFiles=False): """ Revert all files in a changelist. Arguments: changelistNum (int): The changelist number to delete. Keyword Arguments: deleteShelvedFiles (boolean): Delete any shelved files in the changelist. Returns: P4API.P4RecordSet object. """ global p4 p4.Connect() if deleteShelvedFiles: DeleteShelvedFiles(changelistNum) return p4.Run('revert', ['-c', str(changelistNum), '//...']) def DeleteShelvedFiles(changelistNum): """ Delete all shelved files in a changelist. Arguments: changelistNum (int): The changelist number to delete. Returns: P4API.P4RecordSet object. """ global p4 p4.Connect() return p4.Run('shelve', ['-c', str(changelistNum), '-d']) def ShelveChangelist(changelistNum): """ Shelve all files in a changelist. Arguments: changelistNum (int): The changelist number. Returns: P4API.P4RecordSet object. """ global p4 p4.Connect() return p4.Run('shelve', ['-c', str(changelistNum), '-f']) def Shelve(filenames, changelistNum=None): """ Shelve a list of files. Arguments: filenames (list[string, etc.]): The filenames to shelve. Keyword Arguments: changelistNum (int): The changelist number. Returns: P4API.P4RecordSet object. """ global p4 p4.Connect() if type(filenames) not in (types.TupleType, types.ListType): filenames = [filenames] args = [filename for filename in filenames] if changelistNum: args[0:0] = ['-c', str(changelistNum), '-f'] return p4.Run('shelve', args) def Revert(filenames): """ Revert a list of files. Arguments: filenames (list[string, etc.]): The filenames to revert. Returns: P4API.P4RecordSet object. """ global p4 p4.Connect() if type(filenames) not in (types.TupleType, types.ListType): filenames = [filenames] filenames = FixFilePaths(filenames) args = [filename for filename in filenames] return p4.Run('revert', args) def Submit(changelistNum): """ Submit a changelist. Arguments: changelistNum (int): The changelist number to submit. Returns: P4API.P4RecordSet object. """ global p4 p4.Connect() return p4.Run('submit', ['-c', str(changelistNum)]) def Run(p4Cmd, args=None): """ Native P4 Run command. Arguments: p4Cmd (string): valid p4 cmd args (list[strings, etc.]): list of paths and/or perforce arguments Returns: P4 Record or error message """ global p4 p4.Connect() records = p4.Run(p4Cmd, False, args) if records.HasErrors(): # This shouldn't be here, but to stop other tools from breaking, this is a lesser of 2 evils. # Would be good to re-writing this as module a class object QtGui.QMessageBox.critical(None, "P4 Error", "P4 the following errors:\n{0}".format(records.ErrorMessage)) return records else: return records def ConvertPerforceSecondsToDate(seconds): """ Converts the seconds returned by perforce to a date and time Arguments: seconds (int): number of seconds grabbed from the perforce server Return: string """ return str(datetime.datetime(1970, 1, 1) + datetime.timedelta(0, int(seconds))) def getHostClients( server ): """ This will return the p4 hosts client name. There is a possibility a machine can have on or more clients so will return a list :return: List of Strings """ # Using the global G option so we get back a python dictionary which is easier to work with cmd = 'p4 -G -p {0} clients -E *{1}*'.format(server, platform.node()) proc = subprocess.Popen( cmd, stdout=subprocess.PIPE , shell=True) # because we may have multiple results we need to append them to a list clients = [] try: while 1: output = marshal.load(proc.stdout) client = output.get('client',None) if client != None: clients.append(client) except EOFError: pass finally: proc.stdout.close() return clients def SwitchToMocapWorkspace(server="rsglicp4s1:1666"): """ Currently the p4 api doesn't allow the parsing of global parameters- before the actual command This is currently the only way for us to gain access to the mocap server via script """ global p4 p4.Connect() # First check to see if we have already created a config file for the mocap server mocapServerConfig = os.path.normpath("X:\\projects\.p4config") if os.path.exists(mocapServerConfig): SwitchWorkspace(mocapServerConfig) else: # If we don't create one # This is a more robust way to get the valid user clients. clients = getHostClients(server) if len(clients) > 0: # Create config file, we will assume to use the first client we find, should be rare to have multiple # clients CreateConfig("X:\\projects", client=clients[len(clients)-1], port= server, user=User()) # Switch to the mocap server SwitchWorkspace(mocapServerConfig) def Connected(): """ Connects to perforce """ global p4 return p4.IsValidConnection(True, True) class SyncOperation(object): """ Perforce p4.exe command line """ def __init__(self, fileList, start= False, force= False): """ Contructor """ self.p4root = RS.Config.Project.Path.Root self.fileList = FixFilePaths(fileList) self.force = force self.proc = None if start: self.Start() def Start(self): """ Sync Operation """ # send a request for a sync to perforce fileListString = "" for i in range(len(self.fileList)): fileListString= fileListString+ ("\"{0}\" ").format(self.fileList[i]) if self.force: commandText = ('p4 sync -f {0} ').format(fileListString) else: commandText = ('p4 sync {0} ').format(fileListString) # Remember the original working directory originaDir = RS.Config.Project.Path.Root # Change it to the current working one so that we can use the p4 command os.chdir( self.p4root ) try: self.proc = subprocess.Popen(commandText, shell=True, stdout=subprocess.PIPE) except: self.proc = subprocess.Popen(commandText, shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) self.proc.stdin.close() self.proc.stderr.close() # Change it back to the original directory os.chdir( originaDir ) def WaitForCompletion(self): """ Wait for process to finish """ self.proc.wait()