# 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 $