Merge lp:~rockstar/launchpad/merge-queues-api into lp:launchpad/db-devel

Proposed by Paul Hummer
Status: Merged
Merged at revision: 9912
Proposed branch: lp:~rockstar/launchpad/merge-queues-api
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~rockstar/launchpad/merge-queues-model
Diff against target: 535 lines (+248/-67)
12 files modified
lib/lp/code/browser/configure.zcml (+6/-0)
lib/lp/code/configure.zcml (+9/-4)
lib/lp/code/interfaces/branch.py (+32/-16)
lib/lp/code/interfaces/branchmergequeue.py (+60/-39)
lib/lp/code/interfaces/webservice.py (+2/-0)
lib/lp/code/model/tests/test_branch.py (+43/-0)
lib/lp/code/model/tests/test_branchmergequeue.py (+65/-1)
lib/lp/code/stories/webservice/xx-branch.txt (+2/-0)
lib/lp/registry/browser/person.py (+5/-0)
lib/lp/registry/interfaces/person.py (+3/-0)
lib/lp/registry/model/person.py (+7/-0)
lib/lp/testing/factory.py (+14/-7)
To merge this branch: bzr merge lp:~rockstar/launchpad/merge-queues-api
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) Approve
Review via email: mp+38946@code.launchpad.net

Description of the change

This branch exposes the new branch merge queue models through the API. Since there's no UI yet, the URL resolving code also needed to be added. While doing this, I also added some more stuff to the factory method for making merge queues.

There's a lot of lint, but most of it isn't something I introduced. I could fix the lint, but it would make the diff hard to read. If you'd like me to, I'll fix the lint, but I think you'd rather me do that after the review. :)

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Hi Paul,

Just a few minor (stylistic) comments. I'm looking forward to being able
to use the merge queues.

On Wed, 2010-10-20 at 15:37 +0000, Paul Hummer wrote:
> There's a lot of lint, but most of it isn't something I introduced. I
> could fix the lint, but it would make the diff hard to read. If you'd
> like me to, I'll fix the lint, but I think you'd rather me do that
> after the review. :)
Thanks for waiting with those fixes. Some lint cleanup would be nice
though. :-)

> differences between files attachment (review-diff.txt)

> === modified file 'lib/lp/code/interfaces/branch.py'
> --- lib/lp/code/interfaces/branch.py 2010-10-20 15:36:54 +0000
> +++ lib/lp/code/interfaces/branch.py 2010-10-20 15:36:56 +0000
> @@ -990,18 +990,6 @@
> required=False, readonly=True,
> vocabulary=ControlFormat))
>
> - merge_queue = Reference(
> - title=_('Branch Merge Queue'),
> - schema=IBranchMergeQueue, required=False, readonly=True,
> - description=_(
> - "The branch merge queue that manages merges for this branch."))
> -
> - merge_queue_config = TextLine(
> - title=_('Name'), required=True, constraint=branch_name_validator,
> - description=_(
> - "A JSON string of configuration values to send to a branch"
> - "merge robot."))
This indentation seems odd - is there a particular reason for not having
the title= and description= lines start at the same column for
merge_queue_config as for merge_queue ?

> === modified file 'lib/lp/registry/model/person.py'
> --- lib/lp/registry/model/person.py 2010-10-18 21:18:03 +0000
> +++ lib/lp/registry/model/person.py 2010-10-20 15:36:56 +0000
> @@ -2661,6 +2661,13 @@
> SourcePackageRecipe, SourcePackageRecipe.owner == self,
> SourcePackageRecipe.name == name).one()
>
> + def getMergeQueue(self, name):
> + from lp.code.model.branchmergequeue import BranchMergeQueue
> + return Store.of(self).find(
> + BranchMergeQueue,
> + BranchMergeQueue.owner == self,
> + BranchMergeQueue.name == unicode(name)).one()
> +
^^ It would be nice to have a "See IPerson" comment here, if only for
consistency with the rest of the file.

  review approve

Cheers,

Jelmer

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/configure.zcml'
2--- lib/lp/code/browser/configure.zcml 2010-10-06 18:53:53 +0000
3+++ lib/lp/code/browser/configure.zcml 2010-10-20 17:56:59 +0000
4@@ -1326,4 +1326,10 @@
5 permission="zope.Public"/>
6 </facet>
7
8+ <browser:url
9+ for="lp.code.interfaces.branchmergequeue.IBranchMergeQueue"
10+ attribute_to_parent="owner"
11+ path_expression="string:+merge-queues/${name}"
12+ rootsite="code" />
13+
14 </configure>
15
16=== modified file 'lib/lp/code/configure.zcml'
17--- lib/lp/code/configure.zcml 2010-10-18 21:18:03 +0000
18+++ lib/lp/code/configure.zcml 2010-10-20 17:56:59 +0000
19@@ -22,6 +22,11 @@
20 provides="zope.publisher.interfaces.browser.IDefaultBrowserLayer"
21 name="code" />
22
23+ <!-- Branch Merge Queues -->
24+ <class class="lp.code.model.branchmergequeue.BranchMergeQueue">
25+ <allow interface="lp.code.interfaces.branchmergequeue.IBranchMergeQueue" />
26+ </class>
27+
28 <class class="lp.code.model.codereviewvote.CodeReviewVoteReference">
29 <allow interface="lp.code.interfaces.codereviewvote.ICodeReviewVoteReferencePublic"/>
30 <require
31@@ -433,18 +438,18 @@
32 lp.code.interfaces.branch.IBranchAnyone
33 lp.code.interfaces.branch.IBranchEditableAttributes
34 lp.code.interfaces.branch.IBranchPublic
35- lp.code.interfaces.branch.IBranchView
36- "/>
37+ lp.code.interfaces.branch.IBranchView"
38+ attributes="merge_queue merge_queue_config"/>
39 <require
40 permission="launchpad.Edit"
41 interface="lp.code.interfaces.branch.IBranchEdit"
42 set_schema="lp.code.interfaces.branch.IBranchEditableAttributes"
43- attributes="setPrivate"
44+ attributes="setPrivate addToQueue setMergeQueueConfig"
45 set_attributes="branch_format control_format repository_format
46 branch_type
47 last_scanned last_scanned_id
48 last_mirrored last_mirrored_id next_mirror_time
49- revision_count merge_queue mirror_failures
50+ revision_count mirror_failures
51 stacked_on mirror_status_message"/>
52 <require
53 permission="launchpad.AnyPerson"
54
55=== modified file 'lib/lp/code/interfaces/branch.py'
56--- lib/lp/code/interfaces/branch.py 2010-10-20 17:56:58 +0000
57+++ lib/lp/code/interfaces/branch.py 2010-10-20 17:56:59 +0000
58@@ -990,18 +990,6 @@
59 required=False, readonly=True,
60 vocabulary=ControlFormat))
61
62- merge_queue = Reference(
63- title=_('Branch Merge Queue'),
64- schema=IBranchMergeQueue, required=False, readonly=True,
65- description=_(
66- "The branch merge queue that manages merges for this branch."))
67-
68- merge_queue_config = TextLine(
69- title=_('Name'), required=True, constraint=branch_name_validator,
70- description=_(
71- "A JSON string of configuration values to send to a branch"
72- "merge robot."))
73-
74
75 class IBranchEdit(Interface):
76 """IBranch attributes that require launchpad.Edit permission."""
77@@ -1031,9 +1019,9 @@
78 is set, the branch gets moved into the junk namespace of the branch
79 owner.
80
81- :raise: `BranchTargetError` if both project and source_package are set,
82- or if either the project or source_package fail to be adapted to an
83- IBranchTarget.
84+ :raise: `BranchTargetError` if both project and source_package are
85+ set, or if either the project or source_package fail to be
86+ adapted to an IBranchTarget.
87 """
88
89 def requestUpgrade():
90@@ -1075,6 +1063,30 @@
91 :raise: CannotDeleteBranch if the branch cannot be deleted.
92 """
93
94+
95+class IMergeQueueable(Interface):
96+ """An interface for branches that can be queued."""
97+
98+ merge_queue = exported(
99+ Reference(
100+ title=_('Branch Merge Queue'),
101+ schema=IBranchMergeQueue, required=False, readonly=True,
102+ description=_(
103+ "The branch merge queue that manages merges for this "
104+ "branch.")))
105+
106+ merge_queue_config = exported(
107+ TextLine(
108+ title=_('Name'), required=True, readonly=True,
109+ description=_(
110+ "A JSON string of configuration values to send to a "
111+ "branch merge robot.")))
112+
113+ @mutator_for(merge_queue)
114+ @operation_parameters(
115+ queue=Reference(title=_('Branch Merge Queue'),
116+ schema=IBranchMergeQueue))
117+ @export_write_operation()
118 def addToQueue(queue):
119 """Add this branch to a specified queue.
120
121@@ -1083,6 +1095,10 @@
122 :param queue: The branch merge queue that will manage the branch.
123 """
124
125+ @mutator_for(merge_queue_config)
126+ @operation_parameters(
127+ config=TextLine(title=_("A JSON string of config values.")))
128+ @export_write_operation()
129 def setMergeQueueConfig(config):
130 """Set the merge_queue_config property.
131
132@@ -1094,7 +1110,7 @@
133
134
135 class IBranch(IBranchPublic, IBranchView, IBranchEdit,
136- IBranchEditableAttributes, IBranchAnyone):
137+ IBranchEditableAttributes, IBranchAnyone, IMergeQueueable):
138 """A Bazaar branch."""
139
140 # Mark branches as exported entries for the Launchpad API.
141
142=== modified file 'lib/lp/code/interfaces/branchmergequeue.py'
143--- lib/lp/code/interfaces/branchmergequeue.py 2010-10-20 17:56:58 +0000
144+++ lib/lp/code/interfaces/branchmergequeue.py 2010-10-20 17:56:59 +0000
145@@ -10,6 +10,13 @@
146 'IBranchMergeQueueSource',
147 ]
148
149+from lazr.restful.declarations import (
150+ export_as_webservice_entry,
151+ export_write_operation,
152+ exported,
153+ mutator_for,
154+ operation_parameters,
155+ )
156 from lazr.restful.fields import (
157 CollectionField,
158 Reference,
159@@ -32,47 +39,61 @@
160 class IBranchMergeQueue(Interface):
161 """An interface for managing branch merges."""
162
163+ export_as_webservice_entry()
164+
165 id = Int(title=_('ID'), readonly=True, required=True)
166
167- registrant = PublicPersonChoice(
168- title=_("The user that registered the branch."),
169- required=True, readonly=True,
170- vocabulary='ValidPersonOrTeam')
171-
172- owner = PersonChoice(
173- title=_('Owner'),
174- required=True, readonly=True,
175- vocabulary='UserTeamsParticipationPlusSelf',
176- description=_("The owner of the merge queue."))
177-
178- name = TextLine(
179- title=_('Name'), required=True,
180- description=_(
181- "Keep very short, unique, and descriptive, because it will "
182- "be used in URLs. "
183- "Examples: main, devel, release-1.0, gnome-vfs."))
184-
185- description = Text(
186- title=_('Description'), required=False,
187- description=_(
188- 'A short description of the purpose of this merge queue.'))
189-
190- configuration = TextLine(
191- title=_('Configuration'), required=False,
192- description=_(
193- "A JSON string of configuration values."))
194-
195- date_created = Datetime(
196- title=_('Date Created'),
197- required=True,
198- readonly=True)
199-
200- branches = CollectionField(
201- title=_('Dependent Branches'),
202- description=_('A collection of branches that this queue manages.'),
203- readonly=True,
204- value_type=Reference(Interface))
205-
206+ registrant = exported(
207+ PublicPersonChoice(
208+ title=_("The user that registered the branch."),
209+ required=True, readonly=True,
210+ vocabulary='ValidPersonOrTeam'))
211+
212+ owner = exported(
213+ PersonChoice(
214+ title=_('Owner'),
215+ required=True, readonly=True,
216+ vocabulary='UserTeamsParticipationPlusSelf',
217+ description=_("The owner of the merge queue.")))
218+
219+ name = exported(
220+ TextLine(
221+ title=_('Name'), required=True,
222+ description=_(
223+ "Keep very short, unique, and descriptive, because it will "
224+ "be used in URLs. "
225+ "Examples: main, devel, release-1.0, gnome-vfs.")))
226+
227+ description = exported(
228+ Text(
229+ title=_('Description'), required=False,
230+ description=_(
231+ 'A short description of the purpose of this merge queue.')))
232+
233+ configuration = exported(
234+ TextLine(
235+ title=_('Configuration'), required=False, readonly=True,
236+ description=_(
237+ "A JSON string of configuration values.")))
238+
239+ date_created = exported(
240+ Datetime(
241+ title=_('Date Created'),
242+ required=True,
243+ readonly=True))
244+
245+ branches = exported(
246+ CollectionField(
247+ title=_('Dependent Branches'),
248+ description=_(
249+ 'A collection of branches that this queue manages.'),
250+ readonly=True,
251+ value_type=Reference(Interface)))
252+
253+ @mutator_for(configuration)
254+ @operation_parameters(
255+ config=TextLine(title=_("A JSON string of configuration values.")))
256+ @export_write_operation()
257 def setMergeQueueConfig(config):
258 """Set the JSON string configuration of the merge queue.
259
260
261=== modified file 'lib/lp/code/interfaces/webservice.py'
262--- lib/lp/code/interfaces/webservice.py 2010-08-20 20:31:18 +0000
263+++ lib/lp/code/interfaces/webservice.py 2010-10-20 17:56:59 +0000
264@@ -20,6 +20,7 @@
265 IBranchSet,
266 )
267 from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
268+from lp.code.interfaces.branchmergequeue import IBranchMergeQueue
269 from lp.code.interfaces.branchsubscription import IBranchSubscription
270 from lp.code.interfaces.codeimport import ICodeImport
271 from lp.code.interfaces.codereviewcomment import ICodeReviewComment
272@@ -35,3 +36,4 @@
273 )
274
275
276+IBranchMergeQueue['branches'].value_type.schema = IBranch
277
278=== modified file 'lib/lp/code/model/tests/test_branch.py'
279--- lib/lp/code/model/tests/test_branch.py 2010-10-20 17:56:58 +0000
280+++ lib/lp/code/model/tests/test_branch.py 2010-10-20 17:56:59 +0000
281@@ -33,6 +33,7 @@
282 from canonical.launchpad.interfaces.lpstorm import IStore
283 from canonical.launchpad.webapp.interfaces import IOpenLaunchBag
284 from canonical.testing.layers import (
285+ AppServerLayer,
286 DatabaseFunctionalLayer,
287 LaunchpadZopelessLayer,
288 )
289@@ -118,6 +119,7 @@
290 from lp.testing import (
291 ANONYMOUS,
292 celebrity_logged_in,
293+ launchpadlib_for,
294 login,
295 login_person,
296 logout,
297@@ -126,6 +128,7 @@
298 TestCase,
299 TestCaseWithFactory,
300 time_counter,
301+ ws_object,
302 )
303 from lp.testing.factory import LaunchpadObjectFactory
304 from lp.translations.model.translationtemplatesbuildjob import (
305@@ -2744,5 +2747,45 @@
306 config)
307
308
309+class TestWebservice(TestCaseWithFactory):
310+ """Tests for the webservice."""
311+
312+ layer = AppServerLayer
313+
314+ def test_set_merge_queue(self):
315+ """Test that the merge queue can be set properly."""
316+ with person_logged_in(ANONYMOUS):
317+ db_queue = self.factory.makeBranchMergeQueue()
318+ db_branch = self.factory.makeBranch()
319+ launchpad = launchpadlib_for('test', db_branch.owner,
320+ service_root="http://api.launchpad.dev:8085")
321+
322+ configuration = simplejson.dumps({'test': 'make check'})
323+
324+ branch = ws_object(launchpad, db_branch)
325+ queue = ws_object(launchpad, db_queue)
326+ branch.merge_queue = queue
327+ branch.lp_save()
328+
329+ branch2 = ws_object(launchpad, db_branch)
330+ self.assertEqual(branch2.merge_queue, queue)
331+
332+ def test_set_configuration(self):
333+ """Test the mutator for setting configuration."""
334+ with person_logged_in(ANONYMOUS):
335+ db_branch = self.factory.makeBranch()
336+ launchpad = launchpadlib_for('test', db_branch.owner,
337+ service_root="http://api.launchpad.dev:8085")
338+
339+ configuration = simplejson.dumps({'test': 'make check'})
340+
341+ branch = ws_object(launchpad, db_branch)
342+ branch.merge_queue_config = configuration
343+ branch.lp_save()
344+
345+ branch2 = ws_object(launchpad, db_branch)
346+ self.assertEqual(branch2.merge_queue_config, configuration)
347+
348+
349 def test_suite():
350 return TestLoader().loadTestsFromName(__name__)
351
352=== modified file 'lib/lp/code/model/tests/test_branchmergequeue.py'
353--- lib/lp/code/model/tests/test_branchmergequeue.py 2010-10-20 17:56:58 +0000
354+++ lib/lp/code/model/tests/test_branchmergequeue.py 2010-10-20 17:56:59 +0000
355@@ -7,15 +7,23 @@
356
357 import simplejson
358
359+import transaction
360+
361 from canonical.launchpad.interfaces.lpstorm import IStore
362 from canonical.launchpad.webapp.testing import verifyObject
363-from canonical.testing.layers import DatabaseFunctionalLayer
364+from canonical.testing.layers import (
365+ AppServerLayer,
366+ DatabaseFunctionalLayer,
367+ )
368 from lp.code.errors import InvalidMergeQueueConfig
369 from lp.code.interfaces.branchmergequeue import IBranchMergeQueue
370 from lp.code.model.branchmergequeue import BranchMergeQueue
371 from lp.testing import (
372+ ANONYMOUS,
373 person_logged_in,
374+ launchpadlib_for,
375 TestCaseWithFactory,
376+ ws_object,
377 )
378
379
380@@ -89,3 +97,59 @@
381 InvalidMergeQueueConfig,
382 queue.setMergeQueueConfig,
383 'abc')
384+
385+
386+class TestWebservice(TestCaseWithFactory):
387+
388+ layer = AppServerLayer
389+
390+ def test_properties(self):
391+ """Test that the correct properties are exposed."""
392+ with person_logged_in(ANONYMOUS):
393+ name = u'teh-queue'
394+ description = u'Oh hai! I are a queues'
395+ configuration = unicode(simplejson.dumps({'test': 'make check'}))
396+
397+ queuer = self.factory.makePerson()
398+ db_queue = self.factory.makeBranchMergeQueue(
399+ registrant=queuer, owner=queuer, name=name,
400+ description=description,
401+ configuration=configuration)
402+ branch1 = self.factory.makeBranch()
403+ with person_logged_in(branch1.owner):
404+ branch1.addToQueue(db_queue)
405+ branch2 = self.factory.makeBranch()
406+ with person_logged_in(branch2.owner):
407+ branch2.addToQueue(db_queue)
408+ launchpad = launchpadlib_for('test', db_queue.owner,
409+ service_root="http://api.launchpad.dev:8085")
410+ transaction.commit()
411+
412+ queuer = ws_object(launchpad, queuer)
413+ queue = ws_object(launchpad, db_queue)
414+ branch1 = ws_object(launchpad, branch1)
415+ branch2 = ws_object(launchpad, branch2)
416+
417+ self.assertEqual(queue.registrant, queuer)
418+ self.assertEqual(queue.owner, queuer)
419+ self.assertEqual(queue.name, name)
420+ self.assertEqual(queue.description, description)
421+ self.assertEqual(queue.configuration, configuration)
422+ self.assertEqual(queue.date_created, db_queue.date_created)
423+ self.assertEqual(len(queue.branches), 2)
424+
425+ def test_set_configuration(self):
426+ """Test the mutator for setting configuration."""
427+ with person_logged_in(ANONYMOUS):
428+ db_queue = self.factory.makeBranchMergeQueue()
429+ launchpad = launchpadlib_for('test', db_queue.owner,
430+ service_root="http://api.launchpad.dev:8085")
431+
432+ configuration = simplejson.dumps({'test': 'make check'})
433+
434+ queue = ws_object(launchpad, db_queue)
435+ queue.configuration = configuration
436+ queue.lp_save()
437+
438+ queue2 = ws_object(launchpad, db_queue)
439+ self.assertEqual(queue2.configuration, configuration)
440
441=== modified file 'lib/lp/code/stories/webservice/xx-branch.txt'
442--- lib/lp/code/stories/webservice/xx-branch.txt 2010-04-09 01:30:22 +0000
443+++ lib/lp/code/stories/webservice/xx-branch.txt 2010-10-20 17:56:59 +0000
444@@ -123,6 +123,8 @@
445 last_scanned_id: None
446 lifecycle_status: u'Development'
447 linked_bugs_collection_link: u'http://.../~eric/fooix/trunk/linked_bugs'
448+ merge_queue_config: None
449+ merge_queue_link: None
450 mirror_status_message: None
451 name: u'trunk'
452 owner_link: u'.../~eric'
453
454=== modified file 'lib/lp/registry/browser/person.py'
455--- lib/lp/registry/browser/person.py 2010-10-14 20:20:47 +0000
456+++ lib/lp/registry/browser/person.py 2010-10-20 17:56:59 +0000
457@@ -543,6 +543,11 @@
458 """Traverse to this person's recipes."""
459 return self.context.getRecipe(name)
460
461+ @stepthrough('+merge-queues')
462+ def traverse_merge_queue(self, name):
463+ """Traverse to this person's merge queues."""
464+ return self.context.getMergeQueue(name)
465+
466
467 class TeamNavigation(PersonNavigation):
468
469
470=== modified file 'lib/lp/registry/interfaces/person.py'
471--- lib/lp/registry/interfaces/person.py 2010-10-07 14:03:32 +0000
472+++ lib/lp/registry/interfaces/person.py 2010-10-20 17:56:59 +0000
473@@ -896,6 +896,9 @@
474 def getRecipe(name):
475 """Return the person's recipe with the given name."""
476
477+ def getMergeQueue(name):
478+ """Return the person's merge queue with the given name."""
479+
480 @call_with(requester=REQUEST_USER)
481 @export_read_operation()
482 def getArchiveSubscriptionURLs(requester):
483
484=== modified file 'lib/lp/registry/model/person.py'
485--- lib/lp/registry/model/person.py 2010-10-18 21:18:03 +0000
486+++ lib/lp/registry/model/person.py 2010-10-20 17:56:59 +0000
487@@ -2661,6 +2661,13 @@
488 SourcePackageRecipe, SourcePackageRecipe.owner == self,
489 SourcePackageRecipe.name == name).one()
490
491+ def getMergeQueue(self, name):
492+ from lp.code.model.branchmergequeue import BranchMergeQueue
493+ return Store.of(self).find(
494+ BranchMergeQueue,
495+ BranchMergeQueue.owner == self,
496+ BranchMergeQueue.name == unicode(name)).one()
497+
498 def isUploader(self, distribution):
499 """See `IPerson`."""
500 permissions = getUtility(IArchivePermissionSet).componentsForUploader(
501
502=== modified file 'lib/lp/testing/factory.py'
503--- lib/lp/testing/factory.py 2010-10-20 17:56:58 +0000
504+++ lib/lp/testing/factory.py 2010-10-20 17:56:59 +0000
505@@ -1098,16 +1098,23 @@
506 namespace = target.getNamespace(owner)
507 return namespace.createBranch(branch_type, name, creator)
508
509- def makeBranchMergeQueue(self):
510+ def makeBranchMergeQueue(self, registrant=None, owner=None, name=None,
511+ description=None, configuration=None):
512 """Create a BranchMergeQueue."""
513- name = unicode(self.getUniqueString('queue'))
514- owner = self.makePerson()
515- description = unicode(self.getUniqueString('queue-description'))
516- configuration = unicode(simplejson.dumps({
517- self.getUniqueString('key'): self.getUniqueString('value')}))
518+ if name is None:
519+ name = unicode(self.getUniqueString('queue'))
520+ if owner is None:
521+ owner = self.makePerson()
522+ if registrant is None:
523+ registrant = self.makePerson()
524+ if description is None:
525+ description = unicode(self.getUniqueString('queue-description'))
526+ if configuration is None:
527+ configuration = unicode(simplejson.dumps({
528+ self.getUniqueString('key'): self.getUniqueString('value')}))
529
530 queue = BranchMergeQueue.new(
531- name, owner, owner, description, configuration)
532+ name, registrant, owner, description, configuration)
533 return queue
534
535 def enableDefaultStackingForProduct(self, product, branch=None):

Subscribers

People subscribed via source and target branches

to status/vote changes: