# dt_bugzilla.py -- defect tracking interface (Bugzilla). # Nicholas Barnes, Ravenbrook Limited, 2000-11-21. # # $Id: //info.ravenbrook.com/project/p4dti/branch/2000-11-14/bugzilla/code/replicator/dt_bugzilla.py#10 $ # # Copyright 2000 Ravenbrook Limited. This document is provided "as is", # without any express or implied warranty. In no event will the authors # be held liable for any damages arising from the use of this document. # You may make and distribute copies and derivative works of this # document provided that (1) you do not charge a fee for this document or # for its distribution, and (2) you retain as they appear all copyright # and licence notices and document history entries, and (3) you append # descriptions of your modifications to the document history. import replicator import re import socket import string import bugzilla import types error = 'P4DTI Bugzilla interface error' class bugzilla_bug(replicator.defect_tracker_issue): dt = None # The defect tracker this bug belongs to. bug = None # The dictionary representing the bugzilla bug. p4dti_bug = None # The dictionary representing the p4dti_bugs record. def __init__(self, bug, dt): self.dt = dt self.bug = bug self.p4dti_bug = self.dt.bugzilla.bug_p4dti_bug(bug) def __getitem__(self, key): if self.bug.has_key(key) : return self.bug[key] else : return self.p4dti_bug[key] def action(self): return self.p4dti_bug['action'] def add_filespec(self, filespec): raise error, "Trying to add a Bugzilla filespec." def add_fix(self, change, client, date, user, status): fix_record = {} fix_record['bug_id'] = bug_id fix_record['changelist'] = change fix_record['client'] = client fix_record['p4date'] = date fix_record['user'] = user fix_record['status'] = status fix = bugzilla_fix(self, self.bug['bug_id'], fix_record) fix.add() def id(self): return str(self.bug['bug_id']) def filespecs(self): return [] # Not currently implementing filespecs def fixes(self): fixes = [] for fix in self.dt.bugzilla.fixes_from_bug_id(self.bug['bug_id']) : fixes.append(bugzilla_fix(fix, self)) return fixes def readable_name(self): if (self.p4dti_bug != None) and (self.p4dti_bug.has_key('jobname')): return self.p4dti_bug['jobname'] else: return self.dt.config['jobname-function'](self.bug) def rid(self): if self.p4dti_bug == None : # not yet replicated return "" else: return self.p4dti_bug['rid'] def setup_for_replication(self): assert self.p4dti_bug == None self.p4dti_bug = {} self.p4dti_bug['bug_id'] = self.bug['bug_id'] self.p4dti_bug['jobname'] = self.readable_name() self.p4dti_bug['action'] = 'replicate' self.dt.bugzilla.add_p4dti_bug(self.p4dti_bug) def update(self, user, changes) : # should check permissions for this user to make # these changes here. changes_bug = {} changes_p4dti_bug = {} for key, value in changes.items() : if self.bug.has_key(key) : changes_bug[key] = value self.bug[key] = value elif self.p4dti_bug.has_key(key): changes_p4dti_bug[key] = value self.p4dti_bug[key] = value else: raise error, ("updating non-existent bug field '%s'" % key) self.dt.bugzilla.update_bug(changes_bug, self.bug['bug_id']) self.dt.bugzilla.update_p4dti_bug(changes_p4dti_bug, self.bug['bug_id']) def update_action(self, action): if self.p4dti_bug['action'] != action: self.p4dti_bug['action'] = action self.dt.bugzilla.update_p4dti_bug({'action' : action}, self.bug['bug_id']) class bugzilla_fix(replicator.defect_tracker_fix): bug = None # The Bugzilla bug to which the fix refers. fix = None # The dictionary representing the bugzilla fix record. def __init__(self, bug, bug_id, dict): self.bug = bug self.fix = dict def __getitem__(self, key): return self.fix[key] def __repr__(self): return repr(self.fix) def __setitem__(self, key, value): self.fix[key] = value def add(self): self.bug.dt.bugzilla.add_fix(self.fix) def change(self): return self.fix['changelist'] def delete(self): self.bug.dt.bugzilla.delete_fix(self.fix) def status(self): return self.fix['status'] def update(self, p4_fix): self.transform_from_p4(p4_fix) self.bug.dt.bugzilla.update_fix(self.fix, self.fix['bug_id'], self.fix['changelist']) # No filespecs at the moment, but we can do them easily enough by # filling out this code as for bugzilla_fix above. class bugzilla_filespec(replicator.defect_tracker_filespec): bug = None # The Bugzilla bug to which the filespec refers. filespec = None # The dictionary representing the filespec record. def __init__(self, filespec, bug): raise error, "Trying to create a Bugzilla filespec." def delete(self): raise error, "Trying to delete a Bugzilla filespec." def name(self): raise error, "Trying to get a Bugzilla filespec." # The dt_bugzilla class implements a generic interface between the # replicator and the Bugzilla defect tracker. 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_bugzilla(replicator.defect_tracker): config = { } rid = None sid = None bugzilla = None def __init__(self, rid, sid, config = {}): replicator.defect_tracker.__init__(self, rid, sid, config) self.bugzilla = bugzilla.bugzilla(config, rid, sid) self.bugzilla.create_p4dti_tables() def all_issues(self): bugs = self.bugzilla.all_bugs() return map(lambda bug,dt=self: bugzilla_bug(bug,dt), bugs) def changed_entities(self): replication = self.bugzilla.new_replication() last = self.bugzilla.latest_complete_replication() bugs = self.bugzilla.all_bugs_since(last) return ( map(lambda bug,dt=self: bugzilla_bug(bug,dt), bugs), { }, # changed changelists replication ) def mark_changes_done(self, replication): self.bugzilla.end_replication() def init(self): pass def issue(self, bug_id): bug = self.bugzilla.bug_from_bug_id(int(bug_id)) return bugzilla_bug(bug, self) def replicate_changelist(self, change, client, date, description, status, user): dt_changelists = self.bugzilla.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.bugzilla.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.bugzilla.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 dt_changelist[key] != value: dt_changelist[key] = value else: del changes[key] return changes class status_translator(replicator.translator): # A map from Bugzilla status name to Perforce status name. status_bz_to_p4 = { } # A map from Perforce status name to Bugzilla status name (the reverse of # the above map). status_p4_to_bz = { } def __init__(self, dts, statuses): # Call the superclass method. replicator.translator.__init__(self, dts) # Compute the maps. for bz_status, p4_status in statuses: assert isinstance(bz_status, types.StringType) assert isinstance(p4_status, types.StringType) self.status_bz_to_p4[bz_status] = p4_status self.status_p4_to_bz[p4_status] = bz_status def translate_0_to_1(self, bz_status, issues = None): assert isinstance(bz_status, types.StringType) if self.status_bz_to_p4.has_key(bz_status): return self.status_bz_to_p4[bz_status] else: raise error, ("No Perforce status corresponding to " "Bugzilla status '%s'" % bz_status) def translate_1_to_0(self, p4_status, issues = None): assert isinstance(p4_status, types.StringType) if self.status_p4_to_bz.has_key(p4_status): return self.status_p4_to_bz[p4_status] else: raise error, ("No Bugzilla status corresponding to " "Perforce status '%s'." % p4_status) class date_translator(replicator.translator): def translate_0_to_1(self, bz_date, issues = None): return bz_date def translate_1_to_0(self, p4_date, issues = None): return p4_date # The text_translator class translates multi-line text fields between defect # trackers Bugzilla (0) and Perforce (1). class text_translator(replicator.translator): # Transform Bugzilla text field contents to Perforce text field contents # by converting line endings. def translate_0_to_1(self, bz_string, issues = None): assert isinstance(bz_string, types.StringType) # Replace \r\n with \n. string = re.sub('\r\n', '\n', bz_string) # Add final newline, unless the string is empty. if bz_string: bz_string = bz_string + '\n' return bz_string # Transform Perforce text field contents to Bugzilla text field contents # by converting line endings. def translate_1_to_0(self, p4_string, issues = None): assert isinstance(p4_string, types.StringType) # Remove final newline (if any). if p4_string and p4_string[-1] == '\n': p4_string = p4_string[:-1] # Replace \n with \r\n. p4_string = re.sub('\n', '\r\n', p4_string) return p4_string class user_translator(replicator.translator): user_bz_to_p4 = { } user_p4_to_bz = { } bz_id_to_email = { } bz_email_to_id = { } p4 = None bugzilla_user = None p4_user = None def __init__(self, dts, p4, bugzilla_user, p4_user): # Call the superclass method. replicator.translator.__init__(self, dts) # Get data from the database. self.p4 = p4 self.bugzilla_user = bugzilla_user self.p4_user = p4_user self.init_users() # Obtain a dictionary email -> Perforce id. This function # does not belong in this module. def p4_user_dict(self) : p4_users = self.p4.run("users") dict={} for user in p4_users : dict[user['Email']] = user['User'] return dict # Deduce and record the mapping between Bugzilla userid and # Perforce username. def init_users(self): bugzilla_users = self.dts[0].bugzilla.user_id_and_email_list() p4_users = self.p4_user_dict() self.user_bz_to_p4={} self.user_p4_to_bz={} self.bz_email_to_id={} self.bz_id_to_email={} for id, email in bugzilla_users : self.bz_email_to_id[email] = id self.bz_id_to_email[id] = email if p4_users.has_key(email) : p4_user = p4_users[email] self.user_bz_to_p4[id] = p4_user self.user_p4_to_bz[p4_user] = id # if the bugzilla P4DTI user is in the table, # make sure it corresponds to the P4 P4DTI user. if self.bz_email_to_id.has_key(self.bugzilla_user) : bugzilla_id = self.bz_email_to_id[self.bugzilla_user] # special Bugzilla user is in Bugzilla if self.user_bz_to_p4.has_key(bugzilla_id) : # special Bugzilla user has P4 counterpart if (self.user_bz_to_p4[bugzilla_id] != self.p4_user) : raise error, ("Bugzilla P4DTI user '%s' has email address " "matching Perforce user '%s', not Perforce " "P4DTI user '%s'." % (self.bugzilla_user, self.user_bz_to_p4[bugzilla_id], self.p4_user)) else : # Perforce user table doesn't have the counterpart. self.user_bz_to_p4[bugzilla_id] = self.p4_user self.user_p4_to_bz[self.p4_user] = bugzilla_id else : # special Bugzilla user not in Bugzilla raise error, ("Bugzilla P4DTI user '%s' " "is not a known Bugzilla user." % self.bugzilla_user) def translate_1_to_0(self, p4_user, issues = None): if not self.user_p4_to_bz.has_key(p4_user): self.init_users() if self.user_p4_to_bz.has_key(p4_user): return self.user_p4_to_bz[p4_user] elif self.bz_email_to_id.has_key(p4_user): return self.bz_email_to_id[p4_user] else : return self.bz_email_to_id[self.bugzilla_user] def translate_0_to_1(self, bz_user, issues = None): if not self.user_bz_to_p4.has_key(bz_user): self.init_users() if self.user_bz_to_p4.has_key(bz_user): return self.user_bz_to_p4[bz_user] else: return self.bz_id_to_email[bz_user]