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