Forráskód Böngészése

Basic Hook Framework

Implement local shell hooks -
    pre-commit, commit-msg, post-commit
Add to hooks to Repo
milki 12 éve
szülő
commit
c308a06317
3 módosított fájl, 190 hozzáadás és 1 törlés
  1. 4 0
      dulwich/errors.py
  2. 147 0
      dulwich/hooks.py
  3. 39 1
      dulwich/repo.py

+ 4 - 0
dulwich/errors.py

@@ -171,3 +171,7 @@ class CommitError(Exception):
 
 class RefFormatError(Exception):
     """Indicates an invalid ref name."""
+
+
+class HookError(Exception):
+    """An error occurred while executing a hook."""

+ 147 - 0
dulwich/hooks.py

@@ -0,0 +1,147 @@
+# hooks.py -- for dealing with git hooks
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2
+# of the License or (at your option) a later version of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+"""Access to hooks."""
+
+import os
+import subprocess
+import tempfile
+import warnings
+
+from dulwich.errors import (
+    HookError,
+)
+
+
+class Hook(object):
+    """Generic hook object."""
+
+    def execute(elf, *args):
+        """Execute the hook with the given args
+
+        :param args: argument list to hook
+        :raise HookError: hook execution failure
+        :return: a hook may return a useful value
+        """
+        raise NotImplementedError(self.execute)
+
+
+class ShellHook(Hook):
+    """Hook by executable file
+
+    Implements standard githooks(5) [0]:
+
+    [0] http://www.kernel.org/pub/software/scm/git/docs/githooks.html
+    """
+
+    def __init__(self, name, path, numparam,
+                 pre_exec_callback=None, post_exec_callback=None):
+        """Setup shell hook definition
+
+        :param name: name of hook for error messages
+        :param path: absolute path to executable file
+        :param numparam: number of requirements parameters
+        :param pre_exec_callback: closure for setup before execution
+            Defaults to None. Takes in the variable argument list from the
+            execute functions and returns a modified argument list for the
+            shell hook.
+        :param post_exec_callback: closure for cleanup after execution
+            Defaults to None. Takes in a boolean for hook success and the
+            modified argument list and returns the final hook return value
+            if applicable
+        """
+        self.name = name
+        self.filepath = path
+        self.numparam = numparam
+
+        self.pre_exec_callback = pre_exec_callback
+        self.post_exec_callback = post_exec_callback
+
+    def execute(self, *args):
+        """Execute the hook with given args"""
+
+        if len(args) != self.numparam:
+            raise HookError("Hook %s executed with wrong number of args. \
+                            Expected %d. Saw %d. %s"
+                            % (self.name, self.numparam, len(args)))
+
+        if (self.pre_exec_callback is not None):
+            args = self.pre_exec_callback(*args)
+
+        try:
+            ret = subprocess.call([self.filepath] + list(args))
+            if ret != 0:
+                if (self.post_exec_callback is not None):
+                    self.post_exec_callback(0, *args)
+                raise HookError("Hook %s exited with non-zero status"
+                                % (self.name))
+            if (self.post_exec_callback is not None):
+                return self.post_exec_callback(1, *args)
+        except OSError:  # no file. silent failure.
+            if (self.post_exec_callback is not None):
+                self.post_exec_callback(0, *args)
+
+
+class PreCommitShellHook(ShellHook):
+    """pre-commit shell hook"""
+
+    def __init__(self, controldir):
+        filepath = os.path.join(controldir, 'hooks', 'pre-commit')
+
+        ShellHook.__init__(self, 'pre-commit', filepath, 0)
+
+
+class PostCommitShellHook(ShellHook):
+    """post-commit shell hook"""
+
+    def __init__(self, controldir):
+        filepath = os.path.join(controldir, 'hooks', 'post-commit')
+
+        ShellHook.__init__(self, 'post-commit', filepath, 0)
+
+
+class CommitMsgShellHook(ShellHook):
+    """commit-msg shell hook
+
+    :param args[0]: commit message
+    :return: new commit message or None
+    """
+
+    def __init__(self, controldir):
+        filepath = os.path.join(controldir, 'hooks', 'commit-msg')
+
+        def prepare_msg(*args):
+            (fd, path) = tempfile.mkstemp()
+
+            f = os.fdopen(fd, 'wb')
+            try:
+                f.write(args[0])
+            finally:
+                f.close()
+
+            return (path,)
+
+        def clean_msg(success, *args):
+            if success:
+                with open(args[0], 'rb') as f:
+                    new_msg = f.read()
+                os.unlink(args[0])
+                return new_msg
+            os.unlink(args[0])
+
+        ShellHook.__init__(self, 'commit-msg', filepath, 1,
+                           prepare_msg, clean_msg)

+ 39 - 1
dulwich/repo.py

@@ -41,6 +41,7 @@ from dulwich.errors import (
     PackedRefsException,
     CommitError,
     RefFormatError,
+    HookError,
     )
 from dulwich.file import (
     ensure_dir_exists,
@@ -58,6 +59,13 @@ from dulwich.objects import (
     Tree,
     hex_to_sha,
     )
+
+from dulwich.hooks import (
+    PreCommitShellHook,
+    PostCommitShellHook,
+    CommitMsgShellHook,
+)
+
 import warnings
 
 
@@ -813,6 +821,8 @@ class BaseRepo(object):
         self.object_store = object_store
         self.refs = refs
 
+        self.hooks = {}
+
     def _init_files(self, bare):
         """Initialize a default set of named files."""
         from dulwich.config import ConfigFile
@@ -1179,6 +1189,14 @@ class BaseRepo(object):
             if len(tree) != 40:
                 raise ValueError("tree must be a 40-byte hex sha string")
             c.tree = tree
+
+        try:
+            self.hooks['pre-commit'].execute()
+        except HookError, e:
+            raise CommitError(e)
+        except KeyError:  # no hook defined, silent fallthrough
+            pass
+
         if merge_heads is None:
             # FIXME: Read merge heads from .git/MERGE_HEADS
             merge_heads = []
@@ -1206,7 +1224,16 @@ class BaseRepo(object):
         if message is None:
             # FIXME: Try to read commit message from .git/MERGE_MSG
             raise ValueError("No commit message specified")
-        c.message = message
+
+        try:
+            c.message = self.hooks['commit-msg'].execute(message)
+            if c.message is None:
+                c.message = message
+        except HookError, e:
+            raise CommitError(e)
+        except KeyError:  # no hook defined, message not modified
+            c.message = message
+
         try:
             old_head = self.refs[ref]
             c.parents = [old_head] + merge_heads
@@ -1221,6 +1248,13 @@ class BaseRepo(object):
             # all its objects as garbage.
             raise CommitError("%s changed during commit" % (ref,))
 
+        try:
+            self.hooks['post-commit'].execute()
+        except HookError, e:  # silent failure
+            warnings.warn("post-commit hook failed: %s" % e, UserWarning)
+        except KeyError:  # no hook defined, silent fallthrough
+            pass
+
         return c.id
 
 
@@ -1260,6 +1294,10 @@ class Repo(BaseRepo):
         refs = DiskRefsContainer(self.controldir())
         BaseRepo.__init__(self, object_store, refs)
 
+        self.hooks['pre-commit'] = PreCommitShellHook(self.controldir())
+        self.hooks['commit-msg'] = CommitMsgShellHook(self.controldir())
+        self.hooks['post-commit'] = PostCommitShellHook(self.controldir())
+
     def controldir(self):
         """Return the path of the control directory."""
         return self._controldir