← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/git-recipe-model into lp:launchpad with lp:~cjwatson/launchpad/minimal-recipe-text-bzr as a prerequisite.

Commit message:
Add basic model for 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-model/+merge/282248

Add basic model for Git recipes.

This is long, but it's mostly tests, and I couldn't get it much shorter while still testing it properly.

While the database model has git_repository plus a revspec, and there are certainly cases where one might wish to pin a recipe to a particular tag or commit rather than a moving branch (so that should be possible), the common case is going to be to create a recipe based on a branch; this is therefore optimised for that workflow, so you can point the recipe construction code at an IGitRef and it will set the revspec to the branch name.  The browser code will let you create and list recipes from either a GitRef or a GitRepository (and if you don't fill in a revspec manually in the latter case, git-build-recipe will use HEAD, which matches "git clone" and is often what people will want in practice).  I think this approach is least likely to result in confused users unable to find the link.

Other bits like repository deletion handling, staleness handling, and browser code will follow in later branches.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-recipe-model into lp:launchpad.
=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py	2015-06-13 01:45:19 +0000
+++ lib/lp/code/errors.py	2016-01-12 01:39:14 +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).
 
 """Errors used in the lp/code modules."""
@@ -46,6 +46,7 @@
     'NoSuchGitReference',
     'NoSuchGitRepository',
     'PrivateBranchRecipe',
+    'PrivateGitRepositoryRecipe',
     'ReviewNotPending',
     'StaleLastMirrored',
     'TooNewRecipeFormat',
@@ -298,12 +299,22 @@
 
     def __init__(self, branch):
         message = (
-            'Recipe may not refer to private branch: %s' %
-            branch.bzr_identity)
+            'Recipe may not refer to private branch: %s' % branch.identity)
         self.branch = branch
         Exception.__init__(self, message)
 
 
+@error_status(httplib.BAD_REQUEST)
+class PrivateGitRepositoryRecipe(Exception):
+
+    def __init__(self, repository):
+        message = (
+            'Recipe may not refer to private repository: %s' %
+            repository.identity)
+        self.repository = repository
+        Exception.__init__(self, message)
+
+
 class ReviewNotPending(Exception):
     """The requested review is not in a pending state."""
 

=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
--- lib/lp/code/interfaces/sourcepackagerecipe.py	2016-01-12 01:39:14 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipe.py	2016-01-12 01:39:14 +0000
@@ -14,6 +14,7 @@
     'ISourcePackageRecipeDataSource',
     'ISourcePackageRecipeSource',
     'MINIMAL_RECIPE_TEXT_BZR',
+    'MINIMAL_RECIPE_TEXT_GIT',
     ]
 
 
@@ -55,6 +56,7 @@
 from lp import _
 from lp.app.validators.name import name_validator
 from lp.code.interfaces.branch import IBranch
+from lp.code.interfaces.gitrepository import IGitRepository
 from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.role import IHasOwner
@@ -72,13 +74,26 @@
     ''')
 
 
+MINIMAL_RECIPE_TEXT_GIT = dedent(u'''\
+    # git-build-recipe format 0.4 deb-version {debupstream}-0~{revtime}
+    %s %s
+    ''')
+
+
 class ISourcePackageRecipeData(Interface):
     """A recipe as database data, not text."""
 
     base_branch = exported(
         Reference(
             IBranch, title=_("The base branch used by this recipe."),
-            required=True, readonly=True))
+            required=False, readonly=True))
+    base_git_repository = exported(
+        Reference(
+            IGitRepository,
+            title=_("The base Git repository used by this recipe."),
+            required=False, readonly=True))
+    base = Attribute(
+        "The base branch/repository used by this recipe (VCS-agnostic).")
 
     deb_version_template = exported(
         TextLine(

=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
--- lib/lp/code/model/sourcepackagerecipe.py	2016-01-12 01:39:14 +0000
+++ lib/lp/code/model/sourcepackagerecipe.py	2016-01-12 01:39:14 +0000
@@ -13,6 +13,7 @@
     timedelta,
     )
 from operator import attrgetter
+import re
 
 from lazr.delegates import delegate_to
 from pytz import utc
@@ -151,6 +152,18 @@
     def base_branch(self):
         return self._recipe_data.base_branch
 
+    @property
+    def base_git_repository(self):
+        return self._recipe_data.base_git_repository
+
+    @property
+    def base(self):
+        if self.base_branch is not None:
+            return self.base_branch
+        else:
+            assert self.base_git_repository is not None
+            return self.base_git_repository
+
     @staticmethod
     def preLoadDataForSourcePackageRecipes(sourcepackagerecipes):
         # Load the referencing SourcePackageRecipeData.
@@ -173,7 +186,14 @@
 
     @property
     def recipe_text(self):
-        return self.builder_recipe.get_recipe_text()
+        recipe_text = self.builder_recipe.get_recipe_text()
+        # For git-based recipes, mangle the header line to say
+        # "git-build-recipe" to reduce confusion; bzr-builder's recipe
+        # parser will always round-trip this to "bzr-builder".
+        if self.base_git_repository is not None:
+            recipe_text = re.sub(
+                r"^(#\s*)bzr-builder", r"\1git-build-recipe", recipe_text)
+        return recipe_text
 
     def updateSeries(self, distroseries):
         if distroseries != self.distroseries:

=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
--- lib/lp/code/model/sourcepackagerecipebuild.py	2016-01-12 01:39:14 +0000
+++ lib/lp/code/model/sourcepackagerecipebuild.py	2016-01-12 01:39:14 +0000
@@ -183,7 +183,7 @@
         if self.recipe is None:
             branch_name = 'deleted'
         else:
-            branch_name = self.recipe.base_branch.unique_name
+            branch_name = self.recipe.base.unique_name
         return '%s recipe build in %s %s' % (
             branch_name, self.distribution.name, self.distroseries.name)
 

=== modified file 'lib/lp/code/model/sourcepackagerecipedata.py'
--- lib/lp/code/model/sourcepackagerecipedata.py	2016-01-12 01:39:14 +0000
+++ lib/lp/code/model/sourcepackagerecipedata.py	2016-01-12 01:39:14 +0000
@@ -12,6 +12,7 @@
 __all__ = ['SourcePackageRecipeData']
 
 from itertools import groupby
+import re
 
 from bzrlib.plugins.builder.recipe import (
     BaseRecipeBranch,
@@ -45,16 +46,23 @@
 
 from lp.code.errors import (
     NoSuchBranch,
+    NoSuchGitRepository,
     PrivateBranchRecipe,
+    PrivateGitRepositoryRecipe,
     TooNewRecipeFormat,
     )
+from lp.code.interfaces.branch import IBranch
 from lp.code.interfaces.branchlookup import IBranchLookup
+from lp.code.interfaces.gitlookup import IGitLookup
+from lp.code.interfaces.gitref import IGitRef
+from lp.code.interfaces.gitrepository import IGitRepository
 from lp.code.interfaces.sourcepackagerecipe import (
     IRecipeBranchSource,
     ISourcePackageRecipeData,
     ISourcePackageRecipeDataSource,
     )
 from lp.code.model.branch import Branch
+from lp.code.model.gitrepository import GitRepository
 from lp.services.database.bulk import (
     load_referencing,
     load_related,
@@ -92,15 +100,25 @@
 
     __storm_table__ = "SourcePackageRecipeDataInstruction"
 
-    def __init__(self, name, type, comment, line_number, branch, revspec,
-                 directory, recipe_data, parent_instruction,
+    def __init__(self, name, type, comment, line_number, branch_or_repository,
+                 revspec, directory, recipe_data, parent_instruction,
                  source_directory):
         super(_SourcePackageRecipeDataInstruction, self).__init__()
         self.name = unicode(name)
         self.type = type
         self.comment = comment
         self.line_number = line_number
-        self.branch = branch
+        if IGitRepository.providedBy(branch_or_repository):
+            self.git_repository = branch_or_repository
+        elif IGitRef.providedBy(branch_or_repository):
+            self.git_repository = branch_or_repository
+            if revspec is None:
+                revspec = branch_or_repository.name
+        elif IBranch.providedBy(branch_or_repository):
+            self.branch = branch_or_repository
+        else:
+            raise AssertionError(
+                "Unsupported source: %r" % (branch_or_repository,))
         if revspec is not None:
             revspec = unicode(revspec)
         self.revspec = revspec
@@ -118,8 +136,10 @@
     comment = Unicode(allow_none=True)
     line_number = Int(allow_none=False)
 
-    branch_id = Int(name='branch', allow_none=False)
+    branch_id = Int(name='branch', allow_none=True)
     branch = Reference(branch_id, 'Branch.id')
+    git_repository_id = Int(name='git_repository', allow_none=True)
+    git_repository = Reference(git_repository_id, 'GitRepository.id')
 
     revspec = Unicode(allow_none=True)
     directory = Unicode(allow_none=True)
@@ -134,8 +154,12 @@
 
     def appendToRecipe(self, recipe_branch):
         """Append a bzr-builder instruction to the recipe_branch object."""
-        branch = RecipeBranch(
-            self.name, self.branch.bzr_identity, self.revspec)
+        if self.branch is not None:
+            identity = self.branch.identity
+        else:
+            assert self.git_repository is not None
+            identity = self.git_repository.identity
+        branch = RecipeBranch(self.name, identity, self.revspec)
         if self.type == InstructionType.MERGE:
             recipe_branch.merge_branch(branch)
         elif self.type == InstructionType.NEST:
@@ -165,8 +189,29 @@
 
     id = Int(primary=True)
 
-    base_branch_id = Int(name='base_branch', allow_none=False)
+    base_branch_id = Int(name='base_branch', allow_none=True)
     base_branch = Reference(base_branch_id, 'Branch.id')
+    base_git_repository_id = Int(name='base_git_repository', allow_none=True)
+    base_git_repository = Reference(base_git_repository_id, 'GitRepository.id')
+
+    @property
+    def base(self):
+        if self.base_branch is not None:
+            return self.base_branch
+        else:
+            assert self.base_git_repository is not None
+            return self.base_git_repository
+
+    @base.setter
+    def base(self, value):
+        if IGitRepository.providedBy(value):
+            self.base_git_repository = value
+            self.base_branch = None
+        elif IBranch.providedBy(value):
+            self.base_branch = value
+            self.base_git_repository = None
+        else:
+            raise AssertionError("Unsupported base: %r" % (value,))
 
     recipe_format = Unicode(allow_none=False)
     deb_version_template = Unicode(allow_none=True)
@@ -189,6 +234,12 @@
     @staticmethod
     def getParsedRecipe(recipe_text):
         """See `IRecipeBranchSource`."""
+        # We're using bzr-builder to parse the recipe text.  While the
+        # formats are mostly compatible, the header line must say
+        # "bzr-builder" even though git-build-recipe also supports its own
+        # name there.
+        recipe_text = re.sub(
+            r"^(#\s*)git-build-recipe", r"\1bzr-builder", recipe_text)
         parser = RecipeParser(recipe_text)
         return parser.parse(permitted_instructions=SAFE_INSTRUCTIONS)
 
@@ -222,7 +273,7 @@
     def getRecipe(self):
         """The BaseRecipeBranch version of the recipe."""
         base_branch = BaseRecipeBranch(
-            self.base_branch.bzr_identity, self.deb_version_template,
+            self.base.identity, self.deb_version_template,
             self.recipe_format, self.revspec)
         insn_stack = []
         for insn in self.instructions:
@@ -238,7 +289,7 @@
                 dict(insn=insn, recipe_branch=recipe_branch))
         return base_branch
 
-    def _scanInstructions(self, recipe_branch):
+    def _scanInstructions(self, base, recipe_branch):
         """Check the recipe_branch doesn't use 'run' and look up the branches.
 
         We do all the lookups before we start constructing database objects to
@@ -247,15 +298,22 @@
         :return: A map ``{branch_url: db_branch}``.
         """
         r = {}
+        if IGitRepository.providedBy(base):
+            lookup = getUtility(IGitLookup)
+            missing_error = NoSuchGitRepository
+            private_error = PrivateGitRepositoryRecipe
+        else:
+            lookup = getUtility(IBranchLookup)
+            missing_error = NoSuchBranch
+            private_error = PrivateBranchRecipe
         for instruction in recipe_branch.child_branches:
-            db_branch = getUtility(IBranchLookup).getByUrl(
-                instruction.recipe_branch.url)
+            db_branch = lookup.getByUrl(instruction.recipe_branch.url)
             if db_branch is None:
-                raise NoSuchBranch(instruction.recipe_branch.url)
+                raise missing_error(instruction.recipe_branch.url)
             if db_branch.private:
-                raise PrivateBranchRecipe(db_branch)
+                raise private_error(db_branch)
             r[instruction.recipe_branch.url] = db_branch
-            r.update(self._scanInstructions(instruction.recipe_branch))
+            r.update(self._scanInstructions(base, instruction.recipe_branch))
         return r
 
     def _recordInstructions(self, recipe_branch, parent_insn, branch_map,
@@ -280,10 +338,11 @@
                     "Unsupported instruction %r" % instruction)
             line_number += 1
             comment = None
-            db_branch = branch_map[instruction.recipe_branch.url]
+            db_branch_or_repository = branch_map[instruction.recipe_branch.url]
             insn = _SourcePackageRecipeDataInstruction(
                 instruction.recipe_branch.name, type, comment,
-                line_number, db_branch, instruction.recipe_branch.revspec,
+                line_number, db_branch_or_repository,
+                instruction.recipe_branch.revspec,
                 nest_path, self, parent_insn, source_directory)
             line_number = self._recordInstructions(
                 instruction.recipe_branch, insn, branch_map, line_number)
@@ -294,24 +353,33 @@
         clear_property_cache(self)
         if builder_recipe.format > MAX_RECIPE_FORMAT:
             raise TooNewRecipeFormat(builder_recipe.format, MAX_RECIPE_FORMAT)
-        branch_map = self._scanInstructions(builder_recipe)
+        base = getUtility(IBranchLookup).getByUrl(builder_recipe.url)
+        if base is None:
+            base = getUtility(IGitLookup).getByUrl(builder_recipe.url)
+            if base is None:
+                # If possible, try to raise an exception consistent with
+                # whether the current recipe is Bazaar-based or Git-based,
+                # so that error messages make more sense.
+                if self.base_git_repository is not None:
+                    raise NoSuchGitRepository(builder_recipe.url)
+                else:
+                    raise NoSuchBranch(builder_recipe.url)
+            elif base.private:
+                raise PrivateGitRepositoryRecipe(base)
+        elif base.private:
+            raise PrivateBranchRecipe(base)
+        branch_map = self._scanInstructions(base, builder_recipe)
         # If this object hasn't been added to a store yet, there can't be any
         # instructions linking to us yet.
         if Store.of(self) is not None:
             self.instructions.find().remove()
-        branch_lookup = getUtility(IBranchLookup)
-        base_branch = branch_lookup.getByUrl(builder_recipe.url)
-        if base_branch is None:
-            raise NoSuchBranch(builder_recipe.url)
-        if base_branch.private:
-            raise PrivateBranchRecipe(base_branch)
         if builder_recipe.revspec is not None:
             self.revspec = unicode(builder_recipe.revspec)
         else:
             self.revspec = None
         self._recordInstructions(
             builder_recipe, parent_insn=None, branch_map=branch_map)
-        self.base_branch = base_branch
+        self.base = base
         if builder_recipe.deb_version is None:
             self.deb_version_template = None
         else:
@@ -329,44 +397,72 @@
 
     @staticmethod
     def preLoadReferencedBranches(sourcepackagerecipedatas):
-        # Circular import.
+        # Circular imports.
         from lp.code.model.branchcollection import GenericBranchCollection
+        from lp.code.model.gitcollection import GenericGitCollection
         # Load the related Branch, _SourcePackageRecipeDataInstruction.
         base_branches = load_related(
             Branch, sourcepackagerecipedatas, ['base_branch_id'])
+        base_repositories = load_related(
+            GitRepository, sourcepackagerecipedatas,
+            ['base_git_repository_id'])
         sprd_instructions = load_referencing(
             _SourcePackageRecipeDataInstruction,
             sourcepackagerecipedatas, ['recipe_data_id'])
         sub_branches = load_related(
             Branch, sprd_instructions, ['branch_id'])
+        sub_repositories = load_related(
+            GitRepository, sprd_instructions, ['git_repository_id'])
         all_branches = base_branches + sub_branches
-        # Pre-load branches' data.
-        GenericBranchCollection.preloadDataForBranches(all_branches)
+        all_repositories = base_repositories + sub_repositories
+        # Pre-load branches'/repositories' data.
+        if all_branches:
+            GenericBranchCollection.preloadDataForBranches(all_branches)
+        if all_repositories:
+            GenericGitCollection.preloadDataForRepositories(all_repositories)
         # Store the pre-fetched objects on the sourcepackagerecipedatas
         # objects.
-        branch_to_recipe_data = dict([
-            (instr.branch_id, instr.recipe_data_id)
-                for instr in sprd_instructions])
-        caches = dict((sprd.id, [sprd, get_property_cache(sprd)])
-            for sprd in sourcepackagerecipedatas)
+        branch_to_recipe_data = {
+            instr.branch_id: instr.recipe_data_id
+            for instr in sprd_instructions
+            if instr.branch_id is not None}
+        repository_to_recipe_data = {
+            instr.git_repository_id: instr.recipe_data_id
+            for instr in sprd_instructions
+            if instr.git_repository_id is not None}
+        caches = {
+            sprd.id: [sprd, get_property_cache(sprd)]
+            for sprd in sourcepackagerecipedatas}
         for unused, [sprd, cache] in caches.items():
-            cache._referenced_branches = [sprd.base_branch]
+            cache._referenced_branches = [sprd.base]
         for recipe_data_id, branches in groupby(
-            sub_branches, lambda branch: branch_to_recipe_data[branch.id]):
+                sub_branches, lambda branch: branch_to_recipe_data[branch.id]):
             cache = caches[recipe_data_id][1]
             cache._referenced_branches.extend(list(branches))
+        for recipe_data_id, repositories in groupby(
+                sub_repositories,
+                lambda repository: repository_to_recipe_data[repository.id]):
+            cache = caches[recipe_data_id][1]
+            cache._referenced_branches.extend(list(repositories))
 
     def getReferencedBranches(self):
-        """Return an iterator of the Branch objects referenced by this recipe.
+        """Return an iterator of the Branch/GitRepository objects referenced
+        by this recipe.
         """
         return self._referenced_branches
 
     @cachedproperty
     def _referenced_branches(self):
-        referenced_branches = [self.base_branch]
+        referenced_branches = [self.base]
         sub_branches = IStore(self).find(
             Branch,
             _SourcePackageRecipeDataInstruction.recipe_data == self,
             Branch.id == _SourcePackageRecipeDataInstruction.branch_id)
         referenced_branches.extend(sub_branches)
+        sub_repositories = IStore(self).find(
+            GitRepository,
+            _SourcePackageRecipeDataInstruction.recipe_data == self,
+            GitRepository.id ==
+                _SourcePackageRecipeDataInstruction.git_repository_id)
+        referenced_branches.extend(sub_repositories)
         return referenced_branches

=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipe.py	2016-01-12 01:39:14 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py	2016-01-12 01:39:14 +0000
@@ -32,6 +32,7 @@
 from lp.code.errors import (
     BuildAlreadyPending,
     PrivateBranchRecipe,
+    PrivateGitRepositoryRecipe,
     TooNewRecipeFormat,
     )
 from lp.code.interfaces.sourcepackagerecipe import (
@@ -39,6 +40,7 @@
     ISourcePackageRecipeSource,
     ISourcePackageRecipeView,
     MINIMAL_RECIPE_TEXT_BZR,
+    MINIMAL_RECIPE_TEXT_GIT,
     )
 from lp.code.interfaces.sourcepackagerecipebuild import (
     ISourcePackageRecipeBuild,
@@ -86,14 +88,76 @@
 from lp.testing.pages import webservice_for_person
 
 
-class TestSourcePackageRecipe(TestCaseWithFactory):
+class BzrMixin:
+    """Mixin for Bazaar-based recipe tests."""
+
+    private_error = PrivateBranchRecipe
+    branch_type = "branch"
+    recipe_id = "bzr-builder"
+
+    def makeBranch(self, **kwargs):
+        return self.factory.makeAnyBranch(**kwargs)
+
+    @staticmethod
+    def getRepository(branch):
+        return branch
+
+    @staticmethod
+    def getBranchRecipeText(branch):
+        return branch.identity
+
+    @staticmethod
+    def setInformationType(branch, information_type):
+        removeSecurityProxy(branch).information_type = information_type
+
+    def makeRecipeText(self):
+        branch = self.makeBranch()
+        return MINIMAL_RECIPE_TEXT_BZR % branch.identity
+
+
+class GitMixin:
+    """Mixin for Git-based recipe tests."""
+
+    private_error = PrivateGitRepositoryRecipe
+    branch_type = "repository"
+    recipe_id = "git-build-recipe"
+
+    def makeBranch(self, **kwargs):
+        return self.factory.makeGitRefs(**kwargs)[0]
+
+    @staticmethod
+    def getRepository(branch):
+        return branch.repository
+
+    @staticmethod
+    def getBranchRecipeText(branch):
+        return branch.identity
+
+    @staticmethod
+    def setInformationType(branch, information_type):
+        removeSecurityProxy(branch.repository).information_type = (
+            information_type)
+
+    def makeRecipeText(self):
+        branch = self.makeBranch()
+        return MINIMAL_RECIPE_TEXT_GIT % (
+            branch.repository.identity, branch.name)
+
+
+class TestSourcePackageRecipeMixin:
     """Tests for `SourcePackageRecipe` objects."""
 
     layer = DatabaseFunctionalLayer
 
+    def makeSourcePackageRecipe(self, branches=(), recipe=None, **kwargs):
+        if recipe is None and len(branches) == 0:
+            branches = [self.makeBranch()]
+        return self.factory.makeSourcePackageRecipe(
+            branches=branches, recipe=recipe, **kwargs)
+
     def test_implements_interface(self):
         """SourcePackageRecipe implements ISourcePackageRecipe."""
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         verifyObject(ISourcePackageRecipe, recipe)
 
     def test_avoids_problematic_snapshots(self):
@@ -103,7 +167,7 @@
             'pending_builds',
             ]
         self.assertThat(
-            self.factory.makeSourcePackageRecipe(),
+            self.makeSourcePackageRecipe(),
             DoesNotSnapshot(problematic_properties, ISourcePackageRecipeView))
 
     def makeRecipeComponents(self, branches=()):
@@ -128,7 +192,7 @@
         components = self.makeRecipeComponents()
         recipe = getUtility(ISourcePackageRecipeSource).new(**components)
         transaction.commit()
-        self.assertEquals(
+        self.assertEqual(
             (components['registrant'], components['owner'],
              set(components['distroseries']), components['name']),
             (recipe.registrant, recipe.owner, set(recipe.distroseries),
@@ -139,35 +203,38 @@
         """An exception should be raised if the base branch is private."""
         owner = self.factory.makePerson()
         with person_logged_in(owner):
-            branch = self.factory.makeAnyBranch(
+            branch = self.makeBranch(
                 owner=owner, information_type=InformationType.USERDATA)
             components = self.makeRecipeComponents(branches=[branch])
             recipe_source = getUtility(ISourcePackageRecipeSource)
             e = self.assertRaises(
-                PrivateBranchRecipe, recipe_source.new, **components)
+                self.private_error, recipe_source.new, **components)
             self.assertEqual(
-                'Recipe may not refer to private branch: %s' %
-                branch.bzr_identity, str(e))
+                'Recipe may not refer to private %s: %s' %
+                    (self.branch_type, self.getRepository(branch).identity),
+                str(e))
 
     def test_creation_private_referenced_branch(self):
         """An exception should be raised if a referenced branch is private."""
         owner = self.factory.makePerson()
         with person_logged_in(owner):
-            base_branch = self.factory.makeAnyBranch(owner=owner)
-            referenced_branch = self.factory.makeAnyBranch(
+            base_branch = self.makeBranch(owner=owner)
+            referenced_branch = self.makeBranch(
                 owner=owner, information_type=InformationType.USERDATA)
             branches = [base_branch, referenced_branch]
             components = self.makeRecipeComponents(branches=branches)
             recipe_source = getUtility(ISourcePackageRecipeSource)
             e = self.assertRaises(
-                PrivateBranchRecipe, recipe_source.new, **components)
+                self.private_error, recipe_source.new, **components)
             self.assertEqual(
-                'Recipe may not refer to private branch: %s' %
-                referenced_branch.bzr_identity, str(e))
+                'Recipe may not refer to private %s: %s' % (
+                    self.branch_type,
+                    self.getRepository(referenced_branch).identity),
+                str(e))
 
     def test_exists(self):
         # Test ISourcePackageRecipeSource.exists
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
 
         self.assertTrue(
             getUtility(ISourcePackageRecipeSource).exists(
@@ -185,32 +252,33 @@
 
     def test_recipe_implements_interface(self):
         # SourcePackageRecipe objects implement ISourcePackageRecipe.
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         transaction.commit()
         with person_logged_in(recipe.owner):
             self.assertProvides(recipe, ISourcePackageRecipe)
 
     def test_base_branch(self):
         # When a recipe is created, we can access its base branch.
-        branch = self.factory.makeAnyBranch()
-        sp_recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
+        branch = self.makeBranch()
+        sp_recipe = self.makeSourcePackageRecipe(branches=[branch])
         transaction.commit()
-        self.assertEquals(branch, sp_recipe.base_branch)
+        self.assertEqual(self.getRepository(branch), sp_recipe.base)
 
     def test_branch_links_created(self):
         # When a recipe is created, we can query it for links to the branch
         # it references.
-        branch = self.factory.makeAnyBranch()
-        sp_recipe = self.factory.makeSourcePackageRecipe(
-            branches=[branch])
+        branch = self.makeBranch()
+        sp_recipe = self.makeSourcePackageRecipe(branches=[branch])
         transaction.commit()
-        self.assertEquals([branch], list(sp_recipe.getReferencedBranches()))
+        self.assertEqual(
+            [self.getRepository(branch)],
+            list(sp_recipe.getReferencedBranches()))
 
     def createSourcePackageRecipe(self, number_of_branches=2):
         branches = []
         for i in range(number_of_branches):
-            branches.append(self.factory.makeAnyBranch())
-        sp_recipe = self.factory.makeSourcePackageRecipe(branches=branches)
+            branches.append(self.makeBranch())
+        sp_recipe = self.makeSourcePackageRecipe(branches=branches)
         transaction.commit()
         return sp_recipe, branches
 
@@ -218,8 +286,8 @@
         # If a recipe links to more than one branch, getReferencedBranches()
         # returns all of them.
         sp_recipe, [branch1, branch2] = self.createSourcePackageRecipe()
-        self.assertEquals(
-            sorted([branch1, branch2]),
+        self.assertEqual(
+            sorted([self.getRepository(branch1), self.getRepository(branch2)]),
             sorted(sp_recipe.getReferencedBranches()))
 
     def test_preLoadReferencedBranches(self):
@@ -230,16 +298,15 @@
         referenced_branches = sp_recipe.getReferencedBranches()
         clear_property_cache(recipe_data)
         SourcePackageRecipeData.preLoadReferencedBranches([recipe_data])
-        self.assertEquals(
+        self.assertEqual(
             sorted(referenced_branches),
             sorted(sp_recipe.getReferencedBranches()))
 
     def test_random_user_cant_edit(self):
         # An arbitrary user can't set attributes.
-        branch1 = self.factory.makeAnyBranch()
+        branch1 = self.makeBranch()
         recipe_1 = self.factory.makeRecipeText(branch1)
-        sp_recipe = self.factory.makeSourcePackageRecipe(
-            recipe=recipe_1)
+        sp_recipe = self.makeSourcePackageRecipe(recipe=recipe_1)
         login_person(self.factory.makePerson())
         self.assertRaises(
             Unauthorized, getattr, sp_recipe, 'setRecipeText')
@@ -247,71 +314,78 @@
     def test_set_recipe_text_resets_branch_references(self):
         # When the recipe_text is replaced, getReferencedBranches returns
         # (only) the branches referenced by the new recipe.
-        branch1 = self.factory.makeAnyBranch()
-        sp_recipe = self.factory.makeSourcePackageRecipe(
-            branches=[branch1])
-        branch2 = self.factory.makeAnyBranch()
+        branch1 = self.makeBranch()
+        sp_recipe = self.makeSourcePackageRecipe(branches=[branch1])
+        branch2 = self.makeBranch()
         new_recipe = self.factory.makeRecipeText(branch2)
         with person_logged_in(sp_recipe.owner):
             sp_recipe.setRecipeText(new_recipe)
-        self.assertEquals([branch2], list(sp_recipe.getReferencedBranches()))
+        self.assertEqual(
+            [self.getRepository(branch2)],
+            list(sp_recipe.getReferencedBranches()))
 
     def test_rejects_run_command(self):
         recipe_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s
         run touch test
-        ''' % dict(base=self.factory.makeAnyBranch().bzr_identity)
+        ''' % dict(recipe_id=self.recipe_id,
+                   base=self.getBranchRecipeText(self.makeBranch()))
         recipe_text = textwrap.dedent(recipe_text)
         self.assertRaises(
-            ForbiddenInstructionError, self.factory.makeSourcePackageRecipe,
+            ForbiddenInstructionError, self.makeSourcePackageRecipe,
             recipe=recipe_text)
 
     def test_run_rejected_without_mangling_recipe(self):
-        sp_recipe = self.factory.makeSourcePackageRecipe()
+        sp_recipe = self.makeSourcePackageRecipe()
         old_branches = list(sp_recipe.getReferencedBranches())
         recipe_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s
         run touch test
-        ''' % dict(base=self.factory.makeAnyBranch().bzr_identity)
+        ''' % dict(recipe_id=self.recipe_id,
+                   base=self.getBranchRecipeText(self.makeBranch()))
         recipe_text = textwrap.dedent(recipe_text)
         with person_logged_in(sp_recipe.owner):
             self.assertRaises(
                 ForbiddenInstructionError, sp_recipe.setRecipeText,
                 recipe_text)
-        self.assertEquals(
+        self.assertEqual(
             old_branches, list(sp_recipe.getReferencedBranches()))
 
     def test_nest_part(self):
         """nest-part instruction can be round-tripped."""
-        base = self.factory.makeBranch()
-        nested = self.factory.makeBranch()
+        base = self.makeBranch()
+        nested = self.makeBranch()
         recipe_text = (
-            "# bzr-builder format 0.3 deb-version 1\n"
+            "# %s format 0.3 deb-version 1\n"
             "%s revid:base_revid\n"
             "nest-part nested1 %s foo bar tag:foo\n" %
-            (base.bzr_identity, nested.bzr_identity))
-        recipe = self.factory.makeSourcePackageRecipe(recipe=recipe_text)
+            (self.recipe_id,
+             self.getRepository(base).identity,
+             self.getRepository(nested).identity))
+        recipe = self.makeSourcePackageRecipe(recipe=recipe_text)
         self.assertEqual(recipe_text, recipe.recipe_text)
 
     def test_nest_part_no_target(self):
         """nest-part instruction with no target-dir can be round-tripped."""
-        base = self.factory.makeBranch()
-        nested = self.factory.makeBranch()
+        base = self.makeBranch()
+        nested = self.makeBranch()
         recipe_text = (
-            "# bzr-builder format 0.3 deb-version 1\n"
+            "# %s format 0.3 deb-version 1\n"
             "%s revid:base_revid\n"
             "nest-part nested1 %s foo\n" %
-            (base.bzr_identity, nested.bzr_identity))
-        recipe = self.factory.makeSourcePackageRecipe(recipe=recipe_text)
+            (self.recipe_id,
+             self.getRepository(base).identity,
+             self.getRepository(nested).identity))
+        recipe = self.makeSourcePackageRecipe(recipe=recipe_text)
         self.assertEqual(recipe_text, recipe.recipe_text)
 
     def test_accept_format_0_3(self):
         """Recipe format 0.3 is accepted."""
         builder_recipe = self.factory.makeRecipe()
         builder_recipe.format = 0.3
-        self.factory.makeSourcePackageRecipe(recipe=str(builder_recipe))
+        self.makeSourcePackageRecipe(recipe=str(builder_recipe))
 
     def test_reject_newer_formats(self):
         with recipe_parser_newest_version(145.115):
@@ -319,11 +393,11 @@
             builder_recipe.format = 145.115
             self.assertRaises(
                 TooNewRecipeFormat,
-                self.factory.makeSourcePackageRecipe,
+                self.makeSourcePackageRecipe,
                 recipe=str(builder_recipe))
 
     def test_requestBuild(self):
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         (distroseries,) = list(recipe.distroseries)
         ppa = self.factory.makeArchive()
         build = recipe.requestBuild(ppa, ppa.owner, distroseries,
@@ -342,17 +416,17 @@
                 removeSecurityProxy(build).build_farm_job_id).one()
         self.assertProvides(build_queue, IBuildQueue)
         self.assertTrue(build_queue.virtualized)
-        self.assertEquals(build_queue.status, BuildQueueStatus.WAITING)
+        self.assertEqual(build_queue.status, BuildQueueStatus.WAITING)
 
     def test_requestBuildRejectsNotPPA(self):
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         not_ppa = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
         (distroseries,) = list(recipe.distroseries)
         self.assertRaises(NonPPABuildRequest, recipe.requestBuild, not_ppa,
                 not_ppa.owner, distroseries, PackagePublishingPocket.RELEASE)
 
     def test_requestBuildRejectsNoPermission(self):
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         ppa = self.factory.makeArchive()
         requester = self.factory.makePerson()
         (distroseries,) = list(recipe.distroseries)
@@ -360,14 +434,14 @@
                 requester, distroseries, PackagePublishingPocket.RELEASE)
 
     def test_requestBuildRejectsInvalidPocket(self):
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         ppa = self.factory.makeArchive()
         (distroseries,) = list(recipe.distroseries)
         self.assertRaises(InvalidPocketForPPA, recipe.requestBuild, ppa,
                 ppa.owner, distroseries, PackagePublishingPocket.BACKPORTS)
 
     def test_requestBuildRejectsDisabledArchive(self):
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         ppa = self.factory.makeArchive()
         removeSecurityProxy(ppa).disable()
         (distroseries,) = list(recipe.distroseries)
@@ -377,7 +451,7 @@
 
     def test_requestBuildScore(self):
         """Normal build requests have a relatively low queue score (2505)."""
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         build = recipe.requestBuild(recipe.daily_build_archive,
             recipe.owner, list(recipe.distroseries)[0],
             PackagePublishingPocket.RELEASE)
@@ -387,7 +461,7 @@
 
     def test_requestBuildManualScore(self):
         """Manual build requests have a score equivalent to binary builds."""
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         build = recipe.requestBuild(recipe.daily_build_archive,
             recipe.owner, list(recipe.distroseries)[0],
             PackagePublishingPocket.RELEASE, manual=True)
@@ -397,7 +471,7 @@
 
     def test_requestBuild_relative_build_score(self):
         """Offsets for archives are respected."""
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         archive = recipe.daily_build_archive
         removeSecurityProxy(archive).relative_build_score = 100
         build = recipe.requestBuild(
@@ -409,7 +483,7 @@
 
     def test_requestBuildRejectRepeats(self):
         """Reject build requests that are identical to pending builds."""
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         series = list(recipe.distroseries)[0]
         archive = self.factory.makeArchive(owner=recipe.owner)
         old_build = recipe.requestBuild(archive, recipe.owner, series,
@@ -447,7 +521,7 @@
             private=True)
 
         # Create a recipe with the team P3A as the build destination.
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
 
         # Add upload component rights for the non-team person.
         with person_logged_in(team_owner):
@@ -467,13 +541,13 @@
     def test_sourcepackagerecipe_description(self):
         """Ensure that the SourcePackageRecipe has a proper description."""
         description = u'The whoozits and whatzits.'
-        source_package_recipe = self.factory.makeSourcePackageRecipe(
+        source_package_recipe = self.makeSourcePackageRecipe(
             description=description)
         self.assertEqual(description, source_package_recipe.description)
 
     def test_distroseries(self):
         """Test that the distroseries behaves as a set."""
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         distroseries = self.factory.makeDistroSeries()
         (old_distroseries,) = recipe.distroseries
         recipe.distroseries.add(distroseries)
@@ -486,7 +560,7 @@
 
     def test_build_daily(self):
         """Test that build_daily behaves as a bool."""
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         self.assertFalse(recipe.build_daily)
         login_person(recipe.owner)
         recipe.build_daily = True
@@ -495,9 +569,9 @@
     def test_view_public(self):
         """Anyone can view a recipe with public branches."""
         owner = self.factory.makePerson()
-        branch = self.factory.makeAnyBranch(owner=owner)
+        branch = self.makeBranch(owner=owner)
         with person_logged_in(owner):
-            recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
+            recipe = self.makeSourcePackageRecipe(branches=[branch])
             self.assertTrue(check_permission('launchpad.View', recipe))
         with person_logged_in(self.factory.makePerson()):
             self.assertTrue(check_permission('launchpad.View', recipe))
@@ -506,19 +580,18 @@
     def test_view_private(self):
         """Recipes with private branches are restricted."""
         owner = self.factory.makePerson()
-        branch = self.factory.makeAnyBranch(owner=owner)
+        branch = self.makeBranch(owner=owner)
         with person_logged_in(owner):
-            recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
+            recipe = self.makeSourcePackageRecipe(branches=[branch])
             self.assertTrue(check_permission('launchpad.View', recipe))
-        removeSecurityProxy(branch).information_type = (
-            InformationType.USERDATA)
+        self.setInformationType(branch, InformationType.USERDATA)
         with person_logged_in(self.factory.makePerson()):
             self.assertFalse(check_permission('launchpad.View', recipe))
         self.assertFalse(check_permission('launchpad.View', recipe))
 
     def test_edit(self):
         """Only the owner can edit a sourcepackagerecipe."""
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         self.assertFalse(check_permission('launchpad.Edit', recipe))
         with person_logged_in(self.factory.makePerson()):
             self.assertFalse(check_permission('launchpad.Edit', recipe))
@@ -528,8 +601,8 @@
     def test_destroySelf(self):
         """Should destroy associated builds, distroseries, etc."""
         # Recipe should have at least one datainstruction.
-        branches = [self.factory.makeBranch() for count in range(2)]
-        recipe = self.factory.makeSourcePackageRecipe(branches=branches)
+        branches = [self.makeBranch() for count in range(2)]
+        recipe = self.makeSourcePackageRecipe(branches=branches)
         pending_build = self.factory.makeSourcePackageRecipeBuild(
             recipe=recipe)
         pending_build.queueBuild()
@@ -545,7 +618,7 @@
     def test_destroySelf_preserves_release(self):
         # Destroying a sourcepackagerecipe removes references to its builds
         # from their releases.
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
         release = self.factory.makeSourcePackageRelease(
             source_package_recipe_build=build)
@@ -557,7 +630,7 @@
     def test_destroySelf_retains_build(self):
         # Destroying a sourcepackagerecipe removes references to its builds
         # from their releases.
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
         store = Store.of(build)
         store.flush()
@@ -578,12 +651,11 @@
 
     def test_findStaleDailyBuilds(self):
         # Stale recipe not built daily.
-        self.factory.makeSourcePackageRecipe()
+        self.makeSourcePackageRecipe()
         # Daily build recipe not stale.
-        self.factory.makeSourcePackageRecipe(
-            build_daily=True, is_stale=False)
+        self.makeSourcePackageRecipe(build_daily=True, is_stale=False)
         # Stale daily build.
-        stale_daily = self.factory.makeSourcePackageRecipe(
+        stale_daily = self.makeSourcePackageRecipe(
             build_daily=True, is_stale=True)
         self.assertContentEqual([stale_daily],
             SourcePackageRecipe.findStaleDailyBuilds())
@@ -591,8 +663,7 @@
     def test_findStaleDailyBuildsDistinct(self):
         # If a recipe has 2 builds due to 2 distroseries, it only returns
         # one recipe.
-        recipe = self.factory.makeSourcePackageRecipe(
-            build_daily=True, is_stale=True)
+        recipe = self.makeSourcePackageRecipe(build_daily=True, is_stale=True)
         hoary = self.factory.makeSourcePackageRecipeDistroseries("hoary")
         recipe.distroseries.add(hoary)
         for series in recipe.distroseries:
@@ -613,7 +684,7 @@
             build.updateStatus(
                 BuildStatus.FULLYBUILT,
                 date_finished=build.date_started + duration)
-        recipe = removeSecurityProxy(self.factory.makeSourcePackageRecipe())
+        recipe = removeSecurityProxy(self.makeSourcePackageRecipe())
         self.assertIs(None, recipe.getMedianBuildDuration())
         build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
         set_duration(build, 10)
@@ -632,7 +703,7 @@
 
     def test_getBuilds(self):
         # Test the various getBuilds methods.
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         builds = [
                 self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
                 for x in range(3)]
@@ -654,7 +725,7 @@
         person = self.factory.makePerson()
         archives = [self.factory.makeArchive(owner=person) for x in range(4)]
         distroseries = self.factory.makeSourcePackageRecipeDistroseries()
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
 
         build_info = []
         for archive in archives:
@@ -666,7 +737,7 @@
 
     def test_getBuilds_cancelled(self):
         # Cancelled builds are not considered pending.
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
         with admin_logged_in():
             build.queueBuild()
@@ -676,40 +747,42 @@
         self.assertEqual([], list(recipe.pending_builds))
 
     def test_setRecipeText_private_base_branch(self):
-        source_package_recipe = self.factory.makeSourcePackageRecipe()
+        source_package_recipe = self.makeSourcePackageRecipe()
         with person_logged_in(source_package_recipe.owner):
-            branch = self.factory.makeAnyBranch(
+            branch = self.makeBranch(
                 owner=source_package_recipe.owner,
                 information_type=InformationType.USERDATA)
             recipe_text = self.factory.makeRecipeText(branch)
             e = self.assertRaises(
-                PrivateBranchRecipe, source_package_recipe.setRecipeText,
+                self.private_error, source_package_recipe.setRecipeText,
                 recipe_text)
             self.assertEqual(
-                'Recipe may not refer to private branch: %s' %
-                branch.bzr_identity, str(e))
+                'Recipe may not refer to private %s: %s' %
+                    (self.branch_type, self.getRepository(branch).identity),
+                str(e))
 
     def test_setRecipeText_private_referenced_branch(self):
-        source_package_recipe = self.factory.makeSourcePackageRecipe()
+        source_package_recipe = self.makeSourcePackageRecipe()
         with person_logged_in(source_package_recipe.owner):
-            base_branch = self.factory.makeAnyBranch(
-                owner=source_package_recipe.owner)
-            referenced_branch = self.factory.makeAnyBranch(
+            base_branch = self.makeBranch(owner=source_package_recipe.owner)
+            referenced_branch = self.makeBranch(
                 owner=source_package_recipe.owner,
                 information_type=InformationType.USERDATA)
             recipe_text = self.factory.makeRecipeText(
                 base_branch, referenced_branch)
             e = self.assertRaises(
-                PrivateBranchRecipe, source_package_recipe.setRecipeText,
+                self.private_error, source_package_recipe.setRecipeText,
                 recipe_text)
             self.assertEqual(
-                'Recipe may not refer to private branch: %s' %
-                referenced_branch.bzr_identity, str(e))
+                'Recipe may not refer to private %s: %s' %
+                    (self.branch_type,
+                     self.getRepository(referenced_branch).identity),
+                str(e))
 
     def test_getBuilds_ignores_disabled_archive(self):
         # Builds into a disabled archive aren't returned.
         archive = self.factory.makeArchive()
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         self.factory.makeSourcePackageRecipeBuild(
             recipe=recipe, archive=archive)
         with person_logged_in(archive.owner):
@@ -719,19 +792,19 @@
         self.assertEqual([], list(recipe.pending_builds))
 
     def test_containsUnbuildableSeries(self):
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         self.assertFalse(recipe.containsUnbuildableSeries(
             recipe.daily_build_archive))
 
     def test_containsUnbuildableSeries_with_obsolete_series(self):
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         warty = self.factory.makeSourcePackageRecipeDistroseries()
         removeSecurityProxy(warty).status = SeriesStatus.OBSOLETE
         self.assertTrue(recipe.containsUnbuildableSeries(
             recipe.daily_build_archive))
 
     def test_performDailyBuild_filters_obsolete_series(self):
-        recipe = self.factory.makeSourcePackageRecipe()
+        recipe = self.makeSourcePackageRecipe()
         warty = self.factory.makeSourcePackageRecipeDistroseries()
         hoary = self.factory.makeSourcePackageRecipeDistroseries(name='hoary')
         with person_logged_in(recipe.owner):
@@ -741,19 +814,30 @@
         self.assertEqual([build.recipe for build in builds], [recipe])
 
 
-class TestRecipeBranchRoundTripping(TestCaseWithFactory):
+class TestSourcePackageRecipeBzr(
+    TestSourcePackageRecipeMixin, BzrMixin, TestCaseWithFactory):
+    """Test `SourcePackageRecipe` objects for Bazaar."""
+
+
+class TestSourcePackageRecipeGit(
+    TestSourcePackageRecipeMixin, GitMixin, TestCaseWithFactory):
+    """Test `SourcePackageRecipe` objects for Git."""
+
+
+class TestRecipeBranchRoundTrippingMixin:
 
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
-        super(TestRecipeBranchRoundTripping, self).setUp()
-        self.base_branch = self.factory.makeAnyBranch()
-        self.nested_branch = self.factory.makeAnyBranch()
-        self.merged_branch = self.factory.makeAnyBranch()
+        super(TestRecipeBranchRoundTrippingMixin, self).setUp()
+        self.base_branch = self.makeBranch()
+        self.nested_branch = self.makeBranch()
+        self.merged_branch = self.makeBranch()
         self.branch_identities = {
-            'base': self.base_branch.bzr_identity,
-            'nested': self.nested_branch.bzr_identity,
-            'merged': self.merged_branch.bzr_identity,
+            'recipe_id': self.recipe_id,
+            'base': self.getRepository(self.base_branch).identity,
+            'nested': self.getRepository(self.nested_branch).identity,
+            'merged': self.getRepository(self.merged_branch).identity,
             }
 
     def get_recipe(self, recipe_text):
@@ -785,161 +869,173 @@
 
     def test_builds_simplest_recipe(self):
         recipe_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s
         ''' % self.branch_identities
         base_branch = self.get_recipe(recipe_text).builder_recipe
         self.check_base_recipe_branch(
-            base_branch, self.base_branch.bzr_identity,
+            base_branch, self.getRepository(self.base_branch).identity,
             deb_version='0.1-{revno}')
 
     def test_builds_recipe_with_merge(self):
         recipe_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s
         merge bar %(merged)s
         ''' % self.branch_identities
         base_branch = self.get_recipe(recipe_text).builder_recipe
         self.check_base_recipe_branch(
-            base_branch, self.base_branch.bzr_identity, num_child_branches=1,
-            deb_version='0.1-{revno}')
+            base_branch, self.getRepository(self.base_branch).identity,
+            num_child_branches=1, deb_version='0.1-{revno}')
         child_branch, location = base_branch.child_branches[0].as_tuple()
         self.assertEqual(None, location)
         self.check_recipe_branch(
-            child_branch, "bar", self.merged_branch.bzr_identity)
+            child_branch, "bar",
+            self.getRepository(self.merged_branch).identity)
 
     def test_builds_recipe_with_nest(self):
         recipe_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s
         nest bar %(nested)s baz
         ''' % self.branch_identities
         base_branch = self.get_recipe(recipe_text).builder_recipe
         self.check_base_recipe_branch(
-            base_branch, self.base_branch.bzr_identity, num_child_branches=1,
-            deb_version='0.1-{revno}')
+            base_branch, self.getRepository(self.base_branch).identity,
+            num_child_branches=1, deb_version='0.1-{revno}')
         child_branch, location = base_branch.child_branches[0].as_tuple()
         self.assertEqual("baz", location)
         self.check_recipe_branch(
-            child_branch, "bar", self.nested_branch.bzr_identity)
+            child_branch, "bar",
+            self.getRepository(self.nested_branch).identity)
 
     def test_builds_recipe_with_nest_then_merge(self):
         recipe_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s
         nest bar %(nested)s baz
         merge zam %(merged)s
         ''' % self.branch_identities
         base_branch = self.get_recipe(recipe_text).builder_recipe
         self.check_base_recipe_branch(
-            base_branch, self.base_branch.bzr_identity, num_child_branches=2,
-            deb_version='0.1-{revno}')
+            base_branch, self.getRepository(self.base_branch).identity,
+            num_child_branches=2, deb_version='0.1-{revno}')
         child_branch, location = base_branch.child_branches[0].as_tuple()
         self.assertEqual("baz", location)
         self.check_recipe_branch(
-            child_branch, "bar", self.nested_branch.bzr_identity)
+            child_branch, "bar",
+            self.getRepository(self.nested_branch).identity)
         child_branch, location = base_branch.child_branches[1].as_tuple()
         self.assertEqual(None, location)
         self.check_recipe_branch(
-            child_branch, "zam", self.merged_branch.bzr_identity)
+            child_branch, "zam",
+            self.getRepository(self.merged_branch).identity)
 
     def test_builds_recipe_with_merge_then_nest(self):
         recipe_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s
         merge zam %(merged)s
         nest bar %(nested)s baz
         ''' % self.branch_identities
         base_branch = self.get_recipe(recipe_text).builder_recipe
         self.check_base_recipe_branch(
-            base_branch, self.base_branch.bzr_identity, num_child_branches=2,
-            deb_version='0.1-{revno}')
+            base_branch, self.getRepository(self.base_branch).identity,
+            num_child_branches=2, deb_version='0.1-{revno}')
         child_branch, location = base_branch.child_branches[0].as_tuple()
         self.assertEqual(None, location)
         self.check_recipe_branch(
-            child_branch, "zam", self.merged_branch.bzr_identity)
+            child_branch, "zam",
+            self.getRepository(self.merged_branch).identity)
         child_branch, location = base_branch.child_branches[1].as_tuple()
         self.assertEqual("baz", location)
         self.check_recipe_branch(
-            child_branch, "bar", self.nested_branch.bzr_identity)
+            child_branch, "bar",
+            self.getRepository(self.nested_branch).identity)
 
     def test_builds_a_merge_in_to_a_nest(self):
         recipe_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s
         nest bar %(nested)s baz
           merge zam %(merged)s
         ''' % self.branch_identities
         base_branch = self.get_recipe(recipe_text).builder_recipe
         self.check_base_recipe_branch(
-            base_branch, self.base_branch.bzr_identity, num_child_branches=1,
-            deb_version='0.1-{revno}')
+            base_branch, self.getRepository(self.base_branch).identity,
+            num_child_branches=1, deb_version='0.1-{revno}')
         child_branch, location = base_branch.child_branches[0].as_tuple()
         self.assertEqual("baz", location)
         self.check_recipe_branch(
-            child_branch, "bar", self.nested_branch.bzr_identity,
+            child_branch, "bar",
+            self.getRepository(self.nested_branch).identity,
             num_child_branches=1)
         child_branch, location = child_branch.child_branches[0].as_tuple()
         self.assertEqual(None, location)
         self.check_recipe_branch(
-            child_branch, "zam", self.merged_branch.bzr_identity)
+            child_branch, "zam",
+            self.getRepository(self.merged_branch).identity)
 
     def tests_builds_nest_into_a_nest(self):
-        nested2 = self.factory.makeAnyBranch()
-        self.branch_identities['nested2'] = nested2.bzr_identity
+        nested2 = self.makeBranch()
+        self.branch_identities['nested2'] = (
+            self.getRepository(nested2).identity)
         recipe_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s
         nest bar %(nested)s baz
           nest zam %(nested2)s zoo
         ''' % self.branch_identities
         base_branch = self.get_recipe(recipe_text).builder_recipe
         self.check_base_recipe_branch(
-            base_branch, self.base_branch.bzr_identity, num_child_branches=1,
-            deb_version='0.1-{revno}')
+            base_branch, self.getRepository(self.base_branch).identity,
+            num_child_branches=1, deb_version='0.1-{revno}')
         child_branch, location = base_branch.child_branches[0].as_tuple()
         self.assertEqual("baz", location)
         self.check_recipe_branch(
-            child_branch, "bar", self.nested_branch.bzr_identity,
+            child_branch, "bar",
+            self.getRepository(self.nested_branch).identity,
             num_child_branches=1)
         child_branch, location = child_branch.child_branches[0].as_tuple()
         self.assertEqual("zoo", location)
-        self.check_recipe_branch(child_branch, "zam", nested2.bzr_identity)
+        self.check_recipe_branch(
+            child_branch, "zam", self.getRepository(nested2).identity)
 
     def tests_builds_recipe_with_revspecs(self):
         recipe_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s revid:a
         nest bar %(nested)s baz tag:b
         merge zam %(merged)s 2
         ''' % self.branch_identities
         base_branch = self.get_recipe(recipe_text).builder_recipe
         self.check_base_recipe_branch(
-            base_branch, self.base_branch.bzr_identity, num_child_branches=2,
-            revspec="revid:a", deb_version='0.1-{revno}')
+            base_branch, self.getRepository(self.base_branch).identity,
+            num_child_branches=2, revspec="revid:a", deb_version='0.1-{revno}')
         instruction = base_branch.child_branches[0]
         child_branch = instruction.recipe_branch
         location = instruction.nest_path
         self.assertEqual("baz", location)
         self.check_recipe_branch(
-            child_branch, "bar", self.nested_branch.bzr_identity,
-            revspec="tag:b")
+            child_branch, "bar",
+            self.getRepository(self.nested_branch).identity, revspec="tag:b")
         child_branch, location = base_branch.child_branches[1].as_tuple()
         self.assertEqual(None, location)
         self.check_recipe_branch(
-            child_branch, "zam", self.merged_branch.bzr_identity, revspec="2")
+            child_branch, "zam",
+            self.getRepository(self.merged_branch).identity, revspec="2")
 
     def test_unsets_revspecs(self):
         # Changing a recipe's text to no longer include revspecs unsets
         # them from the stored copy.
         revspec_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s revid:a
         nest bar %(nested)s baz tag:b
         merge zam %(merged)s 2
         ''' % self.branch_identities
         no_revspec_text = '''\
-        # bzr-builder format 0.3 deb-version 0.1-{revno}
+        # %(recipe_id)s format 0.3 deb-version 0.1-{revno}
         %(base)s
         nest bar %(nested)s baz
         merge zam %(merged)s
@@ -952,18 +1048,29 @@
 
     def test_builds_recipe_without_debversion(self):
         recipe_text = '''\
-        # bzr-builder format 0.4
+        # %(recipe_id)s format 0.4
         %(base)s
         nest bar %(nested)s baz
         ''' % self.branch_identities
         base_branch = self.get_recipe(recipe_text).builder_recipe
         self.check_base_recipe_branch(
-            base_branch, self.base_branch.bzr_identity, num_child_branches=1,
-            deb_version=None)
+            base_branch, self.getRepository(self.base_branch).identity,
+            num_child_branches=1, deb_version=None)
         child_branch, location = base_branch.child_branches[0].as_tuple()
         self.assertEqual("baz", location)
         self.check_recipe_branch(
-            child_branch, "bar", self.nested_branch.bzr_identity)
+            child_branch, "bar",
+            self.getRepository(self.nested_branch).identity)
+
+
+class TestRecipeBranchRoundTrippingBzr(
+    TestRecipeBranchRoundTrippingMixin, BzrMixin, TestCaseWithFactory):
+    pass
+
+
+class TestRecipeBranchRoundTrippingGit(
+    TestRecipeBranchRoundTrippingMixin, GitMixin, TestCaseWithFactory):
+    pass
 
 
 class RecipeDateLastModified(TestCaseWithFactory):

=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py	2015-10-19 10:56:16 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py	2016-01-12 01:39:14 +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 source package builds."""
@@ -106,11 +106,11 @@
         self.assertEqual(bq, spb.buildqueue_record)
 
     def test_title(self):
-        # A recipe build's title currently consists of the base
-        # branch's unique name.
+        # A recipe build's title currently consists of the base source
+        # location's unique name.
         spb = self.makeSourcePackageRecipeBuild()
         title = "%s recipe build in %s %s" % (
-            spb.recipe.base_branch.unique_name, spb.distribution.name,
+            spb.recipe.base.unique_name, spb.distribution.name,
             spb.distroseries.name)
         self.assertEqual(spb.title, title)
 

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2016-01-12 01:39:14 +0000
+++ lib/lp/testing/factory.py	2016-01-12 01:39:14 +0000
@@ -2,7 +2,7 @@
 # NOTE: The first line above must stay first; do not move the copyright
 # notice to the top.  See http://www.python.org/dev/peps/pep-0263/.
 #
-# 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).
 
 """Testing infrastructure for the Launchpad application.
@@ -112,6 +112,7 @@
     RevisionControlSystems,
     )
 from lp.code.errors import UnknownBranchTypeError
+from lp.code.interfaces.branch import IBranch
 from lp.code.interfaces.branchnamespace import get_branch_namespace
 from lp.code.interfaces.branchtarget import IBranchTarget
 from lp.code.interfaces.codeimport import ICodeImportSet
@@ -119,11 +120,13 @@
 from lp.code.interfaces.codeimportmachine import ICodeImportMachineSet
 from lp.code.interfaces.codeimportresult import ICodeImportResultSet
 from lp.code.interfaces.gitnamespace import get_git_namespace
+from lp.code.interfaces.gitref import IGitRef
 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
 from lp.code.interfaces.revision import IRevisionSet
 from lp.code.interfaces.sourcepackagerecipe import (
     ISourcePackageRecipeSource,
     MINIMAL_RECIPE_TEXT_BZR,
+    MINIMAL_RECIPE_TEXT_GIT,
     )
 from lp.code.interfaces.sourcepackagerecipebuild import (
     ISourcePackageRecipeBuildSource,
@@ -2897,9 +2900,22 @@
             branches = (self.makeAnyBranch(), )
         base_branch = branches[0]
         other_branches = branches[1:]
-        text = MINIMAL_RECIPE_TEXT_BZR % base_branch.bzr_identity
+        if IBranch.providedBy(base_branch):
+            text = MINIMAL_RECIPE_TEXT_BZR % base_branch.identity
+        elif IGitRef.providedBy(base_branch):
+            text = MINIMAL_RECIPE_TEXT_GIT % (
+                base_branch.repository.identity, base_branch.name)
+        else:
+            raise AssertionError(
+                "Unsupported base_branch: %r" % (base_branch,))
         for i, branch in enumerate(other_branches):
-            text += 'merge dummy-%s %s\n' % (i, branch.bzr_identity)
+            if IBranch.providedBy(branch):
+                text += 'merge dummy-%s %s\n' % (i, branch.identity)
+            elif IGitRef.providedBy(branch):
+                text += 'merge dummy-%s %s %s\n' % (
+                    i, branch.repository.identity, branch.name)
+            else:
+                raise AssertionError("Unsupported branch: %r" % (branch,))
         return text
 
     def makeRecipe(self, *branches):


Follow ups