Merge lp:~jameinel/bzr/2.4-launchpad-package-freshness-609187 into lp:bzr/2.4
- 2.4-launchpad-package-freshness-609187
- Merge into 2.4
Status: | Merged |
---|---|
Approved by: | John A Meinel |
Approved revision: | no longer in the source branch. |
Merged at revision: | 6024 |
Proposed branch: | lp:~jameinel/bzr/2.4-launchpad-package-freshness-609187 |
Merge into: | lp:bzr/2.4 |
Diff against target: |
974 lines (+917/-5) 4 files modified
bzrlib/plugins/launchpad/__init__.py (+63/-5) bzrlib/plugins/launchpad/lp_api_lite.py (+286/-0) bzrlib/plugins/launchpad/test_lp_api_lite.py (+543/-0) doc/en/release-notes/bzr-2.4.txt (+25/-0) |
To merge this branch: | bzr merge lp:~jameinel/bzr/2.4-launchpad-package-freshness-609187 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andrew Bennetts | Approve | ||
Review via email: mp+69218@code.launchpad.net |
Commit message
Bug #609187, check that packaging import branches are up-to-date when accessing them.
Description of the change
This backports my bug #609187 fix now that we've settled on the output.
So it will check if "lp:ubuntu/*" and "lp:debian/*" are up-to-date, which can be disabled with the config setting, and the verbosity level adjusted.
Andrew Bennetts (spiv) wrote : | # |
Although it occurs to me that the text you add to release-notes would make a useful help topic.
(Also, at a glance the ReST markup of that release-notes entry doesn't look like it'll render the way you intend.)
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 7/27/2011 9:11 AM, Andrew Bennetts wrote:
> Although it occurs to me that the text you add to release-notes would make a useful help topic.
>
> (Also, at a glance the ReST markup of that release-notes entry doesn't look like it'll render the way you intend.)
How do you think I intend it? I was going for a Definition List:
http://
Which seems to be how it is formatted, though I was expecting the
headers to be bolded, but it is showing up as <dd> and <dt>.
Do you have a different recommendation?
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk4
5ugAnRZH/
=8cZK
-----END PGP SIGNATURE-----
John A Meinel (jameinel) wrote : | # |
sent to pqm by email
Preview Diff
1 | === modified file 'bzrlib/plugins/launchpad/__init__.py' |
2 | --- bzrlib/plugins/launchpad/__init__.py 2011-04-05 01:12:15 +0000 |
3 | +++ bzrlib/plugins/launchpad/__init__.py 2011-07-26 08:23:04 +0000 |
4 | @@ -40,19 +40,21 @@ |
5 | |
6 | # see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool |
7 | |
8 | -# Since we are a built-in plugin we share the bzrlib version |
9 | -from bzrlib import version_info |
10 | - |
11 | from bzrlib.lazy_import import lazy_import |
12 | lazy_import(globals(), """ |
13 | from bzrlib import ( |
14 | - branch as _mod_branch, |
15 | ui, |
16 | trace, |
17 | ) |
18 | """) |
19 | |
20 | -from bzrlib import bzrdir |
21 | +from bzrlib import ( |
22 | + branch as _mod_branch, |
23 | + bzrdir, |
24 | + lazy_regex, |
25 | + # Since we are a built-in plugin we share the bzrlib version |
26 | + version_info, |
27 | + ) |
28 | from bzrlib.commands import ( |
29 | Command, |
30 | register_command, |
31 | @@ -462,12 +464,68 @@ |
32 | |
33 | _register_directory() |
34 | |
35 | +# This is kept in __init__ so that we don't load lp_api_lite unless the branch |
36 | +# actually matches. That way we can avoid importing extra dependencies like |
37 | +# json. |
38 | +_package_branch = lazy_regex.lazy_compile( |
39 | + r'bazaar.launchpad.net.*?/' |
40 | + r'(?P<user>~[^/]+/)?(?P<archive>ubuntu|debian)/(?P<series>[^/]+/)?' |
41 | + r'(?P<project>[^/]+)(?P<branch>/[^/]+)?' |
42 | + ) |
43 | + |
44 | +def _get_package_branch_info(url): |
45 | + """Determine the packaging information for this URL. |
46 | + |
47 | + :return: If this isn't a packaging branch, return None. If it is, return |
48 | + (archive, series, project) |
49 | + """ |
50 | + m = _package_branch.search(url) |
51 | + if m is None: |
52 | + return |
53 | + archive, series, project, user = m.group('archive', 'series', |
54 | + 'project', 'user') |
55 | + if series is not None: |
56 | + # series is optional, so the regex includes the extra '/', we don't |
57 | + # want to send that on (it causes Internal Server Errors.) |
58 | + series = series.strip('/') |
59 | + if user is not None: |
60 | + user = user.strip('~/') |
61 | + if user != 'ubuntu-branches': |
62 | + return None |
63 | + return archive, series, project |
64 | + |
65 | + |
66 | +def _check_is_up_to_date(the_branch): |
67 | + info = _get_package_branch_info(the_branch.base) |
68 | + if info is None: |
69 | + return |
70 | + c = the_branch.get_config() |
71 | + verbosity = c.get_user_option('launchpad.packaging_verbosity') |
72 | + if verbosity is not None: |
73 | + verbosity = verbosity.lower() |
74 | + if verbosity == 'off': |
75 | + trace.mutter('not checking %s because verbosity is turned off' |
76 | + % (the_branch.base,)) |
77 | + return |
78 | + archive, series, project = info |
79 | + from bzrlib.plugins.launchpad import lp_api_lite |
80 | + latest_pub = lp_api_lite.LatestPublication(archive, series, project) |
81 | + lp_api_lite.report_freshness(the_branch, verbosity, latest_pub) |
82 | + |
83 | + |
84 | +def _register_hooks(): |
85 | + _mod_branch.Branch.hooks.install_named_hook('open', |
86 | + _check_is_up_to_date, 'package-branch-up-to-date') |
87 | + |
88 | + |
89 | +_register_hooks() |
90 | |
91 | def load_tests(basic_tests, module, loader): |
92 | testmod_names = [ |
93 | 'test_account', |
94 | 'test_register', |
95 | 'test_lp_api', |
96 | + 'test_lp_api_lite', |
97 | 'test_lp_directory', |
98 | 'test_lp_login', |
99 | 'test_lp_open', |
100 | |
101 | === added file 'bzrlib/plugins/launchpad/lp_api_lite.py' |
102 | --- bzrlib/plugins/launchpad/lp_api_lite.py 1970-01-01 00:00:00 +0000 |
103 | +++ bzrlib/plugins/launchpad/lp_api_lite.py 2011-07-26 08:23:04 +0000 |
104 | @@ -0,0 +1,286 @@ |
105 | +# Copyright (C) 2011 Canonical Ltd |
106 | +# |
107 | +# This program is free software; you can redistribute it and/or modify |
108 | +# it under the terms of the GNU General Public License as published by |
109 | +# the Free Software Foundation; either version 2 of the License, or |
110 | +# (at your option) any later version. |
111 | +# |
112 | +# This program is distributed in the hope that it will be useful, |
113 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
114 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
115 | +# GNU General Public License for more details. |
116 | +# |
117 | +# You should have received a copy of the GNU General Public License |
118 | +# along with this program; if not, write to the Free Software |
119 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
120 | + |
121 | +"""Tools for dealing with the Launchpad API without using launchpadlib. |
122 | + |
123 | +The api itself is a RESTful interface, so we can make HTTP queries directly. |
124 | +loading launchpadlib itself has a fairly high overhead (just calling |
125 | +Launchpad.login_anonymously() takes a 500ms once the WADL is cached, and 5+s to |
126 | +get the WADL. |
127 | +""" |
128 | + |
129 | +try: |
130 | + # Use simplejson if available, much faster, and can be easily installed in |
131 | + # older versions of python |
132 | + import simplejson as json |
133 | +except ImportError: |
134 | + # Is present since python 2.6 |
135 | + try: |
136 | + import json |
137 | + except ImportError: |
138 | + json = None |
139 | + |
140 | +import time |
141 | +import urllib |
142 | +import urllib2 |
143 | + |
144 | +from bzrlib import ( |
145 | + revision, |
146 | + trace, |
147 | + ) |
148 | + |
149 | + |
150 | +class LatestPublication(object): |
151 | + """Encapsulate how to find the latest publication for a given project.""" |
152 | + |
153 | + LP_API_ROOT = 'https://api.launchpad.net/1.0' |
154 | + |
155 | + def __init__(self, archive, series, project): |
156 | + self._archive = archive |
157 | + self._project = project |
158 | + self._setup_series_and_pocket(series) |
159 | + |
160 | + def _setup_series_and_pocket(self, series): |
161 | + """Parse the 'series' info into a series and a pocket. |
162 | + |
163 | + eg:: |
164 | + _setup_series_and_pocket('natty-proposed') |
165 | + => _series == 'natty' |
166 | + _pocket == 'Proposed' |
167 | + """ |
168 | + self._series = series |
169 | + self._pocket = None |
170 | + if self._series is not None and '-' in self._series: |
171 | + self._series, self._pocket = self._series.split('-', 1) |
172 | + self._pocket = self._pocket.title() |
173 | + else: |
174 | + self._pocket = 'Release' |
175 | + |
176 | + def _archive_URL(self): |
177 | + """Return the Launchpad 'Archive' URL that we will query. |
178 | + This is everything in the URL except the query parameters. |
179 | + """ |
180 | + return '%s/%s/+archive/primary' % (self.LP_API_ROOT, self._archive) |
181 | + |
182 | + def _publication_status(self): |
183 | + """Handle the 'status' field. |
184 | + It seems that Launchpad tracks all 'debian' packages as 'Pending', while |
185 | + for 'ubuntu' we care about the 'Published' packages. |
186 | + """ |
187 | + if self._archive == 'debian': |
188 | + # Launchpad only tracks debian packages as "Pending", it doesn't mark |
189 | + # them Published |
190 | + return 'Pending' |
191 | + return 'Published' |
192 | + |
193 | + def _query_params(self): |
194 | + """Get the parameters defining our query. |
195 | + This defines the actions we are making against the archive. |
196 | + :return: A dict of query parameters. |
197 | + """ |
198 | + params = {'ws.op': 'getPublishedSources', |
199 | + 'exact_match': 'true', |
200 | + # If we need to use "" shouldn't we quote the project somehow? |
201 | + 'source_name': '"%s"' % (self._project,), |
202 | + 'status': self._publication_status(), |
203 | + # We only need the latest one, the results seem to be properly |
204 | + # most-recent-debian-version sorted |
205 | + 'ws.size': '1', |
206 | + } |
207 | + if self._series is not None: |
208 | + params['distro_series'] = '/%s/%s' % (self._archive, self._series) |
209 | + if self._pocket is not None: |
210 | + params['pocket'] = self._pocket |
211 | + return params |
212 | + |
213 | + def _query_URL(self): |
214 | + """Create the full URL that we need to query, including parameters.""" |
215 | + params = self._query_params() |
216 | + # We sort to give deterministic results for testing |
217 | + encoded = urllib.urlencode(sorted(params.items())) |
218 | + return '%s?%s' % (self._archive_URL(), encoded) |
219 | + |
220 | + def _get_lp_info(self): |
221 | + """Place an actual HTTP query against the Launchpad service.""" |
222 | + if json is None: |
223 | + return None |
224 | + query_URL = self._query_URL() |
225 | + try: |
226 | + req = urllib2.Request(query_URL) |
227 | + response = urllib2.urlopen(req) |
228 | + json_info = response.read() |
229 | + # TODO: We haven't tested the HTTPError |
230 | + except (urllib2.URLError, urllib2.HTTPError), e: |
231 | + trace.mutter('failed to place query to %r' % (query_URL,)) |
232 | + trace.log_exception_quietly() |
233 | + return None |
234 | + return json_info |
235 | + |
236 | + def _parse_json_info(self, json_info): |
237 | + """Parse the json response from Launchpad into objects.""" |
238 | + if json is None: |
239 | + return None |
240 | + try: |
241 | + return json.loads(json_info) |
242 | + except Exception: |
243 | + trace.mutter('Failed to parse json info: %r' % (json_info,)) |
244 | + trace.log_exception_quietly() |
245 | + return None |
246 | + |
247 | + def get_latest_version(self): |
248 | + """Get the latest published version for the given package.""" |
249 | + json_info = self._get_lp_info() |
250 | + if json_info is None: |
251 | + return None |
252 | + info = self._parse_json_info(json_info) |
253 | + if info is None: |
254 | + return None |
255 | + try: |
256 | + entries = info['entries'] |
257 | + if len(entries) == 0: |
258 | + return None |
259 | + return entries[0]['source_package_version'] |
260 | + except KeyError: |
261 | + trace.log_exception_quietly() |
262 | + return None |
263 | + |
264 | + def place(self): |
265 | + """Text-form for what location this represents. |
266 | + |
267 | + Example:: |
268 | + ubuntu, natty => Ubuntu Natty |
269 | + ubuntu, natty-proposed => Ubuntu Natty Proposed |
270 | + :return: A string representing the location we are checking. |
271 | + """ |
272 | + place = self._archive |
273 | + if self._series is not None: |
274 | + place = '%s %s' % (place, self._series) |
275 | + if self._pocket is not None and self._pocket != 'Release': |
276 | + place = '%s %s' % (place, self._pocket) |
277 | + return place.title() |
278 | + |
279 | + |
280 | +def get_latest_publication(archive, series, project): |
281 | + """Get the most recent publication for a given project. |
282 | + |
283 | + :param archive: Either 'ubuntu' or 'debian' |
284 | + :param series: Something like 'natty', 'sid', etc. Can be set as None. Can |
285 | + also include a pocket such as 'natty-proposed'. |
286 | + :param project: Something like 'bzr' |
287 | + :return: A version string indicating the most-recent version published in |
288 | + Launchpad. Might return None if there is an error. |
289 | + """ |
290 | + lp = LatestPublication(archive, series, project) |
291 | + return lp.get_latest_version() |
292 | + |
293 | + |
294 | +def get_most_recent_tag(tag_dict, the_branch): |
295 | + """Get the most recent revision that has been tagged.""" |
296 | + # Note: this assumes that a given rev won't get tagged multiple times. But |
297 | + # it should be valid for the package importer branches that we care |
298 | + # about |
299 | + reverse_dict = dict((rev, tag) for tag, rev in tag_dict.iteritems()) |
300 | + the_branch.lock_read() |
301 | + try: |
302 | + last_rev = the_branch.last_revision() |
303 | + graph = the_branch.repository.get_graph() |
304 | + stop_revisions = (None, revision.NULL_REVISION) |
305 | + for rev_id in graph.iter_lefthand_ancestry(last_rev, stop_revisions): |
306 | + if rev_id in reverse_dict: |
307 | + return reverse_dict[rev_id] |
308 | + finally: |
309 | + the_branch.unlock() |
310 | + |
311 | + |
312 | +def _get_newest_versions(the_branch, latest_pub): |
313 | + """Get information about how 'fresh' this packaging branch is. |
314 | + |
315 | + :param the_branch: The Branch to check |
316 | + :param latest_pub: The LatestPublication used to check most recent |
317 | + published version. |
318 | + :return: (latest_ver, branch_latest_ver) |
319 | + """ |
320 | + t = time.time() |
321 | + latest_ver = latest_pub.get_latest_version() |
322 | + t_latest_ver = time.time() - t |
323 | + trace.mutter('LatestPublication.get_latest_version took: %.3fs' |
324 | + % (t_latest_ver,)) |
325 | + if latest_ver is None: |
326 | + return None, None |
327 | + t = time.time() |
328 | + tags = the_branch.tags.get_tag_dict() |
329 | + t_tag_dict = time.time() - t |
330 | + trace.mutter('LatestPublication.get_tag_dict took: %.3fs' % (t_tag_dict,)) |
331 | + if latest_ver in tags: |
332 | + # branch might have a newer tag, but we don't really care |
333 | + return latest_ver, latest_ver |
334 | + else: |
335 | + best_tag = get_most_recent_tag(tags, the_branch) |
336 | + return latest_ver, best_tag |
337 | + |
338 | + |
339 | +def _report_freshness(latest_ver, branch_latest_ver, place, verbosity, |
340 | + report_func): |
341 | + """Report if the branch is up-to-date.""" |
342 | + if latest_ver is None: |
343 | + if verbosity == 'all': |
344 | + report_func('Most recent %s version: MISSING' % (place,)) |
345 | + elif verbosity == 'short': |
346 | + report_func('%s is MISSING a version' % (place,)) |
347 | + return |
348 | + elif latest_ver == branch_latest_ver: |
349 | + if verbosity == 'minimal': |
350 | + return |
351 | + elif verbosity == 'short': |
352 | + report_func('%s is CURRENT in %s' % (latest_ver, place)) |
353 | + else: |
354 | + report_func('Most recent %s version: %s\n' |
355 | + 'Packaging branch status: CURRENT' |
356 | + % (place, latest_ver)) |
357 | + else: |
358 | + if verbosity in ('minimal', 'short'): |
359 | + if branch_latest_ver is None: |
360 | + branch_latest_ver = 'Branch' |
361 | + report_func('%s is OUT-OF-DATE, %s has %s' |
362 | + % (branch_latest_ver, place, latest_ver)) |
363 | + else: |
364 | + report_func('Most recent %s version: %s\n' |
365 | + 'Packaging branch version: %s\n' |
366 | + 'Packaging branch status: OUT-OF-DATE' |
367 | + % (place, latest_ver, branch_latest_ver)) |
368 | + |
369 | + |
370 | +def report_freshness(the_branch, verbosity, latest_pub): |
371 | + """Report to the user how up-to-date the packaging branch is. |
372 | + |
373 | + :param the_branch: A Branch object |
374 | + :param verbosity: Can be one of: |
375 | + off: Do not print anything, and skip all checks. |
376 | + all: Print all information that we have in a verbose manner, this |
377 | + includes misses, etc. |
378 | + short: Print information, but only one-line summaries |
379 | + minimal: Only print a one-line summary when the package branch is |
380 | + out-of-date |
381 | + :param latest_pub: A LatestPublication instance |
382 | + """ |
383 | + if verbosity == 'off': |
384 | + return |
385 | + if verbosity is None: |
386 | + verbosity = 'all' |
387 | + latest_ver, branch_ver = _get_newest_versions(the_branch, latest_pub) |
388 | + place = latest_pub.place() |
389 | + _report_freshness(latest_ver, branch_ver, place, verbosity, |
390 | + trace.note) |
391 | |
392 | === added file 'bzrlib/plugins/launchpad/test_lp_api_lite.py' |
393 | --- bzrlib/plugins/launchpad/test_lp_api_lite.py 1970-01-01 00:00:00 +0000 |
394 | +++ bzrlib/plugins/launchpad/test_lp_api_lite.py 2011-07-26 08:23:04 +0000 |
395 | @@ -0,0 +1,543 @@ |
396 | +# Copyright (C) 2011 Canonical Ltd |
397 | +# |
398 | +# This program is free software; you can redistribute it and/or modify |
399 | +# it under the terms of the GNU General Public License as published by |
400 | +# the Free Software Foundation; either version 2 of the License, or |
401 | +# (at your option) any later version. |
402 | +# |
403 | +# This program is distributed in the hope that it will be useful, |
404 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
405 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
406 | +# GNU General Public License for more details. |
407 | +# |
408 | +# You should have received a copy of the GNU General Public License |
409 | +# along with this program; if not, write to the Free Software |
410 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
411 | + |
412 | +"""Tools for dealing with the Launchpad API without using launchpadlib. |
413 | +""" |
414 | + |
415 | +import doctest |
416 | +import socket |
417 | + |
418 | +from bzrlib import tests |
419 | +from bzrlib.plugins import launchpad |
420 | +from bzrlib.plugins.launchpad import lp_api_lite |
421 | + |
422 | +from testtools.matchers import DocTestMatches |
423 | + |
424 | + |
425 | +class _JSONParserFeature(tests.Feature): |
426 | + |
427 | + def _probe(self): |
428 | + return lp_api_lite.json is not None |
429 | + |
430 | + def feature_name(self): |
431 | + return 'simplejson or json' |
432 | + |
433 | +JSONParserFeature = _JSONParserFeature() |
434 | + |
435 | +_example_response = r""" |
436 | +{ |
437 | + "total_size": 2, |
438 | + "start": 0, |
439 | + "next_collection_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary?distro_series=%2Fubuntu%2Flucid&exact_match=true&source_name=%22bzr%22&status=Published&ws.op=getPublishedSources&ws.start=1&ws.size=1", |
440 | + "entries": [ |
441 | + { |
442 | + "package_creator_link": "https://api.launchpad.net/1.0/~maxb", |
443 | + "package_signer_link": "https://api.launchpad.net/1.0/~jelmer", |
444 | + "source_package_name": "bzr", |
445 | + "removal_comment": null, |
446 | + "display_name": "bzr 2.1.4-0ubuntu1 in lucid", |
447 | + "date_made_pending": null, |
448 | + "source_package_version": "2.1.4-0ubuntu1", |
449 | + "date_superseded": null, |
450 | + "http_etag": "\"9ba966152dec474dc0fe1629d0bbce2452efaf3b-5f4c3fbb3eaf26d502db4089777a9b6a0537ffab\"", |
451 | + "self_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/1750327", |
452 | + "distro_series_link": "https://api.launchpad.net/1.0/ubuntu/lucid", |
453 | + "component_name": "main", |
454 | + "status": "Published", |
455 | + "date_removed": null, |
456 | + "pocket": "Updates", |
457 | + "date_published": "2011-05-30T06:09:58.653984+00:00", |
458 | + "removed_by_link": null, |
459 | + "section_name": "devel", |
460 | + "resource_type_link": "https://api.launchpad.net/1.0/#source_package_publishing_history", |
461 | + "archive_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary", |
462 | + "package_maintainer_link": "https://api.launchpad.net/1.0/~ubuntu-devel-discuss-lists", |
463 | + "date_created": "2011-05-30T05:19:12.233621+00:00", |
464 | + "scheduled_deletion_date": null |
465 | + } |
466 | + ] |
467 | +}""" |
468 | + |
469 | +_no_versions_response = '{"total_size": 0, "start": 0, "entries": []}' |
470 | + |
471 | + |
472 | +class TestLatestPublication(tests.TestCase): |
473 | + |
474 | + def make_latest_publication(self, archive='ubuntu', series='natty', |
475 | + project='bzr'): |
476 | + return lp_api_lite.LatestPublication(archive, series, project) |
477 | + |
478 | + def assertPlace(self, place, archive, series, project): |
479 | + lp = lp_api_lite.LatestPublication(archive, series, project) |
480 | + self.assertEqual(place, lp.place()) |
481 | + |
482 | + def test_init(self): |
483 | + latest_pub = self.make_latest_publication() |
484 | + self.assertEqual('ubuntu', latest_pub._archive) |
485 | + self.assertEqual('natty', latest_pub._series) |
486 | + self.assertEqual('bzr', latest_pub._project) |
487 | + self.assertEqual('Release', latest_pub._pocket) |
488 | + |
489 | + def test__archive_URL(self): |
490 | + latest_pub = self.make_latest_publication() |
491 | + self.assertEqual( |
492 | + 'https://api.launchpad.net/1.0/ubuntu/+archive/primary', |
493 | + latest_pub._archive_URL()) |
494 | + |
495 | + def test__publication_status_for_ubuntu(self): |
496 | + latest_pub = self.make_latest_publication() |
497 | + self.assertEqual('Published', latest_pub._publication_status()) |
498 | + |
499 | + def test__publication_status_for_debian(self): |
500 | + latest_pub = self.make_latest_publication(archive='debian') |
501 | + self.assertEqual('Pending', latest_pub._publication_status()) |
502 | + |
503 | + def test_pocket(self): |
504 | + latest_pub = self.make_latest_publication(series='natty-proposed') |
505 | + self.assertEqual('natty', latest_pub._series) |
506 | + self.assertEqual('Proposed', latest_pub._pocket) |
507 | + |
508 | + def test_series_None(self): |
509 | + latest_pub = self.make_latest_publication(series=None) |
510 | + self.assertEqual('ubuntu', latest_pub._archive) |
511 | + self.assertEqual(None, latest_pub._series) |
512 | + self.assertEqual('bzr', latest_pub._project) |
513 | + self.assertEqual('Release', latest_pub._pocket) |
514 | + |
515 | + def test__query_params(self): |
516 | + latest_pub = self.make_latest_publication() |
517 | + self.assertEqual({'ws.op': 'getPublishedSources', |
518 | + 'exact_match': 'true', |
519 | + 'source_name': '"bzr"', |
520 | + 'status': 'Published', |
521 | + 'ws.size': '1', |
522 | + 'distro_series': '/ubuntu/natty', |
523 | + 'pocket': 'Release', |
524 | + }, latest_pub._query_params()) |
525 | + |
526 | + def test__query_params_no_series(self): |
527 | + latest_pub = self.make_latest_publication(series=None) |
528 | + self.assertEqual({'ws.op': 'getPublishedSources', |
529 | + 'exact_match': 'true', |
530 | + 'source_name': '"bzr"', |
531 | + 'status': 'Published', |
532 | + 'ws.size': '1', |
533 | + 'pocket': 'Release', |
534 | + }, latest_pub._query_params()) |
535 | + |
536 | + def test__query_params_pocket(self): |
537 | + latest_pub = self.make_latest_publication(series='natty-proposed') |
538 | + self.assertEqual({'ws.op': 'getPublishedSources', |
539 | + 'exact_match': 'true', |
540 | + 'source_name': '"bzr"', |
541 | + 'status': 'Published', |
542 | + 'ws.size': '1', |
543 | + 'distro_series': '/ubuntu/natty', |
544 | + 'pocket': 'Proposed', |
545 | + }, latest_pub._query_params()) |
546 | + |
547 | + def test__query_URL(self): |
548 | + latest_pub = self.make_latest_publication() |
549 | + # we explicitly sort params, so we can be sure this URL matches exactly |
550 | + self.assertEqual( |
551 | + 'https://api.launchpad.net/1.0/ubuntu/+archive/primary' |
552 | + '?distro_series=%2Fubuntu%2Fnatty&exact_match=true' |
553 | + '&pocket=Release&source_name=%22bzr%22&status=Published' |
554 | + '&ws.op=getPublishedSources&ws.size=1', |
555 | + latest_pub._query_URL()) |
556 | + |
557 | + def DONT_test__gracefully_handle_failed_rpc_connection(self): |
558 | + # TODO: This test kind of sucks. We intentionally create an arbitrary |
559 | + # port and don't listen to it, because we want the request to fail. |
560 | + # However, it seems to take 1s for it to timeout. Is there a way |
561 | + # to make it fail faster? |
562 | + latest_pub = self.make_latest_publication() |
563 | + s = socket.socket() |
564 | + s.bind(('127.0.0.1', 0)) |
565 | + addr, port = s.getsockname() |
566 | + latest_pub.LP_API_ROOT = 'http://%s:%s/' % (addr, port) |
567 | + s.close() |
568 | + self.assertIs(None, latest_pub._get_lp_info()) |
569 | + |
570 | + def DONT_test__query_launchpad(self): |
571 | + # TODO: This is a test that we are making a valid request against |
572 | + # launchpad. This seems important, but it is slow, requires net |
573 | + # access, and requires launchpad to be up and running. So for |
574 | + # now, it is commented out for production tests. |
575 | + latest_pub = self.make_latest_publication() |
576 | + json_txt = latest_pub._get_lp_info() |
577 | + self.assertIsNot(None, json_txt) |
578 | + if lp_api_lite.json is None: |
579 | + # We don't have a way to parse the text |
580 | + return |
581 | + # The content should be a valid json result |
582 | + content = lp_api_lite.json.loads(json_txt) |
583 | + entries = content['entries'] # It should have an 'entries' field. |
584 | + # ws.size should mean we get 0 or 1, and there should be something |
585 | + self.assertEqual(1, len(entries)) |
586 | + entry = entries[0] |
587 | + self.assertEqual('bzr', entry['source_package_name']) |
588 | + version = entry['source_package_version'] |
589 | + self.assertIsNot(None, version) |
590 | + |
591 | + def test__get_lp_info_no_json(self): |
592 | + # If we can't parse the json, we don't make the query. |
593 | + self.overrideAttr(lp_api_lite, 'json', None) |
594 | + latest_pub = self.make_latest_publication() |
595 | + self.assertIs(None, latest_pub._get_lp_info()) |
596 | + |
597 | + def test__parse_json_info_no_module(self): |
598 | + # If a json parsing module isn't available, we just return None here. |
599 | + self.overrideAttr(lp_api_lite, 'json', None) |
600 | + latest_pub = self.make_latest_publication() |
601 | + self.assertIs(None, latest_pub._parse_json_info(_example_response)) |
602 | + |
603 | + def test__parse_json_example_response(self): |
604 | + self.requireFeature(JSONParserFeature) |
605 | + latest_pub = self.make_latest_publication() |
606 | + content = latest_pub._parse_json_info(_example_response) |
607 | + self.assertIsNot(None, content) |
608 | + self.assertEqual(2, content['total_size']) |
609 | + entries = content['entries'] |
610 | + self.assertEqual(1, len(entries)) |
611 | + entry = entries[0] |
612 | + self.assertEqual('bzr', entry['source_package_name']) |
613 | + self.assertEqual("2.1.4-0ubuntu1", entry["source_package_version"]) |
614 | + |
615 | + def test__parse_json_not_json(self): |
616 | + self.requireFeature(JSONParserFeature) |
617 | + latest_pub = self.make_latest_publication() |
618 | + self.assertIs(None, latest_pub._parse_json_info('Not_valid_json')) |
619 | + |
620 | + def test_get_latest_version_no_response(self): |
621 | + latest_pub = self.make_latest_publication() |
622 | + latest_pub._get_lp_info = lambda: None |
623 | + self.assertEqual(None, latest_pub.get_latest_version()) |
624 | + |
625 | + def test_get_latest_version_no_json(self): |
626 | + self.overrideAttr(lp_api_lite, 'json', None) |
627 | + latest_pub = self.make_latest_publication() |
628 | + self.assertEqual(None, latest_pub.get_latest_version()) |
629 | + |
630 | + def test_get_latest_version_invalid_json(self): |
631 | + self.requireFeature(JSONParserFeature) |
632 | + latest_pub = self.make_latest_publication() |
633 | + latest_pub._get_lp_info = lambda: "not json" |
634 | + self.assertEqual(None, latest_pub.get_latest_version()) |
635 | + |
636 | + def test_get_latest_version_no_versions(self): |
637 | + self.requireFeature(JSONParserFeature) |
638 | + latest_pub = self.make_latest_publication() |
639 | + latest_pub._get_lp_info = lambda: _no_versions_response |
640 | + self.assertEqual(None, latest_pub.get_latest_version()) |
641 | + |
642 | + def test_get_latest_version_missing_entries(self): |
643 | + # Launchpad's no-entries response does have an empty entries value. |
644 | + # However, lets test that we handle other failures without tracebacks |
645 | + self.requireFeature(JSONParserFeature) |
646 | + latest_pub = self.make_latest_publication() |
647 | + latest_pub._get_lp_info = lambda: '{}' |
648 | + self.assertEqual(None, latest_pub.get_latest_version()) |
649 | + |
650 | + def test_get_latest_version_invalid_entries(self): |
651 | + # Make sure we sanely handle a json response we don't understand |
652 | + self.requireFeature(JSONParserFeature) |
653 | + latest_pub = self.make_latest_publication() |
654 | + latest_pub._get_lp_info = lambda: '{"entries": {"a": 1}}' |
655 | + self.assertEqual(None, latest_pub.get_latest_version()) |
656 | + |
657 | + def test_get_latest_version_example(self): |
658 | + self.requireFeature(JSONParserFeature) |
659 | + latest_pub = self.make_latest_publication() |
660 | + latest_pub._get_lp_info = lambda: _example_response |
661 | + self.assertEqual("2.1.4-0ubuntu1", latest_pub.get_latest_version()) |
662 | + |
663 | + def DONT_test_get_latest_version_from_launchpad(self): |
664 | + self.requireFeature(JSONParserFeature) |
665 | + latest_pub = self.make_latest_publication() |
666 | + self.assertIsNot(None, latest_pub.get_latest_version()) |
667 | + |
668 | + def test_place(self): |
669 | + self.assertPlace('Ubuntu', 'ubuntu', None, 'bzr') |
670 | + self.assertPlace('Ubuntu Natty', 'ubuntu', 'natty', 'bzr') |
671 | + self.assertPlace('Ubuntu Natty Proposed', 'ubuntu', 'natty-proposed', |
672 | + 'bzr') |
673 | + self.assertPlace('Debian', 'debian', None, 'bzr') |
674 | + self.assertPlace('Debian Sid', 'debian', 'sid', 'bzr') |
675 | + |
676 | + |
677 | +class TestIsUpToDate(tests.TestCase): |
678 | + |
679 | + def assertPackageBranchRe(self, url, user, archive, series, project): |
680 | + m = launchpad._package_branch.search(url) |
681 | + if m is None: |
682 | + self.fail('package_branch regex did not match url: %s' % (url,)) |
683 | + self.assertEqual( |
684 | + (user, archive, series, project), |
685 | + m.group('user', 'archive', 'series', 'project')) |
686 | + |
687 | + def assertNotPackageBranch(self, url): |
688 | + self.assertIs(None, launchpad._get_package_branch_info(url)) |
689 | + |
690 | + def assertBranchInfo(self, url, archive, series, project): |
691 | + self.assertEqual((archive, series, project), |
692 | + launchpad._get_package_branch_info(url)) |
693 | + |
694 | + def test_package_branch_regex(self): |
695 | + self.assertPackageBranchRe( |
696 | + 'http://bazaar.launchpad.net/+branch/ubuntu/foo', |
697 | + None, 'ubuntu', None, 'foo') |
698 | + self.assertPackageBranchRe( |
699 | + 'bzr+ssh://bazaar.launchpad.net/+branch/ubuntu/natty/foo', |
700 | + None, 'ubuntu', 'natty/', 'foo') |
701 | + self.assertPackageBranchRe( |
702 | + 'sftp://bazaar.launchpad.net/+branch/debian/foo', |
703 | + None, 'debian', None, 'foo') |
704 | + self.assertPackageBranchRe( |
705 | + 'http://bazaar.launchpad.net/+branch/debian/sid/foo', |
706 | + None, 'debian', 'sid/', 'foo') |
707 | + self.assertPackageBranchRe( |
708 | + 'http://bazaar.launchpad.net/+branch' |
709 | + '/~ubuntu-branches/ubuntu/natty/foo/natty', |
710 | + '~ubuntu-branches/', 'ubuntu', 'natty/', 'foo') |
711 | + self.assertPackageBranchRe( |
712 | + 'http://bazaar.launchpad.net/+branch' |
713 | + '/~user/ubuntu/natty/foo/test', |
714 | + '~user/', 'ubuntu', 'natty/', 'foo') |
715 | + |
716 | + def test_package_branch_doesnt_match(self): |
717 | + self.assertNotPackageBranch('http://example.com/ubuntu/foo') |
718 | + self.assertNotPackageBranch( |
719 | + 'http://bazaar.launchpad.net/+branch/bzr') |
720 | + self.assertNotPackageBranch( |
721 | + 'http://bazaar.launchpad.net/+branch/~bzr-pqm/bzr/bzr.dev') |
722 | + # Not a packaging branch because ~user isn't ~ubuntu-branches |
723 | + self.assertNotPackageBranch( |
724 | + 'http://bazaar.launchpad.net/+branch' |
725 | + '/~user/ubuntu/natty/foo/natty') |
726 | + |
727 | + def test__get_package_branch_info(self): |
728 | + self.assertBranchInfo( |
729 | + 'bzr+ssh://bazaar.launchpad.net/+branch/ubuntu/natty/foo', |
730 | + 'ubuntu', 'natty', 'foo') |
731 | + self.assertBranchInfo( |
732 | + 'bzr+ssh://bazaar.launchpad.net/+branch' |
733 | + '/~ubuntu-branches/ubuntu/natty/foo/natty', |
734 | + 'ubuntu', 'natty', 'foo') |
735 | + self.assertBranchInfo( |
736 | + 'http://bazaar.launchpad.net/+branch' |
737 | + '/~ubuntu-branches/debian/sid/foo/sid', |
738 | + 'debian', 'sid', 'foo') |
739 | + |
740 | + |
741 | +class TestGetMostRecentTag(tests.TestCaseWithMemoryTransport): |
742 | + |
743 | + def make_simple_builder(self): |
744 | + builder = self.make_branch_builder('tip') |
745 | + builder.build_snapshot('A', [], [ |
746 | + ('add', ('', 'root-id', 'directory', None))]) |
747 | + b = builder.get_branch() |
748 | + b.tags.set_tag('tip-1.0', 'A') |
749 | + return builder, b, b.tags.get_tag_dict() |
750 | + |
751 | + def test_get_most_recent_tag_tip(self): |
752 | + builder, b, tag_dict = self.make_simple_builder() |
753 | + self.assertEqual('tip-1.0', |
754 | + lp_api_lite.get_most_recent_tag(tag_dict, b)) |
755 | + |
756 | + def test_get_most_recent_tag_older(self): |
757 | + builder, b, tag_dict = self.make_simple_builder() |
758 | + builder.build_snapshot('B', ['A'], []) |
759 | + self.assertEqual('B', b.last_revision()) |
760 | + self.assertEqual('tip-1.0', |
761 | + lp_api_lite.get_most_recent_tag(tag_dict, b)) |
762 | + |
763 | + |
764 | +class StubLatestPublication(object): |
765 | + |
766 | + def __init__(self, latest): |
767 | + self.called = False |
768 | + self.latest = latest |
769 | + |
770 | + def get_latest_version(self): |
771 | + self.called = True |
772 | + return self.latest |
773 | + |
774 | + def place(self): |
775 | + return 'Ubuntu Natty' |
776 | + |
777 | + |
778 | +class TestReportFreshness(tests.TestCaseWithMemoryTransport): |
779 | + |
780 | + def setUp(self): |
781 | + super(TestReportFreshness, self).setUp() |
782 | + builder = self.make_branch_builder('tip') |
783 | + builder.build_snapshot('A', [], [ |
784 | + ('add', ('', 'root-id', 'directory', None))]) |
785 | + self.branch = builder.get_branch() |
786 | + |
787 | + def assertFreshnessReports(self, verbosity, latest_version, content): |
788 | + """Assert that lp_api_lite.report_freshness reports the given content. |
789 | + |
790 | + :param verbosity: The reporting level |
791 | + :param latest_version: The version reported by StubLatestPublication |
792 | + :param content: The expected content. This should be in DocTest form. |
793 | + """ |
794 | + orig_log_len = len(self.get_log()) |
795 | + lp_api_lite.report_freshness(self.branch, verbosity, |
796 | + StubLatestPublication(latest_version)) |
797 | + new_content = self.get_log()[orig_log_len:] |
798 | + # Strip out lines that have LatestPublication.get_* because those are |
799 | + # timing related lines. While interesting to log for now, they aren't |
800 | + # something we want to be testing |
801 | + new_content = new_content.split('\n') |
802 | + for i in range(2): |
803 | + if (len(new_content) > 0 |
804 | + and 'LatestPublication.get_' in new_content[0]): |
805 | + new_content = new_content[1:] |
806 | + new_content = '\n'.join(new_content) |
807 | + self.assertThat(new_content, |
808 | + DocTestMatches(content, |
809 | + doctest.ELLIPSIS | doctest.REPORT_UDIFF)) |
810 | + |
811 | + def test_verbosity_off_skips_check(self): |
812 | + # We force _get_package_branch_info so that we know it would otherwise |
813 | + # try to connect to launcphad |
814 | + self.overrideAttr(launchpad, '_get_package_branch_info', |
815 | + lambda x: ('ubuntu', 'natty', 'bzr')) |
816 | + self.overrideAttr(lp_api_lite, 'LatestPublication', |
817 | + lambda *args: self.fail('Tried to query launchpad')) |
818 | + c = self.branch.get_config() |
819 | + c.set_user_option('launchpad.packaging_verbosity', 'off') |
820 | + orig_log_len = len(self.get_log()) |
821 | + launchpad._check_is_up_to_date(self.branch) |
822 | + new_content = self.get_log()[orig_log_len:] |
823 | + self.assertContainsRe(new_content, |
824 | + 'not checking memory.*/tip/ because verbosity is turned off') |
825 | + |
826 | + def test_verbosity_off(self): |
827 | + latest_pub = StubLatestPublication('1.0-1ubuntu2') |
828 | + lp_api_lite.report_freshness(self.branch, 'off', latest_pub) |
829 | + self.assertFalse(latest_pub.called) |
830 | + |
831 | + def test_verbosity_all_out_of_date_smoke(self): |
832 | + self.branch.tags.set_tag('1.0-1ubuntu1', 'A') |
833 | + self.assertFreshnessReports('all', '1.0-1ubuntu2', |
834 | + ' INFO Most recent Ubuntu Natty version: 1.0-1ubuntu2\n' |
835 | + 'Packaging branch version: 1.0-1ubuntu1\n' |
836 | + 'Packaging branch status: OUT-OF-DATE\n') |
837 | + |
838 | + |
839 | +class Test_GetNewestVersions(tests.TestCaseWithMemoryTransport): |
840 | + |
841 | + def setUp(self): |
842 | + super(Test_GetNewestVersions, self).setUp() |
843 | + builder = self.make_branch_builder('tip') |
844 | + builder.build_snapshot('A', [], [ |
845 | + ('add', ('', 'root-id', 'directory', None))]) |
846 | + self.branch = builder.get_branch() |
847 | + |
848 | + def assertLatestVersions(self, latest_branch_version, pub_version): |
849 | + if latest_branch_version is not None: |
850 | + self.branch.tags.set_tag(latest_branch_version, 'A') |
851 | + latest_pub = StubLatestPublication(pub_version) |
852 | + self.assertEqual((pub_version, latest_branch_version), |
853 | + lp_api_lite._get_newest_versions(self.branch, latest_pub)) |
854 | + |
855 | + def test_no_tags(self): |
856 | + self.assertLatestVersions(None, '1.0-1ubuntu2') |
857 | + |
858 | + def test_out_of_date(self): |
859 | + self.assertLatestVersions('1.0-1ubuntu1', '1.0-1ubuntu2') |
860 | + |
861 | + def test_up_to_date(self): |
862 | + self.assertLatestVersions('1.0-1ubuntu2', '1.0-1ubuntu2') |
863 | + |
864 | + def test_missing(self): |
865 | + self.assertLatestVersions(None, None) |
866 | + |
867 | + |
868 | +class Test_ReportFreshness(tests.TestCase): |
869 | + |
870 | + def assertReportedFreshness(self, verbosity, latest_ver, branch_latest_ver, |
871 | + content, place='Ubuntu Natty'): |
872 | + """Assert that lp_api_lite.report_freshness reports the given content. |
873 | + """ |
874 | + reported = [] |
875 | + def report_func(value): |
876 | + reported.append(value) |
877 | + lp_api_lite._report_freshness(latest_ver, branch_latest_ver, place, |
878 | + verbosity, report_func) |
879 | + new_content = '\n'.join(reported) |
880 | + self.assertThat(new_content, |
881 | + DocTestMatches(content, |
882 | + doctest.ELLIPSIS | doctest.REPORT_UDIFF)) |
883 | + |
884 | + def test_verbosity_minimal_no_tags(self): |
885 | + self.assertReportedFreshness('minimal', '1.0-1ubuntu2', None, |
886 | + 'Branch is OUT-OF-DATE, Ubuntu Natty has 1.0-1ubuntu2\n') |
887 | + |
888 | + def test_verbosity_minimal_out_of_date(self): |
889 | + self.assertReportedFreshness('minimal', '1.0-1ubuntu2', '1.0-1ubuntu1', |
890 | + '1.0-1ubuntu1 is OUT-OF-DATE,' |
891 | + ' Ubuntu Natty has 1.0-1ubuntu2\n') |
892 | + |
893 | + def test_verbosity_minimal_up_to_date(self): |
894 | + self.assertReportedFreshness('minimal', '1.0-1ubuntu2', '1.0-1ubuntu2', |
895 | + '') |
896 | + |
897 | + def test_verbosity_minimal_missing(self): |
898 | + self.assertReportedFreshness('minimal', None, None, |
899 | + '') |
900 | + |
901 | + def test_verbosity_short_out_of_date(self): |
902 | + self.assertReportedFreshness('short', '1.0-1ubuntu2', '1.0-1ubuntu1', |
903 | + '1.0-1ubuntu1 is OUT-OF-DATE,' |
904 | + ' Ubuntu Natty has 1.0-1ubuntu2\n') |
905 | + |
906 | + def test_verbosity_short_up_to_date(self): |
907 | + self.assertReportedFreshness('short', '1.0-1ubuntu2', '1.0-1ubuntu2', |
908 | + '1.0-1ubuntu2 is CURRENT in Ubuntu Natty') |
909 | + |
910 | + def test_verbosity_short_missing(self): |
911 | + self.assertReportedFreshness('short', None, None, |
912 | + 'Ubuntu Natty is MISSING a version') |
913 | + |
914 | + def test_verbosity_all_no_tags(self): |
915 | + self.assertReportedFreshness('all', '1.0-1ubuntu2', None, |
916 | + 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n' |
917 | + 'Packaging branch version: None\n' |
918 | + 'Packaging branch status: OUT-OF-DATE\n') |
919 | + |
920 | + def test_verbosity_all_out_of_date(self): |
921 | + self.assertReportedFreshness('all', '1.0-1ubuntu2', '1.0-1ubuntu1', |
922 | + 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n' |
923 | + 'Packaging branch version: 1.0-1ubuntu1\n' |
924 | + 'Packaging branch status: OUT-OF-DATE\n') |
925 | + |
926 | + def test_verbosity_all_up_to_date(self): |
927 | + self.assertReportedFreshness('all', '1.0-1ubuntu2', '1.0-1ubuntu2', |
928 | + 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n' |
929 | + 'Packaging branch status: CURRENT\n') |
930 | + |
931 | + def test_verbosity_all_missing(self): |
932 | + self.assertReportedFreshness('all', None, None, |
933 | + 'Most recent Ubuntu Natty version: MISSING\n') |
934 | + |
935 | + def test_verbosity_None_is_all(self): |
936 | + self.assertReportedFreshness(None, '1.0-1ubuntu2', '1.0-1ubuntu2', |
937 | + 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n' |
938 | + 'Packaging branch status: CURRENT\n') |
939 | |
940 | === modified file 'doc/en/release-notes/bzr-2.4.txt' |
941 | --- doc/en/release-notes/bzr-2.4.txt 2011-07-18 14:43:35 +0000 |
942 | +++ doc/en/release-notes/bzr-2.4.txt 2011-07-26 08:23:04 +0000 |
943 | @@ -32,6 +32,31 @@ |
944 | .. Fixes for situations where bzr would previously crash or give incorrect |
945 | or undesirable results. |
946 | |
947 | +* Accessing a packaging branch on Launchpad (eg, ``lp:ubuntu/bzr``) now |
948 | + checks to see if the most recent published source package version for |
949 | + that project is present in the branch tags. This should help developers |
950 | + trust whether the packaging branch is up-to-date and can be used for new |
951 | + changes. The level of verbosity is controlled by the config item |
952 | + ``launchpad.packaging_verbosity``. It can be set to one of |
953 | + |
954 | + off |
955 | + disable all checks |
956 | + |
957 | + |
958 | + minimal |
959 | + only display if the branch is out-of-date |
960 | + |
961 | + short |
962 | + also display single-line up-to-date and missing, |
963 | + |
964 | + |
965 | + all |
966 | + (default) display multi-line content for all states |
967 | + |
968 | + |
969 | + (John Arbash Meinel, #609187, #812928) |
970 | + |
971 | + |
972 | * The fix for bug #513709 caused us to open a new connection when |
973 | switching a lightweight checkout that was pointing at a bound branch. |
974 | This isn't necessary because we know the master URL without opening it, |
I haven't read this patch closely, but this is just a direct backport of the patch that landed on lp:bzr, right? In that case, I say: do it!