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
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2016-01-12 15:10:57 +0000
+++ lib/lp/code/browser/configure.zcml 2016-01-20 12:22:22 +0000
@@ -1219,6 +1219,18 @@
1219 name="+new-recipe"1219 name="+new-recipe"
1220 template="../templates/sourcepackagerecipe-new.pt"/>1220 template="../templates/sourcepackagerecipe-new.pt"/>
1221 <browser:page1221 <browser:page
1222 for="lp.code.interfaces.gitrepository.IGitRepository"
1223 class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeAddView"
1224 permission="launchpad.AnyPerson"
1225 name="+new-recipe"
1226 template="../templates/sourcepackagerecipe-new.pt"/>
1227 <browser:page
1228 for="lp.code.interfaces.gitref.IGitRef"
1229 class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeAddView"
1230 permission="launchpad.AnyPerson"
1231 name="+new-recipe"
1232 template="../templates/sourcepackagerecipe-new.pt"/>
1233 <browser:page
1222 for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"1234 for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"
1223 class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeEditView"1235 class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeEditView"
1224 permission="launchpad.Edit"1236 permission="launchpad.Edit"
12251237
=== modified file 'lib/lp/code/browser/gitref.py'
--- lib/lp/code/browser/gitref.py 2016-01-12 15:10:57 +0000
+++ lib/lp/code/browser/gitref.py 2016-01-20 12:22:22 +0000
@@ -41,6 +41,8 @@
41from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference41from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
42from lp.code.interfaces.gitref import IGitRef42from lp.code.interfaces.gitref import IGitRef
43from lp.code.interfaces.gitrepository import IGitRepositorySet43from lp.code.interfaces.gitrepository import IGitRepositorySet
44from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG
45from lp.services.features import getFeatureFlag
44from lp.services.helpers import english_list46from lp.services.helpers import english_list
45from lp.services.propertycache import cachedproperty47from lp.services.propertycache import cachedproperty
46from lp.services.webapp import (48from lp.services.webapp import (
@@ -62,7 +64,9 @@
6264
63 usedfor = IGitRef65 usedfor = IGitRef
64 facet = 'branches'66 facet = 'branches'
65 links = ['create_snap', 'register_merge', 'source', 'view_recipes']67 links = [
68 'create_recipe', 'create_snap', 'register_merge', 'source',
69 'view_recipes']
6670
67 def source(self):71 def source(self):
68 """Return a link to the branch's browsing interface."""72 """Return a link to the branch's browsing interface."""
@@ -75,6 +79,14 @@
75 enabled = self.context.namespace.supports_merge_proposals79 enabled = self.context.namespace.supports_merge_proposals
76 return Link('+register-merge', text, icon='add', enabled=enabled)80 return Link('+register-merge', text, icon='add', enabled=enabled)
7781
82 def create_recipe(self):
83 # You can't create a recipe for a reference in a private repository.
84 enabled = (
85 not self.context.private and
86 bool(getFeatureFlag(GIT_RECIPES_FEATURE_FLAG)))
87 text = "Create packaging recipe"
88 return Link("+new-recipe", text, enabled=enabled, icon="add")
89
7890
79class GitRefView(LaunchpadView, HasSnapsViewMixin):91class GitRefView(LaunchpadView, HasSnapsViewMixin):
8092
8193
=== modified file 'lib/lp/code/browser/gitrepository.py'
--- lib/lp/code/browser/gitrepository.py 2016-01-12 15:10:57 +0000
+++ lib/lp/code/browser/gitrepository.py 2016-01-20 12:22:22 +0000
@@ -70,6 +70,7 @@
70 IPerson,70 IPerson,
71 IPersonSet,71 IPersonSet,
72 )72 )
73from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG
73from lp.registry.vocabularies import UserTeamsParticipationPlusSelfVocabulary74from lp.registry.vocabularies import UserTeamsParticipationPlusSelfVocabulary
74from lp.services.config import config75from lp.services.config import config
75from lp.services.database.constants import UTC_NOW76from lp.services.database.constants import UTC_NOW
@@ -210,7 +211,7 @@
210 usedfor = IGitRepository211 usedfor = IGitRepository
211 facet = "branches"212 facet = "branches"
212 links = [213 links = [
213 "add_subscriber", "source", "subscription",214 "add_subscriber", "create_recipe", "source", "subscription",
214 "view_recipes", "visibility"]215 "view_recipes", "visibility"]
215216
216 @enabled_with_permission("launchpad.AnyPerson")217 @enabled_with_permission("launchpad.AnyPerson")
@@ -242,6 +243,14 @@
242 text = "Change information type"243 text = "Change information type"
243 return Link("+edit-information-type", text)244 return Link("+edit-information-type", text)
244245
246 def create_recipe(self):
247 # You can't create a recipe for a private repository.
248 enabled = (
249 not self.context.private and
250 bool(getFeatureFlag(GIT_RECIPES_FEATURE_FLAG)))
251 text = "Create packaging recipe"
252 return Link("+new-recipe", text, enabled=enabled, icon="add")
253
245254
246@implementer(IGitRefBatchNavigator)255@implementer(IGitRefBatchNavigator)
247class GitRefBatchNavigator(TableBatchNavigator):256class GitRefBatchNavigator(TableBatchNavigator):
248257
=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py 2016-01-15 12:42:28 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py 2016-01-20 12:22:22 +0000
@@ -92,11 +92,14 @@
92 )92 )
93from lp.code.interfaces.branch import IBranch93from lp.code.interfaces.branch import IBranch
94from lp.code.interfaces.branchtarget import IBranchTarget94from lp.code.interfaces.branchtarget import IBranchTarget
95from lp.code.interfaces.gitref import IGitRef
96from lp.code.interfaces.gitrepository import IGitRepository
95from lp.code.interfaces.sourcepackagerecipe import (97from lp.code.interfaces.sourcepackagerecipe import (
96 IRecipeBranchSource,98 IRecipeBranchSource,
97 ISourcePackageRecipe,99 ISourcePackageRecipe,
98 ISourcePackageRecipeSource,100 ISourcePackageRecipeSource,
99 MINIMAL_RECIPE_TEXT_BZR,101 MINIMAL_RECIPE_TEXT_BZR,
102 MINIMAL_RECIPE_TEXT_GIT,
100 )103 )
101from lp.code.vocabularies.sourcepackagerecipe import BuildableDistroSeries104from lp.code.vocabularies.sourcepackagerecipe import BuildableDistroSeries
102from lp.registry.interfaces.series import SeriesStatus105from lp.registry.interfaces.series import SeriesStatus
@@ -737,14 +740,18 @@
737 self.form_fields['distroseries'].for_input = True740 self.form_fields['distroseries'].for_input = True
738741
739 def getBranch(self):742 def getBranch(self):
740 """The branch on which the recipe is built."""743 """The branch or repository on which the recipe is built."""
741 return self.context744 return self.context
742745
743 def _recipe_names(self):746 def _recipe_names(self):
744 """A generator of recipe names."""747 """A generator of recipe names."""
745 # +junk-daily doesn't make a very good recipe name, so use the748 # +junk-daily doesn't make a very good recipe name, so use the
746 # branch name in that case.749 # branch name in that case; similarly for personal Git repositories.
747 if self.context.target.allow_recipe_name_from_target:750 if ((IBranch.providedBy(self.context) and
751 self.context.target.allow_recipe_name_from_target) or
752 ((IGitRepository.providedBy(self.context) or
753 IGitRef.providedBy(self.context)) and
754 self.context.namespace.allow_recipe_name_from_target)):
748 branch_target_name = self.context.target.name.split('/')[-1]755 branch_target_name = self.context.target.name.split('/')[-1]
749 else:756 else:
750 branch_target_name = self.context.name757 branch_target_name = self.context.name
@@ -765,9 +772,27 @@
765 distroseries = BuildableDistroSeries.findSeries(self.user)772 distroseries = BuildableDistroSeries.findSeries(self.user)
766 series = [series for series in distroseries if series.status in (773 series = [series for series in distroseries if series.status in (
767 SeriesStatus.CURRENT, SeriesStatus.DEVELOPMENT)]774 SeriesStatus.CURRENT, SeriesStatus.DEVELOPMENT)]
775 if IBranch.providedBy(self.context):
776 recipe_text = MINIMAL_RECIPE_TEXT_BZR % self.context.identity
777 elif IGitRepository.providedBy(self.context):
778 default_ref = None
779 if self.context.default_branch is not None:
780 default_ref = self.context.getRefByPath(
781 self.context.default_branch)
782 if default_ref is not None:
783 branch_name = default_ref.name
784 else:
785 branch_name = "ENTER-BRANCH-NAME"
786 recipe_text = MINIMAL_RECIPE_TEXT_GIT % (
787 self.context.identity, branch_name)
788 elif IGitRef.providedBy(self.context):
789 recipe_text = MINIMAL_RECIPE_TEXT_GIT % (
790 self.context.repository.identity, self.context.name)
791 else:
792 raise AssertionError("Unsupported context: %r" % (self.context,))
768 return {793 return {
769 'name': self._find_unused_name(self.user),794 'name': self._find_unused_name(self.user),
770 'recipe_text': MINIMAL_RECIPE_TEXT_BZR % self.context.bzr_identity,795 'recipe_text': recipe_text,
771 'owner': self.user,796 'owner': self.user,
772 'distroseries': series,797 'distroseries': series,
773 'build_daily': True,798 'build_daily': True,
774799
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2016-01-12 12:28:09 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2016-01-20 12:22:22 +0000
@@ -36,6 +36,7 @@
36 SourcePackageRecipeBuildView,36 SourcePackageRecipeBuildView,
37 )37 )
38from lp.code.interfaces.sourcepackagerecipe import (38from lp.code.interfaces.sourcepackagerecipe import (
39 GIT_RECIPES_FEATURE_FLAG,
39 MINIMAL_RECIPE_TEXT_BZR,40 MINIMAL_RECIPE_TEXT_BZR,
40 MINIMAL_RECIPE_TEXT_GIT,41 MINIMAL_RECIPE_TEXT_GIT,
41 )42 )
@@ -45,6 +46,7 @@
45from lp.registry.interfaces.series import SeriesStatus46from lp.registry.interfaces.series import SeriesStatus
46from lp.registry.interfaces.teammembership import TeamMembershipStatus47from lp.registry.interfaces.teammembership import TeamMembershipStatus
47from lp.services.database.constants import UTC_NOW48from lp.services.database.constants import UTC_NOW
49from lp.services.features.testing import FeatureFixture
48from lp.services.propertycache import clear_property_cache50from lp.services.propertycache import clear_property_cache
49from lp.services.webapp import canonical_url51from lp.services.webapp import canonical_url
50from lp.services.webapp.escaping import html_escape52from lp.services.webapp.escaping import html_escape
@@ -204,6 +206,10 @@
204 branch_type = "repository"206 branch_type = "repository"
205 no_such_object_message = "is not a Git repository on Launchpad."207 no_such_object_message = "is not a Git repository on Launchpad."
206208
209 def setUp(self):
210 super(GitMixin, self).setUp()
211 self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
212
207 def makeBranch(self, **kwargs):213 def makeBranch(self, **kwargs):
208 return self.factory.makeGitRefs(**kwargs)[0]214 return self.factory.makeGitRefs(**kwargs)[0]
209215
@@ -265,36 +271,10 @@
265 daily_build_archive=self.ppa, **kwargs)271 daily_build_archive=self.ppa, **kwargs)
266272
267273
268class TestSourcePackageRecipeAddViewInitalValues(TestCaseWithFactory):274class TestSourcePackageRecipeAddViewInitialValuesMixin:
269275
270 layer = DatabaseFunctionalLayer276 layer = DatabaseFunctionalLayer
271277
272 def test_project_branch_initial_name(self):
273 # When a project branch is used, the initial name is the name of the
274 # project followed by "-daily"
275 widget = self.factory.makeProduct(name='widget')
276 branch = self.factory.makeProductBranch(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_package_branch_initial_name(self):
282 # When a package branch is used, the initial name is the name of the
283 # source package followed by "-daily"
284 branch = self.factory.makePackageBranch(sourcepackagename='widget')
285 with person_logged_in(branch.owner):
286 view = create_initialized_view(branch, '+new-recipe')
287 self.assertThat('widget-daily', Equals(view.initial_values['name']))
288
289 def test_personal_branch_initial_name(self):
290 # When a personal branch is used, the initial name is the name of the
291 # branch followed by "-daily". +junk-daily is not valid nor
292 # helpful.
293 branch = self.factory.makePersonalBranch(name='widget')
294 with person_logged_in(branch.owner):
295 view = create_initialized_view(branch, '+new-recipe')
296 self.assertThat('widget-daily', Equals(view.initial_values['name']))
297
298 def test_initial_name_exists(self):278 def test_initial_name_exists(self):
299 # If the initial name exists, a generator is used to find an unused279 # If the initial name exists, a generator is used to find an unused
300 # name by appending a numbered suffix on the end.280 # name by appending a numbered suffix on the end.
@@ -302,7 +282,7 @@
302 self.factory.makeSourcePackageRecipe(282 self.factory.makeSourcePackageRecipe(
303 owner=owner, name=u'widget-daily')283 owner=owner, name=u'widget-daily')
304 widget = self.factory.makeProduct(name='widget')284 widget = self.factory.makeProduct(name='widget')
305 branch = self.factory.makeProductBranch(widget)285 branch = self.makeBranch(target=widget)
306 with person_logged_in(owner):286 with person_logged_in(owner):
307 view = create_initialized_view(branch, '+new-recipe')287 view = create_initialized_view(branch, '+new-recipe')
308 self.assertThat('widget-daily-1', Equals(view.initial_values['name']))288 self.assertThat('widget-daily-1', Equals(view.initial_values['name']))
@@ -331,7 +311,7 @@
331 future = self.factory.makeDistroSeries(311 future = self.factory.makeDistroSeries(
332 distribution=archive.distribution,312 distribution=archive.distribution,
333 status=SeriesStatus.FUTURE)313 status=SeriesStatus.FUTURE)
334 branch = self.factory.makeAnyBranch()314 branch = self.makeBranch()
335 with person_logged_in(archive.owner):315 with person_logged_in(archive.owner):
336 view = create_initialized_view(branch, '+new-recipe')316 view = create_initialized_view(branch, '+new-recipe')
337 series = set(view.initial_values['distroseries'])317 series = set(view.initial_values['distroseries'])
@@ -342,30 +322,88 @@
342 self.assertEqual(set(), series.intersection(other_series))322 self.assertEqual(set(), series.intersection(other_series))
343323
344324
345class TestSourcePackageRecipeAddView(BzrMixin, TestCaseForRecipe):325class TestSourcePackageRecipeAddViewInitialValuesBzr(
326 TestSourcePackageRecipeAddViewInitialValuesMixin, BzrMixin,
327 TestCaseWithFactory):
328
329 def test_project_branch_initial_name(self):
330 # When a project branch is used, the initial name is the name of the
331 # project followed by "-daily".
332 widget = self.factory.makeProduct(name='widget')
333 branch = self.factory.makeProductBranch(widget)
334 with person_logged_in(branch.owner):
335 view = create_initialized_view(branch, '+new-recipe')
336 self.assertThat('widget-daily', Equals(view.initial_values['name']))
337
338 def test_package_branch_initial_name(self):
339 # When a package branch is used, the initial name is the name of the
340 # source package followed by "-daily".
341 branch = self.factory.makePackageBranch(sourcepackagename='widget')
342 with person_logged_in(branch.owner):
343 view = create_initialized_view(branch, '+new-recipe')
344 self.assertThat('widget-daily', Equals(view.initial_values['name']))
345
346 def test_personal_branch_initial_name(self):
347 # When a personal branch is used, the initial name is the name of
348 # the branch followed by "-daily". +junk-daily is neither valid nor
349 # helpful.
350 branch = self.factory.makePersonalBranch(name='widget')
351 with person_logged_in(branch.owner):
352 view = create_initialized_view(branch, '+new-recipe')
353 self.assertThat('widget-daily', Equals(view.initial_values['name']))
354
355
356class TestSourcePackageRecipeAddViewInitialValuesGit(
357 TestSourcePackageRecipeAddViewInitialValuesMixin, GitMixin,
358 TestCaseWithFactory):
359
360 def test_project_repository_initial_name(self):
361 # When a project repository is used, the initial name is the name of
362 # the project followed by "-daily".
363 widget = self.factory.makeProduct(name='widget')
364 repository = self.factory.makeGitRepository(target=widget)
365 with person_logged_in(repository.owner):
366 view = create_initialized_view(repository, '+new-recipe')
367 self.assertThat('widget-daily', Equals(view.initial_values['name']))
368
369 def test_package_repository_initial_name(self):
370 # When a package repository is used, the initial name is the name of
371 # the source package followed by "-daily".
372 dsp = self.factory.makeDistributionSourcePackage(
373 sourcepackagename='widget')
374 repository = self.factory.makeGitRepository(target=dsp)
375 with person_logged_in(repository.owner):
376 view = create_initialized_view(repository, '+new-recipe')
377 self.assertThat('widget-daily', Equals(view.initial_values['name']))
378
379 def test_personal_repository_initial_name(self):
380 # When a personal repository is used, the initial name is the name
381 # of the repository followed by "-daily". <person-name>-daily is
382 # not helpful.
383 owner = self.factory.makePerson()
384 repository = self.factory.makeGitRepository(
385 owner=owner, target=owner, name=u'widget')
386 with person_logged_in(repository.owner):
387 view = create_initialized_view(repository, '+new-recipe')
388 self.assertThat('widget-daily', Equals(view.initial_values['name']))
389
390
391class TestSourcePackageRecipeAddViewMixin:
346392
347 layer = DatabaseFunctionalLayer393 layer = DatabaseFunctionalLayer
348394
349 def makeBranch(self):
350 product = self.factory.makeProduct(
351 name='ratatouille', displayname='Ratatouille')
352 branch = self.factory.makeBranch(
353 owner=self.chef, product=product, name='veggies')
354 self.factory.makeSourcePackage(sourcepackagename='ratatouille')
355 return branch
356
357 def test_create_new_recipe_not_logged_in(self):395 def test_create_new_recipe_not_logged_in(self):
358 product = self.factory.makeProduct(396 product = self.factory.makeProduct(
359 name='ratatouille', displayname='Ratatouille')397 name='ratatouille', displayname='Ratatouille')
360 branch = self.factory.makeBranch(398 branch = self.makeBranch(
361 owner=self.chef, product=product, name='veggies')399 owner=self.chef, target=product, name=u'veggies')
362400
363 browser = self.getViewBrowser(branch, no_login=True)401 browser = self.getViewBrowser(branch, no_login=True)
364 self.assertRaises(402 self.assertRaises(
365 Unauthorized, browser.getLink('Create packaging recipe').click)403 Unauthorized, browser.getLink('Create packaging recipe').click)
366404
367 def test_create_new_recipe(self):405 def test_create_new_recipe(self):
368 branch = self.makeBranch()406 branch = self.makeBranchAndPackage()
369 # A new recipe can be created from the branch page.407 # A new recipe can be created from the branch page.
370 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)408 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)
371 browser.getLink('Create packaging recipe').click()409 browser.getLink('Create packaging recipe').click()
@@ -388,7 +426,7 @@
388 def test_create_new_recipe_private_branch(self):426 def test_create_new_recipe_private_branch(self):
389 # Recipes can't be created on private branches.427 # Recipes can't be created on private branches.
390 with person_logged_in(self.chef):428 with person_logged_in(self.chef):
391 branch = self.factory.makeBranch(429 branch = self.makeBranch(
392 owner=self.chef, information_type=InformationType.USERDATA)430 owner=self.chef, information_type=InformationType.USERDATA)
393 branch_url = canonical_url(branch)431 branch_url = canonical_url(branch)
394432
@@ -403,7 +441,7 @@
403 self.factory.makeTeam(441 self.factory.makeTeam(
404 name='good-chefs', displayname='Good Chefs', members=[self.chef])442 name='good-chefs', displayname='Good Chefs', members=[self.chef])
405 browser = self.getViewBrowser(443 browser = self.getViewBrowser(
406 self.makeBranch(), '+new-recipe', user=self.chef)444 self.makeBranchAndPackage(), '+new-recipe', user=self.chef)
407 # The options for the owner include the Good Chefs team.445 # The options for the owner include the Good Chefs team.
408 options = browser.getControl(name='field.owner.owner').displayOptions446 options = browser.getControl(name='field.owner.owner').displayOptions
409 self.assertEquals(447 self.assertEquals(
@@ -415,7 +453,7 @@
415 team = self.factory.makeTeam(453 team = self.factory.makeTeam(
416 name='good-chefs', displayname='Good Chefs', members=[self.chef])454 name='good-chefs', displayname='Good Chefs', members=[self.chef])
417 browser = self.getViewBrowser(455 browser = self.getViewBrowser(
418 self.makeBranch(), '+new-recipe', user=self.chef)456 self.makeBranchAndPackage(), '+new-recipe', user=self.chef)
419 browser.getControl(name='field.name').value = 'daily'457 browser.getControl(name='field.name').value = 'daily'
420 browser.getControl('Description').value = 'Make some food!'458 browser.getControl('Description').value = 'Make some food!'
421 browser.getControl('Other').click()459 browser.getControl('Other').click()
@@ -430,7 +468,7 @@
430468
431 def test_create_new_recipe_suggests_user(self):469 def test_create_new_recipe_suggests_user(self):
432 """The current user is suggested as a recipe owner, once."""470 """The current user is suggested as a recipe owner, once."""
433 branch = self.factory.makeBranch(owner=self.chef)471 branch = self.makeBranch(owner=self.chef)
434 text = self.getMainText(branch, '+new-recipe')472 text = self.getMainText(branch, '+new-recipe')
435 self.assertTextMatchesExpressionIgnoreWhitespace(473 self.assertTextMatchesExpressionIgnoreWhitespace(
436 r'Owner: Master Chef \(chef\) Other:', text)474 r'Owner: Master Chef \(chef\) Other:', text)
@@ -440,7 +478,7 @@
440 team = self.factory.makeTeam(478 team = self.factory.makeTeam(
441 name='branch-team', displayname='Branch Team',479 name='branch-team', displayname='Branch Team',
442 members=[self.chef])480 members=[self.chef])
443 branch = self.factory.makeBranch(owner=team)481 branch = self.makeBranch(owner=team)
444 text = self.getMainText(branch, '+new-recipe')482 text = self.getMainText(branch, '+new-recipe')
445 self.assertTextMatchesExpressionIgnoreWhitespace(483 self.assertTextMatchesExpressionIgnoreWhitespace(
446 r'Owner: Master Chef \(chef\)'484 r'Owner: Master Chef \(chef\)'
@@ -450,7 +488,7 @@
450 """If current user isn't a member of branch owner, it is ignored."""488 """If current user isn't a member of branch owner, it is ignored."""
451 team = self.factory.makeTeam(489 team = self.factory.makeTeam(
452 name='branch-team', displayname='Branch Team')490 name='branch-team', displayname='Branch Team')
453 branch = self.factory.makeBranch(owner=team)491 branch = self.makeBranch(owner=team)
454 text = self.getMainText(branch, '+new-recipe')492 text = self.getMainText(branch, '+new-recipe')
455 self.assertTextMatchesExpressionIgnoreWhitespace(493 self.assertTextMatchesExpressionIgnoreWhitespace(
456 r'Owner: Master Chef \(chef\) Other:', text)494 r'Owner: Master Chef \(chef\) Other:', text)
@@ -460,8 +498,8 @@
460 # is communicated to the user properly.498 # is communicated to the user properly.
461 product = self.factory.makeProduct(499 product = self.factory.makeProduct(
462 name='ratatouille', displayname='Ratatouille')500 name='ratatouille', displayname='Ratatouille')
463 branch = self.factory.makeBranch(501 branch = self.makeBranch(
464 owner=self.chef, product=product, name='veggies')502 owner=self.chef, target=product, name=u'veggies')
465 browser = self.getViewBrowser(branch, '+new-recipe', user=self.chef)503 browser = self.getViewBrowser(branch, '+new-recipe', user=self.chef)
466 browser.getControl('Description').value = 'Make some food!'504 browser.getControl('Description').value = 'Make some food!'
467 browser.getControl('Recipe text').value = (505 browser.getControl('Recipe text').value = (
@@ -475,8 +513,8 @@
475 if branch is None:513 if branch is None:
476 product = self.factory.makeProduct(514 product = self.factory.makeProduct(
477 name='ratatouille', displayname='Ratatouille')515 name='ratatouille', displayname='Ratatouille')
478 branch = self.factory.makeBranch(516 branch = self.makeBranch(
479 owner=self.chef, product=product, name='veggies')517 owner=self.chef, target=product, name=u'veggies')
480 browser = self.getViewBrowser(branch, '+new-recipe', user=self.chef)518 browser = self.getViewBrowser(branch, '+new-recipe', user=self.chef)
481 browser.getControl(name='field.name').value = 'daily'519 browser.getControl(name='field.name').value = 'daily'
482 browser.getControl('Description').value = 'Make some food!'520 browser.getControl('Description').value = 'Make some food!'
@@ -487,18 +525,11 @@
487 def test_create_recipe_usage(self):525 def test_create_recipe_usage(self):
488 # The error for a recipe with invalid instruction parameters should526 # The error for a recipe with invalid instruction parameters should
489 # include instruction usage.527 # include instruction usage.
490 branch = self.factory.makeBranch(name='veggies')528 branch = self.makeBranch(name=u'veggies')
491 self.factory.makeBranch(name='packaging')529 self.makeBranch(name=u'packaging')
492530
493 browser = self.createRecipe(531 browser = self.createRecipe(
494 dedent('''\532 self.getMinimalRecipeText(branch) + "merge\n", branch=branch)
495 # bzr-builder format 0.2 deb-version 0+{revno}
496 %(branch)s
497 merge
498 ''' % {
499 'branch': branch.bzr_identity,
500 }),
501 branch=branch)
502 self.assertEqual(533 self.assertEqual(
503 'Error parsing recipe:3:6: '534 'Error parsing recipe:3:6: '
504 'End of line while looking for the branch id.\n'535 'End of line while looking for the branch id.\n'
@@ -506,7 +537,8 @@
506 get_feedback_messages(browser.contents)[1])537 get_feedback_messages(browser.contents)[1])
507538
508 def test_create_recipe_no_distroseries(self):539 def test_create_recipe_no_distroseries(self):
509 browser = self.getViewBrowser(self.makeBranch(), '+new-recipe')540 browser = self.getViewBrowser(
541 self.makeBranchAndPackage(), '+new-recipe')
510 browser.getControl(name='field.name').value = 'daily'542 browser.getControl(name='field.name').value = 'daily'
511 browser.getControl('Description').value = 'Make some food!'543 browser.getControl('Description').value = 'Make some food!'
512 browser.getControl(name='field.distroseries').value = []544 browser.getControl(name='field.distroseries').value = []
@@ -518,7 +550,11 @@
518 def test_create_recipe_bad_base_branch(self):550 def test_create_recipe_bad_base_branch(self):
519 # If a user tries to create source package recipe with a bad base551 # If a user tries to create source package recipe with a bad base
520 # branch location, they should get an error.552 # branch location, they should get an error.
521 browser = self.createRecipe(MINIMAL_RECIPE_TEXT_BZR % 'foo')553 browser = self.createRecipe(
554 self.minimal_recipe_text.splitlines()[0] + '\nfoo\n')
555 # This page doesn't know whether the user was aiming for a Bazaar
556 # branch or a Git repository; the error message always says
557 # "branch".
522 self.assertEqual(558 self.assertEqual(
523 get_feedback_messages(browser.contents)[1],559 get_feedback_messages(browser.contents)[1],
524 'foo is not a branch on Launchpad.')560 'foo is not a branch on Launchpad.')
@@ -528,28 +564,27 @@
528 # instruction branch location, they should get an error.564 # instruction branch location, they should get an error.
529 product = self.factory.makeProduct(565 product = self.factory.makeProduct(
530 name='ratatouille', displayname='Ratatouille')566 name='ratatouille', displayname='Ratatouille')
531 branch = self.factory.makeBranch(567 branch = self.makeBranch(
532 owner=self.chef, product=product, name='veggies')568 owner=self.chef, target=product, name=u'veggies')
533 recipe = MINIMAL_RECIPE_TEXT_BZR % branch.bzr_identity569 recipe = self.getMinimalRecipeText(branch)
534 recipe += 'nest packaging foo debian'570 recipe += 'nest packaging foo debian'
535 browser = self.createRecipe(recipe, branch)571 browser = self.createRecipe(recipe, branch)
536 self.assertEqual(572 self.assertEqual(
537 get_feedback_messages(browser.contents)[1],573 get_feedback_messages(browser.contents)[1],
538 'foo is not a branch on Launchpad.')574 'foo %s' % self.no_such_object_message)
539575
540 def test_create_recipe_format_too_new(self):576 def test_create_recipe_format_too_new(self):
541 # If the recipe's format version is too new, we should notify the577 # If the recipe's format version is too new, we should notify the
542 # user.578 # user.
543 product = self.factory.makeProduct(579 product = self.factory.makeProduct(
544 name='ratatouille', displayname='Ratatouille')580 name='ratatouille', displayname='Ratatouille')
545 branch = self.factory.makeBranch(581 branch = self.makeBranch(
546 owner=self.chef, product=product, name='veggies')582 owner=self.chef, target=product, name=u'veggies')
547583
548 with recipe_parser_newest_version(145.115):584 with recipe_parser_newest_version(145.115):
549 recipe = dedent(u'''\585 recipe = re.sub(
550 # bzr-builder format 145.115 deb-version {debupstream}-0~{revno}586 'format [^ ]*', 'format 145.115',
551 %s587 self.getMinimalRecipeText(branch))
552 ''') % branch.bzr_identity
553 browser = self.createRecipe(recipe, branch)588 browser = self.createRecipe(recipe, branch)
554 self.assertEqual(589 self.assertEqual(
555 get_feedback_messages(browser.contents)[1],590 get_feedback_messages(browser.contents)[1],
@@ -564,8 +599,8 @@
564599
565 product = self.factory.makeProduct(600 product = self.factory.makeProduct(
566 name='ratatouille', displayname='Ratatouille')601 name='ratatouille', displayname='Ratatouille')
567 branch = self.factory.makeBranch(602 branch = self.makeBranch(
568 owner=self.chef, product=product, name='veggies')603 owner=self.chef, target=product, name=u'veggies')
569604
570 # A new recipe can be created from the branch page.605 # A new recipe can be created from the branch page.
571 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)606 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)
@@ -583,15 +618,16 @@
583 def test_create_recipe_private_branch(self):618 def test_create_recipe_private_branch(self):
584 # If a user tries to create source package recipe with a private619 # If a user tries to create source package recipe with a private
585 # base branch, they should get an error.620 # base branch, they should get an error.
586 branch = self.factory.makeAnyBranch(621 branch = self.makeBranch(
587 owner=self.user, information_type=InformationType.USERDATA)622 owner=self.user, information_type=InformationType.USERDATA)
588 with person_logged_in(self.user):623 with person_logged_in(self.user):
589 bzr_identity = branch.bzr_identity624 identity = self.getRepository(branch).identity
590 recipe_text = MINIMAL_RECIPE_TEXT_BZR % bzr_identity625 recipe_text = self.getMinimalRecipeText(branch)
591 browser = self.createRecipe(recipe_text)626 browser = self.createRecipe(recipe_text)
592 self.assertEqual(627 self.assertEqual(
593 get_feedback_messages(browser.contents)[1],628 get_feedback_messages(browser.contents)[1],
594 'Recipe may not refer to private branch: %s' % bzr_identity)629 'Recipe may not refer to private %s: %s' % (
630 self.branch_type, identity))
595631
596 def _test_new_recipe_with_no_related_branches(self, branch):632 def _test_new_recipe_with_no_related_branches(self, branch):
597 # The Related Branches section should not appear if there are no633 # The Related Branches section should not appear if there are no
@@ -607,72 +643,20 @@
607643
608 def test_new_product_branch_with_no_related_branches_recipe(self):644 def test_new_product_branch_with_no_related_branches_recipe(self):
609 # We can create a new recipe off a product branch.645 # We can create a new recipe off a product branch.
610 branch = self.factory.makeBranch()646 branch = self.makeBranch()
611 self._test_new_recipe_with_no_related_branches(branch)647 self._test_new_recipe_with_no_related_branches(branch)
612648
613 def test_new_package_branch_with_no_linked_branches_recipe(self):649 def test_new_package_branch_with_no_linked_branches_recipe(self):
614 # We can create a new recipe off a sourcepackage branch where the650 # We can create a new recipe off a sourcepackage branch where the
615 # sourcepackage has no linked branches.651 # sourcepackage has no linked branches.
616 branch = self.factory.makePackageBranch()652 branch = self.makePackageBranch()
617 self._test_new_recipe_with_no_related_branches(branch)653 self._test_new_recipe_with_no_related_branches(branch)
618654
619 def test_new_recipe_with_package_branches(self):
620 # The series branches table should not appear if there are none.
621 (branch, related_series_branch_info, related_package_branches) = (
622 self.factory.makeRelatedBranches(with_series_branches=False))
623 browser = self.getUserBrowser(
624 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
625 soup = BeautifulSoup(browser.contents)
626 related_branches = soup.find('fieldset', {'id': 'related-branches'})
627 self.assertIsNot(related_branches, None)
628 related_branches = soup.find(
629 'div', {'id': 'related-package-branches'})
630 self.assertIsNot(related_branches, None)
631 related_branches = soup.find(
632 'div', {'id': 'related-series-branches'})
633 self.assertIs(related_branches, None)
634
635 def test_new_recipe_with_series_branches(self):
636 # The package branches table should not appear if there are none.
637 (branch, related_series_branch_info, related_package_branches) = (
638 self.factory.makeRelatedBranches(with_package_branches=False))
639 browser = self.getUserBrowser(
640 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
641 soup = BeautifulSoup(browser.contents)
642 related_branches = soup.find('fieldset', {'id': 'related-branches'})
643 self.assertIsNot(related_branches, None)
644 related_branches = soup.find(
645 'div', {'id': 'related-series-branches'})
646 self.assertIsNot(related_branches, None)
647 related_branches = soup.find(
648 'div', {'id': 'related-package-branches'})
649 self.assertIs(related_branches, None)
650
651 def test_new_product_branch_recipe_with_related_branches(self):
652 # The related branches should be rendered correctly on the page.
653 (branch, related_series_branch_info,
654 related_package_branch_info) = self.factory.makeRelatedBranches()
655 browser = self.getUserBrowser(
656 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
657 self.checkRelatedBranches(
658 related_series_branch_info, related_package_branch_info,
659 browser.contents)
660
661 def test_new_sourcepackage_branch_recipe_with_related_branches(self):
662 # The related branches should be rendered correctly on the page.
663 reference_branch = self.factory.makePackageBranch()
664 (branch, ignore, related_package_branch_info) = (
665 self.factory.makeRelatedBranches(reference_branch))
666 browser = self.getUserBrowser(
667 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
668 self.checkRelatedBranches(
669 set(), related_package_branch_info, browser.contents)
670
671 def test_ppa_selector_not_shown_if_user_has_no_ppas(self):655 def test_ppa_selector_not_shown_if_user_has_no_ppas(self):
672 # If the user creating a recipe has no existing PPAs, the selector656 # If the user creating a recipe has no existing PPAs, the selector
673 # isn't shown, but the field to enter a new PPA name is.657 # isn't shown, but the field to enter a new PPA name is.
674 self.user = self.factory.makePerson()658 self.user = self.factory.makePerson()
675 branch = self.factory.makeAnyBranch()659 branch = self.makeBranch()
676 with person_logged_in(self.user):660 with person_logged_in(self.user):
677 content = self.getMainContent(branch, '+new-recipe')661 content = self.getMainContent(branch, '+new-recipe')
678 ppa_name = content.find(attrs={'id': 'field.ppa_name'})662 ppa_name = content.find(attrs={'id': 'field.ppa_name'})
@@ -692,7 +676,7 @@
692 # If the user creating a recipe has existing PPAs, the selector is676 # If the user creating a recipe has existing PPAs, the selector is
693 # shown, along with radio buttons to decide whether to use an existing677 # shown, along with radio buttons to decide whether to use an existing
694 # ppa or to create a new one.678 # ppa or to create a new one.
695 branch = self.factory.makeAnyBranch()679 branch = self.makeBranch()
696 with person_logged_in(self.user):680 with person_logged_in(self.user):
697 content = self.getMainContent(branch, '+new-recipe')681 content = self.getMainContent(branch, '+new-recipe')
698 ppa_name = content.find(attrs={'id': 'field.ppa_name'})682 ppa_name = content.find(attrs={'id': 'field.ppa_name'})
@@ -712,7 +696,7 @@
712 def test_create_new_ppa(self):696 def test_create_new_ppa(self):
713 # If the user doesn't have any PPAs, a new once can be created.697 # If the user doesn't have any PPAs, a new once can be created.
714 self.user = self.factory.makePerson(name='eric')698 self.user = self.factory.makePerson(name='eric')
715 branch = self.factory.makeAnyBranch()699 branch = self.makeBranch()
716700
717 # A new recipe can be created from the branch page.701 # A new recipe can be created from the branch page.
718 browser = self.getUserBrowser(canonical_url(branch), user=self.user)702 browser = self.getUserBrowser(canonical_url(branch), user=self.user)
@@ -737,7 +721,7 @@
737 # Make a PPA called 'ppa' using the default.721 # Make a PPA called 'ppa' using the default.
738 with person_logged_in(self.user):722 with person_logged_in(self.user):
739 self.user.createPPA(name='foo')723 self.user.createPPA(name='foo')
740 branch = self.factory.makeAnyBranch()724 branch = self.makeBranch()
741725
742 # A new recipe can be created from the branch page.726 # A new recipe can be created from the branch page.
743 browser = self.getUserBrowser(canonical_url(branch), user=self.user)727 browser = self.getUserBrowser(canonical_url(branch), user=self.user)
@@ -756,7 +740,7 @@
756 # If a new PPA is being created, and the user has not specified a740 # If a new PPA is being created, and the user has not specified a
757 # name, an error is shown.741 # name, an error is shown.
758 self.user = self.factory.makePerson(name='eric')742 self.user = self.factory.makePerson(name='eric')
759 branch = self.factory.makeAnyBranch()743 branch = self.makeBranch()
760744
761 # A new recipe can be created from the branch page.745 # A new recipe can be created from the branch page.
762 browser = self.getUserBrowser(canonical_url(branch), user=self.user)746 browser = self.getUserBrowser(canonical_url(branch), user=self.user)
@@ -779,7 +763,7 @@
779 with person_logged_in(team.teamowner):763 with person_logged_in(team.teamowner):
780 team.setMembershipData(764 team.setMembershipData(
781 self.user, TeamMembershipStatus.ADMIN, team.teamowner)765 self.user, TeamMembershipStatus.ADMIN, team.teamowner)
782 branch = self.factory.makeAnyBranch(owner=team)766 branch = self.makeBranch(owner=team)
783767
784 # A new recipe can be created from the branch page.768 # A new recipe can be created from the branch page.
785 browser = self.getUserBrowser(canonical_url(branch), user=self.user)769 browser = self.getUserBrowser(canonical_url(branch), user=self.user)
@@ -799,6 +783,83 @@
799 self.assertIsNot(None, new_ppa)783 self.assertIsNot(None, new_ppa)
800784
801785
786class TestSourcePackageRecipeAddViewBzr(
787 TestSourcePackageRecipeAddViewMixin, BzrMixin, TestCaseForRecipe):
788
789 def makeBranchAndPackage(self):
790 product = self.factory.makeProduct(
791 name='ratatouille', displayname='Ratatouille')
792 branch = self.factory.makeBranch(
793 owner=self.chef, product=product, name='veggies')
794 self.factory.makeSourcePackage(sourcepackagename='ratatouille')
795 return branch
796
797 def test_new_recipe_with_package_branches(self):
798 # The series branches table should not appear if there are none.
799 branch, related_series_branch_info, related_package_branches = (
800 self.makeRelatedBranches(with_series_branches=False))
801 browser = self.getUserBrowser(
802 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
803 soup = BeautifulSoup(browser.contents)
804 related_branches = soup.find('fieldset', {'id': 'related-branches'})
805 self.assertIsNot(related_branches, None)
806 related_branches = soup.find(
807 'div', {'id': 'related-package-branches'})
808 self.assertIsNot(related_branches, None)
809 related_branches = soup.find(
810 'div', {'id': 'related-series-branches'})
811 self.assertIs(related_branches, None)
812
813 def test_new_recipe_with_series_branches(self):
814 # The package branches table should not appear if there are none.
815 branch, related_series_branch_info, related_package_branches = (
816 self.makeRelatedBranches(with_package_branches=False))
817 browser = self.getUserBrowser(
818 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
819 soup = BeautifulSoup(browser.contents)
820 related_branches = soup.find('fieldset', {'id': 'related-branches'})
821 self.assertIsNot(related_branches, None)
822 related_branches = soup.find(
823 'div', {'id': 'related-series-branches'})
824 self.assertIsNot(related_branches, None)
825 related_branches = soup.find(
826 'div', {'id': 'related-package-branches'})
827 self.assertIs(related_branches, None)
828
829 def test_new_product_branch_recipe_with_related_branches(self):
830 # The related branches should be rendered correctly on the page.
831 branch, related_series_branch_info, related_package_branch_info = (
832 self.makeRelatedBranches())
833 browser = self.getUserBrowser(
834 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
835 self.checkRelatedBranches(
836 related_series_branch_info, related_package_branch_info,
837 browser.contents)
838
839 def test_new_sourcepackage_branch_recipe_with_related_branches(self):
840 # The related branches should be rendered correctly on the page.
841 reference_branch = self.makePackageBranch()
842 branch, _, related_package_branch_info = (
843 self.makeRelatedBranches(reference_branch))
844 browser = self.getUserBrowser(
845 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
846 self.checkRelatedBranches(
847 set(), related_package_branch_info, browser.contents)
848
849
850class TestSourcePackageRecipeAddViewGit(
851 TestSourcePackageRecipeAddViewMixin, GitMixin, TestCaseForRecipe):
852
853 def makeBranchAndPackage(self):
854 product = self.factory.makeProduct(
855 name='ratatouille', displayname='Ratatouille')
856 repository = self.factory.makeGitRepository(
857 owner=self.chef, target=product, name=u'veggies')
858 self.factory.makeDistributionSourcePackage(
859 sourcepackagename='ratatouille')
860 return repository
861
862
802class TestSourcePackageRecipeEditViewMixin:863class TestSourcePackageRecipeEditViewMixin:
803 """Test the editing behaviour of a source package recipe."""864 """Test the editing behaviour of a source package recipe."""
804865
805866
=== modified file 'lib/lp/code/browser/tests/test_tales.py'
--- lib/lp/code/browser/tests/test_tales.py 2015-09-11 06:04:36 +0000
+++ lib/lp/code/browser/tests/test_tales.py 2016-01-20 12:22:22 +0000
@@ -235,7 +235,7 @@
235 '<a href="%s">%s recipe build in ubuntu shiny</a> '235 '<a href="%s">%s recipe build in ubuntu shiny</a> '
236 '[~eric/ubuntu/ppa]'236 '[~eric/ubuntu/ppa]'
237 % (canonical_url(build, path_only_if_possible=True),237 % (canonical_url(build, path_only_if_possible=True),
238 build.recipe.base_branch.unique_name)))238 build.recipe.base.unique_name)))
239239
240 def test_link_no_recipe(self):240 def test_link_no_recipe(self):
241 eric = self.factory.makePerson(name='eric')241 eric = self.factory.makePerson(name='eric')
242242
=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py 2016-01-12 01:24:06 +0000
+++ lib/lp/code/errors.py 2016-01-20 12:22:22 +0000
@@ -30,6 +30,7 @@
30 'ClaimReviewFailed',30 'ClaimReviewFailed',
31 'DiffNotFound',31 'DiffNotFound',
32 'GitDefaultConflict',32 'GitDefaultConflict',
33 'GitRecipesFeatureDisabled',
33 'GitRepositoryCreationException',34 'GitRepositoryCreationException',
34 'GitRepositoryCreationFault',35 'GitRepositoryCreationFault',
35 'GitRepositoryCreationForbidden',36 'GitRepositoryCreationForbidden',
@@ -485,6 +486,15 @@
485 self.newest_supported = newest_supported486 self.newest_supported = newest_supported
486487
487488
489@error_status(httplib.UNAUTHORIZED)
490class GitRecipesFeatureDisabled(Exception):
491 """Only certain users can create new Git recipes."""
492
493 def __init__(self):
494 message = "You do not have permission to create Git recipes."
495 Exception.__init__(self, message)
496
497
488@error_status(httplib.BAD_REQUEST)498@error_status(httplib.BAD_REQUEST)
489class RecipeBuildException(Exception):499class RecipeBuildException(Exception):
490500
491501
=== modified file 'lib/lp/code/help/related-recipes.html'
--- lib/lp/code/help/related-recipes.html 2010-11-04 22:50:52 +0000
+++ lib/lp/code/help/related-recipes.html 2016-01-20 12:22:22 +0000
@@ -18,8 +18,8 @@
18 </p>18 </p>
1919
20 <p>A "recipe" is a description of the steps Launchpad's package builder20 <p>A "recipe" is a description of the steps Launchpad's package builder
21 should take to construct a source package from a set of Bazaar branches21 should take to construct a source package from a set of Bazaar or Git
22 that you specify.22 branches that you specify.
23 </p>23 </p>
2424
25 <p>25 <p>
2626
=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
--- lib/lp/code/interfaces/sourcepackagerecipe.py 2016-01-15 11:50:57 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2016-01-20 12:22:22 +0000
@@ -8,6 +8,7 @@
88
99
10__all__ = [10__all__ = [
11 'GIT_RECIPES_FEATURE_FLAG',
11 'IRecipeBranchSource',12 'IRecipeBranchSource',
12 'ISourcePackageRecipe',13 'ISourcePackageRecipe',
13 'ISourcePackageRecipeData',14 'ISourcePackageRecipeData',
@@ -68,6 +69,9 @@
68from lp.soyuz.interfaces.archive import IArchive69from lp.soyuz.interfaces.archive import IArchive
6970
7071
72GIT_RECIPES_FEATURE_FLAG = u'code.git.recipes.enabled'
73
74
71MINIMAL_RECIPE_TEXT_BZR = dedent(u'''\75MINIMAL_RECIPE_TEXT_BZR = dedent(u'''\
72 # bzr-builder format 0.3 deb-version {debupstream}-0~{revno}76 # bzr-builder format 0.3 deb-version {debupstream}-0~{revno}
73 %s77 %s
7478
=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
--- lib/lp/code/model/sourcepackagerecipe.py 2016-01-15 11:50:57 +0000
+++ lib/lp/code/model/sourcepackagerecipe.py 2016-01-20 12:22:22 +0000
@@ -41,8 +41,10 @@
41from lp.code.errors import (41from lp.code.errors import (
42 BuildAlreadyPending,42 BuildAlreadyPending,
43 BuildNotAllowedForDistro,43 BuildNotAllowedForDistro,
44 GitRecipesFeatureDisabled,
44 )45 )
45from lp.code.interfaces.sourcepackagerecipe import (46from lp.code.interfaces.sourcepackagerecipe import (
47 GIT_RECIPES_FEATURE_FLAG,
46 IRecipeBranchSource,48 IRecipeBranchSource,
47 ISourcePackageRecipe,49 ISourcePackageRecipe,
48 ISourcePackageRecipeData,50 ISourcePackageRecipeData,
@@ -68,6 +70,7 @@
68 IStore,70 IStore,
69 )71 )
70from lp.services.database.stormexpr import Greatest72from lp.services.database.stormexpr import Greatest
73from lp.services.features import getFeatureFlag
71from lp.services.propertycache import (74from lp.services.propertycache import (
72 cachedproperty,75 cachedproperty,
73 get_property_cache,76 get_property_cache,
@@ -227,6 +230,15 @@
227 sprecipe.date_created = date_created230 sprecipe.date_created = date_created
228 sprecipe.date_last_modified = date_created231 sprecipe.date_last_modified = date_created
229 store.add(sprecipe)232 store.add(sprecipe)
233 # We can only do this feature flag check at the end, because we need
234 # to have constructed the SourcePackageRecipeData in order to know
235 # whether it refers to a Git repository and then we need the
236 # SourcePackageRecipe to respect DB constraints before doing
237 # anything else. The transaction will be aborted either way if the
238 # check fails.
239 if (sprecipe.base_git_repository is not None and
240 not getFeatureFlag(GIT_RECIPES_FEATURE_FLAG)):
241 raise GitRecipesFeatureDisabled
230 return sprecipe242 return sprecipe
231243
232 @staticmethod244 @staticmethod
233245
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2016-01-12 04:34:09 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2016-01-20 12:22:22 +0000
@@ -72,6 +72,7 @@
72 IGitRepositoryView,72 IGitRepositoryView,
73 )73 )
74from lp.code.interfaces.revision import IRevisionSet74from lp.code.interfaces.revision import IRevisionSet
75from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG
75from lp.code.model.branchmergeproposal import BranchMergeProposal76from lp.code.model.branchmergeproposal import BranchMergeProposal
76from lp.code.model.branchmergeproposaljob import (77from lp.code.model.branchmergeproposaljob import (
77 BranchMergeProposalJob,78 BranchMergeProposalJob,
@@ -454,12 +455,14 @@
454455
455 def test_destroySelf_with_SourcePackageRecipe(self):456 def test_destroySelf_with_SourcePackageRecipe(self):
456 # If repository is a base_git_repository in a recipe, it is deleted.457 # If repository is a base_git_repository in a recipe, it is deleted.
458 self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
457 recipe = self.factory.makeSourcePackageRecipe(459 recipe = self.factory.makeSourcePackageRecipe(
458 branches=self.factory.makeGitRefs(owner=self.user))460 branches=self.factory.makeGitRefs(owner=self.user))
459 recipe.base_git_repository.destroySelf(break_references=True)461 recipe.base_git_repository.destroySelf(break_references=True)
460462
461 def test_destroySelf_with_SourcePackageRecipe_as_non_base(self):463 def test_destroySelf_with_SourcePackageRecipe_as_non_base(self):
462 # If repository is referred to by a recipe, it is deleted.464 # If repository is referred to by a recipe, it is deleted.
465 self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
463 [ref1] = self.factory.makeGitRefs(owner=self.user)466 [ref1] = self.factory.makeGitRefs(owner=self.user)
464 [ref2] = self.factory.makeGitRefs(owner=self.user)467 [ref2] = self.factory.makeGitRefs(owner=self.user)
465 self.factory.makeSourcePackageRecipe(branches=[ref1, ref2])468 self.factory.makeSourcePackageRecipe(branches=[ref1, ref2])
@@ -698,6 +701,7 @@
698701
699 def test_deletionRequirements_with_SourcePackageRecipe(self):702 def test_deletionRequirements_with_SourcePackageRecipe(self):
700 # Recipes are listed as deletion requirements.703 # Recipes are listed as deletion requirements.
704 self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
701 recipe = self.factory.makeSourcePackageRecipe(705 recipe = self.factory.makeSourcePackageRecipe(
702 branches=self.factory.makeGitRefs())706 branches=self.factory.makeGitRefs())
703 self.assertEqual(707 self.assertEqual(
@@ -1845,6 +1849,10 @@
18451849
1846 layer = ZopelessDatabaseLayer1850 layer = ZopelessDatabaseLayer
18471851
1852 def setUp(self):
1853 super(TestGitRepositoryMarkRecipesStale, self).setUp()
1854 self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
1855
1848 def test_base_repository_recipe(self):1856 def test_base_repository_recipe(self):
1849 # On ref changes, recipes where this ref is the base become stale.1857 # On ref changes, recipes where this ref is the base become stale.
1850 [ref] = self.factory.makeGitRefs()1858 [ref] = self.factory.makeGitRefs()
18511859
=== modified file 'lib/lp/code/model/tests/test_hasrecipes.py'
--- lib/lp/code/model/tests/test_hasrecipes.py 2016-01-14 17:24:14 +0000
+++ lib/lp/code/model/tests/test_hasrecipes.py 2016-01-20 12:22:22 +0000
@@ -6,6 +6,8 @@
6__metaclass__ = type6__metaclass__ = type
77
8from lp.code.interfaces.hasrecipes import IHasRecipes8from lp.code.interfaces.hasrecipes import IHasRecipes
9from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG
10from lp.services.features.testing import FeatureFixture
9from lp.testing import TestCaseWithFactory11from lp.testing import TestCaseWithFactory
10from lp.testing.layers import DatabaseFunctionalLayer12from lp.testing.layers import DatabaseFunctionalLayer
1113
@@ -47,6 +49,7 @@
47 def test_git_repository_recipes(self):49 def test_git_repository_recipes(self):
48 # IGitRepository.recipes should provide all the SourcePackageRecipes50 # IGitRepository.recipes should provide all the SourcePackageRecipes
49 # attached to that repository.51 # attached to that repository.
52 self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
50 base_ref1, base_ref2 = self.factory.makeGitRefs(53 base_ref1, base_ref2 = self.factory.makeGitRefs(
51 paths=[u"refs/heads/ref1", u"refs/heads/ref2"])54 paths=[u"refs/heads/ref1", u"refs/heads/ref2"])
52 [other_ref] = self.factory.makeGitRefs()55 [other_ref] = self.factory.makeGitRefs()
@@ -58,6 +61,7 @@
58 def test_git_repository_recipes_nonbase(self):61 def test_git_repository_recipes_nonbase(self):
59 # IGitRepository.recipes should provide all the SourcePackageRecipes62 # IGitRepository.recipes should provide all the SourcePackageRecipes
60 # that refer to the repository, even as a non-base branch.63 # that refer to the repository, even as a non-base branch.
64 self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
61 [base_ref] = self.factory.makeGitRefs()65 [base_ref] = self.factory.makeGitRefs()
62 [nonbase_ref] = self.factory.makeGitRefs()66 [nonbase_ref] = self.factory.makeGitRefs()
63 [other_ref] = self.factory.makeGitRefs()67 [other_ref] = self.factory.makeGitRefs()
@@ -88,6 +92,7 @@
88 def test_product_recipes(self):92 def test_product_recipes(self):
89 # IProduct.recipes should provide all the SourcePackageRecipes93 # IProduct.recipes should provide all the SourcePackageRecipes
90 # attached to that product's branches and Git repositories.94 # attached to that product's branches and Git repositories.
95 self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
91 product = self.factory.makeProduct()96 product = self.factory.makeProduct()
92 branch = self.factory.makeBranch(product=product)97 branch = self.factory.makeBranch(product=product)
93 [ref] = self.factory.makeGitRefs(target=product)98 [ref] = self.factory.makeGitRefs(target=product)
9499
=== modified file 'lib/lp/code/model/tests/test_recipebuilder.py'
--- lib/lp/code/model/tests/test_recipebuilder.py 2016-01-15 11:50:57 +0000
+++ lib/lp/code/model/tests/test_recipebuilder.py 2016-01-20 12:22:22 +0000
@@ -29,11 +29,13 @@
29 TestHandleStatusMixin,29 TestHandleStatusMixin,
30 TestVerifySuccessfulBuildMixin,30 TestVerifySuccessfulBuildMixin,
31 )31 )
32from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG
32from lp.code.model.recipebuilder import RecipeBuildBehaviour33from lp.code.model.recipebuilder import RecipeBuildBehaviour
33from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild34from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
34from lp.registry.interfaces.pocket import PackagePublishingPocket35from lp.registry.interfaces.pocket import PackagePublishingPocket
35from lp.registry.interfaces.series import SeriesStatus36from lp.registry.interfaces.series import SeriesStatus
36from lp.services.config import config37from lp.services.config import config
38from lp.services.features.testing import FeatureFixture
37from lp.services.log.logger import BufferLogger39from lp.services.log.logger import BufferLogger
38from lp.soyuz.adapters.archivedependencies import (40from lp.soyuz.adapters.archivedependencies import (
39 get_sources_list_for_building,41 get_sources_list_for_building,
@@ -258,6 +260,7 @@
258 self.assertEqual(args["archives"], expected_archives)260 self.assertEqual(args["archives"], expected_archives)
259261
260 def test_extraBuildArgs_git(self):262 def test_extraBuildArgs_git(self):
263 self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
261 job = self.makeJob(git=True)264 job = self.makeJob(git=True)
262 distroarchseries = job.build.distroseries.architectures[0]265 distroarchseries = job.build.distroseries.architectures[0]
263 expected_archives = get_sources_list_for_building(266 expected_archives = get_sources_list_for_building(
264267
=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2016-01-12 03:54:38 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2016-01-20 12:22:22 +0000
@@ -31,11 +31,13 @@
31from lp.buildmaster.model.buildqueue import BuildQueue31from lp.buildmaster.model.buildqueue import BuildQueue
32from lp.code.errors import (32from lp.code.errors import (
33 BuildAlreadyPending,33 BuildAlreadyPending,
34 GitRecipesFeatureDisabled,
34 PrivateBranchRecipe,35 PrivateBranchRecipe,
35 PrivateGitRepositoryRecipe,36 PrivateGitRepositoryRecipe,
36 TooNewRecipeFormat,37 TooNewRecipeFormat,
37 )38 )
38from lp.code.interfaces.sourcepackagerecipe import (39from lp.code.interfaces.sourcepackagerecipe import (
40 GIT_RECIPES_FEATURE_FLAG,
39 ISourcePackageRecipe,41 ISourcePackageRecipe,
40 ISourcePackageRecipeSource,42 ISourcePackageRecipeSource,
41 ISourcePackageRecipeView,43 ISourcePackageRecipeView,
@@ -56,6 +58,7 @@
56from lp.registry.interfaces.series import SeriesStatus58from lp.registry.interfaces.series import SeriesStatus
57from lp.services.database.bulk import load_referencing59from lp.services.database.bulk import load_referencing
58from lp.services.database.constants import UTC_NOW60from lp.services.database.constants import UTC_NOW
61from lp.services.features.testing import FeatureFixture
59from lp.services.propertycache import clear_property_cache62from lp.services.propertycache import clear_property_cache
60from lp.services.webapp.authorization import check_permission63from lp.services.webapp.authorization import check_permission
61from lp.services.webapp.publisher import canonical_url64from lp.services.webapp.publisher import canonical_url
@@ -122,6 +125,10 @@
122 branch_type = "repository"125 branch_type = "repository"
123 recipe_id = "git-build-recipe"126 recipe_id = "git-build-recipe"
124127
128 def setUp(self):
129 super(GitMixin, self).setUp()
130 self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}))
131
125 def makeBranch(self, **kwargs):132 def makeBranch(self, **kwargs):
126 return self.factory.makeGitRefs(**kwargs)[0]133 return self.factory.makeGitRefs(**kwargs)[0]
127134
@@ -823,6 +830,12 @@
823 TestSourcePackageRecipeMixin, GitMixin, TestCaseWithFactory):830 TestSourcePackageRecipeMixin, GitMixin, TestCaseWithFactory):
824 """Test `SourcePackageRecipe` objects for Git."""831 """Test `SourcePackageRecipe` objects for Git."""
825832
833 def test_feature_flag_disabled(self):
834 # Without a feature flag, we will not create new Git recipes.
835 self.useFixture(FeatureFixture({}))
836 self.assertRaises(
837 GitRecipesFeatureDisabled, self.makeSourcePackageRecipe)
838
826839
827class TestRecipeBranchRoundTrippingMixin:840class TestRecipeBranchRoundTrippingMixin:
828841
829842
=== modified file 'lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt'
--- lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt 2016-01-12 15:10:57 +0000
+++ lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt 2016-01-20 12:22:22 +0000
@@ -74,15 +74,21 @@
74Create a new sample repository, some branches in it, and some source package74Create a new sample repository, some branches in it, and some source package
75recipes to go along with them.75recipes to go along with them.
7676
77 >>> from lp.code.interfaces.sourcepackagerecipe import (
78 ... GIT_RECIPES_FEATURE_FLAG,
79 ... )
80 >>> from lp.services.features.testing import FeatureFixture
81
77 >>> login('foo.bar@canonical.com')82 >>> login('foo.bar@canonical.com')
78 >>> repository = factory.makeGitRepository()83 >>> repository = factory.makeGitRepository()
79 >>> ref1, ref2, ref3 = factory.makeGitRefs(84 >>> ref1, ref2, ref3 = factory.makeGitRefs(
80 ... repository=repository,85 ... repository=repository,
81 ... paths=[u"refs/heads/a", u"refs/heads/b", u"refs/heads/c"])86 ... paths=[u"refs/heads/a", u"refs/heads/b", u"refs/heads/c"])
82 >>> recipe1a = factory.makeSourcePackageRecipe(branches=[ref1])87 >>> with FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}):
83 >>> recipe1b = factory.makeSourcePackageRecipe(branches=[ref1])88 ... recipe1a = factory.makeSourcePackageRecipe(branches=[ref1])
84 >>> recipe2 = factory.makeSourcePackageRecipe(branches=[ref2])89 ... recipe1b = factory.makeSourcePackageRecipe(branches=[ref1])
85 >>> recipe3 = factory.makeSourcePackageRecipe(branches=[ref3])90 ... recipe2 = factory.makeSourcePackageRecipe(branches=[ref2])
91 ... recipe3 = factory.makeSourcePackageRecipe(branches=[ref3])
8692
87Keep these urls, including the target url. We'll use these later.93Keep these urls, including the target url. We'll use these later.
8894
8995
=== modified file 'lib/lp/code/templates/gitref-recipes.pt'
--- lib/lp/code/templates/gitref-recipes.pt 2016-01-12 15:10:57 +0000
+++ lib/lp/code/templates/gitref-recipes.pt 2016-01-20 12:22:22 +0000
@@ -2,6 +2,7 @@
2 xmlns:tal="http://xml.zope.org/namespaces/tal"2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 tal:define="context_menu view/context/menu:context"
5 id="related-recipes">6 id="related-recipes">
67
7 <h3>Related source package recipes</h3>8 <h3>Related source package recipes</h3>
@@ -14,6 +15,12 @@
14 <a href="/+help-code/related-recipes.html" target="help"15 <a href="/+help-code/related-recipes.html" target="help"
15 class="sprite maybe action-icon">(?)</a>16 class="sprite maybe action-icon">(?)</a>
16 </div>17 </div>
18
19 <span
20 tal:define="link context_menu/create_recipe"
21 tal:condition="link/enabled"
22 tal:replace="structure link/render"
23 />
17 </div>24 </div>
1825
19</div>26</div>
2027
=== modified file 'lib/lp/code/templates/gitrepository-recipes.pt'
--- lib/lp/code/templates/gitrepository-recipes.pt 2016-01-12 15:10:57 +0000
+++ lib/lp/code/templates/gitrepository-recipes.pt 2016-01-20 12:22:22 +0000
@@ -2,6 +2,7 @@
2 xmlns:tal="http://xml.zope.org/namespaces/tal"2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 tal:define="context_menu view/context/menu:context"
5 id="related-recipes">6 id="related-recipes">
67
7 <h3>Related source package recipes</h3>8 <h3>Related source package recipes</h3>
@@ -14,6 +15,12 @@
14 <a href="/+help-code/related-recipes.html" target="help"15 <a href="/+help-code/related-recipes.html" target="help"
15 class="sprite maybe action-icon">(?)</a>16 class="sprite maybe action-icon">(?)</a>
16 </div>17 </div>
18
19 <span
20 tal:define="link context_menu/create_recipe"
21 tal:condition="link/enabled"
22 tal:replace="structure link/render"
23 />
17 </div>24 </div>
1825
19</div>26</div>