Mercurial > hg
changeset 2155:ff255b41b4aa
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
author | Vadim Gelfer <vadim.gelfer@gmail.com> |
---|---|
date | Fri, 28 Apr 2006 15:50:22 -0700 |
parents | 635653cd73ab |
children | 628bf85f07ee |
files | doc/hgrc.5.txt mercurial/localrepo.py tests/test-hook tests/test-hook.out |
diffstat | 4 files changed, 207 insertions(+), 7 deletions(-) [+] |
line wrap: on
line diff
--- 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):