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