← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/git-repository-type into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/git-repository-type into lp:launchpad with lp:~cjwatson/launchpad/codeimport-mail-unique-name as a prerequisite.

Commit message:
Model GitRepository.repository_type; add garbo job to backfill it with HOSTED.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-repository-type/+merge/307464

Model GitRepository.repository_type; add garbo job to backfill it with HOSTED.

Requires https://code.launchpad.net/~cjwatson/launchpad/db-git-imports/+merge/307439.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-repository-type into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2016-10-02 00:33:51 +0000
+++ database/schema/security.cfg	2016-10-03 16:23:58 +0000
@@ -2370,6 +2370,7 @@
 public.emailaddress                     = SELECT, UPDATE, DELETE
 public.garbojobstate                    = SELECT, INSERT, UPDATE, DELETE
 public.gitjob                           = SELECT, DELETE
+public.gitrepository                    = SELECT, UPDATE
 public.hwsubmission                     = SELECT, UPDATE
 public.job                              = SELECT, INSERT, DELETE
 public.latestpersonsourcepackagereleasecache = SELECT, INSERT, UPDATE

=== modified file 'lib/lp/code/enums.py'
--- lib/lp/code/enums.py	2015-09-28 17:38:45 +0000
+++ lib/lp/code/enums.py	2016-10-03 16:23:58 +0000
@@ -21,6 +21,7 @@
     'CodeReviewNotificationLevel',
     'CodeReviewVote',
     'GitObjectType',
+    'GitRepositoryType',
     'NON_CVS_RCS_TYPES',
     'RevisionControlSystems',
     ]
@@ -78,7 +79,7 @@
 class BranchType(DBEnumeratedType):
     """Branch Type
 
-    The type of a branch determins the branch interaction with a number
+    The type of a branch determines the branch's interaction with a number
     of other subsystems.
     """
 
@@ -110,6 +111,30 @@
         """)
 
 
+class GitRepositoryType(DBEnumeratedType):
+    """Git Repository Type
+
+    The type of a repository determines its interaction with other
+    subsystems.
+    """
+
+    HOSTED = DBItem(1, """
+        Hosted
+
+        Launchpad is the primary location of this repository.
+        """)
+
+    # Skipping MIRRORED (2) to stay in sync with BranchType, in order to
+    # reduce confusion for manual database queries.
+
+    IMPORTED = DBItem(3, """
+        Imported
+
+        Repositories that have been imported from an externally hosted
+        repository and are made available through Launchpad.
+        """)
+
+
 class GitObjectType(DBEnumeratedType):
     """Git Object Type
 

=== modified file 'lib/lp/code/interfaces/gitnamespace.py'
--- lib/lp/code/interfaces/gitnamespace.py	2016-01-12 13:43:34 +0000
+++ lib/lp/code/interfaces/gitnamespace.py	2016-10-03 16:23:58 +0000
@@ -33,8 +33,8 @@
 
     target = Attribute("The `IHasGitRepositories` for this namespace.")
 
-    def createRepository(registrant, name, information_type=None,
-                         date_created=None):
+    def createRepository(repository_type, registrant, name,
+                         information_type=None, date_created=None):
         """Create and return an `IGitRepository` in this namespace."""
 
     def isNameUsed(name):

=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py	2016-09-06 15:34:38 +0000
+++ lib/lp/code/interfaces/gitrepository.py	2016-10-03 16:23:58 +0000
@@ -61,6 +61,7 @@
     BranchSubscriptionDiffSize,
     BranchSubscriptionNotificationLevel,
     CodeReviewNotificationLevel,
+    GitRepositoryType,
     )
 from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
 from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
@@ -127,6 +128,10 @@
     date_created = exported(Datetime(
         title=_("Date created"), required=True, readonly=True))
 
+    repository_type = exported(Choice(
+        title=_("Repository type"), required=True, readonly=True,
+        vocabulary=GitRepositoryType))
+
     registrant = exported(PublicPersonChoice(
         title=_("Registrant"), required=True, readonly=True,
         vocabulary="ValidPersonOrTeam",

=== modified file 'lib/lp/code/model/gitnamespace.py'
--- lib/lp/code/model/gitnamespace.py	2016-01-12 13:43:34 +0000
+++ lib/lp/code/model/gitnamespace.py	2016-10-03 16:23:58 +0000
@@ -70,9 +70,9 @@
 class _BaseGitNamespace:
     """Common code for Git repository namespaces."""
 
-    def createRepository(self, registrant, name, reviewer=None,
-                         information_type=None, date_created=DEFAULT,
-                         description=None):
+    def createRepository(self, repository_type, registrant, name,
+                         reviewer=None, information_type=None,
+                         date_created=DEFAULT, description=None):
         """See `IGitNamespace`."""
 
         self.validateRegistrant(registrant)
@@ -84,8 +84,9 @@
                 raise GitRepositoryCreationForbidden()
 
         repository = GitRepository(
-            registrant, self.owner, self.target, name, information_type,
-            date_created, reviewer=reviewer, description=description)
+            repository_type, registrant, self.owner, self.target, name,
+            information_type, date_created, reviewer=reviewer,
+            description=description)
         repository._reconcileAccess()
 
         # The owner of the repository should also be automatically subscribed

=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py	2016-09-06 15:34:38 +0000
+++ lib/lp/code/model/gitrepository.py	2016-10-03 16:23:58 +0000
@@ -70,7 +70,10 @@
     IPrivacy,
     )
 from lp.app.interfaces.services import IService
-from lp.code.enums import GitObjectType
+from lp.code.enums import (
+    GitObjectType,
+    GitRepositoryType,
+    )
 from lp.code.errors import (
     CannotDeleteGitRepository,
     GitDefaultConflict,
@@ -238,6 +241,22 @@
     date_last_modified = DateTime(
         name='date_last_modified', tzinfo=pytz.UTC, allow_none=False)
 
+    _repository_type = EnumCol(
+        dbName='repository_type', enum=GitRepositoryType, notNull=False)
+
+    @property
+    def repository_type(self):
+        # XXX cjwatson 2016-10-03: Remove once this column has been
+        # backfilled.
+        if self._repository_type is None:
+            return GitRepositoryType.HOSTED
+        else:
+            return self._repository_type
+
+    @repository_type.setter
+    def repository_type(self, value):
+        self._repository_type = value
+
     registrant_id = Int(name='registrant', allow_none=False)
     registrant = Reference(registrant_id, 'Person.id')
 
@@ -266,9 +285,11 @@
 
     _default_branch = Unicode(name='default_branch', allow_none=True)
 
-    def __init__(self, registrant, owner, target, name, information_type,
-                 date_created, reviewer=None, description=None):
+    def __init__(self, repository_type, registrant, owner, target, name,
+                 information_type, date_created, reviewer=None,
+                 description=None):
         super(GitRepository, self).__init__()
+        self.repository_type = repository_type
         self.registrant = registrant
         self.owner = owner
         self.reviewer = reviewer

=== modified file 'lib/lp/code/model/tests/test_gitnamespace.py'
--- lib/lp/code/model/tests/test_gitnamespace.py	2015-10-09 15:49:14 +0000
+++ lib/lp/code/model/tests/test_gitnamespace.py	2016-10-03 16:23:58 +0000
@@ -15,6 +15,7 @@
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.interfaces.services import IService
 from lp.app.validators import LaunchpadValidationError
+from lp.code.enums import GitRepositoryType
 from lp.code.errors import (
     GitDefaultConflict,
     GitRepositoryCreatorNotMemberOfOwnerTeam,
@@ -63,7 +64,8 @@
         namespace = self.getNamespace()
         repository_name = self.factory.getUniqueUnicode()
         registrant = removeSecurityProxy(namespace).owner
-        repository = namespace.createRepository(registrant, repository_name)
+        repository = namespace.createRepository(
+            GitRepositoryType.HOSTED, registrant, repository_name)
         self.assertEqual(
             "%s/+git/%s" % (namespace.name, repository_name),
             repository.unique_name)
@@ -78,8 +80,9 @@
         reviewer = self.factory.makePerson()
         description = self.factory.getUniqueUnicode()
         repository = namespace.createRepository(
-            registrant, repository_name, reviewer=reviewer,
-            description=description)
+            GitRepositoryType.HOSTED, registrant, repository_name,
+            reviewer=reviewer, description=description)
+        self.assertEqual(GitRepositoryType.HOSTED, repository.repository_type)
         self.assertEqual(repository_name, repository.name)
         self.assertEqual(registrant, repository.registrant)
         self.assertEqual(reviewer, repository.reviewer)
@@ -90,7 +93,8 @@
         namespace = self.getNamespace(owner)
         repository_name = self.factory.getUniqueUnicode()
         registrant = owner.teamowner
-        repository = namespace.createRepository(registrant, repository_name)
+        repository = namespace.createRepository(
+            GitRepositoryType.HOSTED, registrant, repository_name)
         self.assertEqual([owner], list(repository.subscribers))
 
     def test_getRepositories_no_repositories(self):
@@ -106,7 +110,8 @@
         namespace = self.getNamespace()
         repository_name = self.factory.getUniqueUnicode()
         repository = namespace.createRepository(
-            removeSecurityProxy(namespace).owner, repository_name)
+            GitRepositoryType.HOSTED, removeSecurityProxy(namespace).owner,
+            repository_name)
         self.assertEqual([repository], list(namespace.getRepositories()))
 
     def test_getByName_default(self):
@@ -127,7 +132,8 @@
         namespace = self.getNamespace()
         repository_name = self.factory.getUniqueUnicode()
         repository = namespace.createRepository(
-            removeSecurityProxy(namespace).owner, repository_name)
+            GitRepositoryType.HOSTED, removeSecurityProxy(namespace).owner,
+            repository_name)
         match = namespace.getByName(repository_name)
         self.assertEqual(repository, match)
 
@@ -140,7 +146,8 @@
         namespace = self.getNamespace()
         repository_name = self.factory.getUniqueUnicode()
         namespace.createRepository(
-            removeSecurityProxy(namespace).owner, repository_name)
+            GitRepositoryType.HOSTED, removeSecurityProxy(namespace).owner,
+            repository_name)
         self.assertTrue(namespace.isNameUsed(repository_name))
 
     def test_findUnusedName_unused(self):
@@ -155,7 +162,9 @@
         # it's already used.
         namespace = self.getNamespace()
         name = self.factory.getUniqueUnicode()
-        namespace.createRepository(removeSecurityProxy(namespace).owner, name)
+        namespace.createRepository(
+            GitRepositoryType.HOSTED, removeSecurityProxy(namespace).owner,
+            name)
         unused_name = namespace.findUnusedName(name)
         self.assertEqual("%s-1" % name, unused_name)
 
@@ -164,9 +173,12 @@
         # it's already used.
         namespace = self.getNamespace()
         name = self.factory.getUniqueUnicode()
-        namespace.createRepository(removeSecurityProxy(namespace).owner, name)
-        namespace.createRepository(
-            removeSecurityProxy(namespace).owner, name + "-1")
+        namespace.createRepository(
+            GitRepositoryType.HOSTED, removeSecurityProxy(namespace).owner,
+            name)
+        namespace.createRepository(
+            GitRepositoryType.HOSTED, removeSecurityProxy(namespace).owner,
+            name + "-1")
         unused_name = namespace.findUnusedName(name)
         self.assertEqual("%s-2" % name, unused_name)
 
@@ -187,7 +199,9 @@
         namespace = self.getNamespace()
         namespace_owner = removeSecurityProxy(namespace).owner
         name = self.factory.getUniqueUnicode()
-        namespace.createRepository(removeSecurityProxy(namespace).owner, name)
+        namespace.createRepository(
+            GitRepositoryType.HOSTED, removeSecurityProxy(namespace).owner,
+            name)
         repository = self.factory.makeGitRepository(name=name)
         self.assertRaises(
             GitRepositoryExists,
@@ -224,7 +238,9 @@
         namespace = self.getNamespace()
         namespace_owner = removeSecurityProxy(namespace).owner
         name = self.factory.getUniqueUnicode()
-        namespace.createRepository(removeSecurityProxy(namespace).owner, name)
+        namespace.createRepository(
+            GitRepositoryType.HOSTED, removeSecurityProxy(namespace).owner,
+            name)
         repository = self.factory.makeGitRepository()
         self.assertRaises(
             GitRepositoryExists,
@@ -454,7 +470,7 @@
                 namespace.target, team, namespace.target.owner,
                 {InformationType.PROPRIETARY: SharingPermission.ALL})
         repository = namespace.createRepository(
-            person, self.factory.getUniqueUnicode())
+            GitRepositoryType.HOSTED, person, self.factory.getUniqueUnicode())
         [policy] = getUtility(IAccessPolicySource).find(
             [(namespace.target, InformationType.PROPRIETARY)])
         apgfs = getUtility(IAccessPolicyGrantFlatSource)
@@ -598,7 +614,8 @@
         # GitRepositoryExists is raised.
         namespace = self._getNamespace(self.factory.makePerson())
         repository = namespace.createRepository(
-            namespace.owner, self.factory.getUniqueUnicode())
+            GitRepositoryType.HOSTED, namespace.owner,
+            self.factory.getUniqueUnicode())
         self.assertRaises(
             GitRepositoryExists,
             namespace.validateRepositoryName, repository.name)

=== modified file 'lib/lp/code/xmlrpc/git.py'
--- lib/lp/code/xmlrpc/git.py	2015-07-10 04:51:21 +0000
+++ lib/lp/code/xmlrpc/git.py	2016-10-03 16:23:58 +0000
@@ -20,6 +20,7 @@
 
 from lp.app.errors import NameLookupFailed
 from lp.app.validators import LaunchpadValidationError
+from lp.code.enums import GitRepositoryType
 from lp.code.errors import (
     GitRepositoryCreationException,
     GitRepositoryCreationFault,
@@ -171,7 +172,7 @@
 
         try:
             repository = namespace.createRepository(
-                requester, repository_name)
+                GitRepositoryType.HOSTED, requester, repository_name)
         except LaunchpadValidationError as e:
             # Despite the fault name, this just passes through the exception
             # text so there's no need for a new Git-specific fault.

=== modified file 'lib/lp/scripts/garbo.py'
--- lib/lp/scripts/garbo.py	2016-07-26 22:43:23 +0000
+++ lib/lp/scripts/garbo.py	2016-10-03 16:23:58 +0000
@@ -41,6 +41,7 @@
     Or,
     Row,
     SQL,
+    Update,
     )
 from storm.info import ClassAlias
 from storm.store import EmptyResultSet
@@ -58,6 +59,7 @@
     BugWatchScheduler,
     MAX_SAMPLE_SIZE,
     )
+from lp.code.enums import GitRepositoryType
 from lp.code.interfaces.revision import IRevisionSet
 from lp.code.model.codeimportevent import CodeImportEvent
 from lp.code.model.codeimportresult import CodeImportResult
@@ -65,6 +67,7 @@
     Diff,
     PreviewDiff,
     )
+from lp.code.model.gitrepository import GitRepository
 from lp.code.model.revision import (
     RevisionAuthor,
     RevisionCache,
@@ -1603,6 +1606,33 @@
         transaction.commit()
 
 
+class GitRepositoryTypePopulator(TunableLoop):
+    """Populates GitRepository.repository_type with HOSTED."""
+
+    maximum_chunk_size = 5000
+
+    def __init__(self, log, abort_time=None):
+        super(GitRepositoryTypePopulator, self).__init__(log, abort_time)
+        self.start_at = 1
+        self.store = IMasterStore(GitRepository)
+
+    def findRepositories(self):
+        return self.store.find(
+            GitRepository,
+            GitRepository.id >= self.start_at,
+            GitRepository._repository_type == None).order_by(GitRepository.id)
+
+    def isDone(self):
+        return self.findRepositories().is_empty()
+
+    def __call__(self, chunk_size):
+        ids = [repository.id for repository in self.findRepositories()]
+        self.store.execute(Update(
+            {GitRepository._repository_type: GitRepositoryType.HOSTED.value},
+            where=GitRepository.id.is_in(ids), table=GitRepository))
+        transaction.commit()
+
+
 class BaseDatabaseGarbageCollector(LaunchpadCronScript):
     """Abstract base class to run a collection of TunableLoops."""
     script_name = None  # Script name for locking and database user. Override.
@@ -1810,8 +1840,9 @@
                     tunable_loop.run()
                     loop_logger.debug(
                         "%s completed sucessfully.", loop_name)
-                except Exception:
+                except Exception as e:
                     loop_logger.exception("Unhandled exception")
+                    print(e)
                     self.failure_count += 1
 
             finally:
@@ -1883,6 +1914,7 @@
         CodeImportResultPruner,
         DiffPruner,
         GitJobPruner,
+        GitRepositoryTypePopulator,
         HWSubmissionEmailLinker,
         LiveFSFilePruner,
         LoginTokenPruner,

=== modified file 'lib/lp/scripts/tests/test_garbo.py'
--- lib/lp/scripts/tests/test_garbo.py	2016-07-26 22:43:23 +0000
+++ lib/lp/scripts/tests/test_garbo.py	2016-10-03 16:23:58 +0000
@@ -15,6 +15,7 @@
 from StringIO import StringIO
 import time
 
+from psycopg2 import IntegrityError
 from pytz import UTC
 from storm.exceptions import LostObjectError
 from storm.expr import (
@@ -48,7 +49,10 @@
     BranchFormat,
     RepositoryFormat,
     )
-from lp.code.enums import CodeImportResultStatus
+from lp.code.enums import (
+    CodeImportResultStatus,
+    GitRepositoryType,
+    )
 from lp.code.interfaces.codeimportevent import ICodeImportEventSet
 from lp.code.interfaces.gitrepository import IGitRepositorySet
 from lp.code.model.branchjob import (
@@ -1553,6 +1557,44 @@
         # Snaps with more than one possible store series are untouched.
         self.assertIsNone(snaps[5].store_series)
 
+    def test_GitRepositoryTypePopulator(self):
+        switch_dbuser('testadmin')
+        old_repositories = [self.factory.makeGitRepository() for _ in range(2)]
+        for repository in old_repositories:
+            removeSecurityProxy(repository)._repository_type = None
+        try:
+            Store.of(old_repositories[0]).flush()
+        except IntegrityError:
+            # Now enforced by DB NOT NULL constraint; backfilling is no
+            # longer necessary.
+            return
+        hosted_repositories = [
+            self.factory.makeGitRepository(
+                repository_type=GitRepositoryType.HOSTED)
+            for _ in range(2)]
+        imported_repositories = [
+            self.factory.makeGitRepository(
+                repository_type=GitRepositoryType.IMPORTED)
+            for _ in range(2)]
+        transaction.commit()
+
+        self.runDaily()
+
+        # Old repositories are backfilled.
+        for repository in old_repositories:
+            self.assertEqual(
+                GitRepositoryType.HOSTED,
+                removeSecurityProxy(repository)._repository_type)
+        # Other repositories are left alone.
+        for repository in hosted_repositories:
+            self.assertEqual(
+                GitRepositoryType.HOSTED,
+                removeSecurityProxy(repository)._repository_type)
+        for repository in imported_repositories:
+            self.assertEqual(
+                GitRepositoryType.IMPORTED,
+                removeSecurityProxy(repository)._repository_type)
+
 
 class TestGarboTasks(TestCaseWithFactory):
     layer = LaunchpadZopelessLayer

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2016-09-30 13:27:27 +0000
+++ lib/lp/testing/factory.py	2016-10-03 16:23:58 +0000
@@ -112,6 +112,7 @@
     CodeImportReviewStatus,
     CodeReviewNotificationLevel,
     GitObjectType,
+    GitRepositoryType,
     RevisionControlSystems,
     )
 from lp.code.errors import UnknownBranchTypeError
@@ -1754,14 +1755,17 @@
             revision_date=revision_date)
         return branch.createBranchRevision(sequence, revision)
 
-    def makeGitRepository(self, owner=None, reviewer=None, target=_DEFAULT,
-                          registrant=None, name=None, information_type=None,
+    def makeGitRepository(self, repository_type=None, owner=None,
+                          reviewer=None, target=_DEFAULT, registrant=None,
+                          name=None, information_type=None,
                           **optional_repository_args):
         """Create and return a new, arbitrary GitRepository.
 
         Any parameters for `IGitNamespace.createRepository` can be specified
         to override the default ones.
         """
+        if repository_type is None:
+            repository_type = GitRepositoryType.HOSTED
         if owner is None:
             owner = self.makePerson()
         if name is None:
@@ -1778,8 +1782,8 @@
 
         namespace = get_git_namespace(target, owner)
         repository = namespace.createRepository(
-            registrant=registrant, name=name, reviewer=reviewer,
-            **optional_repository_args)
+            repository_type=repository_type, registrant=registrant, name=name,
+            reviewer=reviewer, **optional_repository_args)
         naked_repository = removeSecurityProxy(repository)
         if information_type is not None:
             naked_repository.transitionToInformationType(


Follow ups