# Perforce Defect Tracking Integration Project
# <http://www.ravenbrook.com/project/p4dti/>
#
# P4.PY -- PYTHON INTERFACE TO PERFORCE
#
# Gareth Rees, Ravenbrook Limited, 2000-09-25
#
#
# 1. INTRODUCTION
#
# This module defines the 'p4' class, which provides an interface to
# Perforce.
#
# "p4 help undoc" says:
#
# p4 -G
# Causes all output (and batch input for form commands with -i)
# to be formatted as marshalled Python dictionary objects.
#
# The intended readership of this document is project developers.
#
# This document is not confidential.
#
#
# 1.1. Using the p4 class
#
# To use this class, create an instance, passing appropriate parameters
# if necessary (if parameters are missing, the interface doesn't supply
# values for them, so Perforce will pick up its normal defaults from
# environment variables).
#
# import p4
# p4i = p4.p4(port = 'perforce:1666', user = 'root')
#
# The 'run' method takes a Perforce command and returns a list of
# dictionaries; for example:
#
# >>> for c in p4i.run('changes -m 2'):
# ... print c['change'], c['desc']
# ...
# 10021 Explaining how to use the autom
# 10020 Archiving new mail
#
# To pass information to Perforce, supply a dictionary as the second
# argument, for example:
#
# >>> job = p4i.run('job -o job000001')[0]
# >>> job['Title'] = string.replace(job['Title'], 'p4dti', 'P4DTI')
# >>> p4i.run('job -i', job)
# [{'code': 'info', 'data': 'Job job000001 saved.', 'level': 0}]
#
# Note the [0] at the end of line 1 of the above example: the run()
# method always returns a list, even of 1 element. This point is easy
# to forget.
import catalog
import marshal
import os
import re
import string
import tempfile
import types
error = 'Perforce error'
# 2. THE P4 CLASS
class p4:
client = None
client_executable = None
logger = None
password = None
port = None
user = None
# 2.1. Create an instance
#
# We supply a default value for the client_executable parameter, but
# for no others; Perforce will use its own default values if these
# are not supplied. If logger is None then no messages will be
# logged.
#
# We check that the Perforce client named by the client_executable
# parameter is recent enough that it supports the -G option. It's
# important to check this now because the error that we would get if
# we just tried using the -G flag is "ValueError: bad marshal data"
# (the marshal module is failing to read Perforce's error message
# "Invalid option: -G.").
#
# We check that the Perforce client version supports "p4 -G" by
# running "p4 -V" which produces some lines of text output, one of
# which looks like "Rev. P4/NTX86/2000.2/19520 (2000/12/18)." The
# changelevel in this case is 19520. If no line looks like this,
# then raise an error anyway. (This makes the module fragile if
# Perforce change the format of the output of "p4 -V".)
#
# We check that the Perforce server named by the port parameter is
# recent enough that it supports p4 -G jobspec -i. We check the
# server changelevel by running the "info" command and looking
# through the output for a line like "Server version:
# P4D/FREEBSD/2000.2/19520 (2000/12/18)". The last number before
# the date is the server changelevel.
server_version_re = re.compile('Server version: '
'[^/]+/[^/]+/[^/]+/([0-9]+)')
server_version_re2 = re.compile('[^/]+/[^/]+/[^/]+/([0-9]+)')
def __init__(self, client = None, client_executable = 'p4',
logger = None, password = None, port = None,
user = None):
self.client = client
self.client_executable = client_executable
self.logger = logger
self.password = password
self.port = port
self.user = user
client_changelevel = 0
supported_client = 16895
p4_exe = self.client_executable
if ' ' in p4_exe:
command = '"%s" -V' % p4_exe
else:
command = '%s -V' % p4_exe
lines = os.popen(command, 'r').readlines()
client_version_re = re.compile(
'Rev\\. [^/]+/[^/]+/[^/]+/([0-9]+)')
for l in lines:
match = client_version_re.match(l)
if match:
client_changelevel = int(match.group(1))
if client_changelevel < supported_client:
# "Perforce client changelevel %d is not supported
# by P4DTI. Client must be at changelevel %d or
# above."
raise error, catalog.msg(704, (client_changelevel,
supported_client))
else:
break
if not client_changelevel:
# "The command '%s' didn't report a recognizable version
# number. Check your setting for the 'p4_client_executable'
# parameter."
raise error, catalog.msg(705, command)
# Check that the Perforce server version is supported by the
# integration.
found_changelevel = 0
for x in self.run('info'):
if (x.has_key('code') and x['code'] == 'info'
and x.has_key('data')):
match = self.server_version_re.match(x['data'])
if match:
self.server_changelevel = int(match.group(1))
found_changelevel = 1
break
elif x.has_key('serverVersion'):
match = self.server_version_re2.match(x['serverVersion'])
if match:
self.server_changelevel = int(match.group(1))
found_changelevel = 1
break
if not found_changelevel:
# "The Perforce command 'p4 info' didn't report a
# recognisable version."
raise error, catalog.msg(835)
if not self.supports('p4dti'):
# "The Perforce server changelevel %d is not supported by
# the P4DTI. See the P4DTI release notes for Perforce
# server versions supported by the P4DTI."
raise error, catalog.msg(834, self.server_changelevel)
# 2.2. Write a message to the log
#
# But only if a logger was supplied.
def log(self, id, args = ()):
if self.logger:
msg = catalog.msg(id, args)
self.logger.log(msg)
# 2.3. Run a Perforce command
#
# run(arguments, input): Run the Perforce client with the given
# command-line arguments, passing the dictionary 'input' to the
# client's standard input.
#
# The arguments should be a Perforce command and its arguments, like
# "jobs -o //foo/...". Options should generally include -i or -o to
# avoid forms being put up interactively.
#
# Return a list of dictionaries containing the output of the
# Perforce command. (Each dictionary contains one Perforce entity,
# so "job -o" will return a list of one element, but "jobs -o" will
# return a list of many elements.)
def run(self, arguments, input = None):
assert isinstance(arguments, types.StringType)
assert input is None or isinstance(input, types.DictType)
# Build a command line suitable for use with CMD.EXE on Windows
# NT, or /bin/sh on POSIX. Make sure to quote the Perforce
# command if it contains spaces. See job000049.
if ' ' in self.client_executable:
command_words = ['"%s"' % self.client_executable]
else:
command_words = [self.client_executable]
command_words.append('-G')
if self.port:
command_words.extend(['-p', self.port])
if self.user:
command_words.extend(['-u', self.user])
if self.password:
command_words.extend(['-P', self.password])
if self.client:
command_words.extend(['-c', self.client])
command_words.append(arguments)
# Pass the input dictionary (if any) to Perforce.
temp_filename = None
if input:
tempfile.template = 'p4dti_data'
temp_filename = tempfile.mktemp()
# Python marshalled dictionaries are binary, so use mode
# 'wb'.
temp_file = open(temp_filename, 'wb')
marshal.dump(input, temp_file)
temp_file.close()
command_words.extend(['<', temp_filename])
# "Perforce input: '%s'."
self.log(700, input)
command = string.join(command_words, ' ')
# "Perforce command: '%s'."
self.log(701, command)
# Python marshalled data is in binary, so we need mode 'rb'.
# However, os.popen() doesn't support this mode on POSIX, so we
# use 'r' there.
if os.name == 'nt':
mode = 'rb'
elif os.name == 'posix':
mode = 'r'
else:
# "The Perforce interface does not support the operating
# system '%s'."
raise error, catalog.msg(709, os.name)
stream = os.popen(command, mode)
# Read the results of the Perforce command.
results = []
try:
while 1:
results.append(marshal.load(stream))
except EOFError:
if temp_filename:
os.remove(temp_filename)
# Check the exit status of the Perforce command, rather than
# simply returning empty output when the command didn't run for
# some reason (such as the Perforce server being down). This
# code was inserted to resolve job job000158. RB 2000-12-14
exit_status = stream.close()
if exit_status != None:
# "Perforce status: '%s'."
self.log(702, exit_status)
# "Perforce results: '%s'."
self.log(703, results)
# Check for errors from Perforce (either errors returned in the
# data, or errors signalled by the exit status, or both) and
# raise a Python exception.
#
# Perforce signals an error by the presence of a 'code' key in
# the dictionary output. (This isn't a totally reliable way to
# spot an error in a Perforce command, because jobs can have
# 'code' fields too. See job000003. However, the P4DTI makes
# sure that its jobs don't have such a field.)
if (len(results) == 1 and results[0].has_key('code')
and results[0]['code'] == 'error'):
msg = results[0]['data']
if exit_status:
# "%s The Perforce client exited with error code %d."
raise error, catalog.msg(706, (msg, exit_status))
else:
# "%s"
raise error, catalog.msg(708, msg)
elif exit_status:
# "The Perforce client exited with error code %d. The
# server might be down; the server address might be
# incorrect; or your Perforce licence might have expired."
raise error, catalog.msg(707, exit_status)
else:
return results
# 2.4. Comparing jobspec field descriptions
#
# This is a function for passing to sort() which
# allows us to sort jobspec field descriptions based on
# the field number.
def compare_field_by_number(self, x, y):
if x[0] < y[0]:
return -1
elif x[0] > y[0]:
return 1
else:
# "Jobspec fields '%s' and '%s' have the same
# number %d."
raise error, catalog.msg(710, (x[1], y[1], x[0]))
# 2.5. Install a new jobspec
#
# We have to build a dictionary suitable for passing to "p4 -G
# jobspec -i". This means that it will look like this:
#
# { 'Comments': '# Form comments...',
# 'Fields0': '101 Job word 32 required',
# 'Fields1': '102 State select 32 required',
# 'Values1': '_new/assigned/closed/verified/deferred',
# 'Presets1': '_new',
# ...
# }
#
# See [GDR 2000-10-16, 8.4] for the format of the "description"
# argument.
def install_jobspec(self, description):
comment, fields = description
assert isinstance(fields, types.ListType)
# "Installing jobspec from comment '%s' and fields %s."
self.log(712, (comment, fields))
for field in fields:
assert isinstance(field, types.TupleType)
assert len(field) >= 8
def make_comment(field):
if field[7] == None:
return ""
else:
return "# %s: %s\n" % (field[1], field[7])
# we will need the jobspec as a dictionary in order to
# give it to Perforce.
jobspec_dict = {}
fields.sort(self.compare_field_by_number)
i = 0
for field in fields:
jobspec_dict['Fields%d' % i] = ("%s %s %s %s %s"
% field[0:5])
i = i + 1
i = 0
for field in fields:
if field[6] != None:
jobspec_dict['Values%d' % i] = "%s %s" % (field[1],
field[6])
i = i + 1
i = 0
for field in fields:
if field[5] != None:
jobspec_dict['Presets%d' % i] = "%s %s" % (field[1],
field[5])
i = i + 1
jobspec_dict['Comments'] = (comment +
string.join(map(make_comment,
fields),
""))
self.run('jobspec -i', jobspec_dict)
# 2.6. Get the jobspec.
#
# Does very little checking on the output of 'jobspec -o'.
# Ought to validate it much more thoroughly than this.
def get_jobspec(self):
jobspec_dict = self.run('jobspec -o')[0]
fields = []
fields_dict = {}
fields_re = re.compile('^Fields[0-9]+$')
presets_re = re.compile('^Presets[0-9]+$')
values_re = re.compile('^Values[0-9]+$')
comments_re = re.compile('^Comments$')
comment = ""
for k,v in jobspec_dict.items():
if fields_re.match(k): # found a field
words = string.split(v)
name = words[1]
if not fields_dict.has_key(name):
fields_dict[name] = {}
fields_dict[name]['code'] = int(words[0])
fields_dict[name]['datatype'] = words[2]
fields_dict[name]['length'] = int(words[3])
fields_dict[name]['disposition'] = words[4]
elif presets_re.match(k): # preset for a non-optional field
space = string.find(v,' ')
name = v[0:space]
preset = v[space+1:]
if not fields_dict.has_key(name):
fields_dict[name] = {}
fields_dict[name]['preset'] = preset
elif values_re.match(k): # values for a select field
space = string.find(v,' ')
name = v[0:space]
values = v[space+1:]
if not fields_dict.has_key(name):
fields_dict[name] = {}
fields_dict[name]['values'] = values
elif comments_re.match(k): # comments for a field
comment = v
for k,v in fields_dict.items():
fields.append((v['code'],
k,
v['datatype'],
v['length'],
v['disposition'],
v.get('preset', None),
v.get('values', None),
None,
None))
fields.sort(self.compare_field_by_number)
# "Decoded jobspec as comment '%s' and fields %s."
self.log(711, (comment, fields))
return comment, fields
# 2.7. Does the jobspec include P4DTI fields?
#
# In fact, this only checks the P4DTI-rid field on the assumption
# that you are very unlikely to get this without the others.
def jobspec_has_p4dti_fields(self, jobspec):
comment, fields = jobspec
for field in fields:
if field[1] == 'P4DTI-rid':
return 1
return 0
# 2.8. Does the Perforce server support a feature?
#
# supports(feature) returns 1 if the Perforce server has the
# feature, 0 if it does not. You can interrogate the following
# features:
#
# fix_update Does Perforce update 'always' fields in a job when it
# is changed using the 'fix' command?
# p4dti Is the Perforce version supported by the P4DTI?
def supports(self, feature):
if feature == 'p4dti':
return self.server_changelevel >= 18974
elif feature == 'fix_update':
return self.server_changelevel >= 29455
else:
return 0
# A. REFERENCES
#
# [GDR 2000-10-16] "Perforce Defect Tracking Integration Integrator's
# Guide"; Gareth Rees; Ravenbrook Limited; 2000-10-16;
# <http://www.ravenbrook.com/project/p4dti/version/2.0/manual/ig/>.
#
#
# B. DOCUMENT HISTORY
#
# 2000-09-25 GDR Created. Moved Perforce interface from replicator.py.
#
# 2000-12-07 GDR Provided defaults for all configuration parameters so
# that you can make a p4 object passing no parameters to get the default
# Perforce behaviour.
#
# 2000-12-14 RB Added check for the exit status of the "p4" command so
# that the caller can tell the difference between empty output and a
# connection (or other) error.
#
# 2000-12-15 NB Added verbosity control.
#
# 2001-01-23 GDR Added check that Perforce client version is supported.
#
# 2001-02-14 GDR Report the Perforce error message together with the
# exit status when we have both.
#
# 2001-02-19 NB Keyword translation updated and moved here (as it is
# Perforce-specific.
#
# 2001-02-21 GDR Moved keyword translator to its own file (keyword.py)
# so that there's no import loop.
#
# 2001-03-02 RB Transferred copyright to Perforce under their license.
#
# 2001-03-12 GDR Use messages for errors and logging.
#
# 2001-03-13 GDR Removed verbose parameter and verbosity control; this
# was made redundant by the log_level parameter.
#
# 2001-03-15 GDR Formatted as a document. Take configuration as
# variables.
#
# 2001-03-24 GDR Check the Perforce server changelevel.
#
# 2001-05-18 GDR Don't log Perforce exit status if it's None.
#
# 2001-06-22 NB New jobspec class containing common code for
# converting lists of tuples into a jobspec dictionary.
#
# 2001-06-22 NB Get jobspec from p4 into our internal list-of-tuples
# format.
#
# 2001-06-25 NB jobspec field codes need to be integers.
#
# 2001-06-29 NB Fixed jobspec parsing (it worked if the fields were in
# the right order). Also added debugging messages 711 and 712.
#
# 2002-01-28 GDR New method 'supports' tells you whether the Perforce
# server supports a feature.
#
#
# C. COPYRIGHT AND LICENCE
#
# This file is copyright (c) 2001 Perforce Software, Inc. All rights
# reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.
#
#
# $Id: //info.ravenbrook.com/project/p4dti/version/2.0/code/replicator/p4.py#2 $