Merge lp:~cjwatson/launchpad/git-recipe-browser-create into lp:launchpad

Proposed by Colin Watson on 2016-01-12
Status: Merged
Merged at revision: 17901
Proposed branch: lp:~cjwatson/launchpad/git-recipe-browser-create
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-recipe-browser-listing
Diff against target: 1107 lines (+346/-152)
17 files modified
lib/lp/code/browser/configure.zcml (+12/-0)
lib/lp/code/browser/gitref.py (+13/-1)
lib/lp/code/browser/gitrepository.py (+10/-1)
lib/lp/code/browser/sourcepackagerecipe.py (+29/-4)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+200/-139)
lib/lp/code/browser/tests/test_tales.py (+1/-1)
lib/lp/code/errors.py (+10/-0)
lib/lp/code/help/related-recipes.html (+2/-2)
lib/lp/code/interfaces/sourcepackagerecipe.py (+4/-0)
lib/lp/code/model/sourcepackagerecipe.py (+12/-0)
lib/lp/code/model/tests/test_gitrepository.py (+8/-0)
lib/lp/code/model/tests/test_hasrecipes.py (+5/-0)
lib/lp/code/model/tests/test_recipebuilder.py (+3/-0)
lib/lp/code/model/tests/test_sourcepackagerecipe.py (+13/-0)
lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt (+10/-4)
lib/lp/code/templates/gitref-recipes.pt (+7/-0)
lib/lp/code/templates/gitrepository-recipes.pt (+7/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-recipe-browser-create
Reviewer Review Type Date Requested Status
William Grant code 2016-01-12 Approve on 2016-01-15
Review via email: mp+282324@code.launchpad.net

Commit message

Add views to create new Git recipes.

Description of the change

Add views to create new Git recipes.

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)

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 2016-01-12 15:10:57 +0000
3+++ lib/lp/code/browser/configure.zcml 2016-01-20 12:22:22 +0000
4@@ -1219,6 +1219,18 @@
5 name="+new-recipe"
6 template="../templates/sourcepackagerecipe-new.pt"/>
7 <browser:page
8+ for="lp.code.interfaces.gitrepository.IGitRepository"
9+ class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeAddView"
10+ permission="launchpad.AnyPerson"
11+ name="+new-recipe"
12+ template="../templates/sourcepackagerecipe-new.pt"/>
13+ <browser:page
14+ for="lp.code.interfaces.gitref.IGitRef"
15+ class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeAddView"
16+ permission="launchpad.AnyPerson"
17+ name="+new-recipe"
18+ template="../templates/sourcepackagerecipe-new.pt"/>
19+ <browser:page
20 for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"
21 class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeEditView"
22 permission="launchpad.Edit"
23
24=== modified file 'lib/lp/code/browser/gitref.py'
25--- lib/lp/code/browser/gitref.py 2016-01-12 15:10:57 +0000
26+++ lib/lp/code/browser/gitref.py 2016-01-20 12:22:22 +0000
27@@ -41,6 +41,8 @@
28 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
29 from lp.code.interfaces.gitref import IGitRef
30 from lp.code.interfaces.gitrepository import IGitRepositorySet
31+from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG
32+from lp.services.features import getFeatureFlag
33 from lp.services.helpers import english_list
34 from lp.services.propertycache import cachedproperty
35 from lp.services.webapp import (
36@@ -62,7 +64,9 @@
37
38 usedfor = IGitRef
39 facet = 'branches'
40- links = ['create_snap', 'register_merge', 'source', 'view_recipes']
41+ links = [
42+ 'create_recipe', 'create_snap', 'register_merge', 'source',
43+ 'view_recipes']
44
45 def source(self):
46 """Return a link to the branch's browsing interface."""
47@@ -75,6 +79,14 @@
48 enabled = self.context.namespace.supports_merge_proposals
49 return Link('+register-merge', text, icon='add', enabled=enabled)
50
51+ def create_recipe(self):
52+ # You can't create a recipe for a reference in a private repository.
53+ enabled = (
54+ not self.context.private and
55+ bool(getFeatureFlag(GIT_RECIPES_FEATURE_FLAG)))
56+ text = "Create packaging recipe"
57+ return Link("+new-recipe", text, enabled=enabled, icon="add")
58+
59
60 class GitRefView(LaunchpadView, HasSnapsViewMixin):
61
62
63=== modified file 'lib/lp/code/browser/gitrepository.py'
64--- lib/lp/code/browser/gitrepository.py 2016-01-12 15:10:57 +0000
65+++ lib/lp/code/browser/gitrepository.py 2016-01-20 12:22:22 +0000
66@@ -70,6 +70,7 @@
67 IPerson,
68 IPersonSet,
69 )
70+from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG
71 from lp.registry.vocabularies import UserTeamsParticipationPlusSelfVocabulary
72 from lp.services.config import config
73 from lp.services.database.constants import UTC_NOW
74@@ -210,7 +211,7 @@
75 usedfor = IGitRepository
76 facet = "branches"
77 links = [
78- "add_subscriber", "source", "subscription",
79+ "add_subscriber", "create_recipe", "source", "subscription",
80 "view_recipes", "visibility"]
81
82 @enabled_with_permission("launchpad.AnyPerson")
83@@ -242,6 +243,14 @@
84 text = "Change information type"
85 return Link("+edit-information-type", text)
86
87+ def create_recipe(self):
88+ # You can't create a recipe for a private repository.
89+ enabled = (
90+ not self.context.private and
91+ bool(getFeatureFlag(GIT_RECIPES_FEATURE_FLAG)))
92+ text = "Create packaging recipe"
93+ return Link("+new-recipe", text, enabled=enabled, icon="add")
94+
95
96 @implementer(IGitRefBatchNavigator)
97 class GitRefBatchNavigator(TableBatchNavigator):
98
99=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
100--- lib/lp/code/browser/sourcepackagerecipe.py 2016-01-15 12:42:28 +0000
101+++ lib/lp/code/browser/sourcepackagerecipe.py 2016-01-20 12:22:22 +0000
102@@ -92,11 +92,14 @@
103 )
104 from lp.code.interfaces.branch import IBranch
105 from lp.code.interfaces.branchtarget import IBranchTarget
106+from lp.code.interfaces.gitref import IGitRef
107+from lp.code.interfaces.gitrepository import IGitRepository
108 from lp.code.interfaces.sourcepackagerecipe import (
109 IRecipeBranchSource,
110 ISourcePackageRecipe,
111 ISourcePackageRecipeSource,
112 MINIMAL_RECIPE_TEXT_BZR,
113+ MINIMAL_RECIPE_TEXT_GIT,
114 )
115 from lp.code.vocabularies.sourcepackagerecipe import BuildableDistroSeries
116 from lp.registry.interfaces.series import SeriesStatus
117@@ -737,14 +740,18 @@
118 self.form_fields['distroseries'].for_input = True
119
120 def getBranch(self):
121- """The branch on which the recipe is built."""
122+ """The branch or repository on which the recipe is built."""
123 return self.context
124
125 def _recipe_names(self):
126 """A generator of recipe names."""
127 # +junk-daily doesn't make a very good recipe name, so use the
128- # branch name in that case.
129- if self.context.target.allow_recipe_name_from_target:
130+ # branch name in that case; similarly for personal Git repositories.
131+ if ((IBranch.providedBy(self.context) and
132+ self.context.target.allow_recipe_name_from_target) or
133+ ((IGitRepository.providedBy(self.context) or
134+ IGitRef.providedBy(self.context)) and
135+ self.context.namespace.allow_recipe_name_from_target)):
136 branch_target_name = self.context.target.name.split('/')[-1]
137 else:
138 branch_target_name = self.context.name
139@@ -765,9 +772,27 @@
140 distroseries = BuildableDistroSeries.findSeries(self.user)
141 series = [series for series in distroseries if series.status in (
142 SeriesStatus.CURRENT, SeriesStatus.DEVELOPMENT)]
143+ if IBranch.providedBy(self.context):
144+ recipe_text = MINIMAL_RECIPE_TEXT_BZR % self.context.identity
145+ elif IGitRepository.providedBy(self.context):
146+ default_ref = None
147+ if self.context.default_branch is not None:
148+ default_ref = self.context.getRefByPath(
149+ self.context.default_branch)
150+ if default_ref is not None:
151+ branch_name = default_ref.name
152+ else:
153+ branch_name = "ENTER-BRANCH-NAME"
154+ recipe_text = MINIMAL_RECIPE_TEXT_GIT % (
155+ self.context.identity, branch_name)
156+ elif IGitRef.providedBy(self.context):
157+ recipe_text = MINIMAL_RECIPE_TEXT_GIT % (
158+ self.context.repository.identity, self.context.name)
159+ else:
160+ raise AssertionError("Unsupported context: %r" % (self.context,))
161 return {
162 'name': self._find_unused_name(self.user),
163- 'recipe_text': MINIMAL_RECIPE_TEXT_BZR % self.context.bzr_identity,
164+ 'recipe_text': recipe_text,
165 'owner': self.user,
166 'distroseries': series,
167 'build_daily': True,
168
169=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
170--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2016-01-12 12:28:09 +0000
171+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2016-01-20 12:22:22 +0000
172@@ -36,6 +36,7 @@
173 SourcePackageRecipeBuildView,
174 )
175 from lp.code.interfaces.sourcepackagerecipe import (
176+ GIT_RECIPES_FEATURE_FLAG,
177 MINIMAL_RECIPE_TEXT_BZR,
178 MINIMAL_RECIPE_TEXT_GIT,
179 )
180@@ -45,6 +46,7 @@
181 from lp.registry.interfaces.series import SeriesStatus
182 from lp.registry.interfaces.teammembership import TeamMembershipStatus
183 from lp.services.database.constants import UTC_NOW
184+from lp.services.features.testing import FeatureFixture
185 from lp.services.propertycache import clear_property_cache
186 from lp.services.webapp import canonical_url
187 from lp.services.webapp.escaping import html_escape
188@@ -204,6 +206,10 @@
189 branch_type = "repository"
190 no_such_object_message = "is not a Git repository on Launchpad."
191
192+ def setUp(self):
193+ super(GitMixin, self).setUp()
194+ self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
195+
196 def makeBranch(self, **kwargs):
197 return self.factory.makeGitRefs(**kwargs)[0]
198
199@@ -265,36 +271,10 @@
200 daily_build_archive=self.ppa, **kwargs)
201
202
203-class TestSourcePackageRecipeAddViewInitalValues(TestCaseWithFactory):
204+class TestSourcePackageRecipeAddViewInitialValuesMixin:
205
206 layer = DatabaseFunctionalLayer
207
208- def test_project_branch_initial_name(self):
209- # When a project branch is used, the initial name is the name of the
210- # project followed by "-daily"
211- widget = self.factory.makeProduct(name='widget')
212- branch = self.factory.makeProductBranch(widget)
213- with person_logged_in(branch.owner):
214- view = create_initialized_view(branch, '+new-recipe')
215- self.assertThat('widget-daily', Equals(view.initial_values['name']))
216-
217- def test_package_branch_initial_name(self):
218- # When a package branch is used, the initial name is the name of the
219- # source package followed by "-daily"
220- branch = self.factory.makePackageBranch(sourcepackagename='widget')
221- with person_logged_in(branch.owner):
222- view = create_initialized_view(branch, '+new-recipe')
223- self.assertThat('widget-daily', Equals(view.initial_values['name']))
224-
225- def test_personal_branch_initial_name(self):
226- # When a personal branch is used, the initial name is the name of the
227- # branch followed by "-daily". +junk-daily is not valid nor
228- # helpful.
229- branch = self.factory.makePersonalBranch(name='widget')
230- with person_logged_in(branch.owner):
231- view = create_initialized_view(branch, '+new-recipe')
232- self.assertThat('widget-daily', Equals(view.initial_values['name']))
233-
234 def test_initial_name_exists(self):
235 # If the initial name exists, a generator is used to find an unused
236 # name by appending a numbered suffix on the end.
237@@ -302,7 +282,7 @@
238 self.factory.makeSourcePackageRecipe(
239 owner=owner, name=u'widget-daily')
240 widget = self.factory.makeProduct(name='widget')
241- branch = self.factory.makeProductBranch(widget)
242+ branch = self.makeBranch(target=widget)
243 with person_logged_in(owner):
244 view = create_initialized_view(branch, '+new-recipe')
245 self.assertThat('widget-daily-1', Equals(view.initial_values['name']))
246@@ -331,7 +311,7 @@
247 future = self.factory.makeDistroSeries(
248 distribution=archive.distribution,
249 status=SeriesStatus.FUTURE)
250- branch = self.factory.makeAnyBranch()
251+ branch = self.makeBranch()
252 with person_logged_in(archive.owner):
253 view = create_initialized_view(branch, '+new-recipe')
254 series = set(view.initial_values['distroseries'])
255@@ -342,30 +322,88 @@
256 self.assertEqual(set(), series.intersection(other_series))
257
258
259-class TestSourcePackageRecipeAddView(BzrMixin, TestCaseForRecipe):
260+class TestSourcePackageRecipeAddViewInitialValuesBzr(
261+ TestSourcePackageRecipeAddViewInitialValuesMixin, BzrMixin,
262+ TestCaseWithFactory):
263+
264+ def test_project_branch_initial_name(self):
265+ # When a project branch is used, the initial name is the name of the
266+ # project followed by "-daily".
267+ widget = self.factory.makeProduct(name='widget')
268+ branch = self.factory.makeProductBranch(widget)
269+ with person_logged_in(branch.owner):
270+ view = create_initialized_view(branch, '+new-recipe')
271+ self.assertThat('widget-daily', Equals(view.initial_values['name']))
272+
273+ def test_package_branch_initial_name(self):
274+ # When a package branch is used, the initial name is the name of the
275+ # source package followed by "-daily".
276+ branch = self.factory.makePackageBranch(sourcepackagename='widget')
277+ with person_logged_in(branch.owner):
278+ view = create_initialized_view(branch, '+new-recipe')
279+ self.assertThat('widget-daily', Equals(view.initial_values['name']))
280+
281+ def test_personal_branch_initial_name(self):
282+ # When a personal branch is used, the initial name is the name of
283+ # the branch followed by "-daily". +junk-daily is neither valid nor
284+ # helpful.
285+ branch = self.factory.makePersonalBranch(name='widget')
286+ with person_logged_in(branch.owner):
287+ view = create_initialized_view(branch, '+new-recipe')
288+ self.assertThat('widget-daily', Equals(view.initial_values['name']))
289+
290+
291+class TestSourcePackageRecipeAddViewInitialValuesGit(
292+ TestSourcePackageRecipeAddViewInitialValuesMixin, GitMixin,
293+ TestCaseWithFactory):
294+
295+ def test_project_repository_initial_name(self):
296+ # When a project repository is used, the initial name is the name of
297+ # the project followed by "-daily".
298+ widget = self.factory.makeProduct(name='widget')
299+ repository = self.factory.makeGitRepository(target=widget)
300+ with person_logged_in(repository.owner):
301+ view = create_initialized_view(repository, '+new-recipe')
302+ self.assertThat('widget-daily', Equals(view.initial_values['name']))
303+
304+ def test_package_repository_initial_name(self):
305+ # When a package repository is used, the initial name is the name of
306+ # the source package followed by "-daily".
307+ dsp = self.factory.makeDistributionSourcePackage(
308+ sourcepackagename='widget')
309+ repository = self.factory.makeGitRepository(target=dsp)
310+ with person_logged_in(repository.owner):
311+ view = create_initialized_view(repository, '+new-recipe')
312+ self.assertThat('widget-daily', Equals(view.initial_values['name']))
313+
314+ def test_personal_repository_initial_name(self):
315+ # When a personal repository is used, the initial name is the name
316+ # of the repository followed by "-daily". <person-name>-daily is
317+ # not helpful.
318+ owner = self.factory.makePerson()
319+ repository = self.factory.makeGitRepository(
320+ owner=owner, target=owner, name=u'widget')
321+ with person_logged_in(repository.owner):
322+ view = create_initialized_view(repository, '+new-recipe')
323+ self.assertThat('widget-daily', Equals(view.initial_values['name']))
324+
325+
326+class TestSourcePackageRecipeAddViewMixin:
327
328 layer = DatabaseFunctionalLayer
329
330- def makeBranch(self):
331- product = self.factory.makeProduct(
332- name='ratatouille', displayname='Ratatouille')
333- branch = self.factory.makeBranch(
334- owner=self.chef, product=product, name='veggies')
335- self.factory.makeSourcePackage(sourcepackagename='ratatouille')
336- return branch
337-
338 def test_create_new_recipe_not_logged_in(self):
339 product = self.factory.makeProduct(
340 name='ratatouille', displayname='Ratatouille')
341- branch = self.factory.makeBranch(
342- owner=self.chef, product=product, name='veggies')
343+ branch = self.makeBranch(
344+ owner=self.chef, target=product, name=u'veggies')
345
346 browser = self.getViewBrowser(branch, no_login=True)
347 self.assertRaises(
348 Unauthorized, browser.getLink('Create packaging recipe').click)
349
350 def test_create_new_recipe(self):
351- branch = self.makeBranch()
352+ branch = self.makeBranchAndPackage()
353 # A new recipe can be created from the branch page.
354 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)
355 browser.getLink('Create packaging recipe').click()
356@@ -388,7 +426,7 @@
357 def test_create_new_recipe_private_branch(self):
358 # Recipes can't be created on private branches.
359 with person_logged_in(self.chef):
360- branch = self.factory.makeBranch(
361+ branch = self.makeBranch(
362 owner=self.chef, information_type=InformationType.USERDATA)
363 branch_url = canonical_url(branch)
364
365@@ -403,7 +441,7 @@
366 self.factory.makeTeam(
367 name='good-chefs', displayname='Good Chefs', members=[self.chef])
368 browser = self.getViewBrowser(
369- self.makeBranch(), '+new-recipe', user=self.chef)
370+ self.makeBranchAndPackage(), '+new-recipe', user=self.chef)
371 # The options for the owner include the Good Chefs team.
372 options = browser.getControl(name='field.owner.owner').displayOptions
373 self.assertEquals(
374@@ -415,7 +453,7 @@
375 team = self.factory.makeTeam(
376 name='good-chefs', displayname='Good Chefs', members=[self.chef])
377 browser = self.getViewBrowser(
378- self.makeBranch(), '+new-recipe', user=self.chef)
379+ self.makeBranchAndPackage(), '+new-recipe', user=self.chef)
380 browser.getControl(name='field.name').value = 'daily'
381 browser.getControl('Description').value = 'Make some food!'
382 browser.getControl('Other').click()
383@@ -430,7 +468,7 @@
384
385 def test_create_new_recipe_suggests_user(self):
386 """The current user is suggested as a recipe owner, once."""
387- branch = self.factory.makeBranch(owner=self.chef)
388+ branch = self.makeBranch(owner=self.chef)
389 text = self.getMainText(branch, '+new-recipe')
390 self.assertTextMatchesExpressionIgnoreWhitespace(
391 r'Owner: Master Chef \(chef\) Other:', text)
392@@ -440,7 +478,7 @@
393 team = self.factory.makeTeam(
394 name='branch-team', displayname='Branch Team',
395 members=[self.chef])
396- branch = self.factory.makeBranch(owner=team)
397+ branch = self.makeBranch(owner=team)
398 text = self.getMainText(branch, '+new-recipe')
399 self.assertTextMatchesExpressionIgnoreWhitespace(
400 r'Owner: Master Chef \(chef\)'
401@@ -450,7 +488,7 @@
402 """If current user isn't a member of branch owner, it is ignored."""
403 team = self.factory.makeTeam(
404 name='branch-team', displayname='Branch Team')
405- branch = self.factory.makeBranch(owner=team)
406+ branch = self.makeBranch(owner=team)
407 text = self.getMainText(branch, '+new-recipe')
408 self.assertTextMatchesExpressionIgnoreWhitespace(
409 r'Owner: Master Chef \(chef\) Other:', text)
410@@ -460,8 +498,8 @@
411 # is communicated to the user properly.
412 product = self.factory.makeProduct(
413 name='ratatouille', displayname='Ratatouille')
414- branch = self.factory.makeBranch(
415- owner=self.chef, product=product, name='veggies')
416+ branch = self.makeBranch(
417+ owner=self.chef, target=product, name=u'veggies')
418 browser = self.getViewBrowser(branch, '+new-recipe', user=self.chef)
419 browser.getControl('Description').value = 'Make some food!'
420 browser.getControl('Recipe text').value = (
421@@ -475,8 +513,8 @@
422 if branch is None:
423 product = self.factory.makeProduct(
424 name='ratatouille', displayname='Ratatouille')
425- branch = self.factory.makeBranch(
426- owner=self.chef, product=product, name='veggies')
427+ branch = self.makeBranch(
428+ owner=self.chef, target=product, name=u'veggies')
429 browser = self.getViewBrowser(branch, '+new-recipe', user=self.chef)
430 browser.getControl(name='field.name').value = 'daily'
431 browser.getControl('Description').value = 'Make some food!'
432@@ -487,18 +525,11 @@
433 def test_create_recipe_usage(self):
434 # The error for a recipe with invalid instruction parameters should
435 # include instruction usage.
436- branch = self.factory.makeBranch(name='veggies')
437- self.factory.makeBranch(name='packaging')
438+ branch = self.makeBranch(name=u'veggies')
439+ self.makeBranch(name=u'packaging')
440
441 browser = self.createRecipe(
442- dedent('''\
443- # bzr-builder format 0.2 deb-version 0+{revno}
444- %(branch)s
445- merge
446- ''' % {
447- 'branch': branch.bzr_identity,
448- }),
449- branch=branch)
450+ self.getMinimalRecipeText(branch) + "merge\n", branch=branch)
451 self.assertEqual(
452 'Error parsing recipe:3:6: '
453 'End of line while looking for the branch id.\n'
454@@ -506,7 +537,8 @@
455 get_feedback_messages(browser.contents)[1])
456
457 def test_create_recipe_no_distroseries(self):
458- browser = self.getViewBrowser(self.makeBranch(), '+new-recipe')
459+ browser = self.getViewBrowser(
460+ self.makeBranchAndPackage(), '+new-recipe')
461 browser.getControl(name='field.name').value = 'daily'
462 browser.getControl('Description').value = 'Make some food!'
463 browser.getControl(name='field.distroseries').value = []
464@@ -518,7 +550,11 @@
465 def test_create_recipe_bad_base_branch(self):
466 # If a user tries to create source package recipe with a bad base
467 # branch location, they should get an error.
468- browser = self.createRecipe(MINIMAL_RECIPE_TEXT_BZR % 'foo')
469+ browser = self.createRecipe(
470+ self.minimal_recipe_text.splitlines()[0] + '\nfoo\n')
471+ # This page doesn't know whether the user was aiming for a Bazaar
472+ # branch or a Git repository; the error message always says
473+ # "branch".
474 self.assertEqual(
475 get_feedback_messages(browser.contents)[1],
476 'foo is not a branch on Launchpad.')
477@@ -528,28 +564,27 @@
478 # instruction branch location, they should get an error.
479 product = self.factory.makeProduct(
480 name='ratatouille', displayname='Ratatouille')
481- branch = self.factory.makeBranch(
482- owner=self.chef, product=product, name='veggies')
483- recipe = MINIMAL_RECIPE_TEXT_BZR % branch.bzr_identity
484+ branch = self.makeBranch(
485+ owner=self.chef, target=product, name=u'veggies')
486+ recipe = self.getMinimalRecipeText(branch)
487 recipe += 'nest packaging foo debian'
488 browser = self.createRecipe(recipe, branch)
489 self.assertEqual(
490 get_feedback_messages(browser.contents)[1],
491- 'foo is not a branch on Launchpad.')
492+ 'foo %s' % self.no_such_object_message)
493
494 def test_create_recipe_format_too_new(self):
495 # If the recipe's format version is too new, we should notify the
496 # user.
497 product = self.factory.makeProduct(
498 name='ratatouille', displayname='Ratatouille')
499- branch = self.factory.makeBranch(
500- owner=self.chef, product=product, name='veggies')
501+ branch = self.makeBranch(
502+ owner=self.chef, target=product, name=u'veggies')
503
504 with recipe_parser_newest_version(145.115):
505- recipe = dedent(u'''\
506- # bzr-builder format 145.115 deb-version {debupstream}-0~{revno}
507- %s
508- ''') % branch.bzr_identity
509+ recipe = re.sub(
510+ 'format [^ ]*', 'format 145.115',
511+ self.getMinimalRecipeText(branch))
512 browser = self.createRecipe(recipe, branch)
513 self.assertEqual(
514 get_feedback_messages(browser.contents)[1],
515@@ -564,8 +599,8 @@
516
517 product = self.factory.makeProduct(
518 name='ratatouille', displayname='Ratatouille')
519- branch = self.factory.makeBranch(
520- owner=self.chef, product=product, name='veggies')
521+ branch = self.makeBranch(
522+ owner=self.chef, target=product, name=u'veggies')
523
524 # A new recipe can be created from the branch page.
525 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)
526@@ -583,15 +618,16 @@
527 def test_create_recipe_private_branch(self):
528 # If a user tries to create source package recipe with a private
529 # base branch, they should get an error.
530- branch = self.factory.makeAnyBranch(
531+ branch = self.makeBranch(
532 owner=self.user, information_type=InformationType.USERDATA)
533 with person_logged_in(self.user):
534- bzr_identity = branch.bzr_identity
535- recipe_text = MINIMAL_RECIPE_TEXT_BZR % bzr_identity
536+ identity = self.getRepository(branch).identity
537+ recipe_text = self.getMinimalRecipeText(branch)
538 browser = self.createRecipe(recipe_text)
539 self.assertEqual(
540 get_feedback_messages(browser.contents)[1],
541- 'Recipe may not refer to private branch: %s' % bzr_identity)
542+ 'Recipe may not refer to private %s: %s' % (
543+ self.branch_type, identity))
544
545 def _test_new_recipe_with_no_related_branches(self, branch):
546 # The Related Branches section should not appear if there are no
547@@ -607,72 +643,20 @@
548
549 def test_new_product_branch_with_no_related_branches_recipe(self):
550 # We can create a new recipe off a product branch.
551- branch = self.factory.makeBranch()
552+ branch = self.makeBranch()
553 self._test_new_recipe_with_no_related_branches(branch)
554
555 def test_new_package_branch_with_no_linked_branches_recipe(self):
556 # We can create a new recipe off a sourcepackage branch where the
557 # sourcepackage has no linked branches.
558- branch = self.factory.makePackageBranch()
559+ branch = self.makePackageBranch()
560 self._test_new_recipe_with_no_related_branches(branch)
561
562- def test_new_recipe_with_package_branches(self):
563- # The series branches table should not appear if there are none.
564- (branch, related_series_branch_info, related_package_branches) = (
565- self.factory.makeRelatedBranches(with_series_branches=False))
566- browser = self.getUserBrowser(
567- canonical_url(branch, view_name='+new-recipe'), user=self.chef)
568- soup = BeautifulSoup(browser.contents)
569- related_branches = soup.find('fieldset', {'id': 'related-branches'})
570- self.assertIsNot(related_branches, None)
571- related_branches = soup.find(
572- 'div', {'id': 'related-package-branches'})
573- self.assertIsNot(related_branches, None)
574- related_branches = soup.find(
575- 'div', {'id': 'related-series-branches'})
576- self.assertIs(related_branches, None)
577-
578- def test_new_recipe_with_series_branches(self):
579- # The package branches table should not appear if there are none.
580- (branch, related_series_branch_info, related_package_branches) = (
581- self.factory.makeRelatedBranches(with_package_branches=False))
582- browser = self.getUserBrowser(
583- canonical_url(branch, view_name='+new-recipe'), user=self.chef)
584- soup = BeautifulSoup(browser.contents)
585- related_branches = soup.find('fieldset', {'id': 'related-branches'})
586- self.assertIsNot(related_branches, None)
587- related_branches = soup.find(
588- 'div', {'id': 'related-series-branches'})
589- self.assertIsNot(related_branches, None)
590- related_branches = soup.find(
591- 'div', {'id': 'related-package-branches'})
592- self.assertIs(related_branches, None)
593-
594- def test_new_product_branch_recipe_with_related_branches(self):
595- # The related branches should be rendered correctly on the page.
596- (branch, related_series_branch_info,
597- related_package_branch_info) = self.factory.makeRelatedBranches()
598- browser = self.getUserBrowser(
599- canonical_url(branch, view_name='+new-recipe'), user=self.chef)
600- self.checkRelatedBranches(
601- related_series_branch_info, related_package_branch_info,
602- browser.contents)
603-
604- def test_new_sourcepackage_branch_recipe_with_related_branches(self):
605- # The related branches should be rendered correctly on the page.
606- reference_branch = self.factory.makePackageBranch()
607- (branch, ignore, related_package_branch_info) = (
608- self.factory.makeRelatedBranches(reference_branch))
609- browser = self.getUserBrowser(
610- canonical_url(branch, view_name='+new-recipe'), user=self.chef)
611- self.checkRelatedBranches(
612- set(), related_package_branch_info, browser.contents)
613-
614 def test_ppa_selector_not_shown_if_user_has_no_ppas(self):
615 # If the user creating a recipe has no existing PPAs, the selector
616 # isn't shown, but the field to enter a new PPA name is.
617 self.user = self.factory.makePerson()
618- branch = self.factory.makeAnyBranch()
619+ branch = self.makeBranch()
620 with person_logged_in(self.user):
621 content = self.getMainContent(branch, '+new-recipe')
622 ppa_name = content.find(attrs={'id': 'field.ppa_name'})
623@@ -692,7 +676,7 @@
624 # If the user creating a recipe has existing PPAs, the selector is
625 # shown, along with radio buttons to decide whether to use an existing
626 # ppa or to create a new one.
627- branch = self.factory.makeAnyBranch()
628+ branch = self.makeBranch()
629 with person_logged_in(self.user):
630 content = self.getMainContent(branch, '+new-recipe')
631 ppa_name = content.find(attrs={'id': 'field.ppa_name'})
632@@ -712,7 +696,7 @@
633 def test_create_new_ppa(self):
634 # If the user doesn't have any PPAs, a new once can be created.
635 self.user = self.factory.makePerson(name='eric')
636- branch = self.factory.makeAnyBranch()
637+ branch = self.makeBranch()
638
639 # A new recipe can be created from the branch page.
640 browser = self.getUserBrowser(canonical_url(branch), user=self.user)
641@@ -737,7 +721,7 @@
642 # Make a PPA called 'ppa' using the default.
643 with person_logged_in(self.user):
644 self.user.createPPA(name='foo')
645- branch = self.factory.makeAnyBranch()
646+ branch = self.makeBranch()
647
648 # A new recipe can be created from the branch page.
649 browser = self.getUserBrowser(canonical_url(branch), user=self.user)
650@@ -756,7 +740,7 @@
651 # If a new PPA is being created, and the user has not specified a
652 # name, an error is shown.
653 self.user = self.factory.makePerson(name='eric')
654- branch = self.factory.makeAnyBranch()
655+ branch = self.makeBranch()
656
657 # A new recipe can be created from the branch page.
658 browser = self.getUserBrowser(canonical_url(branch), user=self.user)
659@@ -779,7 +763,7 @@
660 with person_logged_in(team.teamowner):
661 team.setMembershipData(
662 self.user, TeamMembershipStatus.ADMIN, team.teamowner)
663- branch = self.factory.makeAnyBranch(owner=team)
664+ branch = self.makeBranch(owner=team)
665
666 # A new recipe can be created from the branch page.
667 browser = self.getUserBrowser(canonical_url(branch), user=self.user)
668@@ -799,6 +783,83 @@
669 self.assertIsNot(None, new_ppa)
670
671
672+class TestSourcePackageRecipeAddViewBzr(
673+ TestSourcePackageRecipeAddViewMixin, BzrMixin, TestCaseForRecipe):
674+
675+ def makeBranchAndPackage(self):
676+ product = self.factory.makeProduct(
677+ name='ratatouille', displayname='Ratatouille')
678+ branch = self.factory.makeBranch(
679+ owner=self.chef, product=product, name='veggies')
680+ self.factory.makeSourcePackage(sourcepackagename='ratatouille')
681+ return branch
682+
683+ def test_new_recipe_with_package_branches(self):
684+ # The series branches table should not appear if there are none.
685+ branch, related_series_branch_info, related_package_branches = (
686+ self.makeRelatedBranches(with_series_branches=False))
687+ browser = self.getUserBrowser(
688+ canonical_url(branch, view_name='+new-recipe'), user=self.chef)
689+ soup = BeautifulSoup(browser.contents)
690+ related_branches = soup.find('fieldset', {'id': 'related-branches'})
691+ self.assertIsNot(related_branches, None)
692+ related_branches = soup.find(
693+ 'div', {'id': 'related-package-branches'})
694+ self.assertIsNot(related_branches, None)
695+ related_branches = soup.find(
696+ 'div', {'id': 'related-series-branches'})
697+ self.assertIs(related_branches, None)
698+
699+ def test_new_recipe_with_series_branches(self):
700+ # The package branches table should not appear if there are none.
701+ branch, related_series_branch_info, related_package_branches = (
702+ self.makeRelatedBranches(with_package_branches=False))
703+ browser = self.getUserBrowser(
704+ canonical_url(branch, view_name='+new-recipe'), user=self.chef)
705+ soup = BeautifulSoup(browser.contents)
706+ related_branches = soup.find('fieldset', {'id': 'related-branches'})
707+ self.assertIsNot(related_branches, None)
708+ related_branches = soup.find(
709+ 'div', {'id': 'related-series-branches'})
710+ self.assertIsNot(related_branches, None)
711+ related_branches = soup.find(
712+ 'div', {'id': 'related-package-branches'})
713+ self.assertIs(related_branches, None)
714+
715+ def test_new_product_branch_recipe_with_related_branches(self):
716+ # The related branches should be rendered correctly on the page.
717+ branch, related_series_branch_info, related_package_branch_info = (
718+ self.makeRelatedBranches())
719+ browser = self.getUserBrowser(
720+ canonical_url(branch, view_name='+new-recipe'), user=self.chef)
721+ self.checkRelatedBranches(
722+ related_series_branch_info, related_package_branch_info,
723+ browser.contents)
724+
725+ def test_new_sourcepackage_branch_recipe_with_related_branches(self):
726+ # The related branches should be rendered correctly on the page.
727+ reference_branch = self.makePackageBranch()
728+ branch, _, related_package_branch_info = (
729+ self.makeRelatedBranches(reference_branch))
730+ browser = self.getUserBrowser(
731+ canonical_url(branch, view_name='+new-recipe'), user=self.chef)
732+ self.checkRelatedBranches(
733+ set(), related_package_branch_info, browser.contents)
734+
735+
736+class TestSourcePackageRecipeAddViewGit(
737+ TestSourcePackageRecipeAddViewMixin, GitMixin, TestCaseForRecipe):
738+
739+ def makeBranchAndPackage(self):
740+ product = self.factory.makeProduct(
741+ name='ratatouille', displayname='Ratatouille')
742+ repository = self.factory.makeGitRepository(
743+ owner=self.chef, target=product, name=u'veggies')
744+ self.factory.makeDistributionSourcePackage(
745+ sourcepackagename='ratatouille')
746+ return repository
747+
748+
749 class TestSourcePackageRecipeEditViewMixin:
750 """Test the editing behaviour of a source package recipe."""
751
752
753=== modified file 'lib/lp/code/browser/tests/test_tales.py'
754--- lib/lp/code/browser/tests/test_tales.py 2015-09-11 06:04:36 +0000
755+++ lib/lp/code/browser/tests/test_tales.py 2016-01-20 12:22:22 +0000
756@@ -235,7 +235,7 @@
757 '<a href="%s">%s recipe build in ubuntu shiny</a> '
758 '[~eric/ubuntu/ppa]'
759 % (canonical_url(build, path_only_if_possible=True),
760- build.recipe.base_branch.unique_name)))
761+ build.recipe.base.unique_name)))
762
763 def test_link_no_recipe(self):
764 eric = self.factory.makePerson(name='eric')
765
766=== modified file 'lib/lp/code/errors.py'
767--- lib/lp/code/errors.py 2016-01-12 01:24:06 +0000
768+++ lib/lp/code/errors.py 2016-01-20 12:22:22 +0000
769@@ -30,6 +30,7 @@
770 'ClaimReviewFailed',
771 'DiffNotFound',
772 'GitDefaultConflict',
773+ 'GitRecipesFeatureDisabled',
774 'GitRepositoryCreationException',
775 'GitRepositoryCreationFault',
776 'GitRepositoryCreationForbidden',
777@@ -485,6 +486,15 @@
778 self.newest_supported = newest_supported
779
780
781+@error_status(httplib.UNAUTHORIZED)
782+class GitRecipesFeatureDisabled(Exception):
783+ """Only certain users can create new Git recipes."""
784+
785+ def __init__(self):
786+ message = "You do not have permission to create Git recipes."
787+ Exception.__init__(self, message)
788+
789+
790 @error_status(httplib.BAD_REQUEST)
791 class RecipeBuildException(Exception):
792
793
794=== modified file 'lib/lp/code/help/related-recipes.html'
795--- lib/lp/code/help/related-recipes.html 2010-11-04 22:50:52 +0000
796+++ lib/lp/code/help/related-recipes.html 2016-01-20 12:22:22 +0000
797@@ -18,8 +18,8 @@
798 </p>
799
800 <p>A "recipe" is a description of the steps Launchpad's package builder
801- should take to construct a source package from a set of Bazaar branches
802- that you specify.
803+ should take to construct a source package from a set of Bazaar or Git
804+ branches that you specify.
805 </p>
806
807 <p>
808
809=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
810--- lib/lp/code/interfaces/sourcepackagerecipe.py 2016-01-15 11:50:57 +0000
811+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2016-01-20 12:22:22 +0000
812@@ -8,6 +8,7 @@
813
814
815 __all__ = [
816+ 'GIT_RECIPES_FEATURE_FLAG',
817 'IRecipeBranchSource',
818 'ISourcePackageRecipe',
819 'ISourcePackageRecipeData',
820@@ -68,6 +69,9 @@
821 from lp.soyuz.interfaces.archive import IArchive
822
823
824+GIT_RECIPES_FEATURE_FLAG = u'code.git.recipes.enabled'
825+
826+
827 MINIMAL_RECIPE_TEXT_BZR = dedent(u'''\
828 # bzr-builder format 0.3 deb-version {debupstream}-0~{revno}
829 %s
830
831=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
832--- lib/lp/code/model/sourcepackagerecipe.py 2016-01-15 11:50:57 +0000
833+++ lib/lp/code/model/sourcepackagerecipe.py 2016-01-20 12:22:22 +0000
834@@ -41,8 +41,10 @@
835 from lp.code.errors import (
836 BuildAlreadyPending,
837 BuildNotAllowedForDistro,
838+ GitRecipesFeatureDisabled,
839 )
840 from lp.code.interfaces.sourcepackagerecipe import (
841+ GIT_RECIPES_FEATURE_FLAG,
842 IRecipeBranchSource,
843 ISourcePackageRecipe,
844 ISourcePackageRecipeData,
845@@ -68,6 +70,7 @@
846 IStore,
847 )
848 from lp.services.database.stormexpr import Greatest
849+from lp.services.features import getFeatureFlag
850 from lp.services.propertycache import (
851 cachedproperty,
852 get_property_cache,
853@@ -227,6 +230,15 @@
854 sprecipe.date_created = date_created
855 sprecipe.date_last_modified = date_created
856 store.add(sprecipe)
857+ # We can only do this feature flag check at the end, because we need
858+ # to have constructed the SourcePackageRecipeData in order to know
859+ # whether it refers to a Git repository and then we need the
860+ # SourcePackageRecipe to respect DB constraints before doing
861+ # anything else. The transaction will be aborted either way if the
862+ # check fails.
863+ if (sprecipe.base_git_repository is not None and
864+ not getFeatureFlag(GIT_RECIPES_FEATURE_FLAG)):
865+ raise GitRecipesFeatureDisabled
866 return sprecipe
867
868 @staticmethod
869
870=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
871--- lib/lp/code/model/tests/test_gitrepository.py 2016-01-12 04:34:09 +0000
872+++ lib/lp/code/model/tests/test_gitrepository.py 2016-01-20 12:22:22 +0000
873@@ -72,6 +72,7 @@
874 IGitRepositoryView,
875 )
876 from lp.code.interfaces.revision import IRevisionSet
877+from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG
878 from lp.code.model.branchmergeproposal import BranchMergeProposal
879 from lp.code.model.branchmergeproposaljob import (
880 BranchMergeProposalJob,
881@@ -454,12 +455,14 @@
882
883 def test_destroySelf_with_SourcePackageRecipe(self):
884 # If repository is a base_git_repository in a recipe, it is deleted.
885+ self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
886 recipe = self.factory.makeSourcePackageRecipe(
887 branches=self.factory.makeGitRefs(owner=self.user))
888 recipe.base_git_repository.destroySelf(break_references=True)
889
890 def test_destroySelf_with_SourcePackageRecipe_as_non_base(self):
891 # If repository is referred to by a recipe, it is deleted.
892+ self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
893 [ref1] = self.factory.makeGitRefs(owner=self.user)
894 [ref2] = self.factory.makeGitRefs(owner=self.user)
895 self.factory.makeSourcePackageRecipe(branches=[ref1, ref2])
896@@ -698,6 +701,7 @@
897
898 def test_deletionRequirements_with_SourcePackageRecipe(self):
899 # Recipes are listed as deletion requirements.
900+ self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
901 recipe = self.factory.makeSourcePackageRecipe(
902 branches=self.factory.makeGitRefs())
903 self.assertEqual(
904@@ -1845,6 +1849,10 @@
905
906 layer = ZopelessDatabaseLayer
907
908+ def setUp(self):
909+ super(TestGitRepositoryMarkRecipesStale, self).setUp()
910+ self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
911+
912 def test_base_repository_recipe(self):
913 # On ref changes, recipes where this ref is the base become stale.
914 [ref] = self.factory.makeGitRefs()
915
916=== modified file 'lib/lp/code/model/tests/test_hasrecipes.py'
917--- lib/lp/code/model/tests/test_hasrecipes.py 2016-01-14 17:24:14 +0000
918+++ lib/lp/code/model/tests/test_hasrecipes.py 2016-01-20 12:22:22 +0000
919@@ -6,6 +6,8 @@
920 __metaclass__ = type
921
922 from lp.code.interfaces.hasrecipes import IHasRecipes
923+from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG
924+from lp.services.features.testing import FeatureFixture
925 from lp.testing import TestCaseWithFactory
926 from lp.testing.layers import DatabaseFunctionalLayer
927
928@@ -47,6 +49,7 @@
929 def test_git_repository_recipes(self):
930 # IGitRepository.recipes should provide all the SourcePackageRecipes
931 # attached to that repository.
932+ self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
933 base_ref1, base_ref2 = self.factory.makeGitRefs(
934 paths=[u"refs/heads/ref1", u"refs/heads/ref2"])
935 [other_ref] = self.factory.makeGitRefs()
936@@ -58,6 +61,7 @@
937 def test_git_repository_recipes_nonbase(self):
938 # IGitRepository.recipes should provide all the SourcePackageRecipes
939 # that refer to the repository, even as a non-base branch.
940+ self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
941 [base_ref] = self.factory.makeGitRefs()
942 [nonbase_ref] = self.factory.makeGitRefs()
943 [other_ref] = self.factory.makeGitRefs()
944@@ -88,6 +92,7 @@
945 def test_product_recipes(self):
946 # IProduct.recipes should provide all the SourcePackageRecipes
947 # attached to that product's branches and Git repositories.
948+ self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
949 product = self.factory.makeProduct()
950 branch = self.factory.makeBranch(product=product)
951 [ref] = self.factory.makeGitRefs(target=product)
952
953=== modified file 'lib/lp/code/model/tests/test_recipebuilder.py'
954--- lib/lp/code/model/tests/test_recipebuilder.py 2016-01-15 11:50:57 +0000
955+++ lib/lp/code/model/tests/test_recipebuilder.py 2016-01-20 12:22:22 +0000
956@@ -29,11 +29,13 @@
957 TestHandleStatusMixin,
958 TestVerifySuccessfulBuildMixin,
959 )
960+from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG
961 from lp.code.model.recipebuilder import RecipeBuildBehaviour
962 from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
963 from lp.registry.interfaces.pocket import PackagePublishingPocket
964 from lp.registry.interfaces.series import SeriesStatus
965 from lp.services.config import config
966+from lp.services.features.testing import FeatureFixture
967 from lp.services.log.logger import BufferLogger
968 from lp.soyuz.adapters.archivedependencies import (
969 get_sources_list_for_building,
970@@ -258,6 +260,7 @@
971 self.assertEqual(args["archives"], expected_archives)
972
973 def test_extraBuildArgs_git(self):
974+ self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
975 job = self.makeJob(git=True)
976 distroarchseries = job.build.distroseries.architectures[0]
977 expected_archives = get_sources_list_for_building(
978
979=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
980--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2016-01-12 03:54:38 +0000
981+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2016-01-20 12:22:22 +0000
982@@ -31,11 +31,13 @@
983 from lp.buildmaster.model.buildqueue import BuildQueue
984 from lp.code.errors import (
985 BuildAlreadyPending,
986+ GitRecipesFeatureDisabled,
987 PrivateBranchRecipe,
988 PrivateGitRepositoryRecipe,
989 TooNewRecipeFormat,
990 )
991 from lp.code.interfaces.sourcepackagerecipe import (
992+ GIT_RECIPES_FEATURE_FLAG,
993 ISourcePackageRecipe,
994 ISourcePackageRecipeSource,
995 ISourcePackageRecipeView,
996@@ -56,6 +58,7 @@
997 from lp.registry.interfaces.series import SeriesStatus
998 from lp.services.database.bulk import load_referencing
999 from lp.services.database.constants import UTC_NOW
1000+from lp.services.features.testing import FeatureFixture
1001 from lp.services.propertycache import clear_property_cache
1002 from lp.services.webapp.authorization import check_permission
1003 from lp.services.webapp.publisher import canonical_url
1004@@ -122,6 +125,10 @@
1005 branch_type = "repository"
1006 recipe_id = "git-build-recipe"
1007
1008+ def setUp(self):
1009+ super(GitMixin, self).setUp()
1010+ self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
1011+
1012 def makeBranch(self, **kwargs):
1013 return self.factory.makeGitRefs(**kwargs)[0]
1014
1015@@ -823,6 +830,12 @@
1016 TestSourcePackageRecipeMixin, GitMixin, TestCaseWithFactory):
1017 """Test `SourcePackageRecipe` objects for Git."""
1018
1019+ def test_feature_flag_disabled(self):
1020+ # Without a feature flag, we will not create new Git recipes.
1021+ self.useFixture(FeatureFixture({}))
1022+ self.assertRaises(
1023+ GitRecipesFeatureDisabled, self.makeSourcePackageRecipe)
1024+
1025
1026 class TestRecipeBranchRoundTrippingMixin:
1027
1028
1029=== modified file 'lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt'
1030--- lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt 2016-01-12 15:10:57 +0000
1031+++ lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt 2016-01-20 12:22:22 +0000
1032@@ -74,15 +74,21 @@
1033 Create a new sample repository, some branches in it, and some source package
1034 recipes to go along with them.
1035
1036+ >>> from lp.code.interfaces.sourcepackagerecipe import (
1037+ ... GIT_RECIPES_FEATURE_FLAG,
1038+ ... )
1039+ >>> from lp.services.features.testing import FeatureFixture
1040+
1041 >>> login('foo.bar@canonical.com')
1042 >>> repository = factory.makeGitRepository()
1043 >>> ref1, ref2, ref3 = factory.makeGitRefs(
1044 ... repository=repository,
1045 ... paths=[u"refs/heads/a", u"refs/heads/b", u"refs/heads/c"])
1046- >>> recipe1a = factory.makeSourcePackageRecipe(branches=[ref1])
1047- >>> recipe1b = factory.makeSourcePackageRecipe(branches=[ref1])
1048- >>> recipe2 = factory.makeSourcePackageRecipe(branches=[ref2])
1049- >>> recipe3 = factory.makeSourcePackageRecipe(branches=[ref3])
1050+ >>> with FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}):
1051+ ... recipe1a = factory.makeSourcePackageRecipe(branches=[ref1])
1052+ ... recipe1b = factory.makeSourcePackageRecipe(branches=[ref1])
1053+ ... recipe2 = factory.makeSourcePackageRecipe(branches=[ref2])
1054+ ... recipe3 = factory.makeSourcePackageRecipe(branches=[ref3])
1055
1056 Keep these urls, including the target url. We'll use these later.
1057
1058
1059=== modified file 'lib/lp/code/templates/gitref-recipes.pt'
1060--- lib/lp/code/templates/gitref-recipes.pt 2016-01-12 15:10:57 +0000
1061+++ lib/lp/code/templates/gitref-recipes.pt 2016-01-20 12:22:22 +0000
1062@@ -2,6 +2,7 @@
1063 xmlns:tal="http://xml.zope.org/namespaces/tal"
1064 xmlns:metal="http://xml.zope.org/namespaces/metal"
1065 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1066+ tal:define="context_menu view/context/menu:context"
1067 id="related-recipes">
1068
1069 <h3>Related source package recipes</h3>
1070@@ -14,6 +15,12 @@
1071 <a href="/+help-code/related-recipes.html" target="help"
1072 class="sprite maybe action-icon">(?)</a>
1073 </div>
1074+
1075+ <span
1076+ tal:define="link context_menu/create_recipe"
1077+ tal:condition="link/enabled"
1078+ tal:replace="structure link/render"
1079+ />
1080 </div>
1081
1082 </div>
1083
1084=== modified file 'lib/lp/code/templates/gitrepository-recipes.pt'
1085--- lib/lp/code/templates/gitrepository-recipes.pt 2016-01-12 15:10:57 +0000
1086+++ lib/lp/code/templates/gitrepository-recipes.pt 2016-01-20 12:22:22 +0000
1087@@ -2,6 +2,7 @@
1088 xmlns:tal="http://xml.zope.org/namespaces/tal"
1089 xmlns:metal="http://xml.zope.org/namespaces/metal"
1090 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1091+ tal:define="context_menu view/context/menu:context"
1092 id="related-recipes">
1093
1094 <h3>Related source package recipes</h3>
1095@@ -14,6 +15,12 @@
1096 <a href="/+help-code/related-recipes.html" target="help"
1097 class="sprite maybe action-icon">(?)</a>
1098 </div>
1099+
1100+ <span
1101+ tal:define="link context_menu/create_recipe"
1102+ tal:condition="link/enabled"
1103+ tal:replace="structure link/render"
1104+ />
1105 </div>
1106
1107 </div>