Merge lp:~cjwatson/launchpad/bzr-webhooks into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17771
Proposed branch: lp:~cjwatson/launchpad/bzr-webhooks
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/db-bzr-webhooks
Diff against target: 1669 lines (+563/-321)
24 files modified
lib/lp/code/browser/branch.py (+11/-2)
lib/lp/code/configure.zcml (+3/-0)
lib/lp/code/interfaces/branch.py (+48/-18)
lib/lp/code/interfaces/branchlookup.py (+8/-0)
lib/lp/code/interfaces/branchtarget.py (+1/-1)
lib/lp/code/interfaces/gitrepository.py (+2/-1)
lib/lp/code/model/branch.py (+8/-1)
lib/lp/code/model/branchlookup.py (+11/-8)
lib/lp/code/model/branchtarget.py (+3/-3)
lib/lp/code/model/tests/test_branch.py (+206/-206)
lib/lp/code/model/tests/test_branchlookup.py (+35/-30)
lib/lp/code/model/tests/test_branchset.py (+12/-0)
lib/lp/code/model/tests/test_branchtarget.py (+9/-9)
lib/lp/codehosting/scanner/bzrsync.py (+13/-1)
lib/lp/codehosting/scanner/events.py (+9/-1)
lib/lp/codehosting/scanner/tests/test_bzrsync.py (+33/-1)
lib/lp/registry/browser/productseries.py (+2/-1)
lib/lp/services/webhooks/client.py (+1/-1)
lib/lp/services/webhooks/interfaces.py (+2/-1)
lib/lp/services/webhooks/model.py (+11/-0)
lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt (+2/-2)
lib/lp/services/webhooks/tests/test_browser.py (+73/-8)
lib/lp/services/webhooks/tests/test_model.py (+31/-15)
lib/lp/services/webhooks/tests/test_webservice.py (+29/-11)
To merge this branch: bzr merge lp:~cjwatson/launchpad/bzr-webhooks
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+272248@code.launchpad.net

Commit message

Add webhook support for Bazaar branches.

Description of the change

Add webhook support for Bazaar branches. It's almost exactly the same shape as for Git repositories, but with the webhook triggering hooked into the TipChange event instead of written directly in the job, and with a slightly different payload structure since there's only one pair of revisions involved.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) wrote :

All right, in the name of consistency I cleaned up a bunch of bits of Branch (mainly BzrIdentityMixin) to match the corresponding newer code in GitRepository/GitIdentityMixin, and introduced Branch.shortened_path which lacks the lp: prefix. bzr_branch_path for this branch would now be "~cjwatson/launchpad/bzr-webhooks", or for the target of this merge proposal would be "launchpad". Does this look OK to you?

Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
William Grant (wgrant) wrote :

Actually, is there any way to get a branch by its shortened_name? lp.branches.getByUrl exists, but there's no lp.branches.getByPath.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branch.py'
2--- lib/lp/code/browser/branch.py 2015-09-23 15:38:13 +0000
3+++ lib/lp/code/browser/branch.py 2015-09-29 15:54:57 +0000
4@@ -125,6 +125,7 @@
5 from lp.services import searchbuilder
6 from lp.services.config import config
7 from lp.services.database.constants import UTC_NOW
8+from lp.services.features import getFeatureFlag
9 from lp.services.feeds.browser import (
10 BranchFeedLink,
11 FeedsMixin,
12@@ -152,6 +153,7 @@
13 from lp.services.webapp.breadcrumb import NameBreadcrumb
14 from lp.services.webapp.escaping import structured
15 from lp.services.webapp.interfaces import ICanonicalUrlData
16+from lp.services.webhooks.browser import WebhookTargetNavigationMixin
17 from lp.snappy.browser.hassnaps import (
18 HasSnapsMenuMixin,
19 HasSnapsViewMixin,
20@@ -183,7 +185,7 @@
21 return self.context.target.components[-1]
22
23
24-class BranchNavigation(Navigation):
25+class BranchNavigation(WebhookTargetNavigationMixin, Navigation):
26
27 usedfor = IBranch
28
29@@ -240,7 +242,7 @@
30 facet = 'branches'
31 title = 'Edit branch'
32 links = (
33- 'edit', 'reviewer', 'edit_whiteboard', 'delete')
34+ 'edit', 'reviewer', 'edit_whiteboard', 'webhooks', 'delete')
35
36 def branch_is_import(self):
37 return self.context.branch_type == BranchType.IMPORTED
38@@ -267,6 +269,13 @@
39 text = 'Set branch reviewer'
40 return Link('+reviewer', text, icon='edit')
41
42+ @enabled_with_permission('launchpad.Edit')
43+ def webhooks(self):
44+ text = 'Manage webhooks'
45+ return Link(
46+ '+webhooks', text, icon='edit',
47+ enabled=bool(getFeatureFlag('webhooks.new.enabled')))
48+
49
50 class BranchContextMenu(ContextMenu, HasRecipesMenuMixin, HasSnapsMenuMixin):
51 """Context menu for branches."""
52
53=== modified file 'lib/lp/code/configure.zcml'
54--- lib/lp/code/configure.zcml 2015-09-11 15:11:34 +0000
55+++ lib/lp/code/configure.zcml 2015-09-29 15:54:57 +0000
56@@ -418,6 +418,9 @@
57 for="lp.codehosting.scanner.events.ITipChanged"
58 handler="lp.codehosting.scanner.bzrsync.update_recipes"/>
59 <subscriber
60+ for="lp.codehosting.scanner.events.ITipChanged"
61+ handler="lp.codehosting.scanner.bzrsync.trigger_webhooks"/>
62+ <subscriber
63 for="lp.codehosting.scanner.events.IRevisionsRemoved"
64 handler="lp.codehosting.scanner.email.send_removed_revision_emails"/>
65 <subscriber
66
67=== modified file 'lib/lp/code/interfaces/branch.py'
68--- lib/lp/code/interfaces/branch.py 2015-09-29 00:32:52 +0000
69+++ lib/lp/code/interfaces/branch.py 2015-09-29 15:54:57 +0000
70@@ -105,6 +105,7 @@
71 structured,
72 )
73 from lp.services.webapp.interfaces import ITableBatchNavigator
74+from lp.services.webhooks.interfaces import IWebhookTarget
75
76
77 DEFAULT_BRANCH_STATUS_IN_LISTING = (
78@@ -711,6 +712,10 @@
79 Reference(
80 title=_("The associated CodeImport, if any."), schema=Interface))
81
82+ shortened_path = Attribute(
83+ "The shortest reasonable version of the path to this branch; as "
84+ "bzr_identity but without the 'lp:' prefix.")
85+
86 bzr_identity = exported(
87 Text(
88 title=_('Bazaar Identity'),
89@@ -780,7 +785,7 @@
90 :return: A list of suite source packages ordered by pocket.
91 """
92
93- def branchLinks():
94+ def getBranchLinks():
95 """Return a sorted list of ICanHasLinkedBranch objects.
96
97 There is one result for each related linked object that the branch is
98@@ -792,7 +797,7 @@
99 more important links are sorted first.
100 """
101
102- def branchIdentities():
103+ def getBranchIdentities():
104 """A list of aliases for a branch.
105
106 Returns a list of tuples of bzr identity and context object. There is
107@@ -805,10 +810,10 @@
108
109 For example, a branch linked to the development focus of the 'fooix'
110 project is accessible using:
111- lp:fooix - the linked object is the product fooix
112- lp:fooix/trunk - the linked object is the trunk series of fooix
113- lp:~owner/fooix/name - the unique name of the branch where the
114- linked object is the branch itself.
115+ fooix - the linked object is the product fooix
116+ fooix/trunk - the linked object is the trunk series of fooix
117+ ~owner/fooix/name - the unique name of the branch where the linked
118+ object is the branch itself.
119 """
120
121 # subscription-related methods
122@@ -1129,7 +1134,7 @@
123 vocabulary=ControlFormat))
124
125
126-class IBranchEdit(Interface):
127+class IBranchEdit(IWebhookTarget):
128 """IBranch attributes that require launchpad.Edit permission."""
129
130 @call_with(user=REQUEST_USER)
131@@ -1313,8 +1318,7 @@
132 Return None if no match was found.
133 """
134
135- @operation_parameters(
136- url=TextLine(title=_('Branch URL'), required=True))
137+ @operation_parameters(url=TextLine(title=_('Branch URL'), required=True))
138 @operation_returns_entry(IBranch)
139 @export_read_operation()
140 @operation_for_version('beta')
141@@ -1357,6 +1361,29 @@
142 associated branch, the URL will map to `None`.
143 """
144
145+ @operation_parameters(path=TextLine(title=_('Branch path'), required=True))
146+ @operation_returns_entry(IBranch)
147+ @export_read_operation()
148+ @operation_for_version('devel')
149+ def getByPath(path):
150+ """Find a branch by its path.
151+
152+ The path is the same as its lp: URL, but without the leading lp:, so
153+ it may be in any of these forms::
154+
155+ Unique names:
156+ ~OWNER/PROJECT/NAME
157+ ~OWNER/DISTRO/SERIES/SOURCE/NAME
158+ ~OWNER/+junk/NAME
159+ Aliases linked to other objects:
160+ PROJECT
161+ PROJECT/SERIES
162+ DISTRO/SOURCE
163+ DISTRO/SUITE/SOURCE
164+
165+ Return None if no match was found.
166+ """
167+
168 @collection_default_content()
169 def getBranches(limit=50, eager_load=True):
170 """Return a collection of branches.
171@@ -1481,26 +1508,29 @@
172 """
173
174 @property
175+ def shortened_path(self):
176+ """See `IBranch`."""
177+ return self.getBranchIdentities()[0][0]
178+
179+ @property
180 def bzr_identity(self):
181 """See `IBranch`."""
182- identity, context = self.branchIdentities()[0]
183- return identity
184+ return config.codehosting.bzr_lp_prefix + self.shortened_path
185
186 identity = bzr_identity
187
188- def branchIdentities(self):
189+ def getBranchIdentities(self):
190 """See `IBranch`."""
191- lp_prefix = config.codehosting.bzr_lp_prefix
192- if not self.target.supports_short_identites:
193+ if not self.target.supports_short_identities:
194 identities = []
195 else:
196 identities = [
197- (lp_prefix + link.bzr_path, link.context)
198- for link in self.branchLinks()]
199- identities.append((lp_prefix + self.unique_name, self))
200+ (link.bzr_path, link.context)
201+ for link in self.getBranchLinks()]
202+ identities.append((self.unique_name, self))
203 return identities
204
205- def branchLinks(self):
206+ def getBranchLinks(self):
207 """See `IBranch`."""
208 links = []
209 for suite_sp in self.associatedSuiteSourcePackages():
210
211=== modified file 'lib/lp/code/interfaces/branchlookup.py'
212--- lib/lp/code/interfaces/branchlookup.py 2013-01-07 02:40:55 +0000
213+++ lib/lp/code/interfaces/branchlookup.py 2015-09-29 15:54:57 +0000
214@@ -158,6 +158,14 @@
215 paths are not handled for shortcut paths.
216 """
217
218+ def getByPath(path):
219+ """Find the branch associated with a path.
220+
221+ As with `getByLPPath`, but returns None instead of raising any of
222+ the documented exceptions, and returns only the `IBranch` on success
223+ and not any extra path.
224+ """
225+
226
227 def path_lookups(path):
228 if path.startswith(BRANCH_ID_ALIAS_PREFIX + '/'):
229
230=== modified file 'lib/lp/code/interfaces/branchtarget.py'
231--- lib/lp/code/interfaces/branchtarget.py 2015-04-21 14:32:31 +0000
232+++ lib/lp/code/interfaces/branchtarget.py 2015-09-29 15:54:57 +0000
233@@ -92,7 +92,7 @@
234 supports_merge_proposals = Attribute(
235 "Does this target support merge proposals at all?")
236
237- supports_short_identites = Attribute(
238+ supports_short_identities = Attribute(
239 "Does this target support shortened bazaar identities?")
240
241 supports_code_imports = Attribute(
242
243=== modified file 'lib/lp/code/interfaces/gitrepository.py'
244--- lib/lp/code/interfaces/gitrepository.py 2015-09-21 10:08:08 +0000
245+++ lib/lp/code/interfaces/gitrepository.py 2015-09-29 15:54:57 +0000
246@@ -688,7 +688,8 @@
247 def getByPath(user, path):
248 """Find a repository by its path.
249
250- Any of these forms may be used, with or without a leading slash:
251+ Any of these forms may be used::
252+
253 Unique names:
254 ~OWNER/PROJECT/+git/NAME
255 ~OWNER/DISTRO/+source/SOURCE/+git/NAME
256
257=== modified file 'lib/lp/code/model/branch.py'
258--- lib/lp/code/model/branch.py 2015-09-29 00:32:52 +0000
259+++ lib/lp/code/model/branch.py 2015-09-29 15:54:57 +0000
260@@ -180,10 +180,12 @@
261 from lp.services.webapp import urlappend
262 from lp.services.webapp.authorization import check_permission
263 from lp.services.webapp.interfaces import ILaunchBag
264+from lp.services.webhooks.interfaces import IWebhookSet
265+from lp.services.webhooks.model import WebhookTargetMixin
266
267
268 @implementer(IBranch, IPrivacy, IInformationType)
269-class Branch(SQLBase, BzrIdentityMixin):
270+class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
271 """A sequence of ordered revisions in Bazaar."""
272 _table = 'Branch'
273
274@@ -1336,6 +1338,7 @@
275
276 self._deleteBranchSubscriptions()
277 self._deleteJobs()
278+ getUtility(IWebhookSet).delete(self.webhooks)
279
280 # Now destroy the branch.
281 branch_id = self.id
282@@ -1610,6 +1613,10 @@
283 """See `IBranchSet`."""
284 return getUtility(IBranchLookup).getByUrls(urls)
285
286+ def getByPath(self, path):
287+ """See `IBranchSet`."""
288+ return getUtility(IBranchLookup).getByPath(path)
289+
290 def getBranches(self, limit=50, eager_load=True):
291 """See `IBranchSet`."""
292 anon_branches = getUtility(IAllBranches).visibleByUser(None)
293
294=== modified file 'lib/lp/code/model/branchlookup.py'
295--- lib/lp/code/model/branchlookup.py 2015-07-10 05:57:13 +0000
296+++ lib/lp/code/model/branchlookup.py 2015-09-29 15:54:57 +0000
297@@ -248,14 +248,7 @@
298 if uri.scheme == 'lp':
299 if not self._uriHostAllowed(uri):
300 return None
301- try:
302- return self.getByLPPath(uri.path.lstrip('/'))[0]
303- except (
304- CannotHaveLinkedBranch, InvalidNamespace, InvalidProductName,
305- NoSuchBranch, NoSuchPerson, NoSuchProduct,
306- NoSuchProductSeries, NoSuchDistroSeries,
307- NoSuchSourcePackageName, NoLinkedBranch):
308- return None
309+ return self.getByPath(uri.path.lstrip('/'))
310
311 return Branch.selectOneBy(url=url)
312
313@@ -386,6 +379,16 @@
314 suffix = path[len(bzr_path) + 1:]
315 return branch, suffix
316
317+ def getByPath(self, path):
318+ """See `IBranchLookup`."""
319+ try:
320+ return self.getByLPPath(path)[0]
321+ except (
322+ CannotHaveLinkedBranch, InvalidNamespace, InvalidProductName,
323+ NoSuchBranch, NoSuchPerson, NoSuchProduct, NoSuchProductSeries,
324+ NoSuchDistroSeries, NoSuchSourcePackageName, NoLinkedBranch):
325+ return None
326+
327 def _getLinkedBranchAndPath(self, provided):
328 """Get the linked branch for 'provided', and the bzr_path.
329
330
331=== modified file 'lib/lp/code/model/branchtarget.py'
332--- lib/lp/code/model/branchtarget.py 2015-07-08 16:05:11 +0000
333+++ lib/lp/code/model/branchtarget.py 2015-09-29 15:54:57 +0000
334@@ -113,7 +113,7 @@
335 return True
336
337 @property
338- def supports_short_identites(self):
339+ def supports_short_identities(self):
340 """See `IBranchTarget`."""
341 return True
342
343@@ -229,7 +229,7 @@
344 return False
345
346 @property
347- def supports_short_identites(self):
348+ def supports_short_identities(self):
349 """See `IBranchTarget`."""
350 return False
351
352@@ -314,7 +314,7 @@
353 return True
354
355 @property
356- def supports_short_identites(self):
357+ def supports_short_identities(self):
358 """See `IBranchTarget`."""
359 return True
360
361
362=== modified file 'lib/lp/code/model/tests/test_branch.py'
363--- lib/lp/code/model/tests/test_branch.py 2015-08-06 12:03:36 +0000
364+++ lib/lp/code/model/tests/test_branch.py 2015-09-29 15:54:57 +0000
365@@ -910,202 +910,8 @@
366 self.assertFalse(branch.upgrade_pending)
367
368
369-class TestBranchLinksAndIdentites(TestCaseWithFactory):
370- """Test IBranch.branchLinks and IBranch.branchIdentities."""
371-
372- layer = DatabaseFunctionalLayer
373-
374- def test_default_identities(self):
375- # If there are no links, the only branch identity is the unique name.
376- branch = self.factory.makeAnyBranch()
377- self.assertEqual(
378- [('lp://dev/' + branch.unique_name, branch)],
379- branch.branchIdentities())
380-
381- def test_linked_to_product(self):
382- # If a branch is linked to the product, it is also by definition
383- # linked to the development focus of the product.
384- fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix'))
385- fooix.development_focus.name = 'devel'
386- eric = self.factory.makePerson(name='eric')
387- branch = self.factory.makeProductBranch(
388- product=fooix, owner=eric, name='trunk')
389- linked_branch = ICanHasLinkedBranch(fooix)
390- linked_branch.setBranch(branch)
391- self.assertEqual(
392- [linked_branch, ICanHasLinkedBranch(fooix.development_focus)],
393- branch.branchLinks())
394- self.assertEqual(
395- [('lp://dev/fooix', fooix),
396- ('lp://dev/fooix/devel', fooix.development_focus),
397- ('lp://dev/~eric/fooix/trunk', branch)],
398- branch.branchIdentities())
399-
400- def test_linked_to_product_series(self):
401- # If a branch is linked to a non-development series of a product and
402- # not linked to the product itself, then only the product series is
403- # returned in the links.
404- fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix'))
405- future = self.factory.makeProductSeries(product=fooix, name='future')
406- eric = self.factory.makePerson(name='eric')
407- branch = self.factory.makeProductBranch(
408- product=fooix, owner=eric, name='trunk')
409- linked_branch = ICanHasLinkedBranch(future)
410- login_person(fooix.owner)
411- linked_branch.setBranch(branch)
412- self.assertEqual(
413- [linked_branch],
414- branch.branchLinks())
415- self.assertEqual(
416- [('lp://dev/fooix/future', future),
417- ('lp://dev/~eric/fooix/trunk', branch)],
418- branch.branchIdentities())
419-
420- def test_linked_to_package(self):
421- # If a branch is linked to a suite source package where the
422- # distroseries is the current series for the distribution, there is a
423- # link for both the distribution source package and the suite source
424- # package.
425- mint = self.factory.makeDistribution(name='mint')
426- dev = self.factory.makeDistroSeries(
427- distribution=mint, version='1.0', name='dev')
428- eric = self.factory.makePerson(name='eric')
429- branch = self.factory.makePackageBranch(
430- distroseries=dev, sourcepackagename='choc', name='tip',
431- owner=eric)
432- dsp = self.factory.makeDistributionSourcePackage('choc', mint)
433- distro_link = ICanHasLinkedBranch(dsp)
434- development_package = dsp.development_version
435- suite_sourcepackage = development_package.getSuiteSourcePackage(
436- PackagePublishingPocket.RELEASE)
437- suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
438-
439- registrant = suite_sourcepackage.distribution.owner
440- run_with_login(
441- registrant,
442- suite_sp_link.setBranch, branch, registrant)
443-
444- self.assertEqual(
445- [distro_link, suite_sp_link],
446- branch.branchLinks())
447- self.assertEqual(
448- [('lp://dev/mint/choc', dsp),
449- ('lp://dev/mint/dev/choc', suite_sourcepackage),
450- ('lp://dev/~eric/mint/dev/choc/tip', branch)],
451- branch.branchIdentities())
452-
453- def test_linked_to_package_not_release_pocket(self):
454- # If a branch is linked to a suite source package where the
455- # distroseries is the current series for the distribution, but the
456- # pocket is not the RELEASE pocket, then there is only the link for
457- # the suite source package.
458- mint = self.factory.makeDistribution(name='mint')
459- dev = self.factory.makeDistroSeries(
460- distribution=mint, version='1.0', name='dev')
461- eric = self.factory.makePerson(name='eric')
462- branch = self.factory.makePackageBranch(
463- distroseries=dev, sourcepackagename='choc', name='tip',
464- owner=eric)
465- dsp = self.factory.makeDistributionSourcePackage('choc', mint)
466- development_package = dsp.development_version
467- suite_sourcepackage = development_package.getSuiteSourcePackage(
468- PackagePublishingPocket.BACKPORTS)
469- suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
470-
471- registrant = suite_sourcepackage.distribution.owner
472- run_with_login(
473- registrant,
474- suite_sp_link.setBranch, branch, registrant)
475-
476- self.assertEqual(
477- [suite_sp_link],
478- branch.branchLinks())
479- self.assertEqual(
480- [('lp://dev/mint/dev-backports/choc', suite_sourcepackage),
481- ('lp://dev/~eric/mint/dev/choc/tip', branch)],
482- branch.branchIdentities())
483-
484- def test_linked_to_package_not_current_series(self):
485- # If the branch is linked to a suite source package where the distro
486- # series is not the current series, only the suite source package is
487- # returned in the links.
488- mint = self.factory.makeDistribution(name='mint')
489- self.factory.makeDistroSeries(
490- distribution=mint, version='1.0', name='dev')
491- supported = self.factory.makeDistroSeries(
492- distribution=mint, version='0.9', name='supported')
493- eric = self.factory.makePerson(name='eric')
494- branch = self.factory.makePackageBranch(
495- distroseries=supported, sourcepackagename='choc', name='tip',
496- owner=eric)
497- suite_sp = self.factory.makeSuiteSourcePackage(
498- distroseries=supported, sourcepackagename='choc',
499- pocket=PackagePublishingPocket.RELEASE)
500- suite_sp_link = ICanHasLinkedBranch(suite_sp)
501-
502- registrant = suite_sp.distribution.owner
503- run_with_login(
504- registrant,
505- suite_sp_link.setBranch, branch, registrant)
506-
507- self.assertEqual(
508- [suite_sp_link],
509- branch.branchLinks())
510- self.assertEqual(
511- [('lp://dev/mint/supported/choc', suite_sp),
512- ('lp://dev/~eric/mint/supported/choc/tip', branch)],
513- branch.branchIdentities())
514-
515- def test_linked_across_project_to_package(self):
516- # If a product branch is linked to a suite source package, the links
517- # are the same as if it was a source package branch.
518- mint = self.factory.makeDistribution(name='mint')
519- self.factory.makeDistroSeries(
520- distribution=mint, version='1.0', name='dev')
521- eric = self.factory.makePerson(name='eric')
522- fooix = self.factory.makeProduct(name='fooix')
523- branch = self.factory.makeProductBranch(
524- product=fooix, owner=eric, name='trunk')
525- dsp = self.factory.makeDistributionSourcePackage('choc', mint)
526- distro_link = ICanHasLinkedBranch(dsp)
527- development_package = dsp.development_version
528- suite_sourcepackage = development_package.getSuiteSourcePackage(
529- PackagePublishingPocket.RELEASE)
530- suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
531-
532- registrant = suite_sourcepackage.distribution.owner
533- run_with_login(
534- registrant,
535- suite_sp_link.setBranch, branch, registrant)
536-
537- self.assertEqual(
538- [distro_link, suite_sp_link],
539- branch.branchLinks())
540- self.assertEqual(
541- [('lp://dev/mint/choc', dsp),
542- ('lp://dev/mint/dev/choc', suite_sourcepackage),
543- ('lp://dev/~eric/fooix/trunk', branch)],
544- branch.branchIdentities())
545-
546- def test_junk_branch_links(self):
547- # If a junk branch has links, those links are returned in the
548- # branchLinks, but the branchIdentities just has the branch unique
549- # name.
550- eric = self.factory.makePerson(name='eric')
551- branch = self.factory.makePersonalBranch(owner=eric, name='foo')
552- fooix = removeSecurityProxy(self.factory.makeProduct())
553- linked_branch = ICanHasLinkedBranch(fooix)
554- linked_branch.setBranch(branch)
555- self.assertEqual(
556- [linked_branch, ICanHasLinkedBranch(fooix.development_focus)],
557- branch.branchLinks())
558- self.assertEqual(
559- [('lp://dev/~eric/+junk/foo', branch)],
560- branch.branchIdentities())
561-
562-
563-class TestBzrIdentity(TestCaseWithFactory):
564- """Test IBranch.bzr_identity."""
565+class TestBzrIdentityMixin(TestCaseWithFactory):
566+ """Test the defaults and identities provided by BzrIdentityMixin."""
567
568 layer = DatabaseFunctionalLayer
569
570@@ -1115,16 +921,17 @@
571 Actually, it'll be lp://dev/<identity_path>.
572 """
573 self.assertEqual(
574- 'lp://dev/%s' % identity_path, branch.bzr_identity,
575- "bzr identity")
576+ identity_path, branch.shortened_path, "shortened path")
577+ self.assertEqual(
578+ 'lp://dev/%s' % identity_path, branch.bzr_identity, "bzr identity")
579
580- def test_default_identity(self):
581+ def test_bzr_identity_default(self):
582 # By default, the bzr identity is an lp URL with the branch's unique
583 # name.
584 branch = self.factory.makeAnyBranch()
585 self.assertBzrIdentity(branch, branch.unique_name)
586
587- def test_linked_to_product(self):
588+ def test_bzr_identity_linked_to_product(self):
589 # If a branch is the development focus branch for a product, then it's
590 # bzr identity is lp:product.
591 branch = self.factory.makeProductBranch()
592@@ -1133,7 +940,7 @@
593 linked_branch.setBranch(branch)
594 self.assertBzrIdentity(branch, linked_branch.bzr_path)
595
596- def test_linked_to_product_series(self):
597+ def test_bzr_identity_linked_to_product_series(self):
598 # If a branch is the development focus branch for a product series,
599 # then it's bzr identity is lp:product/series.
600 branch = self.factory.makeProductBranch()
601@@ -1144,7 +951,7 @@
602 linked_branch.setBranch(branch)
603 self.assertBzrIdentity(branch, linked_branch.bzr_path)
604
605- def test_private_linked_to_product(self):
606+ def test_bzr_identity_private_linked_to_product(self):
607 # Private branches also have a short lp:url.
608 branch = self.factory.makeProductBranch(
609 information_type=InformationType.USERDATA)
610@@ -1153,7 +960,7 @@
611 ICanHasLinkedBranch(product).setBranch(branch)
612 self.assertBzrIdentity(branch, product.name)
613
614- def test_linked_to_series_and_dev_focus(self):
615+ def test_bzr_identity_linked_to_series_and_dev_focus(self):
616 # If a branch is the development focus branch for a product and the
617 # branch for a series, the bzr identity will be the storter of the two
618 # URLs.
619@@ -1167,7 +974,7 @@
620 series_link.setBranch(branch)
621 self.assertBzrIdentity(branch, product_link.bzr_path)
622
623- def test_junk_branch_always_unique_name(self):
624+ def test_bzr_identity_junk_branch_always_unique_name(self):
625 # For junk branches, the bzr identity is always based on the unique
626 # name of the branch, even if it's linked to a product, product series
627 # or whatever.
628@@ -1176,7 +983,7 @@
629 ICanHasLinkedBranch(product).setBranch(branch)
630 self.assertBzrIdentity(branch, branch.unique_name)
631
632- def test_linked_to_package(self):
633+ def test_bzr_identity_linked_to_package(self):
634 # If a branch is linked to a pocket of a package, then the
635 # bzr identity is the path to that package.
636 branch = self.factory.makePackageBranch()
637@@ -1191,7 +998,7 @@
638 login(ANONYMOUS)
639 self.assertBzrIdentity(branch, linked_branch.bzr_path)
640
641- def test_linked_to_dev_package(self):
642+ def test_bzr_identity_linked_to_dev_package(self):
643 # If a branch is linked to the development focus version of a package
644 # then the bzr identity is distro/package.
645 sourcepackage = self.factory.makeSourcePackage()
646@@ -1205,6 +1012,192 @@
647 linked_branch.setBranch, branch, registrant)
648 self.assertBzrIdentity(branch, linked_branch.bzr_path)
649
650+ def test_identities_no_links(self):
651+ # If there are no links, the only branch identity is the unique name.
652+ branch = self.factory.makeAnyBranch()
653+ self.assertEqual(
654+ [(branch.unique_name, branch)], branch.getBranchIdentities())
655+
656+ def test_linked_to_product(self):
657+ # If a branch is linked to the product, it is also by definition
658+ # linked to the development focus of the product.
659+ fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix'))
660+ fooix.development_focus.name = 'devel'
661+ eric = self.factory.makePerson(name='eric')
662+ branch = self.factory.makeProductBranch(
663+ product=fooix, owner=eric, name='trunk')
664+ linked_branch = ICanHasLinkedBranch(fooix)
665+ linked_branch.setBranch(branch)
666+ self.assertEqual(
667+ [linked_branch, ICanHasLinkedBranch(fooix.development_focus)],
668+ branch.getBranchLinks())
669+ self.assertEqual(
670+ [('fooix', fooix),
671+ ('fooix/devel', fooix.development_focus),
672+ ('~eric/fooix/trunk', branch)],
673+ branch.getBranchIdentities())
674+
675+ def test_linked_to_product_series(self):
676+ # If a branch is linked to a non-development series of a product and
677+ # not linked to the product itself, then only the product series is
678+ # returned in the links.
679+ fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix'))
680+ future = self.factory.makeProductSeries(product=fooix, name='future')
681+ eric = self.factory.makePerson(name='eric')
682+ branch = self.factory.makeProductBranch(
683+ product=fooix, owner=eric, name='trunk')
684+ linked_branch = ICanHasLinkedBranch(future)
685+ login_person(fooix.owner)
686+ linked_branch.setBranch(branch)
687+ self.assertEqual(
688+ [linked_branch],
689+ branch.getBranchLinks())
690+ self.assertEqual(
691+ [('fooix/future', future),
692+ ('~eric/fooix/trunk', branch)],
693+ branch.getBranchIdentities())
694+
695+ def test_linked_to_package(self):
696+ # If a branch is linked to a suite source package where the
697+ # distroseries is the current series for the distribution, there is a
698+ # link for both the distribution source package and the suite source
699+ # package.
700+ mint = self.factory.makeDistribution(name='mint')
701+ dev = self.factory.makeDistroSeries(
702+ distribution=mint, version='1.0', name='dev')
703+ eric = self.factory.makePerson(name='eric')
704+ branch = self.factory.makePackageBranch(
705+ distroseries=dev, sourcepackagename='choc', name='tip',
706+ owner=eric)
707+ dsp = self.factory.makeDistributionSourcePackage('choc', mint)
708+ distro_link = ICanHasLinkedBranch(dsp)
709+ development_package = dsp.development_version
710+ suite_sourcepackage = development_package.getSuiteSourcePackage(
711+ PackagePublishingPocket.RELEASE)
712+ suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
713+
714+ registrant = suite_sourcepackage.distribution.owner
715+ run_with_login(
716+ registrant,
717+ suite_sp_link.setBranch, branch, registrant)
718+
719+ self.assertEqual(
720+ [distro_link, suite_sp_link],
721+ branch.getBranchLinks())
722+ self.assertEqual(
723+ [('mint/choc', dsp),
724+ ('mint/dev/choc', suite_sourcepackage),
725+ ('~eric/mint/dev/choc/tip', branch)],
726+ branch.getBranchIdentities())
727+
728+ def test_linked_to_package_not_release_pocket(self):
729+ # If a branch is linked to a suite source package where the
730+ # distroseries is the current series for the distribution, but the
731+ # pocket is not the RELEASE pocket, then there is only the link for
732+ # the suite source package.
733+ mint = self.factory.makeDistribution(name='mint')
734+ dev = self.factory.makeDistroSeries(
735+ distribution=mint, version='1.0', name='dev')
736+ eric = self.factory.makePerson(name='eric')
737+ branch = self.factory.makePackageBranch(
738+ distroseries=dev, sourcepackagename='choc', name='tip',
739+ owner=eric)
740+ dsp = self.factory.makeDistributionSourcePackage('choc', mint)
741+ development_package = dsp.development_version
742+ suite_sourcepackage = development_package.getSuiteSourcePackage(
743+ PackagePublishingPocket.BACKPORTS)
744+ suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
745+
746+ registrant = suite_sourcepackage.distribution.owner
747+ run_with_login(
748+ registrant,
749+ suite_sp_link.setBranch, branch, registrant)
750+
751+ self.assertEqual(
752+ [suite_sp_link],
753+ branch.getBranchLinks())
754+ self.assertEqual(
755+ [('mint/dev-backports/choc', suite_sourcepackage),
756+ ('~eric/mint/dev/choc/tip', branch)],
757+ branch.getBranchIdentities())
758+
759+ def test_linked_to_package_not_current_series(self):
760+ # If the branch is linked to a suite source package where the distro
761+ # series is not the current series, only the suite source package is
762+ # returned in the links.
763+ mint = self.factory.makeDistribution(name='mint')
764+ self.factory.makeDistroSeries(
765+ distribution=mint, version='1.0', name='dev')
766+ supported = self.factory.makeDistroSeries(
767+ distribution=mint, version='0.9', name='supported')
768+ eric = self.factory.makePerson(name='eric')
769+ branch = self.factory.makePackageBranch(
770+ distroseries=supported, sourcepackagename='choc', name='tip',
771+ owner=eric)
772+ suite_sp = self.factory.makeSuiteSourcePackage(
773+ distroseries=supported, sourcepackagename='choc',
774+ pocket=PackagePublishingPocket.RELEASE)
775+ suite_sp_link = ICanHasLinkedBranch(suite_sp)
776+
777+ registrant = suite_sp.distribution.owner
778+ run_with_login(
779+ registrant,
780+ suite_sp_link.setBranch, branch, registrant)
781+
782+ self.assertEqual(
783+ [suite_sp_link],
784+ branch.getBranchLinks())
785+ self.assertEqual(
786+ [('mint/supported/choc', suite_sp),
787+ ('~eric/mint/supported/choc/tip', branch)],
788+ branch.getBranchIdentities())
789+
790+ def test_linked_across_project_to_package(self):
791+ # If a product branch is linked to a suite source package, the links
792+ # are the same as if it was a source package branch.
793+ mint = self.factory.makeDistribution(name='mint')
794+ self.factory.makeDistroSeries(
795+ distribution=mint, version='1.0', name='dev')
796+ eric = self.factory.makePerson(name='eric')
797+ fooix = self.factory.makeProduct(name='fooix')
798+ branch = self.factory.makeProductBranch(
799+ product=fooix, owner=eric, name='trunk')
800+ dsp = self.factory.makeDistributionSourcePackage('choc', mint)
801+ distro_link = ICanHasLinkedBranch(dsp)
802+ development_package = dsp.development_version
803+ suite_sourcepackage = development_package.getSuiteSourcePackage(
804+ PackagePublishingPocket.RELEASE)
805+ suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
806+
807+ registrant = suite_sourcepackage.distribution.owner
808+ run_with_login(
809+ registrant,
810+ suite_sp_link.setBranch, branch, registrant)
811+
812+ self.assertEqual(
813+ [distro_link, suite_sp_link],
814+ branch.getBranchLinks())
815+ self.assertEqual(
816+ [('mint/choc', dsp),
817+ ('mint/dev/choc', suite_sourcepackage),
818+ ('~eric/fooix/trunk', branch)],
819+ branch.getBranchIdentities())
820+
821+ def test_junk_branch_links(self):
822+ # If a junk branch has links, those links are returned by
823+ # getBranchLinks, but getBranchIdentities just returns the branch
824+ # unique name.
825+ eric = self.factory.makePerson(name='eric')
826+ branch = self.factory.makePersonalBranch(owner=eric, name='foo')
827+ fooix = removeSecurityProxy(self.factory.makeProduct())
828+ linked_branch = ICanHasLinkedBranch(fooix)
829+ linked_branch.setBranch(branch)
830+ self.assertEqual(
831+ [linked_branch, ICanHasLinkedBranch(fooix.development_focus)],
832+ branch.getBranchLinks())
833+ self.assertEqual(
834+ [('~eric/+junk/foo', branch)], branch.getBranchIdentities())
835+
836
837 class TestBranchDeletion(TestCaseWithFactory):
838 """Test the different cases that makes a branch deletable or not."""
839@@ -1442,6 +1435,13 @@
840 )
841 self.branch.destroySelf(break_references=True)
842
843+ def test_related_webhooks_deleted(self):
844+ webhook = self.factory.makeWebhook(target=self.branch)
845+ webhook.ping()
846+ self.branch.destroySelf()
847+ transaction.commit()
848+ self.assertRaises(LostObjectError, getattr, webhook, 'target')
849+
850
851 class TestBranchDeletionConsequences(TestCase):
852 """Test determination and application of branch deletion consequences."""
853
854=== modified file 'lib/lp/code/model/tests/test_branchlookup.py'
855--- lib/lp/code/model/tests/test_branchlookup.py 2012-11-12 13:38:27 +0000
856+++ lib/lp/code/model/tests/test_branchlookup.py 2015-09-29 15:54:57 +0000
857@@ -153,7 +153,7 @@
858 self.assertEqual((branch, '/foo'), result)
859
860
861-class TestGetByPath(TestCaseWithFactory):
862+class TestGetByLPPath(TestCaseWithFactory):
863 """Test `IBranchLookup.getByLPPath`."""
864
865 layer = DatabaseFunctionalLayer
866@@ -171,54 +171,51 @@
867 self.factory.getUniqueString()
868 for i in range(arbitrary_num_segments)])
869
870+ def assertMissingPath(self, exctype, path):
871+ self.assertRaises(exctype, self.getByPath, path)
872+
873+ def assertPath(self, expected_branch, expected_suffix, path):
874+ branch, suffix = self.getByPath(path)
875+ self.assertEqual(expected_branch, branch)
876+ self.assertEqual(expected_suffix, suffix)
877+
878 def test_finds_exact_personal_branch(self):
879 branch = self.factory.makePersonalBranch()
880- found_branch, suffix = self.getByPath(branch.unique_name)
881- self.assertEqual(branch, found_branch)
882- self.assertEqual('', suffix)
883+ self.assertPath(branch, '', branch.unique_name)
884
885 def test_finds_suffixed_personal_branch(self):
886 branch = self.factory.makePersonalBranch()
887 suffix = self.makeRelativePath()
888- found_branch, found_suffix = self.getByPath(
889- branch.unique_name + '/' + suffix)
890- self.assertEqual(branch, found_branch)
891- self.assertEqual(suffix, found_suffix)
892+ self.assertPath(branch, suffix, branch.unique_name + '/' + suffix)
893
894 def test_missing_personal_branch(self):
895 owner = self.factory.makePerson()
896 namespace = get_branch_namespace(owner)
897 branch_name = namespace.getBranchName(self.factory.getUniqueString())
898- self.assertRaises(NoSuchBranch, self.getByPath, branch_name)
899+ self.assertMissingPath(NoSuchBranch, branch_name)
900
901 def test_missing_suffixed_personal_branch(self):
902 owner = self.factory.makePerson()
903 namespace = get_branch_namespace(owner)
904 branch_name = namespace.getBranchName(self.factory.getUniqueString())
905 suffix = self.makeRelativePath()
906- self.assertRaises(
907- NoSuchBranch, self.getByPath, branch_name + '/' + suffix)
908+ self.assertMissingPath(NoSuchBranch, branch_name + '/' + suffix)
909
910 def test_finds_exact_product_branch(self):
911 branch = self.factory.makeProductBranch()
912- found_branch, suffix = self.getByPath(branch.unique_name)
913- self.assertEqual(branch, found_branch)
914- self.assertEqual('', suffix)
915+ self.assertPath(branch, '', branch.unique_name)
916
917 def test_finds_suffixed_product_branch(self):
918 branch = self.factory.makeProductBranch()
919 suffix = self.makeRelativePath()
920- found_branch, found_suffix = self.getByPath(
921- branch.unique_name + '/' + suffix)
922- self.assertEqual(branch, found_branch)
923- self.assertEqual(suffix, found_suffix)
924+ self.assertPath(branch, suffix, branch.unique_name + '/' + suffix)
925
926 def test_missing_product_branch(self):
927 owner = self.factory.makePerson()
928 product = self.factory.makeProduct()
929 namespace = get_branch_namespace(owner, product=product)
930 branch_name = namespace.getBranchName(self.factory.getUniqueString())
931- self.assertRaises(NoSuchBranch, self.getByPath, branch_name)
932+ self.assertMissingPath(NoSuchBranch, branch_name)
933
934 def test_missing_suffixed_product_branch(self):
935 owner = self.factory.makePerson()
936@@ -226,14 +223,11 @@
937 namespace = get_branch_namespace(owner, product=product)
938 suffix = self.makeRelativePath()
939 branch_name = namespace.getBranchName(self.factory.getUniqueString())
940- self.assertRaises(
941- NoSuchBranch, self.getByPath, branch_name + '/' + suffix)
942+ self.assertMissingPath(NoSuchBranch, branch_name + '/' + suffix)
943
944 def test_finds_exact_package_branch(self):
945 branch = self.factory.makePackageBranch()
946- found_branch, suffix = self.getByPath(branch.unique_name)
947- self.assertEqual(branch, found_branch)
948- self.assertEqual('', suffix)
949+ self.assertPath(branch, '', branch.unique_name)
950
951 def test_missing_package_branch(self):
952 owner = self.factory.makePerson()
953@@ -243,7 +237,7 @@
954 owner, distroseries=distroseries,
955 sourcepackagename=sourcepackagename)
956 branch_name = namespace.getBranchName(self.factory.getUniqueString())
957- self.assertRaises(NoSuchBranch, self.getByPath, branch_name)
958+ self.assertMissingPath(NoSuchBranch, branch_name)
959
960 def test_missing_suffixed_package_branch(self):
961 owner = self.factory.makePerson()
962@@ -254,19 +248,30 @@
963 sourcepackagename=sourcepackagename)
964 suffix = self.makeRelativePath()
965 branch_name = namespace.getBranchName(self.factory.getUniqueString())
966- self.assertRaises(
967- NoSuchBranch, self.getByPath, branch_name + '/' + suffix)
968+ self.assertMissingPath(NoSuchBranch, branch_name + '/' + suffix)
969
970 def test_too_short(self):
971 person = self.factory.makePerson()
972- self.assertRaises(
973- InvalidNamespace, self.getByPath, '~%s' % person.name)
974+ self.assertMissingPath(InvalidNamespace, '~%s' % person.name)
975
976 def test_no_such_product(self):
977 person = self.factory.makePerson()
978 branch_name = '~%s/%s/%s' % (
979 person.name, self.factory.getUniqueString(), 'branch-name')
980- self.assertRaises(NoSuchProduct, self.getByPath, branch_name)
981+ self.assertMissingPath(NoSuchProduct, branch_name)
982+
983+
984+class TestGetByPath(TestGetByLPPath):
985+ """Test `IBranchLookup.getByPath`."""
986+
987+ def getByPath(self, path):
988+ return self.branch_lookup.getByPath(path)
989+
990+ def assertMissingPath(self, exctype, path):
991+ self.assertIsNone(self.getByPath(path))
992+
993+ def assertPath(self, expected_branch, expected_suffix, path):
994+ self.assertEqual(expected_branch, self.getByPath(path))
995
996
997 class TestGetByUrl(TestCaseWithFactory):
998
999=== modified file 'lib/lp/code/model/tests/test_branchset.py'
1000--- lib/lp/code/model/tests/test_branchset.py 2012-09-18 18:36:09 +0000
1001+++ lib/lp/code/model/tests/test_branchset.py 2015-09-29 15:54:57 +0000
1002@@ -10,7 +10,9 @@
1003
1004 from lp.app.enums import InformationType
1005 from lp.code.interfaces.branch import IBranchSet
1006+from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
1007 from lp.code.model.branch import BranchSet
1008+from lp.services.propertycache import clear_property_cache
1009 from lp.testing import (
1010 login_person,
1011 logout,
1012@@ -46,6 +48,16 @@
1013 branches = BranchSet().getByUrls([url])
1014 self.assertEqual({url: None}, branches)
1015
1016+ def test_getByPath(self):
1017+ branch = self.factory.makeProductBranch()
1018+ self.assertEqual(branch, BranchSet().getByPath(branch.shortened_path))
1019+ product = removeSecurityProxy(branch.product)
1020+ ICanHasLinkedBranch(product).setBranch(branch)
1021+ clear_property_cache(branch)
1022+ self.assertEqual(product.name, branch.shortened_path)
1023+ self.assertEqual(branch, BranchSet().getByPath(branch.shortened_path))
1024+ self.assertIsNone(BranchSet().getByPath('nonexistent'))
1025+
1026 def test_api_branches_query_count(self):
1027 webservice = LaunchpadWebServiceCaller()
1028 collector = QueryCollector()
1029
1030=== modified file 'lib/lp/code/model/tests/test_branchtarget.py'
1031--- lib/lp/code/model/tests/test_branchtarget.py 2014-11-28 22:07:05 +0000
1032+++ lib/lp/code/model/tests/test_branchtarget.py 2015-09-29 15:54:57 +0000
1033@@ -141,9 +141,9 @@
1034 # Package branches do support merge proposals.
1035 self.assertTrue(self.target.supports_merge_proposals)
1036
1037- def test_supports_short_identites(self):
1038- # Package branches do support short bzr identites.
1039- self.assertTrue(self.target.supports_short_identites)
1040+ def test_supports_short_identities(self):
1041+ # Package branches do support short bzr identities.
1042+ self.assertTrue(self.target.supports_short_identities)
1043
1044 def test_displayname(self):
1045 # The display name of a source package target is the display name of
1046@@ -280,9 +280,9 @@
1047 # Personal branches do not support merge proposals.
1048 self.assertFalse(self.target.supports_merge_proposals)
1049
1050- def test_supports_short_identites(self):
1051- # Personal branches do not support short bzr identites.
1052- self.assertFalse(self.target.supports_short_identites)
1053+ def test_supports_short_identities(self):
1054+ # Personal branches do not support short bzr identities.
1055+ self.assertFalse(self.target.supports_short_identities)
1056
1057 def test_displayname(self):
1058 # The display name of a person branch target is ~$USER/+junk.
1059@@ -405,9 +405,9 @@
1060 # Product branches do support merge proposals.
1061 self.assertTrue(self.target.supports_merge_proposals)
1062
1063- def test_supports_short_identites(self):
1064- # Product branches do support short bzr identites.
1065- self.assertTrue(self.target.supports_short_identites)
1066+ def test_supports_short_identities(self):
1067+ # Product branches do support short bzr identities.
1068+ self.assertTrue(self.target.supports_short_identities)
1069
1070 def test_displayname(self):
1071 # The display name of a product branch target is the display name of
1072
1073=== modified file 'lib/lp/codehosting/scanner/bzrsync.py'
1074--- lib/lp/codehosting/scanner/bzrsync.py 2015-02-25 12:48:27 +0000
1075+++ lib/lp/codehosting/scanner/bzrsync.py 2015-09-29 15:54:57 +0000
1076@@ -1,6 +1,6 @@
1077 #!/usr/bin/python
1078 #
1079-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
1080+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
1081 # GNU Affero General Public License version 3 (see the file LICENSE).
1082
1083 """Import version control metadata from a Bazaar branch into the database."""
1084@@ -34,7 +34,9 @@
1085 from lp.code.model.revision import Revision
1086 from lp.codehosting.scanner import events
1087 from lp.services.config import config
1088+from lp.services.features import getFeatureFlag
1089 from lp.services.utils import iter_list_chunks
1090+from lp.services.webhooks.interfaces import IWebhookSet
1091 from lp.translations.interfaces.translationtemplatesbuild import (
1092 ITranslationTemplatesBuildSource,
1093 )
1094@@ -323,3 +325,13 @@
1095
1096 def update_recipes(tip_changed):
1097 tip_changed.db_branch.markRecipesStale()
1098+
1099+
1100+def trigger_webhooks(tip_changed):
1101+ old_revid = tip_changed.old_tip_revision_id
1102+ new_revid = tip_changed.new_tip_revision_id
1103+ if getFeatureFlag("code.bzr.webhooks.enabled") and old_revid != new_revid:
1104+ payload = tip_changed.composeWebhookPayload(
1105+ tip_changed.db_branch, old_revid, new_revid)
1106+ getUtility(IWebhookSet).trigger(
1107+ tip_changed.db_branch, "bzr:push:0.1", payload)
1108
1109=== modified file 'lib/lp/codehosting/scanner/events.py'
1110--- lib/lp/codehosting/scanner/events.py 2015-07-08 16:05:11 +0000
1111+++ lib/lp/codehosting/scanner/events.py 2015-09-29 15:54:57 +0000
1112@@ -1,4 +1,4 @@
1113-# Copyright 2009 Canonical Ltd. This software is licensed under the
1114+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
1115 # GNU Affero General Public License version 3 (see the file LICENSE).
1116
1117 """Events generated by the scanner."""
1118@@ -82,6 +82,14 @@
1119 """The new tip revision id from this scan."""
1120 return self.bzr_branch.last_revision()
1121
1122+ @staticmethod
1123+ def composeWebhookPayload(branch, old_revid, new_revid):
1124+ return {
1125+ "bzr_branch_path": branch.shortened_path,
1126+ "old": {"revision_id": old_revid},
1127+ "new": {"revision_id": new_revid},
1128+ }
1129+
1130
1131 class IRevisionsRemoved(IObjectEvent):
1132 """Revisions have been removed from the branch."""
1133
1134=== modified file 'lib/lp/codehosting/scanner/tests/test_bzrsync.py'
1135--- lib/lp/codehosting/scanner/tests/test_bzrsync.py 2013-07-04 07:58:00 +0000
1136+++ lib/lp/codehosting/scanner/tests/test_bzrsync.py 2015-09-29 15:54:57 +0000
1137@@ -1,6 +1,6 @@
1138 #!/usr/bin/python
1139 #
1140-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
1141+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
1142 # GNU Affero General Public License version 3 (see the file LICENSE).
1143
1144 import datetime
1145@@ -17,6 +17,11 @@
1146 from fixtures import TempDir
1147 import pytz
1148 from storm.locals import Store
1149+from testtools.matchers import (
1150+ Equals,
1151+ MatchesDict,
1152+ MatchesStructure,
1153+ )
1154 from twisted.python.util import mergeFunctionMetadata
1155 from zope.component import getUtility
1156 from zope.security.proxy import removeSecurityProxy
1157@@ -44,6 +49,7 @@
1158 from lp.codehosting.scanner.bzrsync import BzrSync
1159 from lp.services.config import config
1160 from lp.services.database.interfaces import IStore
1161+from lp.services.features.testing import FeatureFixture
1162 from lp.services.osutils import override_environ
1163 from lp.testing import TestCaseWithFactory
1164 from lp.testing.dbuser import (
1165@@ -743,6 +749,32 @@
1166 self.assertEqual(False, recipe.is_stale)
1167
1168
1169+class TestTriggerWebhooks(BzrSyncTestCase):
1170+ """Test triggering of webhooks."""
1171+
1172+ def test_triggers_webhooks(self):
1173+ # On tip change, any relevant webhooks are triggered.
1174+ self.useFixture(FeatureFixture({"code.bzr.webhooks.enabled": "on"}))
1175+ self.syncAndCount()
1176+ old_revid = self.db_branch.last_scanned_id
1177+ with dbuser(config.launchpad.dbuser):
1178+ hook = self.factory.makeWebhook(
1179+ target=self.db_branch, event_types=["bzr:push:0.1"])
1180+ self.commitRevision()
1181+ new_revid = self.bzr_branch.last_revision()
1182+ self.makeBzrSync(self.db_branch).syncBranchAndClose()
1183+ delivery = hook.deliveries.one()
1184+ self.assertThat(
1185+ delivery,
1186+ MatchesStructure(
1187+ event_type=Equals("bzr:push:0.1"),
1188+ payload=MatchesDict({
1189+ "bzr_branch_path": Equals(self.db_branch.shortened_path),
1190+ "old": Equals({"revision_id": old_revid}),
1191+ "new": Equals({"revision_id": new_revid}),
1192+ })))
1193+
1194+
1195 class TestRevisionProperty(BzrSyncTestCase):
1196 """Tests for storting revision properties."""
1197
1198
1199=== modified file 'lib/lp/registry/browser/productseries.py'
1200--- lib/lp/registry/browser/productseries.py 2015-07-15 03:58:56 +0000
1201+++ lib/lp/registry/browser/productseries.py 2015-09-29 15:54:57 +0000
1202@@ -417,7 +417,8 @@
1203 @property
1204 def long_bzr_identity(self):
1205 """The bzr identity of the branch including the unique_name."""
1206- return self.context.branch.branchIdentities()[-1][0]
1207+ lp_prefix = config.codehosting.bzr_lp_prefix
1208+ return lp_prefix + self.context.branch.getBranchIdentities()[-1][0]
1209
1210 @property
1211 def is_obsolete(self):
1212
1213=== modified file 'lib/lp/services/webhooks/client.py'
1214--- lib/lp/services/webhooks/client.py 2015-09-01 06:03:55 +0000
1215+++ lib/lp/services/webhooks/client.py 2015-09-29 15:54:57 +0000
1216@@ -1,7 +1,7 @@
1217 # Copyright 2015 Canonical Ltd. This software is licensed under the
1218 # GNU Affero General Public License version 3 (see the file LICENSE).
1219
1220-"""Communication with the Git hosting service."""
1221+"""Communication with webhook delivery endpoints."""
1222
1223 __metaclass__ = type
1224 __all__ = [
1225
1226=== modified file 'lib/lp/services/webhooks/interfaces.py'
1227--- lib/lp/services/webhooks/interfaces.py 2015-09-09 06:11:43 +0000
1228+++ lib/lp/services/webhooks/interfaces.py 2015-09-29 15:54:57 +0000
1229@@ -71,13 +71,14 @@
1230
1231
1232 WEBHOOK_EVENT_TYPES = {
1233+ "bzr:push:0.1": "Bazaar push",
1234 "git:push:0.1": "Git push",
1235 }
1236
1237
1238 @error_status(httplib.UNAUTHORIZED)
1239 class WebhookFeatureDisabled(Exception):
1240- """Only certain users can create new Git repositories."""
1241+ """Only certain users can create new webhooks."""
1242
1243 def __init__(self):
1244 Exception.__init__(
1245
1246=== modified file 'lib/lp/services/webhooks/model.py'
1247--- lib/lp/services/webhooks/model.py 2015-09-09 06:11:43 +0000
1248+++ lib/lp/services/webhooks/model.py 2015-09-29 15:54:57 +0000
1249@@ -93,6 +93,9 @@
1250 git_repository_id = Int(name='git_repository')
1251 git_repository = Reference(git_repository_id, 'GitRepository.id')
1252
1253+ branch_id = Int(name='branch')
1254+ branch = Reference(branch_id, 'Branch.id')
1255+
1256 registrant_id = Int(name='registrant', allow_none=False)
1257 registrant = Reference(registrant_id, 'Person.id')
1258 date_created = DateTime(tzinfo=utc, allow_none=False)
1259@@ -108,6 +111,8 @@
1260 def target(self):
1261 if self.git_repository is not None:
1262 return self.git_repository
1263+ elif self.branch is not None:
1264+ return self.branch
1265 else:
1266 raise AssertionError("No target.")
1267
1268@@ -159,10 +164,13 @@
1269
1270 def new(self, target, registrant, delivery_url, event_types, active,
1271 secret):
1272+ from lp.code.interfaces.branch import IBranch
1273 from lp.code.interfaces.gitrepository import IGitRepository
1274 hook = Webhook()
1275 if IGitRepository.providedBy(target):
1276 hook.git_repository = target
1277+ elif IBranch.providedBy(target):
1278+ hook.branch = target
1279 else:
1280 raise AssertionError("Unsupported target: %r" % (target,))
1281 hook.registrant = registrant
1282@@ -184,9 +192,12 @@
1283 return IStore(Webhook).get(Webhook, id)
1284
1285 def findByTarget(self, target):
1286+ from lp.code.interfaces.branch import IBranch
1287 from lp.code.interfaces.gitrepository import IGitRepository
1288 if IGitRepository.providedBy(target):
1289 target_filter = Webhook.git_repository == target
1290+ elif IBranch.providedBy(target):
1291+ target_filter = Webhook.branch == target
1292 else:
1293 raise AssertionError("Unsupported target: %r" % (target,))
1294 return IStore(Webhook).find(Webhook, target_filter).order_by(
1295
1296=== modified file 'lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt'
1297--- lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt 2015-08-06 00:46:39 +0000
1298+++ lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt 2015-09-29 15:54:57 +0000
1299@@ -18,8 +18,8 @@
1300 <div>
1301 <div class="beta" style="display: inline">
1302 <img class="beta" alt="[BETA]" src="/@@/beta" /></div>
1303- The only currently supported events are Git pushes. We'll be
1304- rolling out webhooks for more soon.
1305+ The only currently supported events are Git and Bazaar pushes. We'll
1306+ be rolling out webhooks for more soon.
1307 </div>
1308 <ul class="horizontal">
1309 <li>
1310
1311=== modified file 'lib/lp/services/webhooks/tests/test_browser.py'
1312--- lib/lp/services/webhooks/tests/test_browser.py 2015-08-10 05:56:25 +0000
1313+++ lib/lp/services/webhooks/tests/test_browser.py 2015-09-29 15:54:57 +0000
1314@@ -27,6 +27,7 @@
1315 from lp.testing.matchers import HasQueryCount
1316 from lp.testing.views import create_view
1317
1318+
1319 breadcrumbs_tag = soupmatchers.Tag(
1320 'breadcrumbs', 'ol', attrs={'class': 'breadcrumbs'})
1321 webhooks_page_crumb_tag = soupmatchers.Tag(
1322@@ -47,12 +48,28 @@
1323 'batch nav links', 'td', attrs={'class': 'batch-navigation-links'})
1324
1325
1326+class GitRepositoryTestHelpers:
1327+
1328+ event_type = "git:push:0.1"
1329+
1330+ def makeTarget(self):
1331+ return self.factory.makeGitRepository()
1332+
1333+
1334+class BranchTestHelpers:
1335+
1336+ event_type = "bzr:push:0.1"
1337+
1338+ def makeTarget(self):
1339+ return self.factory.makeBranch()
1340+
1341+
1342 class WebhookTargetViewTestHelpers:
1343
1344 def setUp(self):
1345 super(WebhookTargetViewTestHelpers, self).setUp()
1346 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
1347- self.target = self.factory.makeGitRepository()
1348+ self.target = self.makeTarget()
1349 self.owner = self.target.owner
1350 login_person(self.owner)
1351
1352@@ -65,7 +82,7 @@
1353 return view
1354
1355
1356-class TestWebhooksView(WebhookTargetViewTestHelpers, TestCaseWithFactory):
1357+class TestWebhooksViewBase(WebhookTargetViewTestHelpers):
1358
1359 layer = DatabaseFunctionalLayer
1360
1361@@ -125,7 +142,19 @@
1362 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
1363
1364
1365-class TestWebhookAddView(WebhookTargetViewTestHelpers, TestCaseWithFactory):
1366+class TestWebhooksViewGitRepository(
1367+ TestWebhooksViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
1368+
1369+ pass
1370+
1371+
1372+class TestWebhooksViewBranch(
1373+ TestWebhooksViewBase, BranchTestHelpers, TestCaseWithFactory):
1374+
1375+ pass
1376+
1377+
1378+class TestWebhookAddViewBase(WebhookTargetViewTestHelpers):
1379
1380 layer = DatabaseFunctionalLayer
1381
1382@@ -150,7 +179,7 @@
1383 form={
1384 "field.delivery_url": "http://example.com/test",
1385 "field.active": "on", "field.event_types-empty-marker": "1",
1386- "field.event_types": "git:push:0.1",
1387+ "field.event_types": self.event_type,
1388 "field.actions.new": "Add webhook"})
1389 self.assertEqual([], view.errors)
1390 hook = self.target.webhooks.one()
1391@@ -161,7 +190,7 @@
1392 registrant=self.owner,
1393 delivery_url="http://example.com/test",
1394 active=True,
1395- event_types=["git:push:0.1"]))
1396+ event_types=[self.event_type]))
1397
1398 def test_rejects_bad_scheme(self):
1399 transaction.commit()
1400@@ -176,12 +205,24 @@
1401 self.assertIs(None, self.target.webhooks.one())
1402
1403
1404+class TestWebhookAddViewGitRepository(
1405+ TestWebhookAddViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
1406+
1407+ pass
1408+
1409+
1410+class TestWebhookAddViewBranch(
1411+ TestWebhookAddViewBase, BranchTestHelpers, TestCaseWithFactory):
1412+
1413+ pass
1414+
1415+
1416 class WebhookViewTestHelpers:
1417
1418 def setUp(self):
1419 super(WebhookViewTestHelpers, self).setUp()
1420 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
1421- self.target = self.factory.makeGitRepository()
1422+ self.target = self.makeTarget()
1423 self.owner = self.target.owner
1424 self.webhook = self.factory.makeWebhook(
1425 target=self.target, delivery_url=u'http://example.com/original')
1426@@ -196,7 +237,7 @@
1427 return view
1428
1429
1430-class TestWebhookView(WebhookViewTestHelpers, TestCaseWithFactory):
1431+class TestWebhookViewBase(WebhookViewTestHelpers):
1432
1433 layer = DatabaseFunctionalLayer
1434
1435@@ -249,7 +290,19 @@
1436 event_types=[]))
1437
1438
1439-class TestWebhookDeleteView(WebhookViewTestHelpers, TestCaseWithFactory):
1440+class TestWebhookViewGitRepository(
1441+ TestWebhookViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
1442+
1443+ pass
1444+
1445+
1446+class TestWebhookViewBranch(
1447+ TestWebhookViewBase, BranchTestHelpers, TestCaseWithFactory):
1448+
1449+ pass
1450+
1451+
1452+class TestWebhookDeleteViewBase(WebhookViewTestHelpers):
1453
1454 layer = DatabaseFunctionalLayer
1455
1456@@ -281,3 +334,15 @@
1457 form={"field.actions.delete": "Delete webhook"})
1458 self.assertEqual([], view.errors)
1459 self.assertIs(None, self.target.webhooks.one())
1460+
1461+
1462+class TestWebhookDeleteViewGitRepository(
1463+ TestWebhookDeleteViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
1464+
1465+ pass
1466+
1467+
1468+class TestWebhookDeleteViewBranch(
1469+ TestWebhookDeleteViewBase, BranchTestHelpers, TestCaseWithFactory):
1470+
1471+ pass
1472
1473=== modified file 'lib/lp/services/webhooks/tests/test_model.py'
1474--- lib/lp/services/webhooks/tests/test_model.py 2015-08-10 07:36:52 +0000
1475+++ lib/lp/services/webhooks/tests/test_model.py 2015-09-29 15:54:57 +0000
1476@@ -115,17 +115,17 @@
1477 expected_set_permissions, checker.set_permissions, 'set')
1478
1479
1480-class TestWebhookSet(TestCaseWithFactory):
1481+class TestWebhookSetBase:
1482
1483 layer = DatabaseFunctionalLayer
1484
1485 def test_new(self):
1486- target = self.factory.makeGitRepository()
1487+ target = self.makeTarget()
1488 login_person(target.owner)
1489 person = self.factory.makePerson()
1490 hook = getUtility(IWebhookSet).new(
1491- target, person, u'http://path/to/something', ['git:push'], True,
1492- u'sekrit')
1493+ target, person, u'http://path/to/something', [self.event_type],
1494+ True, u'sekrit')
1495 Store.of(hook).flush()
1496 self.assertEqual(target, hook.target)
1497 self.assertEqual(person, hook.registrant)
1498@@ -134,7 +134,7 @@
1499 self.assertEqual(u'http://path/to/something', hook.delivery_url)
1500 self.assertEqual(True, hook.active)
1501 self.assertEqual(u'sekrit', hook.secret)
1502- self.assertEqual(['git:push'], hook.event_types)
1503+ self.assertEqual([self.event_type], hook.event_types)
1504
1505 def test_getByID(self):
1506 hook1 = self.factory.makeWebhook()
1507@@ -148,8 +148,8 @@
1508 None, getUtility(IWebhookSet).getByID(1234))
1509
1510 def test_findByTarget(self):
1511- target1 = self.factory.makeGitRepository()
1512- target2 = self.factory.makeGitRepository()
1513+ target1 = self.makeTarget()
1514+ target2 = self.makeTarget()
1515 for target, name in ((target1, 'one'), (target2, 'two')):
1516 for i in range(3):
1517 self.factory.makeWebhook(
1518@@ -168,7 +168,7 @@
1519 getUtility(IWebhookSet).findByTarget(target2)])
1520
1521 def test_delete(self):
1522- target = self.factory.makeGitRepository()
1523+ target = self.makeTarget()
1524 login_person(target.owner)
1525 hooks = []
1526 for i in range(3):
1527@@ -195,21 +195,21 @@
1528
1529 def test_trigger(self):
1530 owner = self.factory.makePerson()
1531- target1 = self.factory.makeGitRepository(owner=owner)
1532- target2 = self.factory.makeGitRepository(owner=owner)
1533+ target1 = self.makeTarget(owner=owner)
1534+ target2 = self.makeTarget(owner=owner)
1535 hook1a = self.factory.makeWebhook(
1536 target=target1, event_types=[])
1537 hook1b = self.factory.makeWebhook(
1538- target=target1, event_types=['git:push:0.1'])
1539+ target=target1, event_types=[self.event_type])
1540 hook2a = self.factory.makeWebhook(
1541- target=target2, event_types=['git:push:0.1'])
1542+ target=target2, event_types=[self.event_type])
1543 hook2b = self.factory.makeWebhook(
1544- target=target2, event_types=['git:push:0.1'], active=False)
1545+ target=target2, event_types=[self.event_type], active=False)
1546
1547 # Only webhooks subscribed to the relevant target and event type
1548 # are triggered.
1549 getUtility(IWebhookSet).trigger(
1550- target1, 'git:push:0.1', {'some': 'payload'})
1551+ target1, self.event_type, {'some': 'payload'})
1552 with admin_logged_in():
1553 self.assertThat(list(hook1a.deliveries), HasLength(0))
1554 self.assertThat(list(hook1b.deliveries), HasLength(1))
1555@@ -220,7 +220,7 @@
1556
1557 # Disabled webhooks aren't triggered.
1558 getUtility(IWebhookSet).trigger(
1559- target2, 'git:push:0.1', {'other': 'payload'})
1560+ target2, self.event_type, {'other': 'payload'})
1561 with admin_logged_in():
1562 self.assertThat(list(hook1a.deliveries), HasLength(0))
1563 self.assertThat(list(hook1b.deliveries), HasLength(1))
1564@@ -228,3 +228,19 @@
1565 self.assertThat(list(hook2b.deliveries), HasLength(0))
1566 delivery = hook2a.deliveries.one()
1567 self.assertEqual(delivery.payload, {'other': 'payload'})
1568+
1569+
1570+class TestWebhookSetGitRepository(TestWebhookSetBase, TestCaseWithFactory):
1571+
1572+ event_type = 'git:push:0.1'
1573+
1574+ def makeTarget(self, owner=None):
1575+ return self.factory.makeGitRepository(owner=owner)
1576+
1577+
1578+class TestWebhookSetBranch(TestWebhookSetBase, TestCaseWithFactory):
1579+
1580+ event_type = 'bzr:push:0.1'
1581+
1582+ def makeTarget(self, owner=None):
1583+ return self.factory.makeBranch(owner=owner)
1584
1585=== modified file 'lib/lp/services/webhooks/tests/test_webservice.py'
1586--- lib/lp/services/webhooks/tests/test_webservice.py 2015-09-09 06:11:43 +0000
1587+++ lib/lp/services/webhooks/tests/test_webservice.py 2015-09-29 15:54:57 +0000
1588@@ -260,12 +260,12 @@
1589 self.assertIs(None, representation['date_first_sent'])
1590
1591
1592-class TestWebhookTarget(TestCaseWithFactory):
1593+class TestWebhookTargetBase:
1594 layer = DatabaseFunctionalLayer
1595
1596 def setUp(self):
1597- super(TestWebhookTarget, self).setUp()
1598- self.target = self.factory.makeGitRepository()
1599+ super(TestWebhookTargetBase, self).setUp()
1600+ self.target = self.makeTarget()
1601 self.owner = self.target.owner
1602 self.target_url = api_url(self.target)
1603 self.webservice = webservice_for_person(
1604@@ -309,13 +309,13 @@
1605 response = self.webservice.named_post(
1606 self.target_url, 'newWebhook',
1607 delivery_url='http://example.com/ep',
1608- event_types=['git:push:0.1'], api_version='devel')
1609+ event_types=[self.event_type], api_version='devel')
1610 self.assertEqual(201, response.status)
1611
1612 representation = self.webservice.get(
1613 self.target_url + '/webhooks', api_version='devel').jsonBody()
1614 self.assertContentEqual(
1615- [('http://example.com/ep', ['git:push:0.1'], True)],
1616+ [('http://example.com/ep', [self.event_type], True)],
1617 [(entry['delivery_url'], entry['event_types'], entry['active'])
1618 for entry in representation['entries']])
1619
1620@@ -323,8 +323,9 @@
1621 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
1622 response = self.webservice.named_post(
1623 self.target_url, 'newWebhook',
1624- delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
1625- secret='sekrit', api_version='devel')
1626+ delivery_url='http://example.com/ep',
1627+ event_types=[self.event_type], secret='sekrit',
1628+ api_version='devel')
1629 self.assertEqual(201, response.status)
1630
1631 # The secret is set, but cannot be read back through the API.
1632@@ -339,16 +340,33 @@
1633 webservice = LaunchpadWebServiceCaller()
1634 response = webservice.named_post(
1635 self.target_url, 'newWebhook',
1636- delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
1637- api_version='devel')
1638+ delivery_url='http://example.com/ep',
1639+ event_types=[self.event_type], api_version='devel')
1640 self.assertEqual(401, response.status)
1641 self.assertIn('launchpad.Edit', response.body)
1642
1643 def test_newWebhook_feature_flag_guard(self):
1644 response = self.webservice.named_post(
1645 self.target_url, 'newWebhook',
1646- delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
1647- api_version='devel')
1648+ delivery_url='http://example.com/ep',
1649+ event_types=[self.event_type], api_version='devel')
1650 self.assertEqual(401, response.status)
1651 self.assertEqual(
1652 'This webhook feature is not available yet.', response.body)
1653+
1654+
1655+class TestWebhookTargetGitRepository(
1656+ TestWebhookTargetBase, TestCaseWithFactory):
1657+
1658+ event_type = 'git:push:0.1'
1659+
1660+ def makeTarget(self):
1661+ return self.factory.makeGitRepository()
1662+
1663+
1664+class TestWebhookTargetBranch(TestWebhookTargetBase, TestCaseWithFactory):
1665+
1666+ event_type = 'bzr:push:0.1'
1667+
1668+ def makeTarget(self):
1669+ return self.factory.makeBranch()