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