# Perforce Defect Tracking Integration Project # # # CONFIGURE_TEAMTRACK.PY -- BUILD P4DTI CONFIGURATION FOR TEAMTRACK # # Gareth Rees, Ravenbrook Limited, 2000-11-17 # # # 1. INTRODUCTION # # This module defines a configuration generator for the TeamTrack # integration. Configuration generators are documented in detail in # [GDR 2000-10-16, 8]. # # The intended readership of this document is project developers. # # This document is not confidential. import catalog import check_config import dt_teamtrack import logger import re import string import teamtrack import time import translator import types error = "TeamTrack configuration error" # 2. BUILD THE MAPPING BETWEEN STATES IN TEAMTRACK AND PERFORCE # # The make_state_pairs function takes a TeamTrack connection and the # "closed state", and returns a list of pairs of state names (TeamTrack # state, Perforce state). This list will be used to translate between # states, and also to generate the possible values for the State field # in Perforce. # # The closed_state argument is the TeamTrack state which maps to the # special state 'closed' in Perforce, or None if there is no such state. # See requirement 45. See the decision decision [RB 2000-11-28b]. # # The case of state names in these pairs is normalized for usability in # Perforce: see the design decision [RB 2000-11-28a]. keyword_translator = translator.keyword_translator() def make_state_pairs(config, teamtrack_connection, closed_state): # 2.1. Get TeamTrack states # # Select states belong to CASES workflows (ignore states belonging # to INCIDENTS workflows). This was motivated by job000194. See # the definition of the TS_STATES table in the TeamTrack database # schema [TeamShare 2001-04-30]. query = ('TS_PROJECTID IN (SELECT TS_ID FROM TS_WORKFLOWS WHERE ' 'TS_TABLEID = %d)' % teamtrack_connection.case_table_id()) if not config.use_deleted_selections: query = query + ' AND TS_STATUS = 0' states = teamtrack_connection.query(teamtrack.table['STATES'], query) state_pairs = [] state_p4_to_tt = {} found_closed_state = 0 # 2.2. Make Perforce versions of the state names # # We convert the TeamTrack state name to lowercase (so that it's # easy to type in Perforce), apply the keyword translator (so that # the state is legal in Perforce), and then apply two special cases: # # 1. The "closed_state" configuration parameter. # # 2. Perforce jobs can't have state "new" (this indicates a fresh # job and Perforce changes the state to "open"). Nor can they have # state "ignore", because that is used in the submit form to # indicate that a job shouldn't be fixed by the change. # # Unfortunately, "new" and "ignore" are common names for states in # the defect tracker (the former is in the default workflow in # TeamTrack), so we don't disallow them, but translate them to # "_new" and "_ignore"; we disallow those instead (or rather, we # quit if two TeamTrack states map to the same state in Perforce.) # See job000141. for s in states: tt_state = string.lower(s['NAME']) if closed_state and tt_state == string.lower(closed_state): p4_state = 'closed' found_closed_state = 1 else: p4_state = keyword_translator.translate_0_to_1(tt_state) if p4_state in ['new', 'ignore']: p4_state = '_' + p4_state if (state_p4_to_tt.has_key(p4_state) and state_p4_to_tt[p4_state] != tt_state): # "Two TeamTrack states '%s' and '%s' map to the same # Perforce state '%s'." raise error, catalog.msg(400, (tt_state, state_p4_to_tt[p4_state], p4_state)) state_p4_to_tt[p4_state] = tt_state pair = (tt_state, p4_state) if pair not in state_pairs: state_pairs.append(pair) if closed_state != None and not found_closed_state: # "You specified the closed_state '%s', but there's no such # TeamTrack state." raise error, catalog.msg(401, closed_state) return state_pairs # 3. CONVERT DATE/TIME TO SECONDS # # This function converts a date/time in standard format, like # '2001-02-12 19:19:24' [ISO 8601] into seconds since the epoch. # # We use this to convert the start_date configuration parameter. It is # specified as an date/time for ease of entry, but TeamTrack represents # date/times as seconds since the epoch. (Note that we specify -1 for # the DST flag -- see job000381). def convert_isodate_to_secs(isodate): assert isinstance(isodate, types.StringType) date_re = "^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$" match = re.match(date_re, isodate) assert match return time.mktime(tuple(map(int, match.groups()) + [0,0,-1])) # 4. MIGRATION FUNCTIONS def prepare_issue_advanced(config, tt, p4, dict, job): # Deduce a SUBMITTER for the issue, unless we have one already. if dict.get('SUBMITTER', 0) == 0: for field in ['P4DTI-user', 'Owner', 'User']: if job.has_key(field): s = config.user_translator.translate_1_to_0( job[field], tt, p4) dict['SUBMITTER'] = s break # if LASTMODIFIEDDATE or SUBMITDATE is replicated and hasn't been # filled in, remove it (TeamTrack will fill it in). if dict.get('LASTMODIFIEDDATE', None) == -2: del dict['LASTMODIFIEDDATE'] if dict.get('SUBMITDATE', None) == -2: del dict['SUBMITDATE'] # Supply default of 0 for ISSUETYPE as promised [GDR 2001-11-14, 3]. dict['ISSUETYPE'] = dict.get('ISSUETYPE', 0) # Supply default of 0 for PROJECTID as promised [GDR 2001-11-14, 3]. dict['PROJECTID'] = dict.get('PROJECTID', 0) # Call user hook (see [GDR 2001-11-14, 3]). config.prepare_issue(dict, job) def translate_jobspec_advanced(config, dt, p4, job): # Call user hook (see [GDR 2001-11-14, 4.6]). return config.translate_jobspec(job) # 5. BUILD P4DTI CONFIGURATION FOR TEAMTRACK def configuration(config): # 5.1. Check TeamTrack-specific configuration parameters check_config.check_list_of(config, 'replicated_fields', types.StringType, 'strings') check_config.check_string(config, 'teamtrack_password') check_config.check_string(config, 'teamtrack_server') check_config.check_string(config, 'teamtrack_user') check_config.check_string(config, 'teamtrack_version') check_config.check_bool(config, 'use_windows_event_log') # 5.2. Open a connection to the TeamTrack server connection = teamtrack.connect(config.teamtrack_user, config.teamtrack_password, config.teamtrack_server) # 5.3. Translators # # We make only one user translator so that the TeamTrack USER table # only needs to be read once when the replicator starts and once # when there's a cache miss. See job000148. date_translator = dt_teamtrack.date_translator() elapsed_time_translator = dt_teamtrack.elapsed_time_translator() int_translator = dt_teamtrack.int_translator() float_translator = dt_teamtrack.float_translator() text_translator = dt_teamtrack.text_translator() user_translator = dt_teamtrack.user_translator() state_pairs = make_state_pairs(config, connection, config.closed_state) state_translator = dt_teamtrack.state_translator(state_pairs) # 5.4. TeamTrack field types # # The table 'type_table' defines which TeamTrack field types are # supported by the P4DTI, and defines how values in these fields are # translated between TeamTrack and Perforce. # # 'type_table' is a map from TeamTrack field type to a tuple # (TeamTrack field type name, Perforce field type, Perforce field # length, translator). # # The TeamTrack field type is the value in the TS_FLDTYPE column in # the TS_FIELDS table; see the TeamTrack schema for details # [TeamShare 2001-04-30]. # # The field type name is for documentation only. # # Commented out fields are ones we don't support. If new # translators are written for those field types, we can uncomment # them. # # This table isn't complete. There are a number of special cases # that will be applied when each field is considered: # # 1. A TEXT field (type 101) with TS_ATTRIBUTES = 0 is actually a # MEMO field. # # 2. A DATETIME field (type 103) with TS_ATTRIBUTES = 2 or 3 is # actually an elapsed time field. We have the dummy type 'time' for # elapsed time fields. # # 3. Each SELECTION field (type 104) needs its own translator, and # the translator needs to know the name of the field in the TS_CASES # table. So we make a translator object for each replicated field. # # 4. A TEXT field (type 101) with TS_ATTRIBUTES = 2 or 3 is a # journal field. See [GDR 2001-09-26]. aux = dt_teamtrack.auxiliary_translator type_table = { 100: ( 'NUMERIC', 'line', 32, int_translator ), 'float': ( 'NUMERIC', 'line', 32, float_translator ), 'fixed': ( 'NUMERIC', 'line', 32, None ), 101: ( 'TEXT', 'text', 0, text_translator ), 102: ( 'MEMO', 'text', 0, text_translator ), 'journal': ( 'MEMO', 'text', 0, None, ), 103: ( 'DATETIME', 'date', 20, date_translator ), 'time': ( 'DATETIME', 'line', 32, elapsed_time_translator ), 104: ( 'SELECTION', 'select', 32, None ), 105: ( 'BINARY', 'select', 32, None ), 106: ( 'STATE', 'select', 32, state_translator ), 107: ( 'USER', 'word', 32, user_translator ), 108: ( 'PROJECTS', 'line', 80, aux('PROJECTS') ), #109: ( 'SUMMATION', '?', ?, ? ), #110: ( 'MULTIPLE_SELECTION', '?', ?, ? ), #111: ( 'CONTACT', 'word', 32, ? ), 112: ( 'COMPANIES', 'line', 80, aux('COMPANIES') ), #113: ( 'INCIDENT', 'word', 32, ? ), 114: ( 'PRODUCTS', 'line', 80, aux('PRODUCTS') ), 115: ( 'SERVICEAGREEMENTS', 'line', 80, aux('SERVICEAGREEMENTS') ), #116: ( 'FOLDER', 'word', 32, ? ), #117: ( 'KEYWORDLIST', '?', ?, ? ), #118: ( 'PRODUCTLIST', '?', ?, ? ), #119: ( 'PROBLEM', 'word', 32, ? ), #120: ( 'RESOLUTION', 'word', 32, ? ), #121: ( 'MERCHANDISE', 'word', 32, ? ), } # 5.5. Read TeamTrack fields # # Read the TS_FIELDS table and turn it into a map from the field's # database name (DBNAME field) to the field definition. # # We only want to consider fields from the TS_CASES (if there's a # field in the TS_INCIDENTS table with the same DBNAME as a field in # the TS_CASES table we don't want to use the former by mistake). # Hence the selection on the TS_TABLEID field. # # If the TS_STATUS of a field is not 0, then it's "inactive", # meaning deleted. We don't want to consider those either. tt_fields = {} for f in connection.query(teamtrack.table['FIELDS'], 'TS_TABLEID = %d AND TS_STATUS = 0' % connection.case_table_id()): tt_fields[f['DBNAME']] = f try: state_description = tt_fields['STATE']['DESCRIPTION'] owner_description = tt_fields['OWNER']['DESCRIPTION'] title_description = tt_fields['TITLE']['DESCRIPTION'] except KeyError: # "Couldn't get descriptions for TeamTrack system fields STATE, # OWNER, and TITLE." raise error, catalog.msg(402) # 5.6. Make values for the State field in Perforce # # Work out the legal values of the State field in Perforce. Note # that "closed" must be a legal state because "p4 fix -c CHANGE # JOBNAME" always sets the State to "closed" even if "closed" is not # a legal value. See job000225. legal_states = map((lambda x: x[1]), state_pairs) if 'closed' not in legal_states: legal_states.append('closed') state_values = string.join(legal_states, '/') # 5.7. Fields that always appear in the Perforce jobspec # # The 'p4_fields' table maps TeamTrack field name to a definition of # the corresponding field in Perforce. The table also has entries # for field not replicated from TeamTrack: these appear under dummy # TeamTrack field names in parentheses. # # Perforce field definitions have nine elements: # # 1. Field number; # # 2. Field name; # # 3. Field type (word, line, select, date, text); # # 4. Field length; # # 5. Field disposition (always, required, optional, default); # # 6. The default value for the field, or None if there isn't one # (field Preset); # # 7. Legal values for the field (if it's a "select" field) or None # otherwise (field Values); # # 8. Help text for the field; # # 9. Translator for the field (if the field is replicated from # TeamTrack), or None (if the field is not replicated). # # The five fields 101 to 105 are predefined because they are # required by Perforce. The fields Job and Date are special: they # are required by Perforce but are not replicated from TeamTrack. # Note that their help text is given (the other help texts will be # fetched from TeamTrack). # # We extend this table with fields from the "replicated_fields" # configuration parameter (section 5.8). Next we use the table to # buid the Perforce jobspec (section 5.9). Finally, we use the # table to build the "field_map" configuration parameter which the # replicator module uses to replicate the field (section 5.10). p4_fields = { '(JOB)': ( 101, 'Job', 'word', 32, 'required', None, None, "The job name.", None ), 'STATE': ( 102, 'State', 'select', 32, 'required', state_pairs[0][1], state_values, state_description, state_translator ), 'OWNER': ( 103, 'Owner', 'word', 32, 'required', '$user', None, owner_description, user_translator ), '(DATE)': ( 104, 'Date', 'date', 20, 'always', '$now', None, "The date this job was last modified.", None ), 'TITLE': ( 105, 'Title', 'text', 0, 'required', '$blank', None, title_description, text_translator ), '(FILESPECS)': ( 191, 'P4DTI-filespecs', 'text', 0, 'optional', None, None, "Associated filespecs.", None ), '(RID)': ( 192, 'P4DTI-rid', 'word', 32, 'required', 'None', None, "P4DTI replicator identifier. Do not edit!", None ), '(ISSUE)': ( 193, 'P4DTI-issue-id', 'word', 32, 'required', 'None', None, "TeamTrack issue database identifier. Do not " "edit!", None ), '(USER)': ( 194, 'P4DTI-user', 'word', 32, 'always', '$user', None, "Last user to edit this job. You can't edit " "this!", None ), } # 5.8. Add replicated fields to table # # Go through the "replicated_fields" configuration parameter. For # each replicated field in TeamTrack, build a structure of none # elements describing the corresponding field in Perforce (see # section 5.7 for the definition of this structure) and add it to # the p4_fields table. # # Replicated fields will start with the Perforce field number 110. # This gives some opportunity for people using advanced # configuration to add extra fields near the start of the Perforce # jobspec. Since the P4DTI fields start at 191, this allows 81 # replicated fields, which should be plenty. p4_field_id = 110 for tt_field_name in config.replicated_fields: # Convert the field name to uppercase. This allows people to # specify field names case-insensively in the # "replicated_fields" configuration parameter. tt_field_name = string.upper(tt_field_name) # The field must be a valid TeamTrack field. if not tt_fields.has_key(tt_field_name): # "Field '%s' specified in 'replicated_fields' list not in # TeamTrack FIELDS table." raise error, catalog.msg(403, tt_field_name) # The field must not be already in the p4_fields table. if p4_fields.has_key(tt_field_name): if tt_field_name in ['STATE', 'OWNER', 'TITLE']: # "Field '%s' specified in 'replicated_fields' list is a # system field: leave it out!" raise error, catalog.msg(404, tt_field_name) else: # "Field '%s' appears twice in 'replicated_fields'." raise error, catalog.msg(405, tt_field_name) f = tt_fields[tt_field_name] tt_field_type = f['FLDTYPE'] # TeamTrack numeric fields have type 100, with ATTRIBUTES set to # 0 (integer), 1 (float), or 2 (fixed precision). if tt_field_type == 100: if f['ATTRIBUTES'] == 1: tt_field_type = 'float' elif f['ATTRIBUTES'] == 2: tt_field_type = 'fixed' # TeamTrack variable length text fields are not given as MEMO # fields (type 102) but as TEXT fields (type 101) with # ATTRIBUTES set to 0 (ordinary variable-length), 2 (journal) or # 3 (append-only journal). Handle these special cases here. # See job000370 for the problem with journal fields. elif tt_field_type == 101: if f['ATTRIBUTES'] in [2, 3]: tt_field_type = 'journal' elif f['ATTRIBUTES'] != 1: tt_field_type = 102 # TeamTrack date/time fields have four types: 0 (date only), 1 # (date and time), 2 (time of day), 3 (elapsed time). The # former two should be represented as Perforce dates and # translated using date_translator; the latter two should be # represented as text fields and translated using # elapsed_time_translator. See job000182. elif tt_field_type == 103 and f['ATTRIBUTES'] in [2,3]: tt_field_type = 'time' # Look up the field type in type_table to find out how to # convert it to a Perforce field. See section 5.4. if not type_table.has_key(tt_field_type): # "Field '%s' has type %d: this is not supported by P4DTI." raise error, catalog.msg(406, (tt_field_name, tt_field_type)) tt_table_name, p4_field_type, p4_field_length, trans = \ type_table[tt_field_type] p4_field_values = None p4_field_preset = None p4_field_name = keyword_translator.translate_0_to_1(f['NAME']) # "p4 -G" uses the field "code" to indicate whether the Perforce # command succeeded or failed. See job000003. if p4_field_name == 'code': # "You can't have a field called 'code' in the Perforce # jobspec." raise error, catalog.msg(407) # For single-select fields, work out the values for that field # in Perforce, and build a translator. if tt_field_type == 104: query = "TS_FLDID = %d" % f['ID'] if not config.use_deleted_selections: query = query + ' AND TS_STATUS = 0' selections = connection.query(teamtrack.table['SELECTIONS'], query) values = map(lambda s: keyword_translator.translate_0_to_1( s['NAME']), selections) values.append('(None)') p4_field_values = string.join(values, '/') trans = dt_teamtrack.single_select_translator( f['DBNAME'], keyword_translator) # For journal fields, the translator needs to know whether the # field is append-only, and the TeamTrack database name of the # field, so that it can enforce the append-only constraint. elif tt_field_type == 'journal': trans = dt_teamtrack.journal_translator( f['ATTRIBUTES'] == 3, f['DBNAME'], user_translator) # For binary fields, the translator needs to know the labels for # 0 and 1 (note that the label for 0 is LABEL1 and the label for # 1 is LABEL2). Supply defaults when the labels are empty # strings in TeamTrack, just in case. elif tt_field_type == 105: tt_labels = (f['LABEL1'], f['LABEL2']) if not tt_labels[0]: tt_labels[0] = 'No' if not tt_labels[1]: tt_labels[1] = 'Yes' p4_labels = map(keyword_translator.translate_0_to_1, tt_labels) p4_field_values = string.join(p4_labels, '/') trans = dt_teamtrack.binary_translator(p4_labels) # For fixed-point numeric fields, the translator needs to know # the number of digits after the decimal point. elif tt_field_type == 'fixed': # TS_FLDOPTION_PRECISION_MASK is 0x0F00 [TeamShare # 2001-04-30]. digits = (f['OPTIONS'] & 0x0F00) >> 8 trans = dt_teamtrack.fixed_point_translator(digits) # Build a structure describing the Perforce field; add it to the # table. p4_fields[tt_field_name] = \ ( p4_field_id, p4_field_name, p4_field_type, p4_field_length, 'optional', p4_field_preset, p4_field_values, f['DESCRIPTION'], trans ) p4_field_id = p4_field_id + 1 if p4_field_id >= 191: # "Too many fields to replicate: Perforce jobs can contain # only 99 fields." raise error, catalog.msg(408) # 5.9. Make jobspec description comment = ("# A Perforce Job Specification automatically " "produced by the\n" "# Perforce Defect Tracking Integration\n") jobspec = (comment, p4_fields.values()) # 5.10. Generate configuration parameters loggers = [] log_params = { 'priority': config.log_level, 'max_length': config.log_max_message_length, } # The log messages should go to (up to) three places: # 1. to standard output (if running from a command line); if config.use_stdout_log: loggers.append(apply(logger.file_logger, (), log_params)) # 2. to the file named by the log_file configuration parameter (if # not None); if config.log_file != None: loggers.append(apply(logger.file_logger, (open(config.log_file, "a"),), log_params)) # 3. to the Windows event log (if use_windows_event_log is true). if config.use_windows_event_log: loggers.append(apply(logger.win32_event_logger, (config.rid,), log_params)) config.logger = logger.multi_logger(loggers) # Set configuration parameters needed by dt_teamtrack. config.start_date = convert_isodate_to_secs(config.start_date) config.state_pairs = state_pairs # Set configuration parameters needed by the replicator. config.date_translator = date_translator config.job_owner_field = 'Owner' config.job_status_field = 'State' config.job_date_field = 'Date' config.jobspec = jobspec config.prepare_issue_advanced = prepare_issue_advanced config.translate_jobspec_advanced = translate_jobspec_advanced config.text_translator = text_translator config.user_translator = user_translator # The field_map parameter is a list of triples (TeamTrack database # field name, Perforce field name, translator) required by the # replicator. # # This is generated from the p4_field table by filtering out fields # that aren't replicated (these have no translator) and selecting # only the three elements of interest. config.field_map = \ map(lambda item: (item[0], item[1][1], item[1][8]), filter(lambda item: item[1][8] != None, p4_fields.items())) return config # A. REFERENCES # # [GDR 2001-11-14] "Perforce Defect Tracking Integration Advanced # Administrator's Guide"; Gareth Rees; Ravenbrook Limited; 2001-11-14; # . # # [GDR 2000-10-16] "Perforce Defect Tracking Integration Integrator's # Guide"; Richard Brooksby; Ravenbrook Limited; 2000-10-16; # . # # [GDR 2001-09-26] "TeamTrack journal fields"; Gareth Rees; Ravenbrook # Limited; 2001-09-26; # . # # [GDR 2000-10-16] "Perforce Defect Tracking Integration Integrator's # Guide"; Gareth Rees; Ravenbrook Limited; 2000-10-16; # . # # [ISO 8601] "Representation of dates and times"; ISO; 1988-06-15. # # [RB 2000-11-28a] "Case of state names" (e-mail message); Richard # Brooksby; Ravenbrook; 2000-11-28; # . # # [RB 2000-11-28b] "Distinguished state to map to 'closed'" (e-mail # message); Richard Brooksby; Ravenbrook; 2000-11-28; # . # # [TeamShare 2000-01-20] "TeamTrack Database Schema (Database Version: # 21)"; TeamShare; 2000-01-20; # . # # [TeamShare 2001-04-30] "TeamTrack Database Schema (Database Version: # 514)"; TeamShare; 2001-04-30; # . # # # B. DOCUMENT HISTORY # # 2000-12-07 RB Updated "resolver" to "administrator" in some error # messages. Fixed the field length on "P4DTI-issue-id". # # 2000-12-08 GDR Translate state name "ignore" to "_ignore". # # 2000-12-15 NB Added verbosity control. # # 2001-01-19 NB Validate config items. log_file may be None. # # 2001-02-04 GDR Added start_date parameter. # # 2001-02-12 GDR Convert start_date to seconds since epoch before # passing to dt_teamtrack. # # 2001-02-13 GDR Allow administrator_address and smtp_server to be None. # # 2001-02-15 GDR Time fields map to text fields, use # elapsed_time_translator # # 2001-02-16 NB Added replicate_p configuration parameter. # # 2001-02-19 NB Moved keyword translation to p4.py. # # 2001-02-22 GDR Moved keyword translation to keyword.py. Made sure # that 'closed' is a legal job state in Perforce. Added text translator # to replicator config. # # 2001-02-26 GDR Changed error name to "TeamTrack configuration error" # for consistency with "Bugzilla configuration error" in # configure_bugzilla.py. Refer to "TeamTrack" explicitly in messages, # not just "defect tracker". # # 2001-03-02 RB Transferred copyright to Perforce under their license. # # 2001-03-12 GDR Using messages for errors. # # 2001-03-13 GDR Remove verbose parameter; added log_level. Removed # P4DTI-action field. Made P4DTI-filespec field optional. Get # keyword_translator from translator, not keyword. # # 2001-03-15 GDR Get configuration from the config module. # # 2001-03-17 GDR Formatted as document. Added links to design. Make # only a single user translator to fix job000148. # # 2001-03-23 GDR Added job-date-field to replicator configuration. # # 2001-06-22 NB Moved common jobspec code into p4.py. # # 2001-06-22 NB Added initial comment to the jobspec description. # # 2001-06-29 GDR Made portable between TeamTrack 4.5 and TeamTrack 5.0 # by using case_table_id() and case_table_name(). # # 2001-07-24 GDR Recognize journal fields as being multi-line text # fields. # # 2001-08-06 GDR Specify -1 for DST argument to mktime(). # # 2001-09-12 GDR Log to the Windows event log. # # 2001-10-01 GDR Use journal_translator for journal fields. # # 2001-10-04 GDR Support numeric and binary fields. # # 2001-10-25 GDR Added migrate_issue function. # # 2001-11-05 GDR Added prepare_issue_advanced to replicator # configuration. # # 2001-11-13 GDR Log to stdout only if use_stdout_log configuration # parameter is set. # # 2001-11-20 GDR Added translate_jobspec_advanced to replicator # configuration. Put jobspec in config rather than returning it. # # 2001-11-21 GDR Check teamtrack_version configuration parameter. # # 2001-11-22 GDR Support use_deleted_selections parameters. Support # fixed-point numeric fields. # # 2001-11-26 GDR Support log_max_message_length configuration parameter. # # 2002-01-15 GDR Use 'text' type in Perforce for all text fields; see # job000442. # # 2002-03-28 NB Make lambda syntax consistent. # # 2002-04-05 NB job000501: handle creation of new jobs when # LASTMODIFIEDDATE or SUBMITDATE are replicated # # # 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/1.4/code/replicator/configure_teamtrack.py#5 $