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