Rev 3520: Add pre_change_branch_tip hook. (Andrew Bennetts) in file:///home/pqm/archives/thelove/bzr/%2Btrunk/

Canonical.com Patch Queue Manager pqm at pqm.ubuntu.com
Fri Jul 4 04:41:31 BST 2008


At file:///home/pqm/archives/thelove/bzr/%2Btrunk/

------------------------------------------------------------
revno: 3520
revision-id:pqm at pqm.ubuntu.com-20080704034121-6ysq01uf3p1h97aj
parent: pqm at pqm.ubuntu.com-20080703180607-kxmp7ftyw7sqg5gj
parent: andrew.bennetts at canonical.com-20080704031947-zkhvfgu22nb3ml53
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Fri 2008-07-04 04:41:21 +0100
message:
  Add pre_change_branch_tip hook. (Andrew Bennetts)
modified:
  NEWS                           NEWS-20050323055033-4e00b5db738777ff
  bzrlib/branch.py               branch.py-20050309040759-e4baf4e0d046576e
  bzrlib/help_topics/en/hooks.txt hooks.txt-20070830033044-xxu2rced13f72dka-1
  bzrlib/tests/branch_implementations/test_hooks.py test_hooks.py-20070129154855-blhpwxmvjs07waei-1
    ------------------------------------------------------------
    revno: 3517.2.6
    revision-id:andrew.bennetts at canonical.com-20080704031947-zkhvfgu22nb3ml53
    parent: andrew.bennetts at canonical.com-20080704013444-24c2sa27q85eqkms
    committer: Andrew Bennetts <andrew.bennetts at canonical.com>
    branch nick: pre-branch-tip-changed-hook
    timestamp: Fri 2008-07-04 13:19:47 +1000
    message:
      Add NEWS entry, and document in the 'hooks' help topic.
    modified:
      NEWS                           NEWS-20050323055033-4e00b5db738777ff
      bzrlib/help_topics/en/hooks.txt hooks.txt-20070830033044-xxu2rced13f72dka-1
    ------------------------------------------------------------
    revno: 3517.2.5
    revision-id:andrew.bennetts at canonical.com-20080704013444-24c2sa27q85eqkms
    parent: andrew.bennetts at canonical.com-20080703075706-7zup8zm007a2dyb9
    committer: Andrew Bennetts <andrew.bennetts at canonical.com>
    branch nick: pre-branch-tip-changed-hook
    timestamp: Fri 2008-07-04 11:34:44 +1000
    message:
      Reduce duplication in test_hooks a little.
    modified:
      bzrlib/tests/branch_implementations/test_hooks.py test_hooks.py-20070129154855-blhpwxmvjs07waei-1
    ------------------------------------------------------------
    revno: 3517.2.4
    revision-id:andrew.bennetts at canonical.com-20080703075706-7zup8zm007a2dyb9
    parent: andrew.bennetts at canonical.com-20080703075602-8n055qsfkjijcz6i
    committer: Andrew Bennetts <andrew.bennetts at canonical.com>
    branch nick: pre-branch-tip-changed-hook
    timestamp: Thu 2008-07-03 17:57:06 +1000
    message:
      Fix typo.
    modified:
      bzrlib/tests/branch_implementations/test_hooks.py test_hooks.py-20070129154855-blhpwxmvjs07waei-1
    ------------------------------------------------------------
    revno: 3517.2.3
    revision-id:andrew.bennetts at canonical.com-20080703075602-8n055qsfkjijcz6i
    parent: andrew.bennetts at canonical.com-20080703071735-33dgjy9niohq197k
    committer: Andrew Bennetts <andrew.bennetts at canonical.com>
    branch nick: pre-branch-tip-changed-hook
    timestamp: Thu 2008-07-03 17:56:02 +1000
    message:
      Better tests for {pre,post}_change_branch_tip hooks.
    modified:
      bzrlib/branch.py               branch.py-20050309040759-e4baf4e0d046576e
      bzrlib/tests/branch_implementations/test_hooks.py test_hooks.py-20070129154855-blhpwxmvjs07waei-1
    ------------------------------------------------------------
    revno: 3517.2.2
    revision-id:andrew.bennetts at canonical.com-20080703071735-33dgjy9niohq197k
    parent: andrew.bennetts at canonical.com-20080703054955-n94lvd3puo6aiwt3
    committer: Andrew Bennetts <andrew.bennetts at canonical.com>
    branch nick: pre-branch-tip-changed-hook
    timestamp: Thu 2008-07-03 17:17:35 +1000
    message:
      Add test for a pre_change_branch_tip hook rejecting a change.
    modified:
      bzrlib/tests/branch_implementations/test_hooks.py test_hooks.py-20070129154855-blhpwxmvjs07waei-1
    ------------------------------------------------------------
    revno: 3517.2.1
    revision-id:andrew.bennetts at canonical.com-20080703054955-n94lvd3puo6aiwt3
    parent: pqm at pqm.ubuntu.com-20080702195105-5gqthymygmtjrwaf
    committer: Andrew Bennetts <andrew.bennetts at canonical.com>
    branch nick: pre-branch-tip-changed-hook
    timestamp: Thu 2008-07-03 15:49:55 +1000
    message:
      Quick draft of pre_change_branch_tip hook.
    modified:
      bzrlib/branch.py               branch.py-20050309040759-e4baf4e0d046576e
      bzrlib/tests/branch_implementations/test_hooks.py test_hooks.py-20070129154855-blhpwxmvjs07waei-1
=== modified file 'NEWS'
--- a/NEWS	2008-07-03 10:44:34 +0000
+++ b/NEWS	2008-07-04 03:41:21 +0000
@@ -10,6 +10,10 @@
 
   FEATURES:
 
+    * New ``pre_change_branch_tip`` hook that is called before the
+      branch tip is moved, while the branch is write-locked.  See the User
+      Reference for signature details.  (Andrew Bennetts)
+
     * Rule-based preferences can now be defined for selected files in
       selected branches, allowing commands and plugins to provide
       custom behaviour for files matching defined patterns.

=== modified file 'bzrlib/branch.py'
--- a/bzrlib/branch.py	2008-06-25 22:27:57 +0000
+++ b/bzrlib/branch.py	2008-07-03 07:56:02 +0000
@@ -1109,6 +1109,12 @@
         # local is the local branch or None, master is the target branch,
         # and an empty branch recieves new_revno of 0, new_revid of None.
         self['post_uncommit'] = []
+        # Introduced in 1.6
+        # Invoked before the tip of a branch changes.
+        # the api signature is
+        # (params) where params is a ChangeBranchTipParams with the members
+        # (branch, old_revno, new_revno, old_revid, new_revid)
+        self['pre_change_branch_tip'] = []
         # Introduced in 1.4
         # Invoked after the tip of a branch changes.
         # the api signature is
@@ -1150,6 +1156,14 @@
         self.old_revid = old_revid
         self.new_revid = new_revid
 
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+    
+    def __repr__(self):
+        return "<%s of %s from (%s, %s) to (%s, %s)>" % (
+            self.__class__.__name__, self.branch, 
+            self.old_revno, self.old_revid, self.new_revno, self.new_revid)
+
 
 class BzrBranchFormat4(BranchFormat):
     """Bzr branch format 4.
@@ -1512,6 +1526,17 @@
         for hook in Branch.hooks['set_rh']:
             hook(self, rev_history)
 
+    def _run_pre_change_branch_tip_hooks(self, new_revno, new_revid):
+        """Run the pre_change_branch_tip hooks."""
+        hooks = Branch.hooks['pre_change_branch_tip']
+        if not hooks:
+            return
+        old_revno, old_revid = self.last_revision_info()
+        params = ChangeBranchTipParams(
+            self, old_revno, new_revno, old_revid, new_revid)
+        for hook in hooks:
+            hook(params)
+ 
     def _run_post_change_branch_tip_hooks(self, old_revno, old_revid):
         """Run the post_change_branch_tip hooks."""
         hooks = Branch.hooks['post_change_branch_tip']
@@ -1543,6 +1568,7 @@
         history = self._lefthand_history(revision_id)
         if len(history) != revno:
             raise AssertionError('%d != %d' % (len(history), revno))
+        self._run_pre_change_branch_tip_hooks(revno, revision_id)
         self.set_revision_history(history)
         self._run_post_change_branch_tip_hooks(old_revno, old_revid)
 
@@ -1958,6 +1984,7 @@
         old_revno, old_revid = self.last_revision_info()
         if self._get_append_revisions_only():
             self._check_history_violation(revision_id)
+        self._run_pre_change_branch_tip_hooks(revno, revision_id)
         self._write_last_revision_info(revno, revision_id)
         self._clear_cached_state()
         self._last_revision_info_cache = revno, revision_id

=== modified file 'bzrlib/help_topics/en/hooks.txt'
--- a/bzrlib/help_topics/en/hooks.txt	2008-05-08 06:47:59 +0000
+++ b/bzrlib/help_topics/en/hooks.txt	2008-07-04 03:19:47 +0000
@@ -105,6 +105,34 @@
 branch, and an empty branch receives new_revno of 0, new_revid of None.
 
 
+pre_change_branch_tip (Branch)
+-------------------------------
+
+Run before a branch tip has been changed, while the branch is write-locked.
+Note that push, pull, commit and uncommit all invoke this hook.
+
+The hook signature is (params), where params is an object containing
+the members
+
+  branch
+    The branch whose tip has been changed.
+
+  old_revno
+    The revision number (eg 10) of the branch before the change.
+
+  old_revid
+    The revision id (eg joe at foo.com-1234234-aoeua34) before the change.
+
+  new_revno
+    The revision number (eg 12) of the branch after the change.
+
+  new_revid
+    The revision id (eg joe at foo.com-5676566-boa234a) after the change.
+
+The old_revno and new_revno members are integers, as the head
+revision never has a dotted revision number.
+
+
 post_change_branch_tip (Branch)
 -------------------------------
 
@@ -130,7 +158,7 @@
     The revision id (eg joe at foo.com-5676566-boa234a) after the change.
 
 The old_revno and new_revno members are integers, as the head
-revision is never has a dotted revision number.
+revision never has a dotted revision number.
 
 
 set_rh (Branch)

=== modified file 'bzrlib/tests/branch_implementations/test_hooks.py'
--- a/bzrlib/tests/branch_implementations/test_hooks.py	2008-04-20 08:21:39 +0000
+++ b/bzrlib/tests/branch_implementations/test_hooks.py	2008-07-04 01:34:44 +0000
@@ -16,7 +16,7 @@
 
 """Tests that branch classes implement hook callouts correctly."""
 
-from bzrlib.branch import Branch
+from bzrlib.branch import Branch, ChangeBranchTipParams
 from bzrlib.revision import NULL_REVISION
 from bzrlib.tests import TestCaseWithMemoryTransport
 
@@ -80,79 +80,165 @@
             ])
 
 
-class TestPostChangeBranchTip(TestCaseWithMemoryTransport):
-
-    def setUp(self):
-        self.hook_calls = []
-        TestCaseWithMemoryTransport.setUp(self)
-
-    def capture_post_change_branch_tip_hook(self, params):
-        """Capture post_change_branch_tip hook calls to self.hook_calls.
-
-        The call is logged, as is some state of the branch.
+class ChangeBranchTipTestCase(TestCaseWithMemoryTransport):
+    """Base TestCase for testing pre/post_change_branch_tip hooks."""
+
+    def install_logging_hook(self, prefix):
+        """Add a hook that logs calls made to it.
+        
+        :returns: the list that the calls will be appended to.
         """
-        self.hook_calls.append((params, params.branch.is_locked()))
-        self.assertEquals(params.branch.last_revision_info(),
-                          (params.new_revno, params.new_revid))
-
-    def test_post_change_branch_tip_empty_history(self):
-        branch = self.make_branch('source')
+        hook_calls = []
         Branch.hooks.install_named_hook(
-            'post_change_branch_tip',
-            self.capture_post_change_branch_tip_hook,
-            None)
-        branch.set_last_revision_info(0, NULL_REVISION)
-        self.assertEqual(len(self.hook_calls), 1)
-        self.assertEqual(self.hook_calls[0][0].branch, branch)
-        self.assertEqual(self.hook_calls[0][0].old_revid, NULL_REVISION)
-        self.assertEqual(self.hook_calls[0][0].old_revno, 0)
-        self.assertEqual(self.hook_calls[0][0].new_revid, NULL_REVISION)
-        self.assertEqual(self.hook_calls[0][0].new_revno, 0)
+            'pre_change_branch_tip', hook_calls.append, None)
+        return hook_calls
 
-    def test_post_change_branch_tip_nonempty_history(self):
+    def make_branch_with_revision_ids(self, *revision_ids):
+        """Makes a branch with the given commits."""
         tree = self.make_branch_and_memory_tree('source')
         tree.lock_write()
         tree.add('')
-        tree.commit('another commit', rev_id='f\xc2\xb5')
-        tree.commit('empty commit', rev_id='foo')
+        for revision_id in revision_ids:
+            tree.commit('Message of ' + revision_id, rev_id=revision_id)
         tree.unlock()
         branch = tree.branch
-        Branch.hooks.install_named_hook(
-            'post_change_branch_tip',
-            self.capture_post_change_branch_tip_hook,
-            None)
-        # some branches require that their history be set to a revision in the
-        # repository
-        branch.set_last_revision_info(1, 'f\xc2\xb5')
-        self.assertEqual(len(self.hook_calls), 1)
-        self.assertEqual(self.hook_calls[0][0].branch, branch)
-        self.assertEqual(self.hook_calls[0][0].old_revid, 'foo')
-        self.assertEqual(self.hook_calls[0][0].old_revno, 2)
-        self.assertEqual(self.hook_calls[0][0].new_revid, 'f\xc2\xb5')
-        self.assertEqual(self.hook_calls[0][0].new_revno, 1)
-
-    def test_post_change_branch_tip_branch_is_locked(self):
-        branch = self.make_branch('source')
-        Branch.hooks.install_named_hook(
-            'post_change_branch_tip',
-            self.capture_post_change_branch_tip_hook,
-            None)
-        branch.set_last_revision_info(0, NULL_REVISION)
-        self.assertEqual(len(self.hook_calls), 1)
-        self.assertEqual(self.hook_calls[0][0].branch, branch)
-        self.assertEqual(self.hook_calls[0][1], True)
-
-    def test_post_change_branch_tip_calls_all_hooks_no_errors(self):
-        branch = self.make_branch('source')
-        Branch.hooks.install_named_hook(
-            'post_change_branch_tip',
-            self.capture_post_change_branch_tip_hook,
-            None)
-        Branch.hooks.install_named_hook(
-            'post_change_branch_tip',
-            self.capture_post_change_branch_tip_hook,
-            None)
-        branch.set_last_revision_info(0, NULL_REVISION)
-        self.assertEqual(len(self.hook_calls), 2)
-        self.assertEqual(self.hook_calls[0][0].branch, branch)
-        self.assertEqual(self.hook_calls[1][0].branch, branch)
+        return branch
+
+
+class TestPreChangeBranchTip(ChangeBranchTipTestCase):
+    """Tests for pre_change_branch_tip hook.
+    
+    Most of these tests are very similar to the tests in
+    TestPostChangeBranchTip.
+    """
+
+    def test_hook_runs_before_change(self):
+        """The hook runs *before* the branch's last_revision_info has changed.
+        """
+        branch = self.make_branch_with_revision_ids('revid-one')
+        def assertBranchAtRevision1(params):
+            self.assertEquals(
+                (1, 'revid-one'), params.branch.last_revision_info())
+        Branch.hooks.install_named_hook(
+            'pre_change_branch_tip', assertBranchAtRevision1, None)
+        branch.set_last_revision_info(0, NULL_REVISION)
+
+    def test_reject_by_hook(self):
+        """If a hook raises an exception, the change does not take effect.
+        
+        Also, the exception will be propogated.
+        """
+        branch = self.make_branch_with_revision_ids(
+            'one-\xc2\xb5', 'two-\xc2\xb5')
+        class PearShapedError(Exception):
+            pass
+        def hook_that_raises(params):
+            raise PearShapedError()
+        Branch.hooks.install_named_hook(
+            'pre_change_branch_tip', hook_that_raises, None)
+        self.assertRaises(
+            PearShapedError, branch.set_last_revision_info, 0, NULL_REVISION)
+        # The revision info is unchanged.
+        self.assertEqual((2, 'two-\xc2\xb5'), branch.last_revision_info())
+        
+    def test_empty_history(self):
+        branch = self.make_branch('source')
+        hook_calls = self.install_logging_hook('pre')
+        branch.set_last_revision_info(0, NULL_REVISION)
+        expected_params = ChangeBranchTipParams(
+            branch, 0, 0, NULL_REVISION, NULL_REVISION)
+        self.assertEqual([expected_params], hook_calls)
+
+    def test_nonempty_history(self):
+        # some branches require that their history be set to a revision in the
+        # repository, so we need to make a branch with non-empty history for
+        # this test.
+        branch = self.make_branch_with_revision_ids(
+            'one-\xc2\xb5', 'two-\xc2\xb5')
+        hook_calls = self.install_logging_hook('pre')
+        branch.set_last_revision_info(1, 'one-\xc2\xb5')
+        expected_params = ChangeBranchTipParams(
+            branch, 2, 1, 'two-\xc2\xb5', 'one-\xc2\xb5')
+        self.assertEqual([expected_params], hook_calls)
+
+    def test_branch_is_locked(self):
+        branch = self.make_branch('source')
+        def assertBranchIsLocked(params):
+            self.assertTrue(params.branch.is_locked())
+        Branch.hooks.install_named_hook(
+            'pre_change_branch_tip', assertBranchIsLocked, None)
+        branch.set_last_revision_info(0, NULL_REVISION)
+
+    def test_calls_all_hooks_no_errors(self):
+        """If multiple hooks are registered, all are called (if none raise
+        errors).
+        """
+        branch = self.make_branch('source')
+        hook_calls_1 = self.install_logging_hook('pre')
+        hook_calls_2 = self.install_logging_hook('pre')
+        self.assertIsNot(hook_calls_1, hook_calls_2)
+        branch.set_last_revision_info(0, NULL_REVISION)
+        # Both hooks are called.
+        self.assertEqual(len(hook_calls_1), 1)
+        self.assertEqual(len(hook_calls_2), 1)
+
+
+class TestPostChangeBranchTip(ChangeBranchTipTestCase):
+    """Tests for post_change_branch_tip hook.
+
+    Most of these tests are very similar to the tests in
+    TestPostChangeBranchTip.
+    """
+
+    def test_hook_runs_after_change(self):
+        """The hook runs *after* the branch's last_revision_info has changed.
+        """
+        branch = self.make_branch_with_revision_ids('revid-one')
+        def assertBranchAtRevision1(params):
+            self.assertEquals(
+                (0, NULL_REVISION), params.branch.last_revision_info())
+        Branch.hooks.install_named_hook(
+            'post_change_branch_tip', assertBranchAtRevision1, None)
+        branch.set_last_revision_info(0, NULL_REVISION)
+
+    def test_empty_history(self):
+        branch = self.make_branch('source')
+        hook_calls = self.install_logging_hook('post')
+        branch.set_last_revision_info(0, NULL_REVISION)
+        expected_params = ChangeBranchTipParams(
+            branch, 0, 0, NULL_REVISION, NULL_REVISION)
+        self.assertEqual([expected_params], hook_calls)
+
+    def test_nonempty_history(self):
+        # some branches require that their history be set to a revision in the
+        # repository, so we need to make a branch with non-empty history for
+        # this test.
+        branch = self.make_branch_with_revision_ids(
+            'one-\xc2\xb5', 'two-\xc2\xb5')
+        hook_calls = self.install_logging_hook('post')
+        branch.set_last_revision_info(1, 'one-\xc2\xb5')
+        expected_params = ChangeBranchTipParams(
+            branch, 2, 1, 'two-\xc2\xb5', 'one-\xc2\xb5')
+        self.assertEqual([expected_params], hook_calls)
+
+    def test_branch_is_locked(self):
+        """The branch passed to the hook is locked."""
+        branch = self.make_branch('source')
+        def assertBranchIsLocked(params):
+            self.assertTrue(params.branch.is_locked())
+        Branch.hooks.install_named_hook(
+            'post_change_branch_tip', assertBranchIsLocked, None)
+        branch.set_last_revision_info(0, NULL_REVISION)
+
+    def test_calls_all_hooks_no_errors(self):
+        """If multiple hooks are registered, all are called (if none raise
+        errors).
+        """
+        branch = self.make_branch('source')
+        hook_calls_1 = self.install_logging_hook('post')
+        hook_calls_2 = self.install_logging_hook('post')
+        self.assertIsNot(hook_calls_1, hook_calls_2)
+        branch.set_last_revision_info(0, NULL_REVISION)
+        # Both hooks are called.
+        self.assertEqual(len(hook_calls_1), 1)
+        self.assertEqual(len(hook_calls_2), 1)




More information about the bazaar-commits mailing list