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

Proposed by Julian Edwards
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 13835
Proposed branch: lp:~julian-edwards/launchpad/sync-close-bugs-bug-833736
Merge into: lp:launchpad
Prerequisite: lp:~julian-edwards/launchpad/changelogs-bug-827576
Diff against target: 447 lines (+266/-30)
7 files modified
database/schema/security.cfg (+4/-1)
lib/lp/soyuz/enums.py (+12/-0)
lib/lp/soyuz/scripts/packagecopier.py (+19/-14)
lib/lp/soyuz/scripts/processaccepted.py (+52/-10)
lib/lp/soyuz/scripts/tests/test_processaccepted.py (+91/-0)
lib/lp/soyuz/tests/test_packagecopyjob.py (+82/-0)
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
Graham Binns (community) code Approve
Review via email: mp+73401@code.launchpad.net

This proposal supersedes a proposal from 2011-08-30.

Commit message

[r=gmb][bug=833736] When syncing packages to a distribution, make sure any bugs referenced in the package's changelog are closed.

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.
Revision history for this message
Graham Binns (gmb) :
review: Approve (code)

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-31 12:04:12 +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/enums.py'
18--- lib/lp/soyuz/enums.py 2011-06-02 11:04:56 +0000
19+++ lib/lp/soyuz/enums.py 2011-08-31 12:04:12 +0000
20@@ -20,15 +20,27 @@
21 'PackagePublishingStatus',
22 'PackageUploadCustomFormat',
23 'PackageUploadStatus',
24+ 're_bug_numbers',
25+ 're_closes',
26+ 're_lp_closes',
27 'SourcePackageFormat',
28 ]
29
30+import re
31+
32 from lazr.enum import (
33 DBEnumeratedType,
34 DBItem,
35 )
36
37
38+# Regexes that match bug numbers for closing in change logs.
39+re_closes = re.compile(
40+ r"closes:\s*(?:bug)?\#?\s?\d+(?:,\s*(?:bug)?\#?\s?\d+)*", re.I)
41+re_lp_closes = re.compile(r"lp:\s+\#\d+(?:,\s*\#\d+)*", re.I)
42+re_bug_numbers = re.compile(r"\#?\s?(\d+)")
43+
44+
45 class ArchiveJobType(DBEnumeratedType):
46 """Values that IArchiveJob.job_type can take."""
47
48
49=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
50--- lib/lp/soyuz/scripts/packagecopier.py 2011-08-26 16:28:04 +0000
51+++ lib/lp/soyuz/scripts/packagecopier.py 2011-08-31 12:04:12 +0000
52@@ -628,21 +628,21 @@
53 override = None
54 if overrides:
55 override = overrides[overrides_index]
56- if send_email:
57- # Make a note of the destination source's version for use
58- # in sending the email notification.
59- existing = archive.getPublishedSources(
60- name=source.sourcepackagerelease.name, exact_match=True,
61- status=active_publishing_status,
62- distroseries=series, pocket=pocket).first()
63- if existing:
64- old_version = existing.sourcepackagerelease.version
65- else:
66- old_version = None
67+ # Make a note of the destination source's version for use
68+ # in sending the email notification and closing bugs.
69+ existing = archive.getPublishedSources(
70+ name=source.sourcepackagerelease.name, exact_match=True,
71+ status=active_publishing_status,
72+ distroseries=series, pocket=pocket).first()
73+ if existing:
74+ old_version = existing.sourcepackagerelease.version
75+ else:
76+ old_version = None
77 sub_copies = _do_direct_copy(
78 source, archive, destination_series, pocket,
79 include_binaries, override, close_bugs=close_bugs,
80- create_dsd_job=create_dsd_job)
81+ create_dsd_job=create_dsd_job,
82+ close_bugs_since_version=old_version)
83 if send_email:
84 notify(
85 person, source.sourcepackagerelease, [], [], archive,
86@@ -658,7 +658,8 @@
87
88
89 def _do_direct_copy(source, archive, series, pocket, include_binaries,
90- override=None, close_bugs=True, create_dsd_job=True):
91+ override=None, close_bugs=True, create_dsd_job=True,
92+ close_bugs_since_version=None):
93 """Copy publishing records to another location.
94
95 Copy each item of the given list of `SourcePackagePublishingHistory`
96@@ -681,6 +682,9 @@
97 copied publication should be closed.
98 :param create_dsd_job: A boolean indicating whether or not a dsd job
99 should be created for the new source publication.
100+ :param close_bugs_since_version: If close_bugs is True,
101+ then this parameter says which changelog entries to parse looking
102+ for bugs to close. See `close_bugs_for_sourcepackagerelease`.
103
104 :return: a list of `ISourcePackagePublishingHistory` and
105 `BinaryPackagePublishingHistory` corresponding to the copied
106@@ -712,7 +716,8 @@
107 source_copy = source.copyTo(
108 series, pocket, archive, override, create_dsd_job=create_dsd_job)
109 if close_bugs:
110- close_bugs_for_sourcepublication(source_copy)
111+ close_bugs_for_sourcepublication(
112+ source_copy, close_bugs_since_version)
113 copies.append(source_copy)
114 else:
115 source_copy = source_in_destination.first()
116
117=== modified file 'lib/lp/soyuz/scripts/processaccepted.py'
118--- lib/lp/soyuz/scripts/processaccepted.py 2011-08-08 07:31:08 +0000
119+++ lib/lp/soyuz/scripts/processaccepted.py 2011-08-31 12:04:12 +0000
120@@ -37,6 +37,9 @@
121 from lp.soyuz.enums import (
122 ArchivePurpose,
123 PackageUploadStatus,
124+ re_bug_numbers,
125+ re_closes,
126+ re_lp_closes,
127 )
128 from lp.soyuz.interfaces.archive import IArchiveSet
129 from lp.soyuz.interfaces.queue import IPackageUploadSet
130@@ -64,6 +67,35 @@
131 return bugs
132
133
134+def get_bugs_from_changelog_entry(sourcepackagerelease, since_version):
135+ """Parse the changelog_entry in the sourcepackagerelease and return a
136+ list of `IBug`s referenced by it.
137+ """
138+ changelog = sourcepackagerelease.aggregate_changelog(since_version)
139+ closes = []
140+ # There are 2 main regexes to match. Each match from those can then
141+ # have further multiple matches from the 3rd regex:
142+ # closes: NNN, NNN
143+ # lp: #NNN, #NNN
144+ regexes = (
145+ re_closes.finditer(changelog), re_lp_closes.finditer(changelog))
146+ for regex in regexes:
147+ for match in regex:
148+ bug_match = re_bug_numbers.findall(match.group(0))
149+ closes += map(int, bug_match)
150+
151+ bugs = []
152+ for bug_id in closes:
153+ try:
154+ bug = getUtility(IBugSet).get(bug_id)
155+ except NotFoundError:
156+ continue
157+ else:
158+ bugs.append(bug)
159+
160+ return bugs
161+
162+
163 def can_close_bugs(target):
164 """Whether or not bugs should be closed in the given target.
165
166@@ -119,7 +151,7 @@
167 source_queue_item.sourcepackagerelease, changesfile_object)
168
169
170-def close_bugs_for_sourcepublication(source_publication):
171+def close_bugs_for_sourcepublication(source_publication, since_version=None):
172 """Close bugs for a given sourcepublication.
173
174 Given a `ISourcePackagePublishingHistory` close bugs mentioned in
175@@ -131,21 +163,31 @@
176 sourcepackagerelease = source_publication.sourcepackagerelease
177 changesfile_object = sourcepackagerelease.upload_changesfile
178
179- # No changesfile available, cannot close bugs.
180- if changesfile_object is None:
181- return
182-
183 close_bugs_for_sourcepackagerelease(
184- sourcepackagerelease, changesfile_object)
185-
186-
187-def close_bugs_for_sourcepackagerelease(source_release, changesfile_object):
188+ sourcepackagerelease, changesfile_object, since_version)
189+
190+
191+def close_bugs_for_sourcepackagerelease(source_release, changesfile_object,
192+ since_version=None):
193 """Close bugs for a given source.
194
195 Given a `ISourcePackageRelease` and a corresponding changesfile object,
196 close bugs mentioned in the changesfile in the context of the source.
197+
198+ If changesfile_object is None and since_version is supplied,
199+ close all the bugs in changelog entries made after that version and up
200+ to and including the source_release's version. It does this by parsing
201+ the changelog on the sourcepackagerelease. This could be extended in
202+ the future to deal with the changes file as well but there is no
203+ requirement to do so right now.
204 """
205- bugs_to_close = get_bugs_from_changes_file(changesfile_object)
206+ if since_version and source_release.changelog:
207+ bugs_to_close = get_bugs_from_changelog_entry(
208+ source_release, since_version=since_version)
209+ elif changesfile_object:
210+ bugs_to_close = get_bugs_from_changes_file(changesfile_object)
211+ else:
212+ return
213
214 # No bugs to be closed by this upload, move on.
215 if not bugs_to_close:
216
217=== added file 'lib/lp/soyuz/scripts/tests/test_processaccepted.py'
218--- lib/lp/soyuz/scripts/tests/test_processaccepted.py 1970-01-01 00:00:00 +0000
219+++ lib/lp/soyuz/scripts/tests/test_processaccepted.py 2011-08-31 12:04:12 +0000
220@@ -0,0 +1,91 @@
221+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
222+# GNU Affero General Public License version 3 (see the file LICENSE).
223+
224+__metaclass__ = type
225+
226+from textwrap import dedent
227+from zope.security.proxy import removeSecurityProxy
228+
229+from canonical.testing.layers import LaunchpadZopelessLayer
230+from lp.bugs.interfaces.bugtask import BugTaskStatus
231+from lp.soyuz.scripts.processaccepted import (
232+ close_bugs_for_sourcepackagerelease,
233+ )
234+from lp.testing import TestCaseWithFactory
235+
236+
237+class TestClosingBugs(TestCaseWithFactory):
238+ """Test the various bug closing methods in processaccepted.py.
239+
240+ Tests are currently spread around the codebase; this is an attempt to
241+ start a unification in a single file and those other tests need
242+ migrating here.
243+ See also:
244+ * lp/soyuz/scripts/tests/test_queue.py
245+ * lib/lp/soyuz/doc/closing-bugs-from-changelogs.txt
246+ * lib/lp/archiveuploader/tests/nascentupload-closing-bugs.txt
247+ """
248+ layer = LaunchpadZopelessLayer
249+
250+ def test_close_bugs_for_sourcepackagerelease_with_no_changes_file(self):
251+ # If there's no changes file it should read the changelog_entry on
252+ # the sourcepackagerelease.
253+
254+ spr = self.factory.makeSourcePackageRelease(changelog_entry="blah")
255+
256+ # Make 4 bugs and corresponding bugtasks and put them in an array
257+ # as tuples.
258+ bugs = []
259+ for i in range(5):
260+ bug = self.factory.makeBug()
261+ bugtask = self.factory.makeBugTask(
262+ target=spr.sourcepackage, bug=bug)
263+ bugs.append((bug, bugtask))
264+
265+ unfixed_bug = self.factory.makeBug()
266+ unfixed_task = self.factory.makeBugTask(
267+ target=spr.sourcepackage, bug=unfixed_bug)
268+
269+ # Make a changelog entry for a package which contains the IDs of
270+ # the 5 bugs separated across 2 releases.
271+ changelog=dedent("""
272+ foo (1.0-3) unstable; urgency=low
273+
274+ * closes: %s, %s
275+ * lp: #%s, #%s
276+
277+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
278+
279+ foo (1.0-2) unstable; urgency=low
280+
281+ * closes: %s
282+
283+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
284+
285+ foo (1.0-1) unstable; urgency=low
286+
287+ * closes: %s
288+
289+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
290+
291+ """ % (
292+ bugs[0][0].id,
293+ bugs[1][0].id,
294+ bugs[2][0].id,
295+ bugs[3][0].id,
296+ bugs[4][0].id,
297+ unfixed_bug.id,
298+ ))
299+ lfa = self.factory.makeLibraryFileAlias(content=changelog)
300+
301+ removeSecurityProxy(spr).changelog = lfa
302+ self.layer.txn.commit()
303+
304+ # Call the method and test it's closed the bugs.
305+ close_bugs_for_sourcepackagerelease(spr, changesfile_object=None,
306+ since_version="1.0-1")
307+ for bug, bugtask in bugs:
308+ self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask.status)
309+
310+ self.assertEqual(BugTaskStatus.NEW, unfixed_task.status)
311+
312
313=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
314--- lib/lp/soyuz/tests/test_packagecopyjob.py 2011-08-23 14:43:56 +0000
315+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2011-08-31 12:04:12 +0000
316@@ -8,6 +8,7 @@
317 from storm.store import Store
318 from testtools.content import text_content
319 from testtools.matchers import MatchesStructure
320+from textwrap import dedent
321 import transaction
322 from zope.component import getUtility
323 from zope.security.interfaces import Unauthorized
324@@ -21,6 +22,7 @@
325 LaunchpadZopelessLayer,
326 ZopelessDatabaseLayer,
327 )
328+from lp.bugs.interfaces.bugtask import BugTaskStatus
329 from lp.registry.interfaces.pocket import PackagePublishingPocket
330 from lp.registry.interfaces.series import SeriesStatus
331 from lp.registry.model.distroseriesdifferencecomment import (
332@@ -919,6 +921,86 @@
333 "Nancy Requester <requester@example.com>",
334 emails[1]['From'])
335
336+ def test_copying_closes_bugs(self):
337+ # Copying a package into a primary archive should close any bugs
338+ # mentioned in its changelog for versions added since the most
339+ # recently published version in the target.
340+
341+ # Firstly, lots of boring set up.
342+ publisher = SoyuzTestPublisher()
343+ publisher.prepareBreezyAutotest()
344+ distroseries = publisher.breezy_autotest
345+ target_archive = self.factory.makeArchive(
346+ distroseries.distribution, purpose=ArchivePurpose.PRIMARY)
347+ source_archive = self.factory.makeArchive()
348+ bug280 = self.factory.makeBug()
349+ bug281 = self.factory.makeBug()
350+ bug282 = self.factory.makeBug()
351+
352+ # Publish a package in the source archive and give it a changelog
353+ # entry that closes a bug.
354+ source_pub = self.factory.makeSourcePackagePublishingHistory(
355+ distroseries=distroseries, sourcepackagename="libc",
356+ version="2.8-2", status=PackagePublishingStatus.PUBLISHED,
357+ archive=source_archive)
358+ spr = removeSecurityProxy(source_pub).sourcepackagerelease
359+ changelog = dedent("""\
360+ libc (2.8-2) unstable; urgency=low
361+
362+ * closes: %s
363+
364+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
365+
366+ libc (2.8-1) unstable; urgency=low
367+
368+ * closes: %s
369+
370+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
371+
372+ libc (2.8-0) unstable; urgency=low
373+
374+ * closes: %s
375+
376+ -- Foo Bar <foo@example.com> Tue, 01 Jan 1970 01:50:41 +0000
377+ """ % (bug282.id, bug281.id, bug280.id))
378+ spr.changelog = self.factory.makeLibraryFileAlias(content=changelog)
379+ spr.changelog_entry = "dummy"
380+ self.layer.txn.commit() # Librarian.
381+ bugtask280 = self.factory.makeBugTask(
382+ target=spr.sourcepackage, bug=bug280)
383+ bugtask281 = self.factory.makeBugTask(
384+ target=spr.sourcepackage, bug=bug281)
385+ bugtask282 = self.factory.makeBugTask(
386+ target=spr.sourcepackage, bug=bug282)
387+
388+ # Now put the same named package in the target archive at the
389+ # oldest version in the changelog.
390+ publisher.getPubSource(
391+ distroseries=distroseries, sourcename="libc",
392+ version="2.8-0", status=PackagePublishingStatus.PUBLISHED,
393+ archive=target_archive)
394+
395+ # Run the copy job.
396+ source = getUtility(IPlainPackageCopyJobSource)
397+ requester = self.factory.makePerson()
398+ with person_logged_in(target_archive.owner):
399+ target_archive.newComponentUploader(requester, "main")
400+ job = source.create(
401+ package_name="libc",
402+ package_version="2.8-2",
403+ source_archive=source_archive,
404+ target_archive=target_archive,
405+ target_distroseries=distroseries,
406+ target_pocket=PackagePublishingPocket.RELEASE,
407+ include_binaries=False,
408+ requester=requester)
409+ self.runJob(job)
410+
411+ # All the bugs apart from the one for 2.8-0 should be fixed.
412+ self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask282.status)
413+ self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask281.status)
414+ self.assertEqual(BugTaskStatus.NEW, bugtask280.status)
415+
416 def test_findMatchingDSDs_matches_all_DSDs_for_job(self):
417 # findMatchingDSDs finds matching DSDs for any of the packages
418 # in the job.
419
420=== modified file 'scripts/ftpmaster-tools/sync-source.py'
421--- scripts/ftpmaster-tools/sync-source.py 2011-06-09 10:50:25 +0000
422+++ scripts/ftpmaster-tools/sync-source.py 2011-08-31 12:04:12 +0000
423@@ -51,7 +51,12 @@
424 from lp.registry.interfaces.distribution import IDistributionSet
425 from lp.registry.interfaces.person import IPersonSet
426 from lp.registry.interfaces.pocket import PackagePublishingPocket
427-from lp.soyuz.enums import PackagePublishingStatus
428+from lp.soyuz.enums import (
429+ PackagePublishingStatus,
430+ re_bug_numbers,
431+ re_closes,
432+ re_lp_closes,
433+ )
434 from lp.soyuz.scripts.ftpmaster import (
435 generate_changes,
436 SyncSource,
437@@ -63,10 +68,6 @@
438 re_strip_revision = re.compile(r"-([^-]+)$")
439 re_changelog_header = re.compile(
440 r"^\S+ \((?P<version>.*)\) .*;.*urgency=(?P<urgency>\w+).*")
441-re_closes = re.compile(
442- r"closes:\s*(?:bug)?\#?\s?\d+(?:,\s*(?:bug)?\#?\s?\d+)*", re.I)
443-re_lp_closes = re.compile(r"lp:\s+\#\d+(?:,\s*\#\d+)*", re.I)
444-re_bug_numbers = re.compile(r"\#?\s?(\d+)")
445
446
447 Blacklisted = None