support hooks written in python.
authorVadim Gelfer <vadim.gelfer@gmail.com>
Fri, 28 Apr 2006 15:50:22 -0700
changeset 2155 ff255b41b4aa
parent 2153 635653cd73ab
child 2156 628bf85f07ee
support hooks written in python. to write hook in python, create module with hook function inside. make sure mercurial can import module (put it in $PYTHONPATH or load it as extension). hook function should look like this: def myhook(ui, repo, hooktype, **kwargs): if hook_passes: return True elif hook_explicitly_fails: return False elif some_other_failure: import util raise util.Abort('helpful failure message') else: return # implicit return of None makes hook fail! then in .hgrc, add hook with "python:" prefix: [hooks] commit = python:mymodule.myhook
doc/hgrc.5.txt
mercurial/localrepo.py
tests/test-hook
tests/test-hook.out
--- a/doc/hgrc.5.txt	Fri Apr 28 14:50:23 2006 -0700
+++ b/doc/hgrc.5.txt	Fri Apr 28 15:50:22 2006 -0700
@@ -131,11 +131,11 @@
     **.txt = tempfile: unix2dos -n INFILE OUTFILE
 
 hooks::
-  Commands that get automatically executed by various actions such as
-  starting or finishing a commit. Multiple commands can be run for
-  the same action by appending a suffix to the action. Overriding a
-  site-wide hook can be done by changing its value or setting it to
-  an empty string.
+  Commands or Python functions that get automatically executed by
+  various actions such as starting or finishing a commit. Multiple
+  hooks can be run for the same action by appending a suffix to the
+  action. Overriding a site-wide hook can be done by changing its
+  value or setting it to an empty string.
 
   Example .hg/hgrc:
 
@@ -211,6 +211,21 @@
   the environment for backwards compatibility, but their use is
   deprecated, and they will be removed in a future release.
 
+  The syntax for Python hooks is as follows:
+
+    hookname = python:modulename.submodule.callable
+
+  Python hooks are run within the Mercurial process.  Each hook is
+  called with at least three keyword arguments: a ui object (keyword
+  "ui"), a repository object (keyword "repo"), and a "hooktype"
+  keyword that tells what kind of hook is used.  Arguments listed as
+  environment variables above are passed as keyword arguments, with no
+  "HG_" prefix, and names in lower case.
+
+  A Python hook must return a "true" value to succeed.  Returning a
+  "false" value or raising an exception is treated as failure of the
+  hook.
+
 http_proxy::
   Used to access web-based Mercurial repositories through a HTTP
   proxy.
--- a/mercurial/localrepo.py	Fri Apr 28 14:50:23 2006 -0700
+++ b/mercurial/localrepo.py	Fri Apr 28 15:50:22 2006 -0700
@@ -11,7 +11,8 @@
 from i18n import gettext as _
 from demandload import *
 demandload(globals(), "appendfile changegroup")
-demandload(globals(), "re lock transaction tempfile stat mdiff errno ui revlog")
+demandload(globals(), "re lock transaction tempfile stat mdiff errno ui")
+demandload(globals(), "revlog sys traceback")
 
 class localrepository(object):
     def __del__(self):
@@ -71,7 +72,59 @@
             os.mkdir(self.join("data"))
 
         self.dirstate = dirstate.dirstate(self.opener, self.ui, self.root)
+
     def hook(self, name, throw=False, **args):
+        def callhook(hname, funcname):
+            '''call python hook. hook is callable object, looked up as
+            name in python module. if callable returns "true", hook
+            passes, else fails. if hook raises exception, treated as
+            hook failure. exception propagates if throw is "true".'''
+
+            self.ui.note(_("calling hook %s: %s\n") % (hname, funcname))
+            d = funcname.rfind('.')
+            if d == -1:
+                raise util.Abort(_('%s hook is invalid ("%s" not in a module)')
+                                 % (hname, funcname))
+            modname = funcname[:d]
+            try:
+                obj = __import__(modname)
+            except ImportError:
+                raise util.Abort(_('%s hook is invalid '
+                                   '(import of "%s" failed)') %
+                                 (hname, modname))
+            try:
+                for p in funcname.split('.')[1:]:
+                    obj = getattr(obj, p)
+            except AttributeError, err:
+                raise util.Abort(_('%s hook is invalid '
+                                   '("%s" is not defined)') %
+                                 (hname, funcname))
+            if not callable(obj):
+                raise util.Abort(_('%s hook is invalid '
+                                   '("%s" is not callable)') %
+                                 (hname, funcname))
+            try:
+                r = obj(ui=ui, repo=repo, hooktype=name, **args)
+            except (KeyboardInterrupt, util.SignalInterrupt):
+                raise
+            except Exception, exc:
+                if isinstance(exc, util.Abort):
+                    self.ui.warn(_('error: %s hook failed: %s\n') %
+                                 (hname, exc.args[0] % exc.args[1:]))
+                else:
+                    self.ui.warn(_('error: %s hook raised an exception: '
+                                   '%s\n') % (hname, exc))
+                if throw:
+                    raise
+                if "--traceback" in sys.argv[1:]:
+                    traceback.print_exc()
+                return False
+            if not r:
+                if throw:
+                    raise util.Abort(_('%s hook failed') % hname)
+                self.ui.warn(_('error: %s hook failed\n') % hname)
+            return r
+
         def runhook(name, cmd):
             self.ui.note(_("running hook %s: %s\n") % (name, cmd))
             env = dict([('HG_' + k.upper(), v) for k, v in args.iteritems()] +
@@ -90,7 +143,10 @@
                  if hname.split(".", 1)[0] == name and cmd]
         hooks.sort()
         for hname, cmd in hooks:
-            r = runhook(hname, cmd) and r
+            if cmd.startswith('python:'):
+                r = callhook(hname, cmd[7:].strip()) and r
+            else:
+                r = runhook(hname, cmd) and r
         return r
 
     def tags(self):
--- a/tests/test-hook	Fri Apr 28 14:50:23 2006 -0700
+++ b/tests/test-hook	Fri Apr 28 15:50:22 2006 -0700
@@ -87,4 +87,93 @@
 echo 'preoutgoing.forbid = echo preoutgoing.forbid hook; exit 1' >> ../a/.hg/hgrc
 hg pull ../a
 
+cat > hooktests.py <<EOF
+from mercurial import util
+
+uncallable = 0
+
+def printargs(args):
+    args.pop('ui', None)
+    args.pop('repo', None)
+    a = list(args.items())
+    a.sort()
+    print 'hook args:'
+    for k, v in a:
+       print ' ', k, v
+    return True
+
+def passhook(**args):
+    printargs(args)
+    return True
+
+def failhook(**args):
+    printargs(args)
+
+class LocalException(Exception):
+    pass
+
+def raisehook(**args):
+    raise LocalException('exception from hook')
+
+def aborthook(**args):
+    raise util.Abort('raise abort from hook')
+
+def brokenhook(**args):
+    return 1 + {}
+
+class container:
+    unreachable = 1
+EOF
+
+echo '# test python hooks'
+PYTHONPATH="$PWD:$PYTHONPATH"
+export PYTHONPATH
+
+echo '[hooks]' > ../a/.hg/hgrc
+echo 'preoutgoing.broken = python:hooktests.brokenhook' >> ../a/.hg/hgrc
+hg pull ../a 2>&1 | grep 'raised an exception'
+
+echo '[hooks]' > ../a/.hg/hgrc
+echo 'preoutgoing.raise = python:hooktests.raisehook' >> ../a/.hg/hgrc
+hg pull ../a 2>&1 | grep 'raised an exception'
+
+echo '[hooks]' > ../a/.hg/hgrc
+echo 'preoutgoing.abort = python:hooktests.aborthook' >> ../a/.hg/hgrc
+hg pull ../a
+
+echo '[hooks]' > ../a/.hg/hgrc
+echo 'preoutgoing.fail = python:hooktests.failhook' >> ../a/.hg/hgrc
+hg pull ../a
+
+echo '[hooks]' > ../a/.hg/hgrc
+echo 'preoutgoing.uncallable = python:hooktests.uncallable' >> ../a/.hg/hgrc
+hg pull ../a
+
+echo '[hooks]' > ../a/.hg/hgrc
+echo 'preoutgoing.nohook = python:hooktests.nohook' >> ../a/.hg/hgrc
+hg pull ../a
+
+echo '[hooks]' > ../a/.hg/hgrc
+echo 'preoutgoing.nomodule = python:nomodule' >> ../a/.hg/hgrc
+hg pull ../a
+
+echo '[hooks]' > ../a/.hg/hgrc
+echo 'preoutgoing.badmodule = python:nomodule.nowhere' >> ../a/.hg/hgrc
+hg pull ../a
+
+echo '[hooks]' > ../a/.hg/hgrc
+echo 'preoutgoing.unreachable = python:hooktests.container.unreachable' >> ../a/.hg/hgrc
+hg pull ../a
+
+echo '[hooks]' > ../a/.hg/hgrc
+echo 'preoutgoing.pass = python:hooktests.passhook' >> ../a/.hg/hgrc
+hg pull ../a
+
+echo '# make sure --traceback works'
+echo '[hooks]' > .hg/hgrc
+echo 'commit.abort = python:hooktests.aborthook' >> .hg/hgrc
+
+echo a >> a
+hg --traceback commit -A -m a 2>&1 | grep '^Traceback'
+
 exit 0
--- a/tests/test-hook.out	Fri Apr 28 14:50:23 2006 -0700
+++ b/tests/test-hook.out	Fri Apr 28 15:50:22 2006 -0700
@@ -89,3 +89,43 @@
 pulling from ../a
 searching for changes
 abort: preoutgoing.forbid hook exited with status 1
+# test python hooks
+error: preoutgoing.broken hook raised an exception: unsupported operand type(s) for +: 'int' and 'dict'
+error: preoutgoing.raise hook raised an exception: exception from hook
+pulling from ../a
+searching for changes
+error: preoutgoing.abort hook failed: raise abort from hook
+abort: raise abort from hook
+pulling from ../a
+searching for changes
+hook args:
+  hooktype preoutgoing
+  source pull
+abort: preoutgoing.fail hook failed
+pulling from ../a
+searching for changes
+abort: preoutgoing.uncallable hook is invalid ("hooktests.uncallable" is not callable)
+pulling from ../a
+searching for changes
+abort: preoutgoing.nohook hook is invalid ("hooktests.nohook" is not defined)
+pulling from ../a
+searching for changes
+abort: preoutgoing.nomodule hook is invalid ("nomodule" not in a module)
+pulling from ../a
+searching for changes
+abort: preoutgoing.badmodule hook is invalid (import of "nomodule" failed)
+pulling from ../a
+searching for changes
+abort: preoutgoing.unreachable hook is invalid (import of "hooktests.container" failed)
+pulling from ../a
+searching for changes
+hook args:
+  hooktype preoutgoing
+  source pull
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+(run 'hg update' to get a working copy)
+# make sure --traceback works
+Traceback (most recent call last):