# Perforce Defect Tracking Integration Project
#
#
# 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
from ctypes import *
# 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',
'Assigned To',
'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)
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):
# Ignore read-only fields - let them wither away!
if not (key in self.dt.config.read_only_fields):
changes_bug[key] = value
else:
# "Updating non-existent Tracker field '%s'."
raise error, catalog.msg(1200, key)
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, bug_list=[]):
if len(bug_list) > 0:
bugs = self.tracker.specific_bugs(bug_list)
else:
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):
# update and save marker for next time
self.tracker.save_marker()
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_dt_to_p4 = {}
# A map from Perforce status name to Tracker status name (the
# reverse of the above map).
status_p4_to_dt = {}
def __init__(self, statuses):
# Compute the maps.
for dt_status, p4_status in statuses:
assert isinstance(dt_status, types.StringType)
if p4_status <> None:
assert isinstance(p4_status, types.StringType)
self.status_dt_to_p4[dt_status] = p4_status
# Allow for multiple Tracker status to map to one Perforce one
# just use the first one.
if not self.status_p4_to_dt.has_key(p4_status):
self.status_p4_to_dt[p4_status] = dt_status
def translate_dt_to_p4(self, dt_status, dt, p4, issue=None, job=None):
assert isinstance(dt_status, types.StringType)
if self.status_dt_to_p4.has_key(dt_status):
return self.status_dt_to_p4[dt_status]
else:
# "No Perforce status corresponding to Tracker status '%s'."
raise error, catalog.msg(1209, dt_status)
def translate_p4_to_dt(self, p4_status, dt, p4, issue=None, job=None):
assert isinstance(p4_status, types.StringType)
if self.status_p4_to_dt.has_key(p4_status):
return self.status_p4_to_dt[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_dt_to_p4(self, dt_enum,
dt = None, p4 = None,
issue = None, job = None):
assert isinstance(dt_enum, types.StringType)
if dt_enum == '':
return 'NONE'
else:
return self.keyword_translator.translate_dt_to_p4(dt_enum)
def translate_p4_to_dt(self, p4_enum,
dt = None, p4 = None,
issue = None, job = None):
if p4_enum == 'NONE':
return ''
else:
return self.keyword_translator.translate_p4_to_dt(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.
#
# Tracker datetime values are controlled by registry settings.
#
# Structure used for time functions
class SYSTEMTIME(Structure):
_fields_ = [("wYear", c_ushort),
("wMonth", c_ushort),
("wDayOfWeek", c_ushort),
("wDay", c_ushort),
("wHour", c_ushort),
("wMinute", c_ushort),
("wSecond", c_ushort),
("wMilliseconds", c_ushort)]
class date_translator(translator.translator):
LOCALE_USER_DEFAULT = 0x800L
LOCALE_SYSTEM_DEFAULT = 0x400L
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]+$")
]
def __init__(self, keys=None):
self.GetTimeFormat = windll.kernel32.GetTimeFormatA
self.GetDateFormat = windll.kernel32.GetDateFormatA
self.from_registry = 0
if keys == None:
self.from_registry = 1
import _winreg
conn = _winreg.ConnectRegistry(None, _winreg.HKEY_CURRENT_USER)
hkey = _winreg.OpenKey(conn, r"Control Panel\International")
keys = {}
for k in ["sShortDate", "sTimeFormat", "iTime", "sTime", "sDate", "s1159", "s2359"]:
keys[k] = _winreg.QueryValueEx(hkey, k)[0]
_winreg.CloseKey(hkey)
self.sShortDate = keys["sShortDate"] # Date format, e.g. dd/MM/yyyy
self.sTimeFormat = keys["sTimeFormat"] # Time format, e.g. H:MM:ss
self.iTime = keys["iTime"] # 0 = 12 hour, 1 = 24 hour format
self.use_am_pm = self.iTime[0] == '0'
self.sTime = keys["sTime"] # time seperator (usually ":")
self.sDate = keys["sDate"] # Date seperator (usually "/")
self.s1159 = keys["s1159"] # AM indicator (if required)
self.s2359 = keys["s2359"] # PM indicator (if required)
date_str = "^([0-9]+)%s([0-9]+)%s([0-9]+) ([0-9]+)%s([0-9]+)%s([0-9]+)" % (
self.sDate, self.sDate, self.sTime, self.sTime)
if self.use_am_pm:
date_str += " ([%s%s]+)" % (self.s1159, self.s2359)
self.dt_date_str = date_str + "$"
self.dt_date_regexp = re.compile(self.dt_date_str)
# Work out index of d/m/y
match = re.match("([dmy]+)%s([dmy]+)%s([dmy]+)" % (self.sDate, self.sDate),
self.sShortDate, re.IGNORECASE)
assert match
assert len(match.groups()) == 3
g = match.groups()
for i in range(3):
if g[i][0] in ['y', 'Y']:
self.ind_year = i + 1 # Note used as 1 based index later
elif g[i][0] == 'M':
self.ind_month = i + 1
else:
self.ind_day = i + 1
def translate_dt_to_p4(self, dt_date, dt, p4, issue=None, job=None):
assert isinstance(dt_date, types.StringType)
assert isinstance(dt, dt_tracker)
assert isinstance(p4, dt_interface.defect_tracker)
assert issue == None or isinstance(issue, tracker_issue)
match = self.dt_date_regexp.match(dt_date)
if match:
hour = int(match.group(4))
if self.iTime[0] == '0' and match.group(7) == self.s2359 and hour < 12:
hour += 12
return ('%s/%02d/%02d %02d:%02d:%02d' %
(match.group(self.ind_year), int(match.group(self.ind_month)),
int(match.group(self.ind_day)),
hour, int(match.group(5)), int(match.group(6))))
else:
return ''
def format_tm(self, tm):
# Formats a time value using windows functions which use regional settings appropriately
assert type(tm) == time.struct_time
buf = create_string_buffer(128)
dt = SYSTEMTIME()
dt.wYear = tm[0]
dt.wMonth = tm[1]
dt.wDayOfWeek = 0
dt.wDay = tm[2]
dt.wHour = tm[3]
dt.wMinute = tm[4]
dt.wSecond = tm[5]
dt.wMilliseconds = 0
if self.from_registry:
ret = self.GetDateFormat(self.LOCALE_SYSTEM_DEFAULT, 0, byref(dt), 0, buf, 64)
else:
format = c_char_p(self.sShortDate)
ret = self.GetDateFormat(self.LOCALE_SYSTEM_DEFAULT, 0, byref(dt), format, buf, 64)
assert ret != 0
result = buf.value
if self.from_registry:
ret = self.GetTimeFormat(self.LOCALE_SYSTEM_DEFAULT, 0, byref(dt), 0, buf, 64)
else:
format = c_char_p(self.sTimeFormat)
ret = self.GetTimeFormat(self.LOCALE_SYSTEM_DEFAULT, 0, byref(dt), format, buf, 64)
assert ret != 0
result += " " + buf.value
return result
def translate_time_to_dt(self, tm):
# Formats a time value like time.gmtime()
assert type(tm) == time.struct_time
return self.format_tm(tm)
def translate_dt_to_secs(self, dt_date):
assert isinstance(dt_date, types.StringType)
match = self.dt_date_regexp.match(dt_date)
if match:
hour = int(match.group(4))
if self.iTime[0] == '0' and match.group(7) == self.s2359 and hour < 12:
hour += 12
tm = [match.group(self.ind_year), match.group(self.ind_month),
match.group(self.ind_day),
str(hour), match.group(5), match.group(6)]
tm = map(int, tm)
tm = time.mktime(tuple(tm + [0, 0, -1]))
return time.localtime(tm)
def translate_p4_to_dt(self, p4_date, dt, p4, issue=None, job=None):
assert isinstance(p4_date, types.StringType)
assert isinstance(dt, 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:
tm = list(match.groups()[:6])
tm = map(int, tm)
tm = time.mktime(tuple(tm + [0, 0, -1]))
return self.format_tm(time.localtime(tm))
elif self.p4_date_regexps[1].match(p4_date):
return self.format_tm(time.localtime(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_dt_to_p4(self, str, dt, 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_p4_to_dt(self, str, dt, 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_dt_to_p4(self, dt_string, dt, p4, issue=None, job=None):
assert isinstance(dt_string, types.StringType)
str = string.replace(dt_string, '#', '|') # Disallowed chars
str = string.replace(dt_string, '"', '~') # Disallowed chars
return self.remove_newline(str)
def translate_p4_to_dt(self, p4_string, dt, 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_dt_to_p4(self, dt_int, dt, p4, issue=None, job=None):
assert (isinstance(dt_int, types.IntType)
or isinstance(dt_int, types.LongType))
s = str(dt_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_p4_to_dt(self, p4_string, dt, 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_dt_to_p4 = {}
# A map from Perforce user names to Tracker user ids
user_p4_to_dt = {}
# A map from Tracker user ids to (downcased) email addresses
dt_id_to_email = None
# A map from Tracker user ids to fullnames
dt_id_to_fullname = None
# A map from (downcased) email addresses to Tracker user ids
dt_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.
dt_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.
dt_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, dt, p4):
# Clear the maps.
self.user_dt_to_p4 = {}
self.user_p4_to_dt = {}
self.dt_email_to_id = {}
self.dt_id_to_email = {}
self.dt_id_to_fullname = {}
self.p4_email_to_user = {}
self.p4_user_to_email = {}
self.p4_duplicates = {}
self.dt_duplicates = {}
self.p4_unmatched = {}
self.dt_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)."
dt.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.
dt_users = dt.tracker.user_id_and_email_list()
for (id, email, fullname) in dt_users:
email = string.lower(email)
if len(email) == 0:
email = id + "@" + dt.config.email_domain
email = re.sub(' ', '_', email)
self.dt_id_to_email[id] = email
self.dt_id_to_fullname[id] = fullname
# Collect ids matching the Tracker P4DTI user
if email == self.tracker_user:
tracker_ids.append(id)
if self.dt_email_to_id.has_key(email):
matching_id = self.dt_email_to_id[email]
dt_real_name = fullname
matching_real_name = self.dt_id_to_fullname[matching_id]
# "Tracker users '%s' and '%s' both have email address
# '%s' (when converted to lower case)."
dt.log(1244, (dt_real_name, matching_real_name, email))
self.dt_duplicates[dt_real_name] = email
self.dt_duplicates[matching_real_name] = email
else:
self.dt_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
# dt_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.dt_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_dt.has_key(p4_user):
matching_id = self.user_p4_to_dt[p4_user]
dt_real_name = self.dt_id_to_fullname[id]
self.dt_unmatched[dt_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."
dt.log(1246,
(dt_real_name, email, p4_user, matching_id))
else:
self.user_dt_to_p4[id] = p4_user
self.user_p4_to_dt[p4_user] = id
# "Tracker user %d matched to Perforce user '%s' by
# e-mail address '%s'."
dt.log(1247, (id, p4_user, email))
else:
dt_real_name = self.dt_id_to_fullname[id]
self.dt_unmatched[dt_real_name] = email
# "Tracker user '%s' (e-mail address '%s') not matched
# to any Perforce user."
dt.log(1248, (dt_real_name, email))
# Identify unmatched Perforce users.
for (user, email) in self.p4_user_to_email.items():
if not self.user_p4_to_dt.has_key(user):
self.p4_unmatched[user] = email
# "Perforce user '%s' (e-mail address '%s') not matched
# to any Tracker user."
dt.log(1249, (user, email))
# Ensure that Tracker P4DTI user and Perforce P4DTI user
# correspond.
if self.user_dt_to_p4.has_key(self.tracker_id):
# Tracker P4DTI user has P4 counterpart:
p4_tracker_user = self.user_dt_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_dt.has_key(self.p4_user):
dt_user = self.user_p4_to_dt[self.p4_user]
dt_email = self.dt_id_to_email[dt_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,
dt_user,
dt_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_dt['None'] = 0
self.user_dt_to_p4[0] = 'None'
def unmatched_users(self, dt, p4):
self.init_users(dt, 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."
dt_user_msg = catalog.msg(1215)
# "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(1216, 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(1236)
# "These Tracker users have duplicate e-mail addresses (when
# converted to lower case). They may have been matched with
# the wrong Perforce user."
duplicate_dt_user_msg = catalog.msg(1252)
return (self.dt_unmatched, self.p4_unmatched,
dt_user_msg, p4_user_msg,
self.dt_duplicates, self.p4_duplicates,
duplicate_dt_user_msg, duplicate_p4_user_msg)
keyword = translator.keyword_translator()
def translate_p4_to_dt(self, p4_user, dt, p4, issue=None, job=None):
if not self.user_p4_to_dt.has_key(p4_user):
self.init_users(dt, p4)
if self.user_p4_to_dt.has_key(p4_user):
return self.user_p4_to_dt[p4_user]
else:
dt_email = self.keyword.translate_p4_to_dt(p4_user)
if self.dt_email_to_id.has_key(dt_email):
return self.dt_email_to_id[dt_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_dt_to_p4(self, dt_user, dt, p4, issue=None, job=None):
if not self.user_dt_to_p4.has_key(dt_user):
self.init_users(dt, p4)
if self.user_dt_to_p4.has_key(dt_user):
return re.sub(' ', '_', self.user_dt_to_p4[dt_user])
else:
if dt_user == '':
return "Unassigned"
else:
if self.dt_id_to_email.has_key(dt_user):
dt_email = self.dt_id_to_email[dt_user]
else:
dt_email = dt_user + "@" + dt.config.email_domain
return re.sub(' ', '_', dt_email)
# A. REFERENCES
#
# [GDR 2000-10-16] "Perforce Defect Tracking Integration Integrator's
# Guide"; Gareth Rees; Ravenbrook Limited; 2000-10-16;
# .
#
#
# 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 $