← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/git-recipe-browser-existing into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/git-recipe-browser-existing into lp:launchpad with lp:~cjwatson/launchpad/git-recipe-stale as a prerequisite.

Commit message:
Allow editing existing Git recipes.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1453022 in Launchpad itself: "Please support daily builds via git"
  https://bugs.launchpad.net/launchpad/+bug/1453022

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-recipe-browser-existing/+merge/282297

Allow editing existing Git recipes.

I plan to put the browser code to create new recipes in place more or less last; adding browser code for existing recipes gives me the opportunity to put the bulk of the test code in place.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-recipe-browser-existing into lp:launchpad.
=== modified file 'lib/lp/app/widgets/suggestion.py'
--- lib/lp/app/widgets/suggestion.py	2015-10-26 14:54:43 +0000
+++ lib/lp/app/widgets/suggestion.py	2016-01-12 12:32:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Widgets related to IBranch."""
@@ -355,7 +355,7 @@
 class RecipeOwnerWidget(SuggestionWidget):
     """Widget for selecting a recipe owner.
 
-    The current user and the base branch owner are suggested.
+    The current user and the base source owner are suggested.
     """
 
     @staticmethod

=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py	2016-01-11 21:11:27 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py	2016-01-12 12:32:25 +0000
@@ -86,9 +86,12 @@
 from lp.code.errors import (
     BuildAlreadyPending,
     NoSuchBranch,
+    NoSuchGitRepository,
     PrivateBranchRecipe,
+    PrivateGitRepositoryRecipe,
     TooNewRecipeFormat,
     )
+from lp.code.interfaces.branch import IBranch
 from lp.code.interfaces.branchtarget import IBranchTarget
 from lp.code.interfaces.sourcepackagerecipe import (
     IRecipeBranchSource,
@@ -627,12 +630,17 @@
         except ForbiddenInstructionError as e:
             self.setFieldError(
                 'recipe_text',
-                'The bzr-builder instruction "%s" is not permitted '
-                'here.' % e.instruction_name)
+                'The recipe instruction "%s" is not permitted here.' %
+                e.instruction_name)
         except NoSuchBranch as e:
             self.setFieldError(
                 'recipe_text', '%s is not a branch on Launchpad.' % e.name)
-        except (RecipeParseError, PrivateBranchRecipe) as e:
+        except NoSuchGitRepository as e:
+            self.setFieldError(
+                'recipe_text',
+                '%s is not a Git repository on Launchpad.' % e.name)
+        except (RecipeParseError, PrivateBranchRecipe,
+                PrivateGitRepositoryRecipe) as e:
             self.setFieldError('recipe_text', str(e))
         raise ErrorHandled()
 
@@ -676,24 +684,24 @@
         super(RecipeRelatedBranchesMixin, self).setUpWidgets(context)
         self.widgets['related-branches'].display_label = False
         self.widgets['related-branches'].setRenderedValue(dict(
-                related_package_branch_info=self.related_package_branch_info,
-                related_series_branch_info=self.related_series_branch_info))
+            related_package_branch_info=self.related_package_branch_info,
+            related_series_branch_info=self.related_series_branch_info))
 
     @cachedproperty
     def related_series_branch_info(self):
         branch_to_check = self.getBranch()
-        return IBranchTarget(
-                branch_to_check.target).getRelatedSeriesBranchInfo(
-                                            branch_to_check,
-                                            limit_results=5)
+        if IBranch.providedBy(branch_to_check):
+            branch_target = IBranchTarget(branch_to_check.target)
+            return branch_target.getRelatedSeriesBranchInfo(
+                branch_to_check, limit_results=5)
 
     @cachedproperty
     def related_package_branch_info(self):
         branch_to_check = self.getBranch()
-        return IBranchTarget(
-                branch_to_check.target).getRelatedPackageBranchInfo(
-                                            branch_to_check,
-                                            limit_results=5)
+        if IBranch.providedBy(branch_to_check):
+            branch_target = IBranchTarget(branch_to_check.target)
+            return branch_target.getRelatedPackageBranchInfo(
+                branch_to_check, limit_results=5)
 
 
 class SourcePackageRecipeAddView(RecipeRelatedBranchesMixin,
@@ -822,8 +830,8 @@
     """View for editing Source Package Recipes."""
 
     def getBranch(self):
-        """The branch on which the recipe is built."""
-        return self.context.base_branch
+        """The branch or repository which the recipe is built."""
+        return self.context.base
 
     @property
     def title(self):
@@ -847,7 +855,7 @@
             any_owner_choice = PersonChoice(
                 __name__='owner', title=owner_field.title,
                 description=(u"As an administrator you are able to assign"
-                             u" this branch to any person or team."),
+                             u" this recipe to any person or team."),
                 required=True, vocabulary='ValidPersonOrTeam')
             any_owner_field = form.Fields(
                 any_owner_choice, render_context=self.render_context)

=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2016-01-11 21:11:27 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2016-01-12 12:32:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the source package recipe view classes and templates."""
@@ -10,6 +10,7 @@
     datetime,
     timedelta,
     )
+import re
 from textwrap import dedent
 
 from BeautifulSoup import BeautifulSoup
@@ -34,7 +35,10 @@
 from lp.code.browser.sourcepackagerecipebuild import (
     SourcePackageRecipeBuildView,
     )
-from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT_BZR
+from lp.code.interfaces.sourcepackagerecipe import (
+    MINIMAL_RECIPE_TEXT_BZR,
+    MINIMAL_RECIPE_TEXT_GIT,
+    )
 from lp.code.tests.helpers import recipe_parser_newest_version
 from lp.registry.interfaces.person import TeamMembershipPolicy
 from lp.registry.interfaces.pocket import PackagePublishingPocket
@@ -89,35 +93,21 @@
             canonical_url(recipe))
 
 
-class TestCaseForRecipe(BrowserTestCase):
-    """Create some sample data for recipe tests."""
-
-    def setUp(self):
-        """Provide useful defaults."""
-        super(TestCaseForRecipe, self).setUp()
-        self.chef = self.factory.makePerson(
-            displayname='Master Chef', name='chef')
-        self.user = self.chef
-        self.ppa = self.factory.makeArchive(
-            displayname='Secret PPA', owner=self.chef, name='ppa')
-        self.squirrel = self.factory.makeDistroSeries(
-            displayname='Secret Squirrel', name='secret', version='100.04',
-            distribution=self.ppa.distribution)
-        naked_squirrel = removeSecurityProxy(self.squirrel)
-        naked_squirrel.nominatedarchindep = self.squirrel.newArch(
-            'i386', getUtility(IProcessorSet).getByName('386'), False,
-            self.chef)
-
-    def makeRecipe(self):
-        """Create and return a specific recipe."""
-        chocolate = self.factory.makeProduct(name='chocolate')
-        cake_branch = self.factory.makeProductBranch(
-            owner=self.chef, name='cake', product=chocolate)
-        return self.factory.makeSourcePackageRecipe(
-            owner=self.chef, distroseries=self.squirrel, name=u'cake_recipe',
-            description=u'This recipe builds a foo for disto bar, with my'
-            ' Secret Squirrel changes.', branches=[cake_branch],
-            daily_build_archive=self.ppa)
+class BzrMixin:
+    """Mixin for Bazaar-based recipe tests."""
+
+    minimal_recipe_text = MINIMAL_RECIPE_TEXT_BZR
+    branch_type = "branch"
+    no_such_object_message = "is not a branch on Launchpad."
+
+    def makeBranch(self, target=None, **kwargs):
+        return self.factory.makeAnyBranch(product=target, **kwargs)
+
+    def makePackageBranch(self, **kwargs):
+        return self.factory.makePackageBranch(**kwargs)
+
+    def makeRelatedBranches(self, *args, **kwargs):
+        return self.factory.makeRelatedBranches(*args, **kwargs)
 
     def checkRelatedBranches(self, related_series_branch_info,
                              related_package_branch_info, browser_contents):
@@ -194,6 +184,86 @@
                 expected_branch_info.append(product_series.name)
             self.assertEqual(expected_branch_info, series_branches_info)
 
+    @staticmethod
+    def getRepository(branch):
+        return branch
+
+    @staticmethod
+    def getBranchRecipeText(branch):
+        return branch.identity
+
+    @staticmethod
+    def getMinimalRecipeText(branch):
+        return MINIMAL_RECIPE_TEXT_BZR % branch.identity
+
+
+class GitMixin:
+    """Mixin for Git-based recipe tests."""
+
+    minimal_recipe_text = MINIMAL_RECIPE_TEXT_GIT
+    branch_type = "repository"
+    no_such_object_message = "is not a Git repository on Launchpad."
+
+    def makeBranch(self, **kwargs):
+        return self.factory.makeGitRefs(**kwargs)[0]
+
+    def makePackageBranch(self, sourcepackagename=None, **kwargs):
+        dsp = self.factory.makeDistributionSourcePackage(
+            sourcepackagename=sourcepackagename)
+        return self.factory.makeGitRefs(target=dsp, **kwargs)[0]
+
+    def makeRelatedBranches(self, reference_branch=None, *args, **kwargs):
+        if reference_branch is None:
+            [reference_branch] = self.factory.makeGitRefs()
+        return reference_branch, [], []
+
+    def checkRelatedBranches(self, *args, **kwargs):
+        pass
+
+    @staticmethod
+    def getRepository(branch):
+        return branch.repository
+
+    @staticmethod
+    def getBranchRecipeText(branch):
+        return "%s %s" % (branch.repository.identity, branch.name)
+
+    @staticmethod
+    def getMinimalRecipeText(branch):
+        return MINIMAL_RECIPE_TEXT_GIT % (
+            branch.repository.identity, branch.name)
+
+
+class TestCaseForRecipe(BrowserTestCase):
+    """Create some sample data for recipe tests."""
+
+    def setUp(self):
+        """Provide useful defaults."""
+        super(TestCaseForRecipe, self).setUp()
+        self.chef = self.factory.makePerson(
+            displayname='Master Chef', name='chef')
+        self.user = self.chef
+        self.ppa = self.factory.makeArchive(
+            displayname='Secret PPA', owner=self.chef, name='ppa')
+        self.squirrel = self.factory.makeDistroSeries(
+            displayname='Secret Squirrel', name='secret', version='100.04',
+            distribution=self.ppa.distribution)
+        naked_squirrel = removeSecurityProxy(self.squirrel)
+        naked_squirrel.nominatedarchindep = self.squirrel.newArch(
+            'i386', getUtility(IProcessorSet).getByName('386'), False,
+            self.chef)
+
+    def makeRecipe(self, **kwargs):
+        """Create and return a specific recipe."""
+        chocolate = self.factory.makeProduct(name='chocolate')
+        cake_branch = self.makeBranch(
+            owner=self.chef, name=u'cake', target=chocolate)
+        return self.factory.makeSourcePackageRecipe(
+            owner=self.chef, distroseries=self.squirrel, name=u'cake_recipe',
+            description=u'This recipe builds a foo for distro bar, with my'
+            ' Secret Squirrel changes.', branches=[cake_branch],
+            daily_build_archive=self.ppa, **kwargs)
+
 
 class TestSourcePackageRecipeAddViewInitalValues(TestCaseWithFactory):
 
@@ -272,7 +342,7 @@
         self.assertEqual(set(), series.intersection(other_series))
 
 
-class TestSourcePackageRecipeAddView(TestCaseForRecipe):
+class TestSourcePackageRecipeAddView(BzrMixin, TestCaseForRecipe):
 
     layer = DatabaseFunctionalLayer
 
@@ -399,8 +469,7 @@
         browser.getControl('Create Recipe').click()
         self.assertEqual(
             get_feedback_messages(browser.contents)[1],
-            html_escape(
-                'The bzr-builder instruction "run" is not permitted here.'))
+            html_escape('The recipe instruction "run" is not permitted here.'))
 
     def createRecipe(self, recipe_text, branch=None):
         if branch is None:
@@ -730,7 +799,7 @@
         self.assertIsNot(None, new_ppa)
 
 
-class TestSourcePackageRecipeEditView(TestCaseForRecipe):
+class TestSourcePackageRecipeEditViewMixin:
     """Test the editing behaviour of a source package recipe."""
 
     layer = DatabaseFunctionalLayer
@@ -741,10 +810,10 @@
             distribution=self.ppa.distribution)
         product = self.factory.makeProduct(
             name='ratatouille', displayname='Ratatouille')
-        veggie_branch = self.factory.makeBranch(
-            owner=self.chef, product=product, name='veggies')
-        meat_branch = self.factory.makeBranch(
-            owner=self.chef, product=product, name='meat')
+        veggie_branch = self.makeBranch(
+            owner=self.chef, target=product, name=u'veggies')
+        meat_branch = self.makeBranch(
+            owner=self.chef, target=product, name=u'meat')
         recipe = self.factory.makeSourcePackageRecipe(
             owner=self.chef, registrant=self.chef,
             name=u'things', description=u'This is a recipe',
@@ -754,14 +823,13 @@
             distribution=self.ppa.distribution, name='ppa2',
             displayname="PPA 2", owner=self.chef)
 
-        meat_path = meat_branch.bzr_identity
+        recipe_text = self.getMinimalRecipeText(meat_branch)
 
         browser = self.getUserBrowser(canonical_url(recipe), user=self.chef)
         browser.getLink('Edit recipe').click()
         browser.getControl(name='field.name').value = 'fings'
         browser.getControl('Description').value = 'This is stuff'
-        browser.getControl('Recipe text').value = (
-            MINIMAL_RECIPE_TEXT_BZR % meat_path)
+        browser.getControl('Recipe text').value = recipe_text
         browser.getControl('Secret Squirrel').click()
         browser.getControl('Mumbly Midget').click()
         browser.getControl('PPA 2').click()
@@ -771,10 +839,7 @@
         self.assertThat(
             'Edit This is stuff', MatchesTagText(content, 'edit-description'))
         self.assertThat(
-            'Edit '
-            '# bzr-builder format 0.3 deb-version {debupstream}-0~{revno}\n'
-            'lp://dev/~chef/ratatouille/meat',
-            MatchesTagText(content, 'edit-recipe_text'))
+            'Edit ' + recipe_text, MatchesTagText(content, 'edit-recipe_text'))
         self.assertThat(
             'Distribution series: Edit Mumbly Midget',
             MatchesTagText(content, 'distroseries'))
@@ -784,8 +849,7 @@
     def test_edit_recipe_sets_date_last_modified(self):
         """Editing a recipe sets the date_last_modified property."""
         date_created = datetime(2000, 1, 1, 12, tzinfo=UTC)
-        recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, date_created=date_created)
+        recipe = self.makeRecipe(date_created=date_created)
 
         login_person(self.chef)
         view = SourcePackageRecipeEditView(recipe, LaunchpadTestRequest())
@@ -803,17 +867,17 @@
             distribution=self.ppa.distribution)
         product = self.factory.makeProduct(
             name='ratatouille', displayname='Ratatouille')
-        veggie_branch = self.factory.makeBranch(
-            owner=self.chef, product=product, name='veggies')
-        meat_branch = self.factory.makeBranch(
-            owner=self.chef, product=product, name='meat')
+        veggie_branch = self.makeBranch(
+            owner=self.chef, target=product, name=u'veggies')
+        meat_branch = self.makeBranch(
+            owner=self.chef, target=product, name=u'meat')
         recipe = self.factory.makeSourcePackageRecipe(
             owner=self.chef, registrant=self.chef,
             name=u'things', description=u'This is a recipe',
             distroseries=self.squirrel, branches=[veggie_branch],
             daily_build_archive=self.ppa)
 
-        meat_path = meat_branch.bzr_identity
+        recipe_text = self.getMinimalRecipeText(meat_branch)
         expert = getUtility(ILaunchpadCelebrities).admin.teamowner
 
         browser = self.getUserBrowser(canonical_url(recipe), user=expert)
@@ -827,8 +891,7 @@
 
         browser.getControl(name='field.name').value = 'fings'
         browser.getControl('Description').value = 'This is stuff'
-        browser.getControl('Recipe text').value = (
-            MINIMAL_RECIPE_TEXT_BZR % meat_path)
+        browser.getControl('Recipe text').value = recipe_text
         browser.getControl('Secret Squirrel').click()
         browser.getControl('Mumbly Midget').click()
         browser.getControl('Update Recipe').click()
@@ -838,10 +901,7 @@
         self.assertThat(
             'Edit This is stuff', MatchesTagText(content, 'edit-description'))
         self.assertThat(
-            'Edit '
-            '# bzr-builder format 0.3 deb-version {debupstream}-0~{revno}\n'
-            'lp://dev/~chef/ratatouille/meat',
-            MatchesTagText(content, 'edit-recipe_text'))
+            'Edit ' + recipe_text, MatchesTagText(content, 'edit-recipe_text'))
         self.assertThat(
             'Distribution series: Edit Mumbly Midget',
             MatchesTagText(content, 'distroseries'))
@@ -852,8 +912,8 @@
             distribution=self.ppa.distribution)
         product = self.factory.makeProduct(
             name='ratatouille', displayname='Ratatouille')
-        veggie_branch = self.factory.makeBranch(
-            owner=self.chef, product=product, name='veggies')
+        veggie_branch = self.makeBranch(
+            owner=self.chef, target=product, name=u'veggies')
         recipe = self.factory.makeSourcePackageRecipe(
             owner=self.chef, registrant=self.chef,
             name=u'things', description=u'This is a recipe',
@@ -867,8 +927,7 @@
 
         self.assertEqual(
             get_feedback_messages(browser.contents)[1],
-            html_escape(
-                'The bzr-builder instruction "run" is not permitted here.'))
+            html_escape('The recipe instruction "run" is not permitted here.'))
 
     def test_edit_recipe_format_too_new(self):
         # If the recipe's format version is too new, we should notify the
@@ -878,17 +937,16 @@
             distribution=self.ppa.distribution)
         product = self.factory.makeProduct(
             name='ratatouille', displayname='Ratatouille')
-        veggie_branch = self.factory.makeBranch(
-            owner=self.chef, product=product, name='veggies')
+        veggie_branch = self.makeBranch(
+            owner=self.chef, target=product, name=u'veggies')
         recipe = self.factory.makeSourcePackageRecipe(
             owner=self.chef, registrant=self.chef,
             name=u'things', description=u'This is a recipe',
             distroseries=self.squirrel, branches=[veggie_branch])
 
-        new_recipe_text = dedent(u'''\
-            # bzr-builder format 145.115 deb-version {debupstream}-0~{revno}
-            %s
-            ''') % recipe.base_branch.bzr_identity
+        new_recipe_text = re.sub(
+            'format [^ ]*', 'format 145.115',
+            self.getMinimalRecipeText(veggie_branch))
 
         with recipe_parser_newest_version(145.115):
             browser = self.getViewBrowser(recipe)
@@ -906,10 +964,10 @@
             distribution=self.ppa.distribution)
         product = self.factory.makeProduct(
             name='ratatouille', displayname='Ratatouille')
-        veggie_branch = self.factory.makeBranch(
-            owner=self.chef, product=product, name='veggies')
-        meat_branch = self.factory.makeBranch(
-            owner=self.chef, product=product, name='meat')
+        veggie_branch = self.makeBranch(
+            owner=self.chef, target=product, name=u'veggies')
+        meat_branch = self.makeBranch(
+            owner=self.chef, target=product, name=u'meat')
         recipe = self.factory.makeSourcePackageRecipe(
             owner=self.chef, registrant=self.chef,
             name=u'things', description=u'This is a recipe',
@@ -919,14 +977,13 @@
             name=u'fings', description=u'This is a recipe',
             distroseries=self.squirrel, branches=[veggie_branch])
 
-        meat_path = meat_branch.bzr_identity
+        recipe_text = self.getMinimalRecipeText(meat_branch)
 
         browser = self.getUserBrowser(canonical_url(recipe), user=self.chef)
         browser.getLink('Edit recipe').click()
         browser.getControl(name='field.name').value = 'fings'
         browser.getControl('Description').value = 'This is stuff'
-        browser.getControl('Recipe text').value = (
-            MINIMAL_RECIPE_TEXT_BZR % meat_path)
+        browser.getControl('Recipe text').value = recipe_text
         browser.getControl('Secret Squirrel').click()
         browser.getControl('Mumbly Midget').click()
         browser.getControl('Update Recipe').click()
@@ -939,30 +996,32 @@
         # If a user tries to set source package recipe to use a private
         # branch, they should get an error.
         recipe = self.factory.makeSourcePackageRecipe(owner=self.user)
-        branch = self.factory.makeAnyBranch(
+        branch = self.makeBranch(
             owner=self.user, information_type=InformationType.USERDATA)
         with person_logged_in(self.user):
-            bzr_identity = branch.bzr_identity
-        recipe_text = MINIMAL_RECIPE_TEXT_BZR % bzr_identity
+            identity = self.getRepository(branch).identity
+            recipe_text = self.getMinimalRecipeText(branch)
         browser = self.getViewBrowser(recipe, '+edit')
         browser.getControl('Recipe text').value = recipe_text
         browser.getControl('Update Recipe').click()
         self.assertEqual(
             get_feedback_messages(browser.contents)[1],
-            'Recipe may not refer to private branch: %s' % bzr_identity)
+            'Recipe may not refer to private %s: %s' % (
+                self.branch_type, identity))
 
     def test_edit_recipe_no_branch(self):
         # If a user tries to set a source package recipe to use a branch
-        # that isn't registred, they will get an error.
-        recipe = self.factory.makeSourcePackageRecipe(owner=self.user)
-        no_branch_recipe_text = recipe.recipe_text[:-4]
-        expected_name = recipe.base_branch.unique_name[:-3]
+        # that isn't registered, they will get an error.
+        recipe = self.factory.makeSourcePackageRecipe(
+            owner=self.user, branches=[self.makeBranch()])
+        no_branch_recipe_text = (
+            recipe.recipe_text.splitlines()[0] + "\nlp:nonexistent\n")
         browser = self.getViewBrowser(recipe, '+edit')
         browser.getControl('Recipe text').value = no_branch_recipe_text
         browser.getControl('Update Recipe').click()
         self.assertEqual(
             get_feedback_messages(browser.contents)[1],
-            'lp://dev/%s is not a branch on Launchpad.' % expected_name)
+            'lp:nonexistent %s' % self.no_such_object_message)
 
     def _test_edit_recipe_with_no_related_branches(self, recipe):
         # The Related Branches section should not appear if there are no
@@ -978,26 +1037,31 @@
     def test_edit_product_branch_with_no_related_branches_recipe(self):
         # The Related Branches section should not appear if there are no
         # related branches.
-        base_branch = self.factory.makeBranch()
+        base_branch = self.makeBranch()
         recipe = self.factory.makeSourcePackageRecipe(
-                owner=self.chef, branches=[base_branch])
+            owner=self.chef, branches=[base_branch])
         self._test_edit_recipe_with_no_related_branches(recipe)
 
     def test_edit_sourcepackage_branch_with_no_related_branches_recipe(self):
         # The Related Branches section should not appear if there are no
         # related branches.
-        base_branch = self.factory.makePackageBranch()
+        base_branch = self.makePackageBranch()
         recipe = self.factory.makeSourcePackageRecipe(
-                owner=self.chef, branches=[base_branch])
+            owner=self.chef, branches=[base_branch])
         self._test_edit_recipe_with_no_related_branches(recipe)
 
+
+class TestSourcePackageRecipeEditViewBzr(
+    TestSourcePackageRecipeEditViewMixin, BzrMixin, TestCaseForRecipe):
+
     def test_edit_recipe_with_package_branches(self):
         # The series branches table should not appear if there are none.
         with person_logged_in(self.chef):
-            recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
-            self.factory.makeRelatedBranches(
-                    reference_branch=recipe.base_branch,
-                    with_series_branches=False)
+            base_branch = self.makeBranch()
+            recipe = self.factory.makeSourcePackageRecipe(
+                owner=self.chef, branches=[base_branch])
+            self.makeRelatedBranches(
+                reference_branch=base_branch, with_series_branches=False)
         browser = self.getUserBrowser(canonical_url(recipe), user=self.chef)
         browser.getLink('Edit recipe').click()
         soup = BeautifulSoup(browser.contents)
@@ -1013,10 +1077,11 @@
     def test_edit_recipe_with_series_branches(self):
         # The package branches table should not appear if there are none.
         with person_logged_in(self.chef):
-            recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
-            self.factory.makeRelatedBranches(
-                    reference_branch=recipe.base_branch,
-                    with_package_branches=False)
+            base_branch = self.makeBranch()
+            recipe = self.factory.makeSourcePackageRecipe(
+                owner=self.chef, branches=[base_branch])
+            self.makeRelatedBranches(
+                reference_branch=base_branch, with_package_branches=False)
         browser = self.getUserBrowser(canonical_url(recipe), user=self.chef)
         browser.getLink('Edit recipe').click()
         soup = BeautifulSoup(browser.contents)
@@ -1032,11 +1097,11 @@
     def test_edit_product_branch_recipe_with_related_branches(self):
         # The related branches should be rendered correctly on the page.
         with person_logged_in(self.chef):
-            recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
-            (branch, related_series_branch_info,
-                related_package_branch_info) = (
-                    self.factory.makeRelatedBranches(
-                    reference_branch=recipe.base_branch))
+            base_branch = self.makeBranch()
+            recipe = self.factory.makeSourcePackageRecipe(
+                owner=self.chef, branches=[base_branch])
+            _, related_series_branch_info, related_package_branch_info = (
+                self.makeRelatedBranches(reference_branch=base_branch))
         browser = self.getUserBrowser(
             canonical_url(recipe, view_name='+edit'), user=self.chef)
         self.checkRelatedBranches(
@@ -1046,18 +1111,23 @@
     def test_edit_sourcepackage_branch_recipe_with_related_branches(self):
         # The related branches should be rendered correctly on the page.
         with person_logged_in(self.chef):
-            reference_branch = self.factory.makePackageBranch()
+            reference_branch = self.makePackageBranch()
             recipe = self.factory.makeSourcePackageRecipe(
                     owner=self.chef, branches=[reference_branch])
-            (branch, ignore, related_package_branch_info) = (
-                    self.factory.makeRelatedBranches(reference_branch))
-            browser = self.getUserBrowser(
-                canonical_url(recipe, view_name='+edit'), user=self.chef)
-            self.checkRelatedBranches(
-                    set(), related_package_branch_info, browser.contents)
-
-
-class TestSourcePackageRecipeView(TestCaseForRecipe):
+            _, _, related_package_branch_info = (
+                self.makeRelatedBranches(reference_branch))
+        browser = self.getUserBrowser(
+            canonical_url(recipe, view_name='+edit'), user=self.chef)
+        self.checkRelatedBranches(
+            set(), related_package_branch_info, browser.contents)
+
+
+class TestSourcePackageRecipeEditViewGit(
+    TestSourcePackageRecipeEditViewMixin, GitMixin, TestCaseForRecipe):
+    pass
+
+
+class TestSourcePackageRecipeViewMixin:
 
     layer = LaunchpadFunctionalLayer
 
@@ -1086,8 +1156,8 @@
             Recipe information
             Build schedule: .* Built on request Edit
             Owner: Master Chef Edit
-            Base branch: lp://dev/~chef/chocolate/cake
-            Debian version: {debupstream}-0~{revno}
+            Base source: lp:.*~chef/chocolate.*cake
+            Debian version: {debupstream}-0~{rev.*}
             Daily build archive: Secret PPA Edit
             Distribution series: Edit Secret Squirrel
 
@@ -1097,8 +1167,8 @@
             Request build\(s\)
 
             Recipe contents Edit
-            # bzr-builder format 0.3 deb-version {debupstream}-0~{revno}
-            lp://dev/~chef/chocolate/cake""", self.getMainText(build.recipe))
+            # .* format .* deb-version {debupstream}-0~{rev.*}
+            lp:.*~chef/chocolate.*cake.*""", self.getMainText(build.recipe))
 
     def test_index_success_with_buildlog(self):
         # The buildlog is shown if it is there.
@@ -1262,9 +1332,7 @@
             [build6, build5, build4, build3, build2], view.builds)
 
     def test_request_builds_redirects_on_get(self):
-        recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, daily_build_archive=self.ppa,
-            is_stale=True, build_daily=True)
+        recipe = self.makeRecipe(is_stale=True, build_daily=True)
         with person_logged_in(self.chef):
             url = canonical_url(recipe)
         browser = self.getViewBrowser(recipe, '+request-daily-build')
@@ -1272,9 +1340,7 @@
 
     def test_request_daily_builds_button_stale(self):
         # Recipes that are stale and are built daily have a build now link
-        recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, daily_build_archive=self.ppa,
-            is_stale=True, build_daily=True)
+        recipe = self.makeRecipe(is_stale=True, build_daily=True)
         browser = self.getViewBrowser(recipe)
         build_button = find_tag_by_id(browser.contents, 'field.actions.build')
         self.assertIsNot(None, build_button)
@@ -1282,9 +1348,7 @@
     def test_request_daily_builds_button_not_stale(self):
         # Recipes that are not stale do not have a build now link
         login(ANONYMOUS)
-        recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, daily_build_archive=self.ppa,
-            is_stale=False, build_daily=True)
+        recipe = self.makeRecipe(is_stale=False, build_daily=True)
         browser = self.getViewBrowser(recipe)
         build_button = find_tag_by_id(browser.contents, 'field.actions.build')
         self.assertIs(None, build_button)
@@ -1292,9 +1356,7 @@
     def test_request_daily_builds_button_not_daily(self):
         # Recipes that are not built daily do not have a build now link
         login(ANONYMOUS)
-        recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, daily_build_archive=self.ppa,
-            is_stale=True, build_daily=False)
+        recipe = self.makeRecipe(is_stale=True, build_daily=False)
         browser = self.getViewBrowser(recipe)
         build_button = find_tag_by_id(browser.contents, 'field.actions.build')
         self.assertIs(None, build_button)
@@ -1302,8 +1364,10 @@
     def test_request_daily_builds_button_no_daily_ppa(self):
         # Recipes that have no daily build ppa do not have a build now link
         login(ANONYMOUS)
+        branch = self.makeBranch()
         recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, is_stale=True, build_daily=True)
+            owner=self.chef, branches=[branch],
+            is_stale=True, build_daily=True)
         naked_recipe = removeSecurityProxy(recipe)
         naked_recipe.daily_build_archive = None
         browser = self.getViewBrowser(recipe)
@@ -1314,8 +1378,7 @@
         # Recipes do not have a build now link if the user does not have edit
         # permission on the recipe.
         login(ANONYMOUS)
-        recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, is_stale=True, build_daily=True)
+        recipe = self.makeRecipe(is_stale=True, build_daily=True)
         person = self.factory.makePerson()
         browser = self.getViewBrowser(recipe, user=person)
         build_button = find_tag_by_id(browser.contents, 'field.actions.build')
@@ -1327,10 +1390,12 @@
         login(ANONYMOUS)
         distroseries = self.factory.makeSourcePackageRecipeDistroseries()
         person = self.factory.makePerson()
+        branch = self.makeBranch()
         daily_build_archive = self.factory.makeArchive(
-                distribution=distroseries.distribution, owner=person)
+            distribution=distroseries.distribution, owner=person)
         recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, daily_build_archive=daily_build_archive,
+            owner=self.chef, branches=[branch],
+            daily_build_archive=daily_build_archive,
             is_stale=True, build_daily=True)
         browser = self.getViewBrowser(recipe)
         build_button = find_tag_by_id(browser.contents, 'field.actions.build')
@@ -1340,12 +1405,14 @@
         # Recipes whose daily build ppa is disabled do not have a build now
         # link.
         distroseries = self.factory.makeSourcePackageRecipeDistroseries()
+        branch = self.makeBranch()
         daily_build_archive = self.factory.makeArchive(
-                distribution=distroseries.distribution, owner=self.user)
+            distribution=distroseries.distribution, owner=self.user)
         with person_logged_in(self.user):
             daily_build_archive.disable()
         recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, daily_build_archive=daily_build_archive,
+            owner=self.chef, branches=[branch],
+            daily_build_archive=daily_build_archive,
             is_stale=True, build_daily=True)
         browser = self.getViewBrowser(recipe)
         build_button = find_tag_by_id(browser.contents, 'field.actions.build')
@@ -1353,17 +1420,16 @@
 
     def test_request_daily_builds_ajax_link_not_rendered(self):
         # The Build now link should not be rendered without javascript.
-        recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, daily_build_archive=self.ppa,
-            is_stale=True, build_daily=True)
+        recipe = self.makeRecipe(is_stale=True, build_daily=True)
         browser = self.getViewBrowser(recipe)
         build_link = find_tag_by_id(browser.contents, 'request-daily-builds')
         self.assertIs(None, build_link)
 
     def test_request_daily_builds_action(self):
         # Daily builds should be triggered when requested.
+        branch = self.makeBranch()
         recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, daily_build_archive=self.ppa,
+            owner=self.chef, branches=[branch], daily_build_archive=self.ppa,
             is_stale=True, build_daily=True)
         browser = self.getViewBrowser(recipe)
         browser.getControl('Build now').click()
@@ -1380,9 +1446,7 @@
 
     def test_request_daily_builds_disabled_archive(self):
         # Requesting a daily build from a disabled archive is a user error.
-        recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, daily_build_archive=self.ppa,
-            name=u'julia', is_stale=True, build_daily=True)
+        recipe = self.makeRecipe(is_stale=True, build_daily=True)
         harness = LaunchpadFormHarness(
             recipe, SourcePackageRecipeRequestDailyBuildView)
         with person_logged_in(self.ppa.owner):
@@ -1394,9 +1458,7 @@
 
     def test_request_daily_builds_obsolete_series(self):
         # Requesting a daily build with an obsolete series gives a warning.
-        recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.chef, daily_build_archive=self.ppa,
-            name=u'julia', is_stale=True, build_daily=True)
+        recipe = self.makeRecipe(is_stale=True, build_daily=True)
         warty = self.factory.makeSourcePackageRecipeDistroseries()
         hoary = self.factory.makeSourcePackageRecipeDistroseries(name='hoary')
         with person_logged_in(self.chef):
@@ -1457,21 +1519,26 @@
             Unauthorized, browser.getLink('Request build(s)').click)
 
     def test_request_builds_archive_no_daily_build_archive(self):
-        recipe = self.factory.makeSourcePackageRecipe()
+        branch = self.makeBranch()
+        recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
         view = SourcePackageRecipeRequestBuildsView(recipe, None)
         self.assertIs(None, view.initial_values.get('archive'))
 
     def test_request_builds_archive_daily_build_archive_unuploadable(self):
+        branch = self.makeBranch()
         ppa = self.factory.makeArchive()
-        recipe = self.factory.makeSourcePackageRecipe(daily_build_archive=ppa)
+        recipe = self.factory.makeSourcePackageRecipe(
+            branches=[branch], daily_build_archive=ppa)
         with person_logged_in(self.chef):
             view = SourcePackageRecipeRequestBuildsView(recipe, None)
             self.assertIs(None, view.initial_values.get('archive'))
 
     def test_request_builds_archive(self):
+        branch = self.makeBranch()
         ppa = self.factory.makeArchive(
             displayname='Secret PPA', owner=self.chef, name='ppa2')
-        recipe = self.factory.makeSourcePackageRecipe(daily_build_archive=ppa)
+        recipe = self.factory.makeSourcePackageRecipe(
+            branches=[branch], daily_build_archive=ppa)
         with person_logged_in(self.chef):
             view = SourcePackageRecipeRequestBuildsView(recipe, None)
             self.assertEqual(ppa, view.initial_values.get('archive'))
@@ -1506,11 +1573,12 @@
         # that owns the PPA.
         registrant = self.factory.makePerson()
         owner_team = self.factory.makeTeam(members=[registrant], name='team1')
+        branch = self.makeBranch()
         ppa_team = self.factory.makeTeam(members=[registrant], name='team2')
         ppa = self.factory.makeArchive(owner=ppa_team, name='ppa')
         return self.factory.makeSourcePackageRecipe(
-            registrant=registrant, owner=owner_team, daily_build_archive=ppa,
-            build_daily=True)
+            registrant=registrant, owner=owner_team, branches=[branch],
+            daily_build_archive=ppa, build_daily=True)
 
     def test_owner_with_no_ppa_upload_permission(self):
         # Daily build with upload issues are a problem.
@@ -1541,7 +1609,9 @@
         # When a PPA is disabled, it is only viewable to the owner. This
         # case is handled with the view not showing builds into a disabled
         # archive, rather than giving an Unauthorized error to the user.
-        recipe = self.factory.makeSourcePackageRecipe(build_daily=True)
+        branch = self.makeBranch()
+        recipe = self.factory.makeSourcePackageRecipe(
+            branches=[branch], build_daily=True)
         recipe.requestBuild(
             recipe.daily_build_archive, recipe.owner, self.squirrel,
             PackagePublishingPocket.RELEASE)
@@ -1553,23 +1623,34 @@
             extract_text(find_main_content(browser.contents)))
 
 
-class TestSourcePackageRecipeBuildView(BrowserTestCase):
+class TestSourcePackageRecipeViewBzr(
+    TestSourcePackageRecipeViewMixin, BzrMixin, TestCaseForRecipe):
+    pass
+
+
+class TestSourcePackageRecipeViewGit(
+    TestSourcePackageRecipeViewMixin, GitMixin, TestCaseForRecipe):
+    pass
+
+
+class TestSourcePackageRecipeBuildViewMixin:
     """Test behaviour of SourcePackageRecipeBuildView."""
 
     layer = LaunchpadFunctionalLayer
 
     def setUp(self):
         """Provide useful defaults."""
-        super(TestSourcePackageRecipeBuildView, self).setUp()
+        super(TestSourcePackageRecipeBuildViewMixin, self).setUp()
         self.user = self.factory.makePerson(
             displayname='Owner', name='build-owner')
 
     def makeBuild(self):
-        """Make a build suitabe for testing."""
+        """Make a build suitable for testing."""
         archive = self.factory.makeArchive(name='build',
             owner=self.user)
+        branch = self.makeBranch()
         recipe = self.factory.makeSourcePackageRecipe(
-            owner=self.user, name=u'my-recipe')
+            owner=self.user, name=u'my-recipe', branches=[branch])
         distro_series = self.factory.makeDistroSeries(
             name='squirrel', distribution=archive.distribution)
         removeSecurityProxy(distro_series).nominatedarchindep = (
@@ -1607,7 +1688,9 @@
         It should be bq.date_started + estimated duration for jobs that have
         started.
         """
-        build = self.factory.makeSourcePackageRecipeBuild()
+        branch = self.makeBranch()
+        recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
+        build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
         view = SourcePackageRecipeBuildView(build, None)
         self.assertIs(None, view.eta)
         queue_entry = removeSecurityProxy(build.queueBuild())
@@ -1750,12 +1833,24 @@
         self.assertEqual(upload_log_url, link.url)
 
 
-class TestSourcePackageRecipeDeleteView(TestCaseForRecipe):
+class TestSourcePackageRecipeBuildViewBzr(
+    TestSourcePackageRecipeBuildViewMixin, BzrMixin, BrowserTestCase):
+    pass
+
+
+class TestSourcePackageRecipeBuildViewGit(
+    TestSourcePackageRecipeBuildViewMixin, GitMixin, BrowserTestCase):
+    pass
+
+
+class TestSourcePackageRecipeDeleteViewMixin:
 
     layer = DatabaseFunctionalLayer
 
     def test_delete_recipe(self):
-        recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
+        branch = self.makeBranch()
+        recipe = self.factory.makeSourcePackageRecipe(
+            owner=self.chef, branches=[branch])
 
         browser = self.getUserBrowser(
             canonical_url(recipe), user=self.chef)
@@ -1768,7 +1863,9 @@
             browser.url)
 
     def test_delete_recipe_no_permissions(self):
-        recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
+        branch = self.makeBranch()
+        recipe = self.factory.makeSourcePackageRecipe(
+            owner=self.chef, branches=[branch])
         nopriv_person = self.factory.makePerson()
         recipe_url = canonical_url(recipe)
 
@@ -1784,7 +1881,17 @@
             self.getUserBrowser, recipe_url + '/+delete', user=nopriv_person)
 
 
-class TestBrokenExistingRecipes(BrowserTestCase):
+class TestSourcePackageRecipeDeleteViewBzr(
+    TestSourcePackageRecipeDeleteViewMixin, BzrMixin, TestCaseForRecipe):
+    pass
+
+
+class TestSourcePackageRecipeDeleteViewGit(
+    TestSourcePackageRecipeDeleteViewMixin, GitMixin, TestCaseForRecipe):
+    pass
+
+
+class TestBrokenExistingRecipesMixin:
     """Existing recipes broken by builder updates need to be editable.
 
     This happened with a 0.2 -> 0.3 release where the nest command was no
@@ -1795,19 +1902,17 @@
 
     layer = LaunchpadFunctionalLayer
 
-    RECIPE_FIRST_LINE = (
-        "# bzr-builder format 0.2 deb-version {debupstream}+{revno}")
-
     def makeBrokenRecipe(self):
         """Make a valid recipe, then break it."""
         product = self.factory.makeProduct()
-        b1 = self.factory.makeProductBranch(product=product)
-        b2 = self.factory.makeProductBranch(product=product)
+        b1 = self.makeBranch(target=product)
+        b2 = self.makeBranch(target=product)
         recipe_text = dedent("""\
             %s
             %s
             nest name %s foo
-            """ % (self.RECIPE_FIRST_LINE, b1.bzr_identity, b2.bzr_identity))
+            """ % (self.RECIPE_FIRST_LINE, self.getBranchRecipeText(b1),
+                   self.getRepository(b2).identity))
         recipe = self.factory.makeSourcePackageRecipe(recipe=recipe_text)
         naked_data = removeSecurityProxy(recipe)._recipe_data
         nest_instruction = list(naked_data.instructions)[0]
@@ -1831,3 +1936,17 @@
         recipe = self.makeBrokenRecipe()
         main_text = self.getMainText(recipe, '+edit', user=recipe.owner)
         self.assertRecipeInText(main_text)
+
+
+class TestBrokenExistingRecipesBzr(
+    TestBrokenExistingRecipesMixin, BzrMixin, BrowserTestCase):
+
+    RECIPE_FIRST_LINE = (
+        "# bzr-builder format 0.2 deb-version {debupstream}+{revno}")
+
+
+class TestBrokenExistingRecipesGit(
+    TestBrokenExistingRecipesMixin, GitMixin, BrowserTestCase):
+
+    RECIPE_FIRST_LINE = (
+        "# git-build-recipe format 0.4 deb-version {debupstream}+{revtime}")

=== modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt'
--- lib/lp/code/templates/sourcepackagerecipe-index.pt	2014-05-14 09:23:56 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-index.pt	2016-01-12 12:32:25 +0000
@@ -108,9 +108,9 @@
             <dt>Owner:</dt>
             <dd tal:content="structure view/person_picker"/>
           </dl>
-          <dl id="base-branch">
-            <dt>Base branch:</dt>
-            <dd tal:content="structure context/base_branch/fmt:link" />
+          <dl id="base-source">
+            <dt>Base source:</dt>
+            <dd tal:content="structure context/base/fmt:link" />
           </dl>
           <dl id="debian-version">
             <dt>Debian version:</dt>
@@ -151,8 +151,8 @@
         Y.on('lp:context:deb_version_template:changed', function(e) {
             Y.lp.deprecated.ui.update_field('#debian-version dd', e.new_value);
         });
-        Y.on('lp:context:base_branch_link:changed', function(e) {
-            Y.lp.deprecated.ui.update_field('#base-branch dd', e.new_value_html);
+        Y.on('lp:context:base_source_link:changed', function(e) {
+            Y.lp.deprecated.ui.update_field('#base-source dd', e.new_value_html);
         });
         Y.on('load', function() {
             Y.lp.code.requestbuild_overlay.hookUpDailyBuildsSchedule();


Follow ups