Merge lp:~julian-edwards/launchpad/sync-close-bugs-bug-833736 into lp:launchpad

Proposed by Julian Edwards
Status: Superseded
Proposed branch: lp:~julian-edwards/launchpad/sync-close-bugs-bug-833736
Merge into: lp:launchpad
Diff against target: 972 lines (+511/-66)
15 files modified
database/schema/security.cfg (+4/-1)
lib/lp/soyuz/adapters/notification.py (+17/-11)
lib/lp/soyuz/adapters/tests/test_notification.py (+58/-21)
lib/lp/soyuz/enums.py (+12/-0)
lib/lp/soyuz/interfaces/archive.py (+3/-1)
lib/lp/soyuz/interfaces/sourcepackagerelease.py (+11/-0)
lib/lp/soyuz/model/sourcepackagerelease.py (+43/-0)
lib/lp/soyuz/scripts/packagecopier.py (+21/-4)
lib/lp/soyuz/scripts/processaccepted.py (+54/-10)
lib/lp/soyuz/scripts/tests/test_copypackage.py (+69/-9)
lib/lp/soyuz/scripts/tests/test_processaccepted.py (+91/-0)
lib/lp/soyuz/tests/test_packagecopyjob.py (+82/-0)
lib/lp/soyuz/tests/test_sourcepackagerelease.py (+27/-0)
lib/lp/testing/factory.py (+13/-4)
scripts/ftpmaster-tools/sync-source.py (+6/-5)
To merge this branch: bzr merge lp:~julian-edwards/launchpad/sync-close-bugs-bug-833736
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+73389@code.launchpad.net

This proposal has been superseded by a proposal from 2011-08-30.

Description of the change

This branch fixes bug 833736 by ensuring that bugs mentioned in Debian package changelogs are closed when the package is synced to Ubuntu.

The fix is mainly in 2 places:
 1. The packagecopier determines the most recently published version of the package being copied and passes that to the bug closing code.
 2. The bug closing code is fixed to parse changelogs (it only used to parse the changes file) and grabs as many version chunks as necessary to complete the missing history.

I also refactored the regexes that the sync-source script uses to scan changelogs.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2011-08-19 16:03:54 +0000
3+++ database/schema/security.cfg 2011-08-30 15:05:02 +0000
4@@ -1014,7 +1014,10 @@
5 type=user
6
7 [sync_packages]
8-groups=script
9+# sync_packages does a lot of the same work that process-accepted.py (queued)
10+# does, so it's easier to inherit its permissions than try and work them all
11+# out from scratch again.
12+groups=script,queued
13 public.account = SELECT
14 public.archive = SELECT
15 public.archivearch = SELECT
16
17=== modified file 'lib/lp/soyuz/adapters/notification.py'
18--- lib/lp/soyuz/adapters/notification.py 2011-08-26 14:57:39 +0000
19+++ lib/lp/soyuz/adapters/notification.py 2011-08-30 15:05:02 +0000
20@@ -127,7 +127,7 @@
21 def notify(blamer, spr, bprs, customfiles, archive, distroseries, pocket,
22 summary_text=None, changes=None, changesfile_content=None,
23 changesfile_object=None, action=None, dry_run=False,
24- logger=None, announce_from_person=None):
25+ logger=None, announce_from_person=None, previous_version=None):
26 """Notify about
27
28 :param blamer: The `IPerson` who is to blame for this notification.
29@@ -149,6 +149,9 @@
30 :param announce_from_person: If passed, use this `IPerson` as the From: in
31 announcement emails. If the person has no preferred email address,
32 the person is ignored and the default From: is used instead.
33+ :param previous_version: If specified, the change log on the email will
34+ include all of the source package's change logs after that version
35+ up to and including the passed spr's version.
36 """
37 # If this is a binary or mixed upload, we don't send *any* emails
38 # provided it's not a rejection or a security upload:
39@@ -213,12 +216,13 @@
40
41 attach_changes = not archive.is_ppa
42
43- def build_and_send_mail(action, recipients, from_addr=None, bcc=None):
44+ def build_and_send_mail(action, recipients, from_addr=None, bcc=None,
45+ previous_version=None):
46 subject = calculate_subject(
47 spr, bprs, customfiles, archive, distroseries, pocket, action)
48 body = assemble_body(
49 blamer, spr, bprs, archive, distroseries, summarystring, changes,
50- action)
51+ action, previous_version=previous_version)
52 body = body.encode("utf8")
53 send_mail(
54 spr, archive, recipients, subject, body, dry_run,
55@@ -226,7 +230,8 @@
56 attach_changes=attach_changes, from_addr=from_addr, bcc=bcc,
57 logger=logger)
58
59- build_and_send_mail(action, recipients)
60+ build_and_send_mail(
61+ action, recipients, previous_version=previous_version)
62
63 info = fetch_information(spr, bprs, changes)
64 from_addr = info['changedby']
65@@ -254,20 +259,21 @@
66
67 build_and_send_mail(
68 'announcement', [str(distroseries.changeslist)], from_addr,
69- bcc_addr)
70+ bcc_addr, previous_version=previous_version)
71
72
73 def assemble_body(blamer, spr, bprs, archive, distroseries, summary, changes,
74- action):
75+ action, previous_version=None):
76 """Assemble the e-mail notification body."""
77 if changes is None:
78 changes = {}
79- info = fetch_information(spr, bprs, changes)
80+ info = fetch_information(
81+ spr, bprs, changes, previous_version=previous_version)
82 information = {
83 'STATUS': ACTION_DESCRIPTIONS[action],
84 'SUMMARY': summary,
85 'DATE': 'Date: %s' % info['date'],
86- 'CHANGESFILE': info['changesfile'],
87+ 'CHANGESFILE': info['changelog'],
88 'DISTRO': distroseries.distribution.title,
89 'ANNOUNCE': 'No announcement sent',
90 'CHANGEDBY': '',
91@@ -596,7 +602,7 @@
92 pocket != PackagePublishingPocket.SECURITY)
93
94
95-def fetch_information(spr, bprs, changes):
96+def fetch_information(spr, bprs, changes, previous_version=None):
97 changedby = None
98 changedby_displayname = None
99 maintainer = None
100@@ -613,7 +619,7 @@
101 elif spr or bprs:
102 if not spr and bprs:
103 spr = bprs[0].build.source_package_release
104- changesfile = spr.changelog_entry
105+ changesfile = spr.aggregate_changelog(previous_version)
106 date = spr.dateuploaded
107 changedby = person_to_email(spr.creator)
108 maintainer = person_to_email(spr.maintainer)
109@@ -629,7 +635,7 @@
110 changesfile = date = None
111
112 return {
113- 'changesfile': changesfile,
114+ 'changelog': changesfile,
115 'date': date,
116 'changedby': changedby,
117 'changedby_displayname': changedby_displayname,
118
119=== modified file 'lib/lp/soyuz/adapters/tests/test_notification.py'
120--- lib/lp/soyuz/adapters/tests/test_notification.py 2011-08-26 14:57:39 +0000
121+++ lib/lp/soyuz/adapters/tests/test_notification.py 2011-08-30 15:05:02 +0000
122@@ -7,6 +7,7 @@
123
124 from email.utils import formataddr
125 from storm.store import Store
126+from textwrap import dedent
127 from zope.component import getUtility
128 from zope.security.proxy import removeSecurityProxy
129
130@@ -132,6 +133,60 @@
131 "=?utf-8?q?Lo=C3=AFc_Mot=C3=B6rhead?= <loic@example.com>",
132 notifications[1]["From"])
133
134+ def test_fetch_information_spr_multiple_changelogs(self):
135+ # If previous_version is passed the "changelog" entry in the
136+ # returned dict should contain the changelogs for all SPRs *since*
137+ # that version and up to and including the passed SPR.
138+ changelog = self.factory.makeChangelog(
139+ spn="foo", versions=["1.2", "1.1", "1.0"])
140+ spph = self.factory.makeSourcePackagePublishingHistory(
141+ sourcepackagename="foo", version="1.3", changelog=changelog)
142+ self.layer.txn.commit() # Yay, librarian.
143+
144+ spr = spph.sourcepackagerelease
145+ info = fetch_information(spr, None, None, previous_version="1.0")
146+
147+ self.assertIn("foo (1.1)", info['changelog'])
148+ self.assertIn("foo (1.2)", info['changelog'])
149+
150+ def test_notify_bpr_rejected(self):
151+ # If we notify about a rejected bpr with no source, a notification is
152+ # sent.
153+ bpr = self.factory.makeBinaryPackageRelease()
154+ changelog = self.factory.makeChangelog(spn="foo", versions=["1.1"])
155+ removeSecurityProxy(
156+ bpr.build.source_package_release).changelog = changelog
157+ self.layer.txn.commit()
158+ archive = self.factory.makeArchive()
159+ pocket = self.factory.getAnyPocket()
160+ distroseries = self.factory.makeDistroSeries()
161+ person = self.factory.makePerson()
162+ notify(
163+ person, None, [bpr], [], archive, distroseries, pocket,
164+ action='rejected')
165+ [notification] = pop_notifications()
166+ body = notification.get_payload()[0].get_payload()
167+ self.assertEqual(person_to_email(person), notification['To'])
168+ expected_body = dedent("""\
169+ Rejected:
170+ Rejected by archive administrator.
171+
172+ foo (1.1) unstable; urgency=3Dlow
173+
174+ * 1.1.
175+
176+ =3D=3D=3D
177+
178+ If you don't understand why your files were rejected please send an email
179+ to launchpad-users@lists.launchpad.net for help (requires membership).
180+
181+ -- =
182+
183+ You are receiving this email because you are the uploader of the above
184+ PPA package.
185+ """)
186+ self.assertEqual(expected_body, body)
187+
188
189 class TestNotification(TestCaseWithFactory):
190
191@@ -147,7 +202,7 @@
192 info = fetch_information(
193 None, None, changes)
194 self.assertEqual('2001-01-01', info['date'])
195- self.assertEqual(' * Foo!', info['changesfile'])
196+ self.assertEqual(' * Foo!', info['changelog'])
197 fields = [
198 info['changedby'],
199 info['maintainer'],
200@@ -164,7 +219,7 @@
201 creator=creator, maintainer=maintainer)
202 info = fetch_information(spr, None, None)
203 self.assertEqual(info['date'], spr.dateuploaded)
204- self.assertEqual(info['changesfile'], spr.changelog_entry)
205+ self.assertEqual(info['changelog'], spr.changelog_entry)
206 self.assertEqual(
207 info['changedby'], format_address_for_person(spr.creator))
208 self.assertEqual(
209@@ -181,7 +236,7 @@
210 info = fetch_information(None, [bpr], None)
211 spr = bpr.build.source_package_release
212 self.assertEqual(info['date'], spr.dateuploaded)
213- self.assertEqual(info['changesfile'], spr.changelog_entry)
214+ self.assertEqual(info['changelog'], spr.changelog_entry)
215 self.assertEqual(
216 info['changedby'], format_address_for_person(spr.creator))
217 self.assertEqual(
218@@ -234,24 +289,6 @@
219 notifications = pop_notifications()
220 self.assertEqual(0, len(notifications))
221
222- def test_notify_bpr_rejected(self):
223- # If we notify about a rejected bpr with no source, a notification is
224- # sent.
225- bpr = self.factory.makeBinaryPackageRelease()
226- removeSecurityProxy(
227- bpr.build.source_package_release).changelog_entry = '* Foo!'
228- archive = self.factory.makeArchive()
229- pocket = self.factory.getAnyPocket()
230- distroseries = self.factory.makeDistroSeries()
231- person = self.factory.makePerson()
232- notify(
233- person, None, [bpr], [], archive, distroseries, pocket,
234- action='rejected')
235- [notification] = pop_notifications()
236- body = notification.as_string()
237- self.assertEqual(person_to_email(person), notification['To'])
238- self.assertIn('Rejected by archive administrator.\n\n* Foo!\n', body)
239-
240 def test_reject_changes_file_no_email(self):
241 # If we are rejecting a mail, and the person to notify has no
242 # preferred email, we should return early.
243
244=== modified file 'lib/lp/soyuz/enums.py'
245--- lib/lp/soyuz/enums.py 2011-06-02 11:04:56 +0000
246+++ lib/lp/soyuz/enums.py 2011-08-30 15:05:02 +0000
247@@ -20,15 +20,27 @@
248 'PackagePublishingStatus',
249 'PackageUploadCustomFormat',
250 'PackageUploadStatus',
251+ 're_bug_numbers',
252+ 're_closes',
253+ 're_lp_closes',
254 'SourcePackageFormat',
255 ]
256
257+import re
258+
259 from lazr.enum import (
260 DBEnumeratedType,
261 DBItem,
262 )
263
264
265+# Regexes that match bug numbers for closing in change logs.
266+re_closes = re.compile(
267+ r"closes:\s*(?:bug)?\#?\s?\d+(?:,\s*(?:bug)?\#?\s?\d+)*", re.I)
268+re_lp_closes = re.compile(r"lp:\s+\#\d+(?:,\s*\#\d+)*", re.I)
269+re_bug_numbers = re.compile(r"\#?\s?(\d+)")
270+
271+
272 class ArchiveJobType(DBEnumeratedType):
273 """Values that IArchiveJob.job_type can take."""
274
275
276=== modified file 'lib/lp/soyuz/interfaces/archive.py'
277--- lib/lp/soyuz/interfaces/archive.py 2011-08-28 08:36:14 +0000
278+++ lib/lp/soyuz/interfaces/archive.py 2011-08-30 15:05:02 +0000
279@@ -974,7 +974,9 @@
280 :param created_since_date: Only return results whose `date_created`
281 is greater than or equal to this date.
282
283- :return: SelectResults containing `ISourcePackagePublishingHistory`.
284+ :return: SelectResults containing `ISourcePackagePublishingHistory`,
285+ ordered by name. If there are multiple results for the same
286+ name then they are sub-ordered newest first.
287 """
288
289 @rename_parameters_as(
290
291=== modified file 'lib/lp/soyuz/interfaces/sourcepackagerelease.py'
292--- lib/lp/soyuz/interfaces/sourcepackagerelease.py 2011-08-26 14:57:39 +0000
293+++ lib/lp/soyuz/interfaces/sourcepackagerelease.py 2011-08-30 15:05:02 +0000
294@@ -240,6 +240,17 @@
295 :return: total size (in KB) of this package
296 """
297
298+ def aggregate_changelog(since_version):
299+ """Get all the changelogs since the version specified.
300+
301+ :param since_version: Return changelogs of all versions
302+ after since_version up to and including the version of the
303+ sourcepackagerelease for this publication.
304+ :return: A concatenated set of changelogs of all the required
305+ versions, with a blank line between each. If there is no
306+ changelog, or there is an error parsing it, None is returned.
307+ """
308+
309
310 class PackageDiffAlreadyRequestedError(Exception):
311 """Raised when an `IPackageDiff` request already exists."""
312
313=== modified file 'lib/lp/soyuz/model/sourcepackagerelease.py'
314--- lib/lp/soyuz/model/sourcepackagerelease.py 2011-08-26 14:57:39 +0000
315+++ lib/lp/soyuz/model/sourcepackagerelease.py 2011-08-30 15:05:02 +0000
316@@ -10,7 +10,13 @@
317 ]
318
319
320+import apt_pkg
321 import datetime
322+from debian.changelog import (
323+ Changelog,
324+ ChangelogCreateError,
325+ ChangelogParseError,
326+ )
327 import operator
328 import re
329 from StringIO import StringIO
330@@ -603,3 +609,40 @@
331 return PackageDiff(
332 from_source=self, to_source=to_sourcepackagerelease,
333 requester=requester, status=status)
334+
335+ def aggregate_changelog(self, since_version):
336+ """See `ISourcePackagePublishingHistory`."""
337+ if self.changelog is None:
338+ return None
339+
340+ apt_pkg.InitSystem()
341+ chunks = []
342+ changelog = self.changelog
343+ # The python-debian API for parsing changelogs is pretty awful. The
344+ # only useful way of extracting info is to use the iterator on
345+ # Changelog and then compare versions.
346+ try:
347+ for block in Changelog(changelog.read()):
348+ version = block._raw_version
349+ if (since_version and
350+ apt_pkg.VersionCompare(version, since_version) <= 0):
351+ break
352+ # Poking in private attributes is not nice but again the
353+ # API is terrible. We want to ensure that the name/date
354+ # line is omitted from these composite changelogs.
355+ block._no_trailer = True
356+ try:
357+ # python-debian adds an extra blank line to the chunks
358+ # so we'll have to sort this out.
359+ chunks.append(str(block).rstrip())
360+ except ChangelogCreateError:
361+ continue
362+ if not since_version:
363+ # If a particular version was not requested we just
364+ # return the most recent changelog entry.
365+ break
366+ except ChangelogParseError:
367+ return None
368+
369+ output = "\n\n".join(chunks)
370+ return output.decode("utf-8", "replace")
371
372=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
373--- lib/lp/soyuz/scripts/packagecopier.py 2011-08-26 14:57:39 +0000
374+++ lib/lp/soyuz/scripts/packagecopier.py 2011-08-30 15:05:02 +0000
375@@ -628,16 +628,28 @@
376 override = None
377 if overrides:
378 override = overrides[overrides_index]
379+ old_version = None
380+ if send_email:
381+ # Make a note of the destination source's version for use
382+ # in sending the email notification.
383+ existing = archive.getPublishedSources(
384+ name=source.sourcepackagerelease.name, exact_match=True,
385+ status=active_publishing_status,
386+ distroseries=series, pocket=pocket).first()
387+ if existing:
388+ old_version = existing.sourcepackagerelease.version
389 sub_copies = _do_direct_copy(
390 source, archive, destination_series, pocket,
391 include_binaries, override, close_bugs=close_bugs,
392- create_dsd_job=create_dsd_job)
393+ create_dsd_job=create_dsd_job,
394+ close_bugs_since_version=old_version)
395 if send_email:
396 notify(
397 person, source.sourcepackagerelease, [], [], archive,
398 destination_series, pocket, changes=None,
399 action='accepted',
400- announce_from_person=announce_from_person)
401+ announce_from_person=announce_from_person,
402+ previous_version=old_version)
403
404 overrides_index += 1
405 copies.extend(sub_copies)
406@@ -646,7 +658,8 @@
407
408
409 def _do_direct_copy(source, archive, series, pocket, include_binaries,
410- override=None, close_bugs=True, create_dsd_job=True):
411+ override=None, close_bugs=True, create_dsd_job=True,
412+ close_bugs_since_version=None):
413 """Copy publishing records to another location.
414
415 Copy each item of the given list of `SourcePackagePublishingHistory`
416@@ -669,6 +682,9 @@
417 copied publication should be closed.
418 :param create_dsd_job: A boolean indicating whether or not a dsd job
419 should be created for the new source publication.
420+ :param close_bugs_since_version: If close_bugs is True,
421+ then this parameter says which changelog entries to parse looking
422+ for bugs to close. See `close_bugs_for_sourcepackagerelease`.
423
424 :return: a list of `ISourcePackagePublishingHistory` and
425 `BinaryPackagePublishingHistory` corresponding to the copied
426@@ -700,7 +716,8 @@
427 source_copy = source.copyTo(
428 series, pocket, archive, override, create_dsd_job=create_dsd_job)
429 if close_bugs:
430- close_bugs_for_sourcepublication(source_copy)
431+ close_bugs_for_sourcepublication(
432+ source_copy, close_bugs_since_version)
433 copies.append(source_copy)
434 else:
435 source_copy = source_in_destination.first()
436
437=== modified file 'lib/lp/soyuz/scripts/processaccepted.py'
438--- lib/lp/soyuz/scripts/processaccepted.py 2011-08-08 07:31:08 +0000
439+++ lib/lp/soyuz/scripts/processaccepted.py 2011-08-30 15:05:02 +0000
440@@ -37,6 +37,9 @@
441 from lp.soyuz.enums import (
442 ArchivePurpose,
443 PackageUploadStatus,
444+ re_bug_numbers,
445+ re_closes,
446+ re_lp_closes,
447 )
448 from lp.soyuz.interfaces.archive import IArchiveSet
449 from lp.soyuz.interfaces.queue import IPackageUploadSet
450@@ -64,6 +67,35 @@
451 return bugs
452
453
454+def get_bugs_from_changelog_entry(sourcepackagerelease, since_version):
455+ """Parse the changelog_entry in the sourcepackagerelease and return a
456+ list of `IBug`s referenced by it.
457+ """
458+ changelog = sourcepackagerelease.aggregate_changelog(since_version)
459+ closes = []
460+ # There are 2 main regexes to match. Each match from those can then
461+ # have further multiple matches from the 3rd regex:
462+ # closes: NNN, NNN
463+ # lp: #NNN, #NNN
464+ regexes = (
465+ re_closes.finditer(changelog), re_lp_closes.finditer(changelog))
466+ for regex in regexes:
467+ for match in regex:
468+ bug_match = re_bug_numbers.findall(match.group(0))
469+ closes += map(int, bug_match)
470+
471+ bugs = []
472+ for bug_id in closes:
473+ try:
474+ bug = getUtility(IBugSet).get(bug_id)
475+ except NotFoundError:
476+ continue
477+ else:
478+ bugs.append(bug)
479+
480+ return bugs
481+
482+
483 def can_close_bugs(target):
484 """Whether or not bugs should be closed in the given target.
485
486@@ -119,7 +151,7 @@
487 source_queue_item.sourcepackagerelease, changesfile_object)
488
489
490-def close_bugs_for_sourcepublication(source_publication):
491+def close_bugs_for_sourcepublication(source_publication, since_version=None):
492 """Close bugs for a given sourcepublication.
493
494 Given a `ISourcePackagePublishingHistory` close bugs mentioned in
495@@ -131,21 +163,33 @@
496 sourcepackagerelease = source_publication.sourcepackagerelease
497 changesfile_object = sourcepackagerelease.upload_changesfile
498
499- # No changesfile available, cannot close bugs.
500- if changesfile_object is None:
501- return
502-
503 close_bugs_for_sourcepackagerelease(
504- sourcepackagerelease, changesfile_object)
505-
506-
507-def close_bugs_for_sourcepackagerelease(source_release, changesfile_object):
508+ sourcepackagerelease, changesfile_object, since_version)
509+
510+
511+def close_bugs_for_sourcepackagerelease(source_release, changesfile_object,
512+ since_version=None):
513 """Close bugs for a given source.
514
515 Given a `ISourcePackageRelease` and a corresponding changesfile object,
516 close bugs mentioned in the changesfile in the context of the source.
517+
518+ If changesfile_object is None and since_version is supplied,
519+ close all the bugs in changelog entries made after that version and up
520+ to and including the source_release's version. It does this by parsing
521+ the changelog on the sourcepackagerelease. This could be extended in
522+ the future to deal with the changes file as well but there is no
523+ requirement to do so right now.
524 """
525- bugs_to_close = get_bugs_from_changes_file(changesfile_object)
526+ if since_version is not None:
527+ assert changesfile_object is None, (
528+ "Only set since_version if changesfile_object is None")
529+
530+ if changesfile_object:
531+ bugs_to_close = get_bugs_from_changes_file(changesfile_object)
532+ else:
533+ bugs_to_close = get_bugs_from_changelog_entry(
534+ source_release, since_version=since_version)
535
536 # No bugs to be closed by this upload, move on.
537 if not bugs_to_close:
538
539=== modified file 'lib/lp/soyuz/scripts/tests/test_copypackage.py'
540--- lib/lp/soyuz/scripts/tests/test_copypackage.py 2011-08-26 14:57:39 +0000
541+++ lib/lp/soyuz/scripts/tests/test_copypackage.py 2011-08-30 15:05:02 +0000
542@@ -1413,7 +1413,9 @@
543 archive = self.test_publisher.ubuntutest.main_archive
544 source = self.test_publisher.getPubSource(
545 archive=archive, version='1.0-2', architecturehintlist='any')
546- source.sourcepackagerelease.changelog_entry = '* Foo!'
547+ changelog = self.factory.makeChangelog(spn="foo", versions=["1.0-2"])
548+ source.sourcepackagerelease.changelog = changelog
549+ transaction.commit() # Librarian.
550 nobby = self.createNobby(('i386', 'hppa'))
551 getUtility(ISourcePackageFormatSelectionSet).add(
552 nobby, SourcePackageFormat.FORMAT_1_0)
553@@ -1427,9 +1429,22 @@
554 self.assertEquals(
555 get_ppa_reference(target_archive),
556 notification['X-Launchpad-PPA'])
557- self.assertIn(
558- source.sourcepackagerelease.changelog_entry,
559- notification.as_string())
560+ body = notification.get_payload()[0].get_payload()
561+ expected = dedent("""\
562+ Accepted:
563+ OK: foo_1.0-2.dsc
564+ -> Component: main Section: base
565+
566+ foo (1.0-2) unstable; urgency=3Dlow
567+
568+ * 1.0-2.
569+
570+ -- =
571+
572+ You are receiving this email because you are the uploader of the above
573+ PPA package.
574+ """)
575+ self.assertEqual(expected, body)
576
577 def test_copy_generates_notification(self):
578 # When a copy into a primary archive is performed, a notification is
579@@ -1437,7 +1452,8 @@
580 archive = self.test_publisher.ubuntutest.main_archive
581 source = self.test_publisher.getPubSource(
582 archive=archive, version='1.0-2', architecturehintlist='any')
583- source.sourcepackagerelease.changelog_entry = '* Foo!'
584+ changelog = self.factory.makeChangelog(spn="foo", versions=["1.0-2"])
585+ source.sourcepackagerelease.changelog = changelog
586 # Copying to a primary archive reads the changes to close bugs.
587 transaction.commit()
588 nobby = self.createNobby(('i386', 'hppa'))
589@@ -1455,15 +1471,59 @@
590 for mail in (notification, announcement):
591 self.assertEquals(
592 '[ubuntutest/nobby] foo 1.0-2 (Accepted)', mail['Subject'])
593- expected_text = dedent("""
594- * Foo!
595+ expected_text = dedent("""\
596+ foo (1.0-2) unstable; urgency=3Dlow
597+
598+ * 1.0-2.
599
600 Date: %s
601 Changed-By: Foo Bar <foo.bar@canonical.com>
602 http://launchpad.dev/ubuntutest/nobby/+source/foo/1.0-2
603 """ % source.sourcepackagerelease.dateuploaded)
604- self.assertIn(expected_text, notification.as_string())
605- self.assertIn(expected_text, announcement.as_string())
606+ # Spurious newlines are a pain and don't really affect the end
607+ # results so stripping is the easiest route here.
608+ expected_text.strip()
609+ body = mail.get_payload()[0].get_payload()
610+ self.assertEqual(expected_text, body)
611+ self.assertEqual(expected_text, body)
612+
613+ def test_copy_notification_contains_aggregate_change_log(self):
614+ # When copying a package that generates a notification,
615+ # the changelog should contain all of the changelog_entry texts for
616+ # all the sourcepackagereleases between the last published version
617+ # and the new version.
618+ archive = self.test_publisher.ubuntutest.main_archive
619+ source3 = self.test_publisher.getPubSource(
620+ sourcename="foo", archive=archive, version='1.2',
621+ architecturehintlist='any')
622+ changelog = self.factory.makeChangelog(
623+ spn="foo", versions=["1.2", "1.1", "1.0"])
624+ source3.sourcepackagerelease.changelog = changelog
625+ transaction.commit()
626+
627+ # Now make a new series, nobby, and publish foo 1.0 in it.
628+ nobby = self.createNobby(('i386', 'hppa'))
629+ getUtility(ISourcePackageFormatSelectionSet).add(
630+ nobby, SourcePackageFormat.FORMAT_1_0)
631+ nobby.changeslist = 'nobby-changes@example.com'
632+ source1 = self.factory.makeSourcePackageRelease(
633+ sourcepackagename="foo", version="1.0")
634+ self.factory.makeSourcePackagePublishingHistory(
635+ sourcepackagerelease=source1, distroseries=nobby,
636+ status=PackagePublishingStatus.PUBLISHED,
637+ pocket=source3.pocket)
638+
639+ # Now copy foo 1.3 from ubuntutest.
640+ [copied_source] = do_copy(
641+ [source3], nobby.main_archive, nobby, source3.pocket, False,
642+ person=source3.sourcepackagerelease.creator,
643+ check_permissions=False, send_email=True)
644+
645+ [notification, announcement] = pop_notifications()
646+ for mail in (notification, announcement):
647+ mailtext = mail.as_string()
648+ self.assertIn("foo (1.1)", mailtext)
649+ self.assertIn("foo (1.2)", mailtext)
650
651 def test_copy_generates_rejection_email(self):
652 # When a copy into a primary archive fails, we expect a rejection
653
654=== added file 'lib/lp/soyuz/scripts/tests/test_processaccepted.py'
655--- lib/lp/soyuz/scripts/tests/test_processaccepted.py 1970-01-01 00:00:00 +0000
656+++ lib/lp/soyuz/scripts/tests/test_processaccepted.py 2011-08-30 15:05:02 +0000
657@@ -0,0 +1,91 @@
658+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
659+# GNU Affero General Public License version 3 (see the file LICENSE).
660+
661+__metaclass__ = type
662+
663+from textwrap import dedent
664+from zope.security.proxy import removeSecurityProxy
665+
666+from canonical.testing.layers import LaunchpadZopelessLayer
667+from lp.bugs.interfaces.bugtask import BugTaskStatus
668+from lp.soyuz.scripts.processaccepted import (
669+ close_bugs_for_sourcepackagerelease,
670+ )
671+from lp.testing import TestCaseWithFactory
672+
673+
674+class TestClosingBugs(TestCaseWithFactory):
675+ """Test the various bug closing methods in processaccepted.py.
676+
677+ Tests are currently spread around the codebase; this is an attempt to
678+ start a unification in a single file and those other tests need
679+ migrating here.
680+ See also:
681+ * lp/soyuz/scripts/tests/test_queue.py
682+ * lib/lp/soyuz/doc/closing-bugs-from-changelogs.txt
683+ * lib/lp/archiveuploader/tests/nascentupload-closing-bugs.txt
684+ """
685+ layer = LaunchpadZopelessLayer
686+
687+ def test_close_bugs_for_sourcepackagerelease_with_no_changes_file(self):
688+ # If there's no changes file it should read the changelog_entry on
689+ # the sourcepackagerelease.
690+
691+ spr = self.factory.makeSourcePackageRelease(changelog_entry="blah")
692+
693+ # Make 4 bugs and corresponding bugtasks and put them in an array
694+ # as tuples.
695+ bugs = []
696+ for i in range(5):
697+ bug = self.factory.makeBug()
698+ bugtask = self.factory.makeBugTask(
699+ target=spr.sourcepackage, bug=bug)
700+ bugs.append((bug, bugtask))
701+
702+ unfixed_bug = self.factory.makeBug()
703+ unfixed_task = self.factory.makeBugTask(
704+ target=spr.sourcepackage, bug=unfixed_bug)
705+
706+ # Make a changelog entry for a package which contains the IDs of
707+ # the 5 bugs separated across 2 releases.
708+ changelog=dedent("""
709+ foo (1.0-3) unstable; urgency=low
710+
711+ * closes: %s, %s
712+ * lp: #%s, #%s
713+
714+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
715+
716+ foo (1.0-2) unstable; urgency=low
717+
718+ * closes: %s
719+
720+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
721+
722+ foo (1.0-1) unstable; urgency=low
723+
724+ * closes: %s
725+
726+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
727+
728+ """ % (
729+ bugs[0][0].id,
730+ bugs[1][0].id,
731+ bugs[2][0].id,
732+ bugs[3][0].id,
733+ bugs[4][0].id,
734+ unfixed_bug.id,
735+ ))
736+ lfa = self.factory.makeLibraryFileAlias(content=changelog)
737+
738+ removeSecurityProxy(spr).changelog = lfa
739+ self.layer.txn.commit()
740+
741+ # Call the method and test it's closed the bugs.
742+ close_bugs_for_sourcepackagerelease(spr, changesfile_object=None,
743+ since_version="1.0-1")
744+ for bug, bugtask in bugs:
745+ self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask.status)
746+
747+ self.assertEqual(BugTaskStatus.NEW, unfixed_task.status)
748+
749
750=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
751--- lib/lp/soyuz/tests/test_packagecopyjob.py 2011-08-23 14:43:56 +0000
752+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2011-08-30 15:05:02 +0000
753@@ -8,6 +8,7 @@
754 from storm.store import Store
755 from testtools.content import text_content
756 from testtools.matchers import MatchesStructure
757+from textwrap import dedent
758 import transaction
759 from zope.component import getUtility
760 from zope.security.interfaces import Unauthorized
761@@ -21,6 +22,7 @@
762 LaunchpadZopelessLayer,
763 ZopelessDatabaseLayer,
764 )
765+from lp.bugs.interfaces.bugtask import BugTaskStatus
766 from lp.registry.interfaces.pocket import PackagePublishingPocket
767 from lp.registry.interfaces.series import SeriesStatus
768 from lp.registry.model.distroseriesdifferencecomment import (
769@@ -919,6 +921,86 @@
770 "Nancy Requester <requester@example.com>",
771 emails[1]['From'])
772
773+ def test_copying_closes_bugs(self):
774+ # Copying a package into a primary archive should close any bugs
775+ # mentioned in its changelog for versions added since the most
776+ # recently published version in the target.
777+
778+ # Firstly, lots of boring set up.
779+ publisher = SoyuzTestPublisher()
780+ publisher.prepareBreezyAutotest()
781+ distroseries = publisher.breezy_autotest
782+ target_archive = self.factory.makeArchive(
783+ distroseries.distribution, purpose=ArchivePurpose.PRIMARY)
784+ source_archive = self.factory.makeArchive()
785+ bug280 = self.factory.makeBug()
786+ bug281 = self.factory.makeBug()
787+ bug282 = self.factory.makeBug()
788+
789+ # Publish a package in the source archive and give it a changelog
790+ # entry that closes a bug.
791+ source_pub = self.factory.makeSourcePackagePublishingHistory(
792+ distroseries=distroseries, sourcepackagename="libc",
793+ version="2.8-2", status=PackagePublishingStatus.PUBLISHED,
794+ archive=source_archive)
795+ spr = removeSecurityProxy(source_pub).sourcepackagerelease
796+ changelog = dedent("""\
797+ libc (2.8-2) unstable; urgency=low
798+
799+ * closes: %s
800+
801+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
802+
803+ libc (2.8-1) unstable; urgency=low
804+
805+ * closes: %s
806+
807+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
808+
809+ libc (2.8-0) unstable; urgency=low
810+
811+ * closes: %s
812+
813+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
814+ """ % (bug282.id, bug281.id, bug280.id))
815+ spr.changelog = self.factory.makeLibraryFileAlias(content=changelog)
816+ spr.changelog_entry = "dummy"
817+ self.layer.txn.commit() # Librarian.
818+ bugtask280 = self.factory.makeBugTask(
819+ target=spr.sourcepackage, bug=bug280)
820+ bugtask281 = self.factory.makeBugTask(
821+ target=spr.sourcepackage, bug=bug281)
822+ bugtask282 = self.factory.makeBugTask(
823+ target=spr.sourcepackage, bug=bug282)
824+
825+ # Now put the same named package in the target archive at the
826+ # oldest version in the changelog.
827+ publisher.getPubSource(
828+ distroseries=distroseries, sourcename="libc",
829+ version="2.8-0", status=PackagePublishingStatus.PUBLISHED,
830+ archive=target_archive)
831+
832+ # Run the copy job.
833+ source = getUtility(IPlainPackageCopyJobSource)
834+ requester = self.factory.makePerson()
835+ with person_logged_in(target_archive.owner):
836+ target_archive.newComponentUploader(requester, "main")
837+ job = source.create(
838+ package_name="libc",
839+ package_version="2.8-2",
840+ source_archive=source_archive,
841+ target_archive=target_archive,
842+ target_distroseries=distroseries,
843+ target_pocket=PackagePublishingPocket.RELEASE,
844+ include_binaries=False,
845+ requester=requester)
846+ self.runJob(job)
847+
848+ # All the bugs apart from the one for 2.8-0 should be fixed.
849+ self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask282.status)
850+ self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask281.status)
851+ self.assertEqual(BugTaskStatus.NEW, bugtask280.status)
852+
853 def test_findMatchingDSDs_matches_all_DSDs_for_job(self):
854 # findMatchingDSDs finds matching DSDs for any of the packages
855 # in the job.
856
857=== modified file 'lib/lp/soyuz/tests/test_sourcepackagerelease.py'
858--- lib/lp/soyuz/tests/test_sourcepackagerelease.py 2011-08-26 14:57:39 +0000
859+++ lib/lp/soyuz/tests/test_sourcepackagerelease.py 2011-08-30 15:05:02 +0000
860@@ -5,6 +5,7 @@
861
862 __metaclass__ = type
863
864+from textwrap import dedent
865 import transaction
866 from zope.component import getUtility
867
868@@ -81,6 +82,32 @@
869 spr = self.factory.makeSourcePackageRelease(homepage="<invalid<url")
870 self.assertEquals("<invalid<url", spr.homepage)
871
872+ def test_aggregate_changelog(self):
873+ # If since_version is passed the "changelog" entry returned
874+ # should contain the changelogs for all releases *since*
875+ # that version and up to and including the context SPR.
876+ changelog = self.factory.makeChangelog(
877+ spn="foo", versions=["1.3", "1.2", "1.1", "1.0"])
878+ expected_changelog = dedent(u"""\
879+ foo (1.3) unstable; urgency=low
880+
881+ * 1.3.
882+
883+ foo (1.2) unstable; urgency=low
884+
885+ * 1.2.
886+
887+ foo (1.1) unstable; urgency=low
888+
889+ * 1.1.""")
890+ spph = self.factory.makeSourcePackagePublishingHistory(
891+ sourcepackagename="foo", version="1.3", changelog=changelog)
892+ transaction.commit() # Yay, librarian.
893+
894+ observed = spph.sourcepackagerelease.aggregate_changelog(
895+ since_version="1.0")
896+ self.assertEqual(expected_changelog, observed)
897+
898
899 class TestSourcePackageReleaseGetBuildByArch(TestCaseWithFactory):
900 """Tests for SourcePackageRelease.getBuildByArch()."""
901
902=== modified file 'lib/lp/testing/factory.py'
903--- lib/lp/testing/factory.py 2011-08-30 00:07:34 +0000
904+++ lib/lp/testing/factory.py 2011-08-30 15:05:02 +0000
905@@ -1,3 +1,7 @@
906+# -*- coding: utf-8 -*-
907+# NOTE: The first line above must stay first; do not move the copyright
908+# notice to the top. See http://www.python.org/dev/peps/pep-0263/.
909+#
910 # Copyright 2009-2011 Canonical Ltd. This software is licensed under the
911 # GNU Affero General Public License version 3 (see the file LICENSE).
912
913@@ -2181,21 +2185,26 @@
914 review_status=review_status)
915
916 def makeChangelog(self, spn=None, versions=[]):
917- """Create and return a LFA of a valid Debian-style changelog."""
918+ """Create and return a LFA of a valid Debian-style changelog.
919+
920+ Note that the changelog returned is unicode - this is deliberate
921+ so that code is forced to cope with it as utf-8 changelogs are
922+ normal.
923+ """
924 if spn is None:
925 spn = self.getUniqueString()
926 changelog = ''
927 for version in versions:
928- entry = dedent('''
929+ entry = dedent(u'''\
930 %s (%s) unstable; urgency=low
931
932 * %s.
933
934- -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
935+ -- Føo Bær <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
936
937 ''' % (spn, version, version))
938 changelog += entry
939- return self.makeLibraryFileAlias(content=changelog)
940+ return self.makeLibraryFileAlias(content=changelog.encode("utf-8"))
941
942 def makeCodeImportEvent(self):
943 """Create and return a CodeImportEvent."""
944
945=== modified file 'scripts/ftpmaster-tools/sync-source.py'
946--- scripts/ftpmaster-tools/sync-source.py 2011-06-09 10:50:25 +0000
947+++ scripts/ftpmaster-tools/sync-source.py 2011-08-30 15:05:02 +0000
948@@ -51,7 +51,12 @@
949 from lp.registry.interfaces.distribution import IDistributionSet
950 from lp.registry.interfaces.person import IPersonSet
951 from lp.registry.interfaces.pocket import PackagePublishingPocket
952-from lp.soyuz.enums import PackagePublishingStatus
953+from lp.soyuz.enums import (
954+ PackagePublishingStatus,
955+ re_bug_numbers,
956+ re_closes,
957+ re_lp_closes,
958+ )
959 from lp.soyuz.scripts.ftpmaster import (
960 generate_changes,
961 SyncSource,
962@@ -63,10 +68,6 @@
963 re_strip_revision = re.compile(r"-([^-]+)$")
964 re_changelog_header = re.compile(
965 r"^\S+ \((?P<version>.*)\) .*;.*urgency=(?P<urgency>\w+).*")
966-re_closes = re.compile(
967- r"closes:\s*(?:bug)?\#?\s?\d+(?:,\s*(?:bug)?\#?\s?\d+)*", re.I)
968-re_lp_closes = re.compile(r"lp:\s+\#\d+(?:,\s*\#\d+)*", re.I)
969-re_bug_numbers = re.compile(r"\#?\s?(\d+)")
970
971
972 Blacklisted = None