##// END OF EJS Templates
registrar: add templatekeyword to mark a function as template keyword (API)...
FUJIWARA Katsunori -
r28538:009f58f1 default
parent child Browse files
Show More
@@ -1,1054 +1,1056 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 atexit
11 11 import difflib
12 12 import errno
13 13 import os
14 14 import pdb
15 15 import re
16 16 import shlex
17 17 import signal
18 18 import socket
19 19 import sys
20 20 import time
21 21 import traceback
22 22
23 23
24 24 from .i18n import _
25 25
26 26 from . import (
27 27 cmdutil,
28 28 commands,
29 29 demandimport,
30 30 encoding,
31 31 error,
32 32 extensions,
33 33 fancyopts,
34 34 fileset,
35 35 hg,
36 36 hook,
37 37 revset,
38 templatekw,
38 39 ui as uimod,
39 40 util,
40 41 )
41 42
42 43 class request(object):
43 44 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
44 45 ferr=None):
45 46 self.args = args
46 47 self.ui = ui
47 48 self.repo = repo
48 49
49 50 # input/output/error streams
50 51 self.fin = fin
51 52 self.fout = fout
52 53 self.ferr = ferr
53 54
54 55 def run():
55 56 "run the command in sys.argv"
56 57 sys.exit((dispatch(request(sys.argv[1:])) or 0) & 255)
57 58
58 59 def _getsimilar(symbols, value):
59 60 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
60 61 # The cutoff for similarity here is pretty arbitrary. It should
61 62 # probably be investigated and tweaked.
62 63 return [s for s in symbols if sim(s) > 0.6]
63 64
64 65 def _reportsimilar(write, similar):
65 66 if len(similar) == 1:
66 67 write(_("(did you mean %s?)\n") % similar[0])
67 68 elif similar:
68 69 ss = ", ".join(sorted(similar))
69 70 write(_("(did you mean one of %s?)\n") % ss)
70 71
71 72 def _formatparse(write, inst):
72 73 similar = []
73 74 if isinstance(inst, error.UnknownIdentifier):
74 75 # make sure to check fileset first, as revset can invoke fileset
75 76 similar = _getsimilar(inst.symbols, inst.function)
76 77 if len(inst.args) > 1:
77 78 write(_("hg: parse error at %s: %s\n") %
78 79 (inst.args[1], inst.args[0]))
79 80 if (inst.args[0][0] == ' '):
80 81 write(_("unexpected leading whitespace\n"))
81 82 else:
82 83 write(_("hg: parse error: %s\n") % inst.args[0])
83 84 _reportsimilar(write, similar)
84 85 if inst.hint:
85 86 write(_("(%s)\n") % inst.hint)
86 87
87 88 def dispatch(req):
88 89 "run the command specified in req.args"
89 90 if req.ferr:
90 91 ferr = req.ferr
91 92 elif req.ui:
92 93 ferr = req.ui.ferr
93 94 else:
94 95 ferr = sys.stderr
95 96
96 97 try:
97 98 if not req.ui:
98 99 req.ui = uimod.ui()
99 100 if '--traceback' in req.args:
100 101 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
101 102
102 103 # set ui streams from the request
103 104 if req.fin:
104 105 req.ui.fin = req.fin
105 106 if req.fout:
106 107 req.ui.fout = req.fout
107 108 if req.ferr:
108 109 req.ui.ferr = req.ferr
109 110 except error.Abort as inst:
110 111 ferr.write(_("abort: %s\n") % inst)
111 112 if inst.hint:
112 113 ferr.write(_("(%s)\n") % inst.hint)
113 114 return -1
114 115 except error.ParseError as inst:
115 116 _formatparse(ferr.write, inst)
116 117 return -1
117 118
118 119 msg = ' '.join(' ' in a and repr(a) or a for a in req.args)
119 120 starttime = time.time()
120 121 ret = None
121 122 try:
122 123 ret = _runcatch(req)
123 124 except KeyboardInterrupt:
124 125 try:
125 126 req.ui.warn(_("interrupted!\n"))
126 127 except IOError as inst:
127 128 if inst.errno != errno.EPIPE:
128 129 raise
129 130 ret = -1
130 131 finally:
131 132 duration = time.time() - starttime
132 133 req.ui.flush()
133 134 req.ui.log("commandfinish", "%s exited %s after %0.2f seconds\n",
134 135 msg, ret or 0, duration)
135 136 return ret
136 137
137 138 def _runcatch(req):
138 139 def catchterm(*args):
139 140 raise error.SignalInterrupt
140 141
141 142 ui = req.ui
142 143 try:
143 144 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
144 145 num = getattr(signal, name, None)
145 146 if num:
146 147 signal.signal(num, catchterm)
147 148 except ValueError:
148 149 pass # happens if called in a thread
149 150
150 151 try:
151 152 try:
152 153 debugger = 'pdb'
153 154 debugtrace = {
154 155 'pdb' : pdb.set_trace
155 156 }
156 157 debugmortem = {
157 158 'pdb' : pdb.post_mortem
158 159 }
159 160
160 161 # read --config before doing anything else
161 162 # (e.g. to change trust settings for reading .hg/hgrc)
162 163 cfgs = _parseconfig(req.ui, _earlygetopt(['--config'], req.args))
163 164
164 165 if req.repo:
165 166 # copy configs that were passed on the cmdline (--config) to
166 167 # the repo ui
167 168 for sec, name, val in cfgs:
168 169 req.repo.ui.setconfig(sec, name, val, source='--config')
169 170
170 171 # developer config: ui.debugger
171 172 debugger = ui.config("ui", "debugger")
172 173 debugmod = pdb
173 174 if not debugger or ui.plain():
174 175 # if we are in HGPLAIN mode, then disable custom debugging
175 176 debugger = 'pdb'
176 177 elif '--debugger' in req.args:
177 178 # This import can be slow for fancy debuggers, so only
178 179 # do it when absolutely necessary, i.e. when actual
179 180 # debugging has been requested
180 181 with demandimport.deactivated():
181 182 try:
182 183 debugmod = __import__(debugger)
183 184 except ImportError:
184 185 pass # Leave debugmod = pdb
185 186
186 187 debugtrace[debugger] = debugmod.set_trace
187 188 debugmortem[debugger] = debugmod.post_mortem
188 189
189 190 # enter the debugger before command execution
190 191 if '--debugger' in req.args:
191 192 ui.warn(_("entering debugger - "
192 193 "type c to continue starting hg or h for help\n"))
193 194
194 195 if (debugger != 'pdb' and
195 196 debugtrace[debugger] == debugtrace['pdb']):
196 197 ui.warn(_("%s debugger specified "
197 198 "but its module was not found\n") % debugger)
198 199 with demandimport.deactivated():
199 200 debugtrace[debugger]()
200 201 try:
201 202 return _dispatch(req)
202 203 finally:
203 204 ui.flush()
204 205 except: # re-raises
205 206 # enter the debugger when we hit an exception
206 207 if '--debugger' in req.args:
207 208 traceback.print_exc()
208 209 debugmortem[debugger](sys.exc_info()[2])
209 210 ui.traceback()
210 211 raise
211 212
212 213 # Global exception handling, alphabetically
213 214 # Mercurial-specific first, followed by built-in and library exceptions
214 215 except error.AmbiguousCommand as inst:
215 216 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
216 217 (inst.args[0], " ".join(inst.args[1])))
217 218 except error.ParseError as inst:
218 219 _formatparse(ui.warn, inst)
219 220 return -1
220 221 except error.LockHeld as inst:
221 222 if inst.errno == errno.ETIMEDOUT:
222 223 reason = _('timed out waiting for lock held by %s') % inst.locker
223 224 else:
224 225 reason = _('lock held by %s') % inst.locker
225 226 ui.warn(_("abort: %s: %s\n") % (inst.desc or inst.filename, reason))
226 227 except error.LockUnavailable as inst:
227 228 ui.warn(_("abort: could not lock %s: %s\n") %
228 229 (inst.desc or inst.filename, inst.strerror))
229 230 except error.CommandError as inst:
230 231 if inst.args[0]:
231 232 ui.warn(_("hg %s: %s\n") % (inst.args[0], inst.args[1]))
232 233 commands.help_(ui, inst.args[0], full=False, command=True)
233 234 else:
234 235 ui.warn(_("hg: %s\n") % inst.args[1])
235 236 commands.help_(ui, 'shortlist')
236 237 except error.OutOfBandError as inst:
237 238 if inst.args:
238 239 msg = _("abort: remote error:\n")
239 240 else:
240 241 msg = _("abort: remote error\n")
241 242 ui.warn(msg)
242 243 if inst.args:
243 244 ui.warn(''.join(inst.args))
244 245 if inst.hint:
245 246 ui.warn('(%s)\n' % inst.hint)
246 247 except error.RepoError as inst:
247 248 ui.warn(_("abort: %s!\n") % inst)
248 249 if inst.hint:
249 250 ui.warn(_("(%s)\n") % inst.hint)
250 251 except error.ResponseError as inst:
251 252 ui.warn(_("abort: %s") % inst.args[0])
252 253 if not isinstance(inst.args[1], basestring):
253 254 ui.warn(" %r\n" % (inst.args[1],))
254 255 elif not inst.args[1]:
255 256 ui.warn(_(" empty string\n"))
256 257 else:
257 258 ui.warn("\n%r\n" % util.ellipsis(inst.args[1]))
258 259 except error.CensoredNodeError as inst:
259 260 ui.warn(_("abort: file censored %s!\n") % inst)
260 261 except error.RevlogError as inst:
261 262 ui.warn(_("abort: %s!\n") % inst)
262 263 except error.SignalInterrupt:
263 264 ui.warn(_("killed!\n"))
264 265 except error.UnknownCommand as inst:
265 266 ui.warn(_("hg: unknown command '%s'\n") % inst.args[0])
266 267 try:
267 268 # check if the command is in a disabled extension
268 269 # (but don't check for extensions themselves)
269 270 commands.help_(ui, inst.args[0], unknowncmd=True)
270 271 except (error.UnknownCommand, error.Abort):
271 272 suggested = False
272 273 if len(inst.args) == 2:
273 274 sim = _getsimilar(inst.args[1], inst.args[0])
274 275 if sim:
275 276 _reportsimilar(ui.warn, sim)
276 277 suggested = True
277 278 if not suggested:
278 279 commands.help_(ui, 'shortlist')
279 280 except error.InterventionRequired as inst:
280 281 ui.warn("%s\n" % inst)
281 282 if inst.hint:
282 283 ui.warn(_("(%s)\n") % inst.hint)
283 284 return 1
284 285 except error.Abort as inst:
285 286 ui.warn(_("abort: %s\n") % inst)
286 287 if inst.hint:
287 288 ui.warn(_("(%s)\n") % inst.hint)
288 289 except ImportError as inst:
289 290 ui.warn(_("abort: %s!\n") % inst)
290 291 m = str(inst).split()[-1]
291 292 if m in "mpatch bdiff".split():
292 293 ui.warn(_("(did you forget to compile extensions?)\n"))
293 294 elif m in "zlib".split():
294 295 ui.warn(_("(is your Python install correct?)\n"))
295 296 except IOError as inst:
296 297 if util.safehasattr(inst, "code"):
297 298 ui.warn(_("abort: %s\n") % inst)
298 299 elif util.safehasattr(inst, "reason"):
299 300 try: # usually it is in the form (errno, strerror)
300 301 reason = inst.reason.args[1]
301 302 except (AttributeError, IndexError):
302 303 # it might be anything, for example a string
303 304 reason = inst.reason
304 305 if isinstance(reason, unicode):
305 306 # SSLError of Python 2.7.9 contains a unicode
306 307 reason = reason.encode(encoding.encoding, 'replace')
307 308 ui.warn(_("abort: error: %s\n") % reason)
308 309 elif (util.safehasattr(inst, "args")
309 310 and inst.args and inst.args[0] == errno.EPIPE):
310 311 pass
311 312 elif getattr(inst, "strerror", None):
312 313 if getattr(inst, "filename", None):
313 314 ui.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename))
314 315 else:
315 316 ui.warn(_("abort: %s\n") % inst.strerror)
316 317 else:
317 318 raise
318 319 except OSError as inst:
319 320 if getattr(inst, "filename", None) is not None:
320 321 ui.warn(_("abort: %s: '%s'\n") % (inst.strerror, inst.filename))
321 322 else:
322 323 ui.warn(_("abort: %s\n") % inst.strerror)
323 324 except KeyboardInterrupt:
324 325 raise
325 326 except MemoryError:
326 327 ui.warn(_("abort: out of memory\n"))
327 328 except SystemExit as inst:
328 329 # Commands shouldn't sys.exit directly, but give a return code.
329 330 # Just in case catch this and and pass exit code to caller.
330 331 return inst.code
331 332 except socket.error as inst:
332 333 ui.warn(_("abort: %s\n") % inst.args[-1])
333 334 except: # re-raises
334 335 # For compatibility checking, we discard the portion of the hg
335 336 # version after the + on the assumption that if a "normal
336 337 # user" is running a build with a + in it the packager
337 338 # probably built from fairly close to a tag and anyone with a
338 339 # 'make local' copy of hg (where the version number can be out
339 340 # of date) will be clueful enough to notice the implausible
340 341 # version number and try updating.
341 342 ct = util.versiontuple(n=2)
342 343 worst = None, ct, ''
343 344 if ui.config('ui', 'supportcontact', None) is None:
344 345 for name, mod in extensions.extensions():
345 346 testedwith = getattr(mod, 'testedwith', '')
346 347 report = getattr(mod, 'buglink', _('the extension author.'))
347 348 if not testedwith.strip():
348 349 # We found an untested extension. It's likely the culprit.
349 350 worst = name, 'unknown', report
350 351 break
351 352
352 353 # Never blame on extensions bundled with Mercurial.
353 354 if testedwith == 'internal':
354 355 continue
355 356
356 357 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
357 358 if ct in tested:
358 359 continue
359 360
360 361 lower = [t for t in tested if t < ct]
361 362 nearest = max(lower or tested)
362 363 if worst[0] is None or nearest < worst[1]:
363 364 worst = name, nearest, report
364 365 if worst[0] is not None:
365 366 name, testedwith, report = worst
366 367 if not isinstance(testedwith, str):
367 368 testedwith = '.'.join([str(c) for c in testedwith])
368 369 warning = (_('** Unknown exception encountered with '
369 370 'possibly-broken third-party extension %s\n'
370 371 '** which supports versions %s of Mercurial.\n'
371 372 '** Please disable %s and try your action again.\n'
372 373 '** If that fixes the bug please report it to %s\n')
373 374 % (name, testedwith, name, report))
374 375 else:
375 376 bugtracker = ui.config('ui', 'supportcontact', None)
376 377 if bugtracker is None:
377 378 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
378 379 warning = (_("** unknown exception encountered, "
379 380 "please report by visiting\n** ") + bugtracker + '\n')
380 381 warning += ((_("** Python %s\n") % sys.version.replace('\n', '')) +
381 382 (_("** Mercurial Distributed SCM (version %s)\n") %
382 383 util.version()) +
383 384 (_("** Extensions loaded: %s\n") %
384 385 ", ".join([x[0] for x in extensions.extensions()])))
385 386 ui.log("commandexception", "%s\n%s\n", warning, traceback.format_exc())
386 387 ui.warn(warning)
387 388 raise
388 389
389 390 return -1
390 391
391 392 def aliasargs(fn, givenargs):
392 393 args = getattr(fn, 'args', [])
393 394 if args:
394 395 cmd = ' '.join(map(util.shellquote, args))
395 396
396 397 nums = []
397 398 def replacer(m):
398 399 num = int(m.group(1)) - 1
399 400 nums.append(num)
400 401 if num < len(givenargs):
401 402 return givenargs[num]
402 403 raise error.Abort(_('too few arguments for command alias'))
403 404 cmd = re.sub(r'\$(\d+|\$)', replacer, cmd)
404 405 givenargs = [x for i, x in enumerate(givenargs)
405 406 if i not in nums]
406 407 args = shlex.split(cmd)
407 408 return args + givenargs
408 409
409 410 def aliasinterpolate(name, args, cmd):
410 411 '''interpolate args into cmd for shell aliases
411 412
412 413 This also handles $0, $@ and "$@".
413 414 '''
414 415 # util.interpolate can't deal with "$@" (with quotes) because it's only
415 416 # built to match prefix + patterns.
416 417 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
417 418 replacemap['$0'] = name
418 419 replacemap['$$'] = '$'
419 420 replacemap['$@'] = ' '.join(args)
420 421 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
421 422 # parameters, separated out into words. Emulate the same behavior here by
422 423 # quoting the arguments individually. POSIX shells will then typically
423 424 # tokenize each argument into exactly one word.
424 425 replacemap['"$@"'] = ' '.join(util.shellquote(arg) for arg in args)
425 426 # escape '\$' for regex
426 427 regex = '|'.join(replacemap.keys()).replace('$', r'\$')
427 428 r = re.compile(regex)
428 429 return r.sub(lambda x: replacemap[x.group()], cmd)
429 430
430 431 class cmdalias(object):
431 432 def __init__(self, name, definition, cmdtable):
432 433 self.name = self.cmd = name
433 434 self.cmdname = ''
434 435 self.definition = definition
435 436 self.fn = None
436 437 self.args = []
437 438 self.opts = []
438 439 self.help = ''
439 440 self.norepo = True
440 441 self.optionalrepo = False
441 442 self.inferrepo = False
442 443 self.badalias = None
443 444 self.unknowncmd = False
444 445
445 446 try:
446 447 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
447 448 for alias, e in cmdtable.iteritems():
448 449 if e is entry:
449 450 self.cmd = alias
450 451 break
451 452 self.shadows = True
452 453 except error.UnknownCommand:
453 454 self.shadows = False
454 455
455 456 if not self.definition:
456 457 self.badalias = _("no definition for alias '%s'") % self.name
457 458 return
458 459
459 460 if self.definition.startswith('!'):
460 461 self.shell = True
461 462 def fn(ui, *args):
462 463 env = {'HG_ARGS': ' '.join((self.name,) + args)}
463 464 def _checkvar(m):
464 465 if m.groups()[0] == '$':
465 466 return m.group()
466 467 elif int(m.groups()[0]) <= len(args):
467 468 return m.group()
468 469 else:
469 470 ui.debug("No argument found for substitution "
470 471 "of %i variable in alias '%s' definition."
471 472 % (int(m.groups()[0]), self.name))
472 473 return ''
473 474 cmd = re.sub(r'\$(\d+|\$)', _checkvar, self.definition[1:])
474 475 cmd = aliasinterpolate(self.name, args, cmd)
475 476 return ui.system(cmd, environ=env)
476 477 self.fn = fn
477 478 return
478 479
479 480 try:
480 481 args = shlex.split(self.definition)
481 482 except ValueError as inst:
482 483 self.badalias = (_("error in definition for alias '%s': %s")
483 484 % (self.name, inst))
484 485 return
485 486 self.cmdname = cmd = args.pop(0)
486 487 args = map(util.expandpath, args)
487 488
488 489 for invalidarg in ("--cwd", "-R", "--repository", "--repo", "--config"):
489 490 if _earlygetopt([invalidarg], args):
490 491 self.badalias = (_("error in definition for alias '%s': %s may "
491 492 "only be given on the command line")
492 493 % (self.name, invalidarg))
493 494 return
494 495
495 496 try:
496 497 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
497 498 if len(tableentry) > 2:
498 499 self.fn, self.opts, self.help = tableentry
499 500 else:
500 501 self.fn, self.opts = tableentry
501 502
502 503 self.args = aliasargs(self.fn, args)
503 504 if not self.fn.norepo:
504 505 self.norepo = False
505 506 if self.fn.optionalrepo:
506 507 self.optionalrepo = True
507 508 if self.fn.inferrepo:
508 509 self.inferrepo = True
509 510 if self.help.startswith("hg " + cmd):
510 511 # drop prefix in old-style help lines so hg shows the alias
511 512 self.help = self.help[4 + len(cmd):]
512 513 self.__doc__ = self.fn.__doc__
513 514
514 515 except error.UnknownCommand:
515 516 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
516 517 % (self.name, cmd))
517 518 self.unknowncmd = True
518 519 except error.AmbiguousCommand:
519 520 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
520 521 % (self.name, cmd))
521 522
522 523 def __call__(self, ui, *args, **opts):
523 524 if self.badalias:
524 525 hint = None
525 526 if self.unknowncmd:
526 527 try:
527 528 # check if the command is in a disabled extension
528 529 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
529 530 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
530 531 except error.UnknownCommand:
531 532 pass
532 533 raise error.Abort(self.badalias, hint=hint)
533 534 if self.shadows:
534 535 ui.debug("alias '%s' shadows command '%s'\n" %
535 536 (self.name, self.cmdname))
536 537
537 538 if util.safehasattr(self, 'shell'):
538 539 return self.fn(ui, *args, **opts)
539 540 else:
540 541 try:
541 542 return util.checksignature(self.fn)(ui, *args, **opts)
542 543 except error.SignatureError:
543 544 args = ' '.join([self.cmdname] + self.args)
544 545 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
545 546 raise
546 547
547 548 def addaliases(ui, cmdtable):
548 549 # aliases are processed after extensions have been loaded, so they
549 550 # may use extension commands. Aliases can also use other alias definitions,
550 551 # but only if they have been defined prior to the current definition.
551 552 for alias, definition in ui.configitems('alias'):
552 553 aliasdef = cmdalias(alias, definition, cmdtable)
553 554
554 555 try:
555 556 olddef = cmdtable[aliasdef.cmd][0]
556 557 if olddef.definition == aliasdef.definition:
557 558 continue
558 559 except (KeyError, AttributeError):
559 560 # definition might not exist or it might not be a cmdalias
560 561 pass
561 562
562 563 cmdtable[aliasdef.name] = (aliasdef, aliasdef.opts, aliasdef.help)
563 564
564 565 def _parse(ui, args):
565 566 options = {}
566 567 cmdoptions = {}
567 568
568 569 try:
569 570 args = fancyopts.fancyopts(args, commands.globalopts, options)
570 571 except fancyopts.getopt.GetoptError as inst:
571 572 raise error.CommandError(None, inst)
572 573
573 574 if args:
574 575 cmd, args = args[0], args[1:]
575 576 aliases, entry = cmdutil.findcmd(cmd, commands.table,
576 577 ui.configbool("ui", "strict"))
577 578 cmd = aliases[0]
578 579 args = aliasargs(entry[0], args)
579 580 defaults = ui.config("defaults", cmd)
580 581 if defaults:
581 582 args = map(util.expandpath, shlex.split(defaults)) + args
582 583 c = list(entry[1])
583 584 else:
584 585 cmd = None
585 586 c = []
586 587
587 588 # combine global options into local
588 589 for o in commands.globalopts:
589 590 c.append((o[0], o[1], options[o[1]], o[3]))
590 591
591 592 try:
592 593 args = fancyopts.fancyopts(args, c, cmdoptions, True)
593 594 except fancyopts.getopt.GetoptError as inst:
594 595 raise error.CommandError(cmd, inst)
595 596
596 597 # separate global options back out
597 598 for o in commands.globalopts:
598 599 n = o[1]
599 600 options[n] = cmdoptions[n]
600 601 del cmdoptions[n]
601 602
602 603 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
603 604
604 605 def _parseconfig(ui, config):
605 606 """parse the --config options from the command line"""
606 607 configs = []
607 608
608 609 for cfg in config:
609 610 try:
610 611 name, value = [cfgelem.strip()
611 612 for cfgelem in cfg.split('=', 1)]
612 613 section, name = name.split('.', 1)
613 614 if not section or not name:
614 615 raise IndexError
615 616 ui.setconfig(section, name, value, '--config')
616 617 configs.append((section, name, value))
617 618 except (IndexError, ValueError):
618 619 raise error.Abort(_('malformed --config option: %r '
619 620 '(use --config section.name=value)') % cfg)
620 621
621 622 return configs
622 623
623 624 def _earlygetopt(aliases, args):
624 625 """Return list of values for an option (or aliases).
625 626
626 627 The values are listed in the order they appear in args.
627 628 The options and values are removed from args.
628 629
629 630 >>> args = ['x', '--cwd', 'foo', 'y']
630 631 >>> _earlygetopt(['--cwd'], args), args
631 632 (['foo'], ['x', 'y'])
632 633
633 634 >>> args = ['x', '--cwd=bar', 'y']
634 635 >>> _earlygetopt(['--cwd'], args), args
635 636 (['bar'], ['x', 'y'])
636 637
637 638 >>> args = ['x', '-R', 'foo', 'y']
638 639 >>> _earlygetopt(['-R'], args), args
639 640 (['foo'], ['x', 'y'])
640 641
641 642 >>> args = ['x', '-Rbar', 'y']
642 643 >>> _earlygetopt(['-R'], args), args
643 644 (['bar'], ['x', 'y'])
644 645 """
645 646 try:
646 647 argcount = args.index("--")
647 648 except ValueError:
648 649 argcount = len(args)
649 650 shortopts = [opt for opt in aliases if len(opt) == 2]
650 651 values = []
651 652 pos = 0
652 653 while pos < argcount:
653 654 fullarg = arg = args[pos]
654 655 equals = arg.find('=')
655 656 if equals > -1:
656 657 arg = arg[:equals]
657 658 if arg in aliases:
658 659 del args[pos]
659 660 if equals > -1:
660 661 values.append(fullarg[equals + 1:])
661 662 argcount -= 1
662 663 else:
663 664 if pos + 1 >= argcount:
664 665 # ignore and let getopt report an error if there is no value
665 666 break
666 667 values.append(args.pop(pos))
667 668 argcount -= 2
668 669 elif arg[:2] in shortopts:
669 670 # short option can have no following space, e.g. hg log -Rfoo
670 671 values.append(args.pop(pos)[2:])
671 672 argcount -= 1
672 673 else:
673 674 pos += 1
674 675 return values
675 676
676 677 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
677 678 # run pre-hook, and abort if it fails
678 679 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
679 680 pats=cmdpats, opts=cmdoptions)
680 681 ret = _runcommand(ui, options, cmd, d)
681 682 # run post-hook, passing command result
682 683 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
683 684 result=ret, pats=cmdpats, opts=cmdoptions)
684 685 return ret
685 686
686 687 def _getlocal(ui, rpath, wd=None):
687 688 """Return (path, local ui object) for the given target path.
688 689
689 690 Takes paths in [cwd]/.hg/hgrc into account."
690 691 """
691 692 if wd is None:
692 693 try:
693 694 wd = os.getcwd()
694 695 except OSError as e:
695 696 raise error.Abort(_("error getting current working directory: %s") %
696 697 e.strerror)
697 698 path = cmdutil.findrepo(wd) or ""
698 699 if not path:
699 700 lui = ui
700 701 else:
701 702 lui = ui.copy()
702 703 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
703 704
704 705 if rpath and rpath[-1]:
705 706 path = lui.expandpath(rpath[-1])
706 707 lui = ui.copy()
707 708 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
708 709
709 710 return path, lui
710 711
711 712 def _checkshellalias(lui, ui, args, precheck=True):
712 713 """Return the function to run the shell alias, if it is required
713 714
714 715 'precheck' is whether this function is invoked before adding
715 716 aliases or not.
716 717 """
717 718 options = {}
718 719
719 720 try:
720 721 args = fancyopts.fancyopts(args, commands.globalopts, options)
721 722 except fancyopts.getopt.GetoptError:
722 723 return
723 724
724 725 if not args:
725 726 return
726 727
727 728 if precheck:
728 729 strict = True
729 730 cmdtable = commands.table.copy()
730 731 addaliases(lui, cmdtable)
731 732 else:
732 733 strict = False
733 734 cmdtable = commands.table
734 735
735 736 cmd = args[0]
736 737 try:
737 738 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
738 739 except (error.AmbiguousCommand, error.UnknownCommand):
739 740 return
740 741
741 742 cmd = aliases[0]
742 743 fn = entry[0]
743 744
744 745 if cmd and util.safehasattr(fn, 'shell'):
745 746 d = lambda: fn(ui, *args[1:])
746 747 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
747 748 [], {})
748 749
749 750 _loaded = set()
750 751
751 752 # list of (objname, loadermod, loadername) tuple:
752 753 # - objname is the name of an object in extension module, from which
753 754 # extra information is loaded
754 755 # - loadermod is the module where loader is placed
755 756 # - loadername is the name of the function, which takes (ui, extensionname,
756 757 # extraobj) arguments
757 758 extraloaders = [
758 759 ('cmdtable', commands, 'loadcmdtable'),
759 760 ('filesetpredicate', fileset, 'loadpredicate'),
760 761 ('revsetpredicate', revset, 'loadpredicate'),
762 ('templatekeyword', templatekw, 'loadkeyword'),
761 763 ]
762 764
763 765 def _dispatch(req):
764 766 args = req.args
765 767 ui = req.ui
766 768
767 769 # check for cwd
768 770 cwd = _earlygetopt(['--cwd'], args)
769 771 if cwd:
770 772 os.chdir(cwd[-1])
771 773
772 774 rpath = _earlygetopt(["-R", "--repository", "--repo"], args)
773 775 path, lui = _getlocal(ui, rpath)
774 776
775 777 # Now that we're operating in the right directory/repository with
776 778 # the right config settings, check for shell aliases
777 779 shellaliasfn = _checkshellalias(lui, ui, args)
778 780 if shellaliasfn:
779 781 return shellaliasfn()
780 782
781 783 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
782 784 # reposetup. Programs like TortoiseHg will call _dispatch several
783 785 # times so we keep track of configured extensions in _loaded.
784 786 extensions.loadall(lui)
785 787 exts = [ext for ext in extensions.extensions() if ext[0] not in _loaded]
786 788 # Propagate any changes to lui.__class__ by extensions
787 789 ui.__class__ = lui.__class__
788 790
789 791 # (uisetup and extsetup are handled in extensions.loadall)
790 792
791 793 for name, module in exts:
792 794 for objname, loadermod, loadername in extraloaders:
793 795 extraobj = getattr(module, objname, None)
794 796 if extraobj is not None:
795 797 getattr(loadermod, loadername)(ui, name, extraobj)
796 798 _loaded.add(name)
797 799
798 800 # (reposetup is handled in hg.repository)
799 801
800 802 addaliases(lui, commands.table)
801 803
802 804 if not lui.configbool("ui", "strict"):
803 805 # All aliases and commands are completely defined, now.
804 806 # Check abbreviation/ambiguity of shell alias again, because shell
805 807 # alias may cause failure of "_parse" (see issue4355)
806 808 shellaliasfn = _checkshellalias(lui, ui, args, precheck=False)
807 809 if shellaliasfn:
808 810 return shellaliasfn()
809 811
810 812 # check for fallback encoding
811 813 fallback = lui.config('ui', 'fallbackencoding')
812 814 if fallback:
813 815 encoding.fallbackencoding = fallback
814 816
815 817 fullargs = args
816 818 cmd, func, args, options, cmdoptions = _parse(lui, args)
817 819
818 820 if options["config"]:
819 821 raise error.Abort(_("option --config may not be abbreviated!"))
820 822 if options["cwd"]:
821 823 raise error.Abort(_("option --cwd may not be abbreviated!"))
822 824 if options["repository"]:
823 825 raise error.Abort(_(
824 826 "option -R has to be separated from other options (e.g. not -qR) "
825 827 "and --repository may only be abbreviated as --repo!"))
826 828
827 829 if options["encoding"]:
828 830 encoding.encoding = options["encoding"]
829 831 if options["encodingmode"]:
830 832 encoding.encodingmode = options["encodingmode"]
831 833 if options["time"]:
832 834 def get_times():
833 835 t = os.times()
834 836 if t[4] == 0.0: # Windows leaves this as zero, so use time.clock()
835 837 t = (t[0], t[1], t[2], t[3], time.clock())
836 838 return t
837 839 s = get_times()
838 840 def print_time():
839 841 t = get_times()
840 842 ui.warn(_("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
841 843 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
842 844 atexit.register(print_time)
843 845
844 846 uis = set([ui, lui])
845 847
846 848 if req.repo:
847 849 uis.add(req.repo.ui)
848 850
849 851 if options['verbose'] or options['debug'] or options['quiet']:
850 852 for opt in ('verbose', 'debug', 'quiet'):
851 853 val = str(bool(options[opt]))
852 854 for ui_ in uis:
853 855 ui_.setconfig('ui', opt, val, '--' + opt)
854 856
855 857 if options['traceback']:
856 858 for ui_ in uis:
857 859 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
858 860
859 861 if options['noninteractive']:
860 862 for ui_ in uis:
861 863 ui_.setconfig('ui', 'interactive', 'off', '-y')
862 864
863 865 if cmdoptions.get('insecure', False):
864 866 for ui_ in uis:
865 867 ui_.setconfig('web', 'cacerts', '!', '--insecure')
866 868
867 869 if options['version']:
868 870 return commands.version_(ui)
869 871 if options['help']:
870 872 return commands.help_(ui, cmd, command=cmd is not None)
871 873 elif not cmd:
872 874 return commands.help_(ui, 'shortlist')
873 875
874 876 repo = None
875 877 cmdpats = args[:]
876 878 if not func.norepo:
877 879 # use the repo from the request only if we don't have -R
878 880 if not rpath and not cwd:
879 881 repo = req.repo
880 882
881 883 if repo:
882 884 # set the descriptors of the repo ui to those of ui
883 885 repo.ui.fin = ui.fin
884 886 repo.ui.fout = ui.fout
885 887 repo.ui.ferr = ui.ferr
886 888 else:
887 889 try:
888 890 repo = hg.repository(ui, path=path)
889 891 if not repo.local():
890 892 raise error.Abort(_("repository '%s' is not local") % path)
891 893 repo.ui.setconfig("bundle", "mainreporoot", repo.root, 'repo')
892 894 except error.RequirementError:
893 895 raise
894 896 except error.RepoError:
895 897 if rpath and rpath[-1]: # invalid -R path
896 898 raise
897 899 if not func.optionalrepo:
898 900 if func.inferrepo and args and not path:
899 901 # try to infer -R from command args
900 902 repos = map(cmdutil.findrepo, args)
901 903 guess = repos[0]
902 904 if guess and repos.count(guess) == len(repos):
903 905 req.args = ['--repository', guess] + fullargs
904 906 return _dispatch(req)
905 907 if not path:
906 908 raise error.RepoError(_("no repository found in '%s'"
907 909 " (.hg not found)")
908 910 % os.getcwd())
909 911 raise
910 912 if repo:
911 913 ui = repo.ui
912 914 if options['hidden']:
913 915 repo = repo.unfiltered()
914 916 args.insert(0, repo)
915 917 elif rpath:
916 918 ui.warn(_("warning: --repository ignored\n"))
917 919
918 920 msg = ' '.join(' ' in a and repr(a) or a for a in fullargs)
919 921 ui.log("command", '%s\n', msg)
920 922 d = lambda: util.checksignature(func)(ui, *args, **cmdoptions)
921 923 try:
922 924 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
923 925 cmdpats, cmdoptions)
924 926 finally:
925 927 if repo and repo != req.repo:
926 928 repo.close()
927 929
928 930 def lsprofile(ui, func, fp):
929 931 format = ui.config('profiling', 'format', default='text')
930 932 field = ui.config('profiling', 'sort', default='inlinetime')
931 933 limit = ui.configint('profiling', 'limit', default=30)
932 934 climit = ui.configint('profiling', 'nested', default=0)
933 935
934 936 if format not in ['text', 'kcachegrind']:
935 937 ui.warn(_("unrecognized profiling format '%s'"
936 938 " - Ignored\n") % format)
937 939 format = 'text'
938 940
939 941 try:
940 942 from . import lsprof
941 943 except ImportError:
942 944 raise error.Abort(_(
943 945 'lsprof not available - install from '
944 946 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
945 947 p = lsprof.Profiler()
946 948 p.enable(subcalls=True)
947 949 try:
948 950 return func()
949 951 finally:
950 952 p.disable()
951 953
952 954 if format == 'kcachegrind':
953 955 from . import lsprofcalltree
954 956 calltree = lsprofcalltree.KCacheGrind(p)
955 957 calltree.output(fp)
956 958 else:
957 959 # format == 'text'
958 960 stats = lsprof.Stats(p.getstats())
959 961 stats.sort(field)
960 962 stats.pprint(limit=limit, file=fp, climit=climit)
961 963
962 964 def flameprofile(ui, func, fp):
963 965 try:
964 966 from flamegraph import flamegraph
965 967 except ImportError:
966 968 raise error.Abort(_(
967 969 'flamegraph not available - install from '
968 970 'https://github.com/evanhempel/python-flamegraph'))
969 971 # developer config: profiling.freq
970 972 freq = ui.configint('profiling', 'freq', default=1000)
971 973 filter_ = None
972 974 collapse_recursion = True
973 975 thread = flamegraph.ProfileThread(fp, 1.0 / freq,
974 976 filter_, collapse_recursion)
975 977 start_time = time.clock()
976 978 try:
977 979 thread.start()
978 980 func()
979 981 finally:
980 982 thread.stop()
981 983 thread.join()
982 984 print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
983 985 time.clock() - start_time, thread.num_frames(),
984 986 thread.num_frames(unique=True)))
985 987
986 988
987 989 def statprofile(ui, func, fp):
988 990 try:
989 991 import statprof
990 992 except ImportError:
991 993 raise error.Abort(_(
992 994 'statprof not available - install using "easy_install statprof"'))
993 995
994 996 freq = ui.configint('profiling', 'freq', default=1000)
995 997 if freq > 0:
996 998 statprof.reset(freq)
997 999 else:
998 1000 ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
999 1001
1000 1002 statprof.start()
1001 1003 try:
1002 1004 return func()
1003 1005 finally:
1004 1006 statprof.stop()
1005 1007 statprof.display(fp)
1006 1008
1007 1009 def _runcommand(ui, options, cmd, cmdfunc):
1008 1010 """Enables the profiler if applicable.
1009 1011
1010 1012 ``profiling.enabled`` - boolean config that enables or disables profiling
1011 1013 """
1012 1014 def checkargs():
1013 1015 try:
1014 1016 return cmdfunc()
1015 1017 except error.SignatureError:
1016 1018 raise error.CommandError(cmd, _("invalid arguments"))
1017 1019
1018 1020 if options['profile'] or ui.configbool('profiling', 'enabled'):
1019 1021 profiler = os.getenv('HGPROF')
1020 1022 if profiler is None:
1021 1023 profiler = ui.config('profiling', 'type', default='ls')
1022 1024 if profiler not in ('ls', 'stat', 'flame'):
1023 1025 ui.warn(_("unrecognized profiler '%s' - ignored\n") % profiler)
1024 1026 profiler = 'ls'
1025 1027
1026 1028 output = ui.config('profiling', 'output')
1027 1029
1028 1030 if output == 'blackbox':
1029 1031 import StringIO
1030 1032 fp = StringIO.StringIO()
1031 1033 elif output:
1032 1034 path = ui.expandpath(output)
1033 1035 fp = open(path, 'wb')
1034 1036 else:
1035 1037 fp = sys.stderr
1036 1038
1037 1039 try:
1038 1040 if profiler == 'ls':
1039 1041 return lsprofile(ui, checkargs, fp)
1040 1042 elif profiler == 'flame':
1041 1043 return flameprofile(ui, checkargs, fp)
1042 1044 else:
1043 1045 return statprofile(ui, checkargs, fp)
1044 1046 finally:
1045 1047 if output:
1046 1048 if output == 'blackbox':
1047 1049 val = "Profile:\n%s" % fp.getvalue()
1048 1050 # ui.log treats the input as a format string,
1049 1051 # so we need to escape any % signs.
1050 1052 val = val.replace('%', '%%')
1051 1053 ui.log('profile', val)
1052 1054 fp.close()
1053 1055 else:
1054 1056 return checkargs()
@@ -1,163 +1,193 b''
1 1 # registrar.py - utilities to register function for specific purpose
2 2 #
3 3 # Copyright FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from . import (
11 11 util,
12 12 )
13 13
14 14 class _funcregistrarbase(object):
15 15 """Base of decorator to register a fuction for specific purpose
16 16
17 17 This decorator stores decorated functions into own dict 'table'.
18 18
19 19 The least derived class can be defined by overriding 'formatdoc',
20 20 for example::
21 21
22 22 class keyword(_funcregistrarbase):
23 23 _docformat = ":%s: %s"
24 24
25 25 This should be used as below:
26 26
27 27 keyword = registrar.keyword()
28 28
29 29 @keyword('bar')
30 30 def barfunc(*args, **kwargs):
31 31 '''Explanation of bar keyword ....
32 32 '''
33 33 pass
34 34
35 35 In this case:
36 36
37 37 - 'barfunc' is stored as 'bar' in '_table' of an instance 'keyword' above
38 38 - 'barfunc.__doc__' becomes ":bar: Explanation of bar keyword"
39 39 """
40 40 def __init__(self, table=None):
41 41 if table is None:
42 42 self._table = {}
43 43 else:
44 44 self._table = table
45 45
46 46 def __call__(self, decl, *args, **kwargs):
47 47 return lambda func: self._doregister(func, decl, *args, **kwargs)
48 48
49 49 def _doregister(self, func, decl, *args, **kwargs):
50 50 name = self._getname(decl)
51 51
52 52 if func.__doc__ and not util.safehasattr(func, '_origdoc'):
53 53 doc = func.__doc__.strip()
54 54 func._origdoc = doc
55 55 func.__doc__ = self._formatdoc(decl, doc)
56 56
57 57 self._table[name] = func
58 58 self._extrasetup(name, func, *args, **kwargs)
59 59
60 60 return func
61 61
62 62 def _parsefuncdecl(self, decl):
63 63 """Parse function declaration and return the name of function in it
64 64 """
65 65 i = decl.find('(')
66 66 if i >= 0:
67 67 return decl[:i]
68 68 else:
69 69 return decl
70 70
71 71 def _getname(self, decl):
72 72 """Return the name of the registered function from decl
73 73
74 74 Derived class should override this, if it allows more
75 75 descriptive 'decl' string than just a name.
76 76 """
77 77 return decl
78 78
79 79 _docformat = None
80 80
81 81 def _formatdoc(self, decl, doc):
82 82 """Return formatted document of the registered function for help
83 83
84 84 'doc' is '__doc__.strip()' of the registered function.
85 85 """
86 86 return self._docformat % (decl, doc)
87 87
88 88 def _extrasetup(self, name, func):
89 89 """Execute exra setup for registered function, if needed
90 90 """
91 91 pass
92 92
93 93 class revsetpredicate(_funcregistrarbase):
94 94 """Decorator to register revset predicate
95 95
96 96 Usage::
97 97
98 98 revsetpredicate = registrar.revsetpredicate()
99 99
100 100 @revsetpredicate('mypredicate(arg1, arg2[, arg3])')
101 101 def mypredicatefunc(repo, subset, x):
102 102 '''Explanation of this revset predicate ....
103 103 '''
104 104 pass
105 105
106 106 The first string argument is used also in online help.
107 107
108 108 Optional argument 'safe' indicates whether a predicate is safe for
109 109 DoS attack (False by default).
110 110
111 111 'revsetpredicate' instance in example above can be used to
112 112 decorate multiple functions.
113 113
114 114 Decorated functions are registered automatically at loading
115 115 extension, if an instance named as 'revsetpredicate' is used for
116 116 decorating in extension.
117 117
118 118 Otherwise, explicit 'revset.loadpredicate()' is needed.
119 119 """
120 120 _getname = _funcregistrarbase._parsefuncdecl
121 121 _docformat = "``%s``\n %s"
122 122
123 123 def _extrasetup(self, name, func, safe=False):
124 124 func._safe = safe
125 125
126 126 class filesetpredicate(_funcregistrarbase):
127 127 """Decorator to register fileset predicate
128 128
129 129 Usage::
130 130
131 131 filesetpredicate = registrar.filesetpredicate()
132 132
133 133 @filesetpredicate('mypredicate()')
134 134 def mypredicatefunc(mctx, x):
135 135 '''Explanation of this fileset predicate ....
136 136 '''
137 137 pass
138 138
139 139 The first string argument is used also in online help.
140 140
141 141 Optional argument 'callstatus' indicates whether a predicate
142 142 implies 'matchctx.status()' at runtime or not (False, by
143 143 default).
144 144
145 145 Optional argument 'callexisting' indicates whether a predicate
146 146 implies 'matchctx.existing()' at runtime or not (False, by
147 147 default).
148 148
149 149 'filesetpredicate' instance in example above can be used to
150 150 decorate multiple functions.
151 151
152 152 Decorated functions are registered automatically at loading
153 153 extension, if an instance named as 'filesetpredicate' is used for
154 154 decorating in extension.
155 155
156 156 Otherwise, explicit 'fileset.loadpredicate()' is needed.
157 157 """
158 158 _getname = _funcregistrarbase._parsefuncdecl
159 159 _docformat = "``%s``\n %s"
160 160
161 161 def _extrasetup(self, name, func, callstatus=False, callexisting=False):
162 162 func._callstatus = callstatus
163 163 func._callexisting = callexisting
164
165 class _templateregistrarbase(_funcregistrarbase):
166 """Base of decorator to register functions as template specific one
167 """
168 _docformat = ":%s: %s"
169
170 class templatekeyword(_templateregistrarbase):
171 """Decorator to register template keyword
172
173 Usage::
174
175 templaetkeyword = registrar.templatekeyword()
176
177 @templatekeyword('mykeyword')
178 def mykeywordfunc(repo, ctx, templ, cache, revcache, **args):
179 '''Explanation of this template keyword ....
180 '''
181 pass
182
183 The first string argument is used also in online help.
184
185 'templatekeyword' instance in example above can be used to
186 decorate multiple functions.
187
188 Decorated functions are registered automatically at loading
189 extension, if an instance named as 'templatekeyword' is used for
190 decorating in extension.
191
192 Otherwise, explicit 'templatekw.loadkeyword()' is needed.
193 """
@@ -1,578 +1,584 b''
1 1 # templatekw.py - common changeset template keywords
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from .node import hex, nullid
11 11 from . import (
12 12 encoding,
13 13 error,
14 14 hbisect,
15 15 patch,
16 16 scmutil,
17 17 util,
18 18 )
19 19
20 20 # This helper class allows us to handle both:
21 21 # "{files}" (legacy command-line-specific list hack) and
22 22 # "{files % '{file}\n'}" (hgweb-style with inlining and function support)
23 23 # and to access raw values:
24 24 # "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
25 25 # "{get(extras, key)}"
26 26
27 27 class _hybrid(object):
28 28 def __init__(self, gen, values, makemap, joinfmt=None):
29 29 self.gen = gen
30 30 self.values = values
31 31 self._makemap = makemap
32 32 if joinfmt:
33 33 self.joinfmt = joinfmt
34 34 else:
35 35 self.joinfmt = lambda x: x.values()[0]
36 36 def __iter__(self):
37 37 return self.gen
38 38 def itermaps(self):
39 39 makemap = self._makemap
40 40 for x in self.values:
41 41 yield makemap(x)
42 42 def __contains__(self, x):
43 43 return x in self.values
44 44 def __len__(self):
45 45 return len(self.values)
46 46 def __getattr__(self, name):
47 47 if name != 'get':
48 48 raise AttributeError(name)
49 49 return getattr(self.values, name)
50 50
51 51 def showlist(name, values, plural=None, element=None, separator=' ', **args):
52 52 if not element:
53 53 element = name
54 54 f = _showlist(name, values, plural, separator, **args)
55 55 return _hybrid(f, values, lambda x: {element: x})
56 56
57 57 def _showlist(name, values, plural=None, separator=' ', **args):
58 58 '''expand set of values.
59 59 name is name of key in template map.
60 60 values is list of strings or dicts.
61 61 plural is plural of name, if not simply name + 's'.
62 62 separator is used to join values as a string
63 63
64 64 expansion works like this, given name 'foo'.
65 65
66 66 if values is empty, expand 'no_foos'.
67 67
68 68 if 'foo' not in template map, return values as a string,
69 69 joined by 'separator'.
70 70
71 71 expand 'start_foos'.
72 72
73 73 for each value, expand 'foo'. if 'last_foo' in template
74 74 map, expand it instead of 'foo' for last key.
75 75
76 76 expand 'end_foos'.
77 77 '''
78 78 templ = args['templ']
79 79 if plural:
80 80 names = plural
81 81 else: names = name + 's'
82 82 if not values:
83 83 noname = 'no_' + names
84 84 if noname in templ:
85 85 yield templ(noname, **args)
86 86 return
87 87 if name not in templ:
88 88 if isinstance(values[0], str):
89 89 yield separator.join(values)
90 90 else:
91 91 for v in values:
92 92 yield dict(v, **args)
93 93 return
94 94 startname = 'start_' + names
95 95 if startname in templ:
96 96 yield templ(startname, **args)
97 97 vargs = args.copy()
98 98 def one(v, tag=name):
99 99 try:
100 100 vargs.update(v)
101 101 except (AttributeError, ValueError):
102 102 try:
103 103 for a, b in v:
104 104 vargs[a] = b
105 105 except ValueError:
106 106 vargs[name] = v
107 107 return templ(tag, **vargs)
108 108 lastname = 'last_' + name
109 109 if lastname in templ:
110 110 last = values.pop()
111 111 else:
112 112 last = None
113 113 for v in values:
114 114 yield one(v)
115 115 if last is not None:
116 116 yield one(last, tag=lastname)
117 117 endname = 'end_' + names
118 118 if endname in templ:
119 119 yield templ(endname, **args)
120 120
121 121 def getfiles(repo, ctx, revcache):
122 122 if 'files' not in revcache:
123 123 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
124 124 return revcache['files']
125 125
126 126 def getlatesttags(repo, ctx, cache, pattern=None):
127 127 '''return date, distance and name for the latest tag of rev'''
128 128
129 129 cachename = 'latesttags'
130 130 if pattern is not None:
131 131 cachename += '-' + pattern
132 132 match = util.stringmatcher(pattern)[2]
133 133 else:
134 134 match = util.always
135 135
136 136 if cachename not in cache:
137 137 # Cache mapping from rev to a tuple with tag date, tag
138 138 # distance and tag name
139 139 cache[cachename] = {-1: (0, 0, ['null'])}
140 140 latesttags = cache[cachename]
141 141
142 142 rev = ctx.rev()
143 143 todo = [rev]
144 144 while todo:
145 145 rev = todo.pop()
146 146 if rev in latesttags:
147 147 continue
148 148 ctx = repo[rev]
149 149 tags = [t for t in ctx.tags()
150 150 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
151 151 and match(t))]
152 152 if tags:
153 153 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
154 154 continue
155 155 try:
156 156 # The tuples are laid out so the right one can be found by
157 157 # comparison.
158 158 pdate, pdist, ptag = max(
159 159 latesttags[p.rev()] for p in ctx.parents())
160 160 except KeyError:
161 161 # Cache miss - recurse
162 162 todo.append(rev)
163 163 todo.extend(p.rev() for p in ctx.parents())
164 164 continue
165 165 latesttags[rev] = pdate, pdist + 1, ptag
166 166 return latesttags[rev]
167 167
168 168 def getrenamedfn(repo, endrev=None):
169 169 rcache = {}
170 170 if endrev is None:
171 171 endrev = len(repo)
172 172
173 173 def getrenamed(fn, rev):
174 174 '''looks up all renames for a file (up to endrev) the first
175 175 time the file is given. It indexes on the changerev and only
176 176 parses the manifest if linkrev != changerev.
177 177 Returns rename info for fn at changerev rev.'''
178 178 if fn not in rcache:
179 179 rcache[fn] = {}
180 180 fl = repo.file(fn)
181 181 for i in fl:
182 182 lr = fl.linkrev(i)
183 183 renamed = fl.renamed(fl.node(i))
184 184 rcache[fn][lr] = renamed
185 185 if lr >= endrev:
186 186 break
187 187 if rev in rcache[fn]:
188 188 return rcache[fn][rev]
189 189
190 190 # If linkrev != rev (i.e. rev not found in rcache) fallback to
191 191 # filectx logic.
192 192 try:
193 193 return repo[rev][fn].renamed()
194 194 except error.LookupError:
195 195 return None
196 196
197 197 return getrenamed
198 198
199 199
200 200 def showauthor(repo, ctx, templ, **args):
201 201 """:author: String. The unmodified author of the changeset."""
202 202 return ctx.user()
203 203
204 204 def showbisect(repo, ctx, templ, **args):
205 205 """:bisect: String. The changeset bisection status."""
206 206 return hbisect.label(repo, ctx.node())
207 207
208 208 def showbranch(**args):
209 209 """:branch: String. The name of the branch on which the changeset was
210 210 committed.
211 211 """
212 212 return args['ctx'].branch()
213 213
214 214 def showbranches(**args):
215 215 """:branches: List of strings. The name of the branch on which the
216 216 changeset was committed. Will be empty if the branch name was
217 217 default. (DEPRECATED)
218 218 """
219 219 branch = args['ctx'].branch()
220 220 if branch != 'default':
221 221 return showlist('branch', [branch], plural='branches', **args)
222 222 return showlist('branch', [], plural='branches', **args)
223 223
224 224 def showbookmarks(**args):
225 225 """:bookmarks: List of strings. Any bookmarks associated with the
226 226 changeset. Also sets 'active', the name of the active bookmark.
227 227 """
228 228 repo = args['ctx']._repo
229 229 bookmarks = args['ctx'].bookmarks()
230 230 active = repo._activebookmark
231 231 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
232 232 f = _showlist('bookmark', bookmarks, **args)
233 233 return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark'])
234 234
235 235 def showchildren(**args):
236 236 """:children: List of strings. The children of the changeset."""
237 237 ctx = args['ctx']
238 238 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
239 239 return showlist('children', childrevs, element='child', **args)
240 240
241 241 # Deprecated, but kept alive for help generation a purpose.
242 242 def showcurrentbookmark(**args):
243 243 """:currentbookmark: String. The active bookmark, if it is
244 244 associated with the changeset (DEPRECATED)"""
245 245 return showactivebookmark(**args)
246 246
247 247 def showactivebookmark(**args):
248 248 """:activebookmark: String. The active bookmark, if it is
249 249 associated with the changeset"""
250 250 active = args['repo']._activebookmark
251 251 if active and active in args['ctx'].bookmarks():
252 252 return active
253 253 return ''
254 254
255 255 def showdate(repo, ctx, templ, **args):
256 256 """:date: Date information. The date when the changeset was committed."""
257 257 return ctx.date()
258 258
259 259 def showdescription(repo, ctx, templ, **args):
260 260 """:desc: String. The text of the changeset description."""
261 261 s = ctx.description()
262 262 if isinstance(s, encoding.localstr):
263 263 # try hard to preserve utf-8 bytes
264 264 return encoding.tolocal(encoding.fromlocal(s).strip())
265 265 else:
266 266 return s.strip()
267 267
268 268 def showdiffstat(repo, ctx, templ, **args):
269 269 """:diffstat: String. Statistics of changes with the following format:
270 270 "modified files: +added/-removed lines"
271 271 """
272 272 stats = patch.diffstatdata(util.iterlines(ctx.diff()))
273 273 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
274 274 return '%s: +%s/-%s' % (len(stats), adds, removes)
275 275
276 276 def showextras(**args):
277 277 """:extras: List of dicts with key, value entries of the 'extras'
278 278 field of this changeset."""
279 279 extras = args['ctx'].extra()
280 280 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
281 281 makemap = lambda k: {'key': k, 'value': extras[k]}
282 282 c = [makemap(k) for k in extras]
283 283 f = _showlist('extra', c, plural='extras', **args)
284 284 return _hybrid(f, extras, makemap,
285 285 lambda x: '%s=%s' % (x['key'], x['value']))
286 286
287 287 def showfileadds(**args):
288 288 """:file_adds: List of strings. Files added by this changeset."""
289 289 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
290 290 return showlist('file_add', getfiles(repo, ctx, revcache)[1],
291 291 element='file', **args)
292 292
293 293 def showfilecopies(**args):
294 294 """:file_copies: List of strings. Files copied in this changeset with
295 295 their sources.
296 296 """
297 297 cache, ctx = args['cache'], args['ctx']
298 298 copies = args['revcache'].get('copies')
299 299 if copies is None:
300 300 if 'getrenamed' not in cache:
301 301 cache['getrenamed'] = getrenamedfn(args['repo'])
302 302 copies = []
303 303 getrenamed = cache['getrenamed']
304 304 for fn in ctx.files():
305 305 rename = getrenamed(fn, ctx.rev())
306 306 if rename:
307 307 copies.append((fn, rename[0]))
308 308
309 309 copies = util.sortdict(copies)
310 310 makemap = lambda k: {'name': k, 'source': copies[k]}
311 311 c = [makemap(k) for k in copies]
312 312 f = _showlist('file_copy', c, plural='file_copies', **args)
313 313 return _hybrid(f, copies, makemap,
314 314 lambda x: '%s (%s)' % (x['name'], x['source']))
315 315
316 316 # showfilecopiesswitch() displays file copies only if copy records are
317 317 # provided before calling the templater, usually with a --copies
318 318 # command line switch.
319 319 def showfilecopiesswitch(**args):
320 320 """:file_copies_switch: List of strings. Like "file_copies" but displayed
321 321 only if the --copied switch is set.
322 322 """
323 323 copies = args['revcache'].get('copies') or []
324 324 copies = util.sortdict(copies)
325 325 makemap = lambda k: {'name': k, 'source': copies[k]}
326 326 c = [makemap(k) for k in copies]
327 327 f = _showlist('file_copy', c, plural='file_copies', **args)
328 328 return _hybrid(f, copies, makemap,
329 329 lambda x: '%s (%s)' % (x['name'], x['source']))
330 330
331 331 def showfiledels(**args):
332 332 """:file_dels: List of strings. Files removed by this changeset."""
333 333 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
334 334 return showlist('file_del', getfiles(repo, ctx, revcache)[2],
335 335 element='file', **args)
336 336
337 337 def showfilemods(**args):
338 338 """:file_mods: List of strings. Files modified by this changeset."""
339 339 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
340 340 return showlist('file_mod', getfiles(repo, ctx, revcache)[0],
341 341 element='file', **args)
342 342
343 343 def showfiles(**args):
344 344 """:files: List of strings. All files modified, added, or removed by this
345 345 changeset.
346 346 """
347 347 return showlist('file', args['ctx'].files(), **args)
348 348
349 349 def showgraphnode(repo, ctx, **args):
350 350 """:graphnode: String. The character representing the changeset node in
351 351 an ASCII revision graph"""
352 352 wpnodes = repo.dirstate.parents()
353 353 if wpnodes[1] == nullid:
354 354 wpnodes = wpnodes[:1]
355 355 if ctx.node() in wpnodes:
356 356 return '@'
357 357 elif ctx.obsolete():
358 358 return 'x'
359 359 elif ctx.closesbranch():
360 360 return '_'
361 361 else:
362 362 return 'o'
363 363
364 364 def showlatesttag(**args):
365 365 """:latesttag: List of strings. The global tags on the most recent globally
366 366 tagged ancestor of this changeset.
367 367 """
368 368 return showlatesttags(None, **args)
369 369
370 370 def showlatesttags(pattern, **args):
371 371 """helper method for the latesttag keyword and function"""
372 372 repo, ctx = args['repo'], args['ctx']
373 373 cache = args['cache']
374 374 latesttags = getlatesttags(repo, ctx, cache, pattern)
375 375
376 376 # latesttag[0] is an implementation detail for sorting csets on different
377 377 # branches in a stable manner- it is the date the tagged cset was created,
378 378 # not the date the tag was created. Therefore it isn't made visible here.
379 379 makemap = lambda v: {
380 380 'changes': _showchangessincetag,
381 381 'distance': latesttags[1],
382 382 'latesttag': v, # BC with {latesttag % '{latesttag}'}
383 383 'tag': v
384 384 }
385 385
386 386 tags = latesttags[2]
387 387 f = _showlist('latesttag', tags, separator=':', **args)
388 388 return _hybrid(f, tags, makemap, lambda x: x['latesttag'])
389 389
390 390 def showlatesttagdistance(repo, ctx, templ, cache, **args):
391 391 """:latesttagdistance: Integer. Longest path to the latest tag."""
392 392 return getlatesttags(repo, ctx, cache)[1]
393 393
394 394 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
395 395 """:changessincelatesttag: Integer. All ancestors not in the latest tag."""
396 396 latesttag = getlatesttags(repo, ctx, cache)[2][0]
397 397
398 398 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
399 399
400 400 def _showchangessincetag(repo, ctx, **args):
401 401 offset = 0
402 402 revs = [ctx.rev()]
403 403 tag = args['tag']
404 404
405 405 # The only() revset doesn't currently support wdir()
406 406 if ctx.rev() is None:
407 407 offset = 1
408 408 revs = [p.rev() for p in ctx.parents()]
409 409
410 410 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
411 411
412 412 def showmanifest(**args):
413 413 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
414 414 mnode = ctx.manifestnode()
415 415 if mnode is None:
416 416 # just avoid crash, we might want to use the 'ff...' hash in future
417 417 return
418 418 args = args.copy()
419 419 args.update({'rev': repo.manifest.rev(mnode), 'node': hex(mnode)})
420 420 return templ('manifest', **args)
421 421
422 422 def shownames(namespace, **args):
423 423 """helper method to generate a template keyword for a namespace"""
424 424 ctx = args['ctx']
425 425 repo = ctx.repo()
426 426 ns = repo.names[namespace]
427 427 names = ns.names(repo, ctx.node())
428 428 return showlist(ns.templatename, names, plural=namespace, **args)
429 429
430 430 def shownamespaces(**args):
431 431 """:namespaces: Dict of lists. Names attached to this changeset per
432 432 namespace."""
433 433 ctx = args['ctx']
434 434 repo = ctx.repo()
435 435 namespaces = util.sortdict((k, showlist('name', ns.names(repo, ctx.node()),
436 436 **args))
437 437 for k, ns in repo.names.iteritems())
438 438 f = _showlist('namespace', list(namespaces), **args)
439 439 return _hybrid(f, namespaces,
440 440 lambda k: {'namespace': k, 'names': namespaces[k]},
441 441 lambda x: x['namespace'])
442 442
443 443 def shownode(repo, ctx, templ, **args):
444 444 """:node: String. The changeset identification hash, as a 40 hexadecimal
445 445 digit string.
446 446 """
447 447 return ctx.hex()
448 448
449 449 def showp1rev(repo, ctx, templ, **args):
450 450 """:p1rev: Integer. The repository-local revision number of the changeset's
451 451 first parent, or -1 if the changeset has no parents."""
452 452 return ctx.p1().rev()
453 453
454 454 def showp2rev(repo, ctx, templ, **args):
455 455 """:p2rev: Integer. The repository-local revision number of the changeset's
456 456 second parent, or -1 if the changeset has no second parent."""
457 457 return ctx.p2().rev()
458 458
459 459 def showp1node(repo, ctx, templ, **args):
460 460 """:p1node: String. The identification hash of the changeset's first parent,
461 461 as a 40 digit hexadecimal string. If the changeset has no parents, all
462 462 digits are 0."""
463 463 return ctx.p1().hex()
464 464
465 465 def showp2node(repo, ctx, templ, **args):
466 466 """:p2node: String. The identification hash of the changeset's second
467 467 parent, as a 40 digit hexadecimal string. If the changeset has no second
468 468 parent, all digits are 0."""
469 469 return ctx.p2().hex()
470 470
471 471 def showparents(**args):
472 472 """:parents: List of strings. The parents of the changeset in "rev:node"
473 473 format. If the changeset has only one "natural" parent (the predecessor
474 474 revision) nothing is shown."""
475 475 repo = args['repo']
476 476 ctx = args['ctx']
477 477 pctxs = scmutil.meaningfulparents(repo, ctx)
478 478 prevs = [str(p.rev()) for p in pctxs] # ifcontains() needs a list of str
479 479 parents = [[('rev', p.rev()),
480 480 ('node', p.hex()),
481 481 ('phase', p.phasestr())]
482 482 for p in pctxs]
483 483 f = _showlist('parent', parents, **args)
484 484 return _hybrid(f, prevs, lambda x: {'ctx': repo[int(x)], 'revcache': {}})
485 485
486 486 def showphase(repo, ctx, templ, **args):
487 487 """:phase: String. The changeset phase name."""
488 488 return ctx.phasestr()
489 489
490 490 def showphaseidx(repo, ctx, templ, **args):
491 491 """:phaseidx: Integer. The changeset phase index."""
492 492 return ctx.phase()
493 493
494 494 def showrev(repo, ctx, templ, **args):
495 495 """:rev: Integer. The repository-local changeset revision number."""
496 496 return scmutil.intrev(ctx.rev())
497 497
498 498 def showrevslist(name, revs, **args):
499 499 """helper to generate a list of revisions in which a mapped template will
500 500 be evaluated"""
501 501 repo = args['ctx'].repo()
502 502 revs = [str(r) for r in revs] # ifcontains() needs a list of str
503 503 f = _showlist(name, revs, **args)
504 504 return _hybrid(f, revs,
505 505 lambda x: {name: x, 'ctx': repo[int(x)], 'revcache': {}})
506 506
507 507 def showsubrepos(**args):
508 508 """:subrepos: List of strings. Updated subrepositories in the changeset."""
509 509 ctx = args['ctx']
510 510 substate = ctx.substate
511 511 if not substate:
512 512 return showlist('subrepo', [], **args)
513 513 psubstate = ctx.parents()[0].substate or {}
514 514 subrepos = []
515 515 for sub in substate:
516 516 if sub not in psubstate or substate[sub] != psubstate[sub]:
517 517 subrepos.append(sub) # modified or newly added in ctx
518 518 for sub in psubstate:
519 519 if sub not in substate:
520 520 subrepos.append(sub) # removed in ctx
521 521 return showlist('subrepo', sorted(subrepos), **args)
522 522
523 523 # don't remove "showtags" definition, even though namespaces will put
524 524 # a helper function for "tags" keyword into "keywords" map automatically,
525 525 # because online help text is built without namespaces initialization
526 526 def showtags(**args):
527 527 """:tags: List of strings. Any tags associated with the changeset."""
528 528 return shownames('tags', **args)
529 529
530 530 # keywords are callables like:
531 531 # fn(repo, ctx, templ, cache, revcache, **args)
532 532 # with:
533 533 # repo - current repository instance
534 534 # ctx - the changectx being displayed
535 535 # templ - the templater instance
536 536 # cache - a cache dictionary for the whole templater run
537 537 # revcache - a cache dictionary for the current revision
538 538 keywords = {
539 539 'activebookmark': showactivebookmark,
540 540 'author': showauthor,
541 541 'bisect': showbisect,
542 542 'branch': showbranch,
543 543 'branches': showbranches,
544 544 'bookmarks': showbookmarks,
545 545 'changessincelatesttag': showchangessincelatesttag,
546 546 'children': showchildren,
547 547 # currentbookmark is deprecated
548 548 'currentbookmark': showcurrentbookmark,
549 549 'date': showdate,
550 550 'desc': showdescription,
551 551 'diffstat': showdiffstat,
552 552 'extras': showextras,
553 553 'file_adds': showfileadds,
554 554 'file_copies': showfilecopies,
555 555 'file_copies_switch': showfilecopiesswitch,
556 556 'file_dels': showfiledels,
557 557 'file_mods': showfilemods,
558 558 'files': showfiles,
559 559 'graphnode': showgraphnode,
560 560 'latesttag': showlatesttag,
561 561 'latesttagdistance': showlatesttagdistance,
562 562 'manifest': showmanifest,
563 563 'namespaces': shownamespaces,
564 564 'node': shownode,
565 565 'p1rev': showp1rev,
566 566 'p1node': showp1node,
567 567 'p2rev': showp2rev,
568 568 'p2node': showp2node,
569 569 'parents': showparents,
570 570 'phase': showphase,
571 571 'phaseidx': showphaseidx,
572 572 'rev': showrev,
573 573 'subrepos': showsubrepos,
574 574 'tags': showtags,
575 575 }
576 576
577 def loadkeyword(ui, extname, registrarobj):
578 """Load template keyword from specified registrarobj
579 """
580 for name, func in registrarobj._table.iteritems():
581 keywords[name] = func
582
577 583 # tell hggettext to extract docstrings from these functions:
578 584 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now