##// END OF EJS Templates
profile: drop maybeprofile...
marmoute -
r32788:eede022f default
parent child Browse files
Show More
@@ -1,1010 +1,1011 b''
1 # dispatch.py - command dispatching for mercurial
1 # dispatch.py - command dispatching for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-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, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import difflib
10 import difflib
11 import errno
11 import errno
12 import getopt
12 import getopt
13 import os
13 import os
14 import pdb
14 import pdb
15 import re
15 import re
16 import signal
16 import signal
17 import sys
17 import sys
18 import time
18 import time
19 import traceback
19 import traceback
20
20
21
21
22 from .i18n import _
22 from .i18n import _
23
23
24 from . import (
24 from . import (
25 cmdutil,
25 cmdutil,
26 color,
26 color,
27 commands,
27 commands,
28 demandimport,
28 demandimport,
29 encoding,
29 encoding,
30 error,
30 error,
31 extensions,
31 extensions,
32 fancyopts,
32 fancyopts,
33 fileset,
33 fileset,
34 help,
34 help,
35 hg,
35 hg,
36 hook,
36 hook,
37 profiling,
37 profiling,
38 pycompat,
38 pycompat,
39 revset,
39 revset,
40 scmutil,
40 scmutil,
41 templatefilters,
41 templatefilters,
42 templatekw,
42 templatekw,
43 templater,
43 templater,
44 ui as uimod,
44 ui as uimod,
45 util,
45 util,
46 )
46 )
47
47
48 class request(object):
48 class request(object):
49 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
49 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
50 ferr=None, prereposetups=None):
50 ferr=None, prereposetups=None):
51 self.args = args
51 self.args = args
52 self.ui = ui
52 self.ui = ui
53 self.repo = repo
53 self.repo = repo
54
54
55 # input/output/error streams
55 # input/output/error streams
56 self.fin = fin
56 self.fin = fin
57 self.fout = fout
57 self.fout = fout
58 self.ferr = ferr
58 self.ferr = ferr
59
59
60 # reposetups which run before extensions, useful for chg to pre-fill
60 # reposetups which run before extensions, useful for chg to pre-fill
61 # low-level repo state (for example, changelog) before extensions.
61 # low-level repo state (for example, changelog) before extensions.
62 self.prereposetups = prereposetups or []
62 self.prereposetups = prereposetups or []
63
63
64 def _runexithandlers(self):
64 def _runexithandlers(self):
65 exc = None
65 exc = None
66 handlers = self.ui._exithandlers
66 handlers = self.ui._exithandlers
67 try:
67 try:
68 while handlers:
68 while handlers:
69 func, args, kwargs = handlers.pop()
69 func, args, kwargs = handlers.pop()
70 try:
70 try:
71 func(*args, **kwargs)
71 func(*args, **kwargs)
72 except: # re-raises below
72 except: # re-raises below
73 if exc is None:
73 if exc is None:
74 exc = sys.exc_info()[1]
74 exc = sys.exc_info()[1]
75 self.ui.warn(('error in exit handlers:\n'))
75 self.ui.warn(('error in exit handlers:\n'))
76 self.ui.traceback(force=True)
76 self.ui.traceback(force=True)
77 finally:
77 finally:
78 if exc is not None:
78 if exc is not None:
79 raise exc
79 raise exc
80
80
81 def run():
81 def run():
82 "run the command in sys.argv"
82 "run the command in sys.argv"
83 req = request(pycompat.sysargv[1:])
83 req = request(pycompat.sysargv[1:])
84 err = None
84 err = None
85 try:
85 try:
86 status = (dispatch(req) or 0) & 255
86 status = (dispatch(req) or 0) & 255
87 except error.StdioError as err:
87 except error.StdioError as err:
88 status = -1
88 status = -1
89 if util.safehasattr(req.ui, 'fout'):
89 if util.safehasattr(req.ui, 'fout'):
90 try:
90 try:
91 req.ui.fout.flush()
91 req.ui.fout.flush()
92 except IOError as err:
92 except IOError as err:
93 status = -1
93 status = -1
94 if util.safehasattr(req.ui, 'ferr'):
94 if util.safehasattr(req.ui, 'ferr'):
95 if err is not None and err.errno != errno.EPIPE:
95 if err is not None and err.errno != errno.EPIPE:
96 req.ui.ferr.write('abort: %s\n' % err.strerror)
96 req.ui.ferr.write('abort: %s\n' % err.strerror)
97 req.ui.ferr.flush()
97 req.ui.ferr.flush()
98 sys.exit(status & 255)
98 sys.exit(status & 255)
99
99
100 def _getsimilar(symbols, value):
100 def _getsimilar(symbols, value):
101 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
101 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
102 # The cutoff for similarity here is pretty arbitrary. It should
102 # The cutoff for similarity here is pretty arbitrary. It should
103 # probably be investigated and tweaked.
103 # probably be investigated and tweaked.
104 return [s for s in symbols if sim(s) > 0.6]
104 return [s for s in symbols if sim(s) > 0.6]
105
105
106 def _reportsimilar(write, similar):
106 def _reportsimilar(write, similar):
107 if len(similar) == 1:
107 if len(similar) == 1:
108 write(_("(did you mean %s?)\n") % similar[0])
108 write(_("(did you mean %s?)\n") % similar[0])
109 elif similar:
109 elif similar:
110 ss = ", ".join(sorted(similar))
110 ss = ", ".join(sorted(similar))
111 write(_("(did you mean one of %s?)\n") % ss)
111 write(_("(did you mean one of %s?)\n") % ss)
112
112
113 def _formatparse(write, inst):
113 def _formatparse(write, inst):
114 similar = []
114 similar = []
115 if isinstance(inst, error.UnknownIdentifier):
115 if isinstance(inst, error.UnknownIdentifier):
116 # make sure to check fileset first, as revset can invoke fileset
116 # make sure to check fileset first, as revset can invoke fileset
117 similar = _getsimilar(inst.symbols, inst.function)
117 similar = _getsimilar(inst.symbols, inst.function)
118 if len(inst.args) > 1:
118 if len(inst.args) > 1:
119 write(_("hg: parse error at %s: %s\n") %
119 write(_("hg: parse error at %s: %s\n") %
120 (inst.args[1], inst.args[0]))
120 (inst.args[1], inst.args[0]))
121 if (inst.args[0][0] == ' '):
121 if (inst.args[0][0] == ' '):
122 write(_("unexpected leading whitespace\n"))
122 write(_("unexpected leading whitespace\n"))
123 else:
123 else:
124 write(_("hg: parse error: %s\n") % inst.args[0])
124 write(_("hg: parse error: %s\n") % inst.args[0])
125 _reportsimilar(write, similar)
125 _reportsimilar(write, similar)
126 if inst.hint:
126 if inst.hint:
127 write(_("(%s)\n") % inst.hint)
127 write(_("(%s)\n") % inst.hint)
128
128
129 def _formatargs(args):
129 def _formatargs(args):
130 return ' '.join(util.shellquote(a) for a in args)
130 return ' '.join(util.shellquote(a) for a in args)
131
131
132 def dispatch(req):
132 def dispatch(req):
133 "run the command specified in req.args"
133 "run the command specified in req.args"
134 if req.ferr:
134 if req.ferr:
135 ferr = req.ferr
135 ferr = req.ferr
136 elif req.ui:
136 elif req.ui:
137 ferr = req.ui.ferr
137 ferr = req.ui.ferr
138 else:
138 else:
139 ferr = util.stderr
139 ferr = util.stderr
140
140
141 try:
141 try:
142 if not req.ui:
142 if not req.ui:
143 req.ui = uimod.ui.load()
143 req.ui = uimod.ui.load()
144 if '--traceback' in req.args:
144 if '--traceback' in req.args:
145 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
145 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
146
146
147 # set ui streams from the request
147 # set ui streams from the request
148 if req.fin:
148 if req.fin:
149 req.ui.fin = req.fin
149 req.ui.fin = req.fin
150 if req.fout:
150 if req.fout:
151 req.ui.fout = req.fout
151 req.ui.fout = req.fout
152 if req.ferr:
152 if req.ferr:
153 req.ui.ferr = req.ferr
153 req.ui.ferr = req.ferr
154 except error.Abort as inst:
154 except error.Abort as inst:
155 ferr.write(_("abort: %s\n") % inst)
155 ferr.write(_("abort: %s\n") % inst)
156 if inst.hint:
156 if inst.hint:
157 ferr.write(_("(%s)\n") % inst.hint)
157 ferr.write(_("(%s)\n") % inst.hint)
158 return -1
158 return -1
159 except error.ParseError as inst:
159 except error.ParseError as inst:
160 _formatparse(ferr.write, inst)
160 _formatparse(ferr.write, inst)
161 return -1
161 return -1
162
162
163 msg = _formatargs(req.args)
163 msg = _formatargs(req.args)
164 starttime = util.timer()
164 starttime = util.timer()
165 ret = None
165 ret = None
166 try:
166 try:
167 ret = _runcatch(req)
167 ret = _runcatch(req)
168 except error.ProgrammingError as inst:
168 except error.ProgrammingError as inst:
169 req.ui.warn(_('** ProgrammingError: %s\n') % inst)
169 req.ui.warn(_('** ProgrammingError: %s\n') % inst)
170 if inst.hint:
170 if inst.hint:
171 req.ui.warn(_('** (%s)\n') % inst.hint)
171 req.ui.warn(_('** (%s)\n') % inst.hint)
172 raise
172 raise
173 except KeyboardInterrupt as inst:
173 except KeyboardInterrupt as inst:
174 try:
174 try:
175 if isinstance(inst, error.SignalInterrupt):
175 if isinstance(inst, error.SignalInterrupt):
176 msg = _("killed!\n")
176 msg = _("killed!\n")
177 else:
177 else:
178 msg = _("interrupted!\n")
178 msg = _("interrupted!\n")
179 req.ui.warn(msg)
179 req.ui.warn(msg)
180 except error.SignalInterrupt:
180 except error.SignalInterrupt:
181 # maybe pager would quit without consuming all the output, and
181 # maybe pager would quit without consuming all the output, and
182 # SIGPIPE was raised. we cannot print anything in this case.
182 # SIGPIPE was raised. we cannot print anything in this case.
183 pass
183 pass
184 except IOError as inst:
184 except IOError as inst:
185 if inst.errno != errno.EPIPE:
185 if inst.errno != errno.EPIPE:
186 raise
186 raise
187 ret = -1
187 ret = -1
188 finally:
188 finally:
189 duration = util.timer() - starttime
189 duration = util.timer() - starttime
190 req.ui.flush()
190 req.ui.flush()
191 if req.ui.logblockedtimes:
191 if req.ui.logblockedtimes:
192 req.ui._blockedtimes['command_duration'] = duration * 1000
192 req.ui._blockedtimes['command_duration'] = duration * 1000
193 req.ui.log('uiblocked', 'ui blocked ms', **req.ui._blockedtimes)
193 req.ui.log('uiblocked', 'ui blocked ms', **req.ui._blockedtimes)
194 req.ui.log("commandfinish", "%s exited %d after %0.2f seconds\n",
194 req.ui.log("commandfinish", "%s exited %d after %0.2f seconds\n",
195 msg, ret or 0, duration)
195 msg, ret or 0, duration)
196 try:
196 try:
197 req._runexithandlers()
197 req._runexithandlers()
198 except: # exiting, so no re-raises
198 except: # exiting, so no re-raises
199 ret = ret or -1
199 ret = ret or -1
200 return ret
200 return ret
201
201
202 def _runcatch(req):
202 def _runcatch(req):
203 def catchterm(*args):
203 def catchterm(*args):
204 raise error.SignalInterrupt
204 raise error.SignalInterrupt
205
205
206 ui = req.ui
206 ui = req.ui
207 try:
207 try:
208 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
208 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
209 num = getattr(signal, name, None)
209 num = getattr(signal, name, None)
210 if num:
210 if num:
211 signal.signal(num, catchterm)
211 signal.signal(num, catchterm)
212 except ValueError:
212 except ValueError:
213 pass # happens if called in a thread
213 pass # happens if called in a thread
214
214
215 def _runcatchfunc():
215 def _runcatchfunc():
216 realcmd = None
216 realcmd = None
217 try:
217 try:
218 cmdargs = fancyopts.fancyopts(req.args[:], commands.globalopts, {})
218 cmdargs = fancyopts.fancyopts(req.args[:], commands.globalopts, {})
219 cmd = cmdargs[0]
219 cmd = cmdargs[0]
220 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
220 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
221 realcmd = aliases[0]
221 realcmd = aliases[0]
222 except (error.UnknownCommand, error.AmbiguousCommand,
222 except (error.UnknownCommand, error.AmbiguousCommand,
223 IndexError, getopt.GetoptError):
223 IndexError, getopt.GetoptError):
224 # Don't handle this here. We know the command is
224 # Don't handle this here. We know the command is
225 # invalid, but all we're worried about for now is that
225 # invalid, but all we're worried about for now is that
226 # it's not a command that server operators expect to
226 # it's not a command that server operators expect to
227 # be safe to offer to users in a sandbox.
227 # be safe to offer to users in a sandbox.
228 pass
228 pass
229 if realcmd == 'serve' and '--stdio' in cmdargs:
229 if realcmd == 'serve' and '--stdio' in cmdargs:
230 # We want to constrain 'hg serve --stdio' instances pretty
230 # We want to constrain 'hg serve --stdio' instances pretty
231 # closely, as many shared-ssh access tools want to grant
231 # closely, as many shared-ssh access tools want to grant
232 # access to run *only* 'hg -R $repo serve --stdio'. We
232 # access to run *only* 'hg -R $repo serve --stdio'. We
233 # restrict to exactly that set of arguments, and prohibit
233 # restrict to exactly that set of arguments, and prohibit
234 # any repo name that starts with '--' to prevent
234 # any repo name that starts with '--' to prevent
235 # shenanigans wherein a user does something like pass
235 # shenanigans wherein a user does something like pass
236 # --debugger or --config=ui.debugger=1 as a repo
236 # --debugger or --config=ui.debugger=1 as a repo
237 # name. This used to actually run the debugger.
237 # name. This used to actually run the debugger.
238 if (len(req.args) != 4 or
238 if (len(req.args) != 4 or
239 req.args[0] != '-R' or
239 req.args[0] != '-R' or
240 req.args[1].startswith('--') or
240 req.args[1].startswith('--') or
241 req.args[2] != 'serve' or
241 req.args[2] != 'serve' or
242 req.args[3] != '--stdio'):
242 req.args[3] != '--stdio'):
243 raise error.Abort(
243 raise error.Abort(
244 _('potentially unsafe serve --stdio invocation: %r') %
244 _('potentially unsafe serve --stdio invocation: %r') %
245 (req.args,))
245 (req.args,))
246
246
247 try:
247 try:
248 debugger = 'pdb'
248 debugger = 'pdb'
249 debugtrace = {
249 debugtrace = {
250 'pdb' : pdb.set_trace
250 'pdb' : pdb.set_trace
251 }
251 }
252 debugmortem = {
252 debugmortem = {
253 'pdb' : pdb.post_mortem
253 'pdb' : pdb.post_mortem
254 }
254 }
255
255
256 # read --config before doing anything else
256 # read --config before doing anything else
257 # (e.g. to change trust settings for reading .hg/hgrc)
257 # (e.g. to change trust settings for reading .hg/hgrc)
258 cfgs = _parseconfig(req.ui, _earlygetopt(['--config'], req.args))
258 cfgs = _parseconfig(req.ui, _earlygetopt(['--config'], req.args))
259
259
260 if req.repo:
260 if req.repo:
261 # copy configs that were passed on the cmdline (--config) to
261 # copy configs that were passed on the cmdline (--config) to
262 # the repo ui
262 # the repo ui
263 for sec, name, val in cfgs:
263 for sec, name, val in cfgs:
264 req.repo.ui.setconfig(sec, name, val, source='--config')
264 req.repo.ui.setconfig(sec, name, val, source='--config')
265
265
266 # developer config: ui.debugger
266 # developer config: ui.debugger
267 debugger = ui.config("ui", "debugger")
267 debugger = ui.config("ui", "debugger")
268 debugmod = pdb
268 debugmod = pdb
269 if not debugger or ui.plain():
269 if not debugger or ui.plain():
270 # if we are in HGPLAIN mode, then disable custom debugging
270 # if we are in HGPLAIN mode, then disable custom debugging
271 debugger = 'pdb'
271 debugger = 'pdb'
272 elif '--debugger' in req.args:
272 elif '--debugger' in req.args:
273 # This import can be slow for fancy debuggers, so only
273 # This import can be slow for fancy debuggers, so only
274 # do it when absolutely necessary, i.e. when actual
274 # do it when absolutely necessary, i.e. when actual
275 # debugging has been requested
275 # debugging has been requested
276 with demandimport.deactivated():
276 with demandimport.deactivated():
277 try:
277 try:
278 debugmod = __import__(debugger)
278 debugmod = __import__(debugger)
279 except ImportError:
279 except ImportError:
280 pass # Leave debugmod = pdb
280 pass # Leave debugmod = pdb
281
281
282 debugtrace[debugger] = debugmod.set_trace
282 debugtrace[debugger] = debugmod.set_trace
283 debugmortem[debugger] = debugmod.post_mortem
283 debugmortem[debugger] = debugmod.post_mortem
284
284
285 # enter the debugger before command execution
285 # enter the debugger before command execution
286 if '--debugger' in req.args:
286 if '--debugger' in req.args:
287 ui.warn(_("entering debugger - "
287 ui.warn(_("entering debugger - "
288 "type c to continue starting hg or h for help\n"))
288 "type c to continue starting hg or h for help\n"))
289
289
290 if (debugger != 'pdb' and
290 if (debugger != 'pdb' and
291 debugtrace[debugger] == debugtrace['pdb']):
291 debugtrace[debugger] == debugtrace['pdb']):
292 ui.warn(_("%s debugger specified "
292 ui.warn(_("%s debugger specified "
293 "but its module was not found\n") % debugger)
293 "but its module was not found\n") % debugger)
294 with demandimport.deactivated():
294 with demandimport.deactivated():
295 debugtrace[debugger]()
295 debugtrace[debugger]()
296 try:
296 try:
297 return _dispatch(req)
297 return _dispatch(req)
298 finally:
298 finally:
299 ui.flush()
299 ui.flush()
300 except: # re-raises
300 except: # re-raises
301 # enter the debugger when we hit an exception
301 # enter the debugger when we hit an exception
302 if '--debugger' in req.args:
302 if '--debugger' in req.args:
303 traceback.print_exc()
303 traceback.print_exc()
304 debugmortem[debugger](sys.exc_info()[2])
304 debugmortem[debugger](sys.exc_info()[2])
305 raise
305 raise
306
306
307 return _callcatch(ui, _runcatchfunc)
307 return _callcatch(ui, _runcatchfunc)
308
308
309 def _callcatch(ui, func):
309 def _callcatch(ui, func):
310 """like scmutil.callcatch but handles more high-level exceptions about
310 """like scmutil.callcatch but handles more high-level exceptions about
311 config parsing and commands. besides, use handlecommandexception to handle
311 config parsing and commands. besides, use handlecommandexception to handle
312 uncaught exceptions.
312 uncaught exceptions.
313 """
313 """
314 try:
314 try:
315 return scmutil.callcatch(ui, func)
315 return scmutil.callcatch(ui, func)
316 except error.AmbiguousCommand as inst:
316 except error.AmbiguousCommand as inst:
317 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
317 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
318 (inst.args[0], " ".join(inst.args[1])))
318 (inst.args[0], " ".join(inst.args[1])))
319 except error.CommandError as inst:
319 except error.CommandError as inst:
320 if inst.args[0]:
320 if inst.args[0]:
321 ui.pager('help')
321 ui.pager('help')
322 msgbytes = pycompat.bytestr(inst.args[1])
322 msgbytes = pycompat.bytestr(inst.args[1])
323 ui.warn(_("hg %s: %s\n") % (inst.args[0], msgbytes))
323 ui.warn(_("hg %s: %s\n") % (inst.args[0], msgbytes))
324 commands.help_(ui, inst.args[0], full=False, command=True)
324 commands.help_(ui, inst.args[0], full=False, command=True)
325 else:
325 else:
326 ui.pager('help')
326 ui.pager('help')
327 ui.warn(_("hg: %s\n") % inst.args[1])
327 ui.warn(_("hg: %s\n") % inst.args[1])
328 commands.help_(ui, 'shortlist')
328 commands.help_(ui, 'shortlist')
329 except error.ParseError as inst:
329 except error.ParseError as inst:
330 _formatparse(ui.warn, inst)
330 _formatparse(ui.warn, inst)
331 return -1
331 return -1
332 except error.UnknownCommand as inst:
332 except error.UnknownCommand as inst:
333 nocmdmsg = _("hg: unknown command '%s'\n") % inst.args[0]
333 nocmdmsg = _("hg: unknown command '%s'\n") % inst.args[0]
334 try:
334 try:
335 # check if the command is in a disabled extension
335 # check if the command is in a disabled extension
336 # (but don't check for extensions themselves)
336 # (but don't check for extensions themselves)
337 formatted = help.formattedhelp(ui, commands, inst.args[0],
337 formatted = help.formattedhelp(ui, commands, inst.args[0],
338 unknowncmd=True)
338 unknowncmd=True)
339 ui.warn(nocmdmsg)
339 ui.warn(nocmdmsg)
340 ui.write(formatted)
340 ui.write(formatted)
341 except (error.UnknownCommand, error.Abort):
341 except (error.UnknownCommand, error.Abort):
342 suggested = False
342 suggested = False
343 if len(inst.args) == 2:
343 if len(inst.args) == 2:
344 sim = _getsimilar(inst.args[1], inst.args[0])
344 sim = _getsimilar(inst.args[1], inst.args[0])
345 if sim:
345 if sim:
346 ui.warn(nocmdmsg)
346 ui.warn(nocmdmsg)
347 _reportsimilar(ui.warn, sim)
347 _reportsimilar(ui.warn, sim)
348 suggested = True
348 suggested = True
349 if not suggested:
349 if not suggested:
350 ui.pager('help')
350 ui.pager('help')
351 ui.warn(nocmdmsg)
351 ui.warn(nocmdmsg)
352 commands.help_(ui, 'shortlist')
352 commands.help_(ui, 'shortlist')
353 except IOError:
353 except IOError:
354 raise
354 raise
355 except KeyboardInterrupt:
355 except KeyboardInterrupt:
356 raise
356 raise
357 except: # probably re-raises
357 except: # probably re-raises
358 if not handlecommandexception(ui):
358 if not handlecommandexception(ui):
359 raise
359 raise
360
360
361 return -1
361 return -1
362
362
363 def aliasargs(fn, givenargs):
363 def aliasargs(fn, givenargs):
364 args = getattr(fn, 'args', [])
364 args = getattr(fn, 'args', [])
365 if args:
365 if args:
366 cmd = ' '.join(map(util.shellquote, args))
366 cmd = ' '.join(map(util.shellquote, args))
367
367
368 nums = []
368 nums = []
369 def replacer(m):
369 def replacer(m):
370 num = int(m.group(1)) - 1
370 num = int(m.group(1)) - 1
371 nums.append(num)
371 nums.append(num)
372 if num < len(givenargs):
372 if num < len(givenargs):
373 return givenargs[num]
373 return givenargs[num]
374 raise error.Abort(_('too few arguments for command alias'))
374 raise error.Abort(_('too few arguments for command alias'))
375 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
375 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
376 givenargs = [x for i, x in enumerate(givenargs)
376 givenargs = [x for i, x in enumerate(givenargs)
377 if i not in nums]
377 if i not in nums]
378 args = pycompat.shlexsplit(cmd)
378 args = pycompat.shlexsplit(cmd)
379 return args + givenargs
379 return args + givenargs
380
380
381 def aliasinterpolate(name, args, cmd):
381 def aliasinterpolate(name, args, cmd):
382 '''interpolate args into cmd for shell aliases
382 '''interpolate args into cmd for shell aliases
383
383
384 This also handles $0, $@ and "$@".
384 This also handles $0, $@ and "$@".
385 '''
385 '''
386 # util.interpolate can't deal with "$@" (with quotes) because it's only
386 # util.interpolate can't deal with "$@" (with quotes) because it's only
387 # built to match prefix + patterns.
387 # built to match prefix + patterns.
388 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
388 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
389 replacemap['$0'] = name
389 replacemap['$0'] = name
390 replacemap['$$'] = '$'
390 replacemap['$$'] = '$'
391 replacemap['$@'] = ' '.join(args)
391 replacemap['$@'] = ' '.join(args)
392 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
392 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
393 # parameters, separated out into words. Emulate the same behavior here by
393 # parameters, separated out into words. Emulate the same behavior here by
394 # quoting the arguments individually. POSIX shells will then typically
394 # quoting the arguments individually. POSIX shells will then typically
395 # tokenize each argument into exactly one word.
395 # tokenize each argument into exactly one word.
396 replacemap['"$@"'] = ' '.join(util.shellquote(arg) for arg in args)
396 replacemap['"$@"'] = ' '.join(util.shellquote(arg) for arg in args)
397 # escape '\$' for regex
397 # escape '\$' for regex
398 regex = '|'.join(replacemap.keys()).replace('$', r'\$')
398 regex = '|'.join(replacemap.keys()).replace('$', r'\$')
399 r = re.compile(regex)
399 r = re.compile(regex)
400 return r.sub(lambda x: replacemap[x.group()], cmd)
400 return r.sub(lambda x: replacemap[x.group()], cmd)
401
401
402 class cmdalias(object):
402 class cmdalias(object):
403 def __init__(self, name, definition, cmdtable, source):
403 def __init__(self, name, definition, cmdtable, source):
404 self.name = self.cmd = name
404 self.name = self.cmd = name
405 self.cmdname = ''
405 self.cmdname = ''
406 self.definition = definition
406 self.definition = definition
407 self.fn = None
407 self.fn = None
408 self.givenargs = []
408 self.givenargs = []
409 self.opts = []
409 self.opts = []
410 self.help = ''
410 self.help = ''
411 self.badalias = None
411 self.badalias = None
412 self.unknowncmd = False
412 self.unknowncmd = False
413 self.source = source
413 self.source = source
414
414
415 try:
415 try:
416 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
416 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
417 for alias, e in cmdtable.iteritems():
417 for alias, e in cmdtable.iteritems():
418 if e is entry:
418 if e is entry:
419 self.cmd = alias
419 self.cmd = alias
420 break
420 break
421 self.shadows = True
421 self.shadows = True
422 except error.UnknownCommand:
422 except error.UnknownCommand:
423 self.shadows = False
423 self.shadows = False
424
424
425 if not self.definition:
425 if not self.definition:
426 self.badalias = _("no definition for alias '%s'") % self.name
426 self.badalias = _("no definition for alias '%s'") % self.name
427 return
427 return
428
428
429 if self.definition.startswith('!'):
429 if self.definition.startswith('!'):
430 self.shell = True
430 self.shell = True
431 def fn(ui, *args):
431 def fn(ui, *args):
432 env = {'HG_ARGS': ' '.join((self.name,) + args)}
432 env = {'HG_ARGS': ' '.join((self.name,) + args)}
433 def _checkvar(m):
433 def _checkvar(m):
434 if m.groups()[0] == '$':
434 if m.groups()[0] == '$':
435 return m.group()
435 return m.group()
436 elif int(m.groups()[0]) <= len(args):
436 elif int(m.groups()[0]) <= len(args):
437 return m.group()
437 return m.group()
438 else:
438 else:
439 ui.debug("No argument found for substitution "
439 ui.debug("No argument found for substitution "
440 "of %i variable in alias '%s' definition."
440 "of %i variable in alias '%s' definition."
441 % (int(m.groups()[0]), self.name))
441 % (int(m.groups()[0]), self.name))
442 return ''
442 return ''
443 cmd = re.sub(r'\$(\d+|\$)', _checkvar, self.definition[1:])
443 cmd = re.sub(r'\$(\d+|\$)', _checkvar, self.definition[1:])
444 cmd = aliasinterpolate(self.name, args, cmd)
444 cmd = aliasinterpolate(self.name, args, cmd)
445 return ui.system(cmd, environ=env,
445 return ui.system(cmd, environ=env,
446 blockedtag='alias_%s' % self.name)
446 blockedtag='alias_%s' % self.name)
447 self.fn = fn
447 self.fn = fn
448 return
448 return
449
449
450 try:
450 try:
451 args = pycompat.shlexsplit(self.definition)
451 args = pycompat.shlexsplit(self.definition)
452 except ValueError as inst:
452 except ValueError as inst:
453 self.badalias = (_("error in definition for alias '%s': %s")
453 self.badalias = (_("error in definition for alias '%s': %s")
454 % (self.name, inst))
454 % (self.name, inst))
455 return
455 return
456 self.cmdname = cmd = args.pop(0)
456 self.cmdname = cmd = args.pop(0)
457 self.givenargs = args
457 self.givenargs = args
458
458
459 for invalidarg in ("--cwd", "-R", "--repository", "--repo", "--config"):
459 for invalidarg in ("--cwd", "-R", "--repository", "--repo", "--config"):
460 if _earlygetopt([invalidarg], args):
460 if _earlygetopt([invalidarg], args):
461 self.badalias = (_("error in definition for alias '%s': %s may "
461 self.badalias = (_("error in definition for alias '%s': %s may "
462 "only be given on the command line")
462 "only be given on the command line")
463 % (self.name, invalidarg))
463 % (self.name, invalidarg))
464 return
464 return
465
465
466 try:
466 try:
467 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
467 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
468 if len(tableentry) > 2:
468 if len(tableentry) > 2:
469 self.fn, self.opts, self.help = tableentry
469 self.fn, self.opts, self.help = tableentry
470 else:
470 else:
471 self.fn, self.opts = tableentry
471 self.fn, self.opts = tableentry
472
472
473 if self.help.startswith("hg " + cmd):
473 if self.help.startswith("hg " + cmd):
474 # drop prefix in old-style help lines so hg shows the alias
474 # drop prefix in old-style help lines so hg shows the alias
475 self.help = self.help[4 + len(cmd):]
475 self.help = self.help[4 + len(cmd):]
476 self.__doc__ = self.fn.__doc__
476 self.__doc__ = self.fn.__doc__
477
477
478 except error.UnknownCommand:
478 except error.UnknownCommand:
479 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
479 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
480 % (self.name, cmd))
480 % (self.name, cmd))
481 self.unknowncmd = True
481 self.unknowncmd = True
482 except error.AmbiguousCommand:
482 except error.AmbiguousCommand:
483 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
483 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
484 % (self.name, cmd))
484 % (self.name, cmd))
485
485
486 @property
486 @property
487 def args(self):
487 def args(self):
488 args = pycompat.maplist(util.expandpath, self.givenargs)
488 args = pycompat.maplist(util.expandpath, self.givenargs)
489 return aliasargs(self.fn, args)
489 return aliasargs(self.fn, args)
490
490
491 def __getattr__(self, name):
491 def __getattr__(self, name):
492 adefaults = {r'norepo': True,
492 adefaults = {r'norepo': True,
493 r'optionalrepo': False, r'inferrepo': False}
493 r'optionalrepo': False, r'inferrepo': False}
494 if name not in adefaults:
494 if name not in adefaults:
495 raise AttributeError(name)
495 raise AttributeError(name)
496 if self.badalias or util.safehasattr(self, 'shell'):
496 if self.badalias or util.safehasattr(self, 'shell'):
497 return adefaults[name]
497 return adefaults[name]
498 return getattr(self.fn, name)
498 return getattr(self.fn, name)
499
499
500 def __call__(self, ui, *args, **opts):
500 def __call__(self, ui, *args, **opts):
501 if self.badalias:
501 if self.badalias:
502 hint = None
502 hint = None
503 if self.unknowncmd:
503 if self.unknowncmd:
504 try:
504 try:
505 # check if the command is in a disabled extension
505 # check if the command is in a disabled extension
506 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
506 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
507 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
507 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
508 except error.UnknownCommand:
508 except error.UnknownCommand:
509 pass
509 pass
510 raise error.Abort(self.badalias, hint=hint)
510 raise error.Abort(self.badalias, hint=hint)
511 if self.shadows:
511 if self.shadows:
512 ui.debug("alias '%s' shadows command '%s'\n" %
512 ui.debug("alias '%s' shadows command '%s'\n" %
513 (self.name, self.cmdname))
513 (self.name, self.cmdname))
514
514
515 ui.log('commandalias', "alias '%s' expands to '%s'\n",
515 ui.log('commandalias', "alias '%s' expands to '%s'\n",
516 self.name, self.definition)
516 self.name, self.definition)
517 if util.safehasattr(self, 'shell'):
517 if util.safehasattr(self, 'shell'):
518 return self.fn(ui, *args, **opts)
518 return self.fn(ui, *args, **opts)
519 else:
519 else:
520 try:
520 try:
521 return util.checksignature(self.fn)(ui, *args, **opts)
521 return util.checksignature(self.fn)(ui, *args, **opts)
522 except error.SignatureError:
522 except error.SignatureError:
523 args = ' '.join([self.cmdname] + self.args)
523 args = ' '.join([self.cmdname] + self.args)
524 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
524 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
525 raise
525 raise
526
526
527 def addaliases(ui, cmdtable):
527 def addaliases(ui, cmdtable):
528 # aliases are processed after extensions have been loaded, so they
528 # aliases are processed after extensions have been loaded, so they
529 # may use extension commands. Aliases can also use other alias definitions,
529 # may use extension commands. Aliases can also use other alias definitions,
530 # but only if they have been defined prior to the current definition.
530 # but only if they have been defined prior to the current definition.
531 for alias, definition in ui.configitems('alias'):
531 for alias, definition in ui.configitems('alias'):
532 source = ui.configsource('alias', alias)
532 source = ui.configsource('alias', alias)
533 aliasdef = cmdalias(alias, definition, cmdtable, source)
533 aliasdef = cmdalias(alias, definition, cmdtable, source)
534
534
535 try:
535 try:
536 olddef = cmdtable[aliasdef.cmd][0]
536 olddef = cmdtable[aliasdef.cmd][0]
537 if olddef.definition == aliasdef.definition:
537 if olddef.definition == aliasdef.definition:
538 continue
538 continue
539 except (KeyError, AttributeError):
539 except (KeyError, AttributeError):
540 # definition might not exist or it might not be a cmdalias
540 # definition might not exist or it might not be a cmdalias
541 pass
541 pass
542
542
543 cmdtable[aliasdef.name] = (aliasdef, aliasdef.opts, aliasdef.help)
543 cmdtable[aliasdef.name] = (aliasdef, aliasdef.opts, aliasdef.help)
544
544
545 def _parse(ui, args):
545 def _parse(ui, args):
546 options = {}
546 options = {}
547 cmdoptions = {}
547 cmdoptions = {}
548
548
549 try:
549 try:
550 args = fancyopts.fancyopts(args, commands.globalopts, options)
550 args = fancyopts.fancyopts(args, commands.globalopts, options)
551 except getopt.GetoptError as inst:
551 except getopt.GetoptError as inst:
552 raise error.CommandError(None, inst)
552 raise error.CommandError(None, inst)
553
553
554 if args:
554 if args:
555 cmd, args = args[0], args[1:]
555 cmd, args = args[0], args[1:]
556 aliases, entry = cmdutil.findcmd(cmd, commands.table,
556 aliases, entry = cmdutil.findcmd(cmd, commands.table,
557 ui.configbool("ui", "strict"))
557 ui.configbool("ui", "strict"))
558 cmd = aliases[0]
558 cmd = aliases[0]
559 args = aliasargs(entry[0], args)
559 args = aliasargs(entry[0], args)
560 defaults = ui.config("defaults", cmd)
560 defaults = ui.config("defaults", cmd)
561 if defaults:
561 if defaults:
562 args = pycompat.maplist(
562 args = pycompat.maplist(
563 util.expandpath, pycompat.shlexsplit(defaults)) + args
563 util.expandpath, pycompat.shlexsplit(defaults)) + args
564 c = list(entry[1])
564 c = list(entry[1])
565 else:
565 else:
566 cmd = None
566 cmd = None
567 c = []
567 c = []
568
568
569 # combine global options into local
569 # combine global options into local
570 for o in commands.globalopts:
570 for o in commands.globalopts:
571 c.append((o[0], o[1], options[o[1]], o[3]))
571 c.append((o[0], o[1], options[o[1]], o[3]))
572
572
573 try:
573 try:
574 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
574 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
575 except getopt.GetoptError as inst:
575 except getopt.GetoptError as inst:
576 raise error.CommandError(cmd, inst)
576 raise error.CommandError(cmd, inst)
577
577
578 # separate global options back out
578 # separate global options back out
579 for o in commands.globalopts:
579 for o in commands.globalopts:
580 n = o[1]
580 n = o[1]
581 options[n] = cmdoptions[n]
581 options[n] = cmdoptions[n]
582 del cmdoptions[n]
582 del cmdoptions[n]
583
583
584 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
584 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
585
585
586 def _parseconfig(ui, config):
586 def _parseconfig(ui, config):
587 """parse the --config options from the command line"""
587 """parse the --config options from the command line"""
588 configs = []
588 configs = []
589
589
590 for cfg in config:
590 for cfg in config:
591 try:
591 try:
592 name, value = [cfgelem.strip()
592 name, value = [cfgelem.strip()
593 for cfgelem in cfg.split('=', 1)]
593 for cfgelem in cfg.split('=', 1)]
594 section, name = name.split('.', 1)
594 section, name = name.split('.', 1)
595 if not section or not name:
595 if not section or not name:
596 raise IndexError
596 raise IndexError
597 ui.setconfig(section, name, value, '--config')
597 ui.setconfig(section, name, value, '--config')
598 configs.append((section, name, value))
598 configs.append((section, name, value))
599 except (IndexError, ValueError):
599 except (IndexError, ValueError):
600 raise error.Abort(_('malformed --config option: %r '
600 raise error.Abort(_('malformed --config option: %r '
601 '(use --config section.name=value)') % cfg)
601 '(use --config section.name=value)') % cfg)
602
602
603 return configs
603 return configs
604
604
605 def _earlygetopt(aliases, args):
605 def _earlygetopt(aliases, args):
606 """Return list of values for an option (or aliases).
606 """Return list of values for an option (or aliases).
607
607
608 The values are listed in the order they appear in args.
608 The values are listed in the order they appear in args.
609 The options and values are removed from args.
609 The options and values are removed from args.
610
610
611 >>> args = ['x', '--cwd', 'foo', 'y']
611 >>> args = ['x', '--cwd', 'foo', 'y']
612 >>> _earlygetopt(['--cwd'], args), args
612 >>> _earlygetopt(['--cwd'], args), args
613 (['foo'], ['x', 'y'])
613 (['foo'], ['x', 'y'])
614
614
615 >>> args = ['x', '--cwd=bar', 'y']
615 >>> args = ['x', '--cwd=bar', 'y']
616 >>> _earlygetopt(['--cwd'], args), args
616 >>> _earlygetopt(['--cwd'], args), args
617 (['bar'], ['x', 'y'])
617 (['bar'], ['x', 'y'])
618
618
619 >>> args = ['x', '-R', 'foo', 'y']
619 >>> args = ['x', '-R', 'foo', 'y']
620 >>> _earlygetopt(['-R'], args), args
620 >>> _earlygetopt(['-R'], args), args
621 (['foo'], ['x', 'y'])
621 (['foo'], ['x', 'y'])
622
622
623 >>> args = ['x', '-Rbar', 'y']
623 >>> args = ['x', '-Rbar', 'y']
624 >>> _earlygetopt(['-R'], args), args
624 >>> _earlygetopt(['-R'], args), args
625 (['bar'], ['x', 'y'])
625 (['bar'], ['x', 'y'])
626 """
626 """
627 try:
627 try:
628 argcount = args.index("--")
628 argcount = args.index("--")
629 except ValueError:
629 except ValueError:
630 argcount = len(args)
630 argcount = len(args)
631 shortopts = [opt for opt in aliases if len(opt) == 2]
631 shortopts = [opt for opt in aliases if len(opt) == 2]
632 values = []
632 values = []
633 pos = 0
633 pos = 0
634 while pos < argcount:
634 while pos < argcount:
635 fullarg = arg = args[pos]
635 fullarg = arg = args[pos]
636 equals = arg.find('=')
636 equals = arg.find('=')
637 if equals > -1:
637 if equals > -1:
638 arg = arg[:equals]
638 arg = arg[:equals]
639 if arg in aliases:
639 if arg in aliases:
640 del args[pos]
640 del args[pos]
641 if equals > -1:
641 if equals > -1:
642 values.append(fullarg[equals + 1:])
642 values.append(fullarg[equals + 1:])
643 argcount -= 1
643 argcount -= 1
644 else:
644 else:
645 if pos + 1 >= argcount:
645 if pos + 1 >= argcount:
646 # ignore and let getopt report an error if there is no value
646 # ignore and let getopt report an error if there is no value
647 break
647 break
648 values.append(args.pop(pos))
648 values.append(args.pop(pos))
649 argcount -= 2
649 argcount -= 2
650 elif arg[:2] in shortopts:
650 elif arg[:2] in shortopts:
651 # short option can have no following space, e.g. hg log -Rfoo
651 # short option can have no following space, e.g. hg log -Rfoo
652 values.append(args.pop(pos)[2:])
652 values.append(args.pop(pos)[2:])
653 argcount -= 1
653 argcount -= 1
654 else:
654 else:
655 pos += 1
655 pos += 1
656 return values
656 return values
657
657
658 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
658 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
659 # run pre-hook, and abort if it fails
659 # run pre-hook, and abort if it fails
660 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
660 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
661 pats=cmdpats, opts=cmdoptions)
661 pats=cmdpats, opts=cmdoptions)
662 try:
662 try:
663 ret = _runcommand(ui, options, cmd, d)
663 ret = _runcommand(ui, options, cmd, d)
664 # run post-hook, passing command result
664 # run post-hook, passing command result
665 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
665 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
666 result=ret, pats=cmdpats, opts=cmdoptions)
666 result=ret, pats=cmdpats, opts=cmdoptions)
667 except Exception:
667 except Exception:
668 # run failure hook and re-raise
668 # run failure hook and re-raise
669 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
669 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
670 pats=cmdpats, opts=cmdoptions)
670 pats=cmdpats, opts=cmdoptions)
671 raise
671 raise
672 return ret
672 return ret
673
673
674 def _getlocal(ui, rpath, wd=None):
674 def _getlocal(ui, rpath, wd=None):
675 """Return (path, local ui object) for the given target path.
675 """Return (path, local ui object) for the given target path.
676
676
677 Takes paths in [cwd]/.hg/hgrc into account."
677 Takes paths in [cwd]/.hg/hgrc into account."
678 """
678 """
679 if wd is None:
679 if wd is None:
680 try:
680 try:
681 wd = pycompat.getcwd()
681 wd = pycompat.getcwd()
682 except OSError as e:
682 except OSError as e:
683 raise error.Abort(_("error getting current working directory: %s") %
683 raise error.Abort(_("error getting current working directory: %s") %
684 e.strerror)
684 e.strerror)
685 path = cmdutil.findrepo(wd) or ""
685 path = cmdutil.findrepo(wd) or ""
686 if not path:
686 if not path:
687 lui = ui
687 lui = ui
688 else:
688 else:
689 lui = ui.copy()
689 lui = ui.copy()
690 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
690 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
691
691
692 if rpath and rpath[-1]:
692 if rpath and rpath[-1]:
693 path = lui.expandpath(rpath[-1])
693 path = lui.expandpath(rpath[-1])
694 lui = ui.copy()
694 lui = ui.copy()
695 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
695 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
696
696
697 return path, lui
697 return path, lui
698
698
699 def _checkshellalias(lui, ui, args):
699 def _checkshellalias(lui, ui, args):
700 """Return the function to run the shell alias, if it is required"""
700 """Return the function to run the shell alias, if it is required"""
701 options = {}
701 options = {}
702
702
703 try:
703 try:
704 args = fancyopts.fancyopts(args, commands.globalopts, options)
704 args = fancyopts.fancyopts(args, commands.globalopts, options)
705 except getopt.GetoptError:
705 except getopt.GetoptError:
706 return
706 return
707
707
708 if not args:
708 if not args:
709 return
709 return
710
710
711 cmdtable = commands.table
711 cmdtable = commands.table
712
712
713 cmd = args[0]
713 cmd = args[0]
714 try:
714 try:
715 strict = ui.configbool("ui", "strict")
715 strict = ui.configbool("ui", "strict")
716 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
716 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
717 except (error.AmbiguousCommand, error.UnknownCommand):
717 except (error.AmbiguousCommand, error.UnknownCommand):
718 return
718 return
719
719
720 cmd = aliases[0]
720 cmd = aliases[0]
721 fn = entry[0]
721 fn = entry[0]
722
722
723 if cmd and util.safehasattr(fn, 'shell'):
723 if cmd and util.safehasattr(fn, 'shell'):
724 d = lambda: fn(ui, *args[1:])
724 d = lambda: fn(ui, *args[1:])
725 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
725 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
726 [], {})
726 [], {})
727
727
728 _loaded = set()
728 _loaded = set()
729
729
730 # list of (objname, loadermod, loadername) tuple:
730 # list of (objname, loadermod, loadername) tuple:
731 # - objname is the name of an object in extension module, from which
731 # - objname is the name of an object in extension module, from which
732 # extra information is loaded
732 # extra information is loaded
733 # - loadermod is the module where loader is placed
733 # - loadermod is the module where loader is placed
734 # - loadername is the name of the function, which takes (ui, extensionname,
734 # - loadername is the name of the function, which takes (ui, extensionname,
735 # extraobj) arguments
735 # extraobj) arguments
736 extraloaders = [
736 extraloaders = [
737 ('cmdtable', commands, 'loadcmdtable'),
737 ('cmdtable', commands, 'loadcmdtable'),
738 ('colortable', color, 'loadcolortable'),
738 ('colortable', color, 'loadcolortable'),
739 ('filesetpredicate', fileset, 'loadpredicate'),
739 ('filesetpredicate', fileset, 'loadpredicate'),
740 ('revsetpredicate', revset, 'loadpredicate'),
740 ('revsetpredicate', revset, 'loadpredicate'),
741 ('templatefilter', templatefilters, 'loadfilter'),
741 ('templatefilter', templatefilters, 'loadfilter'),
742 ('templatefunc', templater, 'loadfunction'),
742 ('templatefunc', templater, 'loadfunction'),
743 ('templatekeyword', templatekw, 'loadkeyword'),
743 ('templatekeyword', templatekw, 'loadkeyword'),
744 ]
744 ]
745
745
746 def _dispatch(req):
746 def _dispatch(req):
747 args = req.args
747 args = req.args
748 ui = req.ui
748 ui = req.ui
749
749
750 # check for cwd
750 # check for cwd
751 cwd = _earlygetopt(['--cwd'], args)
751 cwd = _earlygetopt(['--cwd'], args)
752 if cwd:
752 if cwd:
753 os.chdir(cwd[-1])
753 os.chdir(cwd[-1])
754
754
755 rpath = _earlygetopt(["-R", "--repository", "--repo"], args)
755 rpath = _earlygetopt(["-R", "--repository", "--repo"], args)
756 path, lui = _getlocal(ui, rpath)
756 path, lui = _getlocal(ui, rpath)
757
757
758 uis = {ui, lui}
758 uis = {ui, lui}
759
759
760 if req.repo:
760 if req.repo:
761 uis.add(req.repo.ui)
761 uis.add(req.repo.ui)
762
762
763 if '--profile' in args:
763 if '--profile' in args:
764 for ui_ in uis:
764 for ui_ in uis:
765 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
765 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
766
766
767 with profiling.maybeprofile(lui) as profiler:
767 profile = lui.configbool('profiling', 'enabled')
768 with profiling.profile(lui, enabled=profile) as profiler:
768 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
769 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
769 # reposetup. Programs like TortoiseHg will call _dispatch several
770 # reposetup. Programs like TortoiseHg will call _dispatch several
770 # times so we keep track of configured extensions in _loaded.
771 # times so we keep track of configured extensions in _loaded.
771 extensions.loadall(lui)
772 extensions.loadall(lui)
772 exts = [ext for ext in extensions.extensions() if ext[0] not in _loaded]
773 exts = [ext for ext in extensions.extensions() if ext[0] not in _loaded]
773 # Propagate any changes to lui.__class__ by extensions
774 # Propagate any changes to lui.__class__ by extensions
774 ui.__class__ = lui.__class__
775 ui.__class__ = lui.__class__
775
776
776 # (uisetup and extsetup are handled in extensions.loadall)
777 # (uisetup and extsetup are handled in extensions.loadall)
777
778
778 for name, module in exts:
779 for name, module in exts:
779 for objname, loadermod, loadername in extraloaders:
780 for objname, loadermod, loadername in extraloaders:
780 extraobj = getattr(module, objname, None)
781 extraobj = getattr(module, objname, None)
781 if extraobj is not None:
782 if extraobj is not None:
782 getattr(loadermod, loadername)(ui, name, extraobj)
783 getattr(loadermod, loadername)(ui, name, extraobj)
783 _loaded.add(name)
784 _loaded.add(name)
784
785
785 # (reposetup is handled in hg.repository)
786 # (reposetup is handled in hg.repository)
786
787
787 addaliases(lui, commands.table)
788 addaliases(lui, commands.table)
788
789
789 # All aliases and commands are completely defined, now.
790 # All aliases and commands are completely defined, now.
790 # Check abbreviation/ambiguity of shell alias.
791 # Check abbreviation/ambiguity of shell alias.
791 shellaliasfn = _checkshellalias(lui, ui, args)
792 shellaliasfn = _checkshellalias(lui, ui, args)
792 if shellaliasfn:
793 if shellaliasfn:
793 return shellaliasfn()
794 return shellaliasfn()
794
795
795 # check for fallback encoding
796 # check for fallback encoding
796 fallback = lui.config('ui', 'fallbackencoding')
797 fallback = lui.config('ui', 'fallbackencoding')
797 if fallback:
798 if fallback:
798 encoding.fallbackencoding = fallback
799 encoding.fallbackencoding = fallback
799
800
800 fullargs = args
801 fullargs = args
801 cmd, func, args, options, cmdoptions = _parse(lui, args)
802 cmd, func, args, options, cmdoptions = _parse(lui, args)
802
803
803 if options["config"]:
804 if options["config"]:
804 raise error.Abort(_("option --config may not be abbreviated!"))
805 raise error.Abort(_("option --config may not be abbreviated!"))
805 if options["cwd"]:
806 if options["cwd"]:
806 raise error.Abort(_("option --cwd may not be abbreviated!"))
807 raise error.Abort(_("option --cwd may not be abbreviated!"))
807 if options["repository"]:
808 if options["repository"]:
808 raise error.Abort(_(
809 raise error.Abort(_(
809 "option -R has to be separated from other options (e.g. not "
810 "option -R has to be separated from other options (e.g. not "
810 "-qR) and --repository may only be abbreviated as --repo!"))
811 "-qR) and --repository may only be abbreviated as --repo!"))
811
812
812 if options["encoding"]:
813 if options["encoding"]:
813 encoding.encoding = options["encoding"]
814 encoding.encoding = options["encoding"]
814 if options["encodingmode"]:
815 if options["encodingmode"]:
815 encoding.encodingmode = options["encodingmode"]
816 encoding.encodingmode = options["encodingmode"]
816 if options["time"]:
817 if options["time"]:
817 def get_times():
818 def get_times():
818 t = os.times()
819 t = os.times()
819 if t[4] == 0.0:
820 if t[4] == 0.0:
820 # Windows leaves this as zero, so use time.clock()
821 # Windows leaves this as zero, so use time.clock()
821 t = (t[0], t[1], t[2], t[3], time.clock())
822 t = (t[0], t[1], t[2], t[3], time.clock())
822 return t
823 return t
823 s = get_times()
824 s = get_times()
824 def print_time():
825 def print_time():
825 t = get_times()
826 t = get_times()
826 ui.warn(
827 ui.warn(
827 _("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
828 _("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
828 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
829 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
829 ui.atexit(print_time)
830 ui.atexit(print_time)
830 if options["profile"]:
831 if options["profile"]:
831 profiler.start()
832 profiler.start()
832
833
833 if options['verbose'] or options['debug'] or options['quiet']:
834 if options['verbose'] or options['debug'] or options['quiet']:
834 for opt in ('verbose', 'debug', 'quiet'):
835 for opt in ('verbose', 'debug', 'quiet'):
835 val = str(bool(options[opt]))
836 val = str(bool(options[opt]))
836 if pycompat.ispy3:
837 if pycompat.ispy3:
837 val = val.encode('ascii')
838 val = val.encode('ascii')
838 for ui_ in uis:
839 for ui_ in uis:
839 ui_.setconfig('ui', opt, val, '--' + opt)
840 ui_.setconfig('ui', opt, val, '--' + opt)
840
841
841 if options['traceback']:
842 if options['traceback']:
842 for ui_ in uis:
843 for ui_ in uis:
843 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
844 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
844
845
845 if options['noninteractive']:
846 if options['noninteractive']:
846 for ui_ in uis:
847 for ui_ in uis:
847 ui_.setconfig('ui', 'interactive', 'off', '-y')
848 ui_.setconfig('ui', 'interactive', 'off', '-y')
848
849
849 if cmdoptions.get('insecure', False):
850 if cmdoptions.get('insecure', False):
850 for ui_ in uis:
851 for ui_ in uis:
851 ui_.insecureconnections = True
852 ui_.insecureconnections = True
852
853
853 # setup color handling before pager, because setting up pager
854 # setup color handling before pager, because setting up pager
854 # might cause incorrect console information
855 # might cause incorrect console information
855 coloropt = options['color']
856 coloropt = options['color']
856 for ui_ in uis:
857 for ui_ in uis:
857 if coloropt:
858 if coloropt:
858 ui_.setconfig('ui', 'color', coloropt, '--color')
859 ui_.setconfig('ui', 'color', coloropt, '--color')
859 color.setup(ui_)
860 color.setup(ui_)
860
861
861 if util.parsebool(options['pager']):
862 if util.parsebool(options['pager']):
862 ui.pager('internal-always-' + cmd)
863 ui.pager('internal-always-' + cmd)
863 elif options['pager'] != 'auto':
864 elif options['pager'] != 'auto':
864 ui.disablepager()
865 ui.disablepager()
865
866
866 if options['version']:
867 if options['version']:
867 return commands.version_(ui)
868 return commands.version_(ui)
868 if options['help']:
869 if options['help']:
869 return commands.help_(ui, cmd, command=cmd is not None)
870 return commands.help_(ui, cmd, command=cmd is not None)
870 elif not cmd:
871 elif not cmd:
871 return commands.help_(ui, 'shortlist')
872 return commands.help_(ui, 'shortlist')
872
873
873 repo = None
874 repo = None
874 cmdpats = args[:]
875 cmdpats = args[:]
875 if not func.norepo:
876 if not func.norepo:
876 # use the repo from the request only if we don't have -R
877 # use the repo from the request only if we don't have -R
877 if not rpath and not cwd:
878 if not rpath and not cwd:
878 repo = req.repo
879 repo = req.repo
879
880
880 if repo:
881 if repo:
881 # set the descriptors of the repo ui to those of ui
882 # set the descriptors of the repo ui to those of ui
882 repo.ui.fin = ui.fin
883 repo.ui.fin = ui.fin
883 repo.ui.fout = ui.fout
884 repo.ui.fout = ui.fout
884 repo.ui.ferr = ui.ferr
885 repo.ui.ferr = ui.ferr
885 else:
886 else:
886 try:
887 try:
887 repo = hg.repository(ui, path=path,
888 repo = hg.repository(ui, path=path,
888 presetupfuncs=req.prereposetups)
889 presetupfuncs=req.prereposetups)
889 if not repo.local():
890 if not repo.local():
890 raise error.Abort(_("repository '%s' is not local")
891 raise error.Abort(_("repository '%s' is not local")
891 % path)
892 % path)
892 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
893 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
893 'repo')
894 'repo')
894 except error.RequirementError:
895 except error.RequirementError:
895 raise
896 raise
896 except error.RepoError:
897 except error.RepoError:
897 if rpath and rpath[-1]: # invalid -R path
898 if rpath and rpath[-1]: # invalid -R path
898 raise
899 raise
899 if not func.optionalrepo:
900 if not func.optionalrepo:
900 if func.inferrepo and args and not path:
901 if func.inferrepo and args and not path:
901 # try to infer -R from command args
902 # try to infer -R from command args
902 repos = map(cmdutil.findrepo, args)
903 repos = map(cmdutil.findrepo, args)
903 guess = repos[0]
904 guess = repos[0]
904 if guess and repos.count(guess) == len(repos):
905 if guess and repos.count(guess) == len(repos):
905 req.args = ['--repository', guess] + fullargs
906 req.args = ['--repository', guess] + fullargs
906 return _dispatch(req)
907 return _dispatch(req)
907 if not path:
908 if not path:
908 raise error.RepoError(_("no repository found in"
909 raise error.RepoError(_("no repository found in"
909 " '%s' (.hg not found)")
910 " '%s' (.hg not found)")
910 % pycompat.getcwd())
911 % pycompat.getcwd())
911 raise
912 raise
912 if repo:
913 if repo:
913 ui = repo.ui
914 ui = repo.ui
914 if options['hidden']:
915 if options['hidden']:
915 repo = repo.unfiltered()
916 repo = repo.unfiltered()
916 args.insert(0, repo)
917 args.insert(0, repo)
917 elif rpath:
918 elif rpath:
918 ui.warn(_("warning: --repository ignored\n"))
919 ui.warn(_("warning: --repository ignored\n"))
919
920
920 msg = _formatargs(fullargs)
921 msg = _formatargs(fullargs)
921 ui.log("command", '%s\n', msg)
922 ui.log("command", '%s\n', msg)
922 strcmdopt = pycompat.strkwargs(cmdoptions)
923 strcmdopt = pycompat.strkwargs(cmdoptions)
923 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
924 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
924 try:
925 try:
925 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
926 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
926 cmdpats, cmdoptions)
927 cmdpats, cmdoptions)
927 finally:
928 finally:
928 if repo and repo != req.repo:
929 if repo and repo != req.repo:
929 repo.close()
930 repo.close()
930
931
931 def _runcommand(ui, options, cmd, cmdfunc):
932 def _runcommand(ui, options, cmd, cmdfunc):
932 """Run a command function, possibly with profiling enabled."""
933 """Run a command function, possibly with profiling enabled."""
933 try:
934 try:
934 return cmdfunc()
935 return cmdfunc()
935 except error.SignatureError:
936 except error.SignatureError:
936 raise error.CommandError(cmd, _('invalid arguments'))
937 raise error.CommandError(cmd, _('invalid arguments'))
937
938
938 def _exceptionwarning(ui):
939 def _exceptionwarning(ui):
939 """Produce a warning message for the current active exception"""
940 """Produce a warning message for the current active exception"""
940
941
941 # For compatibility checking, we discard the portion of the hg
942 # For compatibility checking, we discard the portion of the hg
942 # version after the + on the assumption that if a "normal
943 # version after the + on the assumption that if a "normal
943 # user" is running a build with a + in it the packager
944 # user" is running a build with a + in it the packager
944 # probably built from fairly close to a tag and anyone with a
945 # probably built from fairly close to a tag and anyone with a
945 # 'make local' copy of hg (where the version number can be out
946 # 'make local' copy of hg (where the version number can be out
946 # of date) will be clueful enough to notice the implausible
947 # of date) will be clueful enough to notice the implausible
947 # version number and try updating.
948 # version number and try updating.
948 ct = util.versiontuple(n=2)
949 ct = util.versiontuple(n=2)
949 worst = None, ct, ''
950 worst = None, ct, ''
950 if ui.config('ui', 'supportcontact', None) is None:
951 if ui.config('ui', 'supportcontact', None) is None:
951 for name, mod in extensions.extensions():
952 for name, mod in extensions.extensions():
952 testedwith = getattr(mod, 'testedwith', '')
953 testedwith = getattr(mod, 'testedwith', '')
953 if pycompat.ispy3 and isinstance(testedwith, str):
954 if pycompat.ispy3 and isinstance(testedwith, str):
954 testedwith = testedwith.encode(u'utf-8')
955 testedwith = testedwith.encode(u'utf-8')
955 report = getattr(mod, 'buglink', _('the extension author.'))
956 report = getattr(mod, 'buglink', _('the extension author.'))
956 if not testedwith.strip():
957 if not testedwith.strip():
957 # We found an untested extension. It's likely the culprit.
958 # We found an untested extension. It's likely the culprit.
958 worst = name, 'unknown', report
959 worst = name, 'unknown', report
959 break
960 break
960
961
961 # Never blame on extensions bundled with Mercurial.
962 # Never blame on extensions bundled with Mercurial.
962 if extensions.ismoduleinternal(mod):
963 if extensions.ismoduleinternal(mod):
963 continue
964 continue
964
965
965 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
966 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
966 if ct in tested:
967 if ct in tested:
967 continue
968 continue
968
969
969 lower = [t for t in tested if t < ct]
970 lower = [t for t in tested if t < ct]
970 nearest = max(lower or tested)
971 nearest = max(lower or tested)
971 if worst[0] is None or nearest < worst[1]:
972 if worst[0] is None or nearest < worst[1]:
972 worst = name, nearest, report
973 worst = name, nearest, report
973 if worst[0] is not None:
974 if worst[0] is not None:
974 name, testedwith, report = worst
975 name, testedwith, report = worst
975 if not isinstance(testedwith, (bytes, str)):
976 if not isinstance(testedwith, (bytes, str)):
976 testedwith = '.'.join([str(c) for c in testedwith])
977 testedwith = '.'.join([str(c) for c in testedwith])
977 warning = (_('** Unknown exception encountered with '
978 warning = (_('** Unknown exception encountered with '
978 'possibly-broken third-party extension %s\n'
979 'possibly-broken third-party extension %s\n'
979 '** which supports versions %s of Mercurial.\n'
980 '** which supports versions %s of Mercurial.\n'
980 '** Please disable %s and try your action again.\n'
981 '** Please disable %s and try your action again.\n'
981 '** If that fixes the bug please report it to %s\n')
982 '** If that fixes the bug please report it to %s\n')
982 % (name, testedwith, name, report))
983 % (name, testedwith, name, report))
983 else:
984 else:
984 bugtracker = ui.config('ui', 'supportcontact', None)
985 bugtracker = ui.config('ui', 'supportcontact', None)
985 if bugtracker is None:
986 if bugtracker is None:
986 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
987 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
987 warning = (_("** unknown exception encountered, "
988 warning = (_("** unknown exception encountered, "
988 "please report by visiting\n** ") + bugtracker + '\n')
989 "please report by visiting\n** ") + bugtracker + '\n')
989 if pycompat.ispy3:
990 if pycompat.ispy3:
990 sysversion = sys.version.encode(u'utf-8')
991 sysversion = sys.version.encode(u'utf-8')
991 else:
992 else:
992 sysversion = sys.version
993 sysversion = sys.version
993 sysversion = sysversion.replace('\n', '')
994 sysversion = sysversion.replace('\n', '')
994 warning += ((_("** Python %s\n") % sysversion) +
995 warning += ((_("** Python %s\n") % sysversion) +
995 (_("** Mercurial Distributed SCM (version %s)\n") %
996 (_("** Mercurial Distributed SCM (version %s)\n") %
996 util.version()) +
997 util.version()) +
997 (_("** Extensions loaded: %s\n") %
998 (_("** Extensions loaded: %s\n") %
998 ", ".join([x[0] for x in extensions.extensions()])))
999 ", ".join([x[0] for x in extensions.extensions()])))
999 return warning
1000 return warning
1000
1001
1001 def handlecommandexception(ui):
1002 def handlecommandexception(ui):
1002 """Produce a warning message for broken commands
1003 """Produce a warning message for broken commands
1003
1004
1004 Called when handling an exception; the exception is reraised if
1005 Called when handling an exception; the exception is reraised if
1005 this function returns False, ignored otherwise.
1006 this function returns False, ignored otherwise.
1006 """
1007 """
1007 warning = _exceptionwarning(ui)
1008 warning = _exceptionwarning(ui)
1008 ui.log("commandexception", "%s\n%s\n", warning, traceback.format_exc())
1009 ui.log("commandexception", "%s\n%s\n", warning, traceback.format_exc())
1009 ui.warn(warning)
1010 ui.warn(warning)
1010 return False # re-raise the exception
1011 return False # re-raise the exception
@@ -1,483 +1,484 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import contextlib
11 import contextlib
12 import os
12 import os
13
13
14 from .common import (
14 from .common import (
15 ErrorResponse,
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
16 HTTP_BAD_REQUEST,
17 HTTP_NOT_FOUND,
17 HTTP_NOT_FOUND,
18 HTTP_NOT_MODIFIED,
18 HTTP_NOT_MODIFIED,
19 HTTP_OK,
19 HTTP_OK,
20 HTTP_SERVER_ERROR,
20 HTTP_SERVER_ERROR,
21 caching,
21 caching,
22 cspvalues,
22 cspvalues,
23 permhooks,
23 permhooks,
24 )
24 )
25 from .request import wsgirequest
25 from .request import wsgirequest
26
26
27 from .. import (
27 from .. import (
28 encoding,
28 encoding,
29 error,
29 error,
30 hg,
30 hg,
31 hook,
31 hook,
32 profiling,
32 profiling,
33 repoview,
33 repoview,
34 templatefilters,
34 templatefilters,
35 templater,
35 templater,
36 ui as uimod,
36 ui as uimod,
37 util,
37 util,
38 )
38 )
39
39
40 from . import (
40 from . import (
41 protocol,
41 protocol,
42 webcommands,
42 webcommands,
43 webutil,
43 webutil,
44 wsgicgi,
44 wsgicgi,
45 )
45 )
46
46
47 perms = {
47 perms = {
48 'changegroup': 'pull',
48 'changegroup': 'pull',
49 'changegroupsubset': 'pull',
49 'changegroupsubset': 'pull',
50 'getbundle': 'pull',
50 'getbundle': 'pull',
51 'stream_out': 'pull',
51 'stream_out': 'pull',
52 'listkeys': 'pull',
52 'listkeys': 'pull',
53 'unbundle': 'push',
53 'unbundle': 'push',
54 'pushkey': 'push',
54 'pushkey': 'push',
55 }
55 }
56
56
57 archivespecs = util.sortdict((
57 archivespecs = util.sortdict((
58 ('zip', ('application/zip', 'zip', '.zip', None)),
58 ('zip', ('application/zip', 'zip', '.zip', None)),
59 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
59 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
60 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
60 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
61 ))
61 ))
62
62
63 def makebreadcrumb(url, prefix=''):
63 def makebreadcrumb(url, prefix=''):
64 '''Return a 'URL breadcrumb' list
64 '''Return a 'URL breadcrumb' list
65
65
66 A 'URL breadcrumb' is a list of URL-name pairs,
66 A 'URL breadcrumb' is a list of URL-name pairs,
67 corresponding to each of the path items on a URL.
67 corresponding to each of the path items on a URL.
68 This can be used to create path navigation entries.
68 This can be used to create path navigation entries.
69 '''
69 '''
70 if url.endswith('/'):
70 if url.endswith('/'):
71 url = url[:-1]
71 url = url[:-1]
72 if prefix:
72 if prefix:
73 url = '/' + prefix + url
73 url = '/' + prefix + url
74 relpath = url
74 relpath = url
75 if relpath.startswith('/'):
75 if relpath.startswith('/'):
76 relpath = relpath[1:]
76 relpath = relpath[1:]
77
77
78 breadcrumb = []
78 breadcrumb = []
79 urlel = url
79 urlel = url
80 pathitems = [''] + relpath.split('/')
80 pathitems = [''] + relpath.split('/')
81 for pathel in reversed(pathitems):
81 for pathel in reversed(pathitems):
82 if not pathel or not urlel:
82 if not pathel or not urlel:
83 break
83 break
84 breadcrumb.append({'url': urlel, 'name': pathel})
84 breadcrumb.append({'url': urlel, 'name': pathel})
85 urlel = os.path.dirname(urlel)
85 urlel = os.path.dirname(urlel)
86 return reversed(breadcrumb)
86 return reversed(breadcrumb)
87
87
88 class requestcontext(object):
88 class requestcontext(object):
89 """Holds state/context for an individual request.
89 """Holds state/context for an individual request.
90
90
91 Servers can be multi-threaded. Holding state on the WSGI application
91 Servers can be multi-threaded. Holding state on the WSGI application
92 is prone to race conditions. Instances of this class exist to hold
92 is prone to race conditions. Instances of this class exist to hold
93 mutable and race-free state for requests.
93 mutable and race-free state for requests.
94 """
94 """
95 def __init__(self, app, repo):
95 def __init__(self, app, repo):
96 self.repo = repo
96 self.repo = repo
97 self.reponame = app.reponame
97 self.reponame = app.reponame
98
98
99 self.archivespecs = archivespecs
99 self.archivespecs = archivespecs
100
100
101 self.maxchanges = self.configint('web', 'maxchanges', 10)
101 self.maxchanges = self.configint('web', 'maxchanges', 10)
102 self.stripecount = self.configint('web', 'stripes', 1)
102 self.stripecount = self.configint('web', 'stripes', 1)
103 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
103 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
104 self.maxfiles = self.configint('web', 'maxfiles', 10)
104 self.maxfiles = self.configint('web', 'maxfiles', 10)
105 self.allowpull = self.configbool('web', 'allowpull', True)
105 self.allowpull = self.configbool('web', 'allowpull', True)
106
106
107 # we use untrusted=False to prevent a repo owner from using
107 # we use untrusted=False to prevent a repo owner from using
108 # web.templates in .hg/hgrc to get access to any file readable
108 # web.templates in .hg/hgrc to get access to any file readable
109 # by the user running the CGI script
109 # by the user running the CGI script
110 self.templatepath = self.config('web', 'templates', untrusted=False)
110 self.templatepath = self.config('web', 'templates', untrusted=False)
111
111
112 # This object is more expensive to build than simple config values.
112 # This object is more expensive to build than simple config values.
113 # It is shared across requests. The app will replace the object
113 # It is shared across requests. The app will replace the object
114 # if it is updated. Since this is a reference and nothing should
114 # if it is updated. Since this is a reference and nothing should
115 # modify the underlying object, it should be constant for the lifetime
115 # modify the underlying object, it should be constant for the lifetime
116 # of the request.
116 # of the request.
117 self.websubtable = app.websubtable
117 self.websubtable = app.websubtable
118
118
119 self.csp, self.nonce = cspvalues(self.repo.ui)
119 self.csp, self.nonce = cspvalues(self.repo.ui)
120
120
121 # Trust the settings from the .hg/hgrc files by default.
121 # Trust the settings from the .hg/hgrc files by default.
122 def config(self, section, name, default=None, untrusted=True):
122 def config(self, section, name, default=None, untrusted=True):
123 return self.repo.ui.config(section, name, default,
123 return self.repo.ui.config(section, name, default,
124 untrusted=untrusted)
124 untrusted=untrusted)
125
125
126 def configbool(self, section, name, default=False, untrusted=True):
126 def configbool(self, section, name, default=False, untrusted=True):
127 return self.repo.ui.configbool(section, name, default,
127 return self.repo.ui.configbool(section, name, default,
128 untrusted=untrusted)
128 untrusted=untrusted)
129
129
130 def configint(self, section, name, default=None, untrusted=True):
130 def configint(self, section, name, default=None, untrusted=True):
131 return self.repo.ui.configint(section, name, default,
131 return self.repo.ui.configint(section, name, default,
132 untrusted=untrusted)
132 untrusted=untrusted)
133
133
134 def configlist(self, section, name, default=None, untrusted=True):
134 def configlist(self, section, name, default=None, untrusted=True):
135 return self.repo.ui.configlist(section, name, default,
135 return self.repo.ui.configlist(section, name, default,
136 untrusted=untrusted)
136 untrusted=untrusted)
137
137
138 def archivelist(self, nodeid):
138 def archivelist(self, nodeid):
139 allowed = self.configlist('web', 'allow_archive')
139 allowed = self.configlist('web', 'allow_archive')
140 for typ, spec in self.archivespecs.iteritems():
140 for typ, spec in self.archivespecs.iteritems():
141 if typ in allowed or self.configbool('web', 'allow%s' % typ):
141 if typ in allowed or self.configbool('web', 'allow%s' % typ):
142 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
142 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
143
143
144 def templater(self, req):
144 def templater(self, req):
145 # determine scheme, port and server name
145 # determine scheme, port and server name
146 # this is needed to create absolute urls
146 # this is needed to create absolute urls
147
147
148 proto = req.env.get('wsgi.url_scheme')
148 proto = req.env.get('wsgi.url_scheme')
149 if proto == 'https':
149 if proto == 'https':
150 proto = 'https'
150 proto = 'https'
151 default_port = '443'
151 default_port = '443'
152 else:
152 else:
153 proto = 'http'
153 proto = 'http'
154 default_port = '80'
154 default_port = '80'
155
155
156 port = req.env['SERVER_PORT']
156 port = req.env['SERVER_PORT']
157 port = port != default_port and (':' + port) or ''
157 port = port != default_port and (':' + port) or ''
158 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
158 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
159 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
159 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
160 logoimg = self.config('web', 'logoimg', 'hglogo.png')
160 logoimg = self.config('web', 'logoimg', 'hglogo.png')
161 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
161 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
162 if not staticurl.endswith('/'):
162 if not staticurl.endswith('/'):
163 staticurl += '/'
163 staticurl += '/'
164
164
165 # some functions for the templater
165 # some functions for the templater
166
166
167 def motd(**map):
167 def motd(**map):
168 yield self.config('web', 'motd', '')
168 yield self.config('web', 'motd', '')
169
169
170 # figure out which style to use
170 # figure out which style to use
171
171
172 vars = {}
172 vars = {}
173 styles = (
173 styles = (
174 req.form.get('style', [None])[0],
174 req.form.get('style', [None])[0],
175 self.config('web', 'style'),
175 self.config('web', 'style'),
176 'paper',
176 'paper',
177 )
177 )
178 style, mapfile = templater.stylemap(styles, self.templatepath)
178 style, mapfile = templater.stylemap(styles, self.templatepath)
179 if style == styles[0]:
179 if style == styles[0]:
180 vars['style'] = style
180 vars['style'] = style
181
181
182 start = req.url[-1] == '?' and '&' or '?'
182 start = req.url[-1] == '?' and '&' or '?'
183 sessionvars = webutil.sessionvars(vars, start)
183 sessionvars = webutil.sessionvars(vars, start)
184
184
185 if not self.reponame:
185 if not self.reponame:
186 self.reponame = (self.config('web', 'name')
186 self.reponame = (self.config('web', 'name')
187 or req.env.get('REPO_NAME')
187 or req.env.get('REPO_NAME')
188 or req.url.strip('/') or self.repo.root)
188 or req.url.strip('/') or self.repo.root)
189
189
190 def websubfilter(text):
190 def websubfilter(text):
191 return templatefilters.websub(text, self.websubtable)
191 return templatefilters.websub(text, self.websubtable)
192
192
193 # create the templater
193 # create the templater
194
194
195 defaults = {
195 defaults = {
196 'url': req.url,
196 'url': req.url,
197 'logourl': logourl,
197 'logourl': logourl,
198 'logoimg': logoimg,
198 'logoimg': logoimg,
199 'staticurl': staticurl,
199 'staticurl': staticurl,
200 'urlbase': urlbase,
200 'urlbase': urlbase,
201 'repo': self.reponame,
201 'repo': self.reponame,
202 'encoding': encoding.encoding,
202 'encoding': encoding.encoding,
203 'motd': motd,
203 'motd': motd,
204 'sessionvars': sessionvars,
204 'sessionvars': sessionvars,
205 'pathdef': makebreadcrumb(req.url),
205 'pathdef': makebreadcrumb(req.url),
206 'style': style,
206 'style': style,
207 'nonce': self.nonce,
207 'nonce': self.nonce,
208 }
208 }
209 tmpl = templater.templater.frommapfile(mapfile,
209 tmpl = templater.templater.frommapfile(mapfile,
210 filters={'websub': websubfilter},
210 filters={'websub': websubfilter},
211 defaults=defaults)
211 defaults=defaults)
212 return tmpl
212 return tmpl
213
213
214
214
215 class hgweb(object):
215 class hgweb(object):
216 """HTTP server for individual repositories.
216 """HTTP server for individual repositories.
217
217
218 Instances of this class serve HTTP responses for a particular
218 Instances of this class serve HTTP responses for a particular
219 repository.
219 repository.
220
220
221 Instances are typically used as WSGI applications.
221 Instances are typically used as WSGI applications.
222
222
223 Some servers are multi-threaded. On these servers, there may
223 Some servers are multi-threaded. On these servers, there may
224 be multiple active threads inside __call__.
224 be multiple active threads inside __call__.
225 """
225 """
226 def __init__(self, repo, name=None, baseui=None):
226 def __init__(self, repo, name=None, baseui=None):
227 if isinstance(repo, str):
227 if isinstance(repo, str):
228 if baseui:
228 if baseui:
229 u = baseui.copy()
229 u = baseui.copy()
230 else:
230 else:
231 u = uimod.ui.load()
231 u = uimod.ui.load()
232 r = hg.repository(u, repo)
232 r = hg.repository(u, repo)
233 else:
233 else:
234 # we trust caller to give us a private copy
234 # we trust caller to give us a private copy
235 r = repo
235 r = repo
236
236
237 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
237 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
239 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
239 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
241 # resolve file patterns relative to repo root
241 # resolve file patterns relative to repo root
242 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
242 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
244 # displaying bundling progress bar while serving feel wrong and may
244 # displaying bundling progress bar while serving feel wrong and may
245 # break some wsgi implementation.
245 # break some wsgi implementation.
246 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
246 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
247 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
247 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
248 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
248 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
249 self._lastrepo = self._repos[0]
249 self._lastrepo = self._repos[0]
250 hook.redirect(True)
250 hook.redirect(True)
251 self.reponame = name
251 self.reponame = name
252
252
253 def _webifyrepo(self, repo):
253 def _webifyrepo(self, repo):
254 repo = getwebview(repo)
254 repo = getwebview(repo)
255 self.websubtable = webutil.getwebsubs(repo)
255 self.websubtable = webutil.getwebsubs(repo)
256 return repo
256 return repo
257
257
258 @contextlib.contextmanager
258 @contextlib.contextmanager
259 def _obtainrepo(self):
259 def _obtainrepo(self):
260 """Obtain a repo unique to the caller.
260 """Obtain a repo unique to the caller.
261
261
262 Internally we maintain a stack of cachedlocalrepo instances
262 Internally we maintain a stack of cachedlocalrepo instances
263 to be handed out. If one is available, we pop it and return it,
263 to be handed out. If one is available, we pop it and return it,
264 ensuring it is up to date in the process. If one is not available,
264 ensuring it is up to date in the process. If one is not available,
265 we clone the most recently used repo instance and return it.
265 we clone the most recently used repo instance and return it.
266
266
267 It is currently possible for the stack to grow without bounds
267 It is currently possible for the stack to grow without bounds
268 if the server allows infinite threads. However, servers should
268 if the server allows infinite threads. However, servers should
269 have a thread limit, thus establishing our limit.
269 have a thread limit, thus establishing our limit.
270 """
270 """
271 if self._repos:
271 if self._repos:
272 cached = self._repos.pop()
272 cached = self._repos.pop()
273 r, created = cached.fetch()
273 r, created = cached.fetch()
274 else:
274 else:
275 cached = self._lastrepo.copy()
275 cached = self._lastrepo.copy()
276 r, created = cached.fetch()
276 r, created = cached.fetch()
277 if created:
277 if created:
278 r = self._webifyrepo(r)
278 r = self._webifyrepo(r)
279
279
280 self._lastrepo = cached
280 self._lastrepo = cached
281 self.mtime = cached.mtime
281 self.mtime = cached.mtime
282 try:
282 try:
283 yield r
283 yield r
284 finally:
284 finally:
285 self._repos.append(cached)
285 self._repos.append(cached)
286
286
287 def run(self):
287 def run(self):
288 """Start a server from CGI environment.
288 """Start a server from CGI environment.
289
289
290 Modern servers should be using WSGI and should avoid this
290 Modern servers should be using WSGI and should avoid this
291 method, if possible.
291 method, if possible.
292 """
292 """
293 if not encoding.environ.get('GATEWAY_INTERFACE',
293 if not encoding.environ.get('GATEWAY_INTERFACE',
294 '').startswith("CGI/1."):
294 '').startswith("CGI/1."):
295 raise RuntimeError("This function is only intended to be "
295 raise RuntimeError("This function is only intended to be "
296 "called while running as a CGI script.")
296 "called while running as a CGI script.")
297 wsgicgi.launch(self)
297 wsgicgi.launch(self)
298
298
299 def __call__(self, env, respond):
299 def __call__(self, env, respond):
300 """Run the WSGI application.
300 """Run the WSGI application.
301
301
302 This may be called by multiple threads.
302 This may be called by multiple threads.
303 """
303 """
304 req = wsgirequest(env, respond)
304 req = wsgirequest(env, respond)
305 return self.run_wsgi(req)
305 return self.run_wsgi(req)
306
306
307 def run_wsgi(self, req):
307 def run_wsgi(self, req):
308 """Internal method to run the WSGI application.
308 """Internal method to run the WSGI application.
309
309
310 This is typically only called by Mercurial. External consumers
310 This is typically only called by Mercurial. External consumers
311 should be using instances of this class as the WSGI application.
311 should be using instances of this class as the WSGI application.
312 """
312 """
313 with self._obtainrepo() as repo:
313 with self._obtainrepo() as repo:
314 with profiling.maybeprofile(repo.ui):
314 profile = repo.ui.configbool('profiling', 'enabled')
315 with profiling.profile(repo.ui, enabled=profile):
315 for r in self._runwsgi(req, repo):
316 for r in self._runwsgi(req, repo):
316 yield r
317 yield r
317
318
318 def _runwsgi(self, req, repo):
319 def _runwsgi(self, req, repo):
319 rctx = requestcontext(self, repo)
320 rctx = requestcontext(self, repo)
320
321
321 # This state is global across all threads.
322 # This state is global across all threads.
322 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
323 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
323 rctx.repo.ui.environ = req.env
324 rctx.repo.ui.environ = req.env
324
325
325 if rctx.csp:
326 if rctx.csp:
326 # hgwebdir may have added CSP header. Since we generate our own,
327 # hgwebdir may have added CSP header. Since we generate our own,
327 # replace it.
328 # replace it.
328 req.headers = [h for h in req.headers
329 req.headers = [h for h in req.headers
329 if h[0] != 'Content-Security-Policy']
330 if h[0] != 'Content-Security-Policy']
330 req.headers.append(('Content-Security-Policy', rctx.csp))
331 req.headers.append(('Content-Security-Policy', rctx.csp))
331
332
332 # work with CGI variables to create coherent structure
333 # work with CGI variables to create coherent structure
333 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
334
335
335 req.url = req.env['SCRIPT_NAME']
336 req.url = req.env['SCRIPT_NAME']
336 if not req.url.endswith('/'):
337 if not req.url.endswith('/'):
337 req.url += '/'
338 req.url += '/'
338 if req.env.get('REPO_NAME'):
339 if req.env.get('REPO_NAME'):
339 req.url += req.env['REPO_NAME'] + '/'
340 req.url += req.env['REPO_NAME'] + '/'
340
341
341 if 'PATH_INFO' in req.env:
342 if 'PATH_INFO' in req.env:
342 parts = req.env['PATH_INFO'].strip('/').split('/')
343 parts = req.env['PATH_INFO'].strip('/').split('/')
343 repo_parts = req.env.get('REPO_NAME', '').split('/')
344 repo_parts = req.env.get('REPO_NAME', '').split('/')
344 if parts[:len(repo_parts)] == repo_parts:
345 if parts[:len(repo_parts)] == repo_parts:
345 parts = parts[len(repo_parts):]
346 parts = parts[len(repo_parts):]
346 query = '/'.join(parts)
347 query = '/'.join(parts)
347 else:
348 else:
348 query = req.env['QUERY_STRING'].partition('&')[0]
349 query = req.env['QUERY_STRING'].partition('&')[0]
349 query = query.partition(';')[0]
350 query = query.partition(';')[0]
350
351
351 # process this if it's a protocol request
352 # process this if it's a protocol request
352 # protocol bits don't need to create any URLs
353 # protocol bits don't need to create any URLs
353 # and the clients always use the old URL structure
354 # and the clients always use the old URL structure
354
355
355 cmd = req.form.get('cmd', [''])[0]
356 cmd = req.form.get('cmd', [''])[0]
356 if protocol.iscmd(cmd):
357 if protocol.iscmd(cmd):
357 try:
358 try:
358 if query:
359 if query:
359 raise ErrorResponse(HTTP_NOT_FOUND)
360 raise ErrorResponse(HTTP_NOT_FOUND)
360 if cmd in perms:
361 if cmd in perms:
361 self.check_perm(rctx, req, perms[cmd])
362 self.check_perm(rctx, req, perms[cmd])
362 return protocol.call(rctx.repo, req, cmd)
363 return protocol.call(rctx.repo, req, cmd)
363 except ErrorResponse as inst:
364 except ErrorResponse as inst:
364 # A client that sends unbundle without 100-continue will
365 # A client that sends unbundle without 100-continue will
365 # break if we respond early.
366 # break if we respond early.
366 if (cmd == 'unbundle' and
367 if (cmd == 'unbundle' and
367 (req.env.get('HTTP_EXPECT',
368 (req.env.get('HTTP_EXPECT',
368 '').lower() != '100-continue') or
369 '').lower() != '100-continue') or
369 req.env.get('X-HgHttp2', '')):
370 req.env.get('X-HgHttp2', '')):
370 req.drain()
371 req.drain()
371 else:
372 else:
372 req.headers.append(('Connection', 'Close'))
373 req.headers.append(('Connection', 'Close'))
373 req.respond(inst, protocol.HGTYPE,
374 req.respond(inst, protocol.HGTYPE,
374 body='0\n%s\n' % inst)
375 body='0\n%s\n' % inst)
375 return ''
376 return ''
376
377
377 # translate user-visible url structure to internal structure
378 # translate user-visible url structure to internal structure
378
379
379 args = query.split('/', 2)
380 args = query.split('/', 2)
380 if 'cmd' not in req.form and args and args[0]:
381 if 'cmd' not in req.form and args and args[0]:
381
382
382 cmd = args.pop(0)
383 cmd = args.pop(0)
383 style = cmd.rfind('-')
384 style = cmd.rfind('-')
384 if style != -1:
385 if style != -1:
385 req.form['style'] = [cmd[:style]]
386 req.form['style'] = [cmd[:style]]
386 cmd = cmd[style + 1:]
387 cmd = cmd[style + 1:]
387
388
388 # avoid accepting e.g. style parameter as command
389 # avoid accepting e.g. style parameter as command
389 if util.safehasattr(webcommands, cmd):
390 if util.safehasattr(webcommands, cmd):
390 req.form['cmd'] = [cmd]
391 req.form['cmd'] = [cmd]
391
392
392 if cmd == 'static':
393 if cmd == 'static':
393 req.form['file'] = ['/'.join(args)]
394 req.form['file'] = ['/'.join(args)]
394 else:
395 else:
395 if args and args[0]:
396 if args and args[0]:
396 node = args.pop(0).replace('%2F', '/')
397 node = args.pop(0).replace('%2F', '/')
397 req.form['node'] = [node]
398 req.form['node'] = [node]
398 if args:
399 if args:
399 req.form['file'] = args
400 req.form['file'] = args
400
401
401 ua = req.env.get('HTTP_USER_AGENT', '')
402 ua = req.env.get('HTTP_USER_AGENT', '')
402 if cmd == 'rev' and 'mercurial' in ua:
403 if cmd == 'rev' and 'mercurial' in ua:
403 req.form['style'] = ['raw']
404 req.form['style'] = ['raw']
404
405
405 if cmd == 'archive':
406 if cmd == 'archive':
406 fn = req.form['node'][0]
407 fn = req.form['node'][0]
407 for type_, spec in rctx.archivespecs.iteritems():
408 for type_, spec in rctx.archivespecs.iteritems():
408 ext = spec[2]
409 ext = spec[2]
409 if fn.endswith(ext):
410 if fn.endswith(ext):
410 req.form['node'] = [fn[:-len(ext)]]
411 req.form['node'] = [fn[:-len(ext)]]
411 req.form['type'] = [type_]
412 req.form['type'] = [type_]
412
413
413 # process the web interface request
414 # process the web interface request
414
415
415 try:
416 try:
416 tmpl = rctx.templater(req)
417 tmpl = rctx.templater(req)
417 ctype = tmpl('mimetype', encoding=encoding.encoding)
418 ctype = tmpl('mimetype', encoding=encoding.encoding)
418 ctype = templater.stringify(ctype)
419 ctype = templater.stringify(ctype)
419
420
420 # check read permissions non-static content
421 # check read permissions non-static content
421 if cmd != 'static':
422 if cmd != 'static':
422 self.check_perm(rctx, req, None)
423 self.check_perm(rctx, req, None)
423
424
424 if cmd == '':
425 if cmd == '':
425 req.form['cmd'] = [tmpl.cache['default']]
426 req.form['cmd'] = [tmpl.cache['default']]
426 cmd = req.form['cmd'][0]
427 cmd = req.form['cmd'][0]
427
428
428 # Don't enable caching if using a CSP nonce because then it wouldn't
429 # Don't enable caching if using a CSP nonce because then it wouldn't
429 # be a nonce.
430 # be a nonce.
430 if rctx.configbool('web', 'cache', True) and not rctx.nonce:
431 if rctx.configbool('web', 'cache', True) and not rctx.nonce:
431 caching(self, req) # sets ETag header or raises NOT_MODIFIED
432 caching(self, req) # sets ETag header or raises NOT_MODIFIED
432 if cmd not in webcommands.__all__:
433 if cmd not in webcommands.__all__:
433 msg = 'no such method: %s' % cmd
434 msg = 'no such method: %s' % cmd
434 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
435 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
435 elif cmd == 'file' and 'raw' in req.form.get('style', []):
436 elif cmd == 'file' and 'raw' in req.form.get('style', []):
436 rctx.ctype = ctype
437 rctx.ctype = ctype
437 content = webcommands.rawfile(rctx, req, tmpl)
438 content = webcommands.rawfile(rctx, req, tmpl)
438 else:
439 else:
439 content = getattr(webcommands, cmd)(rctx, req, tmpl)
440 content = getattr(webcommands, cmd)(rctx, req, tmpl)
440 req.respond(HTTP_OK, ctype)
441 req.respond(HTTP_OK, ctype)
441
442
442 return content
443 return content
443
444
444 except (error.LookupError, error.RepoLookupError) as err:
445 except (error.LookupError, error.RepoLookupError) as err:
445 req.respond(HTTP_NOT_FOUND, ctype)
446 req.respond(HTTP_NOT_FOUND, ctype)
446 msg = str(err)
447 msg = str(err)
447 if (util.safehasattr(err, 'name') and
448 if (util.safehasattr(err, 'name') and
448 not isinstance(err, error.ManifestLookupError)):
449 not isinstance(err, error.ManifestLookupError)):
449 msg = 'revision not found: %s' % err.name
450 msg = 'revision not found: %s' % err.name
450 return tmpl('error', error=msg)
451 return tmpl('error', error=msg)
451 except (error.RepoError, error.RevlogError) as inst:
452 except (error.RepoError, error.RevlogError) as inst:
452 req.respond(HTTP_SERVER_ERROR, ctype)
453 req.respond(HTTP_SERVER_ERROR, ctype)
453 return tmpl('error', error=str(inst))
454 return tmpl('error', error=str(inst))
454 except ErrorResponse as inst:
455 except ErrorResponse as inst:
455 req.respond(inst, ctype)
456 req.respond(inst, ctype)
456 if inst.code == HTTP_NOT_MODIFIED:
457 if inst.code == HTTP_NOT_MODIFIED:
457 # Not allowed to return a body on a 304
458 # Not allowed to return a body on a 304
458 return ['']
459 return ['']
459 return tmpl('error', error=str(inst))
460 return tmpl('error', error=str(inst))
460
461
461 def check_perm(self, rctx, req, op):
462 def check_perm(self, rctx, req, op):
462 for permhook in permhooks:
463 for permhook in permhooks:
463 permhook(rctx, req, op)
464 permhook(rctx, req, op)
464
465
465 def getwebview(repo):
466 def getwebview(repo):
466 """The 'web.view' config controls changeset filter to hgweb. Possible
467 """The 'web.view' config controls changeset filter to hgweb. Possible
467 values are ``served``, ``visible`` and ``all``. Default is ``served``.
468 values are ``served``, ``visible`` and ``all``. Default is ``served``.
468 The ``served`` filter only shows changesets that can be pulled from the
469 The ``served`` filter only shows changesets that can be pulled from the
469 hgweb instance. The``visible`` filter includes secret changesets but
470 hgweb instance. The``visible`` filter includes secret changesets but
470 still excludes "hidden" one.
471 still excludes "hidden" one.
471
472
472 See the repoview module for details.
473 See the repoview module for details.
473
474
474 The option has been around undocumented since Mercurial 2.5, but no
475 The option has been around undocumented since Mercurial 2.5, but no
475 user ever asked about it. So we better keep it undocumented for now."""
476 user ever asked about it. So we better keep it undocumented for now."""
476 viewconfig = repo.ui.config('web', 'view', 'served',
477 viewconfig = repo.ui.config('web', 'view', 'served',
477 untrusted=True)
478 untrusted=True)
478 if viewconfig == 'all':
479 if viewconfig == 'all':
479 return repo.unfiltered()
480 return repo.unfiltered()
480 elif viewconfig in repoview.filtertable:
481 elif viewconfig in repoview.filtertable:
481 return repo.filtered(viewconfig)
482 return repo.filtered(viewconfig)
482 else:
483 else:
483 return repo.filtered('served')
484 return repo.filtered('served')
@@ -1,539 +1,540 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import os
11 import os
12 import re
12 import re
13 import time
13 import time
14
14
15 from ..i18n import _
15 from ..i18n import _
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_NOT_FOUND,
19 HTTP_NOT_FOUND,
20 HTTP_OK,
20 HTTP_OK,
21 HTTP_SERVER_ERROR,
21 HTTP_SERVER_ERROR,
22 cspvalues,
22 cspvalues,
23 get_contact,
23 get_contact,
24 get_mtime,
24 get_mtime,
25 ismember,
25 ismember,
26 paritygen,
26 paritygen,
27 staticfile,
27 staticfile,
28 )
28 )
29 from .request import wsgirequest
29 from .request import wsgirequest
30
30
31 from .. import (
31 from .. import (
32 encoding,
32 encoding,
33 error,
33 error,
34 hg,
34 hg,
35 profiling,
35 profiling,
36 scmutil,
36 scmutil,
37 templater,
37 templater,
38 ui as uimod,
38 ui as uimod,
39 util,
39 util,
40 )
40 )
41
41
42 from . import (
42 from . import (
43 hgweb_mod,
43 hgweb_mod,
44 webutil,
44 webutil,
45 wsgicgi,
45 wsgicgi,
46 )
46 )
47
47
48 def cleannames(items):
48 def cleannames(items):
49 return [(util.pconvert(name).strip('/'), path) for name, path in items]
49 return [(util.pconvert(name).strip('/'), path) for name, path in items]
50
50
51 def findrepos(paths):
51 def findrepos(paths):
52 repos = []
52 repos = []
53 for prefix, root in cleannames(paths):
53 for prefix, root in cleannames(paths):
54 roothead, roottail = os.path.split(root)
54 roothead, roottail = os.path.split(root)
55 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
55 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
56 # /bar/ be served as as foo/N .
56 # /bar/ be served as as foo/N .
57 # '*' will not search inside dirs with .hg (except .hg/patches),
57 # '*' will not search inside dirs with .hg (except .hg/patches),
58 # '**' will search inside dirs with .hg (and thus also find subrepos).
58 # '**' will search inside dirs with .hg (and thus also find subrepos).
59 try:
59 try:
60 recurse = {'*': False, '**': True}[roottail]
60 recurse = {'*': False, '**': True}[roottail]
61 except KeyError:
61 except KeyError:
62 repos.append((prefix, root))
62 repos.append((prefix, root))
63 continue
63 continue
64 roothead = os.path.normpath(os.path.abspath(roothead))
64 roothead = os.path.normpath(os.path.abspath(roothead))
65 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
65 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
66 repos.extend(urlrepos(prefix, roothead, paths))
66 repos.extend(urlrepos(prefix, roothead, paths))
67 return repos
67 return repos
68
68
69 def urlrepos(prefix, roothead, paths):
69 def urlrepos(prefix, roothead, paths):
70 """yield url paths and filesystem paths from a list of repo paths
70 """yield url paths and filesystem paths from a list of repo paths
71
71
72 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
72 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
73 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
73 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
74 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
74 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
75 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
75 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
76 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
76 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
77 """
77 """
78 for path in paths:
78 for path in paths:
79 path = os.path.normpath(path)
79 path = os.path.normpath(path)
80 yield (prefix + '/' +
80 yield (prefix + '/' +
81 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
81 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
82
82
83 def geturlcgivars(baseurl, port):
83 def geturlcgivars(baseurl, port):
84 """
84 """
85 Extract CGI variables from baseurl
85 Extract CGI variables from baseurl
86
86
87 >>> geturlcgivars("http://host.org/base", "80")
87 >>> geturlcgivars("http://host.org/base", "80")
88 ('host.org', '80', '/base')
88 ('host.org', '80', '/base')
89 >>> geturlcgivars("http://host.org:8000/base", "80")
89 >>> geturlcgivars("http://host.org:8000/base", "80")
90 ('host.org', '8000', '/base')
90 ('host.org', '8000', '/base')
91 >>> geturlcgivars('/base', 8000)
91 >>> geturlcgivars('/base', 8000)
92 ('', '8000', '/base')
92 ('', '8000', '/base')
93 >>> geturlcgivars("base", '8000')
93 >>> geturlcgivars("base", '8000')
94 ('', '8000', '/base')
94 ('', '8000', '/base')
95 >>> geturlcgivars("http://host", '8000')
95 >>> geturlcgivars("http://host", '8000')
96 ('host', '8000', '/')
96 ('host', '8000', '/')
97 >>> geturlcgivars("http://host/", '8000')
97 >>> geturlcgivars("http://host/", '8000')
98 ('host', '8000', '/')
98 ('host', '8000', '/')
99 """
99 """
100 u = util.url(baseurl)
100 u = util.url(baseurl)
101 name = u.host or ''
101 name = u.host or ''
102 if u.port:
102 if u.port:
103 port = u.port
103 port = u.port
104 path = u.path or ""
104 path = u.path or ""
105 if not path.startswith('/'):
105 if not path.startswith('/'):
106 path = '/' + path
106 path = '/' + path
107
107
108 return name, str(port), path
108 return name, str(port), path
109
109
110 class hgwebdir(object):
110 class hgwebdir(object):
111 """HTTP server for multiple repositories.
111 """HTTP server for multiple repositories.
112
112
113 Given a configuration, different repositories will be served depending
113 Given a configuration, different repositories will be served depending
114 on the request path.
114 on the request path.
115
115
116 Instances are typically used as WSGI applications.
116 Instances are typically used as WSGI applications.
117 """
117 """
118 def __init__(self, conf, baseui=None):
118 def __init__(self, conf, baseui=None):
119 self.conf = conf
119 self.conf = conf
120 self.baseui = baseui
120 self.baseui = baseui
121 self.ui = None
121 self.ui = None
122 self.lastrefresh = 0
122 self.lastrefresh = 0
123 self.motd = None
123 self.motd = None
124 self.refresh()
124 self.refresh()
125
125
126 def refresh(self):
126 def refresh(self):
127 refreshinterval = 20
127 refreshinterval = 20
128 if self.ui:
128 if self.ui:
129 refreshinterval = self.ui.configint('web', 'refreshinterval',
129 refreshinterval = self.ui.configint('web', 'refreshinterval',
130 refreshinterval)
130 refreshinterval)
131
131
132 # refreshinterval <= 0 means to always refresh.
132 # refreshinterval <= 0 means to always refresh.
133 if (refreshinterval > 0 and
133 if (refreshinterval > 0 and
134 self.lastrefresh + refreshinterval > time.time()):
134 self.lastrefresh + refreshinterval > time.time()):
135 return
135 return
136
136
137 if self.baseui:
137 if self.baseui:
138 u = self.baseui.copy()
138 u = self.baseui.copy()
139 else:
139 else:
140 u = uimod.ui.load()
140 u = uimod.ui.load()
141 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
141 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
142 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
142 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
143 # displaying bundling progress bar while serving feels wrong and may
143 # displaying bundling progress bar while serving feels wrong and may
144 # break some wsgi implementations.
144 # break some wsgi implementations.
145 u.setconfig('progress', 'disable', 'true', 'hgweb')
145 u.setconfig('progress', 'disable', 'true', 'hgweb')
146
146
147 if not isinstance(self.conf, (dict, list, tuple)):
147 if not isinstance(self.conf, (dict, list, tuple)):
148 map = {'paths': 'hgweb-paths'}
148 map = {'paths': 'hgweb-paths'}
149 if not os.path.exists(self.conf):
149 if not os.path.exists(self.conf):
150 raise error.Abort(_('config file %s not found!') % self.conf)
150 raise error.Abort(_('config file %s not found!') % self.conf)
151 u.readconfig(self.conf, remap=map, trust=True)
151 u.readconfig(self.conf, remap=map, trust=True)
152 paths = []
152 paths = []
153 for name, ignored in u.configitems('hgweb-paths'):
153 for name, ignored in u.configitems('hgweb-paths'):
154 for path in u.configlist('hgweb-paths', name):
154 for path in u.configlist('hgweb-paths', name):
155 paths.append((name, path))
155 paths.append((name, path))
156 elif isinstance(self.conf, (list, tuple)):
156 elif isinstance(self.conf, (list, tuple)):
157 paths = self.conf
157 paths = self.conf
158 elif isinstance(self.conf, dict):
158 elif isinstance(self.conf, dict):
159 paths = self.conf.items()
159 paths = self.conf.items()
160
160
161 repos = findrepos(paths)
161 repos = findrepos(paths)
162 for prefix, root in u.configitems('collections'):
162 for prefix, root in u.configitems('collections'):
163 prefix = util.pconvert(prefix)
163 prefix = util.pconvert(prefix)
164 for path in scmutil.walkrepos(root, followsym=True):
164 for path in scmutil.walkrepos(root, followsym=True):
165 repo = os.path.normpath(path)
165 repo = os.path.normpath(path)
166 name = util.pconvert(repo)
166 name = util.pconvert(repo)
167 if name.startswith(prefix):
167 if name.startswith(prefix):
168 name = name[len(prefix):]
168 name = name[len(prefix):]
169 repos.append((name.lstrip('/'), repo))
169 repos.append((name.lstrip('/'), repo))
170
170
171 self.repos = repos
171 self.repos = repos
172 self.ui = u
172 self.ui = u
173 encoding.encoding = self.ui.config('web', 'encoding',
173 encoding.encoding = self.ui.config('web', 'encoding',
174 encoding.encoding)
174 encoding.encoding)
175 self.style = self.ui.config('web', 'style', 'paper')
175 self.style = self.ui.config('web', 'style', 'paper')
176 self.templatepath = self.ui.config('web', 'templates', None)
176 self.templatepath = self.ui.config('web', 'templates', None)
177 self.stripecount = self.ui.config('web', 'stripes', 1)
177 self.stripecount = self.ui.config('web', 'stripes', 1)
178 if self.stripecount:
178 if self.stripecount:
179 self.stripecount = int(self.stripecount)
179 self.stripecount = int(self.stripecount)
180 self._baseurl = self.ui.config('web', 'baseurl')
180 self._baseurl = self.ui.config('web', 'baseurl')
181 prefix = self.ui.config('web', 'prefix', '')
181 prefix = self.ui.config('web', 'prefix', '')
182 if prefix.startswith('/'):
182 if prefix.startswith('/'):
183 prefix = prefix[1:]
183 prefix = prefix[1:]
184 if prefix.endswith('/'):
184 if prefix.endswith('/'):
185 prefix = prefix[:-1]
185 prefix = prefix[:-1]
186 self.prefix = prefix
186 self.prefix = prefix
187 self.lastrefresh = time.time()
187 self.lastrefresh = time.time()
188
188
189 def run(self):
189 def run(self):
190 if not encoding.environ.get('GATEWAY_INTERFACE',
190 if not encoding.environ.get('GATEWAY_INTERFACE',
191 '').startswith("CGI/1."):
191 '').startswith("CGI/1."):
192 raise RuntimeError("This function is only intended to be "
192 raise RuntimeError("This function is only intended to be "
193 "called while running as a CGI script.")
193 "called while running as a CGI script.")
194 wsgicgi.launch(self)
194 wsgicgi.launch(self)
195
195
196 def __call__(self, env, respond):
196 def __call__(self, env, respond):
197 req = wsgirequest(env, respond)
197 req = wsgirequest(env, respond)
198 return self.run_wsgi(req)
198 return self.run_wsgi(req)
199
199
200 def read_allowed(self, ui, req):
200 def read_allowed(self, ui, req):
201 """Check allow_read and deny_read config options of a repo's ui object
201 """Check allow_read and deny_read config options of a repo's ui object
202 to determine user permissions. By default, with neither option set (or
202 to determine user permissions. By default, with neither option set (or
203 both empty), allow all users to read the repo. There are two ways a
203 both empty), allow all users to read the repo. There are two ways a
204 user can be denied read access: (1) deny_read is not empty, and the
204 user can be denied read access: (1) deny_read is not empty, and the
205 user is unauthenticated or deny_read contains user (or *), and (2)
205 user is unauthenticated or deny_read contains user (or *), and (2)
206 allow_read is not empty and the user is not in allow_read. Return True
206 allow_read is not empty and the user is not in allow_read. Return True
207 if user is allowed to read the repo, else return False."""
207 if user is allowed to read the repo, else return False."""
208
208
209 user = req.env.get('REMOTE_USER')
209 user = req.env.get('REMOTE_USER')
210
210
211 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
211 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
212 if deny_read and (not user or ismember(ui, user, deny_read)):
212 if deny_read and (not user or ismember(ui, user, deny_read)):
213 return False
213 return False
214
214
215 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
215 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
216 # by default, allow reading if no allow_read option has been set
216 # by default, allow reading if no allow_read option has been set
217 if (not allow_read) or ismember(ui, user, allow_read):
217 if (not allow_read) or ismember(ui, user, allow_read):
218 return True
218 return True
219
219
220 return False
220 return False
221
221
222 def run_wsgi(self, req):
222 def run_wsgi(self, req):
223 with profiling.maybeprofile(self.ui):
223 profile = self.ui.configbool('profiling', 'enabled')
224 with profiling.profile(self.ui, enabled=profile):
224 for r in self._runwsgi(req):
225 for r in self._runwsgi(req):
225 yield r
226 yield r
226
227
227 def _runwsgi(self, req):
228 def _runwsgi(self, req):
228 try:
229 try:
229 self.refresh()
230 self.refresh()
230
231
231 csp, nonce = cspvalues(self.ui)
232 csp, nonce = cspvalues(self.ui)
232 if csp:
233 if csp:
233 req.headers.append(('Content-Security-Policy', csp))
234 req.headers.append(('Content-Security-Policy', csp))
234
235
235 virtual = req.env.get("PATH_INFO", "").strip('/')
236 virtual = req.env.get("PATH_INFO", "").strip('/')
236 tmpl = self.templater(req, nonce)
237 tmpl = self.templater(req, nonce)
237 ctype = tmpl('mimetype', encoding=encoding.encoding)
238 ctype = tmpl('mimetype', encoding=encoding.encoding)
238 ctype = templater.stringify(ctype)
239 ctype = templater.stringify(ctype)
239
240
240 # a static file
241 # a static file
241 if virtual.startswith('static/') or 'static' in req.form:
242 if virtual.startswith('static/') or 'static' in req.form:
242 if virtual.startswith('static/'):
243 if virtual.startswith('static/'):
243 fname = virtual[7:]
244 fname = virtual[7:]
244 else:
245 else:
245 fname = req.form['static'][0]
246 fname = req.form['static'][0]
246 static = self.ui.config("web", "static", None,
247 static = self.ui.config("web", "static", None,
247 untrusted=False)
248 untrusted=False)
248 if not static:
249 if not static:
249 tp = self.templatepath or templater.templatepaths()
250 tp = self.templatepath or templater.templatepaths()
250 if isinstance(tp, str):
251 if isinstance(tp, str):
251 tp = [tp]
252 tp = [tp]
252 static = [os.path.join(p, 'static') for p in tp]
253 static = [os.path.join(p, 'static') for p in tp]
253 staticfile(static, fname, req)
254 staticfile(static, fname, req)
254 return []
255 return []
255
256
256 # top-level index
257 # top-level index
257
258
258 repos = dict(self.repos)
259 repos = dict(self.repos)
259
260
260 if (not virtual or virtual == 'index') and virtual not in repos:
261 if (not virtual or virtual == 'index') and virtual not in repos:
261 req.respond(HTTP_OK, ctype)
262 req.respond(HTTP_OK, ctype)
262 return self.makeindex(req, tmpl)
263 return self.makeindex(req, tmpl)
263
264
264 # nested indexes and hgwebs
265 # nested indexes and hgwebs
265
266
266 if virtual.endswith('/index') and virtual not in repos:
267 if virtual.endswith('/index') and virtual not in repos:
267 subdir = virtual[:-len('index')]
268 subdir = virtual[:-len('index')]
268 if any(r.startswith(subdir) for r in repos):
269 if any(r.startswith(subdir) for r in repos):
269 req.respond(HTTP_OK, ctype)
270 req.respond(HTTP_OK, ctype)
270 return self.makeindex(req, tmpl, subdir)
271 return self.makeindex(req, tmpl, subdir)
271
272
272 def _virtualdirs():
273 def _virtualdirs():
273 # Check the full virtual path, each parent, and the root ('')
274 # Check the full virtual path, each parent, and the root ('')
274 if virtual != '':
275 if virtual != '':
275 yield virtual
276 yield virtual
276
277
277 for p in util.finddirs(virtual):
278 for p in util.finddirs(virtual):
278 yield p
279 yield p
279
280
280 yield ''
281 yield ''
281
282
282 for virtualrepo in _virtualdirs():
283 for virtualrepo in _virtualdirs():
283 real = repos.get(virtualrepo)
284 real = repos.get(virtualrepo)
284 if real:
285 if real:
285 req.env['REPO_NAME'] = virtualrepo
286 req.env['REPO_NAME'] = virtualrepo
286 try:
287 try:
287 # ensure caller gets private copy of ui
288 # ensure caller gets private copy of ui
288 repo = hg.repository(self.ui.copy(), real)
289 repo = hg.repository(self.ui.copy(), real)
289 return hgweb_mod.hgweb(repo).run_wsgi(req)
290 return hgweb_mod.hgweb(repo).run_wsgi(req)
290 except IOError as inst:
291 except IOError as inst:
291 msg = inst.strerror
292 msg = inst.strerror
292 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
293 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
293 except error.RepoError as inst:
294 except error.RepoError as inst:
294 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
295 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
295
296
296 # browse subdirectories
297 # browse subdirectories
297 subdir = virtual + '/'
298 subdir = virtual + '/'
298 if [r for r in repos if r.startswith(subdir)]:
299 if [r for r in repos if r.startswith(subdir)]:
299 req.respond(HTTP_OK, ctype)
300 req.respond(HTTP_OK, ctype)
300 return self.makeindex(req, tmpl, subdir)
301 return self.makeindex(req, tmpl, subdir)
301
302
302 # prefixes not found
303 # prefixes not found
303 req.respond(HTTP_NOT_FOUND, ctype)
304 req.respond(HTTP_NOT_FOUND, ctype)
304 return tmpl("notfound", repo=virtual)
305 return tmpl("notfound", repo=virtual)
305
306
306 except ErrorResponse as err:
307 except ErrorResponse as err:
307 req.respond(err, ctype)
308 req.respond(err, ctype)
308 return tmpl('error', error=err.message or '')
309 return tmpl('error', error=err.message or '')
309 finally:
310 finally:
310 tmpl = None
311 tmpl = None
311
312
312 def makeindex(self, req, tmpl, subdir=""):
313 def makeindex(self, req, tmpl, subdir=""):
313
314
314 def archivelist(ui, nodeid, url):
315 def archivelist(ui, nodeid, url):
315 allowed = ui.configlist("web", "allow_archive", untrusted=True)
316 allowed = ui.configlist("web", "allow_archive", untrusted=True)
316 archives = []
317 archives = []
317 for typ, spec in hgweb_mod.archivespecs.iteritems():
318 for typ, spec in hgweb_mod.archivespecs.iteritems():
318 if typ in allowed or ui.configbool("web", "allow" + typ,
319 if typ in allowed or ui.configbool("web", "allow" + typ,
319 untrusted=True):
320 untrusted=True):
320 archives.append({"type" : typ, "extension": spec[2],
321 archives.append({"type" : typ, "extension": spec[2],
321 "node": nodeid, "url": url})
322 "node": nodeid, "url": url})
322 return archives
323 return archives
323
324
324 def rawentries(subdir="", **map):
325 def rawentries(subdir="", **map):
325
326
326 descend = self.ui.configbool('web', 'descend', True)
327 descend = self.ui.configbool('web', 'descend', True)
327 collapse = self.ui.configbool('web', 'collapse', False)
328 collapse = self.ui.configbool('web', 'collapse', False)
328 seenrepos = set()
329 seenrepos = set()
329 seendirs = set()
330 seendirs = set()
330 for name, path in self.repos:
331 for name, path in self.repos:
331
332
332 if not name.startswith(subdir):
333 if not name.startswith(subdir):
333 continue
334 continue
334 name = name[len(subdir):]
335 name = name[len(subdir):]
335 directory = False
336 directory = False
336
337
337 if '/' in name:
338 if '/' in name:
338 if not descend:
339 if not descend:
339 continue
340 continue
340
341
341 nameparts = name.split('/')
342 nameparts = name.split('/')
342 rootname = nameparts[0]
343 rootname = nameparts[0]
343
344
344 if not collapse:
345 if not collapse:
345 pass
346 pass
346 elif rootname in seendirs:
347 elif rootname in seendirs:
347 continue
348 continue
348 elif rootname in seenrepos:
349 elif rootname in seenrepos:
349 pass
350 pass
350 else:
351 else:
351 directory = True
352 directory = True
352 name = rootname
353 name = rootname
353
354
354 # redefine the path to refer to the directory
355 # redefine the path to refer to the directory
355 discarded = '/'.join(nameparts[1:])
356 discarded = '/'.join(nameparts[1:])
356
357
357 # remove name parts plus accompanying slash
358 # remove name parts plus accompanying slash
358 path = path[:-len(discarded) - 1]
359 path = path[:-len(discarded) - 1]
359
360
360 try:
361 try:
361 r = hg.repository(self.ui, path)
362 r = hg.repository(self.ui, path)
362 directory = False
363 directory = False
363 except (IOError, error.RepoError):
364 except (IOError, error.RepoError):
364 pass
365 pass
365
366
366 parts = [name]
367 parts = [name]
367 parts.insert(0, '/' + subdir.rstrip('/'))
368 parts.insert(0, '/' + subdir.rstrip('/'))
368 if req.env['SCRIPT_NAME']:
369 if req.env['SCRIPT_NAME']:
369 parts.insert(0, req.env['SCRIPT_NAME'])
370 parts.insert(0, req.env['SCRIPT_NAME'])
370 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
371 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
371
372
372 # show either a directory entry or a repository
373 # show either a directory entry or a repository
373 if directory:
374 if directory:
374 # get the directory's time information
375 # get the directory's time information
375 try:
376 try:
376 d = (get_mtime(path), util.makedate()[1])
377 d = (get_mtime(path), util.makedate()[1])
377 except OSError:
378 except OSError:
378 continue
379 continue
379
380
380 # add '/' to the name to make it obvious that
381 # add '/' to the name to make it obvious that
381 # the entry is a directory, not a regular repository
382 # the entry is a directory, not a regular repository
382 row = {'contact': "",
383 row = {'contact': "",
383 'contact_sort': "",
384 'contact_sort': "",
384 'name': name + '/',
385 'name': name + '/',
385 'name_sort': name,
386 'name_sort': name,
386 'url': url,
387 'url': url,
387 'description': "",
388 'description': "",
388 'description_sort': "",
389 'description_sort': "",
389 'lastchange': d,
390 'lastchange': d,
390 'lastchange_sort': d[1]-d[0],
391 'lastchange_sort': d[1]-d[0],
391 'archives': [],
392 'archives': [],
392 'isdirectory': True,
393 'isdirectory': True,
393 'labels': [],
394 'labels': [],
394 }
395 }
395
396
396 seendirs.add(name)
397 seendirs.add(name)
397 yield row
398 yield row
398 continue
399 continue
399
400
400 u = self.ui.copy()
401 u = self.ui.copy()
401 try:
402 try:
402 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
403 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
403 except Exception as e:
404 except Exception as e:
404 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
405 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
405 continue
406 continue
406 def get(section, name, default=None):
407 def get(section, name, default=None):
407 return u.config(section, name, default, untrusted=True)
408 return u.config(section, name, default, untrusted=True)
408
409
409 if u.configbool("web", "hidden", untrusted=True):
410 if u.configbool("web", "hidden", untrusted=True):
410 continue
411 continue
411
412
412 if not self.read_allowed(u, req):
413 if not self.read_allowed(u, req):
413 continue
414 continue
414
415
415 # update time with local timezone
416 # update time with local timezone
416 try:
417 try:
417 r = hg.repository(self.ui, path)
418 r = hg.repository(self.ui, path)
418 except IOError:
419 except IOError:
419 u.warn(_('error accessing repository at %s\n') % path)
420 u.warn(_('error accessing repository at %s\n') % path)
420 continue
421 continue
421 except error.RepoError:
422 except error.RepoError:
422 u.warn(_('error accessing repository at %s\n') % path)
423 u.warn(_('error accessing repository at %s\n') % path)
423 continue
424 continue
424 try:
425 try:
425 d = (get_mtime(r.spath), util.makedate()[1])
426 d = (get_mtime(r.spath), util.makedate()[1])
426 except OSError:
427 except OSError:
427 continue
428 continue
428
429
429 contact = get_contact(get)
430 contact = get_contact(get)
430 description = get("web", "description", "")
431 description = get("web", "description", "")
431 seenrepos.add(name)
432 seenrepos.add(name)
432 name = get("web", "name", name)
433 name = get("web", "name", name)
433 row = {'contact': contact or "unknown",
434 row = {'contact': contact or "unknown",
434 'contact_sort': contact.upper() or "unknown",
435 'contact_sort': contact.upper() or "unknown",
435 'name': name,
436 'name': name,
436 'name_sort': name,
437 'name_sort': name,
437 'url': url,
438 'url': url,
438 'description': description or "unknown",
439 'description': description or "unknown",
439 'description_sort': description.upper() or "unknown",
440 'description_sort': description.upper() or "unknown",
440 'lastchange': d,
441 'lastchange': d,
441 'lastchange_sort': d[1]-d[0],
442 'lastchange_sort': d[1]-d[0],
442 'archives': archivelist(u, "tip", url),
443 'archives': archivelist(u, "tip", url),
443 'isdirectory': None,
444 'isdirectory': None,
444 'labels': u.configlist('web', 'labels', untrusted=True),
445 'labels': u.configlist('web', 'labels', untrusted=True),
445 }
446 }
446
447
447 yield row
448 yield row
448
449
449 sortdefault = None, False
450 sortdefault = None, False
450 def entries(sortcolumn="", descending=False, subdir="", **map):
451 def entries(sortcolumn="", descending=False, subdir="", **map):
451 rows = rawentries(subdir=subdir, **map)
452 rows = rawentries(subdir=subdir, **map)
452
453
453 if sortcolumn and sortdefault != (sortcolumn, descending):
454 if sortcolumn and sortdefault != (sortcolumn, descending):
454 sortkey = '%s_sort' % sortcolumn
455 sortkey = '%s_sort' % sortcolumn
455 rows = sorted(rows, key=lambda x: x[sortkey],
456 rows = sorted(rows, key=lambda x: x[sortkey],
456 reverse=descending)
457 reverse=descending)
457 for row, parity in zip(rows, paritygen(self.stripecount)):
458 for row, parity in zip(rows, paritygen(self.stripecount)):
458 row['parity'] = parity
459 row['parity'] = parity
459 yield row
460 yield row
460
461
461 self.refresh()
462 self.refresh()
462 sortable = ["name", "description", "contact", "lastchange"]
463 sortable = ["name", "description", "contact", "lastchange"]
463 sortcolumn, descending = sortdefault
464 sortcolumn, descending = sortdefault
464 if 'sort' in req.form:
465 if 'sort' in req.form:
465 sortcolumn = req.form['sort'][0]
466 sortcolumn = req.form['sort'][0]
466 descending = sortcolumn.startswith('-')
467 descending = sortcolumn.startswith('-')
467 if descending:
468 if descending:
468 sortcolumn = sortcolumn[1:]
469 sortcolumn = sortcolumn[1:]
469 if sortcolumn not in sortable:
470 if sortcolumn not in sortable:
470 sortcolumn = ""
471 sortcolumn = ""
471
472
472 sort = [("sort_%s" % column,
473 sort = [("sort_%s" % column,
473 "%s%s" % ((not descending and column == sortcolumn)
474 "%s%s" % ((not descending and column == sortcolumn)
474 and "-" or "", column))
475 and "-" or "", column))
475 for column in sortable]
476 for column in sortable]
476
477
477 self.refresh()
478 self.refresh()
478 self.updatereqenv(req.env)
479 self.updatereqenv(req.env)
479
480
480 return tmpl("index", entries=entries, subdir=subdir,
481 return tmpl("index", entries=entries, subdir=subdir,
481 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
482 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
482 sortcolumn=sortcolumn, descending=descending,
483 sortcolumn=sortcolumn, descending=descending,
483 **dict(sort))
484 **dict(sort))
484
485
485 def templater(self, req, nonce):
486 def templater(self, req, nonce):
486
487
487 def motd(**map):
488 def motd(**map):
488 if self.motd is not None:
489 if self.motd is not None:
489 yield self.motd
490 yield self.motd
490 else:
491 else:
491 yield config('web', 'motd', '')
492 yield config('web', 'motd', '')
492
493
493 def config(section, name, default=None, untrusted=True):
494 def config(section, name, default=None, untrusted=True):
494 return self.ui.config(section, name, default, untrusted)
495 return self.ui.config(section, name, default, untrusted)
495
496
496 self.updatereqenv(req.env)
497 self.updatereqenv(req.env)
497
498
498 url = req.env.get('SCRIPT_NAME', '')
499 url = req.env.get('SCRIPT_NAME', '')
499 if not url.endswith('/'):
500 if not url.endswith('/'):
500 url += '/'
501 url += '/'
501
502
502 vars = {}
503 vars = {}
503 styles = (
504 styles = (
504 req.form.get('style', [None])[0],
505 req.form.get('style', [None])[0],
505 config('web', 'style'),
506 config('web', 'style'),
506 'paper'
507 'paper'
507 )
508 )
508 style, mapfile = templater.stylemap(styles, self.templatepath)
509 style, mapfile = templater.stylemap(styles, self.templatepath)
509 if style == styles[0]:
510 if style == styles[0]:
510 vars['style'] = style
511 vars['style'] = style
511
512
512 start = url[-1] == '?' and '&' or '?'
513 start = url[-1] == '?' and '&' or '?'
513 sessionvars = webutil.sessionvars(vars, start)
514 sessionvars = webutil.sessionvars(vars, start)
514 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
515 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
515 logoimg = config('web', 'logoimg', 'hglogo.png')
516 logoimg = config('web', 'logoimg', 'hglogo.png')
516 staticurl = config('web', 'staticurl') or url + 'static/'
517 staticurl = config('web', 'staticurl') or url + 'static/'
517 if not staticurl.endswith('/'):
518 if not staticurl.endswith('/'):
518 staticurl += '/'
519 staticurl += '/'
519
520
520 defaults = {
521 defaults = {
521 "encoding": encoding.encoding,
522 "encoding": encoding.encoding,
522 "motd": motd,
523 "motd": motd,
523 "url": url,
524 "url": url,
524 "logourl": logourl,
525 "logourl": logourl,
525 "logoimg": logoimg,
526 "logoimg": logoimg,
526 "staticurl": staticurl,
527 "staticurl": staticurl,
527 "sessionvars": sessionvars,
528 "sessionvars": sessionvars,
528 "style": style,
529 "style": style,
529 "nonce": nonce,
530 "nonce": nonce,
530 }
531 }
531 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
532 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
532 return tmpl
533 return tmpl
533
534
534 def updatereqenv(self, env):
535 def updatereqenv(self, env):
535 if self._baseurl is not None:
536 if self._baseurl is not None:
536 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
537 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
537 env['SERVER_NAME'] = name
538 env['SERVER_NAME'] = name
538 env['SERVER_PORT'] = port
539 env['SERVER_PORT'] = port
539 env['SCRIPT_NAME'] = path
540 env['SCRIPT_NAME'] = path
@@ -1,235 +1,221 b''
1 # profiling.py - profiling functions
1 # profiling.py - profiling functions
2 #
2 #
3 # Copyright 2016 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2016 Gregory Szorc <gregory.szorc@gmail.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, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import contextlib
10 import contextlib
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 encoding,
14 encoding,
15 error,
15 error,
16 extensions,
16 extensions,
17 util,
17 util,
18 )
18 )
19
19
20 def _loadprofiler(ui, profiler):
20 def _loadprofiler(ui, profiler):
21 """load profiler extension. return profile method, or None on failure"""
21 """load profiler extension. return profile method, or None on failure"""
22 extname = profiler
22 extname = profiler
23 extensions.loadall(ui, whitelist=[extname])
23 extensions.loadall(ui, whitelist=[extname])
24 try:
24 try:
25 mod = extensions.find(extname)
25 mod = extensions.find(extname)
26 except KeyError:
26 except KeyError:
27 return None
27 return None
28 else:
28 else:
29 return getattr(mod, 'profile', None)
29 return getattr(mod, 'profile', None)
30
30
31 @contextlib.contextmanager
31 @contextlib.contextmanager
32 def lsprofile(ui, fp):
32 def lsprofile(ui, fp):
33 format = ui.config('profiling', 'format', default='text')
33 format = ui.config('profiling', 'format', default='text')
34 field = ui.config('profiling', 'sort', default='inlinetime')
34 field = ui.config('profiling', 'sort', default='inlinetime')
35 limit = ui.configint('profiling', 'limit', default=30)
35 limit = ui.configint('profiling', 'limit', default=30)
36 climit = ui.configint('profiling', 'nested', default=0)
36 climit = ui.configint('profiling', 'nested', default=0)
37
37
38 if format not in ['text', 'kcachegrind']:
38 if format not in ['text', 'kcachegrind']:
39 ui.warn(_("unrecognized profiling format '%s'"
39 ui.warn(_("unrecognized profiling format '%s'"
40 " - Ignored\n") % format)
40 " - Ignored\n") % format)
41 format = 'text'
41 format = 'text'
42
42
43 try:
43 try:
44 from . import lsprof
44 from . import lsprof
45 except ImportError:
45 except ImportError:
46 raise error.Abort(_(
46 raise error.Abort(_(
47 'lsprof not available - install from '
47 'lsprof not available - install from '
48 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
48 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
49 p = lsprof.Profiler()
49 p = lsprof.Profiler()
50 p.enable(subcalls=True)
50 p.enable(subcalls=True)
51 try:
51 try:
52 yield
52 yield
53 finally:
53 finally:
54 p.disable()
54 p.disable()
55
55
56 if format == 'kcachegrind':
56 if format == 'kcachegrind':
57 from . import lsprofcalltree
57 from . import lsprofcalltree
58 calltree = lsprofcalltree.KCacheGrind(p)
58 calltree = lsprofcalltree.KCacheGrind(p)
59 calltree.output(fp)
59 calltree.output(fp)
60 else:
60 else:
61 # format == 'text'
61 # format == 'text'
62 stats = lsprof.Stats(p.getstats())
62 stats = lsprof.Stats(p.getstats())
63 stats.sort(field)
63 stats.sort(field)
64 stats.pprint(limit=limit, file=fp, climit=climit)
64 stats.pprint(limit=limit, file=fp, climit=climit)
65
65
66 @contextlib.contextmanager
66 @contextlib.contextmanager
67 def flameprofile(ui, fp):
67 def flameprofile(ui, fp):
68 try:
68 try:
69 from flamegraph import flamegraph
69 from flamegraph import flamegraph
70 except ImportError:
70 except ImportError:
71 raise error.Abort(_(
71 raise error.Abort(_(
72 'flamegraph not available - install from '
72 'flamegraph not available - install from '
73 'https://github.com/evanhempel/python-flamegraph'))
73 'https://github.com/evanhempel/python-flamegraph'))
74 # developer config: profiling.freq
74 # developer config: profiling.freq
75 freq = ui.configint('profiling', 'freq', default=1000)
75 freq = ui.configint('profiling', 'freq', default=1000)
76 filter_ = None
76 filter_ = None
77 collapse_recursion = True
77 collapse_recursion = True
78 thread = flamegraph.ProfileThread(fp, 1.0 / freq,
78 thread = flamegraph.ProfileThread(fp, 1.0 / freq,
79 filter_, collapse_recursion)
79 filter_, collapse_recursion)
80 start_time = util.timer()
80 start_time = util.timer()
81 try:
81 try:
82 thread.start()
82 thread.start()
83 yield
83 yield
84 finally:
84 finally:
85 thread.stop()
85 thread.stop()
86 thread.join()
86 thread.join()
87 print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
87 print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
88 util.timer() - start_time, thread.num_frames(),
88 util.timer() - start_time, thread.num_frames(),
89 thread.num_frames(unique=True)))
89 thread.num_frames(unique=True)))
90
90
91 @contextlib.contextmanager
91 @contextlib.contextmanager
92 def statprofile(ui, fp):
92 def statprofile(ui, fp):
93 from . import statprof
93 from . import statprof
94
94
95 freq = ui.configint('profiling', 'freq', default=1000)
95 freq = ui.configint('profiling', 'freq', default=1000)
96 if freq > 0:
96 if freq > 0:
97 # Cannot reset when profiler is already active. So silently no-op.
97 # Cannot reset when profiler is already active. So silently no-op.
98 if statprof.state.profile_level == 0:
98 if statprof.state.profile_level == 0:
99 statprof.reset(freq)
99 statprof.reset(freq)
100 else:
100 else:
101 ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
101 ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
102
102
103 statprof.start(mechanism='thread')
103 statprof.start(mechanism='thread')
104
104
105 try:
105 try:
106 yield
106 yield
107 finally:
107 finally:
108 data = statprof.stop()
108 data = statprof.stop()
109
109
110 profformat = ui.config('profiling', 'statformat', 'hotpath')
110 profformat = ui.config('profiling', 'statformat', 'hotpath')
111
111
112 formats = {
112 formats = {
113 'byline': statprof.DisplayFormats.ByLine,
113 'byline': statprof.DisplayFormats.ByLine,
114 'bymethod': statprof.DisplayFormats.ByMethod,
114 'bymethod': statprof.DisplayFormats.ByMethod,
115 'hotpath': statprof.DisplayFormats.Hotpath,
115 'hotpath': statprof.DisplayFormats.Hotpath,
116 'json': statprof.DisplayFormats.Json,
116 'json': statprof.DisplayFormats.Json,
117 'chrome': statprof.DisplayFormats.Chrome,
117 'chrome': statprof.DisplayFormats.Chrome,
118 }
118 }
119
119
120 if profformat in formats:
120 if profformat in formats:
121 displayformat = formats[profformat]
121 displayformat = formats[profformat]
122 else:
122 else:
123 ui.warn(_('unknown profiler output format: %s\n') % profformat)
123 ui.warn(_('unknown profiler output format: %s\n') % profformat)
124 displayformat = statprof.DisplayFormats.Hotpath
124 displayformat = statprof.DisplayFormats.Hotpath
125
125
126 kwargs = {}
126 kwargs = {}
127
127
128 def fraction(s):
128 def fraction(s):
129 if s.endswith('%'):
129 if s.endswith('%'):
130 v = float(s[:-1]) / 100
130 v = float(s[:-1]) / 100
131 else:
131 else:
132 v = float(s)
132 v = float(s)
133 if 0 <= v <= 1:
133 if 0 <= v <= 1:
134 return v
134 return v
135 raise ValueError(s)
135 raise ValueError(s)
136
136
137 if profformat == 'chrome':
137 if profformat == 'chrome':
138 showmin = ui.configwith(fraction, 'profiling', 'showmin', 0.005)
138 showmin = ui.configwith(fraction, 'profiling', 'showmin', 0.005)
139 showmax = ui.configwith(fraction, 'profiling', 'showmax', 0.999)
139 showmax = ui.configwith(fraction, 'profiling', 'showmax', 0.999)
140 kwargs.update(minthreshold=showmin, maxthreshold=showmax)
140 kwargs.update(minthreshold=showmin, maxthreshold=showmax)
141
141
142 statprof.display(fp, data=data, format=displayformat, **kwargs)
142 statprof.display(fp, data=data, format=displayformat, **kwargs)
143
143
144 class profile(object):
144 class profile(object):
145 """Start profiling.
145 """Start profiling.
146
146
147 Profiling is active when the context manager is active. When the context
147 Profiling is active when the context manager is active. When the context
148 manager exits, profiling results will be written to the configured output.
148 manager exits, profiling results will be written to the configured output.
149 """
149 """
150 def __init__(self, ui, enabled=True):
150 def __init__(self, ui, enabled=True):
151 self._ui = ui
151 self._ui = ui
152 self._output = None
152 self._output = None
153 self._fp = None
153 self._fp = None
154 self._profiler = None
154 self._profiler = None
155 self._enabled = enabled
155 self._enabled = enabled
156 self._entered = False
156 self._entered = False
157 self._started = False
157 self._started = False
158
158
159 def __enter__(self):
159 def __enter__(self):
160 self._entered = True
160 self._entered = True
161 if self._enabled:
161 if self._enabled:
162 self.start()
162 self.start()
163 return self
163 return self
164
164
165 def start(self):
165 def start(self):
166 """Start profiling.
166 """Start profiling.
167
167
168 The profiling will stop at the context exit.
168 The profiling will stop at the context exit.
169
169
170 If the profiler was already started, this has no effect."""
170 If the profiler was already started, this has no effect."""
171 if not self._entered:
171 if not self._entered:
172 raise error.ProgrammingError()
172 raise error.ProgrammingError()
173 if self._started:
173 if self._started:
174 return
174 return
175 self._started = True
175 self._started = True
176 profiler = encoding.environ.get('HGPROF')
176 profiler = encoding.environ.get('HGPROF')
177 proffn = None
177 proffn = None
178 if profiler is None:
178 if profiler is None:
179 profiler = self._ui.config('profiling', 'type', default='stat')
179 profiler = self._ui.config('profiling', 'type', default='stat')
180 if profiler not in ('ls', 'stat', 'flame'):
180 if profiler not in ('ls', 'stat', 'flame'):
181 # try load profiler from extension with the same name
181 # try load profiler from extension with the same name
182 proffn = _loadprofiler(self._ui, profiler)
182 proffn = _loadprofiler(self._ui, profiler)
183 if proffn is None:
183 if proffn is None:
184 self._ui.warn(_("unrecognized profiler '%s' - ignored\n")
184 self._ui.warn(_("unrecognized profiler '%s' - ignored\n")
185 % profiler)
185 % profiler)
186 profiler = 'stat'
186 profiler = 'stat'
187
187
188 self._output = self._ui.config('profiling', 'output')
188 self._output = self._ui.config('profiling', 'output')
189
189
190 if self._output == 'blackbox':
190 if self._output == 'blackbox':
191 self._fp = util.stringio()
191 self._fp = util.stringio()
192 elif self._output:
192 elif self._output:
193 path = self._ui.expandpath(self._output)
193 path = self._ui.expandpath(self._output)
194 self._fp = open(path, 'wb')
194 self._fp = open(path, 'wb')
195 else:
195 else:
196 self._fp = self._ui.ferr
196 self._fp = self._ui.ferr
197
197
198 if proffn is not None:
198 if proffn is not None:
199 pass
199 pass
200 elif profiler == 'ls':
200 elif profiler == 'ls':
201 proffn = lsprofile
201 proffn = lsprofile
202 elif profiler == 'flame':
202 elif profiler == 'flame':
203 proffn = flameprofile
203 proffn = flameprofile
204 else:
204 else:
205 proffn = statprofile
205 proffn = statprofile
206
206
207 self._profiler = proffn(self._ui, self._fp)
207 self._profiler = proffn(self._ui, self._fp)
208 self._profiler.__enter__()
208 self._profiler.__enter__()
209
209
210 def __exit__(self, exception_type, exception_value, traceback):
210 def __exit__(self, exception_type, exception_value, traceback):
211 if self._profiler is None:
211 if self._profiler is None:
212 return
212 return
213 self._profiler.__exit__(exception_type, exception_value, traceback)
213 self._profiler.__exit__(exception_type, exception_value, traceback)
214 if self._output:
214 if self._output:
215 if self._output == 'blackbox':
215 if self._output == 'blackbox':
216 val = 'Profile:\n%s' % self._fp.getvalue()
216 val = 'Profile:\n%s' % self._fp.getvalue()
217 # ui.log treats the input as a format string,
217 # ui.log treats the input as a format string,
218 # so we need to escape any % signs.
218 # so we need to escape any % signs.
219 val = val.replace('%', '%%')
219 val = val.replace('%', '%%')
220 self._ui.log('profile', val)
220 self._ui.log('profile', val)
221 self._fp.close()
221 self._fp.close()
222
223 @contextlib.contextmanager
224 def maybeprofile(ui):
225 """Profile if enabled, else do nothing.
226
227 This context manager can be used to optionally profile if profiling
228 is enabled. Otherwise, it does nothing.
229
230 The purpose of this context manager is to make calling code simpler:
231 just use a single code path for calling into code you may want to profile
232 and this function determines whether to start profiling.
233 """
234 with profile(ui, enabled=ui.configbool('profiling', 'enabled')) as p:
235 yield p
General Comments 0
You need to be logged in to leave comments. Login now