~launchpad-pqm/launchpad/devel

8687.15.15 by Karl Fogel
Add the copyright header block to files under lib/lp/bugs/.
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
2701.1.13 by James Henstridge
treat comments,etc as UTF-8. fix up option parsing in import script
3
4
"""Bugzilla to Launchpad import logic"""
5
2701.1.2 by James Henstridge
bugzilla import code
6
7
# Bugzilla schema:
4664.1.1 by Curtis Hovey
Normalized comments for bug 3732.
8
# http://lxr.mozilla.org/mozilla/source/webtools/bugzilla/Bugzilla/DB/Schema.pm
2701.1.2 by James Henstridge
bugzilla import code
9
4664.1.1 by Curtis Hovey
Normalized comments for bug 3732.
10
# XXX: jamesh 2005-10-18
2701.1.6 by James Henstridge
add some TODO notes to bugzilla.py
11
# Currently unhandled bug info:
12
#  * Operating system and platform
13
#  * version (not really used in Ubuntu bugzilla though)
14
#  * keywords
15
#  * private bugs (none of the canonical-only bugs seem sensitive though)
16
#  * bug dependencies
2701.1.46 by James Henstridge
Switch to new Launchpad method naming style
17
#  * "bug XYZ" references inside comment text (at the moment we just
18
#    insert the full URL to the bug afterwards).
2701.1.6 by James Henstridge
add some TODO notes to bugzilla.py
19
#
20
# Not all of these are necessary though
21
2701.1.2 by James Henstridge
bugzilla import code
22
__metaclass__ = type
23
2701.1.4 by James Henstridge
migrate bug attachments too
24
from cStringIO import StringIO
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
25
import datetime
26
import logging
2701.1.14 by James Henstridge
add URLs to bug references in comments, other bug fixes
27
import re
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
28
2701.1.2 by James Henstridge
bugzilla import code
29
import pytz
5821.6.11 by James Henstridge
Fix doc/bugzilla-import.txt test.
30
from storm.store import Store
2701.1.2 by James Henstridge
bugzilla import code
31
from zope.component import getUtility
5821.6.11 by James Henstridge
Fix doc/bugzilla-import.txt test.
32
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
33
from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
34
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
35
from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
12929.9.2 by j.c.sackett
Moved messages from canonical to lp.services
36
from lp.services.messages.interfaces.message import IMessageSet
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
37
from canonical.launchpad.webapp import canonical_url
11270.1.3 by Tim Penhey
Changed NotFoundError imports - gee there were a lot of them.
38
from lp.app.errors import NotFoundError
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
39
from lp.bugs.interfaces.bug import (
40
    CreateBugParams,
41
    IBugSet,
42
    )
10788.5.4 by Abel Deuring
added the bug_importer celebrity to the persons which can set any bug task assignee; fixed some test failures
43
from lp.bugs.interfaces.bugattachment import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
44
    BugAttachmentType,
45
    IBugAttachmentSet,
46
    )
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
47
from lp.bugs.interfaces.bugtask import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
48
    BugTaskImportance,
49
    BugTaskStatus,
50
    IBugTaskSet,
51
    )
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
52
from lp.bugs.interfaces.bugwatch import IBugWatchSet
53
from lp.bugs.interfaces.cve import ICveSet
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
54
from lp.registry.interfaces.person import (
55
    IPersonSet,
56
    PersonCreationRationale,
57
    )
58
2701.1.2 by James Henstridge
bugzilla import code
59
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
60
logger = logging.getLogger('lp.bugs.scripts.bugzilla')
2701.1.5 by James Henstridge
add some basic logging to the bugzilla module
61
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
62
def _add_tz(dt):
2701.1.2 by James Henstridge
bugzilla import code
63
    """Convert a naiive datetime value to a UTC datetime value."""
64
    assert dt.tzinfo is None, 'add_tz() only accepts naiive datetime values'
65
    return datetime.datetime(dt.year, dt.month, dt.day,
66
                             dt.hour, dt.minute, dt.second,
67
                             dt.microsecond, tzinfo=pytz.timezone('UTC'))
68
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
69
class BugzillaBackend:
70
    """A wrapper for all the MySQL database access.
71
72
    The main purpose of this is to make it possible to test the rest
73
    of the import code without access to a MySQL database.
74
    """
2701.1.13 by James Henstridge
treat comments,etc as UTF-8. fix up option parsing in import script
75
    def __init__(self, conn, charset='UTF-8'):
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
76
        self.conn = conn
77
        self.cursor = conn.cursor()
2701.1.13 by James Henstridge
treat comments,etc as UTF-8. fix up option parsing in import script
78
        self.charset = charset
79
80
    def _decode(self, s):
81
        if s is not None:
2701.1.50 by James Henstridge
Filter out non 16-bit unicode characters and surrogates from bugzilla data,
82
            value = s.decode(self.charset, 'replace')
83
            # Postgres doesn't like values outside of the basic multilingual
84
            # plane (U+0000 - U+FFFF), so replace them (and surrogates) with
85
            # U+FFFD (replacement character).
86
            # Existance of these characters generally indicate an encoding
87
            # problem in the existing Bugzilla data.
88
            return re.sub(u'[^\u0000-\ud7ff\ue000-\uffff]', u'\ufffd', value)
2701.1.13 by James Henstridge
treat comments,etc as UTF-8. fix up option parsing in import script
89
        else:
90
            return None
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
91
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
92
    def lookupUser(self, user_id):
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
93
        """Look up information about a particular Bugzilla user ID"""
94
        self.cursor.execute('SELECT login_name, realname '
95
                            '  FROM profiles '
96
                            '  WHERE userid = %d' % user_id)
97
        if self.cursor.rowcount != 1:
2701.1.47 by James Henstridge
Don't raise ValueError
98
            raise NotFoundError('could not look up user %d' % user_id)
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
99
        (login_name, realname) = self.cursor.fetchone()
2701.1.13 by James Henstridge
treat comments,etc as UTF-8. fix up option parsing in import script
100
        realname = self._decode(realname)
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
101
        return (login_name, realname)
102
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
103
    def getBugInfo(self, bug_id):
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
104
        """Retrieve information about a bug."""
2701.1.2 by James Henstridge
bugzilla import code
105
        self.cursor.execute(
106
            'SELECT bug_id, assigned_to, bug_file_loc, bug_severity, '
107
            '    bug_status, creation_ts, short_desc, op_sys, priority, '
108
            '    products.name, rep_platform, reporter, version, '
109
            '    components.name, resolution, target_milestone, qa_contact, '
110
            '    status_whiteboard, keywords, alias '
111
            '  FROM bugs '
112
            '    INNER JOIN products ON bugs.product_id = products.id '
113
            '    INNER JOIN components ON bugs.component_id = components.id '
114
            '  WHERE bug_id = %d' % bug_id)
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
115
        if self.cursor.rowcount != 1:
2701.1.47 by James Henstridge
Don't raise ValueError
116
            raise NotFoundError('could not look up bug %d' % bug_id)
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
117
        (bug_id, assigned_to, bug_file_loc, bug_severity, bug_status,
118
         creation_ts, short_desc, op_sys, priority, product,
119
         rep_platform, reporter, version, component, resolution,
120
         target_milestone, qa_contact, status_whiteboard, keywords,
121
         alias) = self.cursor.fetchone()
122
2701.1.13 by James Henstridge
treat comments,etc as UTF-8. fix up option parsing in import script
123
        bug_file_loc = self._decode(bug_file_loc)
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
124
        creation_ts = _add_tz(creation_ts)
2701.1.13 by James Henstridge
treat comments,etc as UTF-8. fix up option parsing in import script
125
        product = self._decode(product)
126
        version = self._decode(version)
127
        component = self._decode(component)
128
        status_whiteboard = self._decode(status_whiteboard)
129
        keywords = self._decode(keywords)
130
        alias = self._decode(alias)
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
131
132
        return (bug_id, assigned_to, bug_file_loc, bug_severity,
133
                bug_status, creation_ts, short_desc, op_sys, priority,
134
                product, rep_platform, reporter, version, component,
135
                resolution, target_milestone, qa_contact,
136
                status_whiteboard, keywords, alias)
137
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
138
    def getBugCcs(self, bug_id):
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
139
        """Get the IDs of the people CC'd to the bug."""
140
        self.cursor.execute('SELECT who FROM cc WHERE bug_id = %d'
141
                            % bug_id)
142
        return [row[0] for row in self.cursor.fetchall()]
143
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
144
    def getBugComments(self, bug_id):
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
145
        """Get the comments for the bug."""
146
        self.cursor.execute('SELECT who, bug_when, thetext '
147
                            '  FROM longdescs '
148
                            '  WHERE bug_id = %d '
149
                            '  ORDER BY bug_when' % bug_id)
4664.1.1 by Curtis Hovey
Normalized comments for bug 3732.
150
        # XXX: jamesh 2005-12-07:
2701.1.45 by James Henstridge
Add a fix for the debzilla craziness in Ubuntu Bugzilla bug 248
151
        # Due to a bug in Debzilla, Ubuntu bugzilla bug 248 has > 7800
152
        # duplicate comments,consisting of someone's signature.
153
        # For the import, just ignore those comments.
2701.1.13 by James Henstridge
treat comments,etc as UTF-8. fix up option parsing in import script
154
        return [(who, _add_tz(when), self._decode(thetext))
2701.1.45 by James Henstridge
Add a fix for the debzilla craziness in Ubuntu Bugzilla bug 248
155
                 for (who, when, thetext) in self.cursor.fetchall()
156
                 if thetext != '\n--=20\n   Jacobo Tarr=EDo     |     '
157
                               'http://jacobo.tarrio.org/\n\n\n']
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
158
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
159
    def getBugAttachments(self, bug_id):
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
160
        """Get the attachments for the bug."""
161
        self.cursor.execute('SELECT attach_id, creation_ts, description, '
162
                            '    mimetype, ispatch, filename, thedata, '
163
                            '    submitter_id '
164
                            '  FROM attachments '
165
                            '  WHERE bug_id = %d '
166
                            '  ORDER BY attach_id' % bug_id)
2701.1.13 by James Henstridge
treat comments,etc as UTF-8. fix up option parsing in import script
167
        return [(attach_id, _add_tz(creation_ts),
168
                 self._decode(description), mimetype,
169
                 ispatch, self._decode(filename), thedata, submitter_id)
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
170
                for (attach_id, creation_ts, description,
171
                     mimetype, ispatch, filename, thedata,
172
                     submitter_id) in self.cursor.fetchall()]
173
4589.2.1 by Elliot Murphy
Code cleanup, getting rid of places which have default args set to []
174
    def findBugs(self, product=None, component=None, status=None):
2701.1.31 by James Henstridge
Add code to process the bugzilla duplicates table, and import the data into
175
        """Returns the requested bug IDs as a list"""
4589.2.1 by Elliot Murphy
Code cleanup, getting rid of places which have default args set to []
176
        if product is None:
177
            product = []
178
        if component is None:
179
            component = []
180
        if status is None:
181
            status = []
2701.1.10 by James Henstridge
add findBugs code, and update to work with Mark's changes
182
        joins = []
183
        conditions = []
184
        if product:
6602.5.5 by Tom Berger
fix some lint errors
185
            joins.append(
186
                'INNER JOIN products ON bugs.product_id = products.id')
2701.1.10 by James Henstridge
add findBugs code, and update to work with Mark's changes
187
            conditions.append('products.name IN (%s)' %
188
                ', '.join([self.conn.escape(p) for p in product]))
189
        if component:
6602.5.5 by Tom Berger
fix some lint errors
190
            joins.append(
191
                'INNER JOIN components ON bugs.component_id = components.id')
2701.1.10 by James Henstridge
add findBugs code, and update to work with Mark's changes
192
            conditions.append('components.name IN (%s)' %
193
                ', '.join([self.conn.escape(c) for c in component]))
194
        if status:
195
            conditions.append('bugs.bug_status IN (%s)' %
196
                ', '.join([self.conn.escape(s) for s in status]))
197
        if conditions:
198
            conditions = 'WHERE %s' % ' AND '.join(conditions)
199
        else:
200
            conditions = ''
201
        self.cursor.execute('SELECT bug_id FROM bugs %s %s ORDER BY bug_id' %
202
                            (' '.join(joins), conditions))
203
        return [bug_id for (bug_id,) in self.cursor.fetchall()]
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
204
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
205
    def getDuplicates(self):
2701.1.31 by James Henstridge
Add code to process the bugzilla duplicates table, and import the data into
206
        """Returns a list of (dupe_of, dupe) relations."""
207
        self.cursor.execute('SELECT dupe_of, dupe FROM duplicates '
208
                            'ORDER BY dupe, dupe_of')
209
        return [(dupe_of, dupe) for (dupe_of, dupe) in self.cursor.fetchall()]
210
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
211
class Bug:
212
    """Representation of a Bugzilla Bug"""
213
    def __init__(self, backend, bug_id):
214
        self.backend = backend
2701.1.2 by James Henstridge
bugzilla import code
215
        (self.bug_id, self.assigned_to, self.bug_file_loc, self.bug_severity,
216
         self.bug_status, self.creation_ts, self.short_desc, self.op_sys,
217
         self.priority, self.product, self.rep_platform, self.reporter,
218
         self.version, self.component, self.resolution,
219
         self.target_milestone, self.qa_contact, self.status_whiteboard,
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
220
         self.keywords, self.alias) = backend.getBugInfo(bug_id)
2701.1.2 by James Henstridge
bugzilla import code
221
222
        self._ccs = None
223
        self._comments = None
2701.1.4 by James Henstridge
migrate bug attachments too
224
        self._attachments = None
2701.1.2 by James Henstridge
bugzilla import code
225
226
    @property
227
    def ccs(self):
228
        """Return the IDs of people CC'd to this bug"""
10788.5.4 by Abel Deuring
added the bug_importer celebrity to the persons which can set any bug task assignee; fixed some test failures
229
        if self._ccs is not None:
230
            return self._ccs
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
231
        self._ccs = self.backend.getBugCcs(self.bug_id)
2701.1.2 by James Henstridge
bugzilla import code
232
        return self._ccs
233
234
    @property
235
    def comments(self):
236
        """Return the comments attached to this bug"""
10788.5.4 by Abel Deuring
added the bug_importer celebrity to the persons which can set any bug task assignee; fixed some test failures
237
        if self._comments is not None:
238
            return self._comments
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
239
        self._comments = self.backend.getBugComments(self.bug_id)
2701.1.2 by James Henstridge
bugzilla import code
240
        return self._comments
241
2701.1.4 by James Henstridge
migrate bug attachments too
242
    @property
243
    def attachments(self):
244
        """Return the attachments for this bug"""
10788.5.4 by Abel Deuring
added the bug_importer celebrity to the persons which can set any bug task assignee; fixed some test failures
245
        if self._attachments is not None:
246
            return self._attachments
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
247
        self._attachments = self.backend.getBugAttachments(self.bug_id)
2701.1.4 by James Henstridge
migrate bug attachments too
248
        return self._attachments
249
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
250
    def mapSeverity(self, bugtask):
10788.5.4 by Abel Deuring
added the bug_importer celebrity to the persons which can set any bug task assignee; fixed some test failures
251
        """Set a Launchpad bug task's importance based on this bug's severity.
252
        """
6602.5.3 by Tom Berger
convert all assingments to bugtask.importance to calls to transitionToImportance
253
        bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
254
        importance_map = {
3270.3.10 by Matthew Paul Thomas
Changes BugTaskSeverity to BugTaskImportance.
255
            'blocker': BugTaskImportance.CRITICAL,
256
            'critical': BugTaskImportance.CRITICAL,
3270.3.47 by Matthew Paul Thomas
Changes Minor to Low, Major to High, and restores the Priority column to the database.
257
            'major': BugTaskImportance.HIGH,
3270.3.11 by Matthew Paul Thomas
Makes bug pages and bug listings work.
258
            'normal': BugTaskImportance.MEDIUM,
3270.3.47 by Matthew Paul Thomas
Changes Minor to Low, Major to High, and restores the Priority column to the database.
259
            'minor': BugTaskImportance.LOW,
260
            'trivial': BugTaskImportance.LOW,
3270.3.10 by Matthew Paul Thomas
Changes BugTaskSeverity to BugTaskImportance.
261
            'enhancement': BugTaskImportance.WISHLIST
6602.5.3 by Tom Berger
convert all assingments to bugtask.importance to calls to transitionToImportance
262
            }
263
        importance = importance_map.get(
264
            self.bug_severity, BugTaskImportance.UNKNOWN)
265
        bugtask.transitionToImportance(importance, bug_importer)
2701.1.2 by James Henstridge
bugzilla import code
266
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
267
    def mapStatus(self, bugtask):
2701.1.35 by James Henstridge
Address some of BjornT's review concerns
268
        """Set a Launchpad bug task's status based on this bug's status.
269
270
        If the bug is in the RESOLVED, VERIFIED or CLOSED states, the
271
        bug resolution is also taken into account when mapping the
272
        status.
273
274
        Additional information about the bugzilla status is appended
275
        to the bug task's status explanation.
276
        """
4318.3.10 by Gavin Panella
Changing transitionToStatus to accept user argument.
277
        bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
4785.3.7 by Jeroen Vermeulen
Removed whitespace at ends of lines
278
2701.1.2 by James Henstridge
bugzilla import code
279
        if self.bug_status == 'ASSIGNED':
4318.3.10 by Gavin Panella
Changing transitionToStatus to accept user argument.
280
            bugtask.transitionToStatus(
281
                BugTaskStatus.CONFIRMED, bug_importer)
2701.1.21 by James Henstridge
Import NEEDINFO bugs as NEEDINFO rather than NEW
282
        elif self.bug_status == 'NEEDINFO':
4318.3.10 by Gavin Panella
Changing transitionToStatus to accept user argument.
283
            bugtask.transitionToStatus(
284
                BugTaskStatus.INCOMPLETE, bug_importer)
2701.1.2 by James Henstridge
bugzilla import code
285
        elif self.bug_status == 'PENDINGUPLOAD':
4318.3.10 by Gavin Panella
Changing transitionToStatus to accept user argument.
286
            bugtask.transitionToStatus(
287
                BugTaskStatus.FIXCOMMITTED, bug_importer)
2701.1.2 by James Henstridge
bugzilla import code
288
        elif self.bug_status in ['RESOLVED', 'VERIFIED', 'CLOSED']:
289
            # depends on the resolution:
290
            if self.resolution == 'FIXED':
4318.3.10 by Gavin Panella
Changing transitionToStatus to accept user argument.
291
                bugtask.transitionToStatus(
292
                    BugTaskStatus.FIXRELEASED, bug_importer)
2701.1.2 by James Henstridge
bugzilla import code
293
            else:
4318.3.10 by Gavin Panella
Changing transitionToStatus to accept user argument.
294
                bugtask.transitionToStatus(
295
                    BugTaskStatus.INVALID, bug_importer)
2701.1.2 by James Henstridge
bugzilla import code
296
        else:
4318.3.10 by Gavin Panella
Changing transitionToStatus to accept user argument.
297
            bugtask.transitionToStatus(
298
                BugTaskStatus.NEW, bug_importer)
2701.1.2 by James Henstridge
bugzilla import code
299
300
        # add the status to the notes section, to account for any lost
301
        # information
2701.1.5 by James Henstridge
add some basic logging to the bugzilla module
302
        bugzilla_status = 'Bugzilla status=%s' % self.bug_status
303
        if self.resolution:
304
            bugzilla_status += ' %s' % self.resolution
305
        bugzilla_status += ', product=%s' % self.product
306
        bugzilla_status += ', component=%s' % self.component
307
2701.1.2 by James Henstridge
bugzilla import code
308
        if bugtask.statusexplanation:
10788.5.4 by Abel Deuring
added the bug_importer celebrity to the persons which can set any bug task assignee; fixed some test failures
309
            bugtask.statusexplanation = '%s (%s)' % (
310
                bugtask.statusexplanation, bugzilla_status)
2701.1.2 by James Henstridge
bugzilla import code
311
        else:
312
            bugtask.statusexplanation = bugzilla_status
313
314
315
class Bugzilla:
316
    """Representation of a bugzilla instance"""
317
318
    def __init__(self, conn):
2701.1.8 by James Henstridge
add a doctest for the bugzilla import code
319
        if conn is not None:
320
            self.backend = BugzillaBackend(conn)
321
        else:
322
            self.backend = None
2701.1.27 by James Henstridge
Create a debian bug watch and task if the Bugzilla bug's alias matches
323
        self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
324
        self.debian = getUtility(ILaunchpadCelebrities).debian
2701.1.29 by James Henstridge
ubuntu-bugzilla => celebrity
325
        self.bugtracker = getUtility(ILaunchpadCelebrities).ubuntu_bugzilla
2701.1.27 by James Henstridge
Create a debian bug watch and task if the Bugzilla bug's alias matches
326
        self.debbugs = getUtility(ILaunchpadCelebrities).debbugs
2701.1.2 by James Henstridge
bugzilla import code
327
        self.bugset = getUtility(IBugSet)
2701.1.23 by James Henstridge
Add preliminary support for bugs in the UPSTREAM state, by creating a
328
        self.bugtaskset = getUtility(IBugTaskSet)
329
        self.bugwatchset = getUtility(IBugWatchSet)
2701.1.2 by James Henstridge
bugzilla import code
330
        self.cveset = getUtility(ICveSet)
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
331
        self.personset = getUtility(IPersonSet)
2701.1.33 by James Henstridge
If the person has no preferred email address, set the email from the
332
        self.emailset = getUtility(IEmailAddressSet)
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
333
        self.person_mapping = {}
334
335
    def person(self, bugzilla_id):
336
        """Get the Launchpad person corresponding to the given Bugzilla ID"""
2701.1.35 by James Henstridge
Address some of BjornT's review concerns
337
        # Bugzilla treats a user ID of 0 as a NULL
338
        if bugzilla_id == 0:
339
            return None
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
340
341
        # Try and get the person using a cache of the mapping.  We
342
        # check to make sure the person still exists and has not been
343
        # merged.
2701.1.8 by James Henstridge
add a doctest for the bugzilla import code
344
        person = None
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
345
        launchpad_id = self.person_mapping.get(bugzilla_id)
346
        if launchpad_id is not None:
2908.4.3 by Guilherme Salgado
some fixes as per Rob's review
347
            person = self.personset.get(launchpad_id)
348
            if person is not None and person.merged is not None:
349
                person = None
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
350
351
        # look up the person
352
        if person is None:
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
353
            email, displayname = self.backend.lookupUser(bugzilla_id)
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
354
355
            person = self.personset.ensurePerson(
3691.164.3 by Guilherme Salgado
Update all places where a person can be created to provide creation_rationale/creation_comment and change the registration process to Just Work when someone tries to register with an email address that is associated with an unvalidated account
356
                email, displayname, PersonCreationRationale.BUGIMPORT,
3691.164.16 by Guilherme Salgado
Lots of fixes and tests suggested by Bjorn
357
                comment=('when importing bugs from %s'
3691.164.3 by Guilherme Salgado
Update all places where a person can be created to provide creation_rationale/creation_comment and change the registration process to Just Work when someone tries to register with an email address that is associated with an unvalidated account
358
                         % self.bugtracker.baseurl))
2701.1.33 by James Henstridge
If the person has no preferred email address, set the email from the
359
360
            # Bugzilla performs similar address checks to Launchpad, so
361
            # if the Launchpad account has no preferred email, use the
362
            # Bugzilla one.
363
            emailaddr = self.emailset.getByEmail(email)
364
            assert emailaddr is not None
2701.1.35 by James Henstridge
Address some of BjornT's review concerns
365
            if person.preferredemail != emailaddr:
366
                person.validateAndEnsurePreferredEmail(emailaddr)
4785.3.7 by Jeroen Vermeulen
Removed whitespace at ends of lines
367
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
368
            self.person_mapping[bugzilla_id] = person.id
369
370
        return person
2701.1.2 by James Henstridge
bugzilla import code
371
2950.1.1 by James Henstridge
add a custom source package mapping of the "linux" component
372
    def _getPackageNames(self, bug):
373
        """Returns the source and binary package names for the given bug."""
374
        # we currently only support mapping Ubuntu bugs ...
375
        if bug.product != 'Ubuntu':
376
            raise AssertionError('product must be Ubuntu')
377
378
        # kernel bugs are currently filed against the "linux"
379
        # component, which is not a source or binary package.  The
380
        # following mapping was provided by BenC:
381
        if bug.component == 'linux':
382
            cutoffdate = datetime.datetime(2004, 12, 1,
383
                                           tzinfo=pytz.timezone('UTC'))
384
            if bug.bug_status == 'NEEDINFO' and bug.creation_ts < cutoffdate:
385
                pkgname = 'linux-source-2.6.12'
386
            else:
387
                pkgname = 'linux-source-2.6.15'
388
        else:
389
            pkgname = bug.component.encode('ASCII')
4785.3.7 by Jeroen Vermeulen
Removed whitespace at ends of lines
390
2950.1.1 by James Henstridge
add a custom source package mapping of the "linux" component
391
        try:
3504.1.58 by kiko
Hack Distribution.getPackageNames around a bit: rename it to guessPackageNames to make clear it is a best-effort and might not give the right answer; test the fact that it restricts publication to a single distribution, and ensure that the code does that; revamp comments.
392
            srcpkg, binpkg = self.ubuntu.guessPackageNames(pkgname)
2950.1.1 by James Henstridge
add a custom source package mapping of the "linux" component
393
        except NotFoundError, e:
394
            logger.warning('could not find package name for "%s": %s',
395
                           pkgname, str(e))
396
            srcpkg = binpkg = None
397
398
        return srcpkg, binpkg
399
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
400
    def getLaunchpadBugTarget(self, bug):
2701.1.2 by James Henstridge
bugzilla import code
401
        """Returns a dictionary of arguments to createBug() that correspond
402
        to the given bugzilla bug.
403
        """
2950.1.1 by James Henstridge
add a custom source package mapping of the "linux" component
404
        srcpkg, binpkg = self._getPackageNames(bug)
2701.1.2 by James Henstridge
bugzilla import code
405
        return {
2701.1.27 by James Henstridge
Create a debian bug watch and task if the Bugzilla bug's alias matches
406
            'distribution': self.ubuntu,
2701.1.2 by James Henstridge
bugzilla import code
407
            'sourcepackagename': srcpkg,
408
            }
409
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
410
    def getLaunchpadMilestone(self, bug):
2701.1.43 by James Henstridge
Migrate Bugzilla milestones to Launchpad equivalents
411
        """Return the Launchpad milestone for a Bugzilla bug.
412
413
        If the milestone does not exist, then it is created.
414
        """
415
        if bug.product != 'Ubuntu':
2701.1.47 by James Henstridge
Don't raise ValueError
416
            raise AssertionError('product must be Ubuntu')
2701.1.43 by James Henstridge
Migrate Bugzilla milestones to Launchpad equivalents
417
2701.1.47 by James Henstridge
Don't raise ValueError
418
        # Bugzilla uses a value of "---" to represent "no selected Milestone"
419
        # Launchpad represents this by setting the milestone column to NULL.
2701.1.43 by James Henstridge
Migrate Bugzilla milestones to Launchpad equivalents
420
        if bug.target_milestone is None or bug.target_milestone == '---':
421
            return None
422
423
        # generate a Launchpad name from the Milestone name:
424
        name = re.sub(r'[^a-z0-9\+\.\-]', '-', bug.target_milestone.lower())
425
3472.2.2 by Mark Shuttleworth
Test fixes for milestones.
426
        milestone = self.ubuntu.getMilestone(name)
5821.6.11 by James Henstridge
Fix doc/bugzilla-import.txt test.
427
        if milestone is None:
428
            milestone = self.ubuntu.currentseries.newMilestone(name)
429
            Store.of(milestone).flush()
430
        return milestone
2701.1.43 by James Henstridge
Migrate Bugzilla milestones to Launchpad equivalents
431
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
432
    def getLaunchpadUpstreamProduct(self, bug):
2701.1.43 by James Henstridge
Migrate Bugzilla milestones to Launchpad equivalents
433
        """Find the upstream product for the given Bugzilla bug.
434
435
        This function relies on the package -> product linkage having been
436
        entered in advance.
437
        """
2950.1.1 by James Henstridge
add a custom source package mapping of the "linux" component
438
        srcpkgname, binpkgname = self._getPackageNames(bug)
2701.1.23 by James Henstridge
Add preliminary support for bugs in the UPSTREAM state, by creating a
439
        # find a product series
440
        series = None
9760.8.1 by Brad Crittenden
Change the non-English 'serieses' to 'series' throughout our codebase.
441
        for series in self.ubuntu.series:
4285.2.4 by Mark Shuttleworth
Test fixes with renamed distrorelease
442
            srcpkg = series.getSourcePackage(srcpkgname)
2701.1.23 by James Henstridge
Add preliminary support for bugs in the UPSTREAM state, by creating a
443
            if srcpkg:
444
                series = srcpkg.productseries
445
                if series:
446
                    return series.product
447
        else:
2701.1.26 by James Henstridge
Warn if the upstream package could not be deduced when linking to
448
            logger.warning('could not find upstream product for '
449
                           'source package "%s"', srcpkgname.name)
2701.1.23 by James Henstridge
Add preliminary support for bugs in the UPSTREAM state, by creating a
450
            return None
4664.1.1 by Curtis Hovey
Normalized comments for bug 3732.
451
2701.1.14 by James Henstridge
add URLs to bug references in comments, other bug fixes
452
    _bug_re = re.compile('bug\s*#?\s*(?P<id>\d+)', re.IGNORECASE)
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
453
    def replaceBugRef(self, match):
4664.1.1 by Curtis Hovey
Normalized comments for bug 3732.
454
        # XXX: jamesh 2005-10-24:
2701.1.14 by James Henstridge
add URLs to bug references in comments, other bug fixes
455
        # this is where bug number rewriting would be plugged in
456
        bug_id = int(match.group('id'))
2950.2.2 by James Henstridge
When rewriting bug references from imported bugzilla comments, include a
457
        url = '%s/%d' % (canonical_url(self.bugtracker), bug_id)
2701.1.14 by James Henstridge
add URLs to bug references in comments, other bug fixes
458
        return '%s [%s]' % (match.group(0), url)
459
460
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
461
    def handleBug(self, bug_id):
2701.1.10 by James Henstridge
add findBugs code, and update to work with Mark's changes
462
        """Maybe import a single bug.
463
464
        If the bug has already been imported (detected by checking for
465
        a bug watch), it is skipped.
466
        """
2701.1.5 by James Henstridge
add some basic logging to the bugzilla module
467
        logger.info('Handling Bugzilla bug %d', bug_id)
4785.3.7 by Jeroen Vermeulen
Removed whitespace at ends of lines
468
2701.1.2 by James Henstridge
bugzilla import code
469
        # is there a bug watch on the bug?
2701.1.5 by James Henstridge
add some basic logging to the bugzilla module
470
        lp_bug = self.bugset.queryByRemoteBug(self.bugtracker, bug_id)
2701.1.2 by James Henstridge
bugzilla import code
471
472
        # if we already have an associated bug, don't add a new one.
473
        if lp_bug is not None:
2701.1.5 by James Henstridge
add some basic logging to the bugzilla module
474
            logger.info('Bugzilla bug %d is already being watched by '
475
                        'Launchpad bug %d', bug_id, lp_bug.id)
476
            return lp_bug
477
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
478
        bug = Bug(self.backend, bug_id)
2701.1.2 by James Henstridge
bugzilla import code
479
480
        comments = bug.comments[:]
481
482
        # create a message for the initial comment:
483
        msgset = getUtility(IMessageSet)
484
        who, when, text = comments.pop(0)
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
485
        text = self._bug_re.sub(self.replaceBugRef, text)
4697.2.4 by Matthew Paul Thomas
Fixes from self-review.
486
        # If a URL is associated with the bug, add it to the description:
4697.2.1 by Matthew Paul Thomas
Fixes bug 5998 ('Add URL' link only visible if bug already has URLs associated with it), by removing the idea of Web links for bugs.
487
        if bug.bug_file_loc:
4697.2.2 by Matthew Paul Thomas
Fixes doctests.
488
            text = text + '\n\n' + bug.bug_file_loc
489
        # the initial comment can't be empty:
2701.1.14 by James Henstridge
add URLs to bug references in comments, other bug fixes
490
        if not text.strip():
491
            text = '<empty comment>'
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
492
        msg = msgset.fromText(bug.short_desc, text, self.person(who), when)
2701.1.2 by James Henstridge
bugzilla import code
493
494
        # create the bug
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
495
        target = self.getLaunchpadBugTarget(bug)
3598.1.8 by Brad Bollenbach
refactor IBugSet.createBug to use a parameter object
496
        params = CreateBugParams(
497
            msg=msg, datecreated=bug.creation_ts, title=bug.short_desc,
498
            owner=self.person(bug.reporter))
499
        params.setBugTarget(**target)
500
        lp_bug = self.bugset.createBug(params)
2701.1.2 by James Henstridge
bugzilla import code
501
502
        # add the bug watch:
6128.2.17 by Stuart Bishop
PG8.3 fixes
503
        lp_bug.addWatch(self.bugtracker, str(bug.bug_id), lp_bug.owner)
2701.1.2 by James Henstridge
bugzilla import code
504
505
        # add remaining comments, and add CVEs found in all text
4476.1.3 by Bjorn Tillenius
fix test to expose problem when creating CVEs on package uploads. Fix the test failure by requiring a user attribute for linkCVE and findCvesInText.
506
        lp_bug.findCvesInText(text, lp_bug.owner)
2701.1.2 by James Henstridge
bugzilla import code
507
        for (who, when, text) in comments:
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
508
            text = self._bug_re.sub(self.replaceBugRef, text)
2701.1.14 by James Henstridge
add URLs to bug references in comments, other bug fixes
509
            msg = msgset.fromText(msg.followup_title, text,
510
                                  self.person(who), when)
511
            lp_bug.linkMessage(msg)
2701.1.2 by James Henstridge
bugzilla import code
512
513
        # subscribe QA contact and CC's
514
        if bug.qa_contact:
5454.1.5 by Tom Berger
record who created each bug subscription, and display the result in the title of the subscriber link
515
            lp_bug.subscribe(
516
                self.person(bug.qa_contact), self.person(bug.reporter))
2701.1.2 by James Henstridge
bugzilla import code
517
        for cc in bug.ccs:
5454.1.5 by Tom Berger
record who created each bug subscription, and display the result in the title of the subscriber link
518
            lp_bug.subscribe(self.person(cc), self.person(bug.reporter))
2701.1.2 by James Henstridge
bugzilla import code
519
520
        # translate bugzilla status and severity to LP equivalents
521
        task = lp_bug.bugtasks[0]
522
        task.datecreated = bug.creation_ts
3455.1.4 by Brad Bollenbach
checkpoint
523
        task.transitionToAssignee(self.person(bug.assigned_to))
2701.1.2 by James Henstridge
bugzilla import code
524
        task.statusexplanation = bug.status_whiteboard
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
525
        bug.mapSeverity(task)
526
        bug.mapStatus(task)
2701.1.2 by James Henstridge
bugzilla import code
527
2701.1.27 by James Henstridge
Create a debian bug watch and task if the Bugzilla bug's alias matches
528
        # bugs with an alias of the form "deb1234" have been imported
529
        # from the Debian bug tracker by the "debzilla" program.  For
530
        # these bugs, generate a task and watch on the corresponding
531
        # bugs.debian.org bug.
2976.1.1 by James Henstridge
Support migration of bug aliases
532
        if bug.alias:
533
            if re.match(r'^deb\d+$', bug.alias):
534
                watch = self.bugwatchset.createBugWatch(
5821.6.11 by James Henstridge
Fix doc/bugzilla-import.txt test.
535
                    lp_bug, lp_bug.owner, self.debbugs, bug.alias[3:])
2976.1.1 by James Henstridge
Support migration of bug aliases
536
                debtask = self.bugtaskset.createTask(
537
                    lp_bug,
538
                    owner=lp_bug.owner,
539
                    distribution=self.debian,
540
                    sourcepackagename=target['sourcepackagename'])
541
                debtask.datecreated = bug.creation_ts
542
                debtask.bugwatch = watch
543
            else:
544
                # generate a Launchpad name from the alias:
545
                name = re.sub(r'[^a-z0-9\+\.\-]', '-', bug.alias.lower())
546
                lp_bug.name = name
2701.1.27 by James Henstridge
Create a debian bug watch and task if the Bugzilla bug's alias matches
547
2701.1.23 by James Henstridge
Add preliminary support for bugs in the UPSTREAM state, by creating a
548
        # for UPSTREAM bugs, try to find whether the URL field contains
549
        # a bug reference.
550
        if bug.bug_status == 'UPSTREAM':
551
            # see if the URL field contains a bug tracker reference
552
            watches = self.bugwatchset.fromText(bug.bug_file_loc,
553
                                                lp_bug, lp_bug.owner)
554
            # find the upstream product for this bug
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
555
            product = self.getLaunchpadUpstreamProduct(bug)
2701.1.23 by James Henstridge
Add preliminary support for bugs in the UPSTREAM state, by creating a
556
557
            # if we created a watch, and there is an upstream product,
558
            # create a new task and link it to the watch.
2701.1.35 by James Henstridge
Address some of BjornT's review concerns
559
            if len(watches) > 0:
560
                if product:
561
                    upstreamtask = self.bugtaskset.createTask(
562
                        lp_bug, product=product, owner=lp_bug.owner)
563
                    upstreamtask.datecreated = bug.creation_ts
564
                    upstreamtask.bugwatch = watches[0]
565
                else:
566
                    logger.warning('Could not find upstream product to link '
567
                                   'bug %d to', lp_bug.id)
2701.1.23 by James Henstridge
Add preliminary support for bugs in the UPSTREAM state, by creating a
568
2701.1.35 by James Henstridge
Address some of BjornT's review concerns
569
        # translate milestone linkage
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
570
        task.milestone = self.getLaunchpadMilestone(bug)
2701.1.2 by James Henstridge
bugzilla import code
571
2701.1.4 by James Henstridge
migrate bug attachments too
572
        # import attachments
573
        for (attach_id, creation_ts, description, mimetype, ispatch,
574
             filename, thedata, submitter_id) in bug.attachments:
2701.1.50 by James Henstridge
Filter out non 16-bit unicode characters and surrogates from bugzilla data,
575
            # if the filename is missing for some reason, use a generic one.
576
            if filename is None or filename.strip() == '':
577
                filename = 'untitled'
2701.1.5 by James Henstridge
add some basic logging to the bugzilla module
578
            logger.debug('Creating attachment %s for bug %d',
579
                         filename, bug.bug_id)
2701.1.4 by James Henstridge
migrate bug attachments too
580
            if ispatch:
581
                attach_type = BugAttachmentType.PATCH
582
                mimetype = 'text/plain'
583
            else:
584
                attach_type = BugAttachmentType.UNSPECIFIED
585
2701.1.5 by James Henstridge
add some basic logging to the bugzilla module
586
            # look for a message starting with "Created an attachment (id=NN)"
2701.1.4 by James Henstridge
migrate bug attachments too
587
            for msg in lp_bug.messages:
2938.3.3 by Bjorn Tillenius
remove IMessage.contents since there's no current use case for it.
588
                if msg.text_contents.startswith(
589
                        'Created an attachment (id=%d)' % attach_id):
2701.1.4 by James Henstridge
migrate bug attachments too
590
                    break
591
            else:
592
                # could not find the add message, so create one:
593
                msg = msgset.fromText(description,
594
                                      'Created attachment %s' % filename,
2701.1.7 by James Henstridge
extract actual mysql stuff into a separate class so it can be replaced
595
                                      self.person(submitter_id),
2701.1.4 by James Henstridge
migrate bug attachments too
596
                                      creation_ts)
597
                lp_bug.linkMessage(msg)
598
599
            filealias = getUtility(ILibraryFileAliasSet).create(
600
                name=filename,
601
                size=len(thedata),
602
                file=StringIO(thedata),
603
                contentType=mimetype)
604
605
            getUtility(IBugAttachmentSet).create(
606
                bug=lp_bug, filealias=filealias, attach_type=attach_type,
607
                title=description, message=msg)
2701.1.8 by James Henstridge
add a doctest for the bugzilla import code
608
609
        return lp_bug
2701.1.10 by James Henstridge
add findBugs code, and update to work with Mark's changes
610
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
611
    def processDuplicates(self, trans):
2701.1.35 by James Henstridge
Address some of BjornT's review concerns
612
        """Mark Launchpad bugs as duplicates based on Bugzilla duplicates.
613
614
        Launchpad bug A will be marked as a duplicate of bug B if:
615
         * bug A watches bugzilla bug A'
616
         * bug B watches bugzilla bug B'
617
         * bug A' is a duplicate of bug B'
618
         * bug A is not currently a duplicate of any other bug.
619
        """
2701.1.31 by James Henstridge
Add code to process the bugzilla duplicates table, and import the data into
620
        logger.info('Processing duplicate bugs')
621
        bugmap = {}
622
        def getlpbug(bugid):
623
            """Get the Launchpad bug corresponding to the given remote ID
624
625
            This function makes use of a cache dictionary to reduce the
626
            number of lookups.
627
            """
628
            lpbugid = bugmap.get(bugid)
629
            if lpbugid is not None:
630
                if lpbugid != 0:
631
                    lpbug = self.bugset.get(lpbugid)
632
                else:
633
                    lpbug = None
634
            else:
635
                lpbug = self.bugset.queryByRemoteBug(self.bugtracker, bugid)
636
                if lpbug is not None:
637
                    bugmap[bugid] = lpbug.id
638
                else:
639
                    bugmap[bugid] = 0
640
            return lpbug
641
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
642
        for (dupe_of, dupe) in self.backend.getDuplicates():
2701.1.31 by James Henstridge
Add code to process the bugzilla duplicates table, and import the data into
643
            # get the Launchpad bugs corresponding to the two Bugzilla bugs:
2701.1.36 by James Henstridge
More changes based on BjornT's feedback
644
            trans.begin()
645
            lpdupe_of = getlpbug(dupe_of)
646
            lpdupe = getlpbug(dupe)
647
            # if both bugs exist in Launchpad, and lpdupe is not already
648
            # a duplicate, mark it as a duplicate of lpdupe_of.
649
            if (lpdupe_of is not None and lpdupe is not None and
650
                lpdupe.duplicateof is None):
651
                logger.info('Marking %d as a duplicate of %d',
652
                            lpdupe.id, lpdupe_of.id)
11272.1.1 by Deryck Hodge
First pass at getting my dupe finder work that was
653
                lpdupe.markAsDuplicate(lpdupe_of)
2701.1.36 by James Henstridge
More changes based on BjornT's feedback
654
            trans.commit()
655
4589.2.1 by Elliot Murphy
Code cleanup, getting rid of places which have default args set to []
656
    def importBugs(self, trans, product=None, component=None, status=None):
2701.1.36 by James Henstridge
More changes based on BjornT's feedback
657
        """Import Bugzilla bugs matching the given constraints.
658
659
        Each of product, component and status gives a list of
660
        products, components or statuses to limit the import to.  An
661
        empty list matches all products, components or statuses.
662
        """
4589.2.1 by Elliot Murphy
Code cleanup, getting rid of places which have default args set to []
663
        if product is None:
664
            product = []
665
        if component is None:
666
            component = []
667
        if status is None:
668
            status = []
669
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
670
        bugs = self.backend.findBugs(product=product,
671
                                     component=component,
672
                                     status=status)
2701.1.10 by James Henstridge
add findBugs code, and update to work with Mark's changes
673
        for bug_id in bugs:
2701.1.36 by James Henstridge
More changes based on BjornT's feedback
674
            trans.begin()
2701.1.10 by James Henstridge
add findBugs code, and update to work with Mark's changes
675
            try:
2701.1.51 by James Henstridge
Revert method name changes in rev 2747 in light of decision at meeting.
676
                self.handleBug(bug_id)
2701.1.14 by James Henstridge
add URLs to bug references in comments, other bug fixes
677
            except (SystemExit, KeyboardInterrupt):
678
                raise
2701.1.10 by James Henstridge
add findBugs code, and update to work with Mark's changes
679
            except:
680
                logger.exception('Could not import Bugzilla bug #%d', bug_id)
681
                trans.abort()
682
            else:
683
                trans.commit()