#!/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 <http://www.gnu.org/licenses/>.
# ----
#
# 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 <counter>.'''
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