[MERGE #102747] pre_commit hook

Nam Nguyen bitsink+bazaar at gmail.com
Mon Aug 27 09:48:53 BST 2007


Hi list

I'm resubmitting a patch for the pre_commit hook, incorporating
suggestions from John and Aaron.

Cheers
Nam
-------------- next part --------------
# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: namnguyen-20070827083837-t6a5oewzvdct6j8i
# target_branch: ..\\bzr.dev
# testament_sha1: cf58dbfffe77a3690d6d3109afed51f503db17bb
# timestamp: 2007-08-27 16:42:00 +0800
# base_revision_id: pqm at pqm.ubuntu.com-20070825182243-a3w20rpadbfz8euc
# 
# Begin patch
=== modified file 'NEWS'
--- NEWS	2007-08-24 13:02:26 +0000
+++ NEWS	2007-08-27 03:21:16 +0000
@@ -38,6 +38,9 @@
 
   IMPROVEMENTS:
 
+    * ``Branch.hooks`` now supports ``pre_commit`` hook. The hook's signature
+      is documented in BranchHooks constructor. (Nam T. Nguyen, #102747)
+
     * ``pull`` and ``merge`` are much faster at installing bundle format 4.
       (Aaron Bentley)
 

=== modified file 'bzrlib/branch.py'
--- bzrlib/branch.py	2007-08-20 13:07:12 +0000
+++ bzrlib/branch.py	2007-08-27 08:38:37 +0000
@@ -999,6 +999,16 @@
         # is read locked and the target branches write locked. The local
         # branch is the low-latency branch.
         self['post_pull'] = []
+        # invoked before a commit operation takes place.
+        # the api signature is
+        # (local, master, old_revno, old_revid, future_revno, future_revid,
+        #  tree_delta, future_tree).
+        # old_revid is NULL_REVISION for the first commit to a branch
+        # tree_delta is a TreeDelta object describing changes from the basis
+        # revision, hooks MUST NOT modify this delta
+        # future_tree is an in-memory tree obtained from
+        # CommitBuilder.revision_tree() and hooks MUST NOT modify this tree
+        self['pre_commit'] = []
         # invoked after a commit operation completes.
         # the api signature is 
         # (local, master, old_revno, old_revid, new_revno, new_revid)

=== modified file 'bzrlib/commit.py'
--- bzrlib/commit.py	2007-08-15 11:24:06 +0000
+++ bzrlib/commit.py	2007-08-27 08:38:37 +0000
@@ -254,7 +254,7 @@
             self._check_bound_branch()
 
             # Check that the working tree is up to date
-            old_revno,new_revno = self._check_out_of_date_tree()
+            old_revno, new_revno = self._check_out_of_date_tree()
 
             if self.config is None:
                 self.config = self.branch.get_config()
@@ -275,7 +275,7 @@
             # information in the progress bar during the relevant stages.
             self.pb_stage_name = ""
             self.pb_stage_count = 0
-            self.pb_stage_total = 4
+            self.pb_stage_total = 5
             if self.bound_branch:
                 self.pb_stage_total += 1
             self.pb.show_pct = False
@@ -296,6 +296,7 @@
                     entries_title="Directory")
             self.builder = self.branch.get_commit_builder(self.parents,
                 self.config, timestamp, timezone, committer, revprops, rev_id)
+            
             try:
                 self._update_builder_with_changes()
                 self._check_pointless()
@@ -315,11 +316,14 @@
 
                 # Add revision data to the local branch
                 self.rev_id = self.builder.commit(self.message)
+
             except:
                 # perhaps this should be done by the CommitBuilder ?
                 self.work_tree.branch.repository.abort_write_group()
                 raise
 
+            self._process_pre_hooks(old_revno, new_revno)
+
             # Upload revision data to the master.
             # this will propagate merged revisions too if needed.
             if self.bound_branch:
@@ -340,7 +344,7 @@
             rev_tree = self.builder.revision_tree()
             self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
             self.reporter.completed(new_revno, self.rev_id)
-            self._process_hooks(old_revno, new_revno)
+            self._process_post_hooks(old_revno, new_revno)
         finally:
             self._cleanup()
         return self.rev_id
@@ -466,10 +470,15 @@
             new_revno = 1
         return old_revno,new_revno
 
-    def _process_hooks(self, old_revno, new_revno):
-        """Process any registered commit hooks."""
+    def _process_pre_hooks(self, old_revno, new_revno):
+        """Process any registered pre commit hooks."""
+        self._set_progress_stage("Running pre_commit hooks")
+        self._process_hooks("pre_commit", old_revno, new_revno)
+
+    def _process_post_hooks(self, old_revno, new_revno):
+        """Process any registered post commit hooks."""
         # Process the post commit hooks, if any
-        self._set_progress_stage("Running post commit hooks")
+        self._set_progress_stage("Running post_commit hooks")
         # old style commit hooks - should be deprecated ? (obsoleted in
         # 0.15)
         if self.config.post_commit() is not None:
@@ -480,6 +489,13 @@
                               {'branch':self.branch,
                                'bzrlib':bzrlib,
                                'rev_id':self.rev_id})
+        # process new style post commit hooks
+        self._process_hooks("post_commit", old_revno, new_revno)
+
+    def _process_hooks(self, hook_name, old_revno, new_revno):
+        if not Branch.hooks[hook_name]:
+            return
+        
         # new style commit hooks:
         if not self.bound_branch:
             hook_master = self.branch
@@ -494,19 +510,30 @@
             old_revid = self.parents[0]
         else:
             old_revid = bzrlib.revision.NULL_REVISION
-        for hook in Branch.hooks['post_commit']:
+        
+        if hook_name == "pre_commit":
+            future_tree = self.builder.revision_tree()
+            tree_delta = future_tree.changes_from(self.basis_tree,
+                                             include_root=True)
+        
+        for hook in Branch.hooks[hook_name]:
             # show the running hook in the progress bar. As hooks may
             # end up doing nothing (e.g. because they are not configured by
             # the user) this is still showing progress, not showing overall
             # actions - its up to each plugin to show a UI if it want's to
             # (such as 'Emailing diff to foo at example.com').
-            self.pb_stage_name = "Running post commit hooks [%s]" % \
-                Branch.hooks.get_hook_name(hook)
+            self.pb_stage_name = "Running %s hooks [%s]" % \
+                (hook_name, Branch.hooks.get_hook_name(hook))
             self._emit_progress()
             if 'hooks' in debug.debug_flags:
                 mutter("Invoking commit hook: %r", hook)
-            hook(hook_local, hook_master, old_revno, old_revid, new_revno,
-                self.rev_id)
+            if hook_name == "post_commit":
+                hook(hook_local, hook_master, old_revno, old_revid, new_revno,
+                     self.rev_id)
+            elif hook_name == "pre_commit":
+                hook(hook_local, hook_master,
+                     old_revno, old_revid, new_revno, self.rev_id,
+                     tree_delta, future_tree)
 
     def _cleanup(self):
         """Cleanup any open locks, progress bars etc."""

=== modified file 'bzrlib/tests/branch_implementations/test_commit.py'
--- bzrlib/tests/branch_implementations/test_commit.py	2007-02-06 02:33:42 +0000
+++ bzrlib/tests/branch_implementations/test_commit.py	2007-08-27 08:38:37 +0000
@@ -21,6 +21,7 @@
 from bzrlib.tests.branch_implementations.test_branch import TestCaseWithBranch
 from bzrlib.revision import NULL_REVISION
 from bzrlib.transport import get_transport
+from bzrlib.delta import TreeDelta
 
 
 class TestCommit(TestCaseWithBranch):
@@ -59,6 +60,12 @@
             ('post_commit', local_base, master.base, old_revno, old_revid,
              new_revno, new_revid, local_locked, master.is_locked()))
 
+    def capture_pre_commit_hook(self, local, master, old_revno, old_revid,
+                                new_revno, new_revid,
+                                tree_delta, future_tree):
+        self.hook_calls.append(('pre_commit', old_revno, old_revid,
+                                new_revno, new_revid, tree_delta))
+
     def test_post_commit_to_origin(self):
         tree = self.make_branch_and_memory_tree('branch')
         Branch.hooks.install_hook('post_commit',
@@ -112,3 +119,98 @@
             ],
             self.hook_calls)
         tree.unlock()
+    
+    def test_pre_commit_passes(self):
+        empty_delta = TreeDelta()
+        root_delta = TreeDelta()
+        root_delta.added = [('', '', 'directory')]
+        tree = self.make_branch_and_memory_tree('branch')
+        tree.lock_write()
+        tree.add('', '')
+        Branch.hooks.install_hook("pre_commit", self.capture_pre_commit_hook)
+        revid1 = tree.commit('first revision')
+        revid2 = tree.commit('second revision')
+        self.assertEqual([
+            ('pre_commit', 0, NULL_REVISION, 1, revid1, root_delta),
+            ('pre_commit', 1, revid1, 2, revid2, empty_delta)
+            ],
+            self.hook_calls)
+        tree.unlock()
+
+    def test_pre_commit_fails(self):
+        empty_delta = TreeDelta()
+        root_delta = TreeDelta()
+        root_delta.added = [('', '', 'directory')]
+        tree = self.make_branch_and_memory_tree('branch')
+        tree.lock_write()
+        tree.add('', '')
+        class PreCommitException(Exception): pass
+        def hook_func(local, master,
+                      old_revno, old_revid, new_revno, new_revid,
+                      tree_delta, future_tree):
+            raise PreCommitException(new_revid)
+        Branch.hooks.install_hook("pre_commit", self.capture_pre_commit_hook)
+        Branch.hooks.install_hook("pre_commit", hook_func)
+        revids = [None, None, None]
+        # this commit will raise an exception
+        # so the commit is rolled back and revno unchanged
+        err = self.assertRaises(PreCommitException, tree.commit, 'message')
+        # we have to record the revid to use in assertEqual later
+        revids[0] = err.message
+        # unregister all pre_commit hooks
+        Branch.hooks["pre_commit"] = []
+        # and re-register the capture hook
+        Branch.hooks.install_hook("pre_commit", self.capture_pre_commit_hook)
+        # now these commits should go through
+        for i in range(1, 3):
+            revids[i] = tree.commit('message')
+        self.assertEqual([
+            ('pre_commit', 0, NULL_REVISION, 1, revids[0], root_delta),
+            ('pre_commit', 0, NULL_REVISION, 1, revids[1], root_delta),
+            ('pre_commit', 1, revids[1], 2, revids[2], empty_delta)
+            ],
+            self.hook_calls)
+        tree.unlock()
+
+    def test_pre_commit_delta(self):
+        # This tests the TreeDelta object passed to pre_commit hook.
+        # This does not try to validate data correctness in the delta.
+        self.build_tree(['rootfile', 'dir/', 'dir/subfile'])
+        tree = self.make_branch_and_tree('.')
+        tree.lock_write()
+        try:
+            # setting up a playground
+            tree.set_root_id('root_id')
+            tree.add('rootfile', 'rootfile_id')
+            tree.put_file_bytes_non_atomic('rootfile_id', 'abc')
+            tree.add('dir', 'dir_id')
+            tree.add('dir/subfile', 'dir_subfile_id')
+            tree.mkdir('to_be_unversioned', 'to_be_unversioned_id')
+            tree.put_file_bytes_non_atomic('dir_subfile_id', 'def')
+            revid1 = tree.commit('first revision')
+        finally:
+            tree.unlock()
+        
+        tree.lock_write()
+        try:
+            # making changes
+            tree.put_file_bytes_non_atomic('rootfile_id', 'jkl')
+            tree.rename_one('dir/subfile', 'dir/subfile_renamed')
+            tree.unversion(['to_be_unversioned_id'])
+            tree.mkdir('added_dir', 'added_dir_id')
+            # start to capture pre_commit delta
+            Branch.hooks.install_hook("pre_commit", self.capture_pre_commit_hook)
+            revid2 = tree.commit('second revision')
+        finally:
+            tree.unlock()
+        
+        expected_delta = TreeDelta()
+        expected_delta.added = [('added_dir', 'added_dir_id', 'directory')]
+        expected_delta.removed = [('to_be_unversioned',
+                                   'to_be_unversioned_id', 'directory')]
+        expected_delta.renamed = [('dir/subfile', 'dir/subfile_renamed',
+                                   'dir_subfile_id', 'file', False, False)]
+        expected_delta.modified=[('rootfile', 'rootfile_id', 'file', True,
+                                  False)]
+        self.assertEqual([('pre_commit', 1, revid1, 2, revid2,
+                           expected_delta)], self.hook_calls)
\ No newline at end of file

=== modified file 'bzrlib/tests/test_branch.py'
--- bzrlib/tests/test_branch.py	2007-08-14 11:53:00 +0000
+++ bzrlib/tests/test_branch.py	2007-08-27 03:21:16 +0000
@@ -321,6 +321,7 @@
         self.assertTrue("set_rh" in hooks, "set_rh not in %s" % hooks)
         self.assertTrue("post_push" in hooks, "post_push not in %s" % hooks)
         self.assertTrue("post_commit" in hooks, "post_commit not in %s" % hooks)
+        self.assertTrue("pre_commit" in hooks, "pre_commit not in %s" % hooks)
         self.assertTrue("post_pull" in hooks, "post_pull not in %s" % hooks)
         self.assertTrue("post_uncommit" in hooks, "post_uncommit not in %s" % hooks)
 

=== modified file 'bzrlib/tests/workingtree_implementations/test_commit.py'
--- bzrlib/tests/workingtree_implementations/test_commit.py	2007-07-26 00:26:25 +0000
+++ bzrlib/tests/workingtree_implementations/test_commit.py	2007-08-27 08:38:37 +0000
@@ -354,17 +354,18 @@
         # into the factory for this test - just make the test ui factory
         # pun as a reporter. Then we can check the ordering is right.
         tree.commit('second post', specific_files=['b'])
-        # 4 steps, the first of which is reported 2 times, once per dir
+        # 5 steps, the first of which is reported 2 times, once per dir
         self.assertEqual(
-            [('update', 1, 4, 'Collecting changes [Directory 0] - Stage'),
-             ('update', 1, 4, 'Collecting changes [Directory 1] - Stage'),
-             ('update', 2, 4, 'Saving data locally - Stage'),
-             ('update', 3, 4, 'Updating the working tree - Stage'),
-             ('update', 4, 4, 'Running post commit hooks - Stage')],
+            [('update', 1, 5, 'Collecting changes [Directory 0] - Stage'),
+             ('update', 1, 5, 'Collecting changes [Directory 1] - Stage'),
+             ('update', 2, 5, 'Saving data locally - Stage'),
+             ('update', 3, 5, 'Running pre_commit hooks - Stage'),
+             ('update', 4, 5, 'Updating the working tree - Stage'),
+             ('update', 5, 5, 'Running post_commit hooks - Stage')],
             factory._calls
            )
 
-    def test_commit_progress_shows_hook_names(self):
+    def test_commit_progress_shows_post_hook_names(self):
         tree = self.make_branch_and_tree('.')
         # set a progress bar that captures the calls so we can see what is 
         # emitted
@@ -378,15 +379,38 @@
         branch.Branch.hooks.name_hook(a_hook, 'hook name')
         tree.commit('first post')
         self.assertEqual(
-            [('update', 1, 4, 'Collecting changes [Directory 0] - Stage'),
-             ('update', 1, 4, 'Collecting changes [Directory 1] - Stage'),
-             ('update', 2, 4, 'Saving data locally - Stage'),
-             ('update', 3, 4, 'Updating the working tree - Stage'),
-             ('update', 4, 4, 'Running post commit hooks - Stage'),
-             ('update', 4, 4, 'Running post commit hooks [hook name] - Stage'),
-             ],
-            factory._calls
-           )
-
-
-
+            [('update', 1, 5, 'Collecting changes [Directory 0] - Stage'),
+             ('update', 1, 5, 'Collecting changes [Directory 1] - Stage'),
+             ('update', 2, 5, 'Saving data locally - Stage'),
+             ('update', 3, 5, 'Running pre_commit hooks - Stage'),
+             ('update', 4, 5, 'Updating the working tree - Stage'),
+             ('update', 5, 5, 'Running post_commit hooks - Stage'),
+             ('update', 5, 5, 'Running post_commit hooks [hook name] - Stage'),
+             ],
+            factory._calls
+           )
+
+    def test_commit_progress_shows_pre_hook_names(self):
+        tree = self.make_branch_and_tree('.')
+        # set a progress bar that captures the calls so we can see what is 
+        # emitted
+        self.old_ui_factory = ui.ui_factory
+        self.addCleanup(self.restoreDefaults)
+        factory = CapturingUIFactory()
+        ui.ui_factory = factory
+        def a_hook(_, _2, _3, _4, _5, _6, _7, _8):
+            pass
+        branch.Branch.hooks.install_hook('pre_commit', a_hook)
+        branch.Branch.hooks.name_hook(a_hook, 'hook name')
+        tree.commit('first post')
+        self.assertEqual(
+            [('update', 1, 5, 'Collecting changes [Directory 0] - Stage'),
+             ('update', 1, 5, 'Collecting changes [Directory 1] - Stage'),
+             ('update', 2, 5, 'Saving data locally - Stage'),
+             ('update', 3, 5, 'Running pre_commit hooks - Stage'),
+             ('update', 3, 5, 'Running pre_commit hooks [hook name] - Stage'),
+             ('update', 4, 5, 'Updating the working tree - Stage'),
+             ('update', 5, 5, 'Running post_commit hooks - Stage'),
+             ],
+            factory._calls
+           )

# Begin bundle
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
yDEdHv0gFj/Nesf3/osT+ou5IpwoSBIewK6A


More information about the bazaar mailing list