← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rockstar/launchpad/merge-queues-model into lp:launchpad

 

Paul Hummer has proposed merging lp:~rockstar/launchpad/merge-queues-model into lp:launchpad with lp:~rockstar/launchpad/merge-queues-db as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


This branch adds the model code for branch merge queues.  In my next branch, I'll need to break up the interfaces, because I'm exposing this in the API, but I thought that, instead of doing that in this pipe, it'd make more sense to do it when needed, as this branch seems to be a pretty good stopping point for the work it provides.
-- 
https://code.launchpad.net/~rockstar/launchpad/merge-queues-model/+merge/38762
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rockstar/launchpad/merge-queues-model into lp:launchpad.
=== modified file 'lib/lp/code/enums.py'
--- lib/lp/code/enums.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/enums.py	2010-10-18 19:46:51 +0000
@@ -7,7 +7,6 @@
 __all__ = [
     'BranchLifecycleStatus',
     'BranchLifecycleStatusFilter',
-    'BranchMergeControlStatus',
     'BranchMergeProposalStatus',
     'BranchSubscriptionDiffSize',
     'BranchSubscriptionNotificationLevel',
@@ -77,44 +76,6 @@
     ABANDONED = DBItem(80, "Abandoned")
 
 
-class BranchMergeControlStatus(DBEnumeratedType):
-    """Branch Merge Control Status
-
-    Does the branch want Launchpad to manage a merge queue, and if it does,
-    how does the branch owner handle removing items from the queue.
-    """
-
-    NO_QUEUE = DBItem(1, """
-        Does not use a merge queue
-
-        The branch does not use the merge queue managed by Launchpad.  Merges
-        are tracked and managed elsewhere.  Users will not be able to queue up
-        approved branch merge proposals.
-        """)
-
-    MANUAL = DBItem(2, """
-        Manual processing of the merge queue
-
-        One or more people are responsible for manually processing the queued
-        branch merge proposals.
-        """)
-
-    ROBOT = DBItem(3, """
-        A branch merge robot is used to process the merge queue
-
-        An external application, like PQM, is used to merge in the queued
-        approved proposed merges.
-        """)
-
-    ROBOT_RESTRICTED = DBItem(4, """
-        The branch merge robot used to process the queue is in restricted mode
-
-        When the robot is in restricted mode, normal queued branches are not
-        returned for merging, only those with "Queued for Restricted
-        merging" will be.
-        """)
-
-
 class BranchType(DBEnumeratedType):
     """Branch Type
 

=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py	2010-09-02 14:30:47 +0000
+++ lib/lp/code/errors.py	2010-10-18 19:46:51 +0000
@@ -26,6 +26,7 @@
     'CodeImportNotInReviewedState',
     'ClaimReviewFailed',
     'InvalidBranchMergeProposal',
+    'InvalidMergeQueueConfig',
     'InvalidNamespace',
     'NoLinkedBranch',
     'NoSuchBranch',
@@ -291,3 +292,12 @@
         RecipeBuildException.__init__(
             self, recipe, distroseries,
             'A build against this distro is not allowed.')
+
+class InvalidMergeQueueConfig(Exception):
+    """The config specified is not a valid JSON string."""
+
+    webservice_error(400)
+
+    def __init__(self):
+        message = ('The configuration specified is not a valid JSON string.')
+        Exception.__init__(self, message)

=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py	2010-10-18 19:46:48 +0000
+++ lib/lp/code/interfaces/branch.py	2010-10-18 19:46:51 +0000
@@ -75,13 +75,13 @@
     )
 from lp.code.enums import (
     BranchLifecycleStatus,
-    BranchMergeControlStatus,
     BranchSubscriptionDiffSize,
     BranchSubscriptionNotificationLevel,
     CodeReviewNotificationLevel,
     UICreatableBranchType,
     )
 from lp.code.interfaces.branchlookup import IBranchLookup
+from lp.code.interfaces.branchmergequeue import IBranchMergeQueue
 from lp.code.interfaces.branchtarget import IHasBranchTarget
 from lp.code.interfaces.hasbranches import IHasMergeProposals
 from lp.code.interfaces.hasrecipes import IHasRecipes
@@ -591,9 +591,6 @@
     def getStackedBranches():
         """The branches that are stacked on this one."""
 
-    def getMergeQueue():
-        """The proposals that are QUEUED to land on this branch."""
-
     def getMainlineBranchRevisions(start_date, end_date=None,
                                    oldest_first=False):
         """Return the matching mainline branch revision objects.
@@ -993,6 +990,18 @@
             required=False, readonly=True,
             vocabulary=ControlFormat))
 
+    merge_queue = Reference(
+        title=_('Branch Merge Queue'),
+        schema=IBranchMergeQueue, required=False, readonly=True,
+        description=_(
+            "The branch merge queue that manages merges for this branch."))
+
+    merge_queue_config = TextLine(
+            title=_('Name'), required=True, constraint=branch_name_validator,
+            description=_(
+                "A JSON string of configuration values to send to a branch"
+                "merge robot."))
+
 
 class IBranchEdit(Interface):
     """IBranch attributes that require launchpad.Edit permission."""
@@ -1066,6 +1075,23 @@
         :raise: CannotDeleteBranch if the branch cannot be deleted.
         """
 
+    def addToQueue(queue):
+        """Add this branch to a specified queue.
+
+        A branch's merges can be managed by a queue.
+
+        :param queue: The branch merge queue that will manage the branch.
+        """
+
+    def setMergeQueueConfig(config):
+        """Set the merge_queue_config property.
+
+        A branch can store a JSON string of configuration data for a merge
+        robot to retrieve.
+
+        :param config: A JSON string of data.
+        """
+
 
 class IBranch(IBranchPublic, IBranchView, IBranchEdit,
               IBranchEditableAttributes, IBranchAnyone):

=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2010-10-18 19:46:48 +0000
+++ lib/lp/code/model/branch.py	2010-10-18 19:46:51 +0000
@@ -10,6 +10,7 @@
     ]
 
 from datetime import datetime
+import simplejson
 
 from bzrlib import urlutils
 from bzrlib.revision import NULL_REVISION
@@ -31,7 +32,11 @@
     Or,
     Select,
     )
-from storm.locals import AutoReload
+from storm.locals import (
+    AutoReload,
+    Int,
+    Reference,
+    )
 from storm.store import Store
 from zope.component import getUtility
 from zope.event import notify
@@ -70,7 +75,6 @@
     )
 from lp.code.enums import (
     BranchLifecycleStatus,
-    BranchMergeControlStatus,
     BranchMergeProposalStatus,
     BranchType,
     )
@@ -82,6 +86,7 @@
     BranchTypeError,
     CannotDeleteBranch,
     InvalidBranchMergeProposal,
+    InvalidMergeQueueConfig,
     )
 from lp.code.event.branchmergeproposal import NewBranchMergeProposalEvent
 from lp.code.interfaces.branch import (
@@ -475,14 +480,6 @@
         store = Store.of(self)
         return store.find(Branch, Branch.stacked_on == self)
 
-    def getMergeQueue(self):
-        """See `IBranch`."""
-        return BranchMergeProposal.select("""
-            BranchMergeProposal.target_branch = %s AND
-            BranchMergeProposal.queue_status = %s
-            """ % sqlvalues(self, BranchMergeProposalStatus.QUEUED),
-            orderBy="queue_position")
-
     @property
     def code_is_browseable(self):
         """See `IBranch`."""
@@ -1141,6 +1138,23 @@
             SourcePackageRecipeData)
         return SourcePackageRecipeData.findRecipes(self)
 
+    merge_queue_id = Int(name='merge_queue', allow_none=True)
+    merge_queue = Reference(merge_queue_id, 'BranchMergeQueue.id')
+
+    merge_queue_config = StringCol(dbName='merge_queue_config')
+
+    def addToQueue(self, queue):
+        """See `IBranchEdit`."""
+        self.merge_queue = queue
+
+    def setMergeQueueConfig(self, config):
+        """See `IBranchEdit`."""
+        try:
+            simplejson.loads(config)
+            self.merge_queue_config = config
+        except ValueError: # The json string is invalid
+            raise InvalidMergeQueueConfig
+
 
 class DeletionOperation:
     """Represent an operation to perform as part of branch deletion."""

=== added file 'lib/lp/code/model/branchmergequeue.py'
--- lib/lp/code/model/branchmergequeue.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/branchmergequeue.py	2010-10-18 19:46:51 +0000
@@ -0,0 +1,79 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Implementation classes for IBranchMergeQueue, etc."""
+
+__metaclass__ = type
+__all__ = ['BranchMergeQueue']
+
+import simplejson
+
+from storm.locals import (
+    Int,
+    Reference,
+    Store,
+    Storm,
+    Unicode,
+    )
+from zope.interface import (
+    classProvides,
+    implements,
+    )
+
+from canonical.database.datetimecol import UtcDateTimeCol
+from lp.code.errors import InvalidMergeQueueConfig
+from lp.code.interfaces.branchmergequeue import (
+    IBranchMergeQueue,
+    IBranchMergeQueueSource,
+    )
+from lp.code.model.branch import Branch
+
+
+class BranchMergeQueue(Storm):
+    """See `IBranchMergeQueue`."""
+
+    __storm_table__ = 'BranchMergeQueue'
+    implements(IBranchMergeQueue)
+    classProvides(IBranchMergeQueueSource)
+
+    id = Int(primary=True)
+
+    registrant_id = Int(name='registrant', allow_none=True)
+    registrant = Reference(registrant_id, 'Person.id')
+
+    owner_id = Int(name='owner', allow_none=True)
+    owner = Reference(owner_id, 'Person.id')
+
+    name = Unicode(allow_none=False)
+    description = Unicode(allow_none=False)
+    configuration = Unicode(allow_none=False)
+
+    date_created = UtcDateTimeCol(notNull=True)
+
+    @property
+    def branches(self):
+        """See `IBranchMergeQueue`."""
+        return Store.of(self).find(
+            Branch,
+            Branch.merge_queue_id == self.id)
+
+    def setMergeQueueConfig(self, config):
+        """See `IBranchMergeQueue`."""
+        try:
+            simplejson.loads(config)
+            self.configuration = config
+        except ValueError: # The config string is not valid JSON
+            raise InvalidMergeQueueConfig
+
+    @staticmethod
+    def new(name, owner, registrant, description=None,
+            configuration=None):
+        """See `IBranchMergeQueueSource`."""
+        queue = BranchMergeQueue()
+        queue.name = name
+        queue.owner = owner
+        queue.registrant = registrant
+        queue.description = description
+        queue.configuration = configuration
+
+        return queue

=== modified file 'lib/lp/code/model/branchnamespace.py'
--- lib/lp/code/model/branchnamespace.py	2010-10-18 19:46:48 +0000
+++ lib/lp/code/model/branchnamespace.py	2010-10-18 19:46:51 +0000
@@ -27,7 +27,6 @@
     )
 from lp.code.enums import (
     BranchLifecycleStatus,
-    BranchMergeControlStatus,
     BranchSubscriptionDiffSize,
     BranchSubscriptionNotificationLevel,
     BranchVisibilityRule,

=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py	2010-10-04 19:50:45 +0000
+++ lib/lp/code/model/tests/test_branch.py	2010-10-18 19:46:51 +0000
@@ -14,6 +14,7 @@
     datetime,
     timedelta,
     )
+import simplejson
 from unittest import TestLoader
 
 from bzrlib.bzrdir import BzrDir
@@ -66,6 +67,7 @@
     BranchTargetError,
     CannotDeleteBranch,
     InvalidBranchMergeProposal,
+    InvalidMergeQueueConfig,
     )
 from lp.code.interfaces.branch import (
     DEFAULT_BRANCH_STATUS_IN_LISTING,
@@ -2704,5 +2706,43 @@
         self.assertRaises(UnsafeUrlSeen, db_stacked.getBzrBranch)
 
 
+class TestMergeQueue(TestCaseWithFactory):
+    """Tests for branch merge queue functionality in branches."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_addToQueue(self):
+        """Test Branch.addToQueue."""
+        branch = self.factory.makeBranch()
+        queue = self.factory.makeBranchMergeQueue()
+        with person_logged_in(branch.owner):
+            branch.addToQueue(queue)
+
+        self.assertEqual(branch.merge_queue, queue)
+
+    def test_setMergeQueueConfig(self):
+        """Test Branch.setMergeQueueConfig."""
+        branch = self.factory.makeBranch()
+        config = simplejson.dumps({
+            'path': '/',
+            'test': 'make test',})
+
+        with person_logged_in(branch.owner):
+            branch.setMergeQueueConfig(config)
+
+        self.assertEqual(branch.merge_queue_config, config)
+
+    def test_setMergeQueueConfig_invalid(self):
+        """Test that invalid JSON strings aren't added to the database."""
+        branch = self.factory.makeBranch()
+        config = 'abc'
+
+        with person_logged_in(branch.owner):
+            self.assertRaises(
+                InvalidMergeQueueConfig,
+                branch.setMergeQueueConfig,
+                config)
+
+
 def test_suite():
     return TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-10-18 19:46:48 +0000
+++ lib/lp/testing/factory.py	2010-10-18 19:46:51 +0000
@@ -36,6 +36,7 @@
 from operator import isSequenceType
 import os
 from random import randint
+import simplejson
 from StringIO import StringIO
 from textwrap import dedent
 from threading import local
@@ -146,6 +147,7 @@
     PreviewDiff,
     StaticDiff,
     )
+from lp.code.model.branchmergequeue import BranchMergeQueue
 from lp.codehosting.codeimport.worker import CodeImportSourceDetails
 from lp.hardwaredb.interfaces.hwdb import (
     HWSubmissionFormat,
@@ -1096,6 +1098,18 @@
         namespace = target.getNamespace(owner)
         return namespace.createBranch(branch_type, name, creator)
 
+    def makeBranchMergeQueue(self):
+        """Create a BranchMergeQueue."""
+        name = unicode(self.getUniqueString('queue'))
+        owner = self.makePerson()
+        description = unicode(self.getUniqueString('queue-description'))
+        configuration = unicode(simplejson.dumps({
+            self.getUniqueString('key'): self.getUniqueString('value')}))
+
+        queue = BranchMergeQueue.new(
+            name, owner, owner, description, configuration)
+        return queue
+
     def enableDefaultStackingForProduct(self, product, branch=None):
         """Give 'product' a default stacked-on branch.
 


Follow ups