##// END OF EJS Templates
py3: change default priority and length used for sorting hooks to be compatible with python 3...
Charles Chamberlain -
r45371:50416d3d stable
parent child Browse files
Show More
@@ -1,336 +1,336 b''
1 # hook.py - hook support for mercurial
1 # hook.py - hook support for mercurial
2 #
2 #
3 # Copyright 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import contextlib
10 import contextlib
11 import os
11 import os
12 import sys
12 import sys
13
13
14 from .i18n import _
14 from .i18n import _
15 from .pycompat import getattr
15 from .pycompat import getattr
16 from . import (
16 from . import (
17 demandimport,
17 demandimport,
18 encoding,
18 encoding,
19 error,
19 error,
20 extensions,
20 extensions,
21 pycompat,
21 pycompat,
22 util,
22 util,
23 )
23 )
24 from .utils import (
24 from .utils import (
25 procutil,
25 procutil,
26 resourceutil,
26 resourceutil,
27 stringutil,
27 stringutil,
28 )
28 )
29
29
30
30
31 def pythonhook(ui, repo, htype, hname, funcname, args, throw):
31 def pythonhook(ui, repo, htype, hname, funcname, args, throw):
32 '''call python hook. hook is callable object, looked up as
32 '''call python hook. hook is callable object, looked up as
33 name in python module. if callable returns "true", hook
33 name in python module. if callable returns "true", hook
34 fails, else passes. if hook raises exception, treated as
34 fails, else passes. if hook raises exception, treated as
35 hook failure. exception propagates if throw is "true".
35 hook failure. exception propagates if throw is "true".
36
36
37 reason for "true" meaning "hook failed" is so that
37 reason for "true" meaning "hook failed" is so that
38 unmodified commands (e.g. mercurial.commands.update) can
38 unmodified commands (e.g. mercurial.commands.update) can
39 be run as hooks without wrappers to convert return values.'''
39 be run as hooks without wrappers to convert return values.'''
40
40
41 if callable(funcname):
41 if callable(funcname):
42 obj = funcname
42 obj = funcname
43 funcname = pycompat.sysbytes(obj.__module__ + "." + obj.__name__)
43 funcname = pycompat.sysbytes(obj.__module__ + "." + obj.__name__)
44 else:
44 else:
45 d = funcname.rfind(b'.')
45 d = funcname.rfind(b'.')
46 if d == -1:
46 if d == -1:
47 raise error.HookLoadError(
47 raise error.HookLoadError(
48 _(b'%s hook is invalid: "%s" not in a module')
48 _(b'%s hook is invalid: "%s" not in a module')
49 % (hname, funcname)
49 % (hname, funcname)
50 )
50 )
51 modname = funcname[:d]
51 modname = funcname[:d]
52 oldpaths = sys.path
52 oldpaths = sys.path
53 if resourceutil.mainfrozen():
53 if resourceutil.mainfrozen():
54 # binary installs require sys.path manipulation
54 # binary installs require sys.path manipulation
55 modpath, modfile = os.path.split(modname)
55 modpath, modfile = os.path.split(modname)
56 if modpath and modfile:
56 if modpath and modfile:
57 sys.path = sys.path[:] + [modpath]
57 sys.path = sys.path[:] + [modpath]
58 modname = modfile
58 modname = modfile
59 with demandimport.deactivated():
59 with demandimport.deactivated():
60 try:
60 try:
61 obj = __import__(pycompat.sysstr(modname))
61 obj = __import__(pycompat.sysstr(modname))
62 except (ImportError, SyntaxError):
62 except (ImportError, SyntaxError):
63 e1 = sys.exc_info()
63 e1 = sys.exc_info()
64 try:
64 try:
65 # extensions are loaded with hgext_ prefix
65 # extensions are loaded with hgext_ prefix
66 obj = __import__("hgext_%s" % pycompat.sysstr(modname))
66 obj = __import__("hgext_%s" % pycompat.sysstr(modname))
67 except (ImportError, SyntaxError):
67 except (ImportError, SyntaxError):
68 e2 = sys.exc_info()
68 e2 = sys.exc_info()
69 if ui.tracebackflag:
69 if ui.tracebackflag:
70 ui.warn(
70 ui.warn(
71 _(
71 _(
72 b'exception from first failed import '
72 b'exception from first failed import '
73 b'attempt:\n'
73 b'attempt:\n'
74 )
74 )
75 )
75 )
76 ui.traceback(e1)
76 ui.traceback(e1)
77 if ui.tracebackflag:
77 if ui.tracebackflag:
78 ui.warn(
78 ui.warn(
79 _(
79 _(
80 b'exception from second failed import '
80 b'exception from second failed import '
81 b'attempt:\n'
81 b'attempt:\n'
82 )
82 )
83 )
83 )
84 ui.traceback(e2)
84 ui.traceback(e2)
85
85
86 if not ui.tracebackflag:
86 if not ui.tracebackflag:
87 tracebackhint = _(
87 tracebackhint = _(
88 b'run with --traceback for stack trace'
88 b'run with --traceback for stack trace'
89 )
89 )
90 else:
90 else:
91 tracebackhint = None
91 tracebackhint = None
92 raise error.HookLoadError(
92 raise error.HookLoadError(
93 _(b'%s hook is invalid: import of "%s" failed')
93 _(b'%s hook is invalid: import of "%s" failed')
94 % (hname, modname),
94 % (hname, modname),
95 hint=tracebackhint,
95 hint=tracebackhint,
96 )
96 )
97 sys.path = oldpaths
97 sys.path = oldpaths
98 try:
98 try:
99 for p in funcname.split(b'.')[1:]:
99 for p in funcname.split(b'.')[1:]:
100 obj = getattr(obj, p)
100 obj = getattr(obj, p)
101 except AttributeError:
101 except AttributeError:
102 raise error.HookLoadError(
102 raise error.HookLoadError(
103 _(b'%s hook is invalid: "%s" is not defined')
103 _(b'%s hook is invalid: "%s" is not defined')
104 % (hname, funcname)
104 % (hname, funcname)
105 )
105 )
106 if not callable(obj):
106 if not callable(obj):
107 raise error.HookLoadError(
107 raise error.HookLoadError(
108 _(b'%s hook is invalid: "%s" is not callable')
108 _(b'%s hook is invalid: "%s" is not callable')
109 % (hname, funcname)
109 % (hname, funcname)
110 )
110 )
111
111
112 ui.note(_(b"calling hook %s: %s\n") % (hname, funcname))
112 ui.note(_(b"calling hook %s: %s\n") % (hname, funcname))
113 starttime = util.timer()
113 starttime = util.timer()
114
114
115 try:
115 try:
116 r = obj(ui=ui, repo=repo, hooktype=htype, **pycompat.strkwargs(args))
116 r = obj(ui=ui, repo=repo, hooktype=htype, **pycompat.strkwargs(args))
117 except Exception as exc:
117 except Exception as exc:
118 if isinstance(exc, error.Abort):
118 if isinstance(exc, error.Abort):
119 ui.warn(_(b'error: %s hook failed: %s\n') % (hname, exc.args[0]))
119 ui.warn(_(b'error: %s hook failed: %s\n') % (hname, exc.args[0]))
120 else:
120 else:
121 ui.warn(
121 ui.warn(
122 _(b'error: %s hook raised an exception: %s\n')
122 _(b'error: %s hook raised an exception: %s\n')
123 % (hname, stringutil.forcebytestr(exc))
123 % (hname, stringutil.forcebytestr(exc))
124 )
124 )
125 if throw:
125 if throw:
126 raise
126 raise
127 if not ui.tracebackflag:
127 if not ui.tracebackflag:
128 ui.warn(_(b'(run with --traceback for stack trace)\n'))
128 ui.warn(_(b'(run with --traceback for stack trace)\n'))
129 ui.traceback()
129 ui.traceback()
130 return True, True
130 return True, True
131 finally:
131 finally:
132 duration = util.timer() - starttime
132 duration = util.timer() - starttime
133 ui.log(
133 ui.log(
134 b'pythonhook',
134 b'pythonhook',
135 b'pythonhook-%s: %s finished in %0.2f seconds\n',
135 b'pythonhook-%s: %s finished in %0.2f seconds\n',
136 htype,
136 htype,
137 funcname,
137 funcname,
138 duration,
138 duration,
139 )
139 )
140 if r:
140 if r:
141 if throw:
141 if throw:
142 raise error.HookAbort(_(b'%s hook failed') % hname)
142 raise error.HookAbort(_(b'%s hook failed') % hname)
143 ui.warn(_(b'warning: %s hook failed\n') % hname)
143 ui.warn(_(b'warning: %s hook failed\n') % hname)
144 return r, False
144 return r, False
145
145
146
146
147 def _exthook(ui, repo, htype, name, cmd, args, throw):
147 def _exthook(ui, repo, htype, name, cmd, args, throw):
148 starttime = util.timer()
148 starttime = util.timer()
149 env = {}
149 env = {}
150
150
151 # make in-memory changes visible to external process
151 # make in-memory changes visible to external process
152 if repo is not None:
152 if repo is not None:
153 tr = repo.currenttransaction()
153 tr = repo.currenttransaction()
154 repo.dirstate.write(tr)
154 repo.dirstate.write(tr)
155 if tr and tr.writepending():
155 if tr and tr.writepending():
156 env[b'HG_PENDING'] = repo.root
156 env[b'HG_PENDING'] = repo.root
157 env[b'HG_HOOKTYPE'] = htype
157 env[b'HG_HOOKTYPE'] = htype
158 env[b'HG_HOOKNAME'] = name
158 env[b'HG_HOOKNAME'] = name
159
159
160 for k, v in pycompat.iteritems(args):
160 for k, v in pycompat.iteritems(args):
161 if callable(v):
161 if callable(v):
162 v = v()
162 v = v()
163 if isinstance(v, (dict, list)):
163 if isinstance(v, (dict, list)):
164 v = stringutil.pprint(v)
164 v = stringutil.pprint(v)
165 env[b'HG_' + k.upper()] = v
165 env[b'HG_' + k.upper()] = v
166
166
167 if ui.configbool(b'hooks', b'tonative.%s' % name, False):
167 if ui.configbool(b'hooks', b'tonative.%s' % name, False):
168 oldcmd = cmd
168 oldcmd = cmd
169 cmd = procutil.shelltonative(cmd, env)
169 cmd = procutil.shelltonative(cmd, env)
170 if cmd != oldcmd:
170 if cmd != oldcmd:
171 ui.note(_(b'converting hook "%s" to native\n') % name)
171 ui.note(_(b'converting hook "%s" to native\n') % name)
172
172
173 ui.note(_(b"running hook %s: %s\n") % (name, cmd))
173 ui.note(_(b"running hook %s: %s\n") % (name, cmd))
174
174
175 if repo:
175 if repo:
176 cwd = repo.root
176 cwd = repo.root
177 else:
177 else:
178 cwd = encoding.getcwd()
178 cwd = encoding.getcwd()
179 r = ui.system(cmd, environ=env, cwd=cwd, blockedtag=b'exthook-%s' % (name,))
179 r = ui.system(cmd, environ=env, cwd=cwd, blockedtag=b'exthook-%s' % (name,))
180
180
181 duration = util.timer() - starttime
181 duration = util.timer() - starttime
182 ui.log(
182 ui.log(
183 b'exthook',
183 b'exthook',
184 b'exthook-%s: %s finished in %0.2f seconds\n',
184 b'exthook-%s: %s finished in %0.2f seconds\n',
185 name,
185 name,
186 cmd,
186 cmd,
187 duration,
187 duration,
188 )
188 )
189 if r:
189 if r:
190 desc = procutil.explainexit(r)
190 desc = procutil.explainexit(r)
191 if throw:
191 if throw:
192 raise error.HookAbort(_(b'%s hook %s') % (name, desc))
192 raise error.HookAbort(_(b'%s hook %s') % (name, desc))
193 ui.warn(_(b'warning: %s hook %s\n') % (name, desc))
193 ui.warn(_(b'warning: %s hook %s\n') % (name, desc))
194 return r
194 return r
195
195
196
196
197 # represent an untrusted hook command
197 # represent an untrusted hook command
198 _fromuntrusted = object()
198 _fromuntrusted = object()
199
199
200
200
201 def _allhooks(ui):
201 def _allhooks(ui):
202 """return a list of (hook-id, cmd) pairs sorted by priority"""
202 """return a list of (hook-id, cmd) pairs sorted by priority"""
203 hooks = _hookitems(ui)
203 hooks = _hookitems(ui)
204 # Be careful in this section, propagating the real commands from untrusted
204 # Be careful in this section, propagating the real commands from untrusted
205 # sources would create a security vulnerability, make sure anything altered
205 # sources would create a security vulnerability, make sure anything altered
206 # in that section uses "_fromuntrusted" as its command.
206 # in that section uses "_fromuntrusted" as its command.
207 untrustedhooks = _hookitems(ui, _untrusted=True)
207 untrustedhooks = _hookitems(ui, _untrusted=True)
208 for name, value in untrustedhooks.items():
208 for name, value in untrustedhooks.items():
209 trustedvalue = hooks.get(name, (None, None, name, _fromuntrusted))
209 trustedvalue = hooks.get(name, ((), (), name, _fromuntrusted))
210 if value != trustedvalue:
210 if value != trustedvalue:
211 (lp, lo, lk, lv) = trustedvalue
211 (lp, lo, lk, lv) = trustedvalue
212 hooks[name] = (lp, lo, lk, _fromuntrusted)
212 hooks[name] = (lp, lo, lk, _fromuntrusted)
213 # (end of the security sensitive section)
213 # (end of the security sensitive section)
214 return [(k, v) for p, o, k, v in sorted(hooks.values())]
214 return [(k, v) for p, o, k, v in sorted(hooks.values())]
215
215
216
216
217 def _hookitems(ui, _untrusted=False):
217 def _hookitems(ui, _untrusted=False):
218 """return all hooks items ready to be sorted"""
218 """return all hooks items ready to be sorted"""
219 hooks = {}
219 hooks = {}
220 for name, cmd in ui.configitems(b'hooks', untrusted=_untrusted):
220 for name, cmd in ui.configitems(b'hooks', untrusted=_untrusted):
221 if name.startswith(b'priority.') or name.startswith(b'tonative.'):
221 if name.startswith(b'priority.') or name.startswith(b'tonative.'):
222 continue
222 continue
223
223
224 priority = ui.configint(b'hooks', b'priority.%s' % name, 0)
224 priority = ui.configint(b'hooks', b'priority.%s' % name, 0)
225 hooks[name] = (-priority, len(hooks), name, cmd)
225 hooks[name] = ((-priority,), (len(hooks),), name, cmd)
226 return hooks
226 return hooks
227
227
228
228
229 _redirect = False
229 _redirect = False
230
230
231
231
232 def redirect(state):
232 def redirect(state):
233 global _redirect
233 global _redirect
234 _redirect = state
234 _redirect = state
235
235
236
236
237 def hashook(ui, htype):
237 def hashook(ui, htype):
238 """return True if a hook is configured for 'htype'"""
238 """return True if a hook is configured for 'htype'"""
239 if not ui.callhooks:
239 if not ui.callhooks:
240 return False
240 return False
241 for hname, cmd in _allhooks(ui):
241 for hname, cmd in _allhooks(ui):
242 if hname.split(b'.')[0] == htype and cmd:
242 if hname.split(b'.')[0] == htype and cmd:
243 return True
243 return True
244 return False
244 return False
245
245
246
246
247 def hook(ui, repo, htype, throw=False, **args):
247 def hook(ui, repo, htype, throw=False, **args):
248 if not ui.callhooks:
248 if not ui.callhooks:
249 return False
249 return False
250
250
251 hooks = []
251 hooks = []
252 for hname, cmd in _allhooks(ui):
252 for hname, cmd in _allhooks(ui):
253 if hname.split(b'.')[0] == htype and cmd:
253 if hname.split(b'.')[0] == htype and cmd:
254 hooks.append((hname, cmd))
254 hooks.append((hname, cmd))
255
255
256 res = runhooks(ui, repo, htype, hooks, throw=throw, **args)
256 res = runhooks(ui, repo, htype, hooks, throw=throw, **args)
257 r = False
257 r = False
258 for hname, cmd in hooks:
258 for hname, cmd in hooks:
259 r = res[hname][0] or r
259 r = res[hname][0] or r
260 return r
260 return r
261
261
262
262
263 @contextlib.contextmanager
263 @contextlib.contextmanager
264 def redirect_stdio():
264 def redirect_stdio():
265 """Redirects stdout to stderr, if possible."""
265 """Redirects stdout to stderr, if possible."""
266
266
267 oldstdout = -1
267 oldstdout = -1
268 try:
268 try:
269 if _redirect:
269 if _redirect:
270 try:
270 try:
271 stdoutno = procutil.stdout.fileno()
271 stdoutno = procutil.stdout.fileno()
272 stderrno = procutil.stderr.fileno()
272 stderrno = procutil.stderr.fileno()
273 # temporarily redirect stdout to stderr, if possible
273 # temporarily redirect stdout to stderr, if possible
274 if stdoutno >= 0 and stderrno >= 0:
274 if stdoutno >= 0 and stderrno >= 0:
275 procutil.stdout.flush()
275 procutil.stdout.flush()
276 oldstdout = os.dup(stdoutno)
276 oldstdout = os.dup(stdoutno)
277 os.dup2(stderrno, stdoutno)
277 os.dup2(stderrno, stdoutno)
278 except (OSError, AttributeError):
278 except (OSError, AttributeError):
279 # files seem to be bogus, give up on redirecting (WSGI, etc)
279 # files seem to be bogus, give up on redirecting (WSGI, etc)
280 pass
280 pass
281
281
282 yield
282 yield
283
283
284 finally:
284 finally:
285 # The stderr is fully buffered on Windows when connected to a pipe.
285 # The stderr is fully buffered on Windows when connected to a pipe.
286 # A forcible flush is required to make small stderr data in the
286 # A forcible flush is required to make small stderr data in the
287 # remote side available to the client immediately.
287 # remote side available to the client immediately.
288 procutil.stderr.flush()
288 procutil.stderr.flush()
289
289
290 if _redirect and oldstdout >= 0:
290 if _redirect and oldstdout >= 0:
291 procutil.stdout.flush() # write hook output to stderr fd
291 procutil.stdout.flush() # write hook output to stderr fd
292 os.dup2(oldstdout, stdoutno)
292 os.dup2(oldstdout, stdoutno)
293 os.close(oldstdout)
293 os.close(oldstdout)
294
294
295
295
296 def runhooks(ui, repo, htype, hooks, throw=False, **args):
296 def runhooks(ui, repo, htype, hooks, throw=False, **args):
297 args = pycompat.byteskwargs(args)
297 args = pycompat.byteskwargs(args)
298 res = {}
298 res = {}
299
299
300 with redirect_stdio():
300 with redirect_stdio():
301 for hname, cmd in hooks:
301 for hname, cmd in hooks:
302 if cmd is _fromuntrusted:
302 if cmd is _fromuntrusted:
303 if throw:
303 if throw:
304 raise error.HookAbort(
304 raise error.HookAbort(
305 _(b'untrusted hook %s not executed') % hname,
305 _(b'untrusted hook %s not executed') % hname,
306 hint=_(b"see 'hg help config.trusted'"),
306 hint=_(b"see 'hg help config.trusted'"),
307 )
307 )
308 ui.warn(_(b'warning: untrusted hook %s not executed\n') % hname)
308 ui.warn(_(b'warning: untrusted hook %s not executed\n') % hname)
309 r = 1
309 r = 1
310 raised = False
310 raised = False
311 elif callable(cmd):
311 elif callable(cmd):
312 r, raised = pythonhook(ui, repo, htype, hname, cmd, args, throw)
312 r, raised = pythonhook(ui, repo, htype, hname, cmd, args, throw)
313 elif cmd.startswith(b'python:'):
313 elif cmd.startswith(b'python:'):
314 if cmd.count(b':') >= 2:
314 if cmd.count(b':') >= 2:
315 path, cmd = cmd[7:].rsplit(b':', 1)
315 path, cmd = cmd[7:].rsplit(b':', 1)
316 path = util.expandpath(path)
316 path = util.expandpath(path)
317 if repo:
317 if repo:
318 path = os.path.join(repo.root, path)
318 path = os.path.join(repo.root, path)
319 try:
319 try:
320 mod = extensions.loadpath(path, b'hghook.%s' % hname)
320 mod = extensions.loadpath(path, b'hghook.%s' % hname)
321 except Exception:
321 except Exception:
322 ui.write(_(b"loading %s hook failed:\n") % hname)
322 ui.write(_(b"loading %s hook failed:\n") % hname)
323 raise
323 raise
324 hookfn = getattr(mod, cmd)
324 hookfn = getattr(mod, cmd)
325 else:
325 else:
326 hookfn = cmd[7:].strip()
326 hookfn = cmd[7:].strip()
327 r, raised = pythonhook(
327 r, raised = pythonhook(
328 ui, repo, htype, hname, hookfn, args, throw
328 ui, repo, htype, hname, hookfn, args, throw
329 )
329 )
330 else:
330 else:
331 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
331 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
332 raised = False
332 raised = False
333
333
334 res[hname] = r, raised
334 res[hname] = r, raised
335
335
336 return res
336 return res
General Comments 0
You need to be logged in to leave comments. Login now