# 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 usage" (p4 2002.1) says: # # The -G flag 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 import portable error = 'Perforce error' # 2. THE P4 CLASS class p4: client = None client_executable = None logger = None password = None port = None user = None config_file = None unicode = False # 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 server and client are recent enough to support # various options required for the operation of the P4DTI. See # the method check_changelevels. def __init__(self, client = None, client_executable = 'p4', logger = None, password = None, port = None, user = None, config_file = None): self.client = client self.client_executable = client_executable self.logger = logger self.password = password self.port = port self.user = user self.config_file = config_file # If config_file is specified then create and fill # config file. # In future we might check whether the config file # already exists. if self.config_file: f = open(self.config_file, 'w') portable.protect_file(self.config_file) f.write("P4PASSWD="+self.password+"\n") f.close() # Set P4CONFIG environment variable so that the p4 command # picks up the config file. Relies on putenv() being # implemented, which it is on any POSIX system, and Windows. os.environ["P4CONFIG"]=self.config_file # discover and check the client and server changelevels. self.check_changelevels() # 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. Dump a marshalled object (marshalling version 0) to a file. # # This utility function is required because Python 2.4 breaks # binary compatibility of marshalled objects. p4 -G expects # marshalled objects of format 0 (i.e. Python < 2.4). def marshal_dump_0(self, obj, file): if marshal.__dict__.has_key('version'): marshal.dump(obj, file, 0) else: marshal.dump(obj, file) # 2.4. 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, repeat = False): 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 and not self.config_file: command_words.extend(['-P', self.password]) if self.client: command_words.extend(['-c', self.client]) if self.unicode: command_words.extend(['-C', 'utf8']) 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') self.marshal_dump_0(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) stream = portable.popen_read_binary(command) # 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'].strip() if exit_status: if msg.find('Unicode') != -1: self.unicode = not(self.unicode) unicode_switch = (self.unicode and 'on') or 'off' if (not repeat): # "Perforce message '%s'. Switching Unicode # mode %s to retry." self.log(734, (msg, unicode_switch)) return self.run(arguments, input, repeat = True) else: # "Perforce message '%s'. Reverting to Unicode # mode %s." raise error, catalog.msg(735, (msg, unicode_switch)) else: # "%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 license might have expired." raise error, catalog.msg(707, exit_status) else: return results # 2.5. 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? # counter_value Are counter values returned in a 'value' field? def supports(self, feature): if feature == 'p4dti': return self.server_changelevel >= 18974 elif feature == 'fix_update': return self.server_changelevel >= 29455 elif feature == 'counter_value': return self.server_changelevel >= 86944 else: return 0 # 2.6. Check the Perforce client and server changelevels. # # We check that the Perforce client and server are recent enough # to support various operations required by the P4DTI, and store # the client and server changelevels in the p4 object for other # subsequent checks (for example, those made by the 'supports' # function above). # # We check that the Perforce client named by the client_executable # parameter is recent enough that it supports the -G option. If # we use the -G option on an old client, we get an error # "ValueError: bad marshal data" (the marshal module is failing to # read Perforce's error message "Invalid option: -G."). # # We get the client changelevel by running "p4 -V" and parsing the # output. It should contain a line which looks like # "Rev. P4/NTX86/2000.2/19520 (2000/12/18)." In this example the # changelevel 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 get the server changelevel by running "p4 info" and parsing # the output (because the output format of "p4 -G info" is # different in Perforce 2003.2beta from previous Perforce # releases, and may change again in future). It should contain a # line which looks like "Server version: P4D/FREEBSD4/2002.2/40318 # (2003/01/17)" In this example, the changelevel is 40318. 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 info". # # Note that for "p4 info" we do not need the user, the client, or # the password. def check_changelevels(self): # client changelevel first. self.client_changelevel = 0 supported_client = 16895 (command, result, status) = self.run_p4_command('-V') match = re.search('Rev\\. [^/]+/[^/]+/[^/]+/([0-9]+)', result) if match: self.client_changelevel = int(match.group(1)) if self.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, (self.client_changelevel, supported_client)) else: # "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) # now server changelevel. self.server_changelevel = 0 if self.port: command = '-p %s info' % self.port else: command = 'info' (command, result, status) = self.run_p4_command(command) if status: # "The Perforce client exited with error code %d. The # server might be down; the server address might be # incorrect; or your Perforce license might have expired." raise error, catalog.msg(707, status) match = re.search('Server version: ' '[^/]+/[^/]+/[^/]+/([0-9]+)', result) if match: self.server_changelevel = int(match.group(1)) else: # "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) # Run a Perforce command without -G. Returns the command, the # output text, and the exit status. def run_p4_command(self, arguments): command = self.client_executable # quote P4 executable if it contains a space; see job000049. if ' ' in command: command = '"%s"' % command command = command + ' ' + arguments # "Perforce command: '%s'." self.log(701, command) stream = os.popen(command ,'r') result = stream.read() exit_status = stream.close() if exit_status: # "Perforce status: '%s'." self.log(702, exit_status) # "Perforce results: '%s'." self.log(703, result) return (command, result, exit_status) # 3. HANDLING JOBSPECS # # Jobspecs passed to or from Perforce ("p4 -G jobspec -i" # or "p4 -G jobspec -o") 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', # ... # } # # Jobspec structures in the rest of the P4DTI look like this # [GDR 2000-10-16, 8.4]: # # ('# A Perforce Job Specification.\n' # ..., # [(101, 'Job', 'word', 32, 'required', None, None, None, None), # (102, 'Status', 'select', 10, 'required', 'open', 'open/suspended/closed/duplicate', None, None), # ...]) # # The elements in each tuple being: # # 0: number; # 1: name; # 2: "datatype" (word/text/line/select/date); # 3: length (note: relates to GUI display only); # 4: "persistence" (optional/default/required/once/always); # 5: default, or None; # 6: possible values for select fields, as /-delimited string, or None; # 7: string describing the field (for the jobspec comment), or None; # 8: a translator object (not used in this module) or None). # # The comment is not parsed on reading the jobspec, but is # constructed (from the per-field comments) when writing it. # 3.1. Jobspec Utilities # # compare_field_by_number: 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])) # jobspec_attribute_names[i] is the name of attribute i in a # jobspec representation tuple. Used for generating messages # about jobspecs. jobspec_attribute_names = [ 'code', 'name', 'datatype', 'length', 'fieldtype', 'preset', 'values', 'comment', 'translator', # not really needed ] # jobspec_map builds a map from a jobspec, mapping one of the # tuple elements (e.g. number, name) to the whole tuple. def jobspec_map(self, jobspec, index): map = {} comment, fields = jobspec for field in fields: map[field[index]] = field return map # 3.2. Install a new jobspec 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) # 3.3. Get the jobspec. # # Get the jobspec and convert it into P4DTI representation. # # 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 # 3.4. Extending the current jobspec. # # extend_jobspec adds the given fields to the current jobspec if # not already present. def extend_jobspec(self, description, force = 0): current_jobspec = self.get_jobspec() comment, field_list = current_jobspec _, new_fields = description new_fields.sort(self.compare_field_by_number) current_fields = self.jobspec_map(current_jobspec, 1) new_field_names = map(lambda x: x[1], new_fields) field_numbers = map(lambda x: x[0], field_list) # counters for finding a free field number. free_number_p4dti = 194 free_number = 106 for field_spec in new_fields: field = field_spec[1] if current_fields.has_key(field): current_spec = current_fields[field] if (current_spec[2] != field_spec[2] or current_spec[3] != field_spec[3] or current_spec[4] != field_spec[4] or current_spec[5] != field_spec[5] or current_spec[6] != field_spec[6]): if force: # "Forcing replacement of field '%s' in jobspec." self.log(727, field) current_fields[field] = ((current_spec[0],) + field_spec[1:7] + (None,None,)) else: # "Retaining field '%s' in jobspec despite change." self.log(728, field) else: # "No change to field '%s' in jobspec." self.log(733, field) else: if field_spec[0] in field_numbers: # Field numbering clashes; find a free field number. if field[0:6] == 'P4DTI-': while free_number_p4dti in field_numbers: free_number_p4dti = free_number_p4dti - 1 number = free_number_p4dti else: while free_number in field_numbers: free_number = free_number + 1 number = free_number if free_number >= free_number_p4dti: # "Too many fields in jobspec." raise error, catalog.msg(730) field_spec = (number, ) + field_spec[1:] # "Adding field '%s' to jobspec." self.log(729, field) current_fields[field] = field_spec field_numbers.append(field_spec[0]) # Also report jobspec names fields not touched. for field in current_fields.keys(): if field not in new_field_names: # "Retaining unknown field '%s' in jobspec." self.log(732, field) self.install_jobspec((comment, current_fields.values())) # 3.5. Jobspec validation. # # jobspec_has_p4dti_fields: Does the jobspec include all the P4DTI # fields, with the right types etc. The set of things we actually # require is fairly limited. For instance, we don't insist on # having particular field numbers. # # Note that the P4DTI-filespecs field is not required for correct # operation of the P4DTI. p4dti_fields = { 'P4DTI-rid': {2: 'word', 4: 'required', 5: 'None', }, 'P4DTI-issue-id': {2: 'word', 4: 'required', 5: 'None', }, 'P4DTI-user': {2: 'word', 4: 'always', 5: '$user', }, 'P4DTI-filespecs': {}, } def jobspec_has_p4dti_fields(self, jobspec, warn = 1): map = self.jobspec_map(jobspec, 1) correct = 1 for k,v in self.p4dti_fields.items(): if map.has_key(k): for i, value in v.items(): if map[k][i] != value: if warn: # "Jobspec P4DTI field '%s' has incorrect # attribute '%s': '%s' (should be '%s')." self.log(714, (k, self.jobspec_attribute_names[i], map[k][i], value)) correct = 0 elif v: if warn: # "Jobspec does not have required P4DTI field '%s'." self.log(713, k) correct = 0 return correct # validate_jobspec: look at a jobspec and find out whether we can # run P4DTI with it. def validate_jobspec(self, jobspec): if not self.jobspec_has_p4dti_fields(jobspec): # "Jobspec does not support P4DTI." raise error, catalog.msg(715) # increasing order of restriction on Perforce job fields, based on # datatype: restriction_order = { 'text': 1, 'line': 2, 'word': 3, 'select': 4, 'date': 5, } # check_jobspec: does the current jobspec include the fields we want? # Warn on any problem areas, error if they will be fatal. def check_jobspec(self, description): satisfactory = 1 _, wanted_fields = description actual_jobspec = self.get_jobspec() self.validate_jobspec(actual_jobspec) actual_fields = self.jobspec_map(actual_jobspec, 1) wanted_fields = self.jobspec_map(description, 1) # remove P4DTI fields, which are checked by validate_jobspec() for field in self.p4dti_fields.keys(): if actual_fields.has_key(field): del actual_fields[field] if wanted_fields.has_key(field): del wanted_fields[field] shared_fields = [] # check that all wanted fields are present. for field in wanted_fields.keys(): if actual_fields.has_key(field): shared_fields.append(field) else: # field is absent. # "Jobspec does not have field '%s'." self.log(716, field) satisfactory = 0 for field in shared_fields: # field is present actual_spec = actual_fields[field] wanted_spec = wanted_fields[field] del actual_fields[field] # check datatype actual_type = actual_spec[2] wanted_type = wanted_spec[2] if actual_type == wanted_type: # matching datatypes if actual_type == 'select': # select fields should have matching values. actual_values = string.split(actual_spec[6], '/') wanted_values = string.split(wanted_spec[6], '/') shared_values = [] for value in wanted_values: if value in actual_values: shared_values.append(value) for value in shared_values: actual_values.remove(value) wanted_values.remove(value) if wanted_values: if len(wanted_values) > 1: # "The jobspec does not allow values '%s' # in field '%s', so these values cannot be # replicated from the defect tracker." self.log(718, (string.join(wanted_values, '/'), field)) else: # "The jobspec does not allow value '%s' # in field '%s', so this value cannot be # replicated from the defect tracker." self.log(719, (wanted_values[0], field)) if actual_values: if len(actual_values) > 1: # "Field '%s' in the jobspec allows values # '%s', which cannot be replicated to the # defect tracker." self.log(720, (field, string.join(actual_values, '/'))) else: # "Field '%s' in the jobspec allows value # '%s', which cannot be replicated to the # defect tracker." self.log(721, (field, actual_values[0])) elif ((wanted_type == 'date' and (actual_type == 'word' or actual_type == 'select')) or (actual_type == 'date' and (wanted_type == 'word' or wanted_type == 'select'))): # "Field '%s' in the jobspec should be a '%s' field, # not '%s'. This field cannot be replicated to or # from the defect tracker." self.log(724, (field, wanted_type, actual_type)) satisfactory = 0 else: wanted_order = self.restriction_order[wanted_type] actual_order = self.restriction_order.get(actual_type, None) if actual_order is None: # "Jobspec field '%s' has unknown datatype '%s' # which may cause problems when replicating this # field." self.log(731, (field, actual_type)) elif wanted_order > actual_order: # "Jobspec field '%s' has a less restrictive # datatype ('%s' not '%s') which may cause # problems replicating this field to the defect # tracker." self.log(723, (field, actual_type, wanted_type)) else: # "Jobspec field '%s' has a more restrictive # datatype ('%s' not '%s') which may cause # problems replicating this field from the defect # tracker." self.log(722, (field, actual_type, wanted_type)) # check persistence if actual_spec[4] != wanted_spec[4]: # "Field '%s' in the jobspec should have persistence # '%s', not '%s'. There may be problems replicating # this field to or from the defect tracker." self.log(725, (field, wanted_spec[4], actual_spec[4])) if actual_fields: for field in actual_fields.keys(): # "Perforce job field '%s' will not be replicated to the # defect tracker." self.log(726, field) # Possibly should also check that some of the # Perforce-required fields are present. See the lengthy # comment below (under "jobspec_has_p4_fields"). if not satisfactory: # "Current jobspec cannot be used for replication." raise error, catalog.msg(717) # Notes for writing a function "jobspec_has_p4_fields": Does the # jobspec have the fields which are required by Perforce? # # In the default Perforce jobspec. the first five fields look like # this: # # 101 Job word 32 required # 102 Status select 10 required # 103 User word 32 required # 104 Date date 20 always # 105 Description text 0 required # # Perforce documentation emphasizes that the names and types of # the first five fields should not be changed. But in fact, there # isn't much actually required for correct operation of Perforce: # # Field 101: # - the job name, used in various commands and automatically generated # by Perforce server if a job is created with value 'new' in this # field. # - required; # - a word; # # Field 102: # - the job status, used in various commands; # - required; # - a select; # - if the Values don't include 'closed' then things will break # (because 'p4 fix' will set it to 'closed' anyway). # # Field 103: # - the job user. # - Output by "p4 jobs" if it is a "word". # # Field 104: # - the date. # - Output by "p4 jobs" if it is a "date". # # Field 105: # - the job description, output by various commands; # - required; # - text or line. # 4. COUNTERS def counter_value(self, counter): dict = self.run('counter %s' % counter) if self.supports('counter_value'): val = dict[0]['value'] else: val = dict[0]['data'] return val # 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. # # 2002-01-28 GDR New method 'supports' tells you whether the Perforce # server supports a feature. # # 2003-08-20 DRJ Creates and uses a P4CONFIG file if asked. # # 2003-09-17 NB os.chmod doesn't work on Windows, so write a new # routine to achieve it and put it in a new module portable.py. # # 2003-09-25 NB Change name of config file parameter. # # 2003-11-03 NB Moved changelevel checking to a separate function, and # use "p4 info" without -G for the server changelevel, because "p4 -G # info" output is changing in Perforce 2003.2. # # 2003-12-05 NB Extend jobspec-checking functions. # # 2003-12-12 NB Add extend_jobspec. # # 2005-01-03 NB Add marshal_dump_0. # # 2006-02-28 NB Counter value marshal format has changed. job001342. # # # C. COPYRIGHT AND LICENSE # # This file is copyright (c) 2001-2004 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/master/code/replicator/p4.py#44 $