# Perforce Defect Tracking Integration Project
#
#
# 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.
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.
server_version_re = re.compile('Server version: '
'[^/]+/[^/]+/[^/]+/([0-9]+)')
server_changelevel = 0
supported_server = 18974
for x in self.run('info'):
if x.has_key('code') and x['code'] == 'info' and x.has_key('data'):
match = server_version_re.match(x['data'])
if match:
server_changelevel = int(match.group(1))
if server_changelevel < supported_server:
# "The Perforce server changelevel %d is not supported
# by the P4DTI. The server must be at changelevel %d
# or above."
raise error, catalog.msg(834, (server_changelevel, supported_server))
else:
break
if not server_changelevel:
# "The Perforce command 'p4 info' didn't report a recognisable
# version."
raise error, catalog.msg(835)
# 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 self.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
# A. REFERENCES
#
# [GDR 2000-10-16] "Perforce Defect Tracking Integration Integrator's
# Guide"; Gareth Rees; Ravenbrook Limited; 2000-10-16;
# .
#
#
# 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.
#
#
# 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/branch/2001-11-22/bugzilla-parameters/code/replicator/p4.py#4 $