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