#!/usr/bin/python # # p4.py: a perforce API for Python # # Copyright (C) 2003-2008 Servaas Goossens (sgoossens@ortec.nl) # # ---- # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License Version 3 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # ---- # # This Perforce API is based on the p4 command line client # and its -G option. # import os, platform, sys, string, marshal, re if not ('False' in __builtins__): # False and True are new in python v2.3 False = 0 True = not False p4 = 'p4' if platform.system() == 'Windows': # for the win32 API function CreateProcess, the command line length may not exceed 32k characters. # For CMD.exe, the limit is 8k. Unfortunately, the limit for os.popen3 is also 8k (does it use CMD.exe?). max_cmdlinelength = 8 * 1024 else: # Don't know if there's a limit on Linux/Unix. # For Cygwin on Windows, it's still 32k max_cmdlinelength = 32 * 1024 def execute(command, ignoreError = False): '''Executes the given p4 command (assumed to have the -G option) and returns the marshaled output of that command. Raises an exception if the output of the command indicates an error, unless ignoreError is true.''' if len(command) >= max_cmdlinelength: raise Exception("Maximum command line lenght exceeded %d (max = %d)" % (len(command), max_cmdlinelength)) (child_stdin, child_stdout, child_stderr) = os.popen3(command, 'b') child_stdin.close() try: try: output = marshal.load(child_stdout) except: # try to obtain any output from stderr, if available try: stderrtext = child_stderr.read() except: stderrtext = '(failed to read stderr from p4 command)' errormsg = 'Exception ' + repr(sys.exc_info()[0]) + ' was raised during marshalling of perforce output.\n%s\n%s\n.' % \ (str(sys.exc_info()[1]), stderrtext) output = {'code':'error', 'data': errormsg } finally: child_stdout.close() child_stderr.close() if not ignoreError and (output['code'] == 'error'): raise Exception('P4 error while executing command %s:\n%s' % \ (command, output['data'])) return output def executeM(command, ignoreError = False): '''Executes the given p4 command (assumed to have the -G option) and returns the marshaled output of that command. Raises an exception if the output of the command indicates an error, unless ignoreError is true. --> The executeM variant returns a list of dictionaries.''' if len(command) >= max_cmdlinelength: raise Exception("Maximum command line lenght exceeded %d (max = %d)" % (len(command), max_cmdlinelength)) output = [] (child_stdin, child_stdout, child_stderr) = os.popen3(command, 'b') child_stdin.close() try: while 1: try: output.append(marshal.load(child_stdout)) except EOFError: break # this indicates end of output. except: # try to obtain any output from stderr, if available try: stderrtext = child_stderr.read() except: stderrtext = '(failed to read stderr from p4 command)' errormsg = 'Exception ' + repr(sys.exc_info()[0]) + ' was raised during marshalling of perforce output.\n%s\n%s\n.' % \ (str(sys.exc_info()[1]), stderrtext) output = {'code':'error', 'data': errormsg } if not ignoreError and (output[-1]['code'] == 'error'): raise Exception('P4 error while executing command %s:\n%s' % \ (command, output[-1]['data'])) finally: child_stdout.close() child_stderr.close() return output def execute2(input, command): '''Executes the given p4 command (assumed to have the -G option) provides the marshaled input object to the stdin of the command ane returns the marshaled output.''' if len(command) >= max_cmdlinelength: raise Exception("Maximum command line lenght exceeded %d (max = %d)" % (len(command), max_cmdlinelength)) stdi, stdo = os.popen4(command, 'b') try: if ('version' in marshal.__dict__): # python 2.4 introduces a new marshal format. # Perforce only accepts the older format (tested perforce version 2005.2) marshal.dump(input, stdi, 0) else: marshal.dump(input, stdi) stdi.close() try: output = marshal.load(stdo) except EOFError: raise Exception, 'P4 error while executing the command %s:\n%s' % \ (command, 'EOFError') finally: stdo.close() if output['code'] == 'error': raise Exception('P4 error while executing the command %s:\n%s' % \ (command, output['data'])) return output def describe(changenr): '''Get the change description from perforce. Returns a dictionary. ''' # use -s option to minimize output in case of a submitted change command = p4 + ' -G describe -s ' + str(changenr) return execute(command) def describeM(changenrs): '''Get the change descriptions from perforce, for a list of change nrs. Returns a list of dictionaries.''' if changenrs: command = p4 + ' -G describe -s ' + ' '.join(map(str, changenrs)) return executeM(command) else: return [] def job(name): '''Get the job description from perforce. Returns a dictionary. ''' command = p4 + ' -G job -o ' + name return execute(command) def getcounter(counter): '''Get the value of the specified perforce counter ''' command = p4 + ' -G counter ' + counter result = execute(command) if not ('value' in result) or not result['value'].isdigit(): raise Exception('Failed to retrieve the counter value from the output of the command %s:\n%s' % \ (command, str(result))) return int(result['value']) def setcounter(counter, value): '''Sets the value of the specified perforce counter Note: perforce internal counters cannot be set using this function. (you need the -f option for that, which should NOT be added below for safety reasons!!)''' command = p4 + ' -G counter ' + counter + ' ' + str(int(value)) # make sure value is an int execute(command) def fstat(filename): '''Executes p4 -G fstat for the given filename and returns the output (a dictionary).''' command = p4 + ' -G fstat "' + filename + '"' return execute(command) def sync(filename, rev = 'head'): '''Calls p4 sync for the given filespec and revision range.''' command = p4 + ' -G sync "%s#%s"' % (filename, str(rev)) execute(command) def add(filename, changelist='', filetype=''): '''Adds the given file to the given changelist (if provided) and as the given filetype (if provided).''' parameters = [] if changelist: parameters.extend(('-c', str(changelist))) if filetype: parameters.extend(('-t', filetype)) command = p4 + ' -G add %s "%s"' % (' '.join(parameters), filename) execute(command) def edit(filename, changelist='', filetype=''): '''Opens the given file for edit in the given changelist (if provided) and as the given filetype (if provided).''' parameters = [] if changelist: parameters.extend(('-c', str(changelist))) if filetype: parameters.extend(('-t', filetype)) command = p4 + ' -G edit %s "%s"' % (' '.join(parameters), filename) execute(command) def delete(filename, changelist=''): '''Opens the given file for delete in the given changelist (if provided).''' parameters = [] if changelist: parameters.extend(('-c', str(changelist))) command = p4 + ' -G delete %s "%s"' % (' '.join(parameters), filename) execute(command) def revert(filename): '''Reverts the given (opened) file so that it is no longer opened.''' command = p4 + ' -G revert "%s"' % filename execute(command) def read(filename): '''Returns the contents of the given file from the the depot.''' command = p4 + ' -G print "%s"' % filename l = executeM(command) # need at least two dictionaries. First is informational, the others contain the data. if len(l) < 2: raise Exception('P4 error while interpreting the result of the command %s\n: Expecting more than one dictionary.' % command) # check that all dictionaries have a data part. (skip the first item) datadicts = filter(lambda x: 'data' in x, l[1:]) if len(l) != len(datadicts) + 1: raise Exception('P4 error while interpreting the result of the command %s\n: Expecting a "data" key in all dictionaries except the first.' % command) # get all data entries in a list datalist = map(lambda x: x['data'], datadicts) return ''.join(datalist) def readM(filenames): '''Returns the contents of the given files from the the depot Wildcards are not supported. The return value is a dictionary keyed on the (given) filename. Values are dictionaries containing the metadata provided by Perforce, augmented with the key "content" containing the file contents as a string. This key is not added if perforce produced an error for that file (code="error").''' if not filenames: return {} # create local copy of the list (and strip any whitespace) # but keep the original: use that as keys in the result. _filenames = map(string.strip, filenames) cmd = p4 + ' -G print ' p4output = [] while _filenames: # construct a cmdline that is not too long _f = [] l = len(cmd) + len(_filenames[0]) assert(l < max_cmdlinelength) # otherwise we have an endless loop while _filenames and (l < max_cmdlinelength): _f.append(_filenames.pop(0)) if _filenames: l += len(_filenames[0]) + 1 command = cmd + ' '.join(_f) _p4out = executeM(command, True) if len(_p4out) < len(_f): if (len(_p4out) == 1) and (_p4out[0]['code'] == 'error'): raise Exception('P4 error while executing the command %s:\n%s' % \ (command[:70], _p4out[0]['data'])) else: raise Exception('P4 error while interpreting the result of the command\n %s\n: Expecting at least one dictionary per requested filename. Got %d requested %d.' % (command[:70], len(_p4out), len(filenames))) p4output.extend(_p4out) # p4output is a list with both metadata dicts and data dicts # (the latter belonging to the previous metadata in the list) # split the list into two lists: # - one with just metadatadicts (one per file) # - one with lists of datadicts (one list per file) metadatadicts = [ p4output.pop(0) ] datalists = [ [] ] while len(p4output): item = p4output.pop(0) if (item['code'] == 'text') and ('data' in item): datalists[-1].append(item) else: metadatadicts.append(item) datalists.append([]) if len(filenames) != len(metadatadicts): raise Exception('P4 error while interpreting the result of the command\n %s\n: Expecting one metadata dict per requested filename (filenames=%d, metadatadicts=%d).' % (command, len(filenames), len(metadatadicts))) # we now have 3 lists: filenames, metadatadicts and datalists. # introduce local function for merging one item from each of these lists: result = {} def _merge(filename, metadata, datalist): if datalist: content = map(lambda x: x['data'], datalist) metadata['content'] = ''.join(content) result[filename] = metadata map(_merge, filenames, metadatadicts, datalists) return result def review_t(counter): '''Returns the result of p4 -G review -t .''' command = p4 + ' -G review -t ' + counter return executeM(command) def fix(jobname, changenr, status): '''Fixes the given job in the given changenr and using the given status.''' command = p4 + ' -G fix -s "%s" -c %s "%s"' % \ (status, str(changenr), str(jobname)) execute(command) def unfix(jobname, changenr): '''Unfixes the given job in the given changenr.''' command = p4 + ' -G fix -d -c %s "%s"' % \ (str(changenr), str(jobname)) execute(command) def newchange(description): '''Calls p4 change to create a new changelist with the given description. Returns the new change nr.''' if not str(description): raise Exception('A non-empty description is required to create a new changelist') command = p4 + ' -G change -i' input = {'Change': 'new', 'Status': 'new', 'Description': str(description)} output = execute2(input, command) myregex = re.compile('Change ([0-9]+) created') matchobj = myregex.match(output['data']) if matchobj: return int(matchobj.group(1)) else: raise Exception( 'Failed to obtain the change number from the command %s.' % command +\ 'The output was:\n%s' % output['data']) def getclient(name): '''Retrieves the client specification with the given name. Returns a dictionary.''' command = p4 + ' -G client -o "%s"' % name return execute(command) def setclient(clientspec): '''Creates/updates the client specification. Clientspec is a dictionary as returned by getclient()''' command = p4 + ' -G client -i' return execute2(clientspec, command) def where(filename): '''Executes the p4 where command and returns a dictionary containing the depotpath (key 'depotFile'), clientpath (key 'clientFile') and localpath (key 'path').''' # older versions of p4 commandline tools return only a 'data' field containing the # three paths, separated by spaces. # newer versions (>= 2004.2) return the three fields clientFile, depotFile and path. command = p4 + ' -G where "%s"' % filename output = execute(command) if 'clientFile' in output: return output elif 'data' in output: # for older p4 versions... # the output data contains the 3 filenames, separated by spaces # splitting on whitespace only works if the filenames contains no spaces pathlist = output['data'].split() if len(pathlist) != 3: # at least one of the paths returned by p4 where contains a space or a tab. # Do something smarter to get the three filenames from the output. # In the reg expr below, the first and second group match the depotpath and # the clientpath. The third group should match the localpath, which can be # unix format (/abc/def/xyz) or windows format (D:\abc\def\xyz). myregex = re.compile(r'^(//.+) (//.+) (([a-zA-Z]:)?(/|\\).+)$') found = myregex.search(output['data']) if found: pathlist = found.groups() else: # explanation of the verb 'to grok': http://en.wiktionary.org/wiki/Grok raise Exception('Unable to grok the output of p4where() with a regex. ' + \ 'The output was:\n%s' % output['data']) return { 'depotFile': pathlist[0], 'clientFile': pathlist[1], 'path': pathlist[2] } else: raise Exception('The output of the command "p4 -G where" contains no "clientFile" key and no "data" key. Cannot understand.') def info(): '''Returns the output of p4 -G info''' output = execute(p4 + ' -G info') return output def user(username): '''Returns the output of p4 -G user -o username''' output = execute(p4 + ' -G user -o ' + username) return output