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
--- 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):