Merge lp:~jelmer/launchpad/code-import-bug-link into lp:launchpad/db-devel

Proposed by Jelmer Vernooij
Status: Work in progress
Proposed branch: lp:~jelmer/launchpad/code-import-bug-link
Merge into: lp:launchpad/db-devel
Diff against target: 2614 lines (+257/-2015)
24 files modified
cronscripts/create-debwatches.py (+0/-102)
cronscripts/update-debwatches.py (+0/-240)
database/schema/comments.sql (+1/-0)
database/schema/patch-2208-00-2.sql (+16/-0)
lib/canonical/launchpad/scripts/debsync.py (+0/-201)
lib/lp/bugs/browser/bugtask.py (+18/-0)
lib/lp/bugs/browser/configure.zcml (+5/-0)
lib/lp/bugs/browser/tests/test_bugtask.py (+24/-0)
lib/lp/bugs/doc/bugzilla-import.txt (+0/-562)
lib/lp/bugs/scripts/bugzilla.py (+0/-687)
lib/lp/bugs/templates/bugtarget-tasks-and-nominations-table-row.pt (+11/-0)
lib/lp/bugs/tests/test_doc.py (+0/-6)
lib/lp/code/configure.zcml (+3/-1)
lib/lp/code/interfaces/codeimport.py (+14/-0)
lib/lp/code/model/codeimport.py (+12/-0)
lib/lp/code/model/tests/test_codeimport.py (+25/-0)
lib/lp/codehosting/codeimport/tests/test_uifactory.py (+66/-12)
lib/lp/codehosting/codeimport/uifactory.py (+51/-9)
lib/lp/codehosting/codeimport/worker.py (+5/-4)
lib/lp/registry/browser/distributionsourcepackage.py (+6/-0)
lib/lp/registry/templates/distributionsourcepackage-index.pt (+0/-2)
lib/lp/services/scripts/tests/__init__.py (+0/-1)
scripts/bugzilla-import.py (+0/-97)
scripts/migrate-bugzilla-initialcontacts.py (+0/-91)
To merge this branch: bzr merge lp:~jelmer/launchpad/code-import-bug-link
Reviewer Review Type Date Requested Status
Stuart Bishop (community) db Approve
Robert Collins db Pending
Launchpad code reviewers code Pending
Review via email: mp+33595@code.launchpad.net

Commit message

Add a field for linking code imports to related bug reports that explain their failing.

Description of the change

This adds a field to CodeImport that can be used to link the bug that causes the import to be failing. This field can only be set if the import is marked failing. If the field is set when the status of the import changes the code import is unlinked from the bug.

Pre-implementation call
=======================
I haven't had a pre-implementation call.

Tests
=====
./bin/test lp.code.model.tests.test_codeimport

To post a comment you must log in.
9677. By Jelmer Vernooij

merge db-devel

Revision history for this message
Stuart Bishop (stub) wrote :

Looks fine. We will want an index, so please add this in:

CREATE INDEX codeimport__failure_bug__idx ON CodeImport(failure_bug) WHERE failure_bug IS NOT NULL;

patch-2208-06-0.sql

review: Approve (db)
9678. By Jelmer Vernooij

merge trunk

Unmerged revisions

9678. By Jelmer Vernooij

merge trunk

9677. By Jelmer Vernooij

merge db-devel

9676. By Jelmer Vernooij

Add model code.

9675. By Jelmer Vernooij

Add database patch for bug URL column.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'cronscripts/create-debwatches.py'
2--- cronscripts/create-debwatches.py 2011-05-29 01:36:05 +0000
3+++ cronscripts/create-debwatches.py 1970-01-01 00:00:00 +0000
4@@ -1,102 +0,0 @@
5-#!/usr/bin/python -S
6-#
7-# Copyright 2009 Canonical Ltd. This software is licensed under the
8-# GNU Affero General Public License version 3 (see the file LICENSE).
9-
10-# pylint: disable-msg=C0103,W0403
11-
12-# This script aims to ensure that there is a Malone watch on Debian bugs
13-# that meet certain criteria. The Malone watch will be linked to a BugTask
14-# on Debian for that bug. The business of syncing is handled separately.
15-
16-__metaclass__ = type
17-
18-import _pythonpath
19-import os
20-import logging
21-
22-# zope bits
23-from zope.component import getUtility
24-
25-# canonical launchpad modules
26-from lp.services.scripts.base import (
27- LaunchpadCronScript, LaunchpadScriptFailure)
28-from canonical.launchpad.scripts.debsync import do_import
29-from lp.app.interfaces.launchpad import ILaunchpadCelebrities
30-
31-
32-# setup core values and defaults
33-debbugs_location_default = '/srv/bugs-mirror.debian.org/'
34-debbugs_pl = '../lib/canonical/launchpad/scripts/debbugs-log.pl'
35-
36-# the minimum age, in days, of a debbugs bug before we will import it
37-MIN_AGE = 7
38-
39-
40-class CreateDebWatches(LaunchpadCronScript):
41- description = """
42- This script syncs debbugs from http://bugs.debian.org/ into Malone.
43- It selects interesting bugs in debian and makes sure that there is a
44- Malone bug for each of them. See debwatchsync for a tool that
45- syncronises the bugs in Malone and debbugs, too.
46- """
47- loglevel = logging.WARNING
48- def add_my_options(self):
49- self.parser.set_defaults(max=None, debbugs=debbugs_location_default)
50- self.parser.add_option('--debbugs', action='store', type='string',
51- dest='debbugs',
52- help="The location of your debbugs database.")
53- self.parser.add_option(
54- '--max', action='store', type='int', dest='max',
55- help="The maximum number of bugs to create.")
56- self.parser.add_option('--package', action='append', type='string',
57- help="A list of packages for which we should import bugs.",
58- dest="packages", default=[])
59-
60- def main(self):
61- index_db_path = os.path.join(self.options.debbugs, 'index/index.db')
62- if not os.path.exists(index_db_path):
63- # make sure the debbugs location looks sane
64- raise LaunchpadScriptFailure('%s is not a debbugs db.'
65- % self.options.debbugs)
66-
67- # Make sure we import any Debian bugs specified on the command line
68- target_bugs = set()
69- for arg in self.args:
70- try:
71- target_bug = int(arg)
72- except ValueError:
73- self.logger.error(
74- '%s is not a valid debian bug number.' % arg)
75- target_bugs.add(target_bug)
76-
77- target_package_set = set()
78- previousimportset = set()
79-
80- self.logger.info('Calculating target package set...')
81-
82- # first find all the published ubuntu packages
83- ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
84- for p in ubuntu.currentrelease.getAllPublishedBinaries():
85- target_package_set.add(
86- p.binarypackagerelease.binarypackagename.name)
87- # then add packages passed on the command line
88- for package in self.options.packages:
89- target_package_set.add(package)
90- self.logger.info(
91- '%d binary packages targeted.' % len(target_package_set))
92-
93- self.txn.abort()
94- self.txn.begin()
95- do_import(self.logger, self.options.max, self.options.debbugs,
96- target_bugs, target_package_set, previousimportset, MIN_AGE,
97- debbugs_pl)
98- self.txn.commit()
99-
100- self.logger.info('Done!')
101-
102-
103-if __name__ == '__main__':
104- script = CreateDebWatches("debbugs-mkwatch")
105- script.lock_and_run()
106-
107
108=== removed file 'cronscripts/update-debwatches.py'
109--- cronscripts/update-debwatches.py 2011-06-15 15:11:43 +0000
110+++ cronscripts/update-debwatches.py 1970-01-01 00:00:00 +0000
111@@ -1,240 +0,0 @@
112-#!/usr/bin/python -S
113-#
114-# Copyright 2009 Canonical Ltd. This software is licensed under the
115-# GNU Affero General Public License version 3 (see the file LICENSE).
116-
117-# This script runs through the set of Debbugs watches, and tries to
118-# syncronise each of those to the malone bug which is watching it.
119-
120-import _pythonpath
121-import os
122-import sys
123-import email
124-import logging
125-
126-# zope bits
127-from zope.component import getUtility
128-
129-from canonical.database.constants import UTC_NOW
130-from lp.app.errors import NotFoundError
131-from lp.app.interfaces.launchpad import ILaunchpadCelebrities
132-from lp.bugs.interfaces.bug import IBugSet
133-from lp.bugs.interfaces.bugtask import (
134- BugTaskSearchParams,
135- IBugTaskSet,
136- )
137-from lp.bugs.interfaces.bugwatch import IBugWatchSet
138-from lp.bugs.interfaces.cve import ICveSet
139-from lp.bugs.scripts import debbugs
140-from lp.services.scripts.base import (LaunchpadCronScript,
141- LaunchpadScriptFailure)
142-from lp.services.messages.interfaces.message import (
143- InvalidEmailMessage,
144- IMessageSet,
145- )
146-
147-
148-# setup core values and defaults
149-debbugs_location_default = '/srv/bugs-mirror.debian.org/'
150-
151-
152-class DebWatchUpdater(LaunchpadCronScript):
153- loglevel = logging.WARNING
154-
155- def add_my_options(self):
156- self.parser.add_option(
157- '--max', action='store', type='int', dest='max',
158- default=None, help="The maximum number of bugs to synchronise.")
159- self.parser.add_option('--debbugs', action='store', type='string',
160- dest='debbugs',
161- default=debbugs_location_default,
162- help="The location of your debbugs database.")
163-
164- def main(self):
165- if not os.path.exists(
166- os.path.join(self.options.debbugs, 'index/index.db')):
167- raise LaunchpadScriptFailure('%s is not a debbugs db.'
168- % self.options.debbugs)
169-
170- self.txn.begin()
171- self.debbugs_db = debbugs.Database(self.options.debbugs)
172- self.sync()
173- self.txn.commit()
174-
175- self.logger.info('Done!')
176-
177- def sync(self):
178- changedcounter = 0
179-
180- self.logger.info('Finding existing debbugs watches...')
181- debbugs_tracker = getUtility(ILaunchpadCelebrities).debbugs
182- debwatches = debbugs_tracker.watches
183-
184- previousimportset = set([b.remotebug for b in debwatches])
185- self.logger.info(
186- '%d debbugs previously imported.' % len(previousimportset))
187-
188- target_watches = [watch for watch in debwatches if watch.needscheck]
189- self.logger.info(
190- '%d debbugs watches to syncronise.' % len(target_watches))
191-
192- self.logger.info('Sorting bug watches...')
193- target_watches.sort(key=lambda a: a.remotebug)
194-
195- self.logger.info('Syncing bug watches...')
196- for watch in target_watches:
197- if self.sync_watch(watch):
198- changedcounter += 1
199- self.txn.commit()
200- if self.options.max:
201- if changedcounter >= self.options.max:
202- self.logger.info('Synchronised %d bugs!' % changedcounter)
203- return
204-
205- def sync_watch(self, watch):
206- # keep track of whether or not something changed
207- waschanged = False
208- # find the bug in malone
209- malone_bug = watch.bug
210- # find the bug in debbugs
211- debian_bug = self.debbugs_db[int(watch.remotebug)]
212- bugset = getUtility(IBugSet)
213- bugtaskset = getUtility(IBugTaskSet)
214- bugwatchset = getUtility(IBugWatchSet)
215- messageset = getUtility(IMessageSet)
216- debian = getUtility(ILaunchpadCelebrities).debian
217- debbugs_tracker = getUtility(ILaunchpadCelebrities).debbugs
218-
219- # make sure we have tasks for all the debian package linkages, and
220- # also make sure we have updated their status and severity
221- # appropriately.
222- for packagename in debian_bug.packagelist():
223- try:
224- srcpkgname = debian.guessPublishedSourcePackageName(
225- packagename)
226- except NotFoundError:
227- self.logger.error(sys.exc_value)
228- continue
229- search_params = BugTaskSearchParams(user=None, bug=malone_bug,
230- sourcepackagename=srcpkgname)
231- search_params.setDistribution(debian)
232- bugtasks = bugtaskset.search(search_params)
233- if len(bugtasks) == 0:
234- # we need a new task to link the bug to the debian package
235- self.logger.info('Linking %d and debian %s' % (
236- malone_bug.id, srcpkgname.name))
237- # XXX: kiko 2007-02-03:
238- # This code is completely untested and broken.
239- bugtask = malone_bug.addTask(
240- owner=malone_bug.owner, distribution=debian,
241- sourcepackagename=srcpkgname)
242- bugtask.bugwatch = watch
243- waschanged = True
244- else:
245- assert len(bugtasks) == 1, 'Should only find a single task'
246- bugtask = bugtasks[0]
247- status = bugtask.status
248- if status != bugtask.setStatusFromDebbugs(debian_bug.status):
249- waschanged = True
250- severity = bugtask.severity
251- if severity != bugtask.setSeverityFromDebbugs(
252- debian_bug.severity):
253- waschanged = True
254-
255- known_msg_ids = set([msg.rfc822msgid for msg in malone_bug.messages])
256-
257- for raw_msg in debian_bug.comments:
258-
259- # parse it so we can extract the message id easily
260- message = email.message_from_string(raw_msg)
261-
262- # see if we already have imported a message with this id for this
263- # bug
264- message_id = message['message-id']
265- if message_id in known_msg_ids:
266- # Skipping msg that is already imported
267- continue
268-
269- # make sure this message is in the db
270- msg = None
271- try:
272- msg = messageset.fromEmail(raw_msg, parsed_message=message,
273- create_missing_persons=True)
274- except InvalidEmailMessage:
275- self.logger.error('Invalid email: %s' % sys.exc_value)
276- if msg is None:
277- continue
278-
279- # Create the link between the bug and this message.
280- malone_bug.linkMessage(msg)
281-
282- # ok, this is a new message for this bug, so in effect something
283- # has changed
284- waschanged = True
285-
286- # now we need to analyse the message for useful data
287- watches = bugwatchset.fromMessage(msg, malone_bug)
288- for watch in watches:
289- self.logger.info(
290- 'New watch for #%s on %s' % (watch.bug.id, watch.url))
291- waschanged = True
292-
293- # and also for CVE ref clues
294- prior_cves = set(malone_bug.cves)
295- cveset = getUtility(ICveSet)
296- cves = cveset.inMessage(msg)
297- for cve in cves:
298- malone_bug.linkCVE(cve)
299- if cve not in prior_cves:
300- self.logger.info('CVE-%s (%s) found for Malone #%s' % (
301- cve.sequence, cve.status.name, malone_bug.id))
302-
303- # now we know about this message for this bug
304- known_msg_ids.add(message_id)
305-
306- # and best we commit, so that we can see the email that the
307- # librarian has created in the db
308- self.txn.commit()
309-
310- # Mark all merged bugs as duplicates of the lowest-numbered bug
311- if (len(debian_bug.mergedwith) > 0 and
312- min(debian_bug.mergedwith) > debian_bug.id):
313- for merged_id in debian_bug.mergedwith:
314- merged_bug = bugset.queryByRemoteBug(
315- debbugs_tracker, merged_id)
316- if merged_bug is not None:
317- # Bug has been imported already
318- if merged_bug.duplicateof == malone_bug:
319- # we already know about this
320- continue
321- elif merged_bug.duplicateof is not None:
322- # Interesting, we think it's a dup of something else
323- self.logger.warning(
324- 'Debbugs thinks #%d is a dup of #%d' % (
325- merged_bug.id, merged_bug.duplicateof))
326- continue
327- # Go ahead and merge it
328- self.logger.info(
329- "Malone #%d is a duplicate of Malone #%d" % (
330- merged_bug.id, malone_bug.id))
331- merged_bug.duplicateof = malone_bug.id
332-
333- # the dup status has changed
334- waschanged = True
335-
336- # make a note of the remote watch status, if it has changed
337- if watch.remotestatus != debian_bug.status:
338- watch.remotestatus = debian_bug.status
339- waschanged = True
340-
341- # update the watch date details
342- watch.lastchecked = UTC_NOW
343- if waschanged:
344- watch.lastchanged = UTC_NOW
345- self.logger.info('Watch on Malone #%d changed.' % watch.bug.id)
346- return waschanged
347-
348-
349-if __name__ == '__main__':
350- script = DebWatchUpdater('launchpad-debbugs-sync')
351- script.lock_and_run()
352
353=== modified file 'database/schema/comments.sql'
354--- database/schema/comments.sql 2011-08-03 07:52:03 +0000
355+++ database/schema/comments.sql 2011-08-03 17:36:12 +0000
356@@ -438,6 +438,7 @@
357 COMMENT ON COLUMN CodeImport.date_last_successful IS 'When this code import last succeeded. NULL if this import has never succeeded.';
358 COMMENT ON COLUMN CodeImport.assignee IS 'The person in charge of delivering this code import and interacting with the owner.';
359 COMMENT ON COLUMN Codeimport.update_interval IS 'How often should this import be updated. If NULL, defaults to a system-wide value set by the Launchpad administrators.';
360+COMMENT ON COLUMN CodeImport.failure_bug IS 'The bug that causes this code import to fail.';
361 --COMMENT ON COLUMN CodeImport.modified_by IS 'The user modifying the CodeImport. This column is never actually set in the database -- it is only present to communicate to the trigger that creates the event, which will intercept and remove the value for this column.';
362
363 -- CodeImportEvent
364
365=== added file 'database/schema/patch-2208-00-2.sql'
366--- database/schema/patch-2208-00-2.sql 1970-01-01 00:00:00 +0000
367+++ database/schema/patch-2208-00-2.sql 2011-08-03 17:36:12 +0000
368@@ -0,0 +1,16 @@
369+-- Copyright 2010 Canonical Ltd. This software is licensed under the
370+-- GNU Affero General Public License version 3 (see the file LICENSE).
371+SET client_min_messages=ERROR;
372+
373+-- Add reference to bugs to code imports.
374+ALTER TABLE CodeImport
375+ ADD COLUMN failure_bug integer;
376+ALTER TABLE CodeImport
377+ ADD CONSTRAINT codeimport__failure_bug__fk
378+ FOREIGN KEY (failure_bug) REFERENCES Bug;
379+-- A bug for the failure can only be linked if the import is failing (40).
380+ALTER TABLE CodeImport
381+ ADD CONSTRAINT codeimport__failure_bug_requires_failing
382+ CHECK (review_status = 40 OR failure_bug IS NULL);
383+
384+INSERT INTO LaunchpadDatabaseRevision VALUES (2208, 00, 2);
385
386=== removed file 'lib/canonical/launchpad/scripts/debsync.py'
387--- lib/canonical/launchpad/scripts/debsync.py 2011-06-15 15:11:43 +0000
388+++ lib/canonical/launchpad/scripts/debsync.py 1970-01-01 00:00:00 +0000
389@@ -1,201 +0,0 @@
390-# Copyright 2009 Canonical Ltd. This software is licensed under the
391-# GNU Affero General Public License version 3 (see the file LICENSE).
392-
393-"""Functions related to the import of Debbugs bugs into Malone."""
394-
395-__all__ = [
396- 'bug_filter',
397- 'do_import',
398- 'import_bug',
399- ]
400-
401-__metaclass__ = type
402-
403-import datetime
404-import sys
405-
406-from zope.component import getUtility
407-
408-from canonical.database.sqlbase import flush_database_updates
409-from lp.app.errors import NotFoundError
410-from lp.app.interfaces.launchpad import ILaunchpadCelebrities
411-from lp.bugs.interfaces.bug import (
412- CreateBugParams,
413- IBugSet,
414- )
415-from lp.bugs.interfaces.bugwatch import IBugWatchSet
416-from lp.bugs.interfaces.cve import ICveSet
417-from lp.bugs.scripts import debbugs
418-from lp.services.encoding import guess as ensure_unicode
419-from lp.services.messages.interfaces.message import (
420- IMessageSet,
421- InvalidEmailMessage,
422- UnknownSender,
423- )
424-
425-
426-def bug_filter(bug, previous_import_set, target_bugs, target_package_set,
427- minimum_age):
428- """Function to choose which debian bugs will get processed by the sync
429- script.
430- """
431- # don't re-import one that exists already
432- if str(bug.id) in previous_import_set:
433- return False
434- # if we've been given a list, import only those
435- if target_bugs:
436- if bug.id in target_bugs:
437- return True
438- return False
439- # we only want bugs in Sid
440- if not bug.affects_unstable():
441- return False
442- # and we only want RC bugs
443- #if not bug.is_release_critical():
444- # return False
445- # and we only want bugs that affect the packages we care about:
446- if not bug.affects_package(target_package_set):
447- return False
448- # we will not import any dup bugs (any reason to?)
449- if len(bug.mergedwith) > 0:
450- return False
451- # and we won't import any bug that is newer than one week, to give
452- # debian some time to find dups
453- if bug.date > datetime.datetime.now() - datetime.timedelta(minimum_age):
454- return False
455- return True
456-
457-
458-def do_import(logger, max_imports, debbugs_location, target_bugs,
459- target_package_set, previous_import_set, minimum_age, debbugs_pl):
460-
461- # figure out which bugs have been imported previously
462- debbugs_tracker = getUtility(ILaunchpadCelebrities).debbugs
463- for w in debbugs_tracker.watches:
464- previous_import_set.add(w.remotebug)
465- logger.info('%d debian bugs previously imported.' %
466- len(previous_import_set))
467-
468- # find the new bugs to import
469- logger.info('Selecting new debian bugs...')
470- debbugs_db = debbugs.Database(debbugs_location, debbugs_pl)
471- debian_bugs = []
472- for debian_bug in debbugs_db:
473- if bug_filter(debian_bug, previous_import_set, target_bugs,
474- target_package_set, minimum_age):
475- debian_bugs.append(debian_bug)
476- logger.info('%d debian bugs ready to import.' % len(debian_bugs))
477-
478- # put them in ascending order
479- logger.info('Sorting bugs...')
480- debian_bugs.sort(lambda a, b: cmp(a.id, b.id))
481-
482- logger.info('Importing bugs...')
483- newbugs = 0
484- for debian_bug in debian_bugs:
485- newbug = import_bug(debian_bug, logger)
486- if newbug is True:
487- newbugs += 1
488- if max_imports:
489- if newbugs >= max_imports:
490- logger.info('Imported %d new bugs!' % newbugs)
491- break
492-
493-
494-def import_bug(debian_bug, logger):
495- """Consider importing a debian bug, return True if you did."""
496- bugset = getUtility(IBugSet)
497- debbugs_tracker = getUtility(ILaunchpadCelebrities).debbugs
498- malone_bug = bugset.queryByRemoteBug(debbugs_tracker, debian_bug.id)
499- if malone_bug is not None:
500- logger.error('Debbugs #%d was previously imported.' % debian_bug.id)
501- return False
502- # get the email which started it all
503- try:
504- email_txt = debian_bug.comments[0]
505- except IndexError:
506- logger.error('No initial mail for debian #%d' % debian_bug.id)
507- return False
508- except debbugs.LogParseFailed, e:
509- logger.warning(e)
510- return False
511- msg = None
512- messageset = getUtility(IMessageSet)
513- debian = getUtility(ILaunchpadCelebrities).debian
514- ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
515- try:
516- msg = messageset.fromEmail(email_txt, distribution=debian,
517- create_missing_persons=True)
518- except UnknownSender:
519- logger.error('Cannot create person for %s' % sys.exc_value)
520- except InvalidEmailMessage:
521- logger.error('Invalid email: %s' % sys.exc_value)
522- if msg is None:
523- logger.error('Failed to import debian #%d' % debian_bug.id)
524- return False
525-
526- # get the bug details
527- title = debian_bug.subject
528- if not title:
529- title = 'Debbugs #%d with no title' % debian_bug.id
530- title = ensure_unicode(title)
531- # debian_bug.package may have ,-separated package names, but
532- # debian_bug.packagelist[0] is going to be a single package name for
533- # sure. we work through the package list, try to find one we can
534- # work with, otherwise give up
535- srcpkg = pkgname = None
536- for pkgname in debian_bug.packagelist():
537- try:
538- srcpkg = ubuntu.guessPublishedSourcePackageName(pkgname)
539- except NotFoundError:
540- logger.error(sys.exc_value)
541- if srcpkg is None:
542- # none of the package names gave us a source package we can use
543- # XXX sabdfl 2005-09-16: Maybe this should just be connected to the
544- # distro, and allowed to wait for re-assignment to a specific package?
545- logger.error('Unable to find package details for %s' % (
546- debian_bug.package))
547- return False
548- # sometimes debbugs has initial emails that contain the package name, we
549- # can remove that
550- if title.startswith(pkgname + ':'):
551- title = title[len(pkgname) + 2:].strip()
552- params = CreateBugParams(
553- title=title, msg=msg, owner=msg.owner,
554- datecreated=msg.datecreated)
555- params.setBugTarget(distribution=debian, sourcepackagename=srcpkg)
556- malone_bug = bugset.createBug(params)
557- # create a debwatch for this bug
558- thewatch = malone_bug.addWatch(debbugs_tracker, str(debian_bug.id),
559- malone_bug.owner)
560- thewatch.remotestatus = debian_bug.status
561-
562- # link the relevant task to this watch
563- assert len(malone_bug.bugtasks) == 1, 'New bug should have only one task'
564- task = malone_bug.bugtasks[0]
565- task.bugwatch = thewatch
566- task.setStatusFromDebbugs(debian_bug.status)
567- task.setSeverityFromDebbugs(debian_bug.severity)
568-
569- # Let the world know about it!
570- logger.info('%d/%s: %s: %s' % (
571- debian_bug.id, malone_bug.id, debian_bug.package, title))
572-
573- # now we need to analyse the message for bugwatch clues
574- bugwatchset = getUtility(IBugWatchSet)
575- watches = bugwatchset.fromMessage(msg, malone_bug)
576- for watch in watches:
577- logger.info('New watch for %s on %s' % (watch.bug.id, watch.url))
578-
579- # and also for CVE ref clues
580- cveset = getUtility(ICveSet)
581- cves = cveset.inMessage(msg)
582- prior_cves = malone_bug.cves
583- for cve in cves:
584- if cve not in prior_cves:
585- malone_bug.linkCVE(cve)
586- logger.info('CVE-%s (%s) found for Malone #%s' % (
587- cve.sequence, cve.status.name, malone_bug.id))
588-
589- flush_database_updates()
590- return True
591
592=== modified file 'lib/lp/bugs/browser/bugtask.py'
593--- lib/lp/bugs/browser/bugtask.py 2011-08-02 05:35:39 +0000
594+++ lib/lp/bugs/browser/bugtask.py 2011-08-03 17:36:12 +0000
595@@ -3247,7 +3247,25 @@
596 # iteration.
597 bugtasks_by_package = bug.getBugTasksByPackageName(all_bugtasks)
598
599+ latest_parent = None
600+
601 for bugtask in all_bugtasks:
602+ # Series bug targets only display the series name, so they
603+ # must always be preceded by their parent context. Normally
604+ # the parent will have a task, but if not we need to show a
605+ # fake one.
606+ if ISeriesBugTarget.providedBy(bugtask.target):
607+ parent = bugtask.target.bugtarget_parent
608+ else:
609+ latest_parent = parent = bugtask.target
610+
611+ if parent != latest_parent:
612+ latest_parent = parent
613+ bugtask_and_nomination_views.append(
614+ getMultiAdapter(
615+ (parent, self.request),
616+ name='+bugtasks-and-nominations-table-row'))
617+
618 conjoined_master = bugtask.getConjoinedMaster(
619 bugtasks, bugtasks_by_package)
620 view = self._getTableRowView(
621
622=== modified file 'lib/lp/bugs/browser/configure.zcml'
623--- lib/lp/bugs/browser/configure.zcml 2011-07-27 13:23:38 +0000
624+++ lib/lp/bugs/browser/configure.zcml 2011-08-03 17:36:12 +0000
625@@ -561,6 +561,11 @@
626 name="+bugtasks-and-nominations-table-row"
627 template="../templates/bugtask-tasks-and-nominations-table-row.pt"/>
628 <browser:page
629+ for="lp.bugs.interfaces.bugtarget.IBugTarget"
630+ permission="zope.Public"
631+ name="+bugtasks-and-nominations-table-row"
632+ template="../templates/bugtarget-tasks-and-nominations-table-row.pt"/>
633+ <browser:page
634 for="lp.bugs.interfaces.bugtask.IBugTask"
635 name="+bugtask-macros-listing"
636 template="../templates/bugtask-macros-listing.pt"
637
638=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
639--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-08-01 05:25:59 +0000
640+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-08-03 17:36:12 +0000
641@@ -533,6 +533,30 @@
642 foo_bugtasks_and_nominations_view.getBugTaskAndNominationViews())
643 self.assertEqual([], task_and_nomination_views)
644
645+ def test_bugtarget_parent_shown_for_orphaned_series_tasks(self):
646+ # Test that a row is shown for the parent of a series task, even
647+ # if the parent doesn't actually have a task.
648+ series = self.factory.makeProductSeries()
649+ bug = self.factory.makeBug(series=series)
650+ self.assertEqual(2, len(bug.bugtasks))
651+ new_prod = self.factory.makeProduct()
652+ bug.getBugTask(series.product).transitionToTarget(new_prod)
653+
654+ view = create_initialized_view(bug, "+bugtasks-and-nominations-table")
655+ subviews = view.getBugTaskAndNominationViews()
656+ self.assertEqual([
657+ (series.product, '+bugtasks-and-nominations-table-row'),
658+ (bug.getBugTask(series), '+bugtasks-and-nominations-table-row'),
659+ (bug.getBugTask(new_prod), '+bugtasks-and-nominations-table-row'),
660+ ], [(v.context, v.__name__) for v in subviews])
661+
662+ content = subviews[0]()
663+ self.assertIn(
664+ 'href="%s"' % canonical_url(
665+ series.product, path_only_if_possible=True),
666+ content)
667+ self.assertIn(series.product.displayname, content)
668+
669
670 class TestBugTaskEditViewStatusField(TestCaseWithFactory):
671 """We show only those options as possible value in the status
672
673=== removed file 'lib/lp/bugs/doc/bugzilla-import.txt'
674--- lib/lp/bugs/doc/bugzilla-import.txt 2011-06-14 20:35:20 +0000
675+++ lib/lp/bugs/doc/bugzilla-import.txt 1970-01-01 00:00:00 +0000
676@@ -1,562 +0,0 @@
677-Bugzilla Import
678-===============
679-
680-The bugzilla import process makes use of a direct connection to the
681-database. In order to aid in testing, all the database accesses are
682-performed through a single class that can be replaced.
683-
684-We will start by defining a fake backend and some fake information for
685-it to return:
686-
687- >>> from datetime import datetime
688- >>> import pytz
689- >>> UTC = pytz.timezone('UTC')
690-
691- >>> users = [
692- ... ('test@canonical.com', 'Sample User'),
693- ... ('foo.bar@canonical.com', 'Foo Bar'),
694- ... ('new.user@canonical.com', 'New User') # <- not in Launchpad
695- ... ]
696-
697- >>> buginfo = [
698- ... (1, # bug_id
699- ... 1, # assigned_to
700- ... '', # bug_file_loc
701- ... 'normal', # bug_severity
702- ... 'NEW', # status
703- ... datetime(2005, 4, 1, tzinfo=UTC), # creation
704- ... 'Test bug 1', # short_desc,
705- ... 'Linux', # op_sys
706- ... 'P2', # priority
707- ... 'Ubuntu', # product
708- ... 'AMD64', # rep_platform
709- ... 1, # reporter
710- ... '---', # version
711- ... 'mozilla-firefox', # component
712- ... '', # resolution
713- ... 'Ubuntu 5.10', # milestone
714- ... 0, # qa_contact
715- ... 'status', # status_whiteboard
716- ... '', # keywords
717- ... ''), # alias
718- ... # A WONTFIX bug on a non-existant distro package
719- ... (2, 1, 'http://www.ubuntu.com', 'enhancement', 'RESOLVED',
720- ... datetime(2005, 4, 2, tzinfo=UTC), 'Test bug 2',
721- ... 'Linux', 'P1', 'Ubuntu', 'i386', 2, '---', 'unknown',
722- ... 'WONTFIX', '---', 0, '', '', ''),
723- ... # An accepted bug:
724- ... (3, 2, 'http://www.ubuntu.com', 'blocker', 'ASSIGNED',
725- ... datetime(2005, 4, 3, tzinfo=UTC), 'Test bug 3',
726- ... 'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'netapplet',
727- ... '', '---', 0, '', '', 'xyz'),
728- ... # A fixed bug
729- ... (4, 1, 'http://www.ubuntu.com', 'blocker', 'CLOSED',
730- ... datetime(2005, 4, 4, tzinfo=UTC), 'Test bug 4',
731- ... 'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'mozilla-firefox',
732- ... 'FIXED', '---', 0, '', '', 'FooBar'),
733- ... # An UPSTREAM bug
734- ... (5, 1,
735- ... 'http://bugzilla.gnome.org/bugs/show_bug.cgi?id=273041',
736- ... 'blocker', 'UPSTREAM',
737- ... datetime(2005, 4, 4, tzinfo=UTC), 'Test bug 5',
738- ... 'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'evolution',
739- ... '', '---', 0, '', '', 'deb1234'),
740- ... ]
741-
742- >>> ccs = [[], [3], [], [], []]
743-
744- >>> comments = [
745- ... [(1, datetime(2005, 4, 1, tzinfo=UTC), 'First comment'),
746- ... (2, datetime(2005, 4, 1, 1, tzinfo=UTC), 'Second comment')],
747- ... [(1, datetime(2005, 4, 2, tzinfo=UTC), 'First comment'),
748- ... (2, datetime(2005, 4, 2, 1, tzinfo=UTC), 'Second comment')],
749- ... [(2, datetime(2005, 4, 3, tzinfo=UTC), 'First comment'),
750- ... (1, datetime(2005, 4, 3, 1, tzinfo=UTC),
751- ... 'This is related to CVE-2005-1234'),
752- ... (2, datetime(2005, 4, 3, 2, tzinfo=UTC),
753- ... 'Created an attachment (id=1)')],
754- ... [(1, datetime(2005, 4, 4, tzinfo=UTC), 'First comment')],
755- ... [(1, datetime(2005, 4, 5, tzinfo=UTC), 'First comment')],
756- ... ]
757-
758- >>> attachments = [
759- ... [], [],
760- ... [(1, datetime(2005, 4, 3, 2, tzinfo=UTC), 'An attachment',
761- ... 'text/x-patch', True, 'foo.patch', 'the data', 2)],
762- ... [], []
763- ... ]
764-
765- >>> duplicates = [
766- ... (1, 2),
767- ... (3, 4),
768- ... ]
769-
770- >>> class FakeBackend:
771- ... def lookupUser(self, user_id):
772- ... return users[user_id - 1]
773- ... def getBugInfo(self, bug_id):
774- ... return buginfo[bug_id - 1]
775- ... def getBugCcs(self, bug_id):
776- ... return ccs[bug_id - 1]
777- ... def getBugComments(self, bug_id):
778- ... return comments[bug_id - 1]
779- ... def getBugAttachments(self, bug_id):
780- ... return attachments[bug_id - 1]
781- ... def getDuplicates(self):
782- ... return duplicates
783-
784- >>> from itertools import chain
785- >>> from zope.component import getUtility
786- >>> from canonical.launchpad.ftests import login
787- >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
788- >>> from lp.bugs.interfaces.bug import IBugSet
789- >>> from lp.bugs.scripts import bugzilla
790- >>> from lp.registry.interfaces.person import IPersonSet
791-
792-Get a reference to the Ubuntu bug tracker, and log in:
793-
794- >>> login('bug-importer@launchpad.net')
795- >>> bugtracker = getUtility(ILaunchpadCelebrities).ubuntu_bugzilla
796-
797-Now we create a bugzilla.Bugzilla instance to handle the import, using
798-our fake backend data:
799-
800- >>> bz = bugzilla.Bugzilla(None)
801- >>> bz.backend = FakeBackend()
802-
803-In order to verify that things get imported correctly, the following
804-function will be used:
805-
806- >>> def bugInfo(bug):
807- ... print 'Title:', bug.title
808- ... print 'Reporter:', bug.owner.displayname
809- ... print 'Created:', bug.datecreated
810- ... if bug.name:
811- ... print 'Nick: %s' % bug.name
812- ... print 'Subscribers:'
813- ... subscriber_names = sorted(
814- ... p.displayname for p in chain(
815- ... bug.getDirectSubscribers(),
816- ... bug.getIndirectSubscribers()))
817- ... for subscriber_name in subscriber_names:
818- ... print ' %s' % subscriber_name
819- ... for task in bug.bugtasks:
820- ... print 'Task:', task.bugtargetdisplayname
821- ... print ' Status:', task.status.name
822- ... if task.product:
823- ... print ' Product:', task.product.name
824- ... if task.distribution:
825- ... print ' Distro:', task.distribution.name
826- ... if task.sourcepackagename:
827- ... print ' Source package:', task.sourcepackagename.name
828- ... if task.assignee:
829- ... print ' Assignee:', task.assignee.displayname
830- ... if task.importance:
831- ... print ' Importance:', task.importance.name
832- ... if task.statusexplanation:
833- ... print ' Explanation:', task.statusexplanation
834- ... if task.milestone:
835- ... print ' Milestone:', task.milestone.name
836- ... if task.bugwatch:
837- ... print ' Watch:', task.bugwatch.url
838- ... if bug.cves:
839- ... print 'CVEs:'
840- ... for cve in bug.cves:
841- ... print ' %s' % cve.displayname
842- ... print 'Messages:'
843- ... for message in bug.messages:
844- ... print ' Author:', message.owner.displayname
845- ... print ' Date:', message.datecreated
846- ... print ' Subject:', message.subject
847- ... print ' %s' % message.text_contents
848- ... print
849- ... if bug.attachments.any():
850- ... print 'Attachments:'
851- ... for attachment in bug.attachments:
852- ... print ' Title:', attachment.title
853- ... print ' Type:', attachment.type.name
854- ... print ' Name:', attachment.libraryfile.filename
855- ... print ' Mime type:', attachment.libraryfile.mimetype
856-
857-
858-Now we import bug #1 and check the results:
859-
860- >>> bug = bz.handleBug(1)
861- >>> bugInfo(bug)
862- Title: Test bug 1
863- Reporter: Sample Person
864- Created: 2005-04-01 00:00:00+00:00
865- Subscribers:
866- Foo Bar
867- Sample Person
868- Ubuntu Team
869- Task: mozilla-firefox (Ubuntu)
870- Status: NEW
871- Distro: ubuntu
872- Source package: mozilla-firefox
873- Assignee: Sample Person
874- Importance: MEDIUM
875- Explanation: status (Bugzilla status=NEW, product=Ubuntu,
876- component=mozilla-firefox)
877- Milestone: ubuntu-5.10
878- Messages:
879- Author: Sample Person
880- Date: 2005-04-01 00:00:00+00:00
881- Subject: Test bug 1
882- First comment
883- <BLANKLINE>
884- Author: Foo Bar
885- Date: 2005-04-01 01:00:00+00:00
886- Subject: Re: Test bug 1
887- Second comment
888- <BLANKLINE>
889-
890-As well as importing the bug, a bug watch is created, linking the new
891-Launchpad bug to the original Bugzilla bug:
892-
893- >>> linked_bug = getUtility(IBugSet).queryByRemoteBug(bugtracker, 1)
894- >>> linked_bug == bug
895- True
896-
897-This bug watch link is used to prevent multiple imports of the same
898-bug.
899-
900- >>> second_import = bz.handleBug(1)
901- >>> bug == second_import
902- True
903-
904-
905-Next we try bug #2, which is assigned to a non-existant source
906-package, so gets filed directly against the distribution. Some things
907-to notice:
908-
909- * A Launchpad account is created for new.user@canonical.com as a side
910- effect of the import, because they are subscribed to the bug.
911- * The "RESOLVED WONTFIX" status is converted to a status of INVALID.
912- * The fact that the "unknown" package does not exist in Ubuntu has
913- been logged, along with the exception raised by
914- guessPublishedSourcePackageName().
915-
916- >>> print getUtility(IPersonSet).getByEmail('new.user@canonical.com')
917- None
918- >>> bug = bz.handleBug(2)
919- WARNING:lp.bugs.scripts.bugzilla:could not find package name for
920- "unknown": 'Unknown package: unknown'
921- >>> import transaction
922- >>> transaction.commit()
923-
924- >>> bugInfo(bug)
925- Title: Test bug 2
926- Reporter: Foo Bar
927- Created: 2005-04-02 00:00:00+00:00
928- Subscribers:
929- Foo Bar
930- New User
931- Sample Person
932- Ubuntu Team
933- Task: Ubuntu
934- Status: INVALID
935- Distro: ubuntu
936- Assignee: Sample Person
937- Importance: WISHLIST
938- Explanation: Bugzilla status=RESOLVED WONTFIX, product=Ubuntu,
939- component=unknown
940- Messages:
941- Author: Sample Person
942- Date: 2005-04-02 00:00:00+00:00
943- Subject: Test bug 2
944- First comment
945- <BLANKLINE>
946- http://www.ubuntu.com
947- <BLANKLINE>
948- Author: Foo Bar
949- Date: 2005-04-02 01:00:00+00:00
950- Subject: Re: Test bug 2
951- Second comment
952- <BLANKLINE>
953- >>> getUtility(IPersonSet).getByEmail('new.user@canonical.com')
954- <Person at ...>
955-
956-
957-Now import an ASSIGNED bug. Things to note about this import:
958-
959- * the second comment mentions a CVE, causing a link between the bug
960- and CVE to be established.
961- * The attachment on this bug is imported
962-
963- >>> bug = bz.handleBug(3)
964- >>> bugInfo(bug)
965- Title: Test bug 3
966- Reporter: Sample Person
967- Created: 2005-04-03 00:00:00+00:00
968- Nick: xyz
969- Subscribers:
970- Foo Bar
971- Sample Person
972- Ubuntu Team
973- Task: netapplet (Ubuntu)
974- Status: CONFIRMED
975- Distro: ubuntu
976- Source package: netapplet
977- Assignee: Foo Bar
978- Importance: CRITICAL
979- Explanation: Bugzilla status=ASSIGNED, product=Ubuntu,
980- component=netapplet
981- CVEs:
982- CVE-2005-1234
983- Messages:
984- Author: Foo Bar
985- Date: 2005-04-03 00:00:00+00:00
986- Subject: Test bug 3
987- First comment
988- <BLANKLINE>
989- http://www.ubuntu.com
990- <BLANKLINE>
991- Author: Sample Person
992- Date: 2005-04-03 01:00:00+00:00
993- Subject: Re: Test bug 3
994- This is related to CVE-2005-1234
995- <BLANKLINE>
996- Author: Foo Bar
997- Date: 2005-04-03 02:00:00+00:00
998- Subject: Re: Test bug 3
999- Created an attachment (id=1)
1000- <BLANKLINE>
1001- Attachments:
1002- Title: An attachment
1003- Type: PATCH
1004- Name: foo.patch
1005- Mime type: text/plain
1006-
1007-
1008-Next we import a fixed bug:
1009-
1010- >>> bug = bz.handleBug(4)
1011- >>> bugInfo(bug)
1012- Title: Test bug 4
1013- Reporter: Sample Person
1014- Created: 2005-04-04 00:00:00+00:00
1015- Nick: foobar
1016- Subscribers:
1017- Foo Bar
1018- Sample Person
1019- Ubuntu Team
1020- Task: mozilla-firefox (Ubuntu)
1021- Status: FIXRELEASED
1022- Distro: ubuntu
1023- Source package: mozilla-firefox
1024- Assignee: Sample Person
1025- Importance: CRITICAL
1026- Explanation: Bugzilla status=CLOSED FIXED, product=Ubuntu,
1027- component=mozilla-firefox
1028- Messages:
1029- Author: Sample Person
1030- Date: 2005-04-04 00:00:00+00:00
1031- Subject: Test bug 4
1032- First comment
1033- <BLANKLINE>
1034- http://www.ubuntu.com
1035- <BLANKLINE>
1036-
1037-
1038-The Ubuntu bugzilla uses the UPSTREAM state to categorise bugs that
1039-have been forwarded on to the upstream developers. Usually the
1040-upstream bug tracker URL is included in the URL field of the bug.
1041-
1042-The Malone equivalent of this is to create a second task on the bug,
1043-and attach a watch to the upstream bug tracker:
1044-
1045- # Make sane data to play this test.
1046- >>> from zope.component import getUtility
1047- >>> from lp.registry.interfaces.distribution import IDistributionSet
1048- >>> debian = getUtility(IDistributionSet).getByName('debian')
1049- >>> evolution_dsp = debian.getSourcePackage('evolution')
1050- >>> ignore = factory.makeSourcePackagePublishingHistory(
1051- ... distroseries=debian.currentseries,
1052- ... sourcepackagename=evolution_dsp.sourcepackagename)
1053- >>> transaction.commit()
1054-
1055- >>> bug = bz.handleBug(5)
1056- >>> bugInfo(bug)
1057- Title: Test bug 5
1058- Reporter: Sample Person
1059- Created: 2005-04-04 00:00:00+00:00
1060- Subscribers:
1061- Sample Person
1062- Ubuntu Team
1063- Task: Evolution
1064- Status: NEW
1065- Product: evolution
1066- Importance: UNDECIDED
1067- Watch: http://bugzilla.gnome.org/bugs/show_bug.cgi?id=273041
1068- Task: evolution (Ubuntu)
1069- Status: NEW
1070- Distro: ubuntu
1071- Source package: evolution
1072- Assignee: Sample Person
1073- Importance: CRITICAL
1074- Explanation: Bugzilla status=UPSTREAM, product=Ubuntu,
1075- component=evolution
1076- Task: evolution (Debian)
1077- Status: NEW
1078- Distro: debian
1079- Source package: evolution
1080- Importance: UNDECIDED
1081- Watch: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234
1082- Messages:
1083- Author: Sample Person
1084- Date: 2005-04-05 00:00:00+00:00
1085- Subject: Test bug 5
1086- First comment
1087- <BLANKLINE>
1088- http://bugzilla.gnome.org/bugs/show_bug.cgi?id=273041
1089- <BLANKLINE>
1090-
1091-XXX mpt 20060404: In sampledata Evolution uses Malone officially, so adding
1092-a watch to its external bug tracker is a bad example.
1093-
1094-
1095-Severity Mapping
1096-----------------
1097-
1098-Bugzilla severities are mapped to the equivalent Launchpad importance values:
1099-
1100- >>> bug = bugzilla.Bug(bz.backend, 1)
1101- >>> class FakeBugTask:
1102- ... def transitionToStatus(self, status, user):
1103- ... self.status = status
1104- ... def transitionToImportance(self, importance, user):
1105- ... self.importance = importance
1106- >>> bugtask = FakeBugTask()
1107- >>> for severity in ['blocker', 'critical', 'major', 'normal',
1108- ... 'minor', 'trivial', 'enhancement']:
1109- ... bug.bug_severity = severity
1110- ... bug.mapSeverity(bugtask)
1111- ... print '%-11s %s' % (severity, bugtask.importance.name)
1112- blocker CRITICAL
1113- critical CRITICAL
1114- major HIGH
1115- normal MEDIUM
1116- minor LOW
1117- trivial LOW
1118- enhancement WISHLIST
1119-
1120-
1121-Status Mapping
1122---------------
1123-
1124- >>> for status in ['UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED',
1125- ... 'NEEDINFO', 'UPSTREAM', 'PENDINGUPLOAD',
1126- ... 'RESOLVED', 'VERIFIED', 'CLOSED']:
1127- ... bug.bug_status = status
1128- ... bugtask.statusexplanation = ''
1129- ... bug.mapStatus(bugtask)
1130- ... print '%-13s %s' % (status, bugtask.status.name)
1131- UNCONFIRMED NEW
1132- NEW NEW
1133- ASSIGNED CONFIRMED
1134- REOPENED NEW
1135- NEEDINFO INCOMPLETE
1136- UPSTREAM NEW
1137- PENDINGUPLOAD FIXCOMMITTED
1138- RESOLVED INVALID
1139- VERIFIED INVALID
1140- CLOSED INVALID
1141-
1142-(note that RESOLVED, VERIFIED and CLOSED have been mapped to INVALID
1143-here because the Bugzilla resolution is set to WONTFIX).
1144-
1145-
1146-If the bug has been resolved, the resolution will affect the status:
1147-
1148- >>> bug.priority = 'P2'
1149- >>> bug.bug_status = 'RESOLVED'
1150- >>> for resolution in ['FIXED', 'INVALID', 'WONTFIX', 'NOTABUG',
1151- ... 'NOTWARTY', 'UNIVERSE', 'LATER', 'REMIND',
1152- ... 'DUPLICATE', 'WORKSFORME', 'MOVED']:
1153- ... bug.resolution = resolution
1154- ... bugtask.statusexplanation = ''
1155- ... bug.mapStatus(bugtask)
1156- ... print '%-10s %s' % (resolution, bugtask.status.name)
1157- FIXED FIXRELEASED
1158- INVALID INVALID
1159- WONTFIX INVALID
1160- NOTABUG INVALID
1161- NOTWARTY INVALID
1162- UNIVERSE INVALID
1163- LATER INVALID
1164- REMIND INVALID
1165- DUPLICATE INVALID
1166- WORKSFORME INVALID
1167- MOVED INVALID
1168-
1169-
1170-Bug Target Mapping
1171-------------------
1172-
1173-The Bugzilla.getLaunchpadTarget() method is used to map bugzilla bugs
1174-to Launchpad bug targets. This is not general purpose logic: it only
1175-applies to the Ubuntu bugzilla.
1176-
1177-The current mapping only handles bugs filed under the "Ubuntu"
1178-product. If the component the bug is filed under is a known package
1179-name, the bug is targeted at that package in ubuntu. If it isn't,
1180-then the bug is filed directly against the distribution.
1181-
1182- >>> def showMapping(product, component):
1183- ... bug.product = product
1184- ... bug.component = component
1185- ... target = bz.getLaunchpadBugTarget(bug)
1186- ... distribution = target.get('distribution')
1187- ... if distribution:
1188- ... print 'Distribution:', distribution.name
1189- ... spn = target.get('sourcepackagename')
1190- ... if spn:
1191- ... print 'Source package:', spn.name
1192- ... product = target.get('product')
1193- ... if product:
1194- ... print 'Product:', product.name
1195-
1196- >>> showMapping('Ubuntu', 'mozilla-firefox')
1197- Distribution: ubuntu
1198- Source package: mozilla-firefox
1199-
1200- >>> showMapping('Ubuntu', 'netapplet')
1201- Distribution: ubuntu
1202- Source package: netapplet
1203-
1204- >>> showMapping('Ubuntu', 'unknown-package-name')
1205- WARNING:lp.bugs.scripts.bugzilla:could not find package name for
1206- "unknown-package-name": 'Unknown package: unknown-package-name'
1207- Distribution: ubuntu
1208-
1209- >>> showMapping('not-Ubuntu', 'general')
1210- Traceback (most recent call last):
1211- ...
1212- AssertionError: product must be Ubuntu
1213-
1214-
1215-Duplicate Bug Handling
1216-----------------------
1217-
1218-The Bugzilla duplicate bugs table can be used to mark the
1219-corresponding Launchpad bugs as duplicates too:
1220-
1221- >>> from lp.testing.faketransaction import FakeTransaction
1222- >>> bz.processDuplicates(FakeTransaction())
1223-
1224-Now check that the bugs have been marked duplicate:
1225-
1226- >>> bug1 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 1)
1227- >>> bug2 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 2)
1228- >>> bug3 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 3)
1229- >>> bug4 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 4)
1230-
1231- >>> print bug1.duplicateof
1232- None
1233- >>> bug2.duplicateof == bug1
1234- True
1235- >>> bug3.duplicateof == None
1236- True
1237- >>> bug4.duplicateof == bug3
1238- True
1239
1240=== removed file 'lib/lp/bugs/scripts/bugzilla.py'
1241--- lib/lp/bugs/scripts/bugzilla.py 2011-08-02 01:17:15 +0000
1242+++ lib/lp/bugs/scripts/bugzilla.py 1970-01-01 00:00:00 +0000
1243@@ -1,687 +0,0 @@
1244-# Copyright 2009 Canonical Ltd. This software is licensed under the
1245-# GNU Affero General Public License version 3 (see the file LICENSE).
1246-
1247-"""Bugzilla to Launchpad import logic"""
1248-
1249-
1250-# Bugzilla schema:
1251-# http://lxr.mozilla.org/mozilla/source/webtools/bugzilla/Bugzilla/DB/Schema.pm
1252-
1253-# XXX: jamesh 2005-10-18
1254-# Currently unhandled bug info:
1255-# * Operating system and platform
1256-# * version (not really used in Ubuntu bugzilla though)
1257-# * keywords
1258-# * private bugs (none of the canonical-only bugs seem sensitive though)
1259-# * bug dependencies
1260-# * "bug XYZ" references inside comment text (at the moment we just
1261-# insert the full URL to the bug afterwards).
1262-#
1263-# Not all of these are necessary though
1264-
1265-__metaclass__ = type
1266-
1267-from cStringIO import StringIO
1268-import datetime
1269-import logging
1270-import re
1271-
1272-import pytz
1273-from storm.store import Store
1274-from zope.component import getUtility
1275-
1276-from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
1277-from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
1278-from canonical.launchpad.webapp import canonical_url
1279-from lp.app.errors import NotFoundError
1280-from lp.app.interfaces.launchpad import ILaunchpadCelebrities
1281-from lp.bugs.interfaces.bug import (
1282- CreateBugParams,
1283- IBugSet,
1284- )
1285-from lp.bugs.interfaces.bugattachment import (
1286- BugAttachmentType,
1287- IBugAttachmentSet,
1288- )
1289-from lp.bugs.interfaces.bugtask import (
1290- BugTaskImportance,
1291- BugTaskStatus,
1292- IBugTaskSet,
1293- )
1294-from lp.bugs.interfaces.bugwatch import IBugWatchSet
1295-from lp.bugs.interfaces.cve import ICveSet
1296-from lp.registry.interfaces.person import (
1297- IPersonSet,
1298- PersonCreationRationale,
1299- )
1300-from lp.services.messages.interfaces.message import IMessageSet
1301-
1302-
1303-logger = logging.getLogger('lp.bugs.scripts.bugzilla')
1304-
1305-
1306-def _add_tz(dt):
1307- """Convert a naiive datetime value to a UTC datetime value."""
1308- assert dt.tzinfo is None, 'add_tz() only accepts naiive datetime values'
1309- return datetime.datetime(dt.year, dt.month, dt.day,
1310- dt.hour, dt.minute, dt.second,
1311- dt.microsecond, tzinfo=pytz.timezone('UTC'))
1312-
1313-
1314-class BugzillaBackend:
1315- """A wrapper for all the MySQL database access.
1316-
1317- The main purpose of this is to make it possible to test the rest
1318- of the import code without access to a MySQL database.
1319- """
1320- def __init__(self, conn, charset='UTF-8'):
1321- self.conn = conn
1322- self.cursor = conn.cursor()
1323- self.charset = charset
1324-
1325- def _decode(self, s):
1326- if s is not None:
1327- value = s.decode(self.charset, 'replace')
1328- # Postgres doesn't like values outside of the basic multilingual
1329- # plane (U+0000 - U+FFFF), so replace them (and surrogates) with
1330- # U+FFFD (replacement character).
1331- # Existance of these characters generally indicate an encoding
1332- # problem in the existing Bugzilla data.
1333- return re.sub(u'[^\u0000-\ud7ff\ue000-\uffff]', u'\ufffd', value)
1334- else:
1335- return None
1336-
1337- def lookupUser(self, user_id):
1338- """Look up information about a particular Bugzilla user ID"""
1339- self.cursor.execute('SELECT login_name, realname '
1340- ' FROM profiles '
1341- ' WHERE userid = %d' % user_id)
1342- if self.cursor.rowcount != 1:
1343- raise NotFoundError('could not look up user %d' % user_id)
1344- (login_name, realname) = self.cursor.fetchone()
1345- realname = self._decode(realname)
1346- return (login_name, realname)
1347-
1348- def getBugInfo(self, bug_id):
1349- """Retrieve information about a bug."""
1350- self.cursor.execute(
1351- 'SELECT bug_id, assigned_to, bug_file_loc, bug_severity, '
1352- ' bug_status, creation_ts, short_desc, op_sys, priority, '
1353- ' products.name, rep_platform, reporter, version, '
1354- ' components.name, resolution, target_milestone, qa_contact, '
1355- ' status_whiteboard, keywords, alias '
1356- ' FROM bugs '
1357- ' INNER JOIN products ON bugs.product_id = products.id '
1358- ' INNER JOIN components ON bugs.component_id = components.id '
1359- ' WHERE bug_id = %d' % bug_id)
1360- if self.cursor.rowcount != 1:
1361- raise NotFoundError('could not look up bug %d' % bug_id)
1362- (bug_id, assigned_to, bug_file_loc, bug_severity, bug_status,
1363- creation_ts, short_desc, op_sys, priority, product,
1364- rep_platform, reporter, version, component, resolution,
1365- target_milestone, qa_contact, status_whiteboard, keywords,
1366- alias) = self.cursor.fetchone()
1367-
1368- bug_file_loc = self._decode(bug_file_loc)
1369- creation_ts = _add_tz(creation_ts)
1370- product = self._decode(product)
1371- version = self._decode(version)
1372- component = self._decode(component)
1373- status_whiteboard = self._decode(status_whiteboard)
1374- keywords = self._decode(keywords)
1375- alias = self._decode(alias)
1376-
1377- return (bug_id, assigned_to, bug_file_loc, bug_severity,
1378- bug_status, creation_ts, short_desc, op_sys, priority,
1379- product, rep_platform, reporter, version, component,
1380- resolution, target_milestone, qa_contact,
1381- status_whiteboard, keywords, alias)
1382-
1383- def getBugCcs(self, bug_id):
1384- """Get the IDs of the people CC'd to the bug."""
1385- self.cursor.execute('SELECT who FROM cc WHERE bug_id = %d'
1386- % bug_id)
1387- return [row[0] for row in self.cursor.fetchall()]
1388-
1389- def getBugComments(self, bug_id):
1390- """Get the comments for the bug."""
1391- self.cursor.execute('SELECT who, bug_when, thetext '
1392- ' FROM longdescs '
1393- ' WHERE bug_id = %d '
1394- ' ORDER BY bug_when' % bug_id)
1395- # XXX: jamesh 2005-12-07:
1396- # Due to a bug in Debzilla, Ubuntu bugzilla bug 248 has > 7800
1397- # duplicate comments,consisting of someone's signature.
1398- # For the import, just ignore those comments.
1399- return [(who, _add_tz(when), self._decode(thetext))
1400- for (who, when, thetext) in self.cursor.fetchall()
1401- if thetext != '\n--=20\n Jacobo Tarr=EDo | '
1402- 'http://jacobo.tarrio.org/\n\n\n']
1403-
1404- def getBugAttachments(self, bug_id):
1405- """Get the attachments for the bug."""
1406- self.cursor.execute('SELECT attach_id, creation_ts, description, '
1407- ' mimetype, ispatch, filename, thedata, '
1408- ' submitter_id '
1409- ' FROM attachments '
1410- ' WHERE bug_id = %d '
1411- ' ORDER BY attach_id' % bug_id)
1412- return [(attach_id, _add_tz(creation_ts),
1413- self._decode(description), mimetype,
1414- ispatch, self._decode(filename), thedata, submitter_id)
1415- for (attach_id, creation_ts, description,
1416- mimetype, ispatch, filename, thedata,
1417- submitter_id) in self.cursor.fetchall()]
1418-
1419- def findBugs(self, product=None, component=None, status=None):
1420- """Returns the requested bug IDs as a list"""
1421- if product is None:
1422- product = []
1423- if component is None:
1424- component = []
1425- if status is None:
1426- status = []
1427- joins = []
1428- conditions = []
1429- if product:
1430- joins.append(
1431- 'INNER JOIN products ON bugs.product_id = products.id')
1432- conditions.append('products.name IN (%s)' %
1433- ', '.join([self.conn.escape(p) for p in product]))
1434- if component:
1435- joins.append(
1436- 'INNER JOIN components ON bugs.component_id = components.id')
1437- conditions.append('components.name IN (%s)' %
1438- ', '.join([self.conn.escape(c) for c in component]))
1439- if status:
1440- conditions.append('bugs.bug_status IN (%s)' %
1441- ', '.join([self.conn.escape(s) for s in status]))
1442- if conditions:
1443- conditions = 'WHERE %s' % ' AND '.join(conditions)
1444- else:
1445- conditions = ''
1446- self.cursor.execute('SELECT bug_id FROM bugs %s %s ORDER BY bug_id' %
1447- (' '.join(joins), conditions))
1448- return [bug_id for (bug_id,) in self.cursor.fetchall()]
1449-
1450- def getDuplicates(self):
1451- """Returns a list of (dupe_of, dupe) relations."""
1452- self.cursor.execute('SELECT dupe_of, dupe FROM duplicates '
1453- 'ORDER BY dupe, dupe_of')
1454- return [(dupe_of, dupe) for (dupe_of, dupe) in self.cursor.fetchall()]
1455-
1456-
1457-class Bug:
1458- """Representation of a Bugzilla Bug"""
1459- def __init__(self, backend, bug_id):
1460- self.backend = backend
1461- (self.bug_id, self.assigned_to, self.bug_file_loc, self.bug_severity,
1462- self.bug_status, self.creation_ts, self.short_desc, self.op_sys,
1463- self.priority, self.product, self.rep_platform, self.reporter,
1464- self.version, self.component, self.resolution,
1465- self.target_milestone, self.qa_contact, self.status_whiteboard,
1466- self.keywords, self.alias) = backend.getBugInfo(bug_id)
1467-
1468- self._ccs = None
1469- self._comments = None
1470- self._attachments = None
1471-
1472- @property
1473- def ccs(self):
1474- """Return the IDs of people CC'd to this bug"""
1475- if self._ccs is not None:
1476- return self._ccs
1477- self._ccs = self.backend.getBugCcs(self.bug_id)
1478- return self._ccs
1479-
1480- @property
1481- def comments(self):
1482- """Return the comments attached to this bug"""
1483- if self._comments is not None:
1484- return self._comments
1485- self._comments = self.backend.getBugComments(self.bug_id)
1486- return self._comments
1487-
1488- @property
1489- def attachments(self):
1490- """Return the attachments for this bug"""
1491- if self._attachments is not None:
1492- return self._attachments
1493- self._attachments = self.backend.getBugAttachments(self.bug_id)
1494- return self._attachments
1495-
1496- def mapSeverity(self, bugtask):
1497- """Set a Launchpad bug task's importance based on this bug's severity.
1498- """
1499- bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
1500- importance_map = {
1501- 'blocker': BugTaskImportance.CRITICAL,
1502- 'critical': BugTaskImportance.CRITICAL,
1503- 'major': BugTaskImportance.HIGH,
1504- 'normal': BugTaskImportance.MEDIUM,
1505- 'minor': BugTaskImportance.LOW,
1506- 'trivial': BugTaskImportance.LOW,
1507- 'enhancement': BugTaskImportance.WISHLIST
1508- }
1509- importance = importance_map.get(
1510- self.bug_severity, BugTaskImportance.UNKNOWN)
1511- bugtask.transitionToImportance(importance, bug_importer)
1512-
1513- def mapStatus(self, bugtask):
1514- """Set a Launchpad bug task's status based on this bug's status.
1515-
1516- If the bug is in the RESOLVED, VERIFIED or CLOSED states, the
1517- bug resolution is also taken into account when mapping the
1518- status.
1519-
1520- Additional information about the bugzilla status is appended
1521- to the bug task's status explanation.
1522- """
1523- bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
1524-
1525- if self.bug_status == 'ASSIGNED':
1526- bugtask.transitionToStatus(
1527- BugTaskStatus.CONFIRMED, bug_importer)
1528- elif self.bug_status == 'NEEDINFO':
1529- bugtask.transitionToStatus(
1530- BugTaskStatus.INCOMPLETE, bug_importer)
1531- elif self.bug_status == 'PENDINGUPLOAD':
1532- bugtask.transitionToStatus(
1533- BugTaskStatus.FIXCOMMITTED, bug_importer)
1534- elif self.bug_status in ['RESOLVED', 'VERIFIED', 'CLOSED']:
1535- # depends on the resolution:
1536- if self.resolution == 'FIXED':
1537- bugtask.transitionToStatus(
1538- BugTaskStatus.FIXRELEASED, bug_importer)
1539- else:
1540- bugtask.transitionToStatus(
1541- BugTaskStatus.INVALID, bug_importer)
1542- else:
1543- bugtask.transitionToStatus(
1544- BugTaskStatus.NEW, bug_importer)
1545-
1546- # add the status to the notes section, to account for any lost
1547- # information
1548- bugzilla_status = 'Bugzilla status=%s' % self.bug_status
1549- if self.resolution:
1550- bugzilla_status += ' %s' % self.resolution
1551- bugzilla_status += ', product=%s' % self.product
1552- bugzilla_status += ', component=%s' % self.component
1553-
1554- if bugtask.statusexplanation:
1555- bugtask.statusexplanation = '%s (%s)' % (
1556- bugtask.statusexplanation, bugzilla_status)
1557- else:
1558- bugtask.statusexplanation = bugzilla_status
1559-
1560-
1561-class Bugzilla:
1562- """Representation of a bugzilla instance"""
1563-
1564- def __init__(self, conn):
1565- if conn is not None:
1566- self.backend = BugzillaBackend(conn)
1567- else:
1568- self.backend = None
1569- self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
1570- self.debian = getUtility(ILaunchpadCelebrities).debian
1571- self.bugtracker = getUtility(ILaunchpadCelebrities).ubuntu_bugzilla
1572- self.debbugs = getUtility(ILaunchpadCelebrities).debbugs
1573- self.bugset = getUtility(IBugSet)
1574- self.bugtaskset = getUtility(IBugTaskSet)
1575- self.bugwatchset = getUtility(IBugWatchSet)
1576- self.cveset = getUtility(ICveSet)
1577- self.personset = getUtility(IPersonSet)
1578- self.emailset = getUtility(IEmailAddressSet)
1579- self.person_mapping = {}
1580-
1581- def person(self, bugzilla_id):
1582- """Get the Launchpad person corresponding to the given Bugzilla ID"""
1583- # Bugzilla treats a user ID of 0 as a NULL
1584- if bugzilla_id == 0:
1585- return None
1586-
1587- # Try and get the person using a cache of the mapping. We
1588- # check to make sure the person still exists and has not been
1589- # merged.
1590- person = None
1591- launchpad_id = self.person_mapping.get(bugzilla_id)
1592- if launchpad_id is not None:
1593- person = self.personset.get(launchpad_id)
1594- if person is not None and person.merged is not None:
1595- person = None
1596-
1597- # look up the person
1598- if person is None:
1599- email, displayname = self.backend.lookupUser(bugzilla_id)
1600-
1601- person = self.personset.ensurePerson(
1602- email, displayname, PersonCreationRationale.BUGIMPORT,
1603- comment=('when importing bugs from %s'
1604- % self.bugtracker.baseurl))
1605-
1606- # Bugzilla performs similar address checks to Launchpad, so
1607- # if the Launchpad account has no preferred email, use the
1608- # Bugzilla one.
1609- emailaddr = self.emailset.getByEmail(email)
1610- assert emailaddr is not None
1611- if person.preferredemail != emailaddr:
1612- person.validateAndEnsurePreferredEmail(emailaddr)
1613-
1614- self.person_mapping[bugzilla_id] = person.id
1615-
1616- return person
1617-
1618- def _getPackageName(self, bug):
1619- """Returns the source package name for the given bug."""
1620- # we currently only support mapping Ubuntu bugs ...
1621- if bug.product != 'Ubuntu':
1622- raise AssertionError('product must be Ubuntu')
1623-
1624- # kernel bugs are currently filed against the "linux"
1625- # component, which is not a source or binary package. The
1626- # following mapping was provided by BenC:
1627- if bug.component == 'linux':
1628- cutoffdate = datetime.datetime(2004, 12, 1,
1629- tzinfo=pytz.timezone('UTC'))
1630- if bug.bug_status == 'NEEDINFO' and bug.creation_ts < cutoffdate:
1631- pkgname = 'linux-source-2.6.12'
1632- else:
1633- pkgname = 'linux-source-2.6.15'
1634- else:
1635- pkgname = bug.component.encode('ASCII')
1636-
1637- try:
1638- return self.ubuntu.guessPublishedSourcePackageName(pkgname)
1639- except NotFoundError, e:
1640- logger.warning('could not find package name for "%s": %s',
1641- pkgname, str(e))
1642- return None
1643-
1644- def getLaunchpadBugTarget(self, bug):
1645- """Returns a dictionary of arguments to createBug() that correspond
1646- to the given bugzilla bug.
1647- """
1648- srcpkg = self._getPackageName(bug)
1649- return {
1650- 'distribution': self.ubuntu,
1651- 'sourcepackagename': srcpkg,
1652- }
1653-
1654- def getLaunchpadMilestone(self, bug):
1655- """Return the Launchpad milestone for a Bugzilla bug.
1656-
1657- If the milestone does not exist, then it is created.
1658- """
1659- if bug.product != 'Ubuntu':
1660- raise AssertionError('product must be Ubuntu')
1661-
1662- # Bugzilla uses a value of "---" to represent "no selected Milestone"
1663- # Launchpad represents this by setting the milestone column to NULL.
1664- if bug.target_milestone is None or bug.target_milestone == '---':
1665- return None
1666-
1667- # generate a Launchpad name from the Milestone name:
1668- name = re.sub(r'[^a-z0-9\+\.\-]', '-', bug.target_milestone.lower())
1669-
1670- milestone = self.ubuntu.getMilestone(name)
1671- if milestone is None:
1672- milestone = self.ubuntu.currentseries.newMilestone(name)
1673- Store.of(milestone).flush()
1674- return milestone
1675-
1676- def getLaunchpadUpstreamProduct(self, bug):
1677- """Find the upstream product for the given Bugzilla bug.
1678-
1679- This function relies on the package -> product linkage having been
1680- entered in advance.
1681- """
1682- srcpkgname = self._getPackageName(bug)
1683- # find a product series
1684- series = None
1685- for series in self.ubuntu.series:
1686- srcpkg = series.getSourcePackage(srcpkgname)
1687- if srcpkg:
1688- series = srcpkg.productseries
1689- if series:
1690- return series.product
1691- else:
1692- logger.warning('could not find upstream product for '
1693- 'source package "%s"', srcpkgname.name)
1694- return None
1695-
1696- _bug_re = re.compile('bug\s*#?\s*(?P<id>\d+)', re.IGNORECASE)
1697-
1698- def replaceBugRef(self, match):
1699- # XXX: jamesh 2005-10-24:
1700- # this is where bug number rewriting would be plugged in
1701- bug_id = int(match.group('id'))
1702- url = '%s/%d' % (canonical_url(self.bugtracker), bug_id)
1703- return '%s [%s]' % (match.group(0), url)
1704-
1705- def handleBug(self, bug_id):
1706- """Maybe import a single bug.
1707-
1708- If the bug has already been imported (detected by checking for
1709- a bug watch), it is skipped.
1710- """
1711- logger.info('Handling Bugzilla bug %d', bug_id)
1712-
1713- # is there a bug watch on the bug?
1714- lp_bug = self.bugset.queryByRemoteBug(self.bugtracker, bug_id)
1715-
1716- # if we already have an associated bug, don't add a new one.
1717- if lp_bug is not None:
1718- logger.info('Bugzilla bug %d is already being watched by '
1719- 'Launchpad bug %d', bug_id, lp_bug.id)
1720- return lp_bug
1721-
1722- bug = Bug(self.backend, bug_id)
1723-
1724- comments = bug.comments[:]
1725-
1726- # create a message for the initial comment:
1727- msgset = getUtility(IMessageSet)
1728- who, when, text = comments.pop(0)
1729- text = self._bug_re.sub(self.replaceBugRef, text)
1730- # If a URL is associated with the bug, add it to the description:
1731- if bug.bug_file_loc:
1732- text = text + '\n\n' + bug.bug_file_loc
1733- # the initial comment can't be empty:
1734- if not text.strip():
1735- text = '<empty comment>'
1736- msg = msgset.fromText(bug.short_desc, text, self.person(who), when)
1737-
1738- # create the bug
1739- target = self.getLaunchpadBugTarget(bug)
1740- params = CreateBugParams(
1741- msg=msg, datecreated=bug.creation_ts, title=bug.short_desc,
1742- owner=self.person(bug.reporter))
1743- params.setBugTarget(**target)
1744- lp_bug = self.bugset.createBug(params)
1745-
1746- # add the bug watch:
1747- lp_bug.addWatch(self.bugtracker, str(bug.bug_id), lp_bug.owner)
1748-
1749- # add remaining comments, and add CVEs found in all text
1750- lp_bug.findCvesInText(text, lp_bug.owner)
1751- for (who, when, text) in comments:
1752- text = self._bug_re.sub(self.replaceBugRef, text)
1753- msg = msgset.fromText(msg.followup_title, text,
1754- self.person(who), when)
1755- lp_bug.linkMessage(msg)
1756-
1757- # subscribe QA contact and CC's
1758- if bug.qa_contact:
1759- lp_bug.subscribe(
1760- self.person(bug.qa_contact), self.person(bug.reporter))
1761- for cc in bug.ccs:
1762- lp_bug.subscribe(self.person(cc), self.person(bug.reporter))
1763-
1764- # translate bugzilla status and severity to LP equivalents
1765- task = lp_bug.bugtasks[0]
1766- task.datecreated = bug.creation_ts
1767- task.transitionToAssignee(self.person(bug.assigned_to))
1768- task.statusexplanation = bug.status_whiteboard
1769- bug.mapSeverity(task)
1770- bug.mapStatus(task)
1771-
1772- # bugs with an alias of the form "deb1234" have been imported
1773- # from the Debian bug tracker by the "debzilla" program. For
1774- # these bugs, generate a task and watch on the corresponding
1775- # bugs.debian.org bug.
1776- if bug.alias:
1777- if re.match(r'^deb\d+$', bug.alias):
1778- watch = self.bugwatchset.createBugWatch(
1779- lp_bug, lp_bug.owner, self.debbugs, bug.alias[3:])
1780- debtarget = self.debian
1781- if target['sourcepackagename']:
1782- debtarget = debtarget.getSourcePackage(
1783- target['sourcepackagename'])
1784- debtask = self.bugtaskset.createTask(
1785- lp_bug, lp_bug.owner, debtarget)
1786- debtask.datecreated = bug.creation_ts
1787- debtask.bugwatch = watch
1788- else:
1789- # generate a Launchpad name from the alias:
1790- name = re.sub(r'[^a-z0-9\+\.\-]', '-', bug.alias.lower())
1791- lp_bug.name = name
1792-
1793- # for UPSTREAM bugs, try to find whether the URL field contains
1794- # a bug reference.
1795- if bug.bug_status == 'UPSTREAM':
1796- # see if the URL field contains a bug tracker reference
1797- watches = self.bugwatchset.fromText(bug.bug_file_loc,
1798- lp_bug, lp_bug.owner)
1799- # find the upstream product for this bug
1800- product = self.getLaunchpadUpstreamProduct(bug)
1801-
1802- # if we created a watch, and there is an upstream product,
1803- # create a new task and link it to the watch.
1804- if len(watches) > 0:
1805- if product:
1806- upstreamtask = self.bugtaskset.createTask(
1807- lp_bug, lp_bug.owner, product)
1808- upstreamtask.datecreated = bug.creation_ts
1809- upstreamtask.bugwatch = watches[0]
1810- else:
1811- logger.warning('Could not find upstream product to link '
1812- 'bug %d to', lp_bug.id)
1813-
1814- # translate milestone linkage
1815- task.milestone = self.getLaunchpadMilestone(bug)
1816-
1817- # import attachments
1818- for (attach_id, creation_ts, description, mimetype, ispatch,
1819- filename, thedata, submitter_id) in bug.attachments:
1820- # if the filename is missing for some reason, use a generic one.
1821- if filename is None or filename.strip() == '':
1822- filename = 'untitled'
1823- logger.debug('Creating attachment %s for bug %d',
1824- filename, bug.bug_id)
1825- if ispatch:
1826- attach_type = BugAttachmentType.PATCH
1827- mimetype = 'text/plain'
1828- else:
1829- attach_type = BugAttachmentType.UNSPECIFIED
1830-
1831- # look for a message starting with "Created an attachment (id=NN)"
1832- for msg in lp_bug.messages:
1833- if msg.text_contents.startswith(
1834- 'Created an attachment (id=%d)' % attach_id):
1835- break
1836- else:
1837- # could not find the add message, so create one:
1838- msg = msgset.fromText(description,
1839- 'Created attachment %s' % filename,
1840- self.person(submitter_id),
1841- creation_ts)
1842- lp_bug.linkMessage(msg)
1843-
1844- filealias = getUtility(ILibraryFileAliasSet).create(
1845- name=filename,
1846- size=len(thedata),
1847- file=StringIO(thedata),
1848- contentType=mimetype)
1849-
1850- getUtility(IBugAttachmentSet).create(
1851- bug=lp_bug, filealias=filealias, attach_type=attach_type,
1852- title=description, message=msg)
1853-
1854- return lp_bug
1855-
1856- def processDuplicates(self, trans):
1857- """Mark Launchpad bugs as duplicates based on Bugzilla duplicates.
1858-
1859- Launchpad bug A will be marked as a duplicate of bug B if:
1860- * bug A watches bugzilla bug A'
1861- * bug B watches bugzilla bug B'
1862- * bug A' is a duplicate of bug B'
1863- * bug A is not currently a duplicate of any other bug.
1864- """
1865-
1866- logger.info('Processing duplicate bugs')
1867- bugmap = {}
1868-
1869- def getlpbug(bugid):
1870- """Get the Launchpad bug corresponding to the given remote ID
1871-
1872- This function makes use of a cache dictionary to reduce the
1873- number of lookups.
1874- """
1875- lpbugid = bugmap.get(bugid)
1876- if lpbugid is not None:
1877- if lpbugid != 0:
1878- lpbug = self.bugset.get(lpbugid)
1879- else:
1880- lpbug = None
1881- else:
1882- lpbug = self.bugset.queryByRemoteBug(self.bugtracker, bugid)
1883- if lpbug is not None:
1884- bugmap[bugid] = lpbug.id
1885- else:
1886- bugmap[bugid] = 0
1887- return lpbug
1888-
1889- for (dupe_of, dupe) in self.backend.getDuplicates():
1890- # get the Launchpad bugs corresponding to the two Bugzilla bugs:
1891- trans.begin()
1892- lpdupe_of = getlpbug(dupe_of)
1893- lpdupe = getlpbug(dupe)
1894- # if both bugs exist in Launchpad, and lpdupe is not already
1895- # a duplicate, mark it as a duplicate of lpdupe_of.
1896- if (lpdupe_of is not None and lpdupe is not None and
1897- lpdupe.duplicateof is None):
1898- logger.info('Marking %d as a duplicate of %d',
1899- lpdupe.id, lpdupe_of.id)
1900- lpdupe.markAsDuplicate(lpdupe_of)
1901- trans.commit()
1902-
1903- def importBugs(self, trans, product=None, component=None, status=None):
1904- """Import Bugzilla bugs matching the given constraints.
1905-
1906- Each of product, component and status gives a list of
1907- products, components or statuses to limit the import to. An
1908- empty list matches all products, components or statuses.
1909- """
1910- if product is None:
1911- product = []
1912- if component is None:
1913- component = []
1914- if status is None:
1915- status = []
1916-
1917- bugs = self.backend.findBugs(product=product,
1918- component=component,
1919- status=status)
1920- for bug_id in bugs:
1921- trans.begin()
1922- try:
1923- self.handleBug(bug_id)
1924- except (SystemExit, KeyboardInterrupt):
1925- raise
1926- except:
1927- logger.exception('Could not import Bugzilla bug #%d', bug_id)
1928- trans.abort()
1929- else:
1930- trans.commit()
1931
1932=== added file 'lib/lp/bugs/templates/bugtarget-tasks-and-nominations-table-row.pt'
1933--- lib/lp/bugs/templates/bugtarget-tasks-and-nominations-table-row.pt 1970-01-01 00:00:00 +0000
1934+++ lib/lp/bugs/templates/bugtarget-tasks-and-nominations-table-row.pt 2011-08-03 17:36:12 +0000
1935@@ -0,0 +1,11 @@
1936+<tal:bugtarget xmlns:tal="http://xml.zope.org/namespaces/tal">
1937+ <tr style="opacity: 0.5">
1938+ <td></td>
1939+ <td>
1940+ <a tal:attributes="href context/fmt:url;
1941+ class context/image:sprite_css"
1942+ tal:content="context/bugtargetdisplayname" />
1943+ </td>
1944+ <td colspan="5"></td>
1945+ </tr>
1946+</tal:bugtarget>
1947
1948=== modified file 'lib/lp/bugs/tests/test_doc.py'
1949--- lib/lp/bugs/tests/test_doc.py 2010-10-06 18:53:53 +0000
1950+++ lib/lp/bugs/tests/test_doc.py 2011-08-03 17:36:12 +0000
1951@@ -158,12 +158,6 @@
1952 layer=LaunchpadZopelessLayer,
1953 setUp=bugNotificationSendingSetUp,
1954 tearDown=bugNotificationSendingTearDown),
1955- 'bugzilla-import.txt': LayeredDocFileSuite(
1956- '../doc/bugzilla-import.txt',
1957- setUp=setUp, tearDown=tearDown,
1958- stdout_logging_level=logging.WARNING,
1959- layer=LaunchpadZopelessLayer
1960- ),
1961 'bug-export.txt': LayeredDocFileSuite(
1962 '../doc/bug-export.txt',
1963 setUp=setUp, tearDown=tearDown,
1964
1965=== modified file 'lib/lp/code/configure.zcml'
1966--- lib/lp/code/configure.zcml 2011-06-20 18:58:27 +0000
1967+++ lib/lp/code/configure.zcml 2011-08-03 17:36:12 +0000
1968@@ -715,6 +715,7 @@
1969 product
1970 series
1971 review_status
1972+ failure_bug
1973 rcs_type
1974 cvs_root
1975 cvs_module
1976@@ -733,7 +734,8 @@
1977 requestImport"/>
1978 <require
1979 permission="launchpad.Edit"
1980- attributes="updateFromData"/>
1981+ attributes="updateFromData
1982+ linkFailureBug"/>
1983
1984 <!-- ICodeImport has no set_schema, because all modifications should be
1985 done through methods that create CodeImportEvent objects when
1986
1987=== modified file 'lib/lp/code/interfaces/codeimport.py'
1988--- lib/lp/code/interfaces/codeimport.py 2011-02-23 20:26:53 +0000
1989+++ lib/lp/code/interfaces/codeimport.py 2011-08-03 17:36:12 +0000
1990@@ -40,6 +40,7 @@
1991
1992 from canonical.launchpad import _
1993 from lp.app.validators import LaunchpadValidationError
1994+from lp.bugs.interfaces.bug import IBug
1995 from lp.code.enums import (
1996 CodeImportReviewStatus,
1997 RevisionControlSystems,
1998@@ -100,6 +101,11 @@
1999 description=_("The Bazaar branch produced by the "
2000 "import system.")))
2001
2002+ failure_bug = ReferenceChoice(
2003+ title=_('Failure bug'), required=False, readonly=False,
2004+ vocabulary='Bug', schema=IBug,
2005+ description=_("The bug that is causing this import to fail."))
2006+
2007 registrant = PublicPersonChoice(
2008 title=_('Registrant'), required=True, readonly=True,
2009 vocabulary='ValidPersonOrTeam',
2010@@ -188,6 +194,14 @@
2011 None if no changes were made.
2012 """
2013
2014+ def linkFailureBug(bug):
2015+ """Link the bug that causes this import to fail.
2016+
2017+ This method requires the review_status to be FAILING.
2018+
2019+ :param bug: The bug that is causing the import to fail.
2020+ """
2021+
2022 def tryFailingImportAgain(user):
2023 """Try a failing import again.
2024
2025
2026=== modified file 'lib/lp/code/model/codeimport.py'
2027--- lib/lp/code/model/codeimport.py 2011-04-27 01:42:46 +0000
2028+++ lib/lp/code/model/codeimport.py 2011-08-03 17:36:12 +0000
2029@@ -85,6 +85,9 @@
2030 dbName='assignee', foreignKey='Person',
2031 storm_validator=validate_public_person, notNull=False, default=None)
2032
2033+ failure_bug = ForeignKey(
2034+ dbName='failure_bug', foreignKey='Bug', notNull=False, default=None)
2035+
2036 review_status = EnumCol(schema=CodeImportReviewStatus, notNull=True,
2037 default=CodeImportReviewStatus.NEW)
2038
2039@@ -190,6 +193,8 @@
2040 else:
2041 new_whiteboard = whiteboard
2042 self.branch.whiteboard = whiteboard
2043+ if data.get('review_status', None) != CodeImportReviewStatus.FAILING:
2044+ self.failure_bug = None
2045 token = event_set.beginModify(self)
2046 for name, value in data.items():
2047 setattr(self, name, value)
2048@@ -206,6 +211,13 @@
2049 def __repr__(self):
2050 return "<CodeImport for %s>" % self.branch.unique_name
2051
2052+ def linkFailureBug(self, bug):
2053+ """See `ICodeImport`."""
2054+ if self.review_status != CodeImportReviewStatus.FAILING:
2055+ raise AssertionError(
2056+ "review_status is %s not FAILING" % self.review_status.name)
2057+ self.failure_bug = bug
2058+
2059 def tryFailingImportAgain(self, user):
2060 """See `ICodeImport`."""
2061 if self.review_status != CodeImportReviewStatus.FAILING:
2062
2063=== modified file 'lib/lp/code/model/tests/test_codeimport.py'
2064--- lib/lp/code/model/tests/test_codeimport.py 2011-05-27 21:12:25 +0000
2065+++ lib/lp/code/model/tests/test_codeimport.py 2011-08-03 17:36:12 +0000
2066@@ -391,6 +391,31 @@
2067 self.assertEqual(
2068 CodeImportReviewStatus.FAILING, code_import.review_status)
2069
2070+ def test_mark_failing_with_bug(self):
2071+ # Marking an import as failing and linking to a bug.
2072+ code_import = self.factory.makeCodeImport()
2073+ code_import.updateFromData(
2074+ {'review_status':CodeImportReviewStatus.FAILING},
2075+ self.import_operator)
2076+ self.assertEquals(None, code_import.failure_bug)
2077+ bug = self.factory.makeBug()
2078+ code_import.linkFailureBug(bug)
2079+ self.assertEqual(
2080+ CodeImportReviewStatus.FAILING, code_import.review_status)
2081+ self.assertEquals(bug, code_import.failure_bug)
2082+
2083+ def test_mark_no_longer_failing_with_bug(self):
2084+ # Marking an import as no longer failing removes the failure bug link.
2085+ code_import = self.factory.makeCodeImport()
2086+ code_import.updateFromData(
2087+ {'review_status':CodeImportReviewStatus.FAILING},
2088+ self.import_operator)
2089+ code_import.linkFailureBug(self.factory.makeBug())
2090+ code_import.updateFromData(
2091+ {'review_status':CodeImportReviewStatus.REVIEWED},
2092+ self.import_operator)
2093+ self.assertEquals(None, code_import.failure_bug)
2094+
2095
2096 class TestCodeImportResultsAttribute(TestCaseWithFactory):
2097 """Test the results attribute of a CodeImport."""
2098
2099=== modified file 'lib/lp/codehosting/codeimport/tests/test_uifactory.py'
2100--- lib/lp/codehosting/codeimport/tests/test_uifactory.py 2011-07-19 18:09:01 +0000
2101+++ lib/lp/codehosting/codeimport/tests/test_uifactory.py 2011-08-03 17:36:12 +0000
2102@@ -10,6 +10,7 @@
2103 import unittest
2104
2105 from lp.codehosting.codeimport.uifactory import LoggingUIFactory
2106+from lp.services.log.logger import BufferLogger
2107 from lp.testing import (
2108 FakeTime,
2109 TestCase,
2110@@ -22,19 +23,19 @@
2111 def setUp(self):
2112 TestCase.setUp(self)
2113 self.fake_time = FakeTime(12345)
2114- self.messages = []
2115+ self.logger = BufferLogger()
2116
2117 def makeLoggingUIFactory(self):
2118 """Make a `LoggingUIFactory` with fake time and contained output."""
2119 return LoggingUIFactory(
2120- time_source=self.fake_time.now, writer=self.messages.append)
2121+ time_source=self.fake_time.now, logger=self.logger)
2122
2123 def test_first_progress_updates(self):
2124 # The first call to progress generates some output.
2125 factory = self.makeLoggingUIFactory()
2126 bar = factory.nested_progress_bar()
2127 bar.update("hi")
2128- self.assertEqual(['hi'], self.messages)
2129+ self.assertEqual('INFO hi\n', self.logger.getLogBuffer())
2130
2131 def test_second_rapid_progress_doesnt_update(self):
2132 # The second of two progress calls that are less than the factory's
2133@@ -44,7 +45,7 @@
2134 bar.update("hi")
2135 self.fake_time.advance(factory.interval / 2)
2136 bar.update("there")
2137- self.assertEqual(['hi'], self.messages)
2138+ self.assertEqual('INFO hi\n', self.logger.getLogBuffer())
2139
2140 def test_second_slow_progress_updates(self):
2141 # The second of two progress calls that are more than the factory's
2142@@ -54,7 +55,10 @@
2143 bar.update("hi")
2144 self.fake_time.advance(factory.interval * 2)
2145 bar.update("there")
2146- self.assertEqual(['hi', 'there'], self.messages)
2147+ self.assertEqual(
2148+ 'INFO hi\n'
2149+ 'INFO there\n',
2150+ self.logger.getLogBuffer())
2151
2152 def test_first_progress_on_new_bar_updates(self):
2153 # The first progress on a new progress task always generates output.
2154@@ -64,14 +68,15 @@
2155 self.fake_time.advance(factory.interval / 2)
2156 bar2 = factory.nested_progress_bar()
2157 bar2.update("there")
2158- self.assertEqual(['hi', 'hi:there'], self.messages)
2159+ self.assertEqual(
2160+ 'INFO hi\nINFO hi:there\n', self.logger.getLogBuffer())
2161
2162 def test_update_with_count_formats_nicely(self):
2163 # When more details are passed to update, they are formatted nicely.
2164 factory = self.makeLoggingUIFactory()
2165 bar = factory.nested_progress_bar()
2166 bar.update("hi", 1, 8)
2167- self.assertEqual(['hi 1/8'], self.messages)
2168+ self.assertEqual('INFO hi 1/8\n', self.logger.getLogBuffer())
2169
2170 def test_report_transport_activity_reports_bytes_since_last_update(self):
2171 # If there is no call to _progress_updated for 'interval' seconds, the
2172@@ -94,9 +99,54 @@
2173 # activity info.
2174 bar.update("hi", 3, 10)
2175 self.assertEqual(
2176- ['hi 1/10', 'hi 2/10', '110 bytes transferred | hi 2/10',
2177- 'hi 3/10'],
2178- self.messages)
2179+ 'INFO hi 1/10\n'
2180+ 'INFO hi 2/10\n'
2181+ 'INFO 110 bytes transferred | hi 2/10\n'
2182+ 'INFO hi 3/10\n',
2183+ self.logger.getLogBuffer())
2184+
2185+ def test_note(self):
2186+ factory = self.makeLoggingUIFactory()
2187+ factory.note("Banja Luka")
2188+ self.assertEqual('INFO Banja Luka\n', self.logger.getLogBuffer())
2189+
2190+ def test_show_error(self):
2191+ factory = self.makeLoggingUIFactory()
2192+ factory.show_error("Exploding Peaches")
2193+ self.assertEqual(
2194+ "ERROR Exploding Peaches\n", self.logger.getLogBuffer())
2195+
2196+ def test_confirm_action(self):
2197+ factory = self.makeLoggingUIFactory()
2198+ self.assertTrue(factory.confirm_action(
2199+ "How are you %(when)s?", "wellness", {"when": "today"}))
2200+
2201+ def test_show_message(self):
2202+ factory = self.makeLoggingUIFactory()
2203+ factory.show_message("Peaches")
2204+ self.assertEqual("INFO Peaches\n", self.logger.getLogBuffer())
2205+
2206+ def test_get_username(self):
2207+ factory = self.makeLoggingUIFactory()
2208+ self.assertIs(
2209+ None, factory.get_username("Who are you %(when)s?", when="today"))
2210+
2211+ def test_get_password(self):
2212+ factory = self.makeLoggingUIFactory()
2213+ self.assertIs(
2214+ None,
2215+ factory.get_password("How is your %(drink)s", drink="coffee"))
2216+
2217+ def test_show_warning(self):
2218+ factory = self.makeLoggingUIFactory()
2219+ factory.show_warning("Peaches")
2220+ self.assertEqual("WARNING Peaches\n", self.logger.getLogBuffer())
2221+
2222+ def test_show_warning_unicode(self):
2223+ factory = self.makeLoggingUIFactory()
2224+ factory.show_warning(u"Peach\xeas")
2225+ self.assertEqual(
2226+ "WARNING Peach\xc3\xaas\n", self.logger.getLogBuffer())
2227
2228 def test_user_warning(self):
2229 factory = self.makeLoggingUIFactory()
2230@@ -106,9 +156,13 @@
2231 "from_format": "athing",
2232 "to_format": "anotherthing",
2233 }
2234- self.assertEqual([message], self.messages)
2235+ self.assertEqual("WARNING %s\n" % message, self.logger.getLogBuffer())
2236+
2237+ def test_clear_term(self):
2238+ factory = self.makeLoggingUIFactory()
2239+ factory.clear_term()
2240+ self.assertEqual("", self.logger.getLogBuffer())
2241
2242
2243 def test_suite():
2244 return unittest.TestLoader().loadTestsFromName(__name__)
2245-
2246
2247=== modified file 'lib/lp/codehosting/codeimport/uifactory.py'
2248--- lib/lp/codehosting/codeimport/uifactory.py 2011-07-19 18:09:01 +0000
2249+++ lib/lp/codehosting/codeimport/uifactory.py 2011-08-03 17:36:12 +0000
2250@@ -10,38 +10,80 @@
2251 import sys
2252 import time
2253
2254+from bzrlib.ui import NoninteractiveUIFactory
2255 from bzrlib.ui.text import (
2256 TextProgressView,
2257- TextUIFactory,
2258 )
2259
2260
2261-class LoggingUIFactory(TextUIFactory):
2262+class LoggingUIFactory(NoninteractiveUIFactory):
2263 """A UI Factory that produces reasonably sparse logging style output.
2264
2265 The goal is to produce a line of output no more often than once a minute
2266 (by default).
2267 """
2268
2269- def __init__(self, time_source=time.time, writer=None, interval=60.0):
2270+ # XXX: JelmerVernooij 2011-08-02 bug=820127: This seems generic enough to
2271+ # live in bzrlib.ui
2272+
2273+ def __init__(self, time_source=time.time, logger=None, interval=60.0):
2274 """Construct a `LoggingUIFactory`.
2275
2276 :param time_source: A callable that returns time in seconds since the
2277 epoch. Defaults to ``time.time`` and should be replaced with
2278 something deterministic in tests.
2279- :param writer: A callable that takes a string and displays it. It is
2280- not called with newline terminated strings.
2281+ :param logger: The logger object to write to
2282 :param interval: Don't produce output more often than once every this
2283 many seconds. Defaults to 60 seconds.
2284 """
2285- TextUIFactory.__init__(self)
2286+ NoninteractiveUIFactory.__init__(self)
2287 self.interval = interval
2288- self.writer = writer
2289+ self.logger = logger
2290 self._progress_view = LoggingTextProgressView(
2291- time_source, writer, interval)
2292+ time_source, lambda m: logger.info("%s", m), interval)
2293
2294 def show_user_warning(self, warning_id, **message_args):
2295- self.writer(self.format_user_warning(warning_id, message_args))
2296+ self.logger.warning(
2297+ "%s", self.format_user_warning(warning_id, message_args))
2298+
2299+ def show_warning(self, msg):
2300+ if isinstance(msg, unicode):
2301+ msg = msg.encode("utf-8")
2302+ self.logger.warning("%s", msg)
2303+
2304+ def get_username(self, prompt, **kwargs):
2305+ return None
2306+
2307+ def get_password(self, prompt, **kwargs):
2308+ return None
2309+
2310+ def show_message(self, msg):
2311+ self.logger.info("%s", msg)
2312+
2313+ def note(self, msg):
2314+ self.logger.info("%s", msg)
2315+
2316+ def show_error(self, msg):
2317+ self.logger.error("%s", msg)
2318+
2319+ def _progress_updated(self, task):
2320+ """A task has been updated and wants to be displayed.
2321+ """
2322+ if not self._task_stack:
2323+ self.logger.warning("%r updated but no tasks are active", task)
2324+ self._progress_view.show_progress(task)
2325+
2326+ def _progress_all_finished(self):
2327+ self._progress_view.clear()
2328+
2329+ def report_transport_activity(self, transport, byte_count, direction):
2330+ """Called by transports as they do IO.
2331+
2332+ This may update a progress bar, spinner, or similar display.
2333+ By default it does nothing.
2334+ """
2335+ self._progress_view.show_transport_activity(transport,
2336+ direction, byte_count)
2337
2338
2339 class LoggingTextProgressView(TextProgressView):
2340
2341=== modified file 'lib/lp/codehosting/codeimport/worker.py'
2342--- lib/lp/codehosting/codeimport/worker.py 2011-07-20 17:20:03 +0000
2343+++ lib/lp/codehosting/codeimport/worker.py 2011-08-03 17:36:12 +0000
2344@@ -601,8 +601,7 @@
2345 def _doImport(self):
2346 self._logger.info("Starting job.")
2347 saved_factory = bzrlib.ui.ui_factory
2348- bzrlib.ui.ui_factory = LoggingUIFactory(
2349- writer=lambda m: self._logger.info('%s', m))
2350+ bzrlib.ui.ui_factory = LoggingUIFactory(logger=self._logger)
2351 try:
2352 self._logger.info(
2353 "Getting exising bzr branch from central store.")
2354@@ -635,9 +634,11 @@
2355 except Exception, e:
2356 if e.__class__ in self.unsupported_feature_exceptions:
2357 self._logger.info(
2358- "Unable to import branch because of limitations in Bazaar.")
2359+ "Unable to import branch because of limitations in "
2360+ "Bazaar.")
2361 self._logger.info(str(e))
2362- return CodeImportWorkerExitCode.FAILURE_UNSUPPORTED_FEATURE
2363+ return (
2364+ CodeImportWorkerExitCode.FAILURE_UNSUPPORTED_FEATURE)
2365 elif e.__class__ in self.invalid_branch_exceptions:
2366 self._logger.info("Branch invalid: %s", e(str))
2367 return CodeImportWorkerExitCode.FAILURE_INVALID
2368
2369=== modified file 'lib/lp/registry/browser/distributionsourcepackage.py'
2370--- lib/lp/registry/browser/distributionsourcepackage.py 2011-07-28 17:34:34 +0000
2371+++ lib/lp/registry/browser/distributionsourcepackage.py 2011-08-03 17:36:12 +0000
2372@@ -21,6 +21,7 @@
2373 import operator
2374
2375 from lazr.delegates import delegates
2376+from lazr.restful.interfaces import IJSONRequestCache
2377 import pytz
2378 from zope.component import (
2379 adapter,
2380@@ -337,6 +338,11 @@
2381 expose_structural_subscription_data_to_js(
2382 self.context, self.request, self.user)
2383
2384+ pub = self.context.latest_overall_publication
2385+ if pub:
2386+ IJSONRequestCache(self.request).objects['archive_context_url'] = (
2387+ canonical_url(pub.archive, path_only_if_possible=True))
2388+
2389 @property
2390 def label(self):
2391 return self.context.title
2392
2393=== modified file 'lib/lp/registry/templates/distributionsourcepackage-index.pt'
2394--- lib/lp/registry/templates/distributionsourcepackage-index.pt 2011-07-15 11:18:47 +0000
2395+++ lib/lp/registry/templates/distributionsourcepackage-index.pt 2011-08-03 17:36:12 +0000
2396@@ -188,8 +188,6 @@
2397
2398 </tal:rows>
2399 </table>
2400- <script
2401- tal:content="string:LP.cache['archive_context_url'] = '${archive/fmt:url}';"></script>
2402 <metal:js use-macro="archive/@@+macros/expandable-table-js"/>
2403
2404 </div>
2405
2406=== modified file 'lib/lp/services/scripts/tests/__init__.py'
2407--- lib/lp/services/scripts/tests/__init__.py 2010-11-16 12:56:01 +0000
2408+++ lib/lp/services/scripts/tests/__init__.py 2011-08-03 17:36:12 +0000
2409@@ -24,7 +24,6 @@
2410
2411 KNOWN_BROKEN = [
2412 # Needs mysqldb module
2413- 'scripts/bugzilla-import.py',
2414 'scripts/migrate-bugzilla-initialcontacts.py',
2415 # circular import from hell (IHasOwner).
2416 'scripts/clean-sourceforge-project-entries.py',
2417
2418=== removed file 'scripts/bugzilla-import.py'
2419--- scripts/bugzilla-import.py 2010-04-27 19:48:39 +0000
2420+++ scripts/bugzilla-import.py 1970-01-01 00:00:00 +0000
2421@@ -1,97 +0,0 @@
2422-#!/usr/bin/python -S
2423-#
2424-# Copyright 2009 Canonical Ltd. This software is licensed under the
2425-# GNU Affero General Public License version 3 (see the file LICENSE).
2426-
2427-import sys
2428-import logging
2429-import optparse
2430-import MySQLdb
2431-
2432-# pylint: disable-msg=W0403
2433-import _pythonpath
2434-
2435-from canonical.config import config
2436-from canonical.lp import initZopeless
2437-from canonical.launchpad.scripts import (
2438- execute_zcml_for_scripts, logger_options, logger)
2439-from canonical.launchpad.webapp.interaction import setupInteractionByEmail
2440-
2441-from canonical.launchpad.scripts import bugzilla
2442-
2443-
2444-def make_connection(options):
2445- kws = {}
2446- if options.db_name is not None:
2447- kws['db'] = options.db_name
2448- if options.db_user is not None:
2449- kws['user'] = options.db_user
2450- if options.db_password is not None:
2451- kws['passwd'] = options.db_passwd
2452- if options.db_host is not None:
2453- kws['host'] = options.db_host
2454-
2455- return MySQLdb.connect(**kws)
2456-
2457-def main(argv):
2458- parser = optparse.OptionParser(
2459- description=("This script imports bugs from a Bugzilla "
2460- "into Launchpad."))
2461-
2462- parser.add_option('--component', metavar='COMPONENT', action='append',
2463- help='Limit to this bugzilla component',
2464- type='string', dest='component', default=[])
2465- parser.add_option('--status', metavar='STATUS,...', action='store',
2466- help='Only import bugs with the given status',
2467- type='string', dest='status',
2468- default=None)
2469-
2470- # MySQL connection details
2471- parser.add_option('-d', '--dbname', metavar='DB', action='store',
2472- help='The MySQL database name',
2473- type='string', dest='db_name', default='bugs_warty')
2474- parser.add_option('-U', '--username', metavar='USER', action='store',
2475- help='The MySQL user name',
2476- type='string', dest='db_user', default=None)
2477- parser.add_option('-p', '--password', metavar='PASSWORD', action='store',
2478- help='The MySQL password',
2479- type='string', dest='db_password', default=None)
2480- parser.add_option('-H', '--host', metavar='HOST', action='store',
2481- help='The MySQL database host',
2482- type='string', dest='db_host', default=None)
2483-
2484- # logging options
2485- logger_options(parser, logging.INFO)
2486-
2487- options, args = parser.parse_args(argv[1:])
2488- if options.status is not None:
2489- options.status = options.status.split(',')
2490- else:
2491- options.status = []
2492-
2493- logger(options, 'canonical.launchpad.scripts.bugzilla')
2494-
2495- # don't send email
2496- send_email_data = """
2497- [zopeless]
2498- send_email: False
2499- """
2500- config.push('send_email_data', send_email_data)
2501-
2502- execute_zcml_for_scripts()
2503- ztm = initZopeless()
2504- setupInteractionByEmail('bug-importer@launchpad.net')
2505-
2506- db = make_connection(options)
2507- bz = bugzilla.Bugzilla(db)
2508-
2509- bz.importBugs(ztm,
2510- product=['Ubuntu'],
2511- component=options.component,
2512- status=options.status)
2513-
2514- bz.processDuplicates(ztm)
2515- config.pop('send_email_data')
2516-
2517-if __name__ == '__main__':
2518- sys.exit(main(sys.argv))
2519
2520=== removed file 'scripts/migrate-bugzilla-initialcontacts.py'
2521--- scripts/migrate-bugzilla-initialcontacts.py 2011-05-29 01:38:41 +0000
2522+++ scripts/migrate-bugzilla-initialcontacts.py 1970-01-01 00:00:00 +0000
2523@@ -1,91 +0,0 @@
2524-#!/usr/bin/python -S
2525-#
2526-# Copyright 2009 Canonical Ltd. This software is licensed under the
2527-# GNU Affero General Public License version 3 (see the file LICENSE).
2528-
2529-import logging
2530-import MySQLdb
2531-
2532-import _pythonpath
2533-
2534-from zope.component import getUtility
2535-
2536-from canonical.lp import initZopeless
2537-from canonical.launchpad.scripts import execute_zcml_for_scripts
2538-from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
2539-from lp.app.interfaces.launchpad import ILaunchpadCelebrities
2540-from lp.app.errors import NotFoundError
2541-from lp.registry.interfaces.person import IPersonSet
2542-
2543-
2544-execute_zcml_for_scripts()
2545-ztm = initZopeless()
2546-logging.basicConfig(level=logging.INFO)
2547-
2548-ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
2549-techboard = getUtility(IPersonSet).getByName('techboard')
2550-
2551-def getPerson(email, realname):
2552- # The debzilla user acts as a placeholder for "no specific maintainer".
2553- # We don't create a bug contact record for it.
2554- if email is None or email == 'debzilla@ubuntu.com':
2555- return None
2556-
2557- personset = getUtility(IPersonSet)
2558- person = personset.getByEmail(email)
2559- if person:
2560- return person
2561-
2562- # we mark the bugzilla email as preferred email, since it has been
2563- # validated there.
2564- if email.endswith('@lists.ubuntu.com'):
2565- logging.info('creating team for %s (%s)', email, realname)
2566- person = personset.newTeam(techboard, email[:-17], realname)
2567- email = getUtility(IEmailAddressSet).new(email, person.id)
2568- person.setPreferredEmail(email)
2569- else:
2570- logging.info('creating person for %s (%s)', email, realname)
2571- person, email = personset.createPersonAndEmail(email,
2572- displayname=realname)
2573- person.setPreferredEmail(email)
2574-
2575- return person
2576-
2577-
2578-conn = MySQLdb.connect(db='bugs_warty')
2579-cursor = conn.cursor()
2580-
2581-# big arse query that gets all the default assignees and QA contacts:
2582-cursor.execute(
2583- "SELECT components.name, owner.login_name, owner.realname, "
2584- " qa.login_name, qa.realname "
2585- " FROM components "
2586- " JOIN products ON components.product_id = products.id "
2587- " LEFT JOIN profiles AS owner ON components.initialowner = owner.userid"
2588- " LEFT JOIN profiles AS qa ON components.initialqacontact = qa.userid "
2589- " WHERE products.name = 'Ubuntu'")
2590-
2591-for (component, owneremail, ownername, qaemail, qaname) in cursor.fetchall():
2592- logging.info('Processing %s', component)
2593- try:
2594- srcpkgname, binpkgname = ubuntu.getPackageNames(component)
2595- except NotFoundError, e:
2596- logging.warning('could not find package name for "%s": %s', component,
2597- str(e))
2598- continue
2599-
2600- srcpkg = ubuntu.getSourcePackage(srcpkgname)
2601-
2602- # default assignee => maintainer
2603- person = getPerson(owneremail, ownername)
2604- if person:
2605- if not srcpkg.isBugContact(person):
2606- srcpkg.addBugContact(person)
2607-
2608- # QA contact => maintainer
2609- person = getPerson(qaemail, qaname)
2610- if person:
2611- if not srcpkg.isBugContact(person):
2612- srcpkg.addBugContact(person)
2613-
2614-ztm.commit()

Subscribers

People subscribed via source and target branches

to status/vote changes: