Changes to 3 files · Browse files at c308a063178b Showing diff from parent 1d73469befa3 Diff from another changeset...
@@ -171,3 +171,7 @@
class RefFormatError(Exception):
"""Indicates an invalid ref name."""
+
+
+class HookError(Exception):
+ """An error occurred while executing a hook."""
|
|
|
@@ -1,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)
|
|
@@ -41,6 +41,7 @@ PackedRefsException,
CommitError,
RefFormatError,
+ HookError,
)
from dulwich.file import (
ensure_dir_exists,
@@ -58,6 +59,13 @@ Tree,
hex_to_sha,
)
+
+from dulwich.hooks import (
+ PreCommitShellHook,
+ PostCommitShellHook,
+ CommitMsgShellHook,
+)
+
import warnings
@@ -813,6 +821,8 @@ 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 @@ 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 @@ 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
@@ -1220,6 +1247,13 @@ # Fail if the atomic compare-and-swap failed, leaving the commit and
# 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 @@ 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
|
Loading...