##// END OF EJS Templates
dispatch: print traceback in scmutil.callcatch() if --traceback specified...
Yuya Nishihara -
r32041:38963a53 default
parent child Browse files
Show More
@@ -1,961 +1,960 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 debugcommands,
29 29 demandimport,
30 30 encoding,
31 31 error,
32 32 extensions,
33 33 fancyopts,
34 34 fileset,
35 35 help,
36 36 hg,
37 37 hook,
38 38 profiling,
39 39 pycompat,
40 40 revset,
41 41 scmutil,
42 42 templatefilters,
43 43 templatekw,
44 44 templater,
45 45 ui as uimod,
46 46 util,
47 47 )
48 48
49 49 class request(object):
50 50 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
51 51 ferr=None):
52 52 self.args = args
53 53 self.ui = ui
54 54 self.repo = repo
55 55
56 56 # input/output/error streams
57 57 self.fin = fin
58 58 self.fout = fout
59 59 self.ferr = ferr
60 60
61 61 def _runexithandlers(self):
62 62 exc = None
63 63 handlers = self.ui._exithandlers
64 64 try:
65 65 while handlers:
66 66 func, args, kwargs = handlers.pop()
67 67 try:
68 68 func(*args, **kwargs)
69 69 except: # re-raises below
70 70 if exc is None:
71 71 exc = sys.exc_info()[1]
72 72 self.ui.warn(('error in exit handlers:\n'))
73 73 self.ui.traceback(force=True)
74 74 finally:
75 75 if exc is not None:
76 76 raise exc
77 77
78 78 def run():
79 79 "run the command in sys.argv"
80 80 req = request(pycompat.sysargv[1:])
81 81 err = None
82 82 try:
83 83 status = (dispatch(req) or 0) & 255
84 84 except error.StdioError as err:
85 85 status = -1
86 86 if util.safehasattr(req.ui, 'fout'):
87 87 try:
88 88 req.ui.fout.close()
89 89 except IOError as err:
90 90 status = -1
91 91 if util.safehasattr(req.ui, 'ferr'):
92 92 if err is not None and err.errno != errno.EPIPE:
93 93 req.ui.ferr.write('abort: %s\n' % err.strerror)
94 94 req.ui.ferr.close()
95 95 sys.exit(status & 255)
96 96
97 97 def _getsimilar(symbols, value):
98 98 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
99 99 # The cutoff for similarity here is pretty arbitrary. It should
100 100 # probably be investigated and tweaked.
101 101 return [s for s in symbols if sim(s) > 0.6]
102 102
103 103 def _reportsimilar(write, similar):
104 104 if len(similar) == 1:
105 105 write(_("(did you mean %s?)\n") % similar[0])
106 106 elif similar:
107 107 ss = ", ".join(sorted(similar))
108 108 write(_("(did you mean one of %s?)\n") % ss)
109 109
110 110 def _formatparse(write, inst):
111 111 similar = []
112 112 if isinstance(inst, error.UnknownIdentifier):
113 113 # make sure to check fileset first, as revset can invoke fileset
114 114 similar = _getsimilar(inst.symbols, inst.function)
115 115 if len(inst.args) > 1:
116 116 write(_("hg: parse error at %s: %s\n") %
117 117 (inst.args[1], inst.args[0]))
118 118 if (inst.args[0][0] == ' '):
119 119 write(_("unexpected leading whitespace\n"))
120 120 else:
121 121 write(_("hg: parse error: %s\n") % inst.args[0])
122 122 _reportsimilar(write, similar)
123 123 if inst.hint:
124 124 write(_("(%s)\n") % inst.hint)
125 125
126 126 def _formatargs(args):
127 127 return ' '.join(util.shellquote(a) for a in args)
128 128
129 129 def dispatch(req):
130 130 "run the command specified in req.args"
131 131 if req.ferr:
132 132 ferr = req.ferr
133 133 elif req.ui:
134 134 ferr = req.ui.ferr
135 135 else:
136 136 ferr = util.stderr
137 137
138 138 try:
139 139 if not req.ui:
140 140 req.ui = uimod.ui.load()
141 141 if '--traceback' in req.args:
142 142 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
143 143
144 144 # set ui streams from the request
145 145 if req.fin:
146 146 req.ui.fin = req.fin
147 147 if req.fout:
148 148 req.ui.fout = req.fout
149 149 if req.ferr:
150 150 req.ui.ferr = req.ferr
151 151 except error.Abort as inst:
152 152 ferr.write(_("abort: %s\n") % inst)
153 153 if inst.hint:
154 154 ferr.write(_("(%s)\n") % inst.hint)
155 155 return -1
156 156 except error.ParseError as inst:
157 157 _formatparse(ferr.write, inst)
158 158 return -1
159 159
160 160 msg = _formatargs(req.args)
161 161 starttime = util.timer()
162 162 ret = None
163 163 try:
164 164 ret = _runcatch(req)
165 165 except KeyboardInterrupt:
166 166 try:
167 167 req.ui.warn(_("interrupted!\n"))
168 168 except IOError as inst:
169 169 if inst.errno != errno.EPIPE:
170 170 raise
171 171 ret = -1
172 172 finally:
173 173 duration = util.timer() - starttime
174 174 req.ui.flush()
175 175 if req.ui.logblockedtimes:
176 176 req.ui._blockedtimes['command_duration'] = duration * 1000
177 177 req.ui.log('uiblocked', 'ui blocked ms', **req.ui._blockedtimes)
178 178 req.ui.log("commandfinish", "%s exited %s after %0.2f seconds\n",
179 179 msg, ret or 0, duration)
180 180 try:
181 181 req._runexithandlers()
182 182 except: # exiting, so no re-raises
183 183 ret = ret or -1
184 184 return ret
185 185
186 186 def _runcatch(req):
187 187 def catchterm(*args):
188 188 raise error.SignalInterrupt
189 189
190 190 ui = req.ui
191 191 try:
192 192 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
193 193 num = getattr(signal, name, None)
194 194 if num:
195 195 signal.signal(num, catchterm)
196 196 except ValueError:
197 197 pass # happens if called in a thread
198 198
199 199 def _runcatchfunc():
200 200 try:
201 201 debugger = 'pdb'
202 202 debugtrace = {
203 203 'pdb' : pdb.set_trace
204 204 }
205 205 debugmortem = {
206 206 'pdb' : pdb.post_mortem
207 207 }
208 208
209 209 # read --config before doing anything else
210 210 # (e.g. to change trust settings for reading .hg/hgrc)
211 211 cfgs = _parseconfig(req.ui, _earlygetopt(['--config'], req.args))
212 212
213 213 if req.repo:
214 214 # copy configs that were passed on the cmdline (--config) to
215 215 # the repo ui
216 216 for sec, name, val in cfgs:
217 217 req.repo.ui.setconfig(sec, name, val, source='--config')
218 218
219 219 # developer config: ui.debugger
220 220 debugger = ui.config("ui", "debugger")
221 221 debugmod = pdb
222 222 if not debugger or ui.plain():
223 223 # if we are in HGPLAIN mode, then disable custom debugging
224 224 debugger = 'pdb'
225 225 elif '--debugger' in req.args:
226 226 # This import can be slow for fancy debuggers, so only
227 227 # do it when absolutely necessary, i.e. when actual
228 228 # debugging has been requested
229 229 with demandimport.deactivated():
230 230 try:
231 231 debugmod = __import__(debugger)
232 232 except ImportError:
233 233 pass # Leave debugmod = pdb
234 234
235 235 debugtrace[debugger] = debugmod.set_trace
236 236 debugmortem[debugger] = debugmod.post_mortem
237 237
238 238 # enter the debugger before command execution
239 239 if '--debugger' in req.args:
240 240 ui.warn(_("entering debugger - "
241 241 "type c to continue starting hg or h for help\n"))
242 242
243 243 if (debugger != 'pdb' and
244 244 debugtrace[debugger] == debugtrace['pdb']):
245 245 ui.warn(_("%s debugger specified "
246 246 "but its module was not found\n") % debugger)
247 247 with demandimport.deactivated():
248 248 debugtrace[debugger]()
249 249 try:
250 250 return _dispatch(req)
251 251 finally:
252 252 ui.flush()
253 253 except: # re-raises
254 254 # enter the debugger when we hit an exception
255 255 if '--debugger' in req.args:
256 256 traceback.print_exc()
257 257 debugmortem[debugger](sys.exc_info()[2])
258 ui.traceback()
259 258 raise
260 259
261 260 return _callcatch(ui, _runcatchfunc)
262 261
263 262 def _callcatch(ui, func):
264 263 """like scmutil.callcatch but handles more high-level exceptions about
265 264 config parsing and commands. besides, use handlecommandexception to handle
266 265 uncaught exceptions.
267 266 """
268 267 try:
269 268 return scmutil.callcatch(ui, func)
270 269 except error.AmbiguousCommand as inst:
271 270 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
272 271 (inst.args[0], " ".join(inst.args[1])))
273 272 except error.CommandError as inst:
274 273 if inst.args[0]:
275 274 ui.pager('help')
276 275 ui.warn(_("hg %s: %s\n") % (inst.args[0], inst.args[1]))
277 276 commands.help_(ui, inst.args[0], full=False, command=True)
278 277 else:
279 278 ui.pager('help')
280 279 ui.warn(_("hg: %s\n") % inst.args[1])
281 280 commands.help_(ui, 'shortlist')
282 281 except error.ParseError as inst:
283 282 _formatparse(ui.warn, inst)
284 283 return -1
285 284 except error.UnknownCommand as inst:
286 285 nocmdmsg = _("hg: unknown command '%s'\n") % inst.args[0]
287 286 try:
288 287 # check if the command is in a disabled extension
289 288 # (but don't check for extensions themselves)
290 289 formatted = help.formattedhelp(ui, inst.args[0], unknowncmd=True)
291 290 ui.warn(nocmdmsg)
292 291 ui.write(formatted)
293 292 except (error.UnknownCommand, error.Abort):
294 293 suggested = False
295 294 if len(inst.args) == 2:
296 295 sim = _getsimilar(inst.args[1], inst.args[0])
297 296 if sim:
298 297 ui.warn(nocmdmsg)
299 298 _reportsimilar(ui.warn, sim)
300 299 suggested = True
301 300 if not suggested:
302 301 ui.pager('help')
303 302 ui.warn(nocmdmsg)
304 303 commands.help_(ui, 'shortlist')
305 304 except IOError:
306 305 raise
307 306 except KeyboardInterrupt:
308 307 raise
309 308 except: # probably re-raises
310 309 if not handlecommandexception(ui):
311 310 raise
312 311
313 312 return -1
314 313
315 314 def aliasargs(fn, givenargs):
316 315 args = getattr(fn, 'args', [])
317 316 if args:
318 317 cmd = ' '.join(map(util.shellquote, args))
319 318
320 319 nums = []
321 320 def replacer(m):
322 321 num = int(m.group(1)) - 1
323 322 nums.append(num)
324 323 if num < len(givenargs):
325 324 return givenargs[num]
326 325 raise error.Abort(_('too few arguments for command alias'))
327 326 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
328 327 givenargs = [x for i, x in enumerate(givenargs)
329 328 if i not in nums]
330 329 args = pycompat.shlexsplit(cmd)
331 330 return args + givenargs
332 331
333 332 def aliasinterpolate(name, args, cmd):
334 333 '''interpolate args into cmd for shell aliases
335 334
336 335 This also handles $0, $@ and "$@".
337 336 '''
338 337 # util.interpolate can't deal with "$@" (with quotes) because it's only
339 338 # built to match prefix + patterns.
340 339 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
341 340 replacemap['$0'] = name
342 341 replacemap['$$'] = '$'
343 342 replacemap['$@'] = ' '.join(args)
344 343 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
345 344 # parameters, separated out into words. Emulate the same behavior here by
346 345 # quoting the arguments individually. POSIX shells will then typically
347 346 # tokenize each argument into exactly one word.
348 347 replacemap['"$@"'] = ' '.join(util.shellquote(arg) for arg in args)
349 348 # escape '\$' for regex
350 349 regex = '|'.join(replacemap.keys()).replace('$', r'\$')
351 350 r = re.compile(regex)
352 351 return r.sub(lambda x: replacemap[x.group()], cmd)
353 352
354 353 class cmdalias(object):
355 354 def __init__(self, name, definition, cmdtable, source):
356 355 self.name = self.cmd = name
357 356 self.cmdname = ''
358 357 self.definition = definition
359 358 self.fn = None
360 359 self.givenargs = []
361 360 self.opts = []
362 361 self.help = ''
363 362 self.badalias = None
364 363 self.unknowncmd = False
365 364 self.source = source
366 365
367 366 try:
368 367 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
369 368 for alias, e in cmdtable.iteritems():
370 369 if e is entry:
371 370 self.cmd = alias
372 371 break
373 372 self.shadows = True
374 373 except error.UnknownCommand:
375 374 self.shadows = False
376 375
377 376 if not self.definition:
378 377 self.badalias = _("no definition for alias '%s'") % self.name
379 378 return
380 379
381 380 if self.definition.startswith('!'):
382 381 self.shell = True
383 382 def fn(ui, *args):
384 383 env = {'HG_ARGS': ' '.join((self.name,) + args)}
385 384 def _checkvar(m):
386 385 if m.groups()[0] == '$':
387 386 return m.group()
388 387 elif int(m.groups()[0]) <= len(args):
389 388 return m.group()
390 389 else:
391 390 ui.debug("No argument found for substitution "
392 391 "of %i variable in alias '%s' definition."
393 392 % (int(m.groups()[0]), self.name))
394 393 return ''
395 394 cmd = re.sub(r'\$(\d+|\$)', _checkvar, self.definition[1:])
396 395 cmd = aliasinterpolate(self.name, args, cmd)
397 396 return ui.system(cmd, environ=env,
398 397 blockedtag='alias_%s' % self.name)
399 398 self.fn = fn
400 399 return
401 400
402 401 try:
403 402 args = pycompat.shlexsplit(self.definition)
404 403 except ValueError as inst:
405 404 self.badalias = (_("error in definition for alias '%s': %s")
406 405 % (self.name, inst))
407 406 return
408 407 self.cmdname = cmd = args.pop(0)
409 408 self.givenargs = args
410 409
411 410 for invalidarg in ("--cwd", "-R", "--repository", "--repo", "--config"):
412 411 if _earlygetopt([invalidarg], args):
413 412 self.badalias = (_("error in definition for alias '%s': %s may "
414 413 "only be given on the command line")
415 414 % (self.name, invalidarg))
416 415 return
417 416
418 417 try:
419 418 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
420 419 if len(tableentry) > 2:
421 420 self.fn, self.opts, self.help = tableentry
422 421 else:
423 422 self.fn, self.opts = tableentry
424 423
425 424 if self.help.startswith("hg " + cmd):
426 425 # drop prefix in old-style help lines so hg shows the alias
427 426 self.help = self.help[4 + len(cmd):]
428 427 self.__doc__ = self.fn.__doc__
429 428
430 429 except error.UnknownCommand:
431 430 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
432 431 % (self.name, cmd))
433 432 self.unknowncmd = True
434 433 except error.AmbiguousCommand:
435 434 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
436 435 % (self.name, cmd))
437 436
438 437 @property
439 438 def args(self):
440 439 args = pycompat.maplist(util.expandpath, self.givenargs)
441 440 return aliasargs(self.fn, args)
442 441
443 442 def __getattr__(self, name):
444 443 adefaults = {'norepo': True, 'optionalrepo': False, 'inferrepo': False}
445 444 if name not in adefaults:
446 445 raise AttributeError(name)
447 446 if self.badalias or util.safehasattr(self, 'shell'):
448 447 return adefaults[name]
449 448 return getattr(self.fn, name)
450 449
451 450 def __call__(self, ui, *args, **opts):
452 451 if self.badalias:
453 452 hint = None
454 453 if self.unknowncmd:
455 454 try:
456 455 # check if the command is in a disabled extension
457 456 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
458 457 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
459 458 except error.UnknownCommand:
460 459 pass
461 460 raise error.Abort(self.badalias, hint=hint)
462 461 if self.shadows:
463 462 ui.debug("alias '%s' shadows command '%s'\n" %
464 463 (self.name, self.cmdname))
465 464
466 465 ui.log('commandalias', "alias '%s' expands to '%s'\n",
467 466 self.name, self.definition)
468 467 if util.safehasattr(self, 'shell'):
469 468 return self.fn(ui, *args, **opts)
470 469 else:
471 470 try:
472 471 return util.checksignature(self.fn)(ui, *args, **opts)
473 472 except error.SignatureError:
474 473 args = ' '.join([self.cmdname] + self.args)
475 474 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
476 475 raise
477 476
478 477 def addaliases(ui, cmdtable):
479 478 # aliases are processed after extensions have been loaded, so they
480 479 # may use extension commands. Aliases can also use other alias definitions,
481 480 # but only if they have been defined prior to the current definition.
482 481 for alias, definition in ui.configitems('alias'):
483 482 source = ui.configsource('alias', alias)
484 483 aliasdef = cmdalias(alias, definition, cmdtable, source)
485 484
486 485 try:
487 486 olddef = cmdtable[aliasdef.cmd][0]
488 487 if olddef.definition == aliasdef.definition:
489 488 continue
490 489 except (KeyError, AttributeError):
491 490 # definition might not exist or it might not be a cmdalias
492 491 pass
493 492
494 493 cmdtable[aliasdef.name] = (aliasdef, aliasdef.opts, aliasdef.help)
495 494
496 495 def _parse(ui, args):
497 496 options = {}
498 497 cmdoptions = {}
499 498
500 499 try:
501 500 args = fancyopts.fancyopts(args, commands.globalopts, options)
502 501 except getopt.GetoptError as inst:
503 502 raise error.CommandError(None, inst)
504 503
505 504 if args:
506 505 cmd, args = args[0], args[1:]
507 506 aliases, entry = cmdutil.findcmd(cmd, commands.table,
508 507 ui.configbool("ui", "strict"))
509 508 cmd = aliases[0]
510 509 args = aliasargs(entry[0], args)
511 510 defaults = ui.config("defaults", cmd)
512 511 if defaults:
513 512 args = pycompat.maplist(
514 513 util.expandpath, pycompat.shlexsplit(defaults)) + args
515 514 c = list(entry[1])
516 515 else:
517 516 cmd = None
518 517 c = []
519 518
520 519 # combine global options into local
521 520 for o in commands.globalopts:
522 521 c.append((o[0], o[1], options[o[1]], o[3]))
523 522
524 523 try:
525 524 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
526 525 except getopt.GetoptError as inst:
527 526 raise error.CommandError(cmd, inst)
528 527
529 528 # separate global options back out
530 529 for o in commands.globalopts:
531 530 n = o[1]
532 531 options[n] = cmdoptions[n]
533 532 del cmdoptions[n]
534 533
535 534 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
536 535
537 536 def _parseconfig(ui, config):
538 537 """parse the --config options from the command line"""
539 538 configs = []
540 539
541 540 for cfg in config:
542 541 try:
543 542 name, value = [cfgelem.strip()
544 543 for cfgelem in cfg.split('=', 1)]
545 544 section, name = name.split('.', 1)
546 545 if not section or not name:
547 546 raise IndexError
548 547 ui.setconfig(section, name, value, '--config')
549 548 configs.append((section, name, value))
550 549 except (IndexError, ValueError):
551 550 raise error.Abort(_('malformed --config option: %r '
552 551 '(use --config section.name=value)') % cfg)
553 552
554 553 return configs
555 554
556 555 def _earlygetopt(aliases, args):
557 556 """Return list of values for an option (or aliases).
558 557
559 558 The values are listed in the order they appear in args.
560 559 The options and values are removed from args.
561 560
562 561 >>> args = ['x', '--cwd', 'foo', 'y']
563 562 >>> _earlygetopt(['--cwd'], args), args
564 563 (['foo'], ['x', 'y'])
565 564
566 565 >>> args = ['x', '--cwd=bar', 'y']
567 566 >>> _earlygetopt(['--cwd'], args), args
568 567 (['bar'], ['x', 'y'])
569 568
570 569 >>> args = ['x', '-R', 'foo', 'y']
571 570 >>> _earlygetopt(['-R'], args), args
572 571 (['foo'], ['x', 'y'])
573 572
574 573 >>> args = ['x', '-Rbar', 'y']
575 574 >>> _earlygetopt(['-R'], args), args
576 575 (['bar'], ['x', 'y'])
577 576 """
578 577 try:
579 578 argcount = args.index("--")
580 579 except ValueError:
581 580 argcount = len(args)
582 581 shortopts = [opt for opt in aliases if len(opt) == 2]
583 582 values = []
584 583 pos = 0
585 584 while pos < argcount:
586 585 fullarg = arg = args[pos]
587 586 equals = arg.find('=')
588 587 if equals > -1:
589 588 arg = arg[:equals]
590 589 if arg in aliases:
591 590 del args[pos]
592 591 if equals > -1:
593 592 values.append(fullarg[equals + 1:])
594 593 argcount -= 1
595 594 else:
596 595 if pos + 1 >= argcount:
597 596 # ignore and let getopt report an error if there is no value
598 597 break
599 598 values.append(args.pop(pos))
600 599 argcount -= 2
601 600 elif arg[:2] in shortopts:
602 601 # short option can have no following space, e.g. hg log -Rfoo
603 602 values.append(args.pop(pos)[2:])
604 603 argcount -= 1
605 604 else:
606 605 pos += 1
607 606 return values
608 607
609 608 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
610 609 # run pre-hook, and abort if it fails
611 610 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
612 611 pats=cmdpats, opts=cmdoptions)
613 612 try:
614 613 ret = _runcommand(ui, options, cmd, d)
615 614 # run post-hook, passing command result
616 615 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
617 616 result=ret, pats=cmdpats, opts=cmdoptions)
618 617 except Exception:
619 618 # run failure hook and re-raise
620 619 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
621 620 pats=cmdpats, opts=cmdoptions)
622 621 raise
623 622 return ret
624 623
625 624 def _getlocal(ui, rpath, wd=None):
626 625 """Return (path, local ui object) for the given target path.
627 626
628 627 Takes paths in [cwd]/.hg/hgrc into account."
629 628 """
630 629 if wd is None:
631 630 try:
632 631 wd = pycompat.getcwd()
633 632 except OSError as e:
634 633 raise error.Abort(_("error getting current working directory: %s") %
635 634 e.strerror)
636 635 path = cmdutil.findrepo(wd) or ""
637 636 if not path:
638 637 lui = ui
639 638 else:
640 639 lui = ui.copy()
641 640 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
642 641
643 642 if rpath and rpath[-1]:
644 643 path = lui.expandpath(rpath[-1])
645 644 lui = ui.copy()
646 645 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
647 646
648 647 return path, lui
649 648
650 649 def _checkshellalias(lui, ui, args):
651 650 """Return the function to run the shell alias, if it is required"""
652 651 options = {}
653 652
654 653 try:
655 654 args = fancyopts.fancyopts(args, commands.globalopts, options)
656 655 except getopt.GetoptError:
657 656 return
658 657
659 658 if not args:
660 659 return
661 660
662 661 cmdtable = commands.table
663 662
664 663 cmd = args[0]
665 664 try:
666 665 strict = ui.configbool("ui", "strict")
667 666 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
668 667 except (error.AmbiguousCommand, error.UnknownCommand):
669 668 return
670 669
671 670 cmd = aliases[0]
672 671 fn = entry[0]
673 672
674 673 if cmd and util.safehasattr(fn, 'shell'):
675 674 d = lambda: fn(ui, *args[1:])
676 675 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
677 676 [], {})
678 677
679 678 _loaded = set()
680 679
681 680 # list of (objname, loadermod, loadername) tuple:
682 681 # - objname is the name of an object in extension module, from which
683 682 # extra information is loaded
684 683 # - loadermod is the module where loader is placed
685 684 # - loadername is the name of the function, which takes (ui, extensionname,
686 685 # extraobj) arguments
687 686 extraloaders = [
688 687 ('cmdtable', commands, 'loadcmdtable'),
689 688 ('colortable', color, 'loadcolortable'),
690 689 ('filesetpredicate', fileset, 'loadpredicate'),
691 690 ('revsetpredicate', revset, 'loadpredicate'),
692 691 ('templatefilter', templatefilters, 'loadfilter'),
693 692 ('templatefunc', templater, 'loadfunction'),
694 693 ('templatekeyword', templatekw, 'loadkeyword'),
695 694 ]
696 695
697 696 def _dispatch(req):
698 697 args = req.args
699 698 ui = req.ui
700 699
701 700 # check for cwd
702 701 cwd = _earlygetopt(['--cwd'], args)
703 702 if cwd:
704 703 os.chdir(cwd[-1])
705 704
706 705 rpath = _earlygetopt(["-R", "--repository", "--repo"], args)
707 706 path, lui = _getlocal(ui, rpath)
708 707
709 708 # Side-effect of accessing is debugcommands module is guaranteed to be
710 709 # imported and commands.table is populated.
711 710 debugcommands.command
712 711
713 712 uis = set([ui, lui])
714 713
715 714 if req.repo:
716 715 uis.add(req.repo.ui)
717 716
718 717 if '--profile' in args:
719 718 for ui_ in uis:
720 719 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
721 720
722 721 with profiling.maybeprofile(lui):
723 722 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
724 723 # reposetup. Programs like TortoiseHg will call _dispatch several
725 724 # times so we keep track of configured extensions in _loaded.
726 725 extensions.loadall(lui)
727 726 exts = [ext for ext in extensions.extensions() if ext[0] not in _loaded]
728 727 # Propagate any changes to lui.__class__ by extensions
729 728 ui.__class__ = lui.__class__
730 729
731 730 # (uisetup and extsetup are handled in extensions.loadall)
732 731
733 732 for name, module in exts:
734 733 for objname, loadermod, loadername in extraloaders:
735 734 extraobj = getattr(module, objname, None)
736 735 if extraobj is not None:
737 736 getattr(loadermod, loadername)(ui, name, extraobj)
738 737 _loaded.add(name)
739 738
740 739 # (reposetup is handled in hg.repository)
741 740
742 741 addaliases(lui, commands.table)
743 742
744 743 # All aliases and commands are completely defined, now.
745 744 # Check abbreviation/ambiguity of shell alias.
746 745 shellaliasfn = _checkshellalias(lui, ui, args)
747 746 if shellaliasfn:
748 747 return shellaliasfn()
749 748
750 749 # check for fallback encoding
751 750 fallback = lui.config('ui', 'fallbackencoding')
752 751 if fallback:
753 752 encoding.fallbackencoding = fallback
754 753
755 754 fullargs = args
756 755 cmd, func, args, options, cmdoptions = _parse(lui, args)
757 756
758 757 if options["config"]:
759 758 raise error.Abort(_("option --config may not be abbreviated!"))
760 759 if options["cwd"]:
761 760 raise error.Abort(_("option --cwd may not be abbreviated!"))
762 761 if options["repository"]:
763 762 raise error.Abort(_(
764 763 "option -R has to be separated from other options (e.g. not "
765 764 "-qR) and --repository may only be abbreviated as --repo!"))
766 765
767 766 if options["encoding"]:
768 767 encoding.encoding = options["encoding"]
769 768 if options["encodingmode"]:
770 769 encoding.encodingmode = options["encodingmode"]
771 770 if options["time"]:
772 771 def get_times():
773 772 t = os.times()
774 773 if t[4] == 0.0:
775 774 # Windows leaves this as zero, so use time.clock()
776 775 t = (t[0], t[1], t[2], t[3], time.clock())
777 776 return t
778 777 s = get_times()
779 778 def print_time():
780 779 t = get_times()
781 780 ui.warn(
782 781 _("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
783 782 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
784 783 ui.atexit(print_time)
785 784
786 785 if options['verbose'] or options['debug'] or options['quiet']:
787 786 for opt in ('verbose', 'debug', 'quiet'):
788 787 val = str(bool(options[opt]))
789 788 if pycompat.ispy3:
790 789 val = val.encode('ascii')
791 790 for ui_ in uis:
792 791 ui_.setconfig('ui', opt, val, '--' + opt)
793 792
794 793 if options['traceback']:
795 794 for ui_ in uis:
796 795 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
797 796
798 797 if options['noninteractive']:
799 798 for ui_ in uis:
800 799 ui_.setconfig('ui', 'interactive', 'off', '-y')
801 800
802 801 if util.parsebool(options['pager']):
803 802 ui.pager('internal-always-' + cmd)
804 803 elif options['pager'] != 'auto':
805 804 ui.disablepager()
806 805
807 806 if cmdoptions.get('insecure', False):
808 807 for ui_ in uis:
809 808 ui_.insecureconnections = True
810 809
811 810 # setup color handling
812 811 coloropt = options['color']
813 812 for ui_ in uis:
814 813 if coloropt:
815 814 ui_.setconfig('ui', 'color', coloropt, '--color')
816 815 color.setup(ui_)
817 816
818 817 if options['version']:
819 818 return commands.version_(ui)
820 819 if options['help']:
821 820 return commands.help_(ui, cmd, command=cmd is not None)
822 821 elif not cmd:
823 822 return commands.help_(ui, 'shortlist')
824 823
825 824 repo = None
826 825 cmdpats = args[:]
827 826 if not func.norepo:
828 827 # use the repo from the request only if we don't have -R
829 828 if not rpath and not cwd:
830 829 repo = req.repo
831 830
832 831 if repo:
833 832 # set the descriptors of the repo ui to those of ui
834 833 repo.ui.fin = ui.fin
835 834 repo.ui.fout = ui.fout
836 835 repo.ui.ferr = ui.ferr
837 836 else:
838 837 try:
839 838 repo = hg.repository(ui, path=path)
840 839 if not repo.local():
841 840 raise error.Abort(_("repository '%s' is not local")
842 841 % path)
843 842 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
844 843 'repo')
845 844 except error.RequirementError:
846 845 raise
847 846 except error.RepoError:
848 847 if rpath and rpath[-1]: # invalid -R path
849 848 raise
850 849 if not func.optionalrepo:
851 850 if func.inferrepo and args and not path:
852 851 # try to infer -R from command args
853 852 repos = map(cmdutil.findrepo, args)
854 853 guess = repos[0]
855 854 if guess and repos.count(guess) == len(repos):
856 855 req.args = ['--repository', guess] + fullargs
857 856 return _dispatch(req)
858 857 if not path:
859 858 raise error.RepoError(_("no repository found in"
860 859 " '%s' (.hg not found)")
861 860 % pycompat.getcwd())
862 861 raise
863 862 if repo:
864 863 ui = repo.ui
865 864 if options['hidden']:
866 865 repo = repo.unfiltered()
867 866 args.insert(0, repo)
868 867 elif rpath:
869 868 ui.warn(_("warning: --repository ignored\n"))
870 869
871 870 msg = _formatargs(fullargs)
872 871 ui.log("command", '%s\n', msg)
873 872 strcmdopt = pycompat.strkwargs(cmdoptions)
874 873 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
875 874 try:
876 875 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
877 876 cmdpats, cmdoptions)
878 877 finally:
879 878 if repo and repo != req.repo:
880 879 repo.close()
881 880
882 881 def _runcommand(ui, options, cmd, cmdfunc):
883 882 """Run a command function, possibly with profiling enabled."""
884 883 try:
885 884 return cmdfunc()
886 885 except error.SignatureError:
887 886 raise error.CommandError(cmd, _('invalid arguments'))
888 887
889 888 def _exceptionwarning(ui):
890 889 """Produce a warning message for the current active exception"""
891 890
892 891 # For compatibility checking, we discard the portion of the hg
893 892 # version after the + on the assumption that if a "normal
894 893 # user" is running a build with a + in it the packager
895 894 # probably built from fairly close to a tag and anyone with a
896 895 # 'make local' copy of hg (where the version number can be out
897 896 # of date) will be clueful enough to notice the implausible
898 897 # version number and try updating.
899 898 ct = util.versiontuple(n=2)
900 899 worst = None, ct, ''
901 900 if ui.config('ui', 'supportcontact', None) is None:
902 901 for name, mod in extensions.extensions():
903 902 testedwith = getattr(mod, 'testedwith', '')
904 903 if pycompat.ispy3 and isinstance(testedwith, str):
905 904 testedwith = testedwith.encode(u'utf-8')
906 905 report = getattr(mod, 'buglink', _('the extension author.'))
907 906 if not testedwith.strip():
908 907 # We found an untested extension. It's likely the culprit.
909 908 worst = name, 'unknown', report
910 909 break
911 910
912 911 # Never blame on extensions bundled with Mercurial.
913 912 if extensions.ismoduleinternal(mod):
914 913 continue
915 914
916 915 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
917 916 if ct in tested:
918 917 continue
919 918
920 919 lower = [t for t in tested if t < ct]
921 920 nearest = max(lower or tested)
922 921 if worst[0] is None or nearest < worst[1]:
923 922 worst = name, nearest, report
924 923 if worst[0] is not None:
925 924 name, testedwith, report = worst
926 925 if not isinstance(testedwith, (bytes, str)):
927 926 testedwith = '.'.join([str(c) for c in testedwith])
928 927 warning = (_('** Unknown exception encountered with '
929 928 'possibly-broken third-party extension %s\n'
930 929 '** which supports versions %s of Mercurial.\n'
931 930 '** Please disable %s and try your action again.\n'
932 931 '** If that fixes the bug please report it to %s\n')
933 932 % (name, testedwith, name, report))
934 933 else:
935 934 bugtracker = ui.config('ui', 'supportcontact', None)
936 935 if bugtracker is None:
937 936 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
938 937 warning = (_("** unknown exception encountered, "
939 938 "please report by visiting\n** ") + bugtracker + '\n')
940 939 if pycompat.ispy3:
941 940 sysversion = sys.version.encode(u'utf-8')
942 941 else:
943 942 sysversion = sys.version
944 943 sysversion = sysversion.replace('\n', '')
945 944 warning += ((_("** Python %s\n") % sysversion) +
946 945 (_("** Mercurial Distributed SCM (version %s)\n") %
947 946 util.version()) +
948 947 (_("** Extensions loaded: %s\n") %
949 948 ", ".join([x[0] for x in extensions.extensions()])))
950 949 return warning
951 950
952 951 def handlecommandexception(ui):
953 952 """Produce a warning message for broken commands
954 953
955 954 Called when handling an exception; the exception is reraised if
956 955 this function returns False, ignored otherwise.
957 956 """
958 957 warning = _exceptionwarning(ui)
959 958 ui.log("commandexception", "%s\n%s\n", warning, traceback.format_exc())
960 959 ui.warn(warning)
961 960 return False # re-raise the exception
@@ -1,972 +1,976 b''
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright 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 import errno
11 11 import glob
12 12 import hashlib
13 13 import os
14 14 import re
15 15 import socket
16 16
17 17 from .i18n import _
18 18 from .node import wdirrev
19 19 from . import (
20 20 encoding,
21 21 error,
22 22 match as matchmod,
23 23 pathutil,
24 24 phases,
25 25 pycompat,
26 26 revsetlang,
27 27 similar,
28 28 util,
29 29 vfs as vfsmod,
30 30 )
31 31
32 32 if pycompat.osname == 'nt':
33 33 from . import scmwindows as scmplatform
34 34 else:
35 35 from . import scmposix as scmplatform
36 36
37 37 termsize = scmplatform.termsize
38 38
39 39 class status(tuple):
40 40 '''Named tuple with a list of files per status. The 'deleted', 'unknown'
41 41 and 'ignored' properties are only relevant to the working copy.
42 42 '''
43 43
44 44 __slots__ = ()
45 45
46 46 def __new__(cls, modified, added, removed, deleted, unknown, ignored,
47 47 clean):
48 48 return tuple.__new__(cls, (modified, added, removed, deleted, unknown,
49 49 ignored, clean))
50 50
51 51 @property
52 52 def modified(self):
53 53 '''files that have been modified'''
54 54 return self[0]
55 55
56 56 @property
57 57 def added(self):
58 58 '''files that have been added'''
59 59 return self[1]
60 60
61 61 @property
62 62 def removed(self):
63 63 '''files that have been removed'''
64 64 return self[2]
65 65
66 66 @property
67 67 def deleted(self):
68 68 '''files that are in the dirstate, but have been deleted from the
69 69 working copy (aka "missing")
70 70 '''
71 71 return self[3]
72 72
73 73 @property
74 74 def unknown(self):
75 75 '''files not in the dirstate that are not ignored'''
76 76 return self[4]
77 77
78 78 @property
79 79 def ignored(self):
80 80 '''files not in the dirstate that are ignored (by _dirignore())'''
81 81 return self[5]
82 82
83 83 @property
84 84 def clean(self):
85 85 '''files that have not been modified'''
86 86 return self[6]
87 87
88 88 def __repr__(self, *args, **kwargs):
89 89 return (('<status modified=%r, added=%r, removed=%r, deleted=%r, '
90 90 'unknown=%r, ignored=%r, clean=%r>') % self)
91 91
92 92 def itersubrepos(ctx1, ctx2):
93 93 """find subrepos in ctx1 or ctx2"""
94 94 # Create a (subpath, ctx) mapping where we prefer subpaths from
95 95 # ctx1. The subpaths from ctx2 are important when the .hgsub file
96 96 # has been modified (in ctx2) but not yet committed (in ctx1).
97 97 subpaths = dict.fromkeys(ctx2.substate, ctx2)
98 98 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
99 99
100 100 missing = set()
101 101
102 102 for subpath in ctx2.substate:
103 103 if subpath not in ctx1.substate:
104 104 del subpaths[subpath]
105 105 missing.add(subpath)
106 106
107 107 for subpath, ctx in sorted(subpaths.iteritems()):
108 108 yield subpath, ctx.sub(subpath)
109 109
110 110 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
111 111 # status and diff will have an accurate result when it does
112 112 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
113 113 # against itself.
114 114 for subpath in missing:
115 115 yield subpath, ctx2.nullsub(subpath, ctx1)
116 116
117 117 def nochangesfound(ui, repo, excluded=None):
118 118 '''Report no changes for push/pull, excluded is None or a list of
119 119 nodes excluded from the push/pull.
120 120 '''
121 121 secretlist = []
122 122 if excluded:
123 123 for n in excluded:
124 124 if n not in repo:
125 125 # discovery should not have included the filtered revision,
126 126 # we have to explicitly exclude it until discovery is cleanup.
127 127 continue
128 128 ctx = repo[n]
129 129 if ctx.phase() >= phases.secret and not ctx.extinct():
130 130 secretlist.append(n)
131 131
132 132 if secretlist:
133 133 ui.status(_("no changes found (ignored %d secret changesets)\n")
134 134 % len(secretlist))
135 135 else:
136 136 ui.status(_("no changes found\n"))
137 137
138 138 def callcatch(ui, func):
139 139 """call func() with global exception handling
140 140
141 141 return func() if no exception happens. otherwise do some error handling
142 142 and return an exit code accordingly. does not handle all exceptions.
143 143 """
144 144 try:
145 return func()
145 try:
146 return func()
147 except: # re-raises
148 ui.traceback()
149 raise
146 150 # Global exception handling, alphabetically
147 151 # Mercurial-specific first, followed by built-in and library exceptions
148 152 except error.LockHeld as inst:
149 153 if inst.errno == errno.ETIMEDOUT:
150 154 reason = _('timed out waiting for lock held by %s') % inst.locker
151 155 else:
152 156 reason = _('lock held by %s') % inst.locker
153 157 ui.warn(_("abort: %s: %s\n") % (inst.desc or inst.filename, reason))
154 158 except error.LockUnavailable as inst:
155 159 ui.warn(_("abort: could not lock %s: %s\n") %
156 160 (inst.desc or inst.filename, inst.strerror))
157 161 except error.OutOfBandError as inst:
158 162 if inst.args:
159 163 msg = _("abort: remote error:\n")
160 164 else:
161 165 msg = _("abort: remote error\n")
162 166 ui.warn(msg)
163 167 if inst.args:
164 168 ui.warn(''.join(inst.args))
165 169 if inst.hint:
166 170 ui.warn('(%s)\n' % inst.hint)
167 171 except error.RepoError as inst:
168 172 ui.warn(_("abort: %s!\n") % inst)
169 173 if inst.hint:
170 174 ui.warn(_("(%s)\n") % inst.hint)
171 175 except error.ResponseError as inst:
172 176 ui.warn(_("abort: %s") % inst.args[0])
173 177 if not isinstance(inst.args[1], basestring):
174 178 ui.warn(" %r\n" % (inst.args[1],))
175 179 elif not inst.args[1]:
176 180 ui.warn(_(" empty string\n"))
177 181 else:
178 182 ui.warn("\n%r\n" % util.ellipsis(inst.args[1]))
179 183 except error.CensoredNodeError as inst:
180 184 ui.warn(_("abort: file censored %s!\n") % inst)
181 185 except error.RevlogError as inst:
182 186 ui.warn(_("abort: %s!\n") % inst)
183 187 except error.SignalInterrupt:
184 188 ui.warn(_("killed!\n"))
185 189 except error.InterventionRequired as inst:
186 190 ui.warn("%s\n" % inst)
187 191 if inst.hint:
188 192 ui.warn(_("(%s)\n") % inst.hint)
189 193 return 1
190 194 except error.Abort as inst:
191 195 ui.warn(_("abort: %s\n") % inst)
192 196 if inst.hint:
193 197 ui.warn(_("(%s)\n") % inst.hint)
194 198 except ImportError as inst:
195 199 ui.warn(_("abort: %s!\n") % inst)
196 200 m = str(inst).split()[-1]
197 201 if m in "mpatch bdiff".split():
198 202 ui.warn(_("(did you forget to compile extensions?)\n"))
199 203 elif m in "zlib".split():
200 204 ui.warn(_("(is your Python install correct?)\n"))
201 205 except IOError as inst:
202 206 if util.safehasattr(inst, "code"):
203 207 ui.warn(_("abort: %s\n") % inst)
204 208 elif util.safehasattr(inst, "reason"):
205 209 try: # usually it is in the form (errno, strerror)
206 210 reason = inst.reason.args[1]
207 211 except (AttributeError, IndexError):
208 212 # it might be anything, for example a string
209 213 reason = inst.reason
210 214 if isinstance(reason, unicode):
211 215 # SSLError of Python 2.7.9 contains a unicode
212 216 reason = reason.encode(encoding.encoding, 'replace')
213 217 ui.warn(_("abort: error: %s\n") % reason)
214 218 elif (util.safehasattr(inst, "args")
215 219 and inst.args and inst.args[0] == errno.EPIPE):
216 220 pass
217 221 elif getattr(inst, "strerror", None):
218 222 if getattr(inst, "filename", None):
219 223 ui.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename))
220 224 else:
221 225 ui.warn(_("abort: %s\n") % inst.strerror)
222 226 else:
223 227 raise
224 228 except OSError as inst:
225 229 if getattr(inst, "filename", None) is not None:
226 230 ui.warn(_("abort: %s: '%s'\n") % (inst.strerror, inst.filename))
227 231 else:
228 232 ui.warn(_("abort: %s\n") % inst.strerror)
229 233 except MemoryError:
230 234 ui.warn(_("abort: out of memory\n"))
231 235 except SystemExit as inst:
232 236 # Commands shouldn't sys.exit directly, but give a return code.
233 237 # Just in case catch this and and pass exit code to caller.
234 238 return inst.code
235 239 except socket.error as inst:
236 240 ui.warn(_("abort: %s\n") % inst.args[-1])
237 241
238 242 return -1
239 243
240 244 def checknewlabel(repo, lbl, kind):
241 245 # Do not use the "kind" parameter in ui output.
242 246 # It makes strings difficult to translate.
243 247 if lbl in ['tip', '.', 'null']:
244 248 raise error.Abort(_("the name '%s' is reserved") % lbl)
245 249 for c in (':', '\0', '\n', '\r'):
246 250 if c in lbl:
247 251 raise error.Abort(_("%r cannot be used in a name") % c)
248 252 try:
249 253 int(lbl)
250 254 raise error.Abort(_("cannot use an integer as a name"))
251 255 except ValueError:
252 256 pass
253 257
254 258 def checkfilename(f):
255 259 '''Check that the filename f is an acceptable filename for a tracked file'''
256 260 if '\r' in f or '\n' in f:
257 261 raise error.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
258 262
259 263 def checkportable(ui, f):
260 264 '''Check if filename f is portable and warn or abort depending on config'''
261 265 checkfilename(f)
262 266 abort, warn = checkportabilityalert(ui)
263 267 if abort or warn:
264 268 msg = util.checkwinfilename(f)
265 269 if msg:
266 270 msg = "%s: %r" % (msg, f)
267 271 if abort:
268 272 raise error.Abort(msg)
269 273 ui.warn(_("warning: %s\n") % msg)
270 274
271 275 def checkportabilityalert(ui):
272 276 '''check if the user's config requests nothing, a warning, or abort for
273 277 non-portable filenames'''
274 278 val = ui.config('ui', 'portablefilenames', 'warn')
275 279 lval = val.lower()
276 280 bval = util.parsebool(val)
277 281 abort = pycompat.osname == 'nt' or lval == 'abort'
278 282 warn = bval or lval == 'warn'
279 283 if bval is None and not (warn or abort or lval == 'ignore'):
280 284 raise error.ConfigError(
281 285 _("ui.portablefilenames value is invalid ('%s')") % val)
282 286 return abort, warn
283 287
284 288 class casecollisionauditor(object):
285 289 def __init__(self, ui, abort, dirstate):
286 290 self._ui = ui
287 291 self._abort = abort
288 292 allfiles = '\0'.join(dirstate._map)
289 293 self._loweredfiles = set(encoding.lower(allfiles).split('\0'))
290 294 self._dirstate = dirstate
291 295 # The purpose of _newfiles is so that we don't complain about
292 296 # case collisions if someone were to call this object with the
293 297 # same filename twice.
294 298 self._newfiles = set()
295 299
296 300 def __call__(self, f):
297 301 if f in self._newfiles:
298 302 return
299 303 fl = encoding.lower(f)
300 304 if fl in self._loweredfiles and f not in self._dirstate:
301 305 msg = _('possible case-folding collision for %s') % f
302 306 if self._abort:
303 307 raise error.Abort(msg)
304 308 self._ui.warn(_("warning: %s\n") % msg)
305 309 self._loweredfiles.add(fl)
306 310 self._newfiles.add(f)
307 311
308 312 def filteredhash(repo, maxrev):
309 313 """build hash of filtered revisions in the current repoview.
310 314
311 315 Multiple caches perform up-to-date validation by checking that the
312 316 tiprev and tipnode stored in the cache file match the current repository.
313 317 However, this is not sufficient for validating repoviews because the set
314 318 of revisions in the view may change without the repository tiprev and
315 319 tipnode changing.
316 320
317 321 This function hashes all the revs filtered from the view and returns
318 322 that SHA-1 digest.
319 323 """
320 324 cl = repo.changelog
321 325 if not cl.filteredrevs:
322 326 return None
323 327 key = None
324 328 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
325 329 if revs:
326 330 s = hashlib.sha1()
327 331 for rev in revs:
328 332 s.update('%d;' % rev)
329 333 key = s.digest()
330 334 return key
331 335
332 336 def _deprecated(old, new, func):
333 337 msg = ('class at mercurial.scmutil.%s moved to mercurial.vfs.%s'
334 338 % (old, new))
335 339 def wrapper(*args, **kwargs):
336 340 util.nouideprecwarn(msg, '4.2')
337 341 return func(*args, **kwargs)
338 342 return wrapper
339 343
340 344 # compatibility layer since all 'vfs' code moved to 'mercurial.vfs'
341 345 #
342 346 # This is hard to instal deprecation warning to this since we do not have
343 347 # access to a 'ui' object.
344 348 opener = _deprecated('opener', 'vfs', vfsmod.vfs)
345 349 vfs = _deprecated('vfs', 'vfs', vfsmod.vfs)
346 350 filteropener = _deprecated('filteropener', 'filtervfs', vfsmod.filtervfs)
347 351 filtervfs = _deprecated('filtervfs', 'filtervfs', vfsmod.filtervfs)
348 352 abstractvfs = _deprecated('abstractvfs', 'abstractvfs', vfsmod.abstractvfs)
349 353 readonlyvfs = _deprecated('readonlyvfs', 'readonlyvfs', vfsmod.readonlyvfs)
350 354 auditvfs = _deprecated('auditvfs', 'auditvfs', vfsmod.auditvfs)
351 355 checkambigatclosing = vfsmod.checkambigatclosing
352 356
353 357 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
354 358 '''yield every hg repository under path, always recursively.
355 359 The recurse flag will only control recursion into repo working dirs'''
356 360 def errhandler(err):
357 361 if err.filename == path:
358 362 raise err
359 363 samestat = getattr(os.path, 'samestat', None)
360 364 if followsym and samestat is not None:
361 365 def adddir(dirlst, dirname):
362 366 match = False
363 367 dirstat = os.stat(dirname)
364 368 for lstdirstat in dirlst:
365 369 if samestat(dirstat, lstdirstat):
366 370 match = True
367 371 break
368 372 if not match:
369 373 dirlst.append(dirstat)
370 374 return not match
371 375 else:
372 376 followsym = False
373 377
374 378 if (seen_dirs is None) and followsym:
375 379 seen_dirs = []
376 380 adddir(seen_dirs, path)
377 381 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
378 382 dirs.sort()
379 383 if '.hg' in dirs:
380 384 yield root # found a repository
381 385 qroot = os.path.join(root, '.hg', 'patches')
382 386 if os.path.isdir(os.path.join(qroot, '.hg')):
383 387 yield qroot # we have a patch queue repo here
384 388 if recurse:
385 389 # avoid recursing inside the .hg directory
386 390 dirs.remove('.hg')
387 391 else:
388 392 dirs[:] = [] # don't descend further
389 393 elif followsym:
390 394 newdirs = []
391 395 for d in dirs:
392 396 fname = os.path.join(root, d)
393 397 if adddir(seen_dirs, fname):
394 398 if os.path.islink(fname):
395 399 for hgname in walkrepos(fname, True, seen_dirs):
396 400 yield hgname
397 401 else:
398 402 newdirs.append(d)
399 403 dirs[:] = newdirs
400 404
401 405 def intrev(rev):
402 406 """Return integer for a given revision that can be used in comparison or
403 407 arithmetic operation"""
404 408 if rev is None:
405 409 return wdirrev
406 410 return rev
407 411
408 412 def revsingle(repo, revspec, default='.'):
409 413 if not revspec and revspec != 0:
410 414 return repo[default]
411 415
412 416 l = revrange(repo, [revspec])
413 417 if not l:
414 418 raise error.Abort(_('empty revision set'))
415 419 return repo[l.last()]
416 420
417 421 def _pairspec(revspec):
418 422 tree = revsetlang.parse(revspec)
419 423 return tree and tree[0] in ('range', 'rangepre', 'rangepost', 'rangeall')
420 424
421 425 def revpair(repo, revs):
422 426 if not revs:
423 427 return repo.dirstate.p1(), None
424 428
425 429 l = revrange(repo, revs)
426 430
427 431 if not l:
428 432 first = second = None
429 433 elif l.isascending():
430 434 first = l.min()
431 435 second = l.max()
432 436 elif l.isdescending():
433 437 first = l.max()
434 438 second = l.min()
435 439 else:
436 440 first = l.first()
437 441 second = l.last()
438 442
439 443 if first is None:
440 444 raise error.Abort(_('empty revision range'))
441 445 if (first == second and len(revs) >= 2
442 446 and not all(revrange(repo, [r]) for r in revs)):
443 447 raise error.Abort(_('empty revision on one side of range'))
444 448
445 449 # if top-level is range expression, the result must always be a pair
446 450 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
447 451 return repo.lookup(first), None
448 452
449 453 return repo.lookup(first), repo.lookup(second)
450 454
451 455 def revrange(repo, specs):
452 456 """Execute 1 to many revsets and return the union.
453 457
454 458 This is the preferred mechanism for executing revsets using user-specified
455 459 config options, such as revset aliases.
456 460
457 461 The revsets specified by ``specs`` will be executed via a chained ``OR``
458 462 expression. If ``specs`` is empty, an empty result is returned.
459 463
460 464 ``specs`` can contain integers, in which case they are assumed to be
461 465 revision numbers.
462 466
463 467 It is assumed the revsets are already formatted. If you have arguments
464 468 that need to be expanded in the revset, call ``revsetlang.formatspec()``
465 469 and pass the result as an element of ``specs``.
466 470
467 471 Specifying a single revset is allowed.
468 472
469 473 Returns a ``revset.abstractsmartset`` which is a list-like interface over
470 474 integer revisions.
471 475 """
472 476 allspecs = []
473 477 for spec in specs:
474 478 if isinstance(spec, int):
475 479 spec = revsetlang.formatspec('rev(%d)', spec)
476 480 allspecs.append(spec)
477 481 return repo.anyrevs(allspecs, user=True)
478 482
479 483 def meaningfulparents(repo, ctx):
480 484 """Return list of meaningful (or all if debug) parentrevs for rev.
481 485
482 486 For merges (two non-nullrev revisions) both parents are meaningful.
483 487 Otherwise the first parent revision is considered meaningful if it
484 488 is not the preceding revision.
485 489 """
486 490 parents = ctx.parents()
487 491 if len(parents) > 1:
488 492 return parents
489 493 if repo.ui.debugflag:
490 494 return [parents[0], repo['null']]
491 495 if parents[0].rev() >= intrev(ctx.rev()) - 1:
492 496 return []
493 497 return parents
494 498
495 499 def expandpats(pats):
496 500 '''Expand bare globs when running on windows.
497 501 On posix we assume it already has already been done by sh.'''
498 502 if not util.expandglobs:
499 503 return list(pats)
500 504 ret = []
501 505 for kindpat in pats:
502 506 kind, pat = matchmod._patsplit(kindpat, None)
503 507 if kind is None:
504 508 try:
505 509 globbed = glob.glob(pat)
506 510 except re.error:
507 511 globbed = [pat]
508 512 if globbed:
509 513 ret.extend(globbed)
510 514 continue
511 515 ret.append(kindpat)
512 516 return ret
513 517
514 518 def matchandpats(ctx, pats=(), opts=None, globbed=False, default='relpath',
515 519 badfn=None):
516 520 '''Return a matcher and the patterns that were used.
517 521 The matcher will warn about bad matches, unless an alternate badfn callback
518 522 is provided.'''
519 523 if pats == ("",):
520 524 pats = []
521 525 if opts is None:
522 526 opts = {}
523 527 if not globbed and default == 'relpath':
524 528 pats = expandpats(pats or [])
525 529
526 530 def bad(f, msg):
527 531 ctx.repo().ui.warn("%s: %s\n" % (m.rel(f), msg))
528 532
529 533 if badfn is None:
530 534 badfn = bad
531 535
532 536 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
533 537 default, listsubrepos=opts.get('subrepos'), badfn=badfn)
534 538
535 539 if m.always():
536 540 pats = []
537 541 return m, pats
538 542
539 543 def match(ctx, pats=(), opts=None, globbed=False, default='relpath',
540 544 badfn=None):
541 545 '''Return a matcher that will warn about bad matches.'''
542 546 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
543 547
544 548 def matchall(repo):
545 549 '''Return a matcher that will efficiently match everything.'''
546 550 return matchmod.always(repo.root, repo.getcwd())
547 551
548 552 def matchfiles(repo, files, badfn=None):
549 553 '''Return a matcher that will efficiently match exactly these files.'''
550 554 return matchmod.exact(repo.root, repo.getcwd(), files, badfn=badfn)
551 555
552 556 def origpath(ui, repo, filepath):
553 557 '''customize where .orig files are created
554 558
555 559 Fetch user defined path from config file: [ui] origbackuppath = <path>
556 560 Fall back to default (filepath) if not specified
557 561 '''
558 562 origbackuppath = ui.config('ui', 'origbackuppath', None)
559 563 if origbackuppath is None:
560 564 return filepath + ".orig"
561 565
562 566 filepathfromroot = os.path.relpath(filepath, start=repo.root)
563 567 fullorigpath = repo.wjoin(origbackuppath, filepathfromroot)
564 568
565 569 origbackupdir = repo.vfs.dirname(fullorigpath)
566 570 if not repo.vfs.exists(origbackupdir):
567 571 ui.note(_('creating directory: %s\n') % origbackupdir)
568 572 util.makedirs(origbackupdir)
569 573
570 574 return fullorigpath + ".orig"
571 575
572 576 def addremove(repo, matcher, prefix, opts=None, dry_run=None, similarity=None):
573 577 if opts is None:
574 578 opts = {}
575 579 m = matcher
576 580 if dry_run is None:
577 581 dry_run = opts.get('dry_run')
578 582 if similarity is None:
579 583 similarity = float(opts.get('similarity') or 0)
580 584
581 585 ret = 0
582 586 join = lambda f: os.path.join(prefix, f)
583 587
584 588 wctx = repo[None]
585 589 for subpath in sorted(wctx.substate):
586 590 submatch = matchmod.subdirmatcher(subpath, m)
587 591 if opts.get('subrepos') or m.exact(subpath) or any(submatch.files()):
588 592 sub = wctx.sub(subpath)
589 593 try:
590 594 if sub.addremove(submatch, prefix, opts, dry_run, similarity):
591 595 ret = 1
592 596 except error.LookupError:
593 597 repo.ui.status(_("skipping missing subrepository: %s\n")
594 598 % join(subpath))
595 599
596 600 rejected = []
597 601 def badfn(f, msg):
598 602 if f in m.files():
599 603 m.bad(f, msg)
600 604 rejected.append(f)
601 605
602 606 badmatch = matchmod.badmatch(m, badfn)
603 607 added, unknown, deleted, removed, forgotten = _interestingfiles(repo,
604 608 badmatch)
605 609
606 610 unknownset = set(unknown + forgotten)
607 611 toprint = unknownset.copy()
608 612 toprint.update(deleted)
609 613 for abs in sorted(toprint):
610 614 if repo.ui.verbose or not m.exact(abs):
611 615 if abs in unknownset:
612 616 status = _('adding %s\n') % m.uipath(abs)
613 617 else:
614 618 status = _('removing %s\n') % m.uipath(abs)
615 619 repo.ui.status(status)
616 620
617 621 renames = _findrenames(repo, m, added + unknown, removed + deleted,
618 622 similarity)
619 623
620 624 if not dry_run:
621 625 _markchanges(repo, unknown + forgotten, deleted, renames)
622 626
623 627 for f in rejected:
624 628 if f in m.files():
625 629 return 1
626 630 return ret
627 631
628 632 def marktouched(repo, files, similarity=0.0):
629 633 '''Assert that files have somehow been operated upon. files are relative to
630 634 the repo root.'''
631 635 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
632 636 rejected = []
633 637
634 638 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
635 639
636 640 if repo.ui.verbose:
637 641 unknownset = set(unknown + forgotten)
638 642 toprint = unknownset.copy()
639 643 toprint.update(deleted)
640 644 for abs in sorted(toprint):
641 645 if abs in unknownset:
642 646 status = _('adding %s\n') % abs
643 647 else:
644 648 status = _('removing %s\n') % abs
645 649 repo.ui.status(status)
646 650
647 651 renames = _findrenames(repo, m, added + unknown, removed + deleted,
648 652 similarity)
649 653
650 654 _markchanges(repo, unknown + forgotten, deleted, renames)
651 655
652 656 for f in rejected:
653 657 if f in m.files():
654 658 return 1
655 659 return 0
656 660
657 661 def _interestingfiles(repo, matcher):
658 662 '''Walk dirstate with matcher, looking for files that addremove would care
659 663 about.
660 664
661 665 This is different from dirstate.status because it doesn't care about
662 666 whether files are modified or clean.'''
663 667 added, unknown, deleted, removed, forgotten = [], [], [], [], []
664 668 audit_path = pathutil.pathauditor(repo.root)
665 669
666 670 ctx = repo[None]
667 671 dirstate = repo.dirstate
668 672 walkresults = dirstate.walk(matcher, sorted(ctx.substate), True, False,
669 673 full=False)
670 674 for abs, st in walkresults.iteritems():
671 675 dstate = dirstate[abs]
672 676 if dstate == '?' and audit_path.check(abs):
673 677 unknown.append(abs)
674 678 elif dstate != 'r' and not st:
675 679 deleted.append(abs)
676 680 elif dstate == 'r' and st:
677 681 forgotten.append(abs)
678 682 # for finding renames
679 683 elif dstate == 'r' and not st:
680 684 removed.append(abs)
681 685 elif dstate == 'a':
682 686 added.append(abs)
683 687
684 688 return added, unknown, deleted, removed, forgotten
685 689
686 690 def _findrenames(repo, matcher, added, removed, similarity):
687 691 '''Find renames from removed files to added ones.'''
688 692 renames = {}
689 693 if similarity > 0:
690 694 for old, new, score in similar.findrenames(repo, added, removed,
691 695 similarity):
692 696 if (repo.ui.verbose or not matcher.exact(old)
693 697 or not matcher.exact(new)):
694 698 repo.ui.status(_('recording removal of %s as rename to %s '
695 699 '(%d%% similar)\n') %
696 700 (matcher.rel(old), matcher.rel(new),
697 701 score * 100))
698 702 renames[new] = old
699 703 return renames
700 704
701 705 def _markchanges(repo, unknown, deleted, renames):
702 706 '''Marks the files in unknown as added, the files in deleted as removed,
703 707 and the files in renames as copied.'''
704 708 wctx = repo[None]
705 709 with repo.wlock():
706 710 wctx.forget(deleted)
707 711 wctx.add(unknown)
708 712 for new, old in renames.iteritems():
709 713 wctx.copy(old, new)
710 714
711 715 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
712 716 """Update the dirstate to reflect the intent of copying src to dst. For
713 717 different reasons it might not end with dst being marked as copied from src.
714 718 """
715 719 origsrc = repo.dirstate.copied(src) or src
716 720 if dst == origsrc: # copying back a copy?
717 721 if repo.dirstate[dst] not in 'mn' and not dryrun:
718 722 repo.dirstate.normallookup(dst)
719 723 else:
720 724 if repo.dirstate[origsrc] == 'a' and origsrc == src:
721 725 if not ui.quiet:
722 726 ui.warn(_("%s has not been committed yet, so no copy "
723 727 "data will be stored for %s.\n")
724 728 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
725 729 if repo.dirstate[dst] in '?r' and not dryrun:
726 730 wctx.add([dst])
727 731 elif not dryrun:
728 732 wctx.copy(origsrc, dst)
729 733
730 734 def readrequires(opener, supported):
731 735 '''Reads and parses .hg/requires and checks if all entries found
732 736 are in the list of supported features.'''
733 737 requirements = set(opener.read("requires").splitlines())
734 738 missings = []
735 739 for r in requirements:
736 740 if r not in supported:
737 741 if not r or not r[0].isalnum():
738 742 raise error.RequirementError(_(".hg/requires file is corrupt"))
739 743 missings.append(r)
740 744 missings.sort()
741 745 if missings:
742 746 raise error.RequirementError(
743 747 _("repository requires features unknown to this Mercurial: %s")
744 748 % " ".join(missings),
745 749 hint=_("see https://mercurial-scm.org/wiki/MissingRequirement"
746 750 " for more information"))
747 751 return requirements
748 752
749 753 def writerequires(opener, requirements):
750 754 with opener('requires', 'w') as fp:
751 755 for r in sorted(requirements):
752 756 fp.write("%s\n" % r)
753 757
754 758 class filecachesubentry(object):
755 759 def __init__(self, path, stat):
756 760 self.path = path
757 761 self.cachestat = None
758 762 self._cacheable = None
759 763
760 764 if stat:
761 765 self.cachestat = filecachesubentry.stat(self.path)
762 766
763 767 if self.cachestat:
764 768 self._cacheable = self.cachestat.cacheable()
765 769 else:
766 770 # None means we don't know yet
767 771 self._cacheable = None
768 772
769 773 def refresh(self):
770 774 if self.cacheable():
771 775 self.cachestat = filecachesubentry.stat(self.path)
772 776
773 777 def cacheable(self):
774 778 if self._cacheable is not None:
775 779 return self._cacheable
776 780
777 781 # we don't know yet, assume it is for now
778 782 return True
779 783
780 784 def changed(self):
781 785 # no point in going further if we can't cache it
782 786 if not self.cacheable():
783 787 return True
784 788
785 789 newstat = filecachesubentry.stat(self.path)
786 790
787 791 # we may not know if it's cacheable yet, check again now
788 792 if newstat and self._cacheable is None:
789 793 self._cacheable = newstat.cacheable()
790 794
791 795 # check again
792 796 if not self._cacheable:
793 797 return True
794 798
795 799 if self.cachestat != newstat:
796 800 self.cachestat = newstat
797 801 return True
798 802 else:
799 803 return False
800 804
801 805 @staticmethod
802 806 def stat(path):
803 807 try:
804 808 return util.cachestat(path)
805 809 except OSError as e:
806 810 if e.errno != errno.ENOENT:
807 811 raise
808 812
809 813 class filecacheentry(object):
810 814 def __init__(self, paths, stat=True):
811 815 self._entries = []
812 816 for path in paths:
813 817 self._entries.append(filecachesubentry(path, stat))
814 818
815 819 def changed(self):
816 820 '''true if any entry has changed'''
817 821 for entry in self._entries:
818 822 if entry.changed():
819 823 return True
820 824 return False
821 825
822 826 def refresh(self):
823 827 for entry in self._entries:
824 828 entry.refresh()
825 829
826 830 class filecache(object):
827 831 '''A property like decorator that tracks files under .hg/ for updates.
828 832
829 833 Records stat info when called in _filecache.
830 834
831 835 On subsequent calls, compares old stat info with new info, and recreates the
832 836 object when any of the files changes, updating the new stat info in
833 837 _filecache.
834 838
835 839 Mercurial either atomic renames or appends for files under .hg,
836 840 so to ensure the cache is reliable we need the filesystem to be able
837 841 to tell us if a file has been replaced. If it can't, we fallback to
838 842 recreating the object on every call (essentially the same behavior as
839 843 propertycache).
840 844
841 845 '''
842 846 def __init__(self, *paths):
843 847 self.paths = paths
844 848
845 849 def join(self, obj, fname):
846 850 """Used to compute the runtime path of a cached file.
847 851
848 852 Users should subclass filecache and provide their own version of this
849 853 function to call the appropriate join function on 'obj' (an instance
850 854 of the class that its member function was decorated).
851 855 """
852 856 raise NotImplementedError
853 857
854 858 def __call__(self, func):
855 859 self.func = func
856 860 self.name = func.__name__.encode('ascii')
857 861 return self
858 862
859 863 def __get__(self, obj, type=None):
860 864 # if accessed on the class, return the descriptor itself.
861 865 if obj is None:
862 866 return self
863 867 # do we need to check if the file changed?
864 868 if self.name in obj.__dict__:
865 869 assert self.name in obj._filecache, self.name
866 870 return obj.__dict__[self.name]
867 871
868 872 entry = obj._filecache.get(self.name)
869 873
870 874 if entry:
871 875 if entry.changed():
872 876 entry.obj = self.func(obj)
873 877 else:
874 878 paths = [self.join(obj, path) for path in self.paths]
875 879
876 880 # We stat -before- creating the object so our cache doesn't lie if
877 881 # a writer modified between the time we read and stat
878 882 entry = filecacheentry(paths, True)
879 883 entry.obj = self.func(obj)
880 884
881 885 obj._filecache[self.name] = entry
882 886
883 887 obj.__dict__[self.name] = entry.obj
884 888 return entry.obj
885 889
886 890 def __set__(self, obj, value):
887 891 if self.name not in obj._filecache:
888 892 # we add an entry for the missing value because X in __dict__
889 893 # implies X in _filecache
890 894 paths = [self.join(obj, path) for path in self.paths]
891 895 ce = filecacheentry(paths, False)
892 896 obj._filecache[self.name] = ce
893 897 else:
894 898 ce = obj._filecache[self.name]
895 899
896 900 ce.obj = value # update cached copy
897 901 obj.__dict__[self.name] = value # update copy returned by obj.x
898 902
899 903 def __delete__(self, obj):
900 904 try:
901 905 del obj.__dict__[self.name]
902 906 except KeyError:
903 907 raise AttributeError(self.name)
904 908
905 909 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
906 910 if lock is None:
907 911 raise error.LockInheritanceContractViolation(
908 912 'lock can only be inherited while held')
909 913 if environ is None:
910 914 environ = {}
911 915 with lock.inherit() as locker:
912 916 environ[envvar] = locker
913 917 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
914 918
915 919 def wlocksub(repo, cmd, *args, **kwargs):
916 920 """run cmd as a subprocess that allows inheriting repo's wlock
917 921
918 922 This can only be called while the wlock is held. This takes all the
919 923 arguments that ui.system does, and returns the exit code of the
920 924 subprocess."""
921 925 return _locksub(repo, repo.currentwlock(), 'HG_WLOCK_LOCKER', cmd, *args,
922 926 **kwargs)
923 927
924 928 def gdinitconfig(ui):
925 929 """helper function to know if a repo should be created as general delta
926 930 """
927 931 # experimental config: format.generaldelta
928 932 return (ui.configbool('format', 'generaldelta', False)
929 933 or ui.configbool('format', 'usegeneraldelta', True))
930 934
931 935 def gddeltaconfig(ui):
932 936 """helper function to know if incoming delta should be optimised
933 937 """
934 938 # experimental config: format.generaldelta
935 939 return ui.configbool('format', 'generaldelta', False)
936 940
937 941 class simplekeyvaluefile(object):
938 942 """A simple file with key=value lines
939 943
940 944 Keys must be alphanumerics and start with a letter, values must not
941 945 contain '\n' characters"""
942 946
943 947 def __init__(self, vfs, path, keys=None):
944 948 self.vfs = vfs
945 949 self.path = path
946 950
947 951 def read(self):
948 952 lines = self.vfs.readlines(self.path)
949 953 try:
950 954 d = dict(line[:-1].split('=', 1) for line in lines if line)
951 955 except ValueError as e:
952 956 raise error.CorruptedState(str(e))
953 957 return d
954 958
955 959 def write(self, data):
956 960 """Write key=>value mapping to a file
957 961 data is a dict. Keys must be alphanumerical and start with a letter.
958 962 Values must not contain newline characters."""
959 963 lines = []
960 964 for k, v in data.items():
961 965 if not k[0].isalpha():
962 966 e = "keys must start with a letter in a key-value file"
963 967 raise error.ProgrammingError(e)
964 968 if not k.isalnum():
965 969 e = "invalid key name in a simple key-value file"
966 970 raise error.ProgrammingError(e)
967 971 if '\n' in v:
968 972 e = "invalid value in a simple key-value file"
969 973 raise error.ProgrammingError(e)
970 974 lines.append("%s=%s\n" % (k, v))
971 975 with self.vfs(self.path, mode='wb', atomictemp=True) as fp:
972 976 fp.write(''.join(lines))
@@ -1,54 +1,77 b''
1 1 Test UI worker interaction
2 2
3 3 $ cat > t.py <<EOF
4 4 > from __future__ import absolute_import, print_function
5 5 > from mercurial import (
6 6 > cmdutil,
7 > error,
7 8 > ui as uimod,
8 9 > worker,
9 10 > )
11 > def abort(ui, args):
12 > if args[0] == 0:
13 > # by first worker for test stability
14 > raise error.Abort('known exception')
15 > return runme(ui, [])
10 16 > def runme(ui, args):
11 17 > for arg in args:
12 18 > ui.status('run\n')
13 19 > yield 1, arg
20 > functable = {
21 > 'abort': abort,
22 > 'runme': runme,
23 > }
14 24 > cmdtable = {}
15 25 > command = cmdutil.command(cmdtable)
16 > @command('test', [], 'hg test [COST]')
17 > def t(ui, repo, cost=1.0):
26 > @command('test', [], 'hg test [COST] [FUNC]')
27 > def t(ui, repo, cost=1.0, func='runme'):
18 28 > cost = float(cost)
29 > func = functable[func]
19 30 > ui.status('start\n')
20 > runs = worker.worker(ui, cost, runme, (ui,), range(8))
31 > runs = worker.worker(ui, cost, func, (ui,), range(8))
21 32 > for n, i in runs:
22 33 > pass
23 34 > ui.status('done\n')
24 35 > EOF
25 36 $ abspath=`pwd`/t.py
26 37 $ hg init
27 38
28 39 Run tests with worker enable by forcing a heigh cost
29 40
30 41 $ hg --config "extensions.t=$abspath" test 100000.0
31 42 start
32 43 run
33 44 run
34 45 run
35 46 run
36 47 run
37 48 run
38 49 run
39 50 run
40 51 done
41 52
42 53 Run tests without worker by forcing a low cost
43 54
44 55 $ hg --config "extensions.t=$abspath" test 0.0000001
45 56 start
46 57 run
47 58 run
48 59 run
49 60 run
50 61 run
51 62 run
52 63 run
53 64 run
54 65 done
66
67 Known exception should be caught, but printed if --traceback is enabled
68
69 $ hg --config "extensions.t=$abspath" --config 'worker.numcpus=2' \
70 > test 100000.0 abort
71 start
72 abort: known exception
73 done
74
75 $ hg --config "extensions.t=$abspath" --config 'worker.numcpus=2' \
76 > test 100000.0 abort --traceback 2>&1 | grep '^Traceback'
77 Traceback (most recent call last):
General Comments 0
You need to be logged in to leave comments. Login now