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