# HG changeset patch # User Vadim Gelfer # Date 1146264622 25200 # Node ID ff255b41b4aa1eefbc8f696d816c9441a97ace87 # Parent 635653cd73aba71ec9e7b05ec4cb19c6f2132087 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 diff -r 635653cd73ab -r ff255b41b4aa doc/hgrc.5.txt --- 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. diff -r 635653cd73ab -r ff255b41b4aa mercurial/localrepo.py --- 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): diff -r 635653cd73ab -r ff255b41b4aa tests/test-hook --- 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 < ../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 diff -r 635653cd73ab -r ff255b41b4aa tests/test-hook.out --- 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):