# Perforce Defect Tracking Integration Project
# <http://www.ravenbrook.com/project/p4dti/>
#
# DT_TRACKER.PY -- DEFECT TRACKER INTERFACE (TRACKER)
#
# Robert Cowham, Vaccaperna Systems Limited, 2003-11-20
#
#
# 1. INTRODUCTION
#
# This Python module implements an interface between the P4DTI
# replicator and the Tracker defect tracker [Requirements, 18], by
# defining the classes listed in [GDR 2000-10-16, 7]. In particular, it
# defines the following classes:
#
# [3] tracker_issue(dt_interface.defect_tracker_issue) [GDR 2000-10-16,
# 7.2]
#
# [4] tracker_fix(dt_interface.defect_tracker_fix) [GDR 2000-10-16,
# 7.3]
#
# [5] tracker_filespec(dt_interface.defect_tracker_filespec) [GDR
# 2000-10-16, 7.4].
#
# [6] dt_tracker(dt_interface.defect_tracker) [GDR 2000-10-16, 7.1].
#
# [7] Translators [GDR 2000-10-16, 7.5] for dates [GDR 2000-10-16,
# 7.5.1], elapsed times, foreign keys, single select fields, states [GDR
# 2000-10-16, 7.5.2], multi-line text fields [GDR 2000-10-16, 7.5.3] and
# users [GDR 2000-10-16, 7.5.4].
#
# This module accesses the Tracker database using the Python interface
# to Tracker [NB 2000-11-14c] and accesses and stores data according to
# Tracker schema extensions.
#
# The intended readership of this document is project developers.
#
# This document is not confidential.
import catalog
import dt_interface
import message
import re
import string
import translator
import types
import time
# 2. DATA AND UTILITIES
# 2.1. Error object
#
# All exceptions raised by this module use 'error' as the exception
# object.
error = 'Tracker module error'
# 3. TRACKER ISSUE INTERFACE
#
# This class implements the replicator's interface to the issues in
# Tracker [GDR 2000-10-16, 7.2].
class tracker_issue(dt_interface.defect_tracker_issue):
dt = None # The defect tracker this bug belongs to.
bug = None # The dictionary representing the Tracker bug.
def __init__(self, bug, dt):
# the set of keys which we explictly use in this class.
for key in ['Id',
'Title',
'Owner',
'State']:
assert bug.has_key(key)
assert isinstance(dt, dt_tracker)
self.dt = dt
self.bug = bug
def __getitem__(self, key):
assert isinstance(key, types.StringType)
return self.bug[key]
def __repr__(self):
return repr({'issue':self.bug})
def has_key(self, key):
return self.bug.has_key(key)
def add_filespec(self, filespec):
filespec_record = {}
filespec_record['filespec'] = filespec
filespec_record['Id'] = self.bug['Id']
filespec = tracker_filespec(self, filespec_record)
filespec.add()
def add_fix(self, change, client, date, status, user):
fix_record = {}
fix_record['Id'] = self.bug['Id']
fix_record['changelist'] = change
fix_record['client'] = client
fix_record['p4date'] = date
fix_record['status'] = status
fix_record['user'] = user
fix = tracker_fix(self, fix_record)
fix.add()
def corresponding_id(self):
return self.dt.config.jobname_function(self.bug)
def id(self):
return str(self.bug['Id'])
def filespecs(self):
filespecs = self.dt.tracker.filespecs_from_bug_id(
self.bug['Id'])
return map(lambda f, self=self: tracker_filespec(self, f),
filespecs)
def fixes(self):
fixes = self.dt.tracker.fixes_from_bug_id(self.bug['Id'])
return map(lambda f, self=self: tracker_fix(self, f), fixes)
def readable_name(self):
return str(self.bug['Id'])
def rid(self):
if self.bug == None: # not yet replicated
return ""
else:
return self.bug['rid']
def setup_for_replication(self, jobname):
# Record issues has been replicated by this replicator
if self.bug <> None and self.bug['rid'] == '':
self.bug['rid'] = self.dt.config.rid
changes_bug = {}
changes_bug['rid'] = self.bug['rid']
self.dt.tracker.update_bug(changes_bug, self.bug, self.dt.config.tracker_user)
# Some Tracker fields can not be updated from Perforce
def restrict_fields(self, changes):
for key in changes.keys():
if key in self.dt.config.read_only_fields:
# "Cannot change Tracker field '%s'."
raise error, catalog.msg(1204, key)
def update(self, user, changes):
changes_bug = {}
assert isinstance(user, types.StringType)
for key, value in changes.items():
assert isinstance(key, types.StringType)
if self.bug.has_key(key):
changes_bug[key] = value
else:
# "Updating non-existent Tracker field '%s'."
raise error, catalog.msg(1200, key)
self.restrict_fields(changes_bug)
self.dt.tracker.update_bug(changes_bug, self.bug, user)
# Now the bug is updated in the database, update our copy.
for key, value in changes_bug.items():
self.bug[key] = value
# Delete this bug.
def delete(self):
bug_id = self.bug['Id']
self.dt.tracker.delete_fixes_for_bug(bug_id)
self.dt.tracker.delete_filespecs_for_bug(bug_id)
self.dt.tracker.delete_bug(bug_id)
# 4. TRACKER FIX INTERFACE
#
# This class implements the replicator's interface to a fix record in
# Tracker [GDR 2000-10-16, 7.3].
class tracker_fix(dt_interface.defect_tracker_fix):
bug = None # The Tracker bug to which the fix refers.
fix = None # The dictionary representing the Tracker fix record.
def __init__(self, bug, dict):
assert isinstance(bug, tracker_issue)
assert isinstance(dict, types.DictType)
for key in ['changelist',
'client',
'p4date',
'status',
'Id',
'user']:
assert dict.has_key(key)
self.bug = bug
self.fix = dict
def __getitem__(self, key):
assert isinstance(key, types.StringType)
return self.fix[key]
def __repr__(self):
return repr(self.fix)
def __setitem__(self, key, value):
assert isinstance(key, types.StringType)
self.fix[key] = value
def add(self):
self.bug.dt.tracker.add_fix(self.fix)
def change(self):
return self.fix['changelist']
def delete(self):
self.bug.dt.tracker.delete_fix(self.fix)
def status(self):
return self.fix['status']
def update(self, change, client, date, status, user):
changes = {}
if self.fix['changelist'] != change:
changes['changelist'] = change
if self.fix['client'] != client:
changes['client'] = client
if self.fix['p4date'] != date:
changes['p4date'] = date
if self.fix['status'] != status:
changes['status'] = status
if self.fix['user'] != user:
changes['user'] = user
if len(changes) != 0:
self.bug.dt.tracker.update_fix(changes,
self.fix['Id'],
self.fix['changelist'])
# 5. TRACKER FILESPEC INTERFACE
#
# This class implements the replicator's interface to a filespec record
# in Tracker [GDR 2000-10-16, 7.4].
class tracker_filespec(dt_interface.defect_tracker_filespec):
bug = None # The Tracker bug to which the filespec refers.
filespec = None # The dictionary representing the filespec record.
def __init__(self, bug, dict):
self.bug = bug
self.filespec = dict
def __getitem__(self, key):
return self.filespec[key]
def __repr__(self):
return repr(self.filespec)
def __setitem__(self, key, value):
self.filespec[key] = value
def add(self):
self.bug.dt.tracker.add_filespec(self.filespec)
def delete(self):
self.bug.dt.tracker.delete_filespec(self.filespec)
def name(self):
return self.filespec['filespec']
# 6. TRACKER INTERFACE
#
# The dt_tracker class implements a generic interface between the
# replicator and the Tracker defect tracker [GDR 2000-10-16, 7.1].
# Some configuration can be done by passing a configuration hash to the
# constructor; for more advanced configuration you should subclass this
# and replace some of the methods.
class dt_tracker(dt_interface.defect_tracker):
rid = None
sid = None
tracker = None
def __init__(self, config):
self.config = config
self.rid = config.rid
self.sid = config.sid
self.tracker = config.trk
def log(self, msg, args = ()):
if not isinstance(msg, message.message):
msg = catalog.msg(msg, args)
self.config.logger.log(msg)
def all_issues(self):
bugs = self.tracker.all_bugs_since(self.config.start_date)
return map(lambda bug,dt=self: tracker_issue(bug,dt), bugs)
def poll_start(self):
self.tracker.login(self.config.tracker_user, self.config.tracker_password)
def poll_end(self):
self.tracker.logout()
def changed_entities(self):
# Find all changed entitities since last replication
marker = self.tracker.load_marker()
bugs = self.tracker.changed_bugs_since(marker)
return (map(lambda bug,dt=self: tracker_issue(bug,dt), bugs),
{}, # changed changelists
marker)
def mark_changes_done(self, marker):
# save marker for next time
last_tran_id = self.tracker.last_transaction_id()
self.tracker.save_marker(last_tran_id)
def init(self):
# ensure that tracker.replication is valid even outside a
# replication cycle, so that all_issues() works.
self.tracker.first_replication(self.config.start_date)
# Supported features; see [GDR 2000-10-16, 3.5].
feature = {
'filespecs': 0,
'fixes': 1, # link to p4web to browse repository
'migrate_issues': 0,
'new_issues': 0,
'new_users': 0,
}
def supports(self, feature):
return self.feature.get(feature, 0)
def issue(self, bug_id):
bug = self.tracker.bug_from_bug_id(int(bug_id))
return tracker_issue(bug, self)
def replicate_changelist(self, change, client, date, description,
status, user):
return 0 # No easy place for Tracker to store changelist info
## dt_changelists = self.tracker.changelists(change)
## if len(dt_changelists) == 0:
## # no existing changelist; make a new one
## dt_changelist={}
## self.transform_changelist(dt_changelist, change, client,
## date, description, status, user)
## self.tracker.add_changelist(dt_changelist)
## return 1
## else: # determine the changes
## changes = self.transform_changelist(dt_changelists[0],
## change, client, date,
## description, status,
## user)
## if changes:
## self.tracker.update_changelist(changes, change)
## return 1
## else:
## return 0
def transform_changelist(self, dt_changelist,
change, client, date, description,
status, user):
changes = {}
changes['changelist'] = change
changes['client'] = client
changes['p4date'] = date
changes['description'] = description
changes['flags'] = (status == 'submitted')
changes['user'] = user
for key, value in changes.items():
if (not dt_changelist.has_key(key)
or dt_changelist[key] != value):
dt_changelist[key] = value
else:
del changes[key]
return changes
# 7. TRANSLATORS
#
# These classes translate values of particular types between Tracker
# and Perforce [GDR 2000-10-16, 7.5].
# 7.1. State translator
#
# This class translates bug statuses [GDR 2000-10-16, 7.5.2].
class status_translator(translator.translator):
# A map from Tracker status name to Perforce status name.
status_tr_to_p4 = {}
# A map from Perforce status name to Tracker status name (the
# reverse of the above map).
status_p4_to_tr = {}
def __init__(self, statuses):
# Compute the maps.
for tr_status, p4_status in statuses:
assert isinstance(tr_status, types.StringType)
assert isinstance(p4_status, types.StringType)
self.status_tr_to_p4[tr_status] = p4_status
self.status_p4_to_tr[p4_status] = tr_status
def translate_0_to_1(self, tr_status, tr, p4, issue=None, job=None):
assert isinstance(tr_status, types.StringType)
if self.status_tr_to_p4.has_key(tr_status):
return self.status_tr_to_p4[tr_status]
else:
# "No Perforce status corresponding to Tracker status '%s'."
raise error, catalog.msg(1209, tr_status)
def translate_1_to_0(self, p4_status, tr, p4, issue=None, job=None):
assert isinstance(p4_status, types.StringType)
if self.status_p4_to_tr.has_key(p4_status):
return self.status_p4_to_tr[p4_status]
else:
# "No Tracker status corresponding to Perforce status '%s'."
raise error, catalog.msg(1210, p4_status)
# 7.2. Enumerated field translator
#
# This class translates values in enumerated fields. Because enumerated
# fields in Tracker are mapped to select fields in Perforce, we have to
# translate the value using the keyword translator [GDR 2000-10-16,
# 7.5.2] so that it is valid in Perforce.
class enum_translator(translator.translator):
keyword_translator = None
def __init__(self, keyword_translator):
self.keyword_translator = keyword_translator
def translate_0_to_1(self, tr_enum,
tr = None, p4 = None,
issue = None, job = None):
assert isinstance(tr_enum, types.StringType)
if tr_enum == '':
return 'NONE'
else:
return self.keyword_translator.translate_0_to_1(tr_enum)
def translate_1_to_0(self, p4_enum,
tr = None, p4 = None,
issue = None, job = None):
if p4_enum == 'NONE':
return ''
else:
return self.keyword_translator.translate_1_to_0(p4_enum)
# 7.3. Date translator
#
# The date_translator class translates date fields between defect
# trackers Tracker (0) and Perforce (1) [GDR 2000-10-16, 7.5.1].
#
# Some Perforce dates are reported in the form 2000/01/01 00:00:00
# (e.g., dates in changeslists) and others are reported as seconds since
# 1970-01-01 00:00:00 (e.g., dates in fixes). I don't know why this is,
# but I have to deal with it by checking for both formats.
#
# MySQL datetime values are in the form 'YYYY-MM-DD hh:mm:ss'.
#
# Note that we deliberately prevent MySQLdb from using DateTime types
# for datetime values (see job000193, configure_tracker.py). Maybe one
# day that will change.
class date_translator(translator.translator):
p4_date_regexps = [
re.compile("^([0-9][0-9][0-9][0-9])/([0-9][0-9])/([0-9][0-9]) "
"([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$"),
re.compile("^[0-9]+$")
]
tr_date_regexp = re.compile(
"^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9]) "
"([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$")
def translate_0_to_1(self, tr_date, tr, p4, issue=None, job=None):
assert isinstance(tr_date, types.StringType)
assert isinstance(tr, dt_tracker)
assert isinstance(p4, dt_interface.defect_tracker)
assert issue == None or isinstance(issue, tracker_issue)
match = self.tr_date_regexp.match(tr_date)
if match:
return ('%s/%s/%s %s:%s:%s' %
(match.group(1), match.group(2), match.group(3),
match.group(4), match.group(5), match.group(6)))
else:
return ''
def translate_1_to_0(self, p4_date, tr, p4, issue=None, job=None):
assert isinstance(p4_date, types.StringType)
assert isinstance(tr, dt_tracker)
assert isinstance(p4, dt_interface.defect_tracker)
assert issue == None or isinstance(issue, tracker_issue)
match = self.p4_date_regexps[0].match(p4_date)
if match:
return ('%s-%s-%s %s:%s:%s' %
(match.group(1), match.group(2), match.group(3),
match.group(4), match.group(5), match.group(6)))
elif self.p4_date_regexps[1].match(p4_date):
return time.strftime("%Y-%m-%d %H:%M:%S",
time.gmtime(int(p4_date)))
else:
return '' # becomes 0000-00-00 00:00:00 on insertion
# 7.4. Timestamp translator
#
# The timestamp_translator class translates timestamp fields between
# defect trackers Tracker (0) and Perforce (1).
#
# Some Perforce dates are reported in the form 2000/01/01 00:00:00
# (e.g., dates in changeslists) and others are reported as seconds since
# 1970-01-01 00:00:00 (e.g., dates in fixes). I don't know why this is,
# but I have to deal with it by checking for both formats.
#
# MySQL timestamps are YYYYMMDDhhmmss.
#
# If there's nothing in Perforce, translate to '', which will get set
# to now() in MySQL.
class timestamp_translator(translator.translator):
p4_date_regexps = [
re.compile("^([0-9][0-9][0-9][0-9])/([0-9][0-9])/([0-9][0-9]) "
"([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$"),
re.compile("^[0-9]+$")
]
tr_timestamp_regexp = re.compile(
"^([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])"
"([0-9][0-9])([0-9][0-9])([0-9][0-9])$")
def translate_0_to_1(self, tr_date, tr, p4, issue=None, job=None):
assert isinstance(tr_date, types.StringType)
assert isinstance(tr, dt_tracker)
assert isinstance(p4, dt_interface.defect_tracker)
assert issue == None or isinstance(issue, tracker_issue)
match = self.tr_timestamp_regexp.match(tr_date)
if match:
return ('%s/%s/%s %s:%s:%s' %
(match.group(1), match.group(2), match.group(3),
match.group(4), match.group(5), match.group(6)))
else:
return ''
def translate_1_to_0(self, p4_date, tr, p4, issue=None, job=None):
assert isinstance(p4_date, types.StringType)
assert isinstance(tr, dt_tracker)
assert isinstance(p4, dt_interface.defect_tracker)
assert issue == None or isinstance(issue, tracker_issue)
match = self.p4_date_regexps[0].match(p4_date)
if match:
return ('%s%s%s%s%s%s' %
(match.group(1), match.group(2), match.group(3),
match.group(4), match.group(5), match.group(6)))
elif self.p4_date_regexps[1].match(p4_date):
return time.strftime("%Y%m%d%H%M%S",
time.gmtime(int(p4_date)))
else:
return ''
# 7.6. Text translator
#
# The text_translator class translates multi-line text fields between
# defect trackers Tracker (0) and Perforce (1) [GDR 2000-10-16, 7.5.3].
class text_translator(translator.translator):
# Transform Tracker text field contents to Perforce text field
def translate_0_to_1(self, str, tr, p4, issue=None, job=None):
assert isinstance(str, types.StringType)
# Convert and chomp trailing newlines
str = string.replace(str, '\r\n', '\n')
if str and str[-1] <> '\n':
str = str + '\n'
return str
# Transform Perforce text field contents to Tracker text field
# contents by removing a line ending.
def translate_1_to_0(self, str, tr, p4, issue=None, job=None):
assert isinstance(str, types.StringType)
# Remove final newline (if any).
if len(str) > 1 and str[-1] == '\n' and str[-2] == '\n':
str = str[:-1]
if len(str) > 0 and str[-1] <> '\n':
str = str + '\n'
str = string.replace(str, '\n', '\r\n')
return str
# 7.6.a Text line translator
#
# The text_translator class translates single-line text fields between
# defect trackers Tracker (0) and Perforce (1).
class text_line_translator(translator.translator):
# Transform text field contents by removing any newline.
def remove_newline(self, str):
if str and str[-1] == '\n':
str = str[:-1]
return str
def translate_0_to_1(self, tr_string, tr, p4, issue=None, job=None):
assert isinstance(tr_string, types.StringType)
return self.remove_newline(tr_string)
def translate_1_to_0(self, p4_string, tr, p4, issue=None, job=None):
assert isinstance(p4_string, types.StringType)
return self.remove_newline(p4_string)
# 7.7. Integer translator
#
# The int_translator class translates integer fields between defect
# trackers Tracker (0) and Perforce (1).
class int_translator(translator.translator):
# Transform Tracker integer field contents to Perforce word field
# contents by converting line endings.
def translate_0_to_1(self, tr_int, tr, p4, issue=None, job=None):
assert (isinstance(tr_int, types.IntType)
or isinstance(tr_int, types.LongType))
s = str(tr_int)
# Note that there's a difference between Python 1.5.2 and Python
# 1.6 here, in whether str of a long ends in an L.
if s[-1:] == 'L':
s = s[:-1]
return s
# Transform Perforce word field contents to Tracker integer field
# contents.
def translate_1_to_0(self, p4_string, tr, p4, issue=None, job=None):
assert isinstance(p4_string, types.StringType)
try:
if p4_string == '':
return 0L
else:
return long(p4_string)
except:
# "Perforce field value '%s' could not be translated to a
# number for replication to Tracker."
raise error, catalog.msg(1211, p4_string)
# 7.7. User translator
#
# The user_translator class translates user fields between defect
# trackers Tracker (0) and Perforce (1) [GDR 2000-10-16, 7.5.3].
#
# A Perforce user field contains a Perforce user name (for example,
# "nb"). The Perforce user record contains an e-mail address (for
# example, "nb@ravenbrook.com").
#
# A Tracker user field contains name (for example "robertc"),
# The Tracker user record contains an e-mail address (for example,
# "rc@vaccaperna.co.uk").
#
# To translate a user field, we find an identical e-mail address.
#
# If there is no such Perforce user, we just use the e-mail address,
# because we can (in fact) put any string into a Perforce user field.
#
# If there is no such Tracker user, we check whether the Perforce user
# field is in fact the e-mail address of a Tracker user (for example,
# one that we put there because there wasn't a matching Perforce user).
# If so, we use that Tracker user.
#
# Sometimes, a Perforce user field which cannot be translated into
# Tracker is an error. For instance, if a Perforce user sets the
# qa_contact field of a job to a nonsense value, we should catch that
# and report it as an error.
#
# Sometimes, however, we should allow such values. For instance, when
# translating the user field of a fix record or changelist: we should
# not require _all_ past and present Perforce users to have Tracker
# user records. In that case, we should translate to a default value.
# For this purpose, the replicator has a Tracker user of its own.
#
# To distinguish between these two cases, we have a switch
# 'allow_unknown'. If allow_unknown is 1, we use the default
# translation. If allow_unknown is 0, we report an error.
class user_translator(translator.user_translator):
# A map from Tracker user ids to Perforce user names
user_tr_to_p4 = {}
# A map from Perforce user names to Tracker user ids
user_p4_to_tr = {}
# A map from Tracker user ids to (downcased) email addresses
tr_id_to_email = None
# A map from (downcased) email addresses to Tracker user ids
tr_email_to_id = None
# A map from (downcased) email addresses to Perforce user names
p4_email_to_user = None
# A map from Perforce user names to (downcased) email addresses
p4_user_to_email = None
# A map from Perforce user names to Perforce full names.
p4_user_to_fullname = None
# A map from Perforce user name to email address for users with
# duplicate email addresses in Perforce.
p4_duplicates = None
# A map from Tracker user id to email address for users with
# duplicate (downcased) email addresses in Tracker.
tr_duplicates = None
# A map from Perforce user name to email address for Perforce
# users that can't be matched with users in Tracker.
p4_unmatched = None
# A map from Tracker user real name to email address for Tracker
# users that can't be matched with users in Perforce.
tr_unmatched = None
# The Tracker P4DTI user email address (config.replicator_address)
tracker_user = None
# The Tracker P4DTI user id
tracker_id = None
# The Perforce P4DTI user name (config.p4_user)
p4_user = None
# A switch controlling whether this translator will translate
# Perforce users without corresponding Tracker users into
# the Tracker P4DTI user id.
allow_unknown = 0
def __init__(self, tracker_user, p4_user,
allow_unknown = 0):
self.tracker_user = string.lower(tracker_user)
self.p4_user = p4_user
self.allow_unknown = allow_unknown
# Deduce and record the mapping between Tracker userid and
# Perforce username.
def init_users(self, tr, p4):
# Clear the maps.
self.user_tr_to_p4 = {}
self.user_p4_to_tr = {}
self.tr_email_to_id = {}
self.tr_id_to_email = {}
self.p4_email_to_user = {}
self.p4_user_to_email = {}
self.p4_duplicates = {}
self.tr_duplicates = {}
self.p4_unmatched = {}
self.tr_unmatched = {}
self.p4_user_to_fullname = {}
# Populate the Perforce-side maps.
p4_users = p4.p4.run("users")
for u in p4_users:
email = string.lower(u['Email'])
user = u['User']
self.p4_user_to_fullname[user] = u['FullName']
self.p4_user_to_email[user] = email
if self.p4_email_to_user.has_key(email):
matching_user = self.p4_email_to_user[email]
# "Perforce users '%s' and '%s' both have email address
# '%s' (when converted to lower case)."
tr.log(1241, (user, matching_user, email))
self.p4_duplicates[matching_user] = email
self.p4_duplicates[user] = email
else:
self.p4_email_to_user[email] = user
# Check the Perforce P4DTI user exists:
if not self.p4_user_to_email.has_key(self.p4_user):
# "Perforce P4DTI user '%s' is not a known Perforce user."
raise error, catalog.msg(1242, self.p4_user)
p4_email = self.p4_user_to_email[self.p4_user]
# Check that the Perforce P4DTI user has a unique email address:
if self.p4_duplicates.has_key(self.p4_user):
duplicate_users = []
for (user, email) in self.p4_duplicates.items():
if email == p4_email and user != self.p4_user:
duplicate_users.append(user)
# "Perforce P4DTI user '%s' has the same e-mail address
# '%s' as these other Perforce users: %s."
raise error, catalog.msg(1243,
(self.p4_user,
p4_email,
duplicate_users))
# Make a list of all the user ids matching the Tracker P4DTI user.
tracker_ids = []
# Populate the Tracker-side maps.
tr_users = tr.tracker.user_id_and_email_list()
for (id, email) in tr_users:
email = string.lower(email)
self.tr_id_to_email[id] = email
# Collect ids matching the Tracker P4DTI user
if email == self.tracker_user:
tracker_ids.append(id)
if self.tr_email_to_id.has_key(email):
matching_id = self.tr_email_to_id[email]
tr_real_name = tr.tracker.real_name_from_userid(id)
matching_real_name = tr.tracker.real_name_from_userid(matching_id)
# "Tracker users '%s' and '%s' both have email address
# '%s' (when converted to lower case)."
tr.log(1244, (tr_real_name, matching_real_name, email))
self.tr_duplicates[tr_real_name] = email
self.tr_duplicates[matching_real_name] = email
else:
self.tr_email_to_id[email] = id
# Check that the Tracker P4DTI user exists:
if len(tracker_ids) == 0:
# "Tracker P4DTI user '%s' is not a known Tracker user."
raise error, catalog.msg(1213, self.tracker_user)
# Check that the Tracker P4DTI user is unique:
if len(tracker_ids) > 1:
# "Tracker P4DTI user e-mail address '%s' belongs to
# several Tracker users: %s."
raise error, catalog.msg(1245, (self.tracker_user, tracker_ids))
# There can be only one.
self.tracker_id = tracker_ids[0]
# The Perforce-specific and Tracker-specific maps are now
# complete. Note that the p4_user_to_email map and the
# tr_id_to_email map may have duplicate values (in which case
# the keys in the inverse maps are the first-seen
# corresponding keys).
# Populate the translation maps.
# We could do this at the same time as one of the previous phases,
# but IMO it's cleaner to separate it out like this.
for (id, email) in self.tr_id_to_email.items():
if self.p4_email_to_user.has_key(email):
p4_user = self.p4_email_to_user[email]
# Already matched?
if self.user_p4_to_tr.has_key(p4_user):
matching_id = self.user_p4_to_tr[p4_user]
tr_real_name = tr.tracker.real_name_from_userid(id)
self.tr_unmatched[tr_real_name] = email
# "Tracker user '%s' (e-mail address '%s') not
# matched to any Perforce user, because Perforce
# user '%s' already matched to Tracker user %d."
tr.log(1246,
(tr_real_name, email, p4_user, matching_id))
else:
self.user_tr_to_p4[id] = p4_user
self.user_p4_to_tr[p4_user] = id
# "Tracker user %d matched to Perforce user '%s' by
# e-mail address '%s'."
tr.log(1247, (id, p4_user, email))
else:
tr_real_name = tr.tracker.real_name_from_userid(id)
self.tr_unmatched[tr_real_name] = email
# "Tracker user '%s' (e-mail address '%s') not matched
# to any Perforce user."
tr.log(1248, (tr_real_name, email))
# Identify unmatched Perforce users.
for (user, email) in self.p4_user_to_email.items():
if not self.user_p4_to_tr.has_key(user):
self.p4_unmatched[user] = email
# "Perforce user '%s' (e-mail address '%s') not matched
# to any Tracker user."
tr.log(1249, (user, email))
# Ensure that Tracker P4DTI user and Perforce P4DTI user
# correspond.
if self.user_tr_to_p4.has_key(self.tracker_id):
# Tracker P4DTI user has P4 counterpart:
p4_tracker_user = self.user_tr_to_p4[self.tracker_id]
# But is it the p4_user?
if (p4_tracker_user != self.p4_user):
# "Tracker P4DTI user '%s' has e-mail address
# matching Perforce user '%s', not Perforce P4DTI
# user '%s'."
raise error, catalog.msg(1212,
(self.tracker_user,
p4_tracker_user,
self.p4_user))
else:
if self.user_p4_to_tr.has_key(self.p4_user):
tr_user = self.user_p4_to_tr[self.p4_user]
tr_email = self.tr_id_to_email[tr_user]
# "Tracker P4DTI user '%s' does not have a matching
# Perforce user. It should match the Perforce user
# '%s' but that matches the Tracker user %d (e-mail
# address '%s')."
raise error, catalog.msg(1250, (self.tracker_user,
self.p4_user,
tr_user,
tr_email))
else:
# "Tracker P4DTI user '%s' does not have a matching
# Perforce user. It should match the Perforce user
# '%s' (which has e-mail address '%s')."
raise error, catalog.msg(1251, (self.tracker_user,
self.p4_user,
p4_email))
# always translate 0 to 'None' and back again
self.user_p4_to_tr['None'] = 0
self.user_tr_to_p4[0] = 'None'
def unmatched_users(self, tr, p4):
self.init_users(tr, p4)
# "A user field containing one of these users will be translated
# to the user's e-mail address in the corresponding Perforce job
# field."
tr_user_msg = catalog.msg(515)
# "It will not be possible to use Perforce to assign bugs to
# these users. Changes to jobs made by these users will be
# ascribed in Tracker to the replicator user <%s>."
p4_user_msg = catalog.msg(516, self.tracker_user)
# "These Perforce users have duplicate e-mail addresses. They
# may have been matched with the wrong Tracker user."
duplicate_p4_user_msg = catalog.msg(536)
# "These Tracker users have duplicate e-mail addresses (when
# converted to lower case). They may have been matched with
# the wrong Perforce user."
duplicate_tr_user_msg = catalog.msg(552)
return (self.tr_unmatched, self.p4_unmatched,
tr_user_msg, p4_user_msg,
self.tr_duplicates, self.p4_duplicates,
duplicate_tr_user_msg, duplicate_p4_user_msg)
keyword = translator.keyword_translator()
def translate_1_to_0(self, p4_user, tr, p4, issue=None, job=None):
if not self.user_p4_to_tr.has_key(p4_user):
self.init_users(tr, p4)
if self.user_p4_to_tr.has_key(p4_user):
return self.user_p4_to_tr[p4_user]
else:
tr_email = self.keyword.translate_1_to_0(p4_user)
if self.tr_email_to_id.has_key(tr_email):
return self.tr_email_to_id[tr_email]
elif self.allow_unknown:
if p4_user == "Unassigned":
return ''
else:
return self.tracker_id
else:
# "There is no Tracker user corresponding to Perforce
# user '%s'."
raise error, catalog.msg(1214, p4_user)
def translate_0_to_1(self, tr_user, tr, p4, issue=None, job=None):
if not self.user_tr_to_p4.has_key(tr_user):
self.init_users(tr, p4)
if self.user_tr_to_p4.has_key(tr_user):
return self.user_tr_to_p4[tr_user]
else:
if tr_user == '':
return "Unassigned"
else:
if self.tr_id_to_email.has_key(tr_user):
tr_email = self.tr_id_to_email[tr_user]
else:
tr_email = tr_user + "@actix.com"
return self.keyword.translate_0_to_1(tr_email)
# 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
#
# 2003-11-20 RHGC Created.
#
#
# C. COPYRIGHT AND LICENCE
#
# This file is copyright (c) 2003 Vaccaperna Systems Ltd. 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/dt_tracker.py#2 $