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
=== removed file 'cronscripts/create-debwatches.py'
--- cronscripts/create-debwatches.py 2011-05-29 01:36:05 +0000
+++ cronscripts/create-debwatches.py 1970-01-01 00:00:00 +0000
@@ -1,102 +0,0 @@
1#!/usr/bin/python -S
2#
3# Copyright 2009 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6# pylint: disable-msg=C0103,W0403
7
8# This script aims to ensure that there is a Malone watch on Debian bugs
9# that meet certain criteria. The Malone watch will be linked to a BugTask
10# on Debian for that bug. The business of syncing is handled separately.
11
12__metaclass__ = type
13
14import _pythonpath
15import os
16import logging
17
18# zope bits
19from zope.component import getUtility
20
21# canonical launchpad modules
22from lp.services.scripts.base import (
23 LaunchpadCronScript, LaunchpadScriptFailure)
24from canonical.launchpad.scripts.debsync import do_import
25from lp.app.interfaces.launchpad import ILaunchpadCelebrities
26
27
28# setup core values and defaults
29debbugs_location_default = '/srv/bugs-mirror.debian.org/'
30debbugs_pl = '../lib/canonical/launchpad/scripts/debbugs-log.pl'
31
32# the minimum age, in days, of a debbugs bug before we will import it
33MIN_AGE = 7
34
35
36class CreateDebWatches(LaunchpadCronScript):
37 description = """
38 This script syncs debbugs from http://bugs.debian.org/ into Malone.
39 It selects interesting bugs in debian and makes sure that there is a
40 Malone bug for each of them. See debwatchsync for a tool that
41 syncronises the bugs in Malone and debbugs, too.
42 """
43 loglevel = logging.WARNING
44 def add_my_options(self):
45 self.parser.set_defaults(max=None, debbugs=debbugs_location_default)
46 self.parser.add_option('--debbugs', action='store', type='string',
47 dest='debbugs',
48 help="The location of your debbugs database.")
49 self.parser.add_option(
50 '--max', action='store', type='int', dest='max',
51 help="The maximum number of bugs to create.")
52 self.parser.add_option('--package', action='append', type='string',
53 help="A list of packages for which we should import bugs.",
54 dest="packages", default=[])
55
56 def main(self):
57 index_db_path = os.path.join(self.options.debbugs, 'index/index.db')
58 if not os.path.exists(index_db_path):
59 # make sure the debbugs location looks sane
60 raise LaunchpadScriptFailure('%s is not a debbugs db.'
61 % self.options.debbugs)
62
63 # Make sure we import any Debian bugs specified on the command line
64 target_bugs = set()
65 for arg in self.args:
66 try:
67 target_bug = int(arg)
68 except ValueError:
69 self.logger.error(
70 '%s is not a valid debian bug number.' % arg)
71 target_bugs.add(target_bug)
72
73 target_package_set = set()
74 previousimportset = set()
75
76 self.logger.info('Calculating target package set...')
77
78 # first find all the published ubuntu packages
79 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
80 for p in ubuntu.currentrelease.getAllPublishedBinaries():
81 target_package_set.add(
82 p.binarypackagerelease.binarypackagename.name)
83 # then add packages passed on the command line
84 for package in self.options.packages:
85 target_package_set.add(package)
86 self.logger.info(
87 '%d binary packages targeted.' % len(target_package_set))
88
89 self.txn.abort()
90 self.txn.begin()
91 do_import(self.logger, self.options.max, self.options.debbugs,
92 target_bugs, target_package_set, previousimportset, MIN_AGE,
93 debbugs_pl)
94 self.txn.commit()
95
96 self.logger.info('Done!')
97
98
99if __name__ == '__main__':
100 script = CreateDebWatches("debbugs-mkwatch")
101 script.lock_and_run()
102
1030
=== removed file 'cronscripts/update-debwatches.py'
--- cronscripts/update-debwatches.py 2011-06-15 15:11:43 +0000
+++ cronscripts/update-debwatches.py 1970-01-01 00:00:00 +0000
@@ -1,240 +0,0 @@
1#!/usr/bin/python -S
2#
3# Copyright 2009 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6# This script runs through the set of Debbugs watches, and tries to
7# syncronise each of those to the malone bug which is watching it.
8
9import _pythonpath
10import os
11import sys
12import email
13import logging
14
15# zope bits
16from zope.component import getUtility
17
18from canonical.database.constants import UTC_NOW
19from lp.app.errors import NotFoundError
20from lp.app.interfaces.launchpad import ILaunchpadCelebrities
21from lp.bugs.interfaces.bug import IBugSet
22from lp.bugs.interfaces.bugtask import (
23 BugTaskSearchParams,
24 IBugTaskSet,
25 )
26from lp.bugs.interfaces.bugwatch import IBugWatchSet
27from lp.bugs.interfaces.cve import ICveSet
28from lp.bugs.scripts import debbugs
29from lp.services.scripts.base import (LaunchpadCronScript,
30 LaunchpadScriptFailure)
31from lp.services.messages.interfaces.message import (
32 InvalidEmailMessage,
33 IMessageSet,
34 )
35
36
37# setup core values and defaults
38debbugs_location_default = '/srv/bugs-mirror.debian.org/'
39
40
41class DebWatchUpdater(LaunchpadCronScript):
42 loglevel = logging.WARNING
43
44 def add_my_options(self):
45 self.parser.add_option(
46 '--max', action='store', type='int', dest='max',
47 default=None, help="The maximum number of bugs to synchronise.")
48 self.parser.add_option('--debbugs', action='store', type='string',
49 dest='debbugs',
50 default=debbugs_location_default,
51 help="The location of your debbugs database.")
52
53 def main(self):
54 if not os.path.exists(
55 os.path.join(self.options.debbugs, 'index/index.db')):
56 raise LaunchpadScriptFailure('%s is not a debbugs db.'
57 % self.options.debbugs)
58
59 self.txn.begin()
60 self.debbugs_db = debbugs.Database(self.options.debbugs)
61 self.sync()
62 self.txn.commit()
63
64 self.logger.info('Done!')
65
66 def sync(self):
67 changedcounter = 0
68
69 self.logger.info('Finding existing debbugs watches...')
70 debbugs_tracker = getUtility(ILaunchpadCelebrities).debbugs
71 debwatches = debbugs_tracker.watches
72
73 previousimportset = set([b.remotebug for b in debwatches])
74 self.logger.info(
75 '%d debbugs previously imported.' % len(previousimportset))
76
77 target_watches = [watch for watch in debwatches if watch.needscheck]
78 self.logger.info(
79 '%d debbugs watches to syncronise.' % len(target_watches))
80
81 self.logger.info('Sorting bug watches...')
82 target_watches.sort(key=lambda a: a.remotebug)
83
84 self.logger.info('Syncing bug watches...')
85 for watch in target_watches:
86 if self.sync_watch(watch):
87 changedcounter += 1
88 self.txn.commit()
89 if self.options.max:
90 if changedcounter >= self.options.max:
91 self.logger.info('Synchronised %d bugs!' % changedcounter)
92 return
93
94 def sync_watch(self, watch):
95 # keep track of whether or not something changed
96 waschanged = False
97 # find the bug in malone
98 malone_bug = watch.bug
99 # find the bug in debbugs
100 debian_bug = self.debbugs_db[int(watch.remotebug)]
101 bugset = getUtility(IBugSet)
102 bugtaskset = getUtility(IBugTaskSet)
103 bugwatchset = getUtility(IBugWatchSet)
104 messageset = getUtility(IMessageSet)
105 debian = getUtility(ILaunchpadCelebrities).debian
106 debbugs_tracker = getUtility(ILaunchpadCelebrities).debbugs
107
108 # make sure we have tasks for all the debian package linkages, and
109 # also make sure we have updated their status and severity
110 # appropriately.
111 for packagename in debian_bug.packagelist():
112 try:
113 srcpkgname = debian.guessPublishedSourcePackageName(
114 packagename)
115 except NotFoundError:
116 self.logger.error(sys.exc_value)
117 continue
118 search_params = BugTaskSearchParams(user=None, bug=malone_bug,
119 sourcepackagename=srcpkgname)
120 search_params.setDistribution(debian)
121 bugtasks = bugtaskset.search(search_params)
122 if len(bugtasks) == 0:
123 # we need a new task to link the bug to the debian package
124 self.logger.info('Linking %d and debian %s' % (
125 malone_bug.id, srcpkgname.name))
126 # XXX: kiko 2007-02-03:
127 # This code is completely untested and broken.
128 bugtask = malone_bug.addTask(
129 owner=malone_bug.owner, distribution=debian,
130 sourcepackagename=srcpkgname)
131 bugtask.bugwatch = watch
132 waschanged = True
133 else:
134 assert len(bugtasks) == 1, 'Should only find a single task'
135 bugtask = bugtasks[0]
136 status = bugtask.status
137 if status != bugtask.setStatusFromDebbugs(debian_bug.status):
138 waschanged = True
139 severity = bugtask.severity
140 if severity != bugtask.setSeverityFromDebbugs(
141 debian_bug.severity):
142 waschanged = True
143
144 known_msg_ids = set([msg.rfc822msgid for msg in malone_bug.messages])
145
146 for raw_msg in debian_bug.comments:
147
148 # parse it so we can extract the message id easily
149 message = email.message_from_string(raw_msg)
150
151 # see if we already have imported a message with this id for this
152 # bug
153 message_id = message['message-id']
154 if message_id in known_msg_ids:
155 # Skipping msg that is already imported
156 continue
157
158 # make sure this message is in the db
159 msg = None
160 try:
161 msg = messageset.fromEmail(raw_msg, parsed_message=message,
162 create_missing_persons=True)
163 except InvalidEmailMessage:
164 self.logger.error('Invalid email: %s' % sys.exc_value)
165 if msg is None:
166 continue
167
168 # Create the link between the bug and this message.
169 malone_bug.linkMessage(msg)
170
171 # ok, this is a new message for this bug, so in effect something
172 # has changed
173 waschanged = True
174
175 # now we need to analyse the message for useful data
176 watches = bugwatchset.fromMessage(msg, malone_bug)
177 for watch in watches:
178 self.logger.info(
179 'New watch for #%s on %s' % (watch.bug.id, watch.url))
180 waschanged = True
181
182 # and also for CVE ref clues
183 prior_cves = set(malone_bug.cves)
184 cveset = getUtility(ICveSet)
185 cves = cveset.inMessage(msg)
186 for cve in cves:
187 malone_bug.linkCVE(cve)
188 if cve not in prior_cves:
189 self.logger.info('CVE-%s (%s) found for Malone #%s' % (
190 cve.sequence, cve.status.name, malone_bug.id))
191
192 # now we know about this message for this bug
193 known_msg_ids.add(message_id)
194
195 # and best we commit, so that we can see the email that the
196 # librarian has created in the db
197 self.txn.commit()
198
199 # Mark all merged bugs as duplicates of the lowest-numbered bug
200 if (len(debian_bug.mergedwith) > 0 and
201 min(debian_bug.mergedwith) > debian_bug.id):
202 for merged_id in debian_bug.mergedwith:
203 merged_bug = bugset.queryByRemoteBug(
204 debbugs_tracker, merged_id)
205 if merged_bug is not None:
206 # Bug has been imported already
207 if merged_bug.duplicateof == malone_bug:
208 # we already know about this
209 continue
210 elif merged_bug.duplicateof is not None:
211 # Interesting, we think it's a dup of something else
212 self.logger.warning(
213 'Debbugs thinks #%d is a dup of #%d' % (
214 merged_bug.id, merged_bug.duplicateof))
215 continue
216 # Go ahead and merge it
217 self.logger.info(
218 "Malone #%d is a duplicate of Malone #%d" % (
219 merged_bug.id, malone_bug.id))
220 merged_bug.duplicateof = malone_bug.id
221
222 # the dup status has changed
223 waschanged = True
224
225 # make a note of the remote watch status, if it has changed
226 if watch.remotestatus != debian_bug.status:
227 watch.remotestatus = debian_bug.status
228 waschanged = True
229
230 # update the watch date details
231 watch.lastchecked = UTC_NOW
232 if waschanged:
233 watch.lastchanged = UTC_NOW
234 self.logger.info('Watch on Malone #%d changed.' % watch.bug.id)
235 return waschanged
236
237
238if __name__ == '__main__':
239 script = DebWatchUpdater('launchpad-debbugs-sync')
240 script.lock_and_run()
2410
=== modified file 'database/schema/comments.sql'
--- database/schema/comments.sql 2011-08-03 07:52:03 +0000
+++ database/schema/comments.sql 2011-08-03 17:36:12 +0000
@@ -438,6 +438,7 @@
438COMMENT ON COLUMN CodeImport.date_last_successful IS 'When this code import last succeeded. NULL if this import has never succeeded.';438COMMENT ON COLUMN CodeImport.date_last_successful IS 'When this code import last succeeded. NULL if this import has never succeeded.';
439COMMENT ON COLUMN CodeImport.assignee IS 'The person in charge of delivering this code import and interacting with the owner.';439COMMENT ON COLUMN CodeImport.assignee IS 'The person in charge of delivering this code import and interacting with the owner.';
440COMMENT 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.';440COMMENT 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.';
441COMMENT ON COLUMN CodeImport.failure_bug IS 'The bug that causes this code import to fail.';
441--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.';442--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.';
442443
443-- CodeImportEvent444-- CodeImportEvent
444445
=== added file 'database/schema/patch-2208-00-2.sql'
--- database/schema/patch-2208-00-2.sql 1970-01-01 00:00:00 +0000
+++ database/schema/patch-2208-00-2.sql 2011-08-03 17:36:12 +0000
@@ -0,0 +1,16 @@
1-- Copyright 2010 Canonical Ltd. This software is licensed under the
2-- GNU Affero General Public License version 3 (see the file LICENSE).
3SET client_min_messages=ERROR;
4
5-- Add reference to bugs to code imports.
6ALTER TABLE CodeImport
7 ADD COLUMN failure_bug integer;
8ALTER TABLE CodeImport
9 ADD CONSTRAINT codeimport__failure_bug__fk
10 FOREIGN KEY (failure_bug) REFERENCES Bug;
11-- A bug for the failure can only be linked if the import is failing (40).
12ALTER TABLE CodeImport
13 ADD CONSTRAINT codeimport__failure_bug_requires_failing
14 CHECK (review_status = 40 OR failure_bug IS NULL);
15
16INSERT INTO LaunchpadDatabaseRevision VALUES (2208, 00, 2);
017
=== removed file 'lib/canonical/launchpad/scripts/debsync.py'
--- lib/canonical/launchpad/scripts/debsync.py 2011-06-15 15:11:43 +0000
+++ lib/canonical/launchpad/scripts/debsync.py 1970-01-01 00:00:00 +0000
@@ -1,201 +0,0 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Functions related to the import of Debbugs bugs into Malone."""
5
6__all__ = [
7 'bug_filter',
8 'do_import',
9 'import_bug',
10 ]
11
12__metaclass__ = type
13
14import datetime
15import sys
16
17from zope.component import getUtility
18
19from canonical.database.sqlbase import flush_database_updates
20from lp.app.errors import NotFoundError
21from lp.app.interfaces.launchpad import ILaunchpadCelebrities
22from lp.bugs.interfaces.bug import (
23 CreateBugParams,
24 IBugSet,
25 )
26from lp.bugs.interfaces.bugwatch import IBugWatchSet
27from lp.bugs.interfaces.cve import ICveSet
28from lp.bugs.scripts import debbugs
29from lp.services.encoding import guess as ensure_unicode
30from lp.services.messages.interfaces.message import (
31 IMessageSet,
32 InvalidEmailMessage,
33 UnknownSender,
34 )
35
36
37def bug_filter(bug, previous_import_set, target_bugs, target_package_set,
38 minimum_age):
39 """Function to choose which debian bugs will get processed by the sync
40 script.
41 """
42 # don't re-import one that exists already
43 if str(bug.id) in previous_import_set:
44 return False
45 # if we've been given a list, import only those
46 if target_bugs:
47 if bug.id in target_bugs:
48 return True
49 return False
50 # we only want bugs in Sid
51 if not bug.affects_unstable():
52 return False
53 # and we only want RC bugs
54 #if not bug.is_release_critical():
55 # return False
56 # and we only want bugs that affect the packages we care about:
57 if not bug.affects_package(target_package_set):
58 return False
59 # we will not import any dup bugs (any reason to?)
60 if len(bug.mergedwith) > 0:
61 return False
62 # and we won't import any bug that is newer than one week, to give
63 # debian some time to find dups
64 if bug.date > datetime.datetime.now() - datetime.timedelta(minimum_age):
65 return False
66 return True
67
68
69def do_import(logger, max_imports, debbugs_location, target_bugs,
70 target_package_set, previous_import_set, minimum_age, debbugs_pl):
71
72 # figure out which bugs have been imported previously
73 debbugs_tracker = getUtility(ILaunchpadCelebrities).debbugs
74 for w in debbugs_tracker.watches:
75 previous_import_set.add(w.remotebug)
76 logger.info('%d debian bugs previously imported.' %
77 len(previous_import_set))
78
79 # find the new bugs to import
80 logger.info('Selecting new debian bugs...')
81 debbugs_db = debbugs.Database(debbugs_location, debbugs_pl)
82 debian_bugs = []
83 for debian_bug in debbugs_db:
84 if bug_filter(debian_bug, previous_import_set, target_bugs,
85 target_package_set, minimum_age):
86 debian_bugs.append(debian_bug)
87 logger.info('%d debian bugs ready to import.' % len(debian_bugs))
88
89 # put them in ascending order
90 logger.info('Sorting bugs...')
91 debian_bugs.sort(lambda a, b: cmp(a.id, b.id))
92
93 logger.info('Importing bugs...')
94 newbugs = 0
95 for debian_bug in debian_bugs:
96 newbug = import_bug(debian_bug, logger)
97 if newbug is True:
98 newbugs += 1
99 if max_imports:
100 if newbugs >= max_imports:
101 logger.info('Imported %d new bugs!' % newbugs)
102 break
103
104
105def import_bug(debian_bug, logger):
106 """Consider importing a debian bug, return True if you did."""
107 bugset = getUtility(IBugSet)
108 debbugs_tracker = getUtility(ILaunchpadCelebrities).debbugs
109 malone_bug = bugset.queryByRemoteBug(debbugs_tracker, debian_bug.id)
110 if malone_bug is not None:
111 logger.error('Debbugs #%d was previously imported.' % debian_bug.id)
112 return False
113 # get the email which started it all
114 try:
115 email_txt = debian_bug.comments[0]
116 except IndexError:
117 logger.error('No initial mail for debian #%d' % debian_bug.id)
118 return False
119 except debbugs.LogParseFailed, e:
120 logger.warning(e)
121 return False
122 msg = None
123 messageset = getUtility(IMessageSet)
124 debian = getUtility(ILaunchpadCelebrities).debian
125 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
126 try:
127 msg = messageset.fromEmail(email_txt, distribution=debian,
128 create_missing_persons=True)
129 except UnknownSender:
130 logger.error('Cannot create person for %s' % sys.exc_value)
131 except InvalidEmailMessage:
132 logger.error('Invalid email: %s' % sys.exc_value)
133 if msg is None:
134 logger.error('Failed to import debian #%d' % debian_bug.id)
135 return False
136
137 # get the bug details
138 title = debian_bug.subject
139 if not title:
140 title = 'Debbugs #%d with no title' % debian_bug.id
141 title = ensure_unicode(title)
142 # debian_bug.package may have ,-separated package names, but
143 # debian_bug.packagelist[0] is going to be a single package name for
144 # sure. we work through the package list, try to find one we can
145 # work with, otherwise give up
146 srcpkg = pkgname = None
147 for pkgname in debian_bug.packagelist():
148 try:
149 srcpkg = ubuntu.guessPublishedSourcePackageName(pkgname)
150 except NotFoundError:
151 logger.error(sys.exc_value)
152 if srcpkg is None:
153 # none of the package names gave us a source package we can use
154 # XXX sabdfl 2005-09-16: Maybe this should just be connected to the
155 # distro, and allowed to wait for re-assignment to a specific package?
156 logger.error('Unable to find package details for %s' % (
157 debian_bug.package))
158 return False
159 # sometimes debbugs has initial emails that contain the package name, we
160 # can remove that
161 if title.startswith(pkgname + ':'):
162 title = title[len(pkgname) + 2:].strip()
163 params = CreateBugParams(
164 title=title, msg=msg, owner=msg.owner,
165 datecreated=msg.datecreated)
166 params.setBugTarget(distribution=debian, sourcepackagename=srcpkg)
167 malone_bug = bugset.createBug(params)
168 # create a debwatch for this bug
169 thewatch = malone_bug.addWatch(debbugs_tracker, str(debian_bug.id),
170 malone_bug.owner)
171 thewatch.remotestatus = debian_bug.status
172
173 # link the relevant task to this watch
174 assert len(malone_bug.bugtasks) == 1, 'New bug should have only one task'
175 task = malone_bug.bugtasks[0]
176 task.bugwatch = thewatch
177 task.setStatusFromDebbugs(debian_bug.status)
178 task.setSeverityFromDebbugs(debian_bug.severity)
179
180 # Let the world know about it!
181 logger.info('%d/%s: %s: %s' % (
182 debian_bug.id, malone_bug.id, debian_bug.package, title))
183
184 # now we need to analyse the message for bugwatch clues
185 bugwatchset = getUtility(IBugWatchSet)
186 watches = bugwatchset.fromMessage(msg, malone_bug)
187 for watch in watches:
188 logger.info('New watch for %s on %s' % (watch.bug.id, watch.url))
189
190 # and also for CVE ref clues
191 cveset = getUtility(ICveSet)
192 cves = cveset.inMessage(msg)
193 prior_cves = malone_bug.cves
194 for cve in cves:
195 if cve not in prior_cves:
196 malone_bug.linkCVE(cve)
197 logger.info('CVE-%s (%s) found for Malone #%s' % (
198 cve.sequence, cve.status.name, malone_bug.id))
199
200 flush_database_updates()
201 return True
2020
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-08-02 05:35:39 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-08-03 17:36:12 +0000
@@ -3247,7 +3247,25 @@
3247 # iteration.3247 # iteration.
3248 bugtasks_by_package = bug.getBugTasksByPackageName(all_bugtasks)3248 bugtasks_by_package = bug.getBugTasksByPackageName(all_bugtasks)
32493249
3250 latest_parent = None
3251
3250 for bugtask in all_bugtasks:3252 for bugtask in all_bugtasks:
3253 # Series bug targets only display the series name, so they
3254 # must always be preceded by their parent context. Normally
3255 # the parent will have a task, but if not we need to show a
3256 # fake one.
3257 if ISeriesBugTarget.providedBy(bugtask.target):
3258 parent = bugtask.target.bugtarget_parent
3259 else:
3260 latest_parent = parent = bugtask.target
3261
3262 if parent != latest_parent:
3263 latest_parent = parent
3264 bugtask_and_nomination_views.append(
3265 getMultiAdapter(
3266 (parent, self.request),
3267 name='+bugtasks-and-nominations-table-row'))
3268
3251 conjoined_master = bugtask.getConjoinedMaster(3269 conjoined_master = bugtask.getConjoinedMaster(
3252 bugtasks, bugtasks_by_package)3270 bugtasks, bugtasks_by_package)
3253 view = self._getTableRowView(3271 view = self._getTableRowView(
32543272
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2011-07-27 13:23:38 +0000
+++ lib/lp/bugs/browser/configure.zcml 2011-08-03 17:36:12 +0000
@@ -561,6 +561,11 @@
561 name="+bugtasks-and-nominations-table-row"561 name="+bugtasks-and-nominations-table-row"
562 template="../templates/bugtask-tasks-and-nominations-table-row.pt"/>562 template="../templates/bugtask-tasks-and-nominations-table-row.pt"/>
563 <browser:page563 <browser:page
564 for="lp.bugs.interfaces.bugtarget.IBugTarget"
565 permission="zope.Public"
566 name="+bugtasks-and-nominations-table-row"
567 template="../templates/bugtarget-tasks-and-nominations-table-row.pt"/>
568 <browser:page
564 for="lp.bugs.interfaces.bugtask.IBugTask"569 for="lp.bugs.interfaces.bugtask.IBugTask"
565 name="+bugtask-macros-listing"570 name="+bugtask-macros-listing"
566 template="../templates/bugtask-macros-listing.pt"571 template="../templates/bugtask-macros-listing.pt"
567572
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-08-01 05:25:59 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-08-03 17:36:12 +0000
@@ -533,6 +533,30 @@
533 foo_bugtasks_and_nominations_view.getBugTaskAndNominationViews())533 foo_bugtasks_and_nominations_view.getBugTaskAndNominationViews())
534 self.assertEqual([], task_and_nomination_views)534 self.assertEqual([], task_and_nomination_views)
535535
536 def test_bugtarget_parent_shown_for_orphaned_series_tasks(self):
537 # Test that a row is shown for the parent of a series task, even
538 # if the parent doesn't actually have a task.
539 series = self.factory.makeProductSeries()
540 bug = self.factory.makeBug(series=series)
541 self.assertEqual(2, len(bug.bugtasks))
542 new_prod = self.factory.makeProduct()
543 bug.getBugTask(series.product).transitionToTarget(new_prod)
544
545 view = create_initialized_view(bug, "+bugtasks-and-nominations-table")
546 subviews = view.getBugTaskAndNominationViews()
547 self.assertEqual([
548 (series.product, '+bugtasks-and-nominations-table-row'),
549 (bug.getBugTask(series), '+bugtasks-and-nominations-table-row'),
550 (bug.getBugTask(new_prod), '+bugtasks-and-nominations-table-row'),
551 ], [(v.context, v.__name__) for v in subviews])
552
553 content = subviews[0]()
554 self.assertIn(
555 'href="%s"' % canonical_url(
556 series.product, path_only_if_possible=True),
557 content)
558 self.assertIn(series.product.displayname, content)
559
536560
537class TestBugTaskEditViewStatusField(TestCaseWithFactory):561class TestBugTaskEditViewStatusField(TestCaseWithFactory):
538 """We show only those options as possible value in the status562 """We show only those options as possible value in the status
539563
=== removed file 'lib/lp/bugs/doc/bugzilla-import.txt'
--- lib/lp/bugs/doc/bugzilla-import.txt 2011-06-14 20:35:20 +0000
+++ lib/lp/bugs/doc/bugzilla-import.txt 1970-01-01 00:00:00 +0000
@@ -1,562 +0,0 @@
1Bugzilla Import
2===============
3
4The bugzilla import process makes use of a direct connection to the
5database. In order to aid in testing, all the database accesses are
6performed through a single class that can be replaced.
7
8We will start by defining a fake backend and some fake information for
9it to return:
10
11 >>> from datetime import datetime
12 >>> import pytz
13 >>> UTC = pytz.timezone('UTC')
14
15 >>> users = [
16 ... ('test@canonical.com', 'Sample User'),
17 ... ('foo.bar@canonical.com', 'Foo Bar'),
18 ... ('new.user@canonical.com', 'New User') # <- not in Launchpad
19 ... ]
20
21 >>> buginfo = [
22 ... (1, # bug_id
23 ... 1, # assigned_to
24 ... '', # bug_file_loc
25 ... 'normal', # bug_severity
26 ... 'NEW', # status
27 ... datetime(2005, 4, 1, tzinfo=UTC), # creation
28 ... 'Test bug 1', # short_desc,
29 ... 'Linux', # op_sys
30 ... 'P2', # priority
31 ... 'Ubuntu', # product
32 ... 'AMD64', # rep_platform
33 ... 1, # reporter
34 ... '---', # version
35 ... 'mozilla-firefox', # component
36 ... '', # resolution
37 ... 'Ubuntu 5.10', # milestone
38 ... 0, # qa_contact
39 ... 'status', # status_whiteboard
40 ... '', # keywords
41 ... ''), # alias
42 ... # A WONTFIX bug on a non-existant distro package
43 ... (2, 1, 'http://www.ubuntu.com', 'enhancement', 'RESOLVED',
44 ... datetime(2005, 4, 2, tzinfo=UTC), 'Test bug 2',
45 ... 'Linux', 'P1', 'Ubuntu', 'i386', 2, '---', 'unknown',
46 ... 'WONTFIX', '---', 0, '', '', ''),
47 ... # An accepted bug:
48 ... (3, 2, 'http://www.ubuntu.com', 'blocker', 'ASSIGNED',
49 ... datetime(2005, 4, 3, tzinfo=UTC), 'Test bug 3',
50 ... 'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'netapplet',
51 ... '', '---', 0, '', '', 'xyz'),
52 ... # A fixed bug
53 ... (4, 1, 'http://www.ubuntu.com', 'blocker', 'CLOSED',
54 ... datetime(2005, 4, 4, tzinfo=UTC), 'Test bug 4',
55 ... 'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'mozilla-firefox',
56 ... 'FIXED', '---', 0, '', '', 'FooBar'),
57 ... # An UPSTREAM bug
58 ... (5, 1,
59 ... 'http://bugzilla.gnome.org/bugs/show_bug.cgi?id=273041',
60 ... 'blocker', 'UPSTREAM',
61 ... datetime(2005, 4, 4, tzinfo=UTC), 'Test bug 5',
62 ... 'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'evolution',
63 ... '', '---', 0, '', '', 'deb1234'),
64 ... ]
65
66 >>> ccs = [[], [3], [], [], []]
67
68 >>> comments = [
69 ... [(1, datetime(2005, 4, 1, tzinfo=UTC), 'First comment'),
70 ... (2, datetime(2005, 4, 1, 1, tzinfo=UTC), 'Second comment')],
71 ... [(1, datetime(2005, 4, 2, tzinfo=UTC), 'First comment'),
72 ... (2, datetime(2005, 4, 2, 1, tzinfo=UTC), 'Second comment')],
73 ... [(2, datetime(2005, 4, 3, tzinfo=UTC), 'First comment'),
74 ... (1, datetime(2005, 4, 3, 1, tzinfo=UTC),
75 ... 'This is related to CVE-2005-1234'),
76 ... (2, datetime(2005, 4, 3, 2, tzinfo=UTC),
77 ... 'Created an attachment (id=1)')],
78 ... [(1, datetime(2005, 4, 4, tzinfo=UTC), 'First comment')],
79 ... [(1, datetime(2005, 4, 5, tzinfo=UTC), 'First comment')],
80 ... ]
81
82 >>> attachments = [
83 ... [], [],
84 ... [(1, datetime(2005, 4, 3, 2, tzinfo=UTC), 'An attachment',
85 ... 'text/x-patch', True, 'foo.patch', 'the data', 2)],
86 ... [], []
87 ... ]
88
89 >>> duplicates = [
90 ... (1, 2),
91 ... (3, 4),
92 ... ]
93
94 >>> class FakeBackend:
95 ... def lookupUser(self, user_id):
96 ... return users[user_id - 1]
97 ... def getBugInfo(self, bug_id):
98 ... return buginfo[bug_id - 1]
99 ... def getBugCcs(self, bug_id):
100 ... return ccs[bug_id - 1]
101 ... def getBugComments(self, bug_id):
102 ... return comments[bug_id - 1]
103 ... def getBugAttachments(self, bug_id):
104 ... return attachments[bug_id - 1]
105 ... def getDuplicates(self):
106 ... return duplicates
107
108 >>> from itertools import chain
109 >>> from zope.component import getUtility
110 >>> from canonical.launchpad.ftests import login
111 >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
112 >>> from lp.bugs.interfaces.bug import IBugSet
113 >>> from lp.bugs.scripts import bugzilla
114 >>> from lp.registry.interfaces.person import IPersonSet
115
116Get a reference to the Ubuntu bug tracker, and log in:
117
118 >>> login('bug-importer@launchpad.net')
119 >>> bugtracker = getUtility(ILaunchpadCelebrities).ubuntu_bugzilla
120
121Now we create a bugzilla.Bugzilla instance to handle the import, using
122our fake backend data:
123
124 >>> bz = bugzilla.Bugzilla(None)
125 >>> bz.backend = FakeBackend()
126
127In order to verify that things get imported correctly, the following
128function will be used:
129
130 >>> def bugInfo(bug):
131 ... print 'Title:', bug.title
132 ... print 'Reporter:', bug.owner.displayname
133 ... print 'Created:', bug.datecreated
134 ... if bug.name:
135 ... print 'Nick: %s' % bug.name
136 ... print 'Subscribers:'
137 ... subscriber_names = sorted(
138 ... p.displayname for p in chain(
139 ... bug.getDirectSubscribers(),
140 ... bug.getIndirectSubscribers()))
141 ... for subscriber_name in subscriber_names:
142 ... print ' %s' % subscriber_name
143 ... for task in bug.bugtasks:
144 ... print 'Task:', task.bugtargetdisplayname
145 ... print ' Status:', task.status.name
146 ... if task.product:
147 ... print ' Product:', task.product.name
148 ... if task.distribution:
149 ... print ' Distro:', task.distribution.name
150 ... if task.sourcepackagename:
151 ... print ' Source package:', task.sourcepackagename.name
152 ... if task.assignee:
153 ... print ' Assignee:', task.assignee.displayname
154 ... if task.importance:
155 ... print ' Importance:', task.importance.name
156 ... if task.statusexplanation:
157 ... print ' Explanation:', task.statusexplanation
158 ... if task.milestone:
159 ... print ' Milestone:', task.milestone.name
160 ... if task.bugwatch:
161 ... print ' Watch:', task.bugwatch.url
162 ... if bug.cves:
163 ... print 'CVEs:'
164 ... for cve in bug.cves:
165 ... print ' %s' % cve.displayname
166 ... print 'Messages:'
167 ... for message in bug.messages:
168 ... print ' Author:', message.owner.displayname
169 ... print ' Date:', message.datecreated
170 ... print ' Subject:', message.subject
171 ... print ' %s' % message.text_contents
172 ... print
173 ... if bug.attachments.any():
174 ... print 'Attachments:'
175 ... for attachment in bug.attachments:
176 ... print ' Title:', attachment.title
177 ... print ' Type:', attachment.type.name
178 ... print ' Name:', attachment.libraryfile.filename
179 ... print ' Mime type:', attachment.libraryfile.mimetype
180
181
182Now we import bug #1 and check the results:
183
184 >>> bug = bz.handleBug(1)
185 >>> bugInfo(bug)
186 Title: Test bug 1
187 Reporter: Sample Person
188 Created: 2005-04-01 00:00:00+00:00
189 Subscribers:
190 Foo Bar
191 Sample Person
192 Ubuntu Team
193 Task: mozilla-firefox (Ubuntu)
194 Status: NEW
195 Distro: ubuntu
196 Source package: mozilla-firefox
197 Assignee: Sample Person
198 Importance: MEDIUM
199 Explanation: status (Bugzilla status=NEW, product=Ubuntu,
200 component=mozilla-firefox)
201 Milestone: ubuntu-5.10
202 Messages:
203 Author: Sample Person
204 Date: 2005-04-01 00:00:00+00:00
205 Subject: Test bug 1
206 First comment
207 <BLANKLINE>
208 Author: Foo Bar
209 Date: 2005-04-01 01:00:00+00:00
210 Subject: Re: Test bug 1
211 Second comment
212 <BLANKLINE>
213
214As well as importing the bug, a bug watch is created, linking the new
215Launchpad bug to the original Bugzilla bug:
216
217 >>> linked_bug = getUtility(IBugSet).queryByRemoteBug(bugtracker, 1)
218 >>> linked_bug == bug
219 True
220
221This bug watch link is used to prevent multiple imports of the same
222bug.
223
224 >>> second_import = bz.handleBug(1)
225 >>> bug == second_import
226 True
227
228
229Next we try bug #2, which is assigned to a non-existant source
230package, so gets filed directly against the distribution. Some things
231to notice:
232
233 * A Launchpad account is created for new.user@canonical.com as a side
234 effect of the import, because they are subscribed to the bug.
235 * The "RESOLVED WONTFIX" status is converted to a status of INVALID.
236 * The fact that the "unknown" package does not exist in Ubuntu has
237 been logged, along with the exception raised by
238 guessPublishedSourcePackageName().
239
240 >>> print getUtility(IPersonSet).getByEmail('new.user@canonical.com')
241 None
242 >>> bug = bz.handleBug(2)
243 WARNING:lp.bugs.scripts.bugzilla:could not find package name for
244 "unknown": 'Unknown package: unknown'
245 >>> import transaction
246 >>> transaction.commit()
247
248 >>> bugInfo(bug)
249 Title: Test bug 2
250 Reporter: Foo Bar
251 Created: 2005-04-02 00:00:00+00:00
252 Subscribers:
253 Foo Bar
254 New User
255 Sample Person
256 Ubuntu Team
257 Task: Ubuntu
258 Status: INVALID
259 Distro: ubuntu
260 Assignee: Sample Person
261 Importance: WISHLIST
262 Explanation: Bugzilla status=RESOLVED WONTFIX, product=Ubuntu,
263 component=unknown
264 Messages:
265 Author: Sample Person
266 Date: 2005-04-02 00:00:00+00:00
267 Subject: Test bug 2
268 First comment
269 <BLANKLINE>
270 http://www.ubuntu.com
271 <BLANKLINE>
272 Author: Foo Bar
273 Date: 2005-04-02 01:00:00+00:00
274 Subject: Re: Test bug 2
275 Second comment
276 <BLANKLINE>
277 >>> getUtility(IPersonSet).getByEmail('new.user@canonical.com')
278 <Person at ...>
279
280
281Now import an ASSIGNED bug. Things to note about this import:
282
283 * the second comment mentions a CVE, causing a link between the bug
284 and CVE to be established.
285 * The attachment on this bug is imported
286
287 >>> bug = bz.handleBug(3)
288 >>> bugInfo(bug)
289 Title: Test bug 3
290 Reporter: Sample Person
291 Created: 2005-04-03 00:00:00+00:00
292 Nick: xyz
293 Subscribers:
294 Foo Bar
295 Sample Person
296 Ubuntu Team
297 Task: netapplet (Ubuntu)
298 Status: CONFIRMED
299 Distro: ubuntu
300 Source package: netapplet
301 Assignee: Foo Bar
302 Importance: CRITICAL
303 Explanation: Bugzilla status=ASSIGNED, product=Ubuntu,
304 component=netapplet
305 CVEs:
306 CVE-2005-1234
307 Messages:
308 Author: Foo Bar
309 Date: 2005-04-03 00:00:00+00:00
310 Subject: Test bug 3
311 First comment
312 <BLANKLINE>
313 http://www.ubuntu.com
314 <BLANKLINE>
315 Author: Sample Person
316 Date: 2005-04-03 01:00:00+00:00
317 Subject: Re: Test bug 3
318 This is related to CVE-2005-1234
319 <BLANKLINE>
320 Author: Foo Bar
321 Date: 2005-04-03 02:00:00+00:00
322 Subject: Re: Test bug 3
323 Created an attachment (id=1)
324 <BLANKLINE>
325 Attachments:
326 Title: An attachment
327 Type: PATCH
328 Name: foo.patch
329 Mime type: text/plain
330
331
332Next we import a fixed bug:
333
334 >>> bug = bz.handleBug(4)
335 >>> bugInfo(bug)
336 Title: Test bug 4
337 Reporter: Sample Person
338 Created: 2005-04-04 00:00:00+00:00
339 Nick: foobar
340 Subscribers:
341 Foo Bar
342 Sample Person
343 Ubuntu Team
344 Task: mozilla-firefox (Ubuntu)
345 Status: FIXRELEASED
346 Distro: ubuntu
347 Source package: mozilla-firefox
348 Assignee: Sample Person
349 Importance: CRITICAL
350 Explanation: Bugzilla status=CLOSED FIXED, product=Ubuntu,
351 component=mozilla-firefox
352 Messages:
353 Author: Sample Person
354 Date: 2005-04-04 00:00:00+00:00
355 Subject: Test bug 4
356 First comment
357 <BLANKLINE>
358 http://www.ubuntu.com
359 <BLANKLINE>
360
361
362The Ubuntu bugzilla uses the UPSTREAM state to categorise bugs that
363have been forwarded on to the upstream developers. Usually the
364upstream bug tracker URL is included in the URL field of the bug.
365
366The Malone equivalent of this is to create a second task on the bug,
367and attach a watch to the upstream bug tracker:
368
369 # Make sane data to play this test.
370 >>> from zope.component import getUtility
371 >>> from lp.registry.interfaces.distribution import IDistributionSet
372 >>> debian = getUtility(IDistributionSet).getByName('debian')
373 >>> evolution_dsp = debian.getSourcePackage('evolution')
374 >>> ignore = factory.makeSourcePackagePublishingHistory(
375 ... distroseries=debian.currentseries,
376 ... sourcepackagename=evolution_dsp.sourcepackagename)
377 >>> transaction.commit()
378
379 >>> bug = bz.handleBug(5)
380 >>> bugInfo(bug)
381 Title: Test bug 5
382 Reporter: Sample Person
383 Created: 2005-04-04 00:00:00+00:00
384 Subscribers:
385 Sample Person
386 Ubuntu Team
387 Task: Evolution
388 Status: NEW
389 Product: evolution
390 Importance: UNDECIDED
391 Watch: http://bugzilla.gnome.org/bugs/show_bug.cgi?id=273041
392 Task: evolution (Ubuntu)
393 Status: NEW
394 Distro: ubuntu
395 Source package: evolution
396 Assignee: Sample Person
397 Importance: CRITICAL
398 Explanation: Bugzilla status=UPSTREAM, product=Ubuntu,
399 component=evolution
400 Task: evolution (Debian)
401 Status: NEW
402 Distro: debian
403 Source package: evolution
404 Importance: UNDECIDED
405 Watch: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234
406 Messages:
407 Author: Sample Person
408 Date: 2005-04-05 00:00:00+00:00
409 Subject: Test bug 5
410 First comment
411 <BLANKLINE>
412 http://bugzilla.gnome.org/bugs/show_bug.cgi?id=273041
413 <BLANKLINE>
414
415XXX mpt 20060404: In sampledata Evolution uses Malone officially, so adding
416a watch to its external bug tracker is a bad example.
417
418
419Severity Mapping
420----------------
421
422Bugzilla severities are mapped to the equivalent Launchpad importance values:
423
424 >>> bug = bugzilla.Bug(bz.backend, 1)
425 >>> class FakeBugTask:
426 ... def transitionToStatus(self, status, user):
427 ... self.status = status
428 ... def transitionToImportance(self, importance, user):
429 ... self.importance = importance
430 >>> bugtask = FakeBugTask()
431 >>> for severity in ['blocker', 'critical', 'major', 'normal',
432 ... 'minor', 'trivial', 'enhancement']:
433 ... bug.bug_severity = severity
434 ... bug.mapSeverity(bugtask)
435 ... print '%-11s %s' % (severity, bugtask.importance.name)
436 blocker CRITICAL
437 critical CRITICAL
438 major HIGH
439 normal MEDIUM
440 minor LOW
441 trivial LOW
442 enhancement WISHLIST
443
444
445Status Mapping
446--------------
447
448 >>> for status in ['UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED',
449 ... 'NEEDINFO', 'UPSTREAM', 'PENDINGUPLOAD',
450 ... 'RESOLVED', 'VERIFIED', 'CLOSED']:
451 ... bug.bug_status = status
452 ... bugtask.statusexplanation = ''
453 ... bug.mapStatus(bugtask)
454 ... print '%-13s %s' % (status, bugtask.status.name)
455 UNCONFIRMED NEW
456 NEW NEW
457 ASSIGNED CONFIRMED
458 REOPENED NEW
459 NEEDINFO INCOMPLETE
460 UPSTREAM NEW
461 PENDINGUPLOAD FIXCOMMITTED
462 RESOLVED INVALID
463 VERIFIED INVALID
464 CLOSED INVALID
465
466(note that RESOLVED, VERIFIED and CLOSED have been mapped to INVALID
467here because the Bugzilla resolution is set to WONTFIX).
468
469
470If the bug has been resolved, the resolution will affect the status:
471
472 >>> bug.priority = 'P2'
473 >>> bug.bug_status = 'RESOLVED'
474 >>> for resolution in ['FIXED', 'INVALID', 'WONTFIX', 'NOTABUG',
475 ... 'NOTWARTY', 'UNIVERSE', 'LATER', 'REMIND',
476 ... 'DUPLICATE', 'WORKSFORME', 'MOVED']:
477 ... bug.resolution = resolution
478 ... bugtask.statusexplanation = ''
479 ... bug.mapStatus(bugtask)
480 ... print '%-10s %s' % (resolution, bugtask.status.name)
481 FIXED FIXRELEASED
482 INVALID INVALID
483 WONTFIX INVALID
484 NOTABUG INVALID
485 NOTWARTY INVALID
486 UNIVERSE INVALID
487 LATER INVALID
488 REMIND INVALID
489 DUPLICATE INVALID
490 WORKSFORME INVALID
491 MOVED INVALID
492
493
494Bug Target Mapping
495------------------
496
497The Bugzilla.getLaunchpadTarget() method is used to map bugzilla bugs
498to Launchpad bug targets. This is not general purpose logic: it only
499applies to the Ubuntu bugzilla.
500
501The current mapping only handles bugs filed under the "Ubuntu"
502product. If the component the bug is filed under is a known package
503name, the bug is targeted at that package in ubuntu. If it isn't,
504then the bug is filed directly against the distribution.
505
506 >>> def showMapping(product, component):
507 ... bug.product = product
508 ... bug.component = component
509 ... target = bz.getLaunchpadBugTarget(bug)
510 ... distribution = target.get('distribution')
511 ... if distribution:
512 ... print 'Distribution:', distribution.name
513 ... spn = target.get('sourcepackagename')
514 ... if spn:
515 ... print 'Source package:', spn.name
516 ... product = target.get('product')
517 ... if product:
518 ... print 'Product:', product.name
519
520 >>> showMapping('Ubuntu', 'mozilla-firefox')
521 Distribution: ubuntu
522 Source package: mozilla-firefox
523
524 >>> showMapping('Ubuntu', 'netapplet')
525 Distribution: ubuntu
526 Source package: netapplet
527
528 >>> showMapping('Ubuntu', 'unknown-package-name')
529 WARNING:lp.bugs.scripts.bugzilla:could not find package name for
530 "unknown-package-name": 'Unknown package: unknown-package-name'
531 Distribution: ubuntu
532
533 >>> showMapping('not-Ubuntu', 'general')
534 Traceback (most recent call last):
535 ...
536 AssertionError: product must be Ubuntu
537
538
539Duplicate Bug Handling
540----------------------
541
542The Bugzilla duplicate bugs table can be used to mark the
543corresponding Launchpad bugs as duplicates too:
544
545 >>> from lp.testing.faketransaction import FakeTransaction
546 >>> bz.processDuplicates(FakeTransaction())
547
548Now check that the bugs have been marked duplicate:
549
550 >>> bug1 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 1)
551 >>> bug2 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 2)
552 >>> bug3 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 3)
553 >>> bug4 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 4)
554
555 >>> print bug1.duplicateof
556 None
557 >>> bug2.duplicateof == bug1
558 True
559 >>> bug3.duplicateof == None
560 True
561 >>> bug4.duplicateof == bug3
562 True
5630
=== removed file 'lib/lp/bugs/scripts/bugzilla.py'
--- lib/lp/bugs/scripts/bugzilla.py 2011-08-02 01:17:15 +0000
+++ lib/lp/bugs/scripts/bugzilla.py 1970-01-01 00:00:00 +0000
@@ -1,687 +0,0 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Bugzilla to Launchpad import logic"""
5
6
7# Bugzilla schema:
8# http://lxr.mozilla.org/mozilla/source/webtools/bugzilla/Bugzilla/DB/Schema.pm
9
10# XXX: jamesh 2005-10-18
11# Currently unhandled bug info:
12# * Operating system and platform
13# * version (not really used in Ubuntu bugzilla though)
14# * keywords
15# * private bugs (none of the canonical-only bugs seem sensitive though)
16# * bug dependencies
17# * "bug XYZ" references inside comment text (at the moment we just
18# insert the full URL to the bug afterwards).
19#
20# Not all of these are necessary though
21
22__metaclass__ = type
23
24from cStringIO import StringIO
25import datetime
26import logging
27import re
28
29import pytz
30from storm.store import Store
31from zope.component import getUtility
32
33from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
34from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
35from canonical.launchpad.webapp import canonical_url
36from lp.app.errors import NotFoundError
37from lp.app.interfaces.launchpad import ILaunchpadCelebrities
38from lp.bugs.interfaces.bug import (
39 CreateBugParams,
40 IBugSet,
41 )
42from lp.bugs.interfaces.bugattachment import (
43 BugAttachmentType,
44 IBugAttachmentSet,
45 )
46from lp.bugs.interfaces.bugtask import (
47 BugTaskImportance,
48 BugTaskStatus,
49 IBugTaskSet,
50 )
51from lp.bugs.interfaces.bugwatch import IBugWatchSet
52from lp.bugs.interfaces.cve import ICveSet
53from lp.registry.interfaces.person import (
54 IPersonSet,
55 PersonCreationRationale,
56 )
57from lp.services.messages.interfaces.message import IMessageSet
58
59
60logger = logging.getLogger('lp.bugs.scripts.bugzilla')
61
62
63def _add_tz(dt):
64 """Convert a naiive datetime value to a UTC datetime value."""
65 assert dt.tzinfo is None, 'add_tz() only accepts naiive datetime values'
66 return datetime.datetime(dt.year, dt.month, dt.day,
67 dt.hour, dt.minute, dt.second,
68 dt.microsecond, tzinfo=pytz.timezone('UTC'))
69
70
71class BugzillaBackend:
72 """A wrapper for all the MySQL database access.
73
74 The main purpose of this is to make it possible to test the rest
75 of the import code without access to a MySQL database.
76 """
77 def __init__(self, conn, charset='UTF-8'):
78 self.conn = conn
79 self.cursor = conn.cursor()
80 self.charset = charset
81
82 def _decode(self, s):
83 if s is not None:
84 value = s.decode(self.charset, 'replace')
85 # Postgres doesn't like values outside of the basic multilingual
86 # plane (U+0000 - U+FFFF), so replace them (and surrogates) with
87 # U+FFFD (replacement character).
88 # Existance of these characters generally indicate an encoding
89 # problem in the existing Bugzilla data.
90 return re.sub(u'[^\u0000-\ud7ff\ue000-\uffff]', u'\ufffd', value)
91 else:
92 return None
93
94 def lookupUser(self, user_id):
95 """Look up information about a particular Bugzilla user ID"""
96 self.cursor.execute('SELECT login_name, realname '
97 ' FROM profiles '
98 ' WHERE userid = %d' % user_id)
99 if self.cursor.rowcount != 1:
100 raise NotFoundError('could not look up user %d' % user_id)
101 (login_name, realname) = self.cursor.fetchone()
102 realname = self._decode(realname)
103 return (login_name, realname)
104
105 def getBugInfo(self, bug_id):
106 """Retrieve information about a bug."""
107 self.cursor.execute(
108 'SELECT bug_id, assigned_to, bug_file_loc, bug_severity, '
109 ' bug_status, creation_ts, short_desc, op_sys, priority, '
110 ' products.name, rep_platform, reporter, version, '
111 ' components.name, resolution, target_milestone, qa_contact, '
112 ' status_whiteboard, keywords, alias '
113 ' FROM bugs '
114 ' INNER JOIN products ON bugs.product_id = products.id '
115 ' INNER JOIN components ON bugs.component_id = components.id '
116 ' WHERE bug_id = %d' % bug_id)
117 if self.cursor.rowcount != 1:
118 raise NotFoundError('could not look up bug %d' % bug_id)
119 (bug_id, assigned_to, bug_file_loc, bug_severity, bug_status,
120 creation_ts, short_desc, op_sys, priority, product,
121 rep_platform, reporter, version, component, resolution,
122 target_milestone, qa_contact, status_whiteboard, keywords,
123 alias) = self.cursor.fetchone()
124
125 bug_file_loc = self._decode(bug_file_loc)
126 creation_ts = _add_tz(creation_ts)
127 product = self._decode(product)
128 version = self._decode(version)
129 component = self._decode(component)
130 status_whiteboard = self._decode(status_whiteboard)
131 keywords = self._decode(keywords)
132 alias = self._decode(alias)
133
134 return (bug_id, assigned_to, bug_file_loc, bug_severity,
135 bug_status, creation_ts, short_desc, op_sys, priority,
136 product, rep_platform, reporter, version, component,
137 resolution, target_milestone, qa_contact,
138 status_whiteboard, keywords, alias)
139
140 def getBugCcs(self, bug_id):
141 """Get the IDs of the people CC'd to the bug."""
142 self.cursor.execute('SELECT who FROM cc WHERE bug_id = %d'
143 % bug_id)
144 return [row[0] for row in self.cursor.fetchall()]
145
146 def getBugComments(self, bug_id):
147 """Get the comments for the bug."""
148 self.cursor.execute('SELECT who, bug_when, thetext '
149 ' FROM longdescs '
150 ' WHERE bug_id = %d '
151 ' ORDER BY bug_when' % bug_id)
152 # XXX: jamesh 2005-12-07:
153 # Due to a bug in Debzilla, Ubuntu bugzilla bug 248 has > 7800
154 # duplicate comments,consisting of someone's signature.
155 # For the import, just ignore those comments.
156 return [(who, _add_tz(when), self._decode(thetext))
157 for (who, when, thetext) in self.cursor.fetchall()
158 if thetext != '\n--=20\n Jacobo Tarr=EDo | '
159 'http://jacobo.tarrio.org/\n\n\n']
160
161 def getBugAttachments(self, bug_id):
162 """Get the attachments for the bug."""
163 self.cursor.execute('SELECT attach_id, creation_ts, description, '
164 ' mimetype, ispatch, filename, thedata, '
165 ' submitter_id '
166 ' FROM attachments '
167 ' WHERE bug_id = %d '
168 ' ORDER BY attach_id' % bug_id)
169 return [(attach_id, _add_tz(creation_ts),
170 self._decode(description), mimetype,
171 ispatch, self._decode(filename), thedata, submitter_id)
172 for (attach_id, creation_ts, description,
173 mimetype, ispatch, filename, thedata,
174 submitter_id) in self.cursor.fetchall()]
175
176 def findBugs(self, product=None, component=None, status=None):
177 """Returns the requested bug IDs as a list"""
178 if product is None:
179 product = []
180 if component is None:
181 component = []
182 if status is None:
183 status = []
184 joins = []
185 conditions = []
186 if product:
187 joins.append(
188 'INNER JOIN products ON bugs.product_id = products.id')
189 conditions.append('products.name IN (%s)' %
190 ', '.join([self.conn.escape(p) for p in product]))
191 if component:
192 joins.append(
193 'INNER JOIN components ON bugs.component_id = components.id')
194 conditions.append('components.name IN (%s)' %
195 ', '.join([self.conn.escape(c) for c in component]))
196 if status:
197 conditions.append('bugs.bug_status IN (%s)' %
198 ', '.join([self.conn.escape(s) for s in status]))
199 if conditions:
200 conditions = 'WHERE %s' % ' AND '.join(conditions)
201 else:
202 conditions = ''
203 self.cursor.execute('SELECT bug_id FROM bugs %s %s ORDER BY bug_id' %
204 (' '.join(joins), conditions))
205 return [bug_id for (bug_id,) in self.cursor.fetchall()]
206
207 def getDuplicates(self):
208 """Returns a list of (dupe_of, dupe) relations."""
209 self.cursor.execute('SELECT dupe_of, dupe FROM duplicates '
210 'ORDER BY dupe, dupe_of')
211 return [(dupe_of, dupe) for (dupe_of, dupe) in self.cursor.fetchall()]
212
213
214class Bug:
215 """Representation of a Bugzilla Bug"""
216 def __init__(self, backend, bug_id):
217 self.backend = backend
218 (self.bug_id, self.assigned_to, self.bug_file_loc, self.bug_severity,
219 self.bug_status, self.creation_ts, self.short_desc, self.op_sys,
220 self.priority, self.product, self.rep_platform, self.reporter,
221 self.version, self.component, self.resolution,
222 self.target_milestone, self.qa_contact, self.status_whiteboard,
223 self.keywords, self.alias) = backend.getBugInfo(bug_id)
224
225 self._ccs = None
226 self._comments = None
227 self._attachments = None
228
229 @property
230 def ccs(self):
231 """Return the IDs of people CC'd to this bug"""
232 if self._ccs is not None:
233 return self._ccs
234 self._ccs = self.backend.getBugCcs(self.bug_id)
235 return self._ccs
236
237 @property
238 def comments(self):
239 """Return the comments attached to this bug"""
240 if self._comments is not None:
241 return self._comments
242 self._comments = self.backend.getBugComments(self.bug_id)
243 return self._comments
244
245 @property
246 def attachments(self):
247 """Return the attachments for this bug"""
248 if self._attachments is not None:
249 return self._attachments
250 self._attachments = self.backend.getBugAttachments(self.bug_id)
251 return self._attachments
252
253 def mapSeverity(self, bugtask):
254 """Set a Launchpad bug task's importance based on this bug's severity.
255 """
256 bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
257 importance_map = {
258 'blocker': BugTaskImportance.CRITICAL,
259 'critical': BugTaskImportance.CRITICAL,
260 'major': BugTaskImportance.HIGH,
261 'normal': BugTaskImportance.MEDIUM,
262 'minor': BugTaskImportance.LOW,
263 'trivial': BugTaskImportance.LOW,
264 'enhancement': BugTaskImportance.WISHLIST
265 }
266 importance = importance_map.get(
267 self.bug_severity, BugTaskImportance.UNKNOWN)
268 bugtask.transitionToImportance(importance, bug_importer)
269
270 def mapStatus(self, bugtask):
271 """Set a Launchpad bug task's status based on this bug's status.
272
273 If the bug is in the RESOLVED, VERIFIED or CLOSED states, the
274 bug resolution is also taken into account when mapping the
275 status.
276
277 Additional information about the bugzilla status is appended
278 to the bug task's status explanation.
279 """
280 bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
281
282 if self.bug_status == 'ASSIGNED':
283 bugtask.transitionToStatus(
284 BugTaskStatus.CONFIRMED, bug_importer)
285 elif self.bug_status == 'NEEDINFO':
286 bugtask.transitionToStatus(
287 BugTaskStatus.INCOMPLETE, bug_importer)
288 elif self.bug_status == 'PENDINGUPLOAD':
289 bugtask.transitionToStatus(
290 BugTaskStatus.FIXCOMMITTED, bug_importer)
291 elif self.bug_status in ['RESOLVED', 'VERIFIED', 'CLOSED']:
292 # depends on the resolution:
293 if self.resolution == 'FIXED':
294 bugtask.transitionToStatus(
295 BugTaskStatus.FIXRELEASED, bug_importer)
296 else:
297 bugtask.transitionToStatus(
298 BugTaskStatus.INVALID, bug_importer)
299 else:
300 bugtask.transitionToStatus(
301 BugTaskStatus.NEW, bug_importer)
302
303 # add the status to the notes section, to account for any lost
304 # information
305 bugzilla_status = 'Bugzilla status=%s' % self.bug_status
306 if self.resolution:
307 bugzilla_status += ' %s' % self.resolution
308 bugzilla_status += ', product=%s' % self.product
309 bugzilla_status += ', component=%s' % self.component
310
311 if bugtask.statusexplanation:
312 bugtask.statusexplanation = '%s (%s)' % (
313 bugtask.statusexplanation, bugzilla_status)
314 else:
315 bugtask.statusexplanation = bugzilla_status
316
317
318class Bugzilla:
319 """Representation of a bugzilla instance"""
320
321 def __init__(self, conn):
322 if conn is not None:
323 self.backend = BugzillaBackend(conn)
324 else:
325 self.backend = None
326 self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
327 self.debian = getUtility(ILaunchpadCelebrities).debian
328 self.bugtracker = getUtility(ILaunchpadCelebrities).ubuntu_bugzilla
329 self.debbugs = getUtility(ILaunchpadCelebrities).debbugs
330 self.bugset = getUtility(IBugSet)
331 self.bugtaskset = getUtility(IBugTaskSet)
332 self.bugwatchset = getUtility(IBugWatchSet)
333 self.cveset = getUtility(ICveSet)
334 self.personset = getUtility(IPersonSet)
335 self.emailset = getUtility(IEmailAddressSet)
336 self.person_mapping = {}
337
338 def person(self, bugzilla_id):
339 """Get the Launchpad person corresponding to the given Bugzilla ID"""
340 # Bugzilla treats a user ID of 0 as a NULL
341 if bugzilla_id == 0:
342 return None
343
344 # Try and get the person using a cache of the mapping. We
345 # check to make sure the person still exists and has not been
346 # merged.
347 person = None
348 launchpad_id = self.person_mapping.get(bugzilla_id)
349 if launchpad_id is not None:
350 person = self.personset.get(launchpad_id)
351 if person is not None and person.merged is not None:
352 person = None
353
354 # look up the person
355 if person is None:
356 email, displayname = self.backend.lookupUser(bugzilla_id)
357
358 person = self.personset.ensurePerson(
359 email, displayname, PersonCreationRationale.BUGIMPORT,
360 comment=('when importing bugs from %s'
361 % self.bugtracker.baseurl))
362
363 # Bugzilla performs similar address checks to Launchpad, so
364 # if the Launchpad account has no preferred email, use the
365 # Bugzilla one.
366 emailaddr = self.emailset.getByEmail(email)
367 assert emailaddr is not None
368 if person.preferredemail != emailaddr:
369 person.validateAndEnsurePreferredEmail(emailaddr)
370
371 self.person_mapping[bugzilla_id] = person.id
372
373 return person
374
375 def _getPackageName(self, bug):
376 """Returns the source package name for the given bug."""
377 # we currently only support mapping Ubuntu bugs ...
378 if bug.product != 'Ubuntu':
379 raise AssertionError('product must be Ubuntu')
380
381 # kernel bugs are currently filed against the "linux"
382 # component, which is not a source or binary package. The
383 # following mapping was provided by BenC:
384 if bug.component == 'linux':
385 cutoffdate = datetime.datetime(2004, 12, 1,
386 tzinfo=pytz.timezone('UTC'))
387 if bug.bug_status == 'NEEDINFO' and bug.creation_ts < cutoffdate:
388 pkgname = 'linux-source-2.6.12'
389 else:
390 pkgname = 'linux-source-2.6.15'
391 else:
392 pkgname = bug.component.encode('ASCII')
393
394 try:
395 return self.ubuntu.guessPublishedSourcePackageName(pkgname)
396 except NotFoundError, e:
397 logger.warning('could not find package name for "%s": %s',
398 pkgname, str(e))
399 return None
400
401 def getLaunchpadBugTarget(self, bug):
402 """Returns a dictionary of arguments to createBug() that correspond
403 to the given bugzilla bug.
404 """
405 srcpkg = self._getPackageName(bug)
406 return {
407 'distribution': self.ubuntu,
408 'sourcepackagename': srcpkg,
409 }
410
411 def getLaunchpadMilestone(self, bug):
412 """Return the Launchpad milestone for a Bugzilla bug.
413
414 If the milestone does not exist, then it is created.
415 """
416 if bug.product != 'Ubuntu':
417 raise AssertionError('product must be Ubuntu')
418
419 # Bugzilla uses a value of "---" to represent "no selected Milestone"
420 # Launchpad represents this by setting the milestone column to NULL.
421 if bug.target_milestone is None or bug.target_milestone == '---':
422 return None
423
424 # generate a Launchpad name from the Milestone name:
425 name = re.sub(r'[^a-z0-9\+\.\-]', '-', bug.target_milestone.lower())
426
427 milestone = self.ubuntu.getMilestone(name)
428 if milestone is None:
429 milestone = self.ubuntu.currentseries.newMilestone(name)
430 Store.of(milestone).flush()
431 return milestone
432
433 def getLaunchpadUpstreamProduct(self, bug):
434 """Find the upstream product for the given Bugzilla bug.
435
436 This function relies on the package -> product linkage having been
437 entered in advance.
438 """
439 srcpkgname = self._getPackageName(bug)
440 # find a product series
441 series = None
442 for series in self.ubuntu.series:
443 srcpkg = series.getSourcePackage(srcpkgname)
444 if srcpkg:
445 series = srcpkg.productseries
446 if series:
447 return series.product
448 else:
449 logger.warning('could not find upstream product for '
450 'source package "%s"', srcpkgname.name)
451 return None
452
453 _bug_re = re.compile('bug\s*#?\s*(?P<id>\d+)', re.IGNORECASE)
454
455 def replaceBugRef(self, match):
456 # XXX: jamesh 2005-10-24:
457 # this is where bug number rewriting would be plugged in
458 bug_id = int(match.group('id'))
459 url = '%s/%d' % (canonical_url(self.bugtracker), bug_id)
460 return '%s [%s]' % (match.group(0), url)
461
462 def handleBug(self, bug_id):
463 """Maybe import a single bug.
464
465 If the bug has already been imported (detected by checking for
466 a bug watch), it is skipped.
467 """
468 logger.info('Handling Bugzilla bug %d', bug_id)
469
470 # is there a bug watch on the bug?
471 lp_bug = self.bugset.queryByRemoteBug(self.bugtracker, bug_id)
472
473 # if we already have an associated bug, don't add a new one.
474 if lp_bug is not None:
475 logger.info('Bugzilla bug %d is already being watched by '
476 'Launchpad bug %d', bug_id, lp_bug.id)
477 return lp_bug
478
479 bug = Bug(self.backend, bug_id)
480
481 comments = bug.comments[:]
482
483 # create a message for the initial comment:
484 msgset = getUtility(IMessageSet)
485 who, when, text = comments.pop(0)
486 text = self._bug_re.sub(self.replaceBugRef, text)
487 # If a URL is associated with the bug, add it to the description:
488 if bug.bug_file_loc:
489 text = text + '\n\n' + bug.bug_file_loc
490 # the initial comment can't be empty:
491 if not text.strip():
492 text = '<empty comment>'
493 msg = msgset.fromText(bug.short_desc, text, self.person(who), when)
494
495 # create the bug
496 target = self.getLaunchpadBugTarget(bug)
497 params = CreateBugParams(
498 msg=msg, datecreated=bug.creation_ts, title=bug.short_desc,
499 owner=self.person(bug.reporter))
500 params.setBugTarget(**target)
501 lp_bug = self.bugset.createBug(params)
502
503 # add the bug watch:
504 lp_bug.addWatch(self.bugtracker, str(bug.bug_id), lp_bug.owner)
505
506 # add remaining comments, and add CVEs found in all text
507 lp_bug.findCvesInText(text, lp_bug.owner)
508 for (who, when, text) in comments:
509 text = self._bug_re.sub(self.replaceBugRef, text)
510 msg = msgset.fromText(msg.followup_title, text,
511 self.person(who), when)
512 lp_bug.linkMessage(msg)
513
514 # subscribe QA contact and CC's
515 if bug.qa_contact:
516 lp_bug.subscribe(
517 self.person(bug.qa_contact), self.person(bug.reporter))
518 for cc in bug.ccs:
519 lp_bug.subscribe(self.person(cc), self.person(bug.reporter))
520
521 # translate bugzilla status and severity to LP equivalents
522 task = lp_bug.bugtasks[0]
523 task.datecreated = bug.creation_ts
524 task.transitionToAssignee(self.person(bug.assigned_to))
525 task.statusexplanation = bug.status_whiteboard
526 bug.mapSeverity(task)
527 bug.mapStatus(task)
528
529 # bugs with an alias of the form "deb1234" have been imported
530 # from the Debian bug tracker by the "debzilla" program. For
531 # these bugs, generate a task and watch on the corresponding
532 # bugs.debian.org bug.
533 if bug.alias:
534 if re.match(r'^deb\d+$', bug.alias):
535 watch = self.bugwatchset.createBugWatch(
536 lp_bug, lp_bug.owner, self.debbugs, bug.alias[3:])
537 debtarget = self.debian
538 if target['sourcepackagename']:
539 debtarget = debtarget.getSourcePackage(
540 target['sourcepackagename'])
541 debtask = self.bugtaskset.createTask(
542 lp_bug, lp_bug.owner, debtarget)
543 debtask.datecreated = bug.creation_ts
544 debtask.bugwatch = watch
545 else:
546 # generate a Launchpad name from the alias:
547 name = re.sub(r'[^a-z0-9\+\.\-]', '-', bug.alias.lower())
548 lp_bug.name = name
549
550 # for UPSTREAM bugs, try to find whether the URL field contains
551 # a bug reference.
552 if bug.bug_status == 'UPSTREAM':
553 # see if the URL field contains a bug tracker reference
554 watches = self.bugwatchset.fromText(bug.bug_file_loc,
555 lp_bug, lp_bug.owner)
556 # find the upstream product for this bug
557 product = self.getLaunchpadUpstreamProduct(bug)
558
559 # if we created a watch, and there is an upstream product,
560 # create a new task and link it to the watch.
561 if len(watches) > 0:
562 if product:
563 upstreamtask = self.bugtaskset.createTask(
564 lp_bug, lp_bug.owner, product)
565 upstreamtask.datecreated = bug.creation_ts
566 upstreamtask.bugwatch = watches[0]
567 else:
568 logger.warning('Could not find upstream product to link '
569 'bug %d to', lp_bug.id)
570
571 # translate milestone linkage
572 task.milestone = self.getLaunchpadMilestone(bug)
573
574 # import attachments
575 for (attach_id, creation_ts, description, mimetype, ispatch,
576 filename, thedata, submitter_id) in bug.attachments:
577 # if the filename is missing for some reason, use a generic one.
578 if filename is None or filename.strip() == '':
579 filename = 'untitled'
580 logger.debug('Creating attachment %s for bug %d',
581 filename, bug.bug_id)
582 if ispatch:
583 attach_type = BugAttachmentType.PATCH
584 mimetype = 'text/plain'
585 else:
586 attach_type = BugAttachmentType.UNSPECIFIED
587
588 # look for a message starting with "Created an attachment (id=NN)"
589 for msg in lp_bug.messages:
590 if msg.text_contents.startswith(
591 'Created an attachment (id=%d)' % attach_id):
592 break
593 else:
594 # could not find the add message, so create one:
595 msg = msgset.fromText(description,
596 'Created attachment %s' % filename,
597 self.person(submitter_id),
598 creation_ts)
599 lp_bug.linkMessage(msg)
600
601 filealias = getUtility(ILibraryFileAliasSet).create(
602 name=filename,
603 size=len(thedata),
604 file=StringIO(thedata),
605 contentType=mimetype)
606
607 getUtility(IBugAttachmentSet).create(
608 bug=lp_bug, filealias=filealias, attach_type=attach_type,
609 title=description, message=msg)
610
611 return lp_bug
612
613 def processDuplicates(self, trans):
614 """Mark Launchpad bugs as duplicates based on Bugzilla duplicates.
615
616 Launchpad bug A will be marked as a duplicate of bug B if:
617 * bug A watches bugzilla bug A'
618 * bug B watches bugzilla bug B'
619 * bug A' is a duplicate of bug B'
620 * bug A is not currently a duplicate of any other bug.
621 """
622
623 logger.info('Processing duplicate bugs')
624 bugmap = {}
625
626 def getlpbug(bugid):
627 """Get the Launchpad bug corresponding to the given remote ID
628
629 This function makes use of a cache dictionary to reduce the
630 number of lookups.
631 """
632 lpbugid = bugmap.get(bugid)
633 if lpbugid is not None:
634 if lpbugid != 0:
635 lpbug = self.bugset.get(lpbugid)
636 else:
637 lpbug = None
638 else:
639 lpbug = self.bugset.queryByRemoteBug(self.bugtracker, bugid)
640 if lpbug is not None:
641 bugmap[bugid] = lpbug.id
642 else:
643 bugmap[bugid] = 0
644 return lpbug
645
646 for (dupe_of, dupe) in self.backend.getDuplicates():
647 # get the Launchpad bugs corresponding to the two Bugzilla bugs:
648 trans.begin()
649 lpdupe_of = getlpbug(dupe_of)
650 lpdupe = getlpbug(dupe)
651 # if both bugs exist in Launchpad, and lpdupe is not already
652 # a duplicate, mark it as a duplicate of lpdupe_of.
653 if (lpdupe_of is not None and lpdupe is not None and
654 lpdupe.duplicateof is None):
655 logger.info('Marking %d as a duplicate of %d',
656 lpdupe.id, lpdupe_of.id)
657 lpdupe.markAsDuplicate(lpdupe_of)
658 trans.commit()
659
660 def importBugs(self, trans, product=None, component=None, status=None):
661 """Import Bugzilla bugs matching the given constraints.
662
663 Each of product, component and status gives a list of
664 products, components or statuses to limit the import to. An
665 empty list matches all products, components or statuses.
666 """
667 if product is None:
668 product = []
669 if component is None:
670 component = []
671 if status is None:
672 status = []
673
674 bugs = self.backend.findBugs(product=product,
675 component=component,
676 status=status)
677 for bug_id in bugs:
678 trans.begin()
679 try:
680 self.handleBug(bug_id)
681 except (SystemExit, KeyboardInterrupt):
682 raise
683 except:
684 logger.exception('Could not import Bugzilla bug #%d', bug_id)
685 trans.abort()
686 else:
687 trans.commit()
6880
=== added file 'lib/lp/bugs/templates/bugtarget-tasks-and-nominations-table-row.pt'
--- lib/lp/bugs/templates/bugtarget-tasks-and-nominations-table-row.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/templates/bugtarget-tasks-and-nominations-table-row.pt 2011-08-03 17:36:12 +0000
@@ -0,0 +1,11 @@
1<tal:bugtarget xmlns:tal="http://xml.zope.org/namespaces/tal">
2 <tr style="opacity: 0.5">
3 <td></td>
4 <td>
5 <a tal:attributes="href context/fmt:url;
6 class context/image:sprite_css"
7 tal:content="context/bugtargetdisplayname" />
8 </td>
9 <td colspan="5"></td>
10 </tr>
11</tal:bugtarget>
012
=== modified file 'lib/lp/bugs/tests/test_doc.py'
--- lib/lp/bugs/tests/test_doc.py 2010-10-06 18:53:53 +0000
+++ lib/lp/bugs/tests/test_doc.py 2011-08-03 17:36:12 +0000
@@ -158,12 +158,6 @@
158 layer=LaunchpadZopelessLayer,158 layer=LaunchpadZopelessLayer,
159 setUp=bugNotificationSendingSetUp,159 setUp=bugNotificationSendingSetUp,
160 tearDown=bugNotificationSendingTearDown),160 tearDown=bugNotificationSendingTearDown),
161 'bugzilla-import.txt': LayeredDocFileSuite(
162 '../doc/bugzilla-import.txt',
163 setUp=setUp, tearDown=tearDown,
164 stdout_logging_level=logging.WARNING,
165 layer=LaunchpadZopelessLayer
166 ),
167 'bug-export.txt': LayeredDocFileSuite(161 'bug-export.txt': LayeredDocFileSuite(
168 '../doc/bug-export.txt',162 '../doc/bug-export.txt',
169 setUp=setUp, tearDown=tearDown,163 setUp=setUp, tearDown=tearDown,
170164
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2011-06-20 18:58:27 +0000
+++ lib/lp/code/configure.zcml 2011-08-03 17:36:12 +0000
@@ -715,6 +715,7 @@
715 product715 product
716 series716 series
717 review_status717 review_status
718 failure_bug
718 rcs_type719 rcs_type
719 cvs_root720 cvs_root
720 cvs_module721 cvs_module
@@ -733,7 +734,8 @@
733 requestImport"/>734 requestImport"/>
734 <require735 <require
735 permission="launchpad.Edit"736 permission="launchpad.Edit"
736 attributes="updateFromData"/>737 attributes="updateFromData
738 linkFailureBug"/>
737739
738 <!-- ICodeImport has no set_schema, because all modifications should be740 <!-- ICodeImport has no set_schema, because all modifications should be
739 done through methods that create CodeImportEvent objects when741 done through methods that create CodeImportEvent objects when
740742
=== modified file 'lib/lp/code/interfaces/codeimport.py'
--- lib/lp/code/interfaces/codeimport.py 2011-02-23 20:26:53 +0000
+++ lib/lp/code/interfaces/codeimport.py 2011-08-03 17:36:12 +0000
@@ -40,6 +40,7 @@
4040
41from canonical.launchpad import _41from canonical.launchpad import _
42from lp.app.validators import LaunchpadValidationError42from lp.app.validators import LaunchpadValidationError
43from lp.bugs.interfaces.bug import IBug
43from lp.code.enums import (44from lp.code.enums import (
44 CodeImportReviewStatus,45 CodeImportReviewStatus,
45 RevisionControlSystems,46 RevisionControlSystems,
@@ -100,6 +101,11 @@
100 description=_("The Bazaar branch produced by the "101 description=_("The Bazaar branch produced by the "
101 "import system.")))102 "import system.")))
102103
104 failure_bug = ReferenceChoice(
105 title=_('Failure bug'), required=False, readonly=False,
106 vocabulary='Bug', schema=IBug,
107 description=_("The bug that is causing this import to fail."))
108
103 registrant = PublicPersonChoice(109 registrant = PublicPersonChoice(
104 title=_('Registrant'), required=True, readonly=True,110 title=_('Registrant'), required=True, readonly=True,
105 vocabulary='ValidPersonOrTeam',111 vocabulary='ValidPersonOrTeam',
@@ -188,6 +194,14 @@
188 None if no changes were made.194 None if no changes were made.
189 """195 """
190196
197 def linkFailureBug(bug):
198 """Link the bug that causes this import to fail.
199
200 This method requires the review_status to be FAILING.
201
202 :param bug: The bug that is causing the import to fail.
203 """
204
191 def tryFailingImportAgain(user):205 def tryFailingImportAgain(user):
192 """Try a failing import again.206 """Try a failing import again.
193207
194208
=== modified file 'lib/lp/code/model/codeimport.py'
--- lib/lp/code/model/codeimport.py 2011-04-27 01:42:46 +0000
+++ lib/lp/code/model/codeimport.py 2011-08-03 17:36:12 +0000
@@ -85,6 +85,9 @@
85 dbName='assignee', foreignKey='Person',85 dbName='assignee', foreignKey='Person',
86 storm_validator=validate_public_person, notNull=False, default=None)86 storm_validator=validate_public_person, notNull=False, default=None)
8787
88 failure_bug = ForeignKey(
89 dbName='failure_bug', foreignKey='Bug', notNull=False, default=None)
90
88 review_status = EnumCol(schema=CodeImportReviewStatus, notNull=True,91 review_status = EnumCol(schema=CodeImportReviewStatus, notNull=True,
89 default=CodeImportReviewStatus.NEW)92 default=CodeImportReviewStatus.NEW)
9093
@@ -190,6 +193,8 @@
190 else:193 else:
191 new_whiteboard = whiteboard194 new_whiteboard = whiteboard
192 self.branch.whiteboard = whiteboard195 self.branch.whiteboard = whiteboard
196 if data.get('review_status', None) != CodeImportReviewStatus.FAILING:
197 self.failure_bug = None
193 token = event_set.beginModify(self)198 token = event_set.beginModify(self)
194 for name, value in data.items():199 for name, value in data.items():
195 setattr(self, name, value)200 setattr(self, name, value)
@@ -206,6 +211,13 @@
206 def __repr__(self):211 def __repr__(self):
207 return "<CodeImport for %s>" % self.branch.unique_name212 return "<CodeImport for %s>" % self.branch.unique_name
208213
214 def linkFailureBug(self, bug):
215 """See `ICodeImport`."""
216 if self.review_status != CodeImportReviewStatus.FAILING:
217 raise AssertionError(
218 "review_status is %s not FAILING" % self.review_status.name)
219 self.failure_bug = bug
220
209 def tryFailingImportAgain(self, user):221 def tryFailingImportAgain(self, user):
210 """See `ICodeImport`."""222 """See `ICodeImport`."""
211 if self.review_status != CodeImportReviewStatus.FAILING:223 if self.review_status != CodeImportReviewStatus.FAILING:
212224
=== modified file 'lib/lp/code/model/tests/test_codeimport.py'
--- lib/lp/code/model/tests/test_codeimport.py 2011-05-27 21:12:25 +0000
+++ lib/lp/code/model/tests/test_codeimport.py 2011-08-03 17:36:12 +0000
@@ -391,6 +391,31 @@
391 self.assertEqual(391 self.assertEqual(
392 CodeImportReviewStatus.FAILING, code_import.review_status)392 CodeImportReviewStatus.FAILING, code_import.review_status)
393393
394 def test_mark_failing_with_bug(self):
395 # Marking an import as failing and linking to a bug.
396 code_import = self.factory.makeCodeImport()
397 code_import.updateFromData(
398 {'review_status':CodeImportReviewStatus.FAILING},
399 self.import_operator)
400 self.assertEquals(None, code_import.failure_bug)
401 bug = self.factory.makeBug()
402 code_import.linkFailureBug(bug)
403 self.assertEqual(
404 CodeImportReviewStatus.FAILING, code_import.review_status)
405 self.assertEquals(bug, code_import.failure_bug)
406
407 def test_mark_no_longer_failing_with_bug(self):
408 # Marking an import as no longer failing removes the failure bug link.
409 code_import = self.factory.makeCodeImport()
410 code_import.updateFromData(
411 {'review_status':CodeImportReviewStatus.FAILING},
412 self.import_operator)
413 code_import.linkFailureBug(self.factory.makeBug())
414 code_import.updateFromData(
415 {'review_status':CodeImportReviewStatus.REVIEWED},
416 self.import_operator)
417 self.assertEquals(None, code_import.failure_bug)
418
394419
395class TestCodeImportResultsAttribute(TestCaseWithFactory):420class TestCodeImportResultsAttribute(TestCaseWithFactory):
396 """Test the results attribute of a CodeImport."""421 """Test the results attribute of a CodeImport."""
397422
=== modified file 'lib/lp/codehosting/codeimport/tests/test_uifactory.py'
--- lib/lp/codehosting/codeimport/tests/test_uifactory.py 2011-07-19 18:09:01 +0000
+++ lib/lp/codehosting/codeimport/tests/test_uifactory.py 2011-08-03 17:36:12 +0000
@@ -10,6 +10,7 @@
10import unittest10import unittest
1111
12from lp.codehosting.codeimport.uifactory import LoggingUIFactory12from lp.codehosting.codeimport.uifactory import LoggingUIFactory
13from lp.services.log.logger import BufferLogger
13from lp.testing import (14from lp.testing import (
14 FakeTime,15 FakeTime,
15 TestCase,16 TestCase,
@@ -22,19 +23,19 @@
22 def setUp(self):23 def setUp(self):
23 TestCase.setUp(self)24 TestCase.setUp(self)
24 self.fake_time = FakeTime(12345)25 self.fake_time = FakeTime(12345)
25 self.messages = []26 self.logger = BufferLogger()
2627
27 def makeLoggingUIFactory(self):28 def makeLoggingUIFactory(self):
28 """Make a `LoggingUIFactory` with fake time and contained output."""29 """Make a `LoggingUIFactory` with fake time and contained output."""
29 return LoggingUIFactory(30 return LoggingUIFactory(
30 time_source=self.fake_time.now, writer=self.messages.append)31 time_source=self.fake_time.now, logger=self.logger)
3132
32 def test_first_progress_updates(self):33 def test_first_progress_updates(self):
33 # The first call to progress generates some output.34 # The first call to progress generates some output.
34 factory = self.makeLoggingUIFactory()35 factory = self.makeLoggingUIFactory()
35 bar = factory.nested_progress_bar()36 bar = factory.nested_progress_bar()
36 bar.update("hi")37 bar.update("hi")
37 self.assertEqual(['hi'], self.messages)38 self.assertEqual('INFO hi\n', self.logger.getLogBuffer())
3839
39 def test_second_rapid_progress_doesnt_update(self):40 def test_second_rapid_progress_doesnt_update(self):
40 # The second of two progress calls that are less than the factory's41 # The second of two progress calls that are less than the factory's
@@ -44,7 +45,7 @@
44 bar.update("hi")45 bar.update("hi")
45 self.fake_time.advance(factory.interval / 2)46 self.fake_time.advance(factory.interval / 2)
46 bar.update("there")47 bar.update("there")
47 self.assertEqual(['hi'], self.messages)48 self.assertEqual('INFO hi\n', self.logger.getLogBuffer())
4849
49 def test_second_slow_progress_updates(self):50 def test_second_slow_progress_updates(self):
50 # The second of two progress calls that are more than the factory's51 # The second of two progress calls that are more than the factory's
@@ -54,7 +55,10 @@
54 bar.update("hi")55 bar.update("hi")
55 self.fake_time.advance(factory.interval * 2)56 self.fake_time.advance(factory.interval * 2)
56 bar.update("there")57 bar.update("there")
57 self.assertEqual(['hi', 'there'], self.messages)58 self.assertEqual(
59 'INFO hi\n'
60 'INFO there\n',
61 self.logger.getLogBuffer())
5862
59 def test_first_progress_on_new_bar_updates(self):63 def test_first_progress_on_new_bar_updates(self):
60 # The first progress on a new progress task always generates output.64 # The first progress on a new progress task always generates output.
@@ -64,14 +68,15 @@
64 self.fake_time.advance(factory.interval / 2)68 self.fake_time.advance(factory.interval / 2)
65 bar2 = factory.nested_progress_bar()69 bar2 = factory.nested_progress_bar()
66 bar2.update("there")70 bar2.update("there")
67 self.assertEqual(['hi', 'hi:there'], self.messages)71 self.assertEqual(
72 'INFO hi\nINFO hi:there\n', self.logger.getLogBuffer())
6873
69 def test_update_with_count_formats_nicely(self):74 def test_update_with_count_formats_nicely(self):
70 # When more details are passed to update, they are formatted nicely.75 # When more details are passed to update, they are formatted nicely.
71 factory = self.makeLoggingUIFactory()76 factory = self.makeLoggingUIFactory()
72 bar = factory.nested_progress_bar()77 bar = factory.nested_progress_bar()
73 bar.update("hi", 1, 8)78 bar.update("hi", 1, 8)
74 self.assertEqual(['hi 1/8'], self.messages)79 self.assertEqual('INFO hi 1/8\n', self.logger.getLogBuffer())
7580
76 def test_report_transport_activity_reports_bytes_since_last_update(self):81 def test_report_transport_activity_reports_bytes_since_last_update(self):
77 # If there is no call to _progress_updated for 'interval' seconds, the82 # If there is no call to _progress_updated for 'interval' seconds, the
@@ -94,9 +99,54 @@
94 # activity info.99 # activity info.
95 bar.update("hi", 3, 10)100 bar.update("hi", 3, 10)
96 self.assertEqual(101 self.assertEqual(
97 ['hi 1/10', 'hi 2/10', '110 bytes transferred | hi 2/10',102 'INFO hi 1/10\n'
98 'hi 3/10'],103 'INFO hi 2/10\n'
99 self.messages)104 'INFO 110 bytes transferred | hi 2/10\n'
105 'INFO hi 3/10\n',
106 self.logger.getLogBuffer())
107
108 def test_note(self):
109 factory = self.makeLoggingUIFactory()
110 factory.note("Banja Luka")
111 self.assertEqual('INFO Banja Luka\n', self.logger.getLogBuffer())
112
113 def test_show_error(self):
114 factory = self.makeLoggingUIFactory()
115 factory.show_error("Exploding Peaches")
116 self.assertEqual(
117 "ERROR Exploding Peaches\n", self.logger.getLogBuffer())
118
119 def test_confirm_action(self):
120 factory = self.makeLoggingUIFactory()
121 self.assertTrue(factory.confirm_action(
122 "How are you %(when)s?", "wellness", {"when": "today"}))
123
124 def test_show_message(self):
125 factory = self.makeLoggingUIFactory()
126 factory.show_message("Peaches")
127 self.assertEqual("INFO Peaches\n", self.logger.getLogBuffer())
128
129 def test_get_username(self):
130 factory = self.makeLoggingUIFactory()
131 self.assertIs(
132 None, factory.get_username("Who are you %(when)s?", when="today"))
133
134 def test_get_password(self):
135 factory = self.makeLoggingUIFactory()
136 self.assertIs(
137 None,
138 factory.get_password("How is your %(drink)s", drink="coffee"))
139
140 def test_show_warning(self):
141 factory = self.makeLoggingUIFactory()
142 factory.show_warning("Peaches")
143 self.assertEqual("WARNING Peaches\n", self.logger.getLogBuffer())
144
145 def test_show_warning_unicode(self):
146 factory = self.makeLoggingUIFactory()
147 factory.show_warning(u"Peach\xeas")
148 self.assertEqual(
149 "WARNING Peach\xc3\xaas\n", self.logger.getLogBuffer())
100150
101 def test_user_warning(self):151 def test_user_warning(self):
102 factory = self.makeLoggingUIFactory()152 factory = self.makeLoggingUIFactory()
@@ -106,9 +156,13 @@
106 "from_format": "athing",156 "from_format": "athing",
107 "to_format": "anotherthing",157 "to_format": "anotherthing",
108 }158 }
109 self.assertEqual([message], self.messages)159 self.assertEqual("WARNING %s\n" % message, self.logger.getLogBuffer())
160
161 def test_clear_term(self):
162 factory = self.makeLoggingUIFactory()
163 factory.clear_term()
164 self.assertEqual("", self.logger.getLogBuffer())
110165
111166
112def test_suite():167def test_suite():
113 return unittest.TestLoader().loadTestsFromName(__name__)168 return unittest.TestLoader().loadTestsFromName(__name__)
114
115169
=== modified file 'lib/lp/codehosting/codeimport/uifactory.py'
--- lib/lp/codehosting/codeimport/uifactory.py 2011-07-19 18:09:01 +0000
+++ lib/lp/codehosting/codeimport/uifactory.py 2011-08-03 17:36:12 +0000
@@ -10,38 +10,80 @@
10import sys10import sys
11import time11import time
1212
13from bzrlib.ui import NoninteractiveUIFactory
13from bzrlib.ui.text import (14from bzrlib.ui.text import (
14 TextProgressView,15 TextProgressView,
15 TextUIFactory,
16 )16 )
1717
1818
19class LoggingUIFactory(TextUIFactory):19class LoggingUIFactory(NoninteractiveUIFactory):
20 """A UI Factory that produces reasonably sparse logging style output.20 """A UI Factory that produces reasonably sparse logging style output.
2121
22 The goal is to produce a line of output no more often than once a minute22 The goal is to produce a line of output no more often than once a minute
23 (by default).23 (by default).
24 """24 """
2525
26 def __init__(self, time_source=time.time, writer=None, interval=60.0):26 # XXX: JelmerVernooij 2011-08-02 bug=820127: This seems generic enough to
27 # live in bzrlib.ui
28
29 def __init__(self, time_source=time.time, logger=None, interval=60.0):
27 """Construct a `LoggingUIFactory`.30 """Construct a `LoggingUIFactory`.
2831
29 :param time_source: A callable that returns time in seconds since the32 :param time_source: A callable that returns time in seconds since the
30 epoch. Defaults to ``time.time`` and should be replaced with33 epoch. Defaults to ``time.time`` and should be replaced with
31 something deterministic in tests.34 something deterministic in tests.
32 :param writer: A callable that takes a string and displays it. It is35 :param logger: The logger object to write to
33 not called with newline terminated strings.
34 :param interval: Don't produce output more often than once every this36 :param interval: Don't produce output more often than once every this
35 many seconds. Defaults to 60 seconds.37 many seconds. Defaults to 60 seconds.
36 """38 """
37 TextUIFactory.__init__(self)39 NoninteractiveUIFactory.__init__(self)
38 self.interval = interval40 self.interval = interval
39 self.writer = writer41 self.logger = logger
40 self._progress_view = LoggingTextProgressView(42 self._progress_view = LoggingTextProgressView(
41 time_source, writer, interval)43 time_source, lambda m: logger.info("%s", m), interval)
4244
43 def show_user_warning(self, warning_id, **message_args):45 def show_user_warning(self, warning_id, **message_args):
44 self.writer(self.format_user_warning(warning_id, message_args))46 self.logger.warning(
47 "%s", self.format_user_warning(warning_id, message_args))
48
49 def show_warning(self, msg):
50 if isinstance(msg, unicode):
51 msg = msg.encode("utf-8")
52 self.logger.warning("%s", msg)
53
54 def get_username(self, prompt, **kwargs):
55 return None
56
57 def get_password(self, prompt, **kwargs):
58 return None
59
60 def show_message(self, msg):
61 self.logger.info("%s", msg)
62
63 def note(self, msg):
64 self.logger.info("%s", msg)
65
66 def show_error(self, msg):
67 self.logger.error("%s", msg)
68
69 def _progress_updated(self, task):
70 """A task has been updated and wants to be displayed.
71 """
72 if not self._task_stack:
73 self.logger.warning("%r updated but no tasks are active", task)
74 self._progress_view.show_progress(task)
75
76 def _progress_all_finished(self):
77 self._progress_view.clear()
78
79 def report_transport_activity(self, transport, byte_count, direction):
80 """Called by transports as they do IO.
81
82 This may update a progress bar, spinner, or similar display.
83 By default it does nothing.
84 """
85 self._progress_view.show_transport_activity(transport,
86 direction, byte_count)
4587
4688
47class LoggingTextProgressView(TextProgressView):89class LoggingTextProgressView(TextProgressView):
4890
=== modified file 'lib/lp/codehosting/codeimport/worker.py'
--- lib/lp/codehosting/codeimport/worker.py 2011-07-20 17:20:03 +0000
+++ lib/lp/codehosting/codeimport/worker.py 2011-08-03 17:36:12 +0000
@@ -601,8 +601,7 @@
601 def _doImport(self):601 def _doImport(self):
602 self._logger.info("Starting job.")602 self._logger.info("Starting job.")
603 saved_factory = bzrlib.ui.ui_factory603 saved_factory = bzrlib.ui.ui_factory
604 bzrlib.ui.ui_factory = LoggingUIFactory(604 bzrlib.ui.ui_factory = LoggingUIFactory(logger=self._logger)
605 writer=lambda m: self._logger.info('%s', m))
606 try:605 try:
607 self._logger.info(606 self._logger.info(
608 "Getting exising bzr branch from central store.")607 "Getting exising bzr branch from central store.")
@@ -635,9 +634,11 @@
635 except Exception, e:634 except Exception, e:
636 if e.__class__ in self.unsupported_feature_exceptions:635 if e.__class__ in self.unsupported_feature_exceptions:
637 self._logger.info(636 self._logger.info(
638 "Unable to import branch because of limitations in Bazaar.")637 "Unable to import branch because of limitations in "
638 "Bazaar.")
639 self._logger.info(str(e))639 self._logger.info(str(e))
640 return CodeImportWorkerExitCode.FAILURE_UNSUPPORTED_FEATURE640 return (
641 CodeImportWorkerExitCode.FAILURE_UNSUPPORTED_FEATURE)
641 elif e.__class__ in self.invalid_branch_exceptions:642 elif e.__class__ in self.invalid_branch_exceptions:
642 self._logger.info("Branch invalid: %s", e(str))643 self._logger.info("Branch invalid: %s", e(str))
643 return CodeImportWorkerExitCode.FAILURE_INVALID644 return CodeImportWorkerExitCode.FAILURE_INVALID
644645
=== modified file 'lib/lp/registry/browser/distributionsourcepackage.py'
--- lib/lp/registry/browser/distributionsourcepackage.py 2011-07-28 17:34:34 +0000
+++ lib/lp/registry/browser/distributionsourcepackage.py 2011-08-03 17:36:12 +0000
@@ -21,6 +21,7 @@
21import operator21import operator
2222
23from lazr.delegates import delegates23from lazr.delegates import delegates
24from lazr.restful.interfaces import IJSONRequestCache
24import pytz25import pytz
25from zope.component import (26from zope.component import (
26 adapter,27 adapter,
@@ -337,6 +338,11 @@
337 expose_structural_subscription_data_to_js(338 expose_structural_subscription_data_to_js(
338 self.context, self.request, self.user)339 self.context, self.request, self.user)
339340
341 pub = self.context.latest_overall_publication
342 if pub:
343 IJSONRequestCache(self.request).objects['archive_context_url'] = (
344 canonical_url(pub.archive, path_only_if_possible=True))
345
340 @property346 @property
341 def label(self):347 def label(self):
342 return self.context.title348 return self.context.title
343349
=== modified file 'lib/lp/registry/templates/distributionsourcepackage-index.pt'
--- lib/lp/registry/templates/distributionsourcepackage-index.pt 2011-07-15 11:18:47 +0000
+++ lib/lp/registry/templates/distributionsourcepackage-index.pt 2011-08-03 17:36:12 +0000
@@ -188,8 +188,6 @@
188188
189 </tal:rows>189 </tal:rows>
190 </table>190 </table>
191 <script
192 tal:content="string:LP.cache['archive_context_url'] = '${archive/fmt:url}';"></script>
193 <metal:js use-macro="archive/@@+macros/expandable-table-js"/>191 <metal:js use-macro="archive/@@+macros/expandable-table-js"/>
194192
195 </div>193 </div>
196194
=== modified file 'lib/lp/services/scripts/tests/__init__.py'
--- lib/lp/services/scripts/tests/__init__.py 2010-11-16 12:56:01 +0000
+++ lib/lp/services/scripts/tests/__init__.py 2011-08-03 17:36:12 +0000
@@ -24,7 +24,6 @@
2424
25KNOWN_BROKEN = [25KNOWN_BROKEN = [
26 # Needs mysqldb module26 # Needs mysqldb module
27 'scripts/bugzilla-import.py',
28 'scripts/migrate-bugzilla-initialcontacts.py',27 'scripts/migrate-bugzilla-initialcontacts.py',
29 # circular import from hell (IHasOwner).28 # circular import from hell (IHasOwner).
30 'scripts/clean-sourceforge-project-entries.py',29 'scripts/clean-sourceforge-project-entries.py',
3130
=== removed file 'scripts/bugzilla-import.py'
--- scripts/bugzilla-import.py 2010-04-27 19:48:39 +0000
+++ scripts/bugzilla-import.py 1970-01-01 00:00:00 +0000
@@ -1,97 +0,0 @@
1#!/usr/bin/python -S
2#
3# Copyright 2009 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6import sys
7import logging
8import optparse
9import MySQLdb
10
11# pylint: disable-msg=W0403
12import _pythonpath
13
14from canonical.config import config
15from canonical.lp import initZopeless
16from canonical.launchpad.scripts import (
17 execute_zcml_for_scripts, logger_options, logger)
18from canonical.launchpad.webapp.interaction import setupInteractionByEmail
19
20from canonical.launchpad.scripts import bugzilla
21
22
23def make_connection(options):
24 kws = {}
25 if options.db_name is not None:
26 kws['db'] = options.db_name
27 if options.db_user is not None:
28 kws['user'] = options.db_user
29 if options.db_password is not None:
30 kws['passwd'] = options.db_passwd
31 if options.db_host is not None:
32 kws['host'] = options.db_host
33
34 return MySQLdb.connect(**kws)
35
36def main(argv):
37 parser = optparse.OptionParser(
38 description=("This script imports bugs from a Bugzilla "
39 "into Launchpad."))
40
41 parser.add_option('--component', metavar='COMPONENT', action='append',
42 help='Limit to this bugzilla component',
43 type='string', dest='component', default=[])
44 parser.add_option('--status', metavar='STATUS,...', action='store',
45 help='Only import bugs with the given status',
46 type='string', dest='status',
47 default=None)
48
49 # MySQL connection details
50 parser.add_option('-d', '--dbname', metavar='DB', action='store',
51 help='The MySQL database name',
52 type='string', dest='db_name', default='bugs_warty')
53 parser.add_option('-U', '--username', metavar='USER', action='store',
54 help='The MySQL user name',
55 type='string', dest='db_user', default=None)
56 parser.add_option('-p', '--password', metavar='PASSWORD', action='store',
57 help='The MySQL password',
58 type='string', dest='db_password', default=None)
59 parser.add_option('-H', '--host', metavar='HOST', action='store',
60 help='The MySQL database host',
61 type='string', dest='db_host', default=None)
62
63 # logging options
64 logger_options(parser, logging.INFO)
65
66 options, args = parser.parse_args(argv[1:])
67 if options.status is not None:
68 options.status = options.status.split(',')
69 else:
70 options.status = []
71
72 logger(options, 'canonical.launchpad.scripts.bugzilla')
73
74 # don't send email
75 send_email_data = """
76 [zopeless]
77 send_email: False
78 """
79 config.push('send_email_data', send_email_data)
80
81 execute_zcml_for_scripts()
82 ztm = initZopeless()
83 setupInteractionByEmail('bug-importer@launchpad.net')
84
85 db = make_connection(options)
86 bz = bugzilla.Bugzilla(db)
87
88 bz.importBugs(ztm,
89 product=['Ubuntu'],
90 component=options.component,
91 status=options.status)
92
93 bz.processDuplicates(ztm)
94 config.pop('send_email_data')
95
96if __name__ == '__main__':
97 sys.exit(main(sys.argv))
980
=== removed file 'scripts/migrate-bugzilla-initialcontacts.py'
--- scripts/migrate-bugzilla-initialcontacts.py 2011-05-29 01:38:41 +0000
+++ scripts/migrate-bugzilla-initialcontacts.py 1970-01-01 00:00:00 +0000
@@ -1,91 +0,0 @@
1#!/usr/bin/python -S
2#
3# Copyright 2009 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6import logging
7import MySQLdb
8
9import _pythonpath
10
11from zope.component import getUtility
12
13from canonical.lp import initZopeless
14from canonical.launchpad.scripts import execute_zcml_for_scripts
15from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
16from lp.app.interfaces.launchpad import ILaunchpadCelebrities
17from lp.app.errors import NotFoundError
18from lp.registry.interfaces.person import IPersonSet
19
20
21execute_zcml_for_scripts()
22ztm = initZopeless()
23logging.basicConfig(level=logging.INFO)
24
25ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
26techboard = getUtility(IPersonSet).getByName('techboard')
27
28def getPerson(email, realname):
29 # The debzilla user acts as a placeholder for "no specific maintainer".
30 # We don't create a bug contact record for it.
31 if email is None or email == 'debzilla@ubuntu.com':
32 return None
33
34 personset = getUtility(IPersonSet)
35 person = personset.getByEmail(email)
36 if person:
37 return person
38
39 # we mark the bugzilla email as preferred email, since it has been
40 # validated there.
41 if email.endswith('@lists.ubuntu.com'):
42 logging.info('creating team for %s (%s)', email, realname)
43 person = personset.newTeam(techboard, email[:-17], realname)
44 email = getUtility(IEmailAddressSet).new(email, person.id)
45 person.setPreferredEmail(email)
46 else:
47 logging.info('creating person for %s (%s)', email, realname)
48 person, email = personset.createPersonAndEmail(email,
49 displayname=realname)
50 person.setPreferredEmail(email)
51
52 return person
53
54
55conn = MySQLdb.connect(db='bugs_warty')
56cursor = conn.cursor()
57
58# big arse query that gets all the default assignees and QA contacts:
59cursor.execute(
60 "SELECT components.name, owner.login_name, owner.realname, "
61 " qa.login_name, qa.realname "
62 " FROM components "
63 " JOIN products ON components.product_id = products.id "
64 " LEFT JOIN profiles AS owner ON components.initialowner = owner.userid"
65 " LEFT JOIN profiles AS qa ON components.initialqacontact = qa.userid "
66 " WHERE products.name = 'Ubuntu'")
67
68for (component, owneremail, ownername, qaemail, qaname) in cursor.fetchall():
69 logging.info('Processing %s', component)
70 try:
71 srcpkgname, binpkgname = ubuntu.getPackageNames(component)
72 except NotFoundError, e:
73 logging.warning('could not find package name for "%s": %s', component,
74 str(e))
75 continue
76
77 srcpkg = ubuntu.getSourcePackage(srcpkgname)
78
79 # default assignee => maintainer
80 person = getPerson(owneremail, ownername)
81 if person:
82 if not srcpkg.isBugContact(person):
83 srcpkg.addBugContact(person)
84
85 # QA contact => maintainer
86 person = getPerson(qaemail, qaname)
87 if person:
88 if not srcpkg.isBugContact(person):
89 srcpkg.addBugContact(person)
90
91ztm.commit()

Subscribers

People subscribed via source and target branches

to status/vote changes: