← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/bug-bmp-activity into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/bug-bmp-activity into lp:launchpad with lp:~cjwatson/launchpad/bmp-buglinktarget-git as a prerequisite.

Commit message:
Record merge proposal links in bug activity logs and karma.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1492926 in Launchpad itself: "Git merge proposals can't be linked to bugs"
  https://bugs.launchpad.net/launchpad/+bug/1492926

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/bug-bmp-activity/+merge/298367

Record merge proposal links in bug activity logs and karma.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/bug-bmp-activity into lp:launchpad.
=== modified file 'lib/lp/bugs/adapters/bugchange.py'
--- lib/lp/bugs/adapters/bugchange.py	2015-07-08 16:05:11 +0000
+++ lib/lp/bugs/adapters/bugchange.py	2016-06-25 09:48:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 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).
 
 """Implementations for bug changes."""
@@ -9,16 +9,10 @@
     'ATTACHMENT_REMOVED',
     'BRANCH_LINKED',
     'BRANCH_UNLINKED',
+    'BranchLinkedToBug',
+    'BranchUnlinkedFromBug',
     'BUG_WATCH_ADDED',
     'BUG_WATCH_REMOVED',
-    'CHANGED_DUPLICATE_MARKER',
-    'CVE_LINKED',
-    'CVE_UNLINKED',
-    'MARKED_AS_DUPLICATE',
-    'REMOVED_DUPLICATE_MARKER',
-    'REMOVED_SUBSCRIBER',
-    'BranchLinkedToBug',
-    'BranchUnlinkedFromBug',
     'BugAttachmentChange',
     'BugConvertedToQuestion',
     'BugDescriptionChange',
@@ -36,12 +30,22 @@
     'BugTitleChange',
     'BugWatchAdded',
     'BugWatchRemoved',
+    'CHANGED_DUPLICATE_MARKER',
+    'CVE_LINKED',
+    'CVE_UNLINKED',
     'CveLinkedToBug',
     'CveUnlinkedFromBug',
+    'get_bug_change_class',
+    'get_bug_changes',
+    'MARKED_AS_DUPLICATE',
+    'MERGE_PROPOSAL_LINKED',
+    'MERGE_PROPOSAL_UNLINKED',
+    'MergeProposalLinkedToBug',
+    'MergeProposalUnlinkedFromBug',
+    'REMOVED_DUPLICATE_MARKER',
+    'REMOVED_SUBSCRIBER',
     'SeriesNominated',
     'UnsubscribedFromBug',
-    'get_bug_change_class',
-    'get_bug_changes',
     ]
 
 from textwrap import dedent
@@ -73,6 +77,8 @@
 CVE_LINKED = 'cve linked'
 CVE_UNLINKED = 'cve unlinked'
 MARKED_AS_DUPLICATE = 'marked as duplicate'
+MERGE_PROPOSAL_LINKED = 'merge proposal linked'
+MERGE_PROPOSAL_UNLINKED = 'merge proposal unlinked'
 REMOVED_DUPLICATE_MARKER = 'removed duplicate marker'
 REMOVED_SUBSCRIBER = 'removed subscriber'
 
@@ -395,6 +401,56 @@
         return {'text': '** Branch unlinked: %s' % self.branch.bzr_identity}
 
 
+class MergeProposalLinkedToBug(BugChangeBase):
+    """A merge proposal got linked to the bug."""
+
+    def __init__(self, when, person, merge_proposal, bug):
+        super(MergeProposalLinkedToBug, self).__init__(when, person)
+        self.merge_proposal = merge_proposal
+        self.bug = bug
+
+    def getBugActivity(self):
+        """See `IBugChange`."""
+        if self.merge_proposal.private:
+            return None
+        return dict(
+            whatchanged=MERGE_PROPOSAL_LINKED,
+            newvalue=canonical_url(self.merge_proposal))
+
+    def getBugNotification(self):
+        """See `IBugChange`."""
+        if self.merge_proposal.private or self.bug.is_complete:
+            return None
+        return {
+            'text': '** Merge proposal linked: %s' % (
+                canonical_url(self.merge_proposal))}
+
+
+class MergeProposalUnlinkedFromBug(BugChangeBase):
+    """A merge proposal got unlinked from the bug."""
+
+    def __init__(self, when, person, merge_proposal, bug):
+        super(MergeProposalUnlinkedFromBug, self).__init__(when, person)
+        self.merge_proposal = merge_proposal
+        self.bug = bug
+
+    def getBugActivity(self):
+        """See `IBugChange`."""
+        if self.merge_proposal.private:
+            return None
+        return dict(
+            whatchanged=MERGE_PROPOSAL_UNLINKED,
+            oldvalue=canonical_url(self.merge_proposal))
+
+    def getBugNotification(self):
+        """See `IBugChange`."""
+        if self.merge_proposal.private or self.bug.is_complete:
+            return None
+        return {
+            'text': '** Merge proposal unlinked: %s' % (
+                canonical_url(self.merge_proposal))}
+
+
 class BugDescriptionChange(AttributeChange):
     """Describes a change to a bug's description."""
 

=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml	2016-06-25 09:48:33 +0000
+++ lib/lp/bugs/configure.zcml	2016-06-25 09:48:33 +0000
@@ -86,6 +86,9 @@
         for="lp.bugs.interfaces.bug.IBug lp.bugs.interfaces.buglink.IObjectLinkedEvent"
         handler="lp.bugs.subscribers.karma.branch_linked"/>
     <subscriber
+        for="lp.bugs.interfaces.bug.IBug lp.bugs.interfaces.buglink.IObjectLinkedEvent"
+        handler="lp.bugs.subscribers.karma.merge_proposal_linked"/>
+    <subscriber
         for="lp.bugs.interfaces.bugmessage.IBugMessage                 lazr.lifecycle.interfaces.IObjectCreatedEvent"
         handler="lp.bugs.subscribers.bug.notify_bug_comment_added"/>
     <subscriber
@@ -819,6 +822,12 @@
         for="lp.bugs.interfaces.bug.IBug lp.bugs.interfaces.buglink.IObjectUnlinkedEvent"
         handler="lp.bugs.subscribers.bugactivity.record_cve_unlinked_from_bug"/>
     <subscriber
+        for="lp.bugs.interfaces.bug.IBug lp.bugs.interfaces.buglink.IObjectLinkedEvent"
+        handler="lp.bugs.subscribers.bugactivity.record_merge_proposal_linked_to_bug"/>
+    <subscriber
+        for="lp.bugs.interfaces.bug.IBug lp.bugs.interfaces.buglink.IObjectUnlinkedEvent"
+        handler="lp.bugs.subscribers.bugactivity.record_merge_proposal_unlinked_from_bug"/>
+    <subscriber
         for="lp.bugs.interfaces.bugsubscription.IBugSubscription                     zope.lifecycleevent.interfaces.IObjectCreatedEvent"
         handler="lp.bugs.subscribers.bugactivity.record_bugsubscription_added"/>
     <subscriber

=== modified file 'lib/lp/bugs/doc/malone-karma.txt'
--- lib/lp/bugs/doc/malone-karma.txt	2015-10-08 09:46:08 +0000
+++ lib/lp/bugs/doc/malone-karma.txt	2016-06-25 09:48:33 +0000
@@ -59,6 +59,14 @@
     Karma added: action=bugcverefadded, distribution=debian
     True
 
+Link a merge proposal to a bug:
+
+    >>> dsp = factory.makeDistributionSourcePackage(distribution=debian)
+    >>> merge_proposal = factory.makeBranchMergeProposalForGit(target=dsp)
+    Karma added: action=branchmergeproposed, distribution=debian
+    >>> bug.linkMergeProposal(merge_proposal, getUtility(ILaunchBag).user)
+    Karma added: action=bugbranchcreated, distribution=debian
+
 Add watch for external bug to the bug:
 
     >>> from lp.bugs.interfaces.bugtracker import IBugTrackerSet
@@ -245,7 +253,7 @@
     >>> link_change = getUtility(
     ...     IKarmaActionSet).getByName('bugextrefadded')
     >>> karma_helper.added_karma_actions.add(link_change)
-    >>> karma_helper.added_karma_actions == set(bugs_karma_actions)
+    >>> karma_helper.added_karma_actions.issuperset(bugs_karma_actions)
     True
 
 Unregister the event listener to make sure we won't interfere in other tests.

=== modified file 'lib/lp/bugs/model/bugactivity.py'
--- lib/lp/bugs/model/bugactivity.py	2016-02-04 04:52:25 +0000
+++ lib/lp/bugs/model/bugactivity.py	2016-06-25 09:48:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 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).
 
 __metaclass__ = type
@@ -24,6 +24,8 @@
     CVE_LINKED,
     CVE_UNLINKED,
     MARKED_AS_DUPLICATE,
+    MERGE_PROPOSAL_LINKED,
+    MERGE_PROPOSAL_UNLINKED,
     REMOVED_DUPLICATE_MARKER,
     REMOVED_SUBSCRIBER,
     )
@@ -101,6 +103,8 @@
                 result = 'watches'
             elif result in (CVE_LINKED, CVE_UNLINKED):
                 result = 'cves'
+            elif result in (MERGE_PROPOSAL_LINKED, MERGE_PROPOSAL_UNLINKED):
+                result = 'linked_merge_proposals'
             elif str(result).startswith(REMOVED_SUBSCRIBER):
                 result = 'removed_subscriber'
             elif result == 'summary':

=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
--- lib/lp/bugs/scripts/bugnotification.py	2016-02-04 04:52:25 +0000
+++ lib/lp/bugs/scripts/bugnotification.py	2016-06-25 09:48:33 +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).
 
 """Functions related to sending bug notifications."""
@@ -65,7 +65,8 @@
         key = activity.attribute
         if activity.target is not None:
             key = ':'.join((activity.target, key))
-        if key in ('attachments', 'watches', 'cves', 'linked_branches'):
+        if key in ('attachments', 'watches', 'cves', 'linked_branches',
+                   'linked_merge_proposals'):
             # We are intentionally leaving bug task bugwatches out of this
             # list, so we use the key rather than the activity.attribute.
             if activity.oldvalue is not None:

=== modified file 'lib/lp/bugs/subscribers/bugactivity.py'
--- lib/lp/bugs/subscribers/bugactivity.py	2015-09-25 08:50:34 +0000
+++ lib/lp/bugs/subscribers/bugactivity.py	2016-06-25 09:48:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 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).
 
 __metaclass__ = type
@@ -17,11 +17,14 @@
     BugWatchRemoved,
     CveLinkedToBug,
     CveUnlinkedFromBug,
+    MergeProposalLinkedToBug,
+    MergeProposalUnlinkedFromBug,
     )
 from lp.bugs.enums import BugNotificationLevel
 from lp.bugs.interfaces.bug import IBug
 from lp.bugs.interfaces.bugactivity import IBugActivitySet
 from lp.bugs.interfaces.cve import ICve
+from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
 from lp.registry.enums import PersonVisibility
 from lp.registry.interfaces.milestone import IMilestone
 from lp.registry.interfaces.person import IPerson
@@ -128,6 +131,28 @@
 
 
 @block_implicit_flushes
+def record_merge_proposal_linked_to_bug(bug, event):
+    """Record when a merge proposal is linked to a bug."""
+    if not IBranchMergeProposal.providedBy(event.other_object):
+        return
+    bug.addChange(
+        MergeProposalLinkedToBug(
+            when=None, person=IPerson(event.user),
+            merge_proposal=event.other_object, bug=event.object))
+
+
+@block_implicit_flushes
+def record_merge_proposal_unlinked_from_bug(bug, event):
+    """Record when a merge proposal is unlinked from a bug."""
+    if not IBranchMergeProposal.providedBy(event.other_object):
+        return
+    bug.addChange(
+        MergeProposalUnlinkedFromBug(
+            when=None, person=IPerson(event.user),
+            merge_proposal=event.other_object, bug=event.object))
+
+
+@block_implicit_flushes
 def record_bugsubscription_added(bugsubscription_added, object_created_event):
     subscribed_user = bugsubscription_added.person
     if subscribed_user.visibility == PersonVisibility.PUBLIC:

=== modified file 'lib/lp/bugs/subscribers/karma.py'
--- lib/lp/bugs/subscribers/karma.py	2016-02-04 12:37:59 +0000
+++ lib/lp/bugs/subscribers/karma.py	2016-06-25 09:48:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 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).
 
 """Assign karma for bugs domain activity."""
@@ -128,3 +128,13 @@
         return
     event.other_object.target.assignKarma(
         IPerson(event.user), 'bugbranchcreated')
+
+
+@block_implicit_flushes
+def merge_proposal_linked(bug, event):
+    """Assign karma to the user who linked the bug to the merge proposal."""
+    from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
+    if not IBranchMergeProposal.providedBy(event.other_object):
+        return
+    event.other_object.target.assignKarma(
+        IPerson(event.user), 'bugbranchcreated')

=== modified file 'lib/lp/bugs/tests/test_bugchanges.py'
--- lib/lp/bugs/tests/test_bugchanges.py	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/tests/test_bugchanges.py	2016-06-25 09:48:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 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).
 
 """Tests for recording changes done to a bug."""
@@ -555,6 +555,115 @@
         self.bug.unlinkBranch(branch, self.user)
         self.assertRecordedChange()
 
+    def test_link_merge_proposal(self):
+        # Linking a merge proposal to a bug adds both to the activity log
+        # and sends an email notification.
+        bmp = self.factory.makeBranchMergeProposalForGit()
+        self.bug.linkMergeProposal(bmp, self.user)
+
+        # This checks the activity's attribute and target attributes.
+        activity = self.bug.activity[-1]
+        self.assertEqual(activity.attribute, 'linked_merge_proposals')
+        self.assertEqual(activity.target, None)
+
+        added_activity = {
+            'person': self.user,
+            'whatchanged': 'merge proposal linked',
+            'newvalue': canonical_url(bmp),
+            }
+        added_notification = {
+            'text': "** Merge proposal linked: %s" % canonical_url(bmp),
+            'person': self.user,
+            }
+        self.assertRecordedChange(
+            expected_activity=added_activity,
+            expected_notification=added_notification)
+
+    def test_link_merge_proposal_to_complete_bug(self):
+        # Linking a merge proposal to a bug that is "complete" (see
+        # IBug.is_complete) adds to the activity log but does *not* send an
+        # email notification.
+        for bug_task in self.bug.bugtasks:
+            bug_task.transitionToStatus(
+                BugTaskStatus.FIXRELEASED, user=self.user)
+        self.assertTrue(self.bug.is_complete)
+        self.saveOldChanges()
+        bmp = self.factory.makeBranchMergeProposalForGit()
+        self.bug.linkMergeProposal(bmp, self.user)
+        expected_activity = {
+            'person': self.user,
+            'whatchanged': 'merge proposal linked',
+            'newvalue': canonical_url(bmp),
+            }
+        self.assertRecordedChange(
+            expected_activity=expected_activity)
+
+    def test_link_private_merge_proposal(self):
+        # Linking a *private* merge proposal to a bug adds *nothing* to the
+        # activity log and does *not* send an email notification.
+        [git_ref] = self.factory.makeGitRefs(
+            information_type=InformationType.USERDATA)
+        bmp = self.factory.makeBranchMergeProposalForGit(source_ref=git_ref)
+        self.bug.linkMergeProposal(bmp, self.user)
+        self.assertRecordedChange()
+
+    def test_unlink_merge_proposal(self):
+        # Unlinking a merge proposal from a bug adds both to the activity
+        # log and sends an email notification.
+        bmp = self.factory.makeBranchMergeProposalForGit()
+        self.bug.linkMergeProposal(bmp, self.user)
+        self.saveOldChanges()
+        self.bug.unlinkMergeProposal(bmp, self.user)
+
+        # This checks the activity's attribute and target attributes.
+        activity = self.bug.activity[-1]
+        self.assertEqual(activity.attribute, 'linked_merge_proposals')
+        self.assertEqual(activity.target, None)
+
+        added_activity = {
+            'person': self.user,
+            'whatchanged': 'merge proposal unlinked',
+            'oldvalue': canonical_url(bmp),
+            }
+        added_notification = {
+            'text': "** Merge proposal unlinked: %s" % canonical_url(bmp),
+            'person': self.user,
+            }
+        self.assertRecordedChange(
+            expected_activity=added_activity,
+            expected_notification=added_notification)
+
+    def test_unlink_merge_proposal_from_complete_bug(self):
+        # Unlinking a merge proposal from a bug that is "complete" (see
+        # IBug.is_complete) adds to the activity log but does *not* send an
+        # email notification.
+        for bug_task in self.bug.bugtasks:
+            bug_task.transitionToStatus(
+                BugTaskStatus.FIXRELEASED, user=self.user)
+        self.assertTrue(self.bug.is_complete)
+        bmp = self.factory.makeBranchMergeProposalForGit()
+        self.bug.linkMergeProposal(bmp, self.user)
+        self.saveOldChanges()
+        self.bug.unlinkMergeProposal(bmp, self.user)
+        expected_activity = {
+            'person': self.user,
+            'whatchanged': 'merge proposal unlinked',
+            'oldvalue': canonical_url(bmp),
+            }
+        self.assertRecordedChange(
+            expected_activity=expected_activity)
+
+    def test_unlink_private_merge_proposal(self):
+        # Unlinking a *private* merge proposal from a bug adds *nothing* to
+        # the activity log and does *not* send an email notification.
+        [git_ref] = self.factory.makeGitRefs(
+            information_type=InformationType.USERDATA)
+        bmp = self.factory.makeBranchMergeProposalForGit(source_ref=git_ref)
+        self.bug.linkMergeProposal(bmp, self.user)
+        self.saveOldChanges()
+        self.bug.unlinkMergeProposal(bmp, self.user)
+        self.assertRecordedChange()
+
     def test_change_information_type(self):
         # Changing the information type of a bug adds items to the activity
         # log and notifications.


Follow ups