##// END OF EJS Templates
dispatch: load shared source repository config in share-safe mode...
Pulkit Goyal -
r46367:fd1de908 default
parent child Browse files
Show More
@@ -1,1335 +1,1363 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 io
14 14 import os
15 15 import pdb
16 16 import re
17 17 import signal
18 18 import sys
19 19 import traceback
20 20
21 21
22 22 from .i18n import _
23 23 from .pycompat import getattr
24 24
25 25 from hgdemandimport import tracing
26 26
27 27 from . import (
28 28 cmdutil,
29 29 color,
30 30 commands,
31 31 demandimport,
32 32 encoding,
33 33 error,
34 34 extensions,
35 35 fancyopts,
36 36 help,
37 37 hg,
38 38 hook,
39 localrepo,
39 40 profiling,
40 41 pycompat,
41 42 rcutil,
42 43 registrar,
44 requirements as requirementsmod,
43 45 scmutil,
44 46 ui as uimod,
45 47 util,
48 vfs,
46 49 )
47 50
48 51 from .utils import (
49 52 procutil,
50 53 stringutil,
51 54 )
52 55
53 56
54 57 class request(object):
55 58 def __init__(
56 59 self,
57 60 args,
58 61 ui=None,
59 62 repo=None,
60 63 fin=None,
61 64 fout=None,
62 65 ferr=None,
63 66 fmsg=None,
64 67 prereposetups=None,
65 68 ):
66 69 self.args = args
67 70 self.ui = ui
68 71 self.repo = repo
69 72
70 73 # input/output/error streams
71 74 self.fin = fin
72 75 self.fout = fout
73 76 self.ferr = ferr
74 77 # separate stream for status/error messages
75 78 self.fmsg = fmsg
76 79
77 80 # remember options pre-parsed by _earlyparseopts()
78 81 self.earlyoptions = {}
79 82
80 83 # reposetups which run before extensions, useful for chg to pre-fill
81 84 # low-level repo state (for example, changelog) before extensions.
82 85 self.prereposetups = prereposetups or []
83 86
84 87 # store the parsed and canonical command
85 88 self.canonical_command = None
86 89
87 90 def _runexithandlers(self):
88 91 exc = None
89 92 handlers = self.ui._exithandlers
90 93 try:
91 94 while handlers:
92 95 func, args, kwargs = handlers.pop()
93 96 try:
94 97 func(*args, **kwargs)
95 98 except: # re-raises below
96 99 if exc is None:
97 100 exc = sys.exc_info()[1]
98 101 self.ui.warnnoi18n(b'error in exit handlers:\n')
99 102 self.ui.traceback(force=True)
100 103 finally:
101 104 if exc is not None:
102 105 raise exc
103 106
104 107
105 108 def run():
106 109 """run the command in sys.argv"""
107 110 try:
108 111 initstdio()
109 112 with tracing.log('parse args into request'):
110 113 req = request(pycompat.sysargv[1:])
111 114 err = None
112 115 try:
113 116 status = dispatch(req)
114 117 except error.StdioError as e:
115 118 err = e
116 119 status = -1
117 120
118 121 # In all cases we try to flush stdio streams.
119 122 if util.safehasattr(req.ui, b'fout'):
120 123 assert req.ui is not None # help pytype
121 124 assert req.ui.fout is not None # help pytype
122 125 try:
123 126 req.ui.fout.flush()
124 127 except IOError as e:
125 128 err = e
126 129 status = -1
127 130
128 131 if util.safehasattr(req.ui, b'ferr'):
129 132 assert req.ui is not None # help pytype
130 133 assert req.ui.ferr is not None # help pytype
131 134 try:
132 135 if err is not None and err.errno != errno.EPIPE:
133 136 req.ui.ferr.write(
134 137 b'abort: %s\n' % encoding.strtolocal(err.strerror)
135 138 )
136 139 req.ui.ferr.flush()
137 140 # There's not much we can do about an I/O error here. So (possibly)
138 141 # change the status code and move on.
139 142 except IOError:
140 143 status = -1
141 144
142 145 _silencestdio()
143 146 except KeyboardInterrupt:
144 147 # Catch early/late KeyboardInterrupt as last ditch. Here nothing will
145 148 # be printed to console to avoid another IOError/KeyboardInterrupt.
146 149 status = -1
147 150 sys.exit(status & 255)
148 151
149 152
150 153 if pycompat.ispy3:
151 154
152 155 def initstdio():
153 156 # stdio streams on Python 3 are io.TextIOWrapper instances proxying another
154 157 # buffer. These streams will normalize \n to \r\n by default. Mercurial's
155 158 # preferred mechanism for writing output (ui.write()) uses io.BufferedWriter
156 159 # instances, which write to the underlying stdio file descriptor in binary
157 160 # mode. ui.write() uses \n for line endings and no line ending normalization
158 161 # is attempted through this interface. This "just works," even if the system
159 162 # preferred line ending is not \n.
160 163 #
161 164 # But some parts of Mercurial (e.g. hooks) can still send data to sys.stdout
162 165 # and sys.stderr. They will inherit the line ending normalization settings,
163 166 # potentially causing e.g. \r\n to be emitted. Since emitting \n should
164 167 # "just work," here we change the sys.* streams to disable line ending
165 168 # normalization, ensuring compatibility with our ui type.
166 169
167 170 # write_through is new in Python 3.7.
168 171 kwargs = {
169 172 "newline": "\n",
170 173 "line_buffering": sys.stdout.line_buffering,
171 174 }
172 175 if util.safehasattr(sys.stdout, "write_through"):
173 176 kwargs["write_through"] = sys.stdout.write_through
174 177 sys.stdout = io.TextIOWrapper(
175 178 sys.stdout.buffer, sys.stdout.encoding, sys.stdout.errors, **kwargs
176 179 )
177 180
178 181 kwargs = {
179 182 "newline": "\n",
180 183 "line_buffering": sys.stderr.line_buffering,
181 184 }
182 185 if util.safehasattr(sys.stderr, "write_through"):
183 186 kwargs["write_through"] = sys.stderr.write_through
184 187 sys.stderr = io.TextIOWrapper(
185 188 sys.stderr.buffer, sys.stderr.encoding, sys.stderr.errors, **kwargs
186 189 )
187 190
188 191 # No write_through on read-only stream.
189 192 sys.stdin = io.TextIOWrapper(
190 193 sys.stdin.buffer,
191 194 sys.stdin.encoding,
192 195 sys.stdin.errors,
193 196 # None is universal newlines mode.
194 197 newline=None,
195 198 line_buffering=sys.stdin.line_buffering,
196 199 )
197 200
198 201 def _silencestdio():
199 202 for fp in (sys.stdout, sys.stderr):
200 203 # Check if the file is okay
201 204 try:
202 205 fp.flush()
203 206 continue
204 207 except IOError:
205 208 pass
206 209 # Otherwise mark it as closed to silence "Exception ignored in"
207 210 # message emitted by the interpreter finalizer. Be careful to
208 211 # not close procutil.stdout, which may be a fdopen-ed file object
209 212 # and its close() actually closes the underlying file descriptor.
210 213 try:
211 214 fp.close()
212 215 except IOError:
213 216 pass
214 217
215 218
216 219 else:
217 220
218 221 def initstdio():
219 222 for fp in (sys.stdin, sys.stdout, sys.stderr):
220 223 procutil.setbinary(fp)
221 224
222 225 def _silencestdio():
223 226 pass
224 227
225 228
226 229 def _getsimilar(symbols, value):
227 230 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
228 231 # The cutoff for similarity here is pretty arbitrary. It should
229 232 # probably be investigated and tweaked.
230 233 return [s for s in symbols if sim(s) > 0.6]
231 234
232 235
233 236 def _reportsimilar(write, similar):
234 237 if len(similar) == 1:
235 238 write(_(b"(did you mean %s?)\n") % similar[0])
236 239 elif similar:
237 240 ss = b", ".join(sorted(similar))
238 241 write(_(b"(did you mean one of %s?)\n") % ss)
239 242
240 243
241 244 def _formatparse(write, inst):
242 245 similar = []
243 246 if isinstance(inst, error.UnknownIdentifier):
244 247 # make sure to check fileset first, as revset can invoke fileset
245 248 similar = _getsimilar(inst.symbols, inst.function)
246 249 if inst.location is not None:
247 250 write(
248 251 _(b"hg: parse error at %s: %s\n")
249 252 % (pycompat.bytestr(inst.location), inst.message)
250 253 )
251 254 else:
252 255 write(_(b"hg: parse error: %s\n") % inst.message)
253 256 _reportsimilar(write, similar)
254 257 if inst.hint:
255 258 write(_(b"(%s)\n") % inst.hint)
256 259
257 260
258 261 def _formatargs(args):
259 262 return b' '.join(procutil.shellquote(a) for a in args)
260 263
261 264
262 265 def dispatch(req):
263 266 """run the command specified in req.args; returns an integer status code"""
264 267 with tracing.log('dispatch.dispatch'):
265 268 if req.ferr:
266 269 ferr = req.ferr
267 270 elif req.ui:
268 271 ferr = req.ui.ferr
269 272 else:
270 273 ferr = procutil.stderr
271 274
272 275 try:
273 276 if not req.ui:
274 277 req.ui = uimod.ui.load()
275 278 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
276 279 if req.earlyoptions[b'traceback']:
277 280 req.ui.setconfig(b'ui', b'traceback', b'on', b'--traceback')
278 281
279 282 # set ui streams from the request
280 283 if req.fin:
281 284 req.ui.fin = req.fin
282 285 if req.fout:
283 286 req.ui.fout = req.fout
284 287 if req.ferr:
285 288 req.ui.ferr = req.ferr
286 289 if req.fmsg:
287 290 req.ui.fmsg = req.fmsg
288 291 except error.Abort as inst:
289 292 ferr.write(_(b"abort: %s\n") % inst.message)
290 293 if inst.hint:
291 294 ferr.write(_(b"(%s)\n") % inst.hint)
292 295 return -1
293 296 except error.ParseError as inst:
294 297 _formatparse(ferr.write, inst)
295 298 return -1
296 299
297 300 msg = _formatargs(req.args)
298 301 starttime = util.timer()
299 302 ret = 1 # default of Python exit code on unhandled exception
300 303 try:
301 304 ret = _runcatch(req) or 0
302 305 except error.ProgrammingError as inst:
303 306 req.ui.error(_(b'** ProgrammingError: %s\n') % inst)
304 307 if inst.hint:
305 308 req.ui.error(_(b'** (%s)\n') % inst.hint)
306 309 raise
307 310 except KeyboardInterrupt as inst:
308 311 try:
309 312 if isinstance(inst, error.SignalInterrupt):
310 313 msg = _(b"killed!\n")
311 314 else:
312 315 msg = _(b"interrupted!\n")
313 316 req.ui.error(msg)
314 317 except error.SignalInterrupt:
315 318 # maybe pager would quit without consuming all the output, and
316 319 # SIGPIPE was raised. we cannot print anything in this case.
317 320 pass
318 321 except IOError as inst:
319 322 if inst.errno != errno.EPIPE:
320 323 raise
321 324 ret = -1
322 325 finally:
323 326 duration = util.timer() - starttime
324 327 req.ui.flush() # record blocked times
325 328 if req.ui.logblockedtimes:
326 329 req.ui._blockedtimes[b'command_duration'] = duration * 1000
327 330 req.ui.log(
328 331 b'uiblocked',
329 332 b'ui blocked ms\n',
330 333 **pycompat.strkwargs(req.ui._blockedtimes)
331 334 )
332 335 return_code = ret & 255
333 336 req.ui.log(
334 337 b"commandfinish",
335 338 b"%s exited %d after %0.2f seconds\n",
336 339 msg,
337 340 return_code,
338 341 duration,
339 342 return_code=return_code,
340 343 duration=duration,
341 344 canonical_command=req.canonical_command,
342 345 )
343 346 try:
344 347 req._runexithandlers()
345 348 except: # exiting, so no re-raises
346 349 ret = ret or -1
347 350 # do flush again since ui.log() and exit handlers may write to ui
348 351 req.ui.flush()
349 352 return ret
350 353
351 354
352 355 def _runcatch(req):
353 356 with tracing.log('dispatch._runcatch'):
354 357
355 358 def catchterm(*args):
356 359 raise error.SignalInterrupt
357 360
358 361 ui = req.ui
359 362 try:
360 363 for name in b'SIGBREAK', b'SIGHUP', b'SIGTERM':
361 364 num = getattr(signal, name, None)
362 365 if num:
363 366 signal.signal(num, catchterm)
364 367 except ValueError:
365 368 pass # happens if called in a thread
366 369
367 370 def _runcatchfunc():
368 371 realcmd = None
369 372 try:
370 373 cmdargs = fancyopts.fancyopts(
371 374 req.args[:], commands.globalopts, {}
372 375 )
373 376 cmd = cmdargs[0]
374 377 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
375 378 realcmd = aliases[0]
376 379 except (
377 380 error.UnknownCommand,
378 381 error.AmbiguousCommand,
379 382 IndexError,
380 383 getopt.GetoptError,
381 384 ):
382 385 # Don't handle this here. We know the command is
383 386 # invalid, but all we're worried about for now is that
384 387 # it's not a command that server operators expect to
385 388 # be safe to offer to users in a sandbox.
386 389 pass
387 390 if realcmd == b'serve' and b'--stdio' in cmdargs:
388 391 # We want to constrain 'hg serve --stdio' instances pretty
389 392 # closely, as many shared-ssh access tools want to grant
390 393 # access to run *only* 'hg -R $repo serve --stdio'. We
391 394 # restrict to exactly that set of arguments, and prohibit
392 395 # any repo name that starts with '--' to prevent
393 396 # shenanigans wherein a user does something like pass
394 397 # --debugger or --config=ui.debugger=1 as a repo
395 398 # name. This used to actually run the debugger.
396 399 if (
397 400 len(req.args) != 4
398 401 or req.args[0] != b'-R'
399 402 or req.args[1].startswith(b'--')
400 403 or req.args[2] != b'serve'
401 404 or req.args[3] != b'--stdio'
402 405 ):
403 406 raise error.Abort(
404 407 _(b'potentially unsafe serve --stdio invocation: %s')
405 408 % (stringutil.pprint(req.args),)
406 409 )
407 410
408 411 try:
409 412 debugger = b'pdb'
410 413 debugtrace = {b'pdb': pdb.set_trace}
411 414 debugmortem = {b'pdb': pdb.post_mortem}
412 415
413 416 # read --config before doing anything else
414 417 # (e.g. to change trust settings for reading .hg/hgrc)
415 418 cfgs = _parseconfig(req.ui, req.earlyoptions[b'config'])
416 419
417 420 if req.repo:
418 421 # copy configs that were passed on the cmdline (--config) to
419 422 # the repo ui
420 423 for sec, name, val in cfgs:
421 424 req.repo.ui.setconfig(
422 425 sec, name, val, source=b'--config'
423 426 )
424 427
425 428 # developer config: ui.debugger
426 429 debugger = ui.config(b"ui", b"debugger")
427 430 debugmod = pdb
428 431 if not debugger or ui.plain():
429 432 # if we are in HGPLAIN mode, then disable custom debugging
430 433 debugger = b'pdb'
431 434 elif req.earlyoptions[b'debugger']:
432 435 # This import can be slow for fancy debuggers, so only
433 436 # do it when absolutely necessary, i.e. when actual
434 437 # debugging has been requested
435 438 with demandimport.deactivated():
436 439 try:
437 440 debugmod = __import__(debugger)
438 441 except ImportError:
439 442 pass # Leave debugmod = pdb
440 443
441 444 debugtrace[debugger] = debugmod.set_trace
442 445 debugmortem[debugger] = debugmod.post_mortem
443 446
444 447 # enter the debugger before command execution
445 448 if req.earlyoptions[b'debugger']:
446 449 ui.warn(
447 450 _(
448 451 b"entering debugger - "
449 452 b"type c to continue starting hg or h for help\n"
450 453 )
451 454 )
452 455
453 456 if (
454 457 debugger != b'pdb'
455 458 and debugtrace[debugger] == debugtrace[b'pdb']
456 459 ):
457 460 ui.warn(
458 461 _(
459 462 b"%s debugger specified "
460 463 b"but its module was not found\n"
461 464 )
462 465 % debugger
463 466 )
464 467 with demandimport.deactivated():
465 468 debugtrace[debugger]()
466 469 try:
467 470 return _dispatch(req)
468 471 finally:
469 472 ui.flush()
470 473 except: # re-raises
471 474 # enter the debugger when we hit an exception
472 475 if req.earlyoptions[b'debugger']:
473 476 traceback.print_exc()
474 477 debugmortem[debugger](sys.exc_info()[2])
475 478 raise
476 479
477 480 return _callcatch(ui, _runcatchfunc)
478 481
479 482
480 483 def _callcatch(ui, func):
481 484 """like scmutil.callcatch but handles more high-level exceptions about
482 485 config parsing and commands. besides, use handlecommandexception to handle
483 486 uncaught exceptions.
484 487 """
485 488 try:
486 489 return scmutil.callcatch(ui, func)
487 490 except error.AmbiguousCommand as inst:
488 491 ui.warn(
489 492 _(b"hg: command '%s' is ambiguous:\n %s\n")
490 493 % (inst.prefix, b" ".join(inst.matches))
491 494 )
492 495 except error.CommandError as inst:
493 496 if inst.command:
494 497 ui.pager(b'help')
495 498 msgbytes = pycompat.bytestr(inst.message)
496 499 ui.warn(_(b"hg %s: %s\n") % (inst.command, msgbytes))
497 500 commands.help_(ui, inst.command, full=False, command=True)
498 501 else:
499 502 ui.warn(_(b"hg: %s\n") % inst.message)
500 503 ui.warn(_(b"(use 'hg help -v' for a list of global options)\n"))
501 504 except error.ParseError as inst:
502 505 _formatparse(ui.warn, inst)
503 506 return -1
504 507 except error.UnknownCommand as inst:
505 508 nocmdmsg = _(b"hg: unknown command '%s'\n") % inst.command
506 509 try:
507 510 # check if the command is in a disabled extension
508 511 # (but don't check for extensions themselves)
509 512 formatted = help.formattedhelp(
510 513 ui, commands, inst.command, unknowncmd=True
511 514 )
512 515 ui.warn(nocmdmsg)
513 516 ui.write(formatted)
514 517 except (error.UnknownCommand, error.Abort):
515 518 suggested = False
516 519 if inst.all_commands:
517 520 sim = _getsimilar(inst.all_commands, inst.command)
518 521 if sim:
519 522 ui.warn(nocmdmsg)
520 523 _reportsimilar(ui.warn, sim)
521 524 suggested = True
522 525 if not suggested:
523 526 ui.warn(nocmdmsg)
524 527 ui.warn(_(b"(use 'hg help' for a list of commands)\n"))
525 528 except IOError:
526 529 raise
527 530 except KeyboardInterrupt:
528 531 raise
529 532 except: # probably re-raises
530 533 if not handlecommandexception(ui):
531 534 raise
532 535
533 536 return -1
534 537
535 538
536 539 def aliasargs(fn, givenargs):
537 540 args = []
538 541 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
539 542 if not util.safehasattr(fn, b'_origfunc'):
540 543 args = getattr(fn, 'args', args)
541 544 if args:
542 545 cmd = b' '.join(map(procutil.shellquote, args))
543 546
544 547 nums = []
545 548
546 549 def replacer(m):
547 550 num = int(m.group(1)) - 1
548 551 nums.append(num)
549 552 if num < len(givenargs):
550 553 return givenargs[num]
551 554 raise error.Abort(_(b'too few arguments for command alias'))
552 555
553 556 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
554 557 givenargs = [x for i, x in enumerate(givenargs) if i not in nums]
555 558 args = pycompat.shlexsplit(cmd)
556 559 return args + givenargs
557 560
558 561
559 562 def aliasinterpolate(name, args, cmd):
560 563 '''interpolate args into cmd for shell aliases
561 564
562 565 This also handles $0, $@ and "$@".
563 566 '''
564 567 # util.interpolate can't deal with "$@" (with quotes) because it's only
565 568 # built to match prefix + patterns.
566 569 replacemap = {b'$%d' % (i + 1): arg for i, arg in enumerate(args)}
567 570 replacemap[b'$0'] = name
568 571 replacemap[b'$$'] = b'$'
569 572 replacemap[b'$@'] = b' '.join(args)
570 573 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
571 574 # parameters, separated out into words. Emulate the same behavior here by
572 575 # quoting the arguments individually. POSIX shells will then typically
573 576 # tokenize each argument into exactly one word.
574 577 replacemap[b'"$@"'] = b' '.join(procutil.shellquote(arg) for arg in args)
575 578 # escape '\$' for regex
576 579 regex = b'|'.join(replacemap.keys()).replace(b'$', br'\$')
577 580 r = re.compile(regex)
578 581 return r.sub(lambda x: replacemap[x.group()], cmd)
579 582
580 583
581 584 class cmdalias(object):
582 585 def __init__(self, ui, name, definition, cmdtable, source):
583 586 self.name = self.cmd = name
584 587 self.cmdname = b''
585 588 self.definition = definition
586 589 self.fn = None
587 590 self.givenargs = []
588 591 self.opts = []
589 592 self.help = b''
590 593 self.badalias = None
591 594 self.unknowncmd = False
592 595 self.source = source
593 596
594 597 try:
595 598 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
596 599 for alias, e in pycompat.iteritems(cmdtable):
597 600 if e is entry:
598 601 self.cmd = alias
599 602 break
600 603 self.shadows = True
601 604 except error.UnknownCommand:
602 605 self.shadows = False
603 606
604 607 if not self.definition:
605 608 self.badalias = _(b"no definition for alias '%s'") % self.name
606 609 return
607 610
608 611 if self.definition.startswith(b'!'):
609 612 shdef = self.definition[1:]
610 613 self.shell = True
611 614
612 615 def fn(ui, *args):
613 616 env = {b'HG_ARGS': b' '.join((self.name,) + args)}
614 617
615 618 def _checkvar(m):
616 619 if m.groups()[0] == b'$':
617 620 return m.group()
618 621 elif int(m.groups()[0]) <= len(args):
619 622 return m.group()
620 623 else:
621 624 ui.debug(
622 625 b"No argument found for substitution "
623 626 b"of %i variable in alias '%s' definition.\n"
624 627 % (int(m.groups()[0]), self.name)
625 628 )
626 629 return b''
627 630
628 631 cmd = re.sub(br'\$(\d+|\$)', _checkvar, shdef)
629 632 cmd = aliasinterpolate(self.name, args, cmd)
630 633 return ui.system(
631 634 cmd, environ=env, blockedtag=b'alias_%s' % self.name
632 635 )
633 636
634 637 self.fn = fn
635 638 self.alias = True
636 639 self._populatehelp(ui, name, shdef, self.fn)
637 640 return
638 641
639 642 try:
640 643 args = pycompat.shlexsplit(self.definition)
641 644 except ValueError as inst:
642 645 self.badalias = _(b"error in definition for alias '%s': %s") % (
643 646 self.name,
644 647 stringutil.forcebytestr(inst),
645 648 )
646 649 return
647 650 earlyopts, args = _earlysplitopts(args)
648 651 if earlyopts:
649 652 self.badalias = _(
650 653 b"error in definition for alias '%s': %s may "
651 654 b"only be given on the command line"
652 655 ) % (self.name, b'/'.join(pycompat.ziplist(*earlyopts)[0]))
653 656 return
654 657 self.cmdname = cmd = args.pop(0)
655 658 self.givenargs = args
656 659
657 660 try:
658 661 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
659 662 if len(tableentry) > 2:
660 663 self.fn, self.opts, cmdhelp = tableentry
661 664 else:
662 665 self.fn, self.opts = tableentry
663 666 cmdhelp = None
664 667
665 668 self.alias = True
666 669 self._populatehelp(ui, name, cmd, self.fn, cmdhelp)
667 670
668 671 except error.UnknownCommand:
669 672 self.badalias = _(
670 673 b"alias '%s' resolves to unknown command '%s'"
671 674 ) % (self.name, cmd,)
672 675 self.unknowncmd = True
673 676 except error.AmbiguousCommand:
674 677 self.badalias = _(
675 678 b"alias '%s' resolves to ambiguous command '%s'"
676 679 ) % (self.name, cmd,)
677 680
678 681 def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None):
679 682 # confine strings to be passed to i18n.gettext()
680 683 cfg = {}
681 684 for k in (b'doc', b'help', b'category'):
682 685 v = ui.config(b'alias', b'%s:%s' % (name, k), None)
683 686 if v is None:
684 687 continue
685 688 if not encoding.isasciistr(v):
686 689 self.badalias = _(
687 690 b"non-ASCII character in alias definition '%s:%s'"
688 691 ) % (name, k)
689 692 return
690 693 cfg[k] = v
691 694
692 695 self.help = cfg.get(b'help', defaulthelp or b'')
693 696 if self.help and self.help.startswith(b"hg " + cmd):
694 697 # drop prefix in old-style help lines so hg shows the alias
695 698 self.help = self.help[4 + len(cmd) :]
696 699
697 700 self.owndoc = b'doc' in cfg
698 701 doc = cfg.get(b'doc', pycompat.getdoc(fn))
699 702 if doc is not None:
700 703 doc = pycompat.sysstr(doc)
701 704 self.__doc__ = doc
702 705
703 706 self.helpcategory = cfg.get(
704 707 b'category', registrar.command.CATEGORY_NONE
705 708 )
706 709
707 710 @property
708 711 def args(self):
709 712 args = pycompat.maplist(util.expandpath, self.givenargs)
710 713 return aliasargs(self.fn, args)
711 714
712 715 def __getattr__(self, name):
713 716 adefaults = {
714 717 'norepo': True,
715 718 'intents': set(),
716 719 'optionalrepo': False,
717 720 'inferrepo': False,
718 721 }
719 722 if name not in adefaults:
720 723 raise AttributeError(name)
721 724 if self.badalias or util.safehasattr(self, b'shell'):
722 725 return adefaults[name]
723 726 return getattr(self.fn, name)
724 727
725 728 def __call__(self, ui, *args, **opts):
726 729 if self.badalias:
727 730 hint = None
728 731 if self.unknowncmd:
729 732 try:
730 733 # check if the command is in a disabled extension
731 734 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
732 735 hint = _(b"'%s' is provided by '%s' extension") % (cmd, ext)
733 736 except error.UnknownCommand:
734 737 pass
735 738 raise error.Abort(self.badalias, hint=hint)
736 739 if self.shadows:
737 740 ui.debug(
738 741 b"alias '%s' shadows command '%s'\n" % (self.name, self.cmdname)
739 742 )
740 743
741 744 ui.log(
742 745 b'commandalias',
743 746 b"alias '%s' expands to '%s'\n",
744 747 self.name,
745 748 self.definition,
746 749 )
747 750 if util.safehasattr(self, b'shell'):
748 751 return self.fn(ui, *args, **opts)
749 752 else:
750 753 try:
751 754 return util.checksignature(self.fn)(ui, *args, **opts)
752 755 except error.SignatureError:
753 756 args = b' '.join([self.cmdname] + self.args)
754 757 ui.debug(b"alias '%s' expands to '%s'\n" % (self.name, args))
755 758 raise
756 759
757 760
758 761 class lazyaliasentry(object):
759 762 """like a typical command entry (func, opts, help), but is lazy"""
760 763
761 764 def __init__(self, ui, name, definition, cmdtable, source):
762 765 self.ui = ui
763 766 self.name = name
764 767 self.definition = definition
765 768 self.cmdtable = cmdtable.copy()
766 769 self.source = source
767 770 self.alias = True
768 771
769 772 @util.propertycache
770 773 def _aliasdef(self):
771 774 return cmdalias(
772 775 self.ui, self.name, self.definition, self.cmdtable, self.source
773 776 )
774 777
775 778 def __getitem__(self, n):
776 779 aliasdef = self._aliasdef
777 780 if n == 0:
778 781 return aliasdef
779 782 elif n == 1:
780 783 return aliasdef.opts
781 784 elif n == 2:
782 785 return aliasdef.help
783 786 else:
784 787 raise IndexError
785 788
786 789 def __iter__(self):
787 790 for i in range(3):
788 791 yield self[i]
789 792
790 793 def __len__(self):
791 794 return 3
792 795
793 796
794 797 def addaliases(ui, cmdtable):
795 798 # aliases are processed after extensions have been loaded, so they
796 799 # may use extension commands. Aliases can also use other alias definitions,
797 800 # but only if they have been defined prior to the current definition.
798 801 for alias, definition in ui.configitems(b'alias', ignoresub=True):
799 802 try:
800 803 if cmdtable[alias].definition == definition:
801 804 continue
802 805 except (KeyError, AttributeError):
803 806 # definition might not exist or it might not be a cmdalias
804 807 pass
805 808
806 809 source = ui.configsource(b'alias', alias)
807 810 entry = lazyaliasentry(ui, alias, definition, cmdtable, source)
808 811 cmdtable[alias] = entry
809 812
810 813
811 814 def _parse(ui, args):
812 815 options = {}
813 816 cmdoptions = {}
814 817
815 818 try:
816 819 args = fancyopts.fancyopts(args, commands.globalopts, options)
817 820 except getopt.GetoptError as inst:
818 821 raise error.CommandError(None, stringutil.forcebytestr(inst))
819 822
820 823 if args:
821 824 cmd, args = args[0], args[1:]
822 825 aliases, entry = cmdutil.findcmd(
823 826 cmd, commands.table, ui.configbool(b"ui", b"strict")
824 827 )
825 828 cmd = aliases[0]
826 829 args = aliasargs(entry[0], args)
827 830 defaults = ui.config(b"defaults", cmd)
828 831 if defaults:
829 832 args = (
830 833 pycompat.maplist(util.expandpath, pycompat.shlexsplit(defaults))
831 834 + args
832 835 )
833 836 c = list(entry[1])
834 837 else:
835 838 cmd = None
836 839 c = []
837 840
838 841 # combine global options into local
839 842 for o in commands.globalopts:
840 843 c.append((o[0], o[1], options[o[1]], o[3]))
841 844
842 845 try:
843 846 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
844 847 except getopt.GetoptError as inst:
845 848 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
846 849
847 850 # separate global options back out
848 851 for o in commands.globalopts:
849 852 n = o[1]
850 853 options[n] = cmdoptions[n]
851 854 del cmdoptions[n]
852 855
853 856 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
854 857
855 858
856 859 def _parseconfig(ui, config):
857 860 """parse the --config options from the command line"""
858 861 configs = []
859 862
860 863 for cfg in config:
861 864 try:
862 865 name, value = [cfgelem.strip() for cfgelem in cfg.split(b'=', 1)]
863 866 section, name = name.split(b'.', 1)
864 867 if not section or not name:
865 868 raise IndexError
866 869 ui.setconfig(section, name, value, b'--config')
867 870 configs.append((section, name, value))
868 871 except (IndexError, ValueError):
869 872 raise error.Abort(
870 873 _(
871 874 b'malformed --config option: %r '
872 875 b'(use --config section.name=value)'
873 876 )
874 877 % pycompat.bytestr(cfg)
875 878 )
876 879
877 880 return configs
878 881
879 882
880 883 def _earlyparseopts(ui, args):
881 884 options = {}
882 885 fancyopts.fancyopts(
883 886 args,
884 887 commands.globalopts,
885 888 options,
886 889 gnu=not ui.plain(b'strictflags'),
887 890 early=True,
888 891 optaliases={b'repository': [b'repo']},
889 892 )
890 893 return options
891 894
892 895
893 896 def _earlysplitopts(args):
894 897 """Split args into a list of possible early options and remainder args"""
895 898 shortoptions = b'R:'
896 899 # TODO: perhaps 'debugger' should be included
897 900 longoptions = [b'cwd=', b'repository=', b'repo=', b'config=']
898 901 return fancyopts.earlygetopt(
899 902 args, shortoptions, longoptions, gnu=True, keepsep=True
900 903 )
901 904
902 905
903 906 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
904 907 # run pre-hook, and abort if it fails
905 908 hook.hook(
906 909 lui,
907 910 repo,
908 911 b"pre-%s" % cmd,
909 912 True,
910 913 args=b" ".join(fullargs),
911 914 pats=cmdpats,
912 915 opts=cmdoptions,
913 916 )
914 917 try:
915 918 ret = _runcommand(ui, options, cmd, d)
916 919 # run post-hook, passing command result
917 920 hook.hook(
918 921 lui,
919 922 repo,
920 923 b"post-%s" % cmd,
921 924 False,
922 925 args=b" ".join(fullargs),
923 926 result=ret,
924 927 pats=cmdpats,
925 928 opts=cmdoptions,
926 929 )
927 930 except Exception:
928 931 # run failure hook and re-raise
929 932 hook.hook(
930 933 lui,
931 934 repo,
932 935 b"fail-%s" % cmd,
933 936 False,
934 937 args=b" ".join(fullargs),
935 938 pats=cmdpats,
936 939 opts=cmdoptions,
937 940 )
938 941 raise
939 942 return ret
940 943
941 944
945 def _readsharedsourceconfig(ui, path):
946 """if the current repository is shared one, this tries to read
947 .hg/hgrc of shared source if we are in share-safe mode
948
949 Config read is loaded into the ui object passed
950
951 This should be called before reading .hg/hgrc or the main repo
952 as that overrides config set in shared source"""
953 try:
954 with open(os.path.join(path, b".hg", b"requires"), "rb") as fp:
955 requirements = set(fp.read().splitlines())
956 if not (
957 requirementsmod.SHARESAFE_REQUIREMENT in requirements
958 and requirementsmod.SHARED_REQUIREMENT in requirements
959 ):
960 return
961 hgvfs = vfs.vfs(os.path.join(path, b".hg"))
962 sharedvfs = localrepo._getsharedvfs(hgvfs, requirements)
963 ui.readconfig(sharedvfs.join(b"hgrc"), path)
964 except IOError:
965 pass
966
967
942 968 def _getlocal(ui, rpath, wd=None):
943 969 """Return (path, local ui object) for the given target path.
944 970
945 971 Takes paths in [cwd]/.hg/hgrc into account."
946 972 """
947 973 if wd is None:
948 974 try:
949 975 wd = encoding.getcwd()
950 976 except OSError as e:
951 977 raise error.Abort(
952 978 _(b"error getting current working directory: %s")
953 979 % encoding.strtolocal(e.strerror)
954 980 )
955 981
956 982 path = cmdutil.findrepo(wd) or b""
957 983 if not path:
958 984 lui = ui
959 985 else:
960 986 lui = ui.copy()
961 987 if rcutil.use_repo_hgrc():
988 _readsharedsourceconfig(lui, path)
962 989 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
963 990
964 991 if rpath:
965 992 path = lui.expandpath(rpath)
966 993 lui = ui.copy()
967 994 if rcutil.use_repo_hgrc():
995 _readsharedsourceconfig(lui, path)
968 996 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
969 997
970 998 return path, lui
971 999
972 1000
973 1001 def _checkshellalias(lui, ui, args):
974 1002 """Return the function to run the shell alias, if it is required"""
975 1003 options = {}
976 1004
977 1005 try:
978 1006 args = fancyopts.fancyopts(args, commands.globalopts, options)
979 1007 except getopt.GetoptError:
980 1008 return
981 1009
982 1010 if not args:
983 1011 return
984 1012
985 1013 cmdtable = commands.table
986 1014
987 1015 cmd = args[0]
988 1016 try:
989 1017 strict = ui.configbool(b"ui", b"strict")
990 1018 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
991 1019 except (error.AmbiguousCommand, error.UnknownCommand):
992 1020 return
993 1021
994 1022 cmd = aliases[0]
995 1023 fn = entry[0]
996 1024
997 1025 if cmd and util.safehasattr(fn, b'shell'):
998 1026 # shell alias shouldn't receive early options which are consumed by hg
999 1027 _earlyopts, args = _earlysplitopts(args)
1000 1028 d = lambda: fn(ui, *args[1:])
1001 1029 return lambda: runcommand(
1002 1030 lui, None, cmd, args[:1], ui, options, d, [], {}
1003 1031 )
1004 1032
1005 1033
1006 1034 def _dispatch(req):
1007 1035 args = req.args
1008 1036 ui = req.ui
1009 1037
1010 1038 # check for cwd
1011 1039 cwd = req.earlyoptions[b'cwd']
1012 1040 if cwd:
1013 1041 os.chdir(cwd)
1014 1042
1015 1043 rpath = req.earlyoptions[b'repository']
1016 1044 path, lui = _getlocal(ui, rpath)
1017 1045
1018 1046 uis = {ui, lui}
1019 1047
1020 1048 if req.repo:
1021 1049 uis.add(req.repo.ui)
1022 1050
1023 1051 if (
1024 1052 req.earlyoptions[b'verbose']
1025 1053 or req.earlyoptions[b'debug']
1026 1054 or req.earlyoptions[b'quiet']
1027 1055 ):
1028 1056 for opt in (b'verbose', b'debug', b'quiet'):
1029 1057 val = pycompat.bytestr(bool(req.earlyoptions[opt]))
1030 1058 for ui_ in uis:
1031 1059 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1032 1060
1033 1061 if req.earlyoptions[b'profile']:
1034 1062 for ui_ in uis:
1035 1063 ui_.setconfig(b'profiling', b'enabled', b'true', b'--profile')
1036 1064
1037 1065 profile = lui.configbool(b'profiling', b'enabled')
1038 1066 with profiling.profile(lui, enabled=profile) as profiler:
1039 1067 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
1040 1068 # reposetup
1041 1069 extensions.loadall(lui)
1042 1070 # Propagate any changes to lui.__class__ by extensions
1043 1071 ui.__class__ = lui.__class__
1044 1072
1045 1073 # (uisetup and extsetup are handled in extensions.loadall)
1046 1074
1047 1075 # (reposetup is handled in hg.repository)
1048 1076
1049 1077 addaliases(lui, commands.table)
1050 1078
1051 1079 # All aliases and commands are completely defined, now.
1052 1080 # Check abbreviation/ambiguity of shell alias.
1053 1081 shellaliasfn = _checkshellalias(lui, ui, args)
1054 1082 if shellaliasfn:
1055 1083 # no additional configs will be set, set up the ui instances
1056 1084 for ui_ in uis:
1057 1085 extensions.populateui(ui_)
1058 1086 return shellaliasfn()
1059 1087
1060 1088 # check for fallback encoding
1061 1089 fallback = lui.config(b'ui', b'fallbackencoding')
1062 1090 if fallback:
1063 1091 encoding.fallbackencoding = fallback
1064 1092
1065 1093 fullargs = args
1066 1094 cmd, func, args, options, cmdoptions = _parse(lui, args)
1067 1095
1068 1096 # store the canonical command name in request object for later access
1069 1097 req.canonical_command = cmd
1070 1098
1071 1099 if options[b"config"] != req.earlyoptions[b"config"]:
1072 1100 raise error.Abort(_(b"option --config may not be abbreviated!"))
1073 1101 if options[b"cwd"] != req.earlyoptions[b"cwd"]:
1074 1102 raise error.Abort(_(b"option --cwd may not be abbreviated!"))
1075 1103 if options[b"repository"] != req.earlyoptions[b"repository"]:
1076 1104 raise error.Abort(
1077 1105 _(
1078 1106 b"option -R has to be separated from other options (e.g. not "
1079 1107 b"-qR) and --repository may only be abbreviated as --repo!"
1080 1108 )
1081 1109 )
1082 1110 if options[b"debugger"] != req.earlyoptions[b"debugger"]:
1083 1111 raise error.Abort(_(b"option --debugger may not be abbreviated!"))
1084 1112 # don't validate --profile/--traceback, which can be enabled from now
1085 1113
1086 1114 if options[b"encoding"]:
1087 1115 encoding.encoding = options[b"encoding"]
1088 1116 if options[b"encodingmode"]:
1089 1117 encoding.encodingmode = options[b"encodingmode"]
1090 1118 if options[b"time"]:
1091 1119
1092 1120 def get_times():
1093 1121 t = os.times()
1094 1122 if t[4] == 0.0:
1095 1123 # Windows leaves this as zero, so use time.perf_counter()
1096 1124 t = (t[0], t[1], t[2], t[3], util.timer())
1097 1125 return t
1098 1126
1099 1127 s = get_times()
1100 1128
1101 1129 def print_time():
1102 1130 t = get_times()
1103 1131 ui.warn(
1104 1132 _(b"time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n")
1105 1133 % (
1106 1134 t[4] - s[4],
1107 1135 t[0] - s[0],
1108 1136 t[2] - s[2],
1109 1137 t[1] - s[1],
1110 1138 t[3] - s[3],
1111 1139 )
1112 1140 )
1113 1141
1114 1142 ui.atexit(print_time)
1115 1143 if options[b"profile"]:
1116 1144 profiler.start()
1117 1145
1118 1146 # if abbreviated version of this were used, take them in account, now
1119 1147 if options[b'verbose'] or options[b'debug'] or options[b'quiet']:
1120 1148 for opt in (b'verbose', b'debug', b'quiet'):
1121 1149 if options[opt] == req.earlyoptions[opt]:
1122 1150 continue
1123 1151 val = pycompat.bytestr(bool(options[opt]))
1124 1152 for ui_ in uis:
1125 1153 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1126 1154
1127 1155 if options[b'traceback']:
1128 1156 for ui_ in uis:
1129 1157 ui_.setconfig(b'ui', b'traceback', b'on', b'--traceback')
1130 1158
1131 1159 if options[b'noninteractive']:
1132 1160 for ui_ in uis:
1133 1161 ui_.setconfig(b'ui', b'interactive', b'off', b'-y')
1134 1162
1135 1163 if cmdoptions.get(b'insecure', False):
1136 1164 for ui_ in uis:
1137 1165 ui_.insecureconnections = True
1138 1166
1139 1167 # setup color handling before pager, because setting up pager
1140 1168 # might cause incorrect console information
1141 1169 coloropt = options[b'color']
1142 1170 for ui_ in uis:
1143 1171 if coloropt:
1144 1172 ui_.setconfig(b'ui', b'color', coloropt, b'--color')
1145 1173 color.setup(ui_)
1146 1174
1147 1175 if stringutil.parsebool(options[b'pager']):
1148 1176 # ui.pager() expects 'internal-always-' prefix in this case
1149 1177 ui.pager(b'internal-always-' + cmd)
1150 1178 elif options[b'pager'] != b'auto':
1151 1179 for ui_ in uis:
1152 1180 ui_.disablepager()
1153 1181
1154 1182 # configs are fully loaded, set up the ui instances
1155 1183 for ui_ in uis:
1156 1184 extensions.populateui(ui_)
1157 1185
1158 1186 if options[b'version']:
1159 1187 return commands.version_(ui)
1160 1188 if options[b'help']:
1161 1189 return commands.help_(ui, cmd, command=cmd is not None)
1162 1190 elif not cmd:
1163 1191 return commands.help_(ui, b'shortlist')
1164 1192
1165 1193 repo = None
1166 1194 cmdpats = args[:]
1167 1195 assert func is not None # help out pytype
1168 1196 if not func.norepo:
1169 1197 # use the repo from the request only if we don't have -R
1170 1198 if not rpath and not cwd:
1171 1199 repo = req.repo
1172 1200
1173 1201 if repo:
1174 1202 # set the descriptors of the repo ui to those of ui
1175 1203 repo.ui.fin = ui.fin
1176 1204 repo.ui.fout = ui.fout
1177 1205 repo.ui.ferr = ui.ferr
1178 1206 repo.ui.fmsg = ui.fmsg
1179 1207 else:
1180 1208 try:
1181 1209 repo = hg.repository(
1182 1210 ui,
1183 1211 path=path,
1184 1212 presetupfuncs=req.prereposetups,
1185 1213 intents=func.intents,
1186 1214 )
1187 1215 if not repo.local():
1188 1216 raise error.Abort(
1189 1217 _(b"repository '%s' is not local") % path
1190 1218 )
1191 1219 repo.ui.setconfig(
1192 1220 b"bundle", b"mainreporoot", repo.root, b'repo'
1193 1221 )
1194 1222 except error.RequirementError:
1195 1223 raise
1196 1224 except error.RepoError:
1197 1225 if rpath: # invalid -R path
1198 1226 raise
1199 1227 if not func.optionalrepo:
1200 1228 if func.inferrepo and args and not path:
1201 1229 # try to infer -R from command args
1202 1230 repos = pycompat.maplist(cmdutil.findrepo, args)
1203 1231 guess = repos[0]
1204 1232 if guess and repos.count(guess) == len(repos):
1205 1233 req.args = [b'--repository', guess] + fullargs
1206 1234 req.earlyoptions[b'repository'] = guess
1207 1235 return _dispatch(req)
1208 1236 if not path:
1209 1237 raise error.RepoError(
1210 1238 _(
1211 1239 b"no repository found in"
1212 1240 b" '%s' (.hg not found)"
1213 1241 )
1214 1242 % encoding.getcwd()
1215 1243 )
1216 1244 raise
1217 1245 if repo:
1218 1246 ui = repo.ui
1219 1247 if options[b'hidden']:
1220 1248 repo = repo.unfiltered()
1221 1249 args.insert(0, repo)
1222 1250 elif rpath:
1223 1251 ui.warn(_(b"warning: --repository ignored\n"))
1224 1252
1225 1253 msg = _formatargs(fullargs)
1226 1254 ui.log(b"command", b'%s\n', msg)
1227 1255 strcmdopt = pycompat.strkwargs(cmdoptions)
1228 1256 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
1229 1257 try:
1230 1258 return runcommand(
1231 1259 lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions
1232 1260 )
1233 1261 finally:
1234 1262 if repo and repo != req.repo:
1235 1263 repo.close()
1236 1264
1237 1265
1238 1266 def _runcommand(ui, options, cmd, cmdfunc):
1239 1267 """Run a command function, possibly with profiling enabled."""
1240 1268 try:
1241 1269 with tracing.log("Running %s command" % cmd):
1242 1270 return cmdfunc()
1243 1271 except error.SignatureError:
1244 1272 raise error.CommandError(cmd, _(b'invalid arguments'))
1245 1273
1246 1274
1247 1275 def _exceptionwarning(ui):
1248 1276 """Produce a warning message for the current active exception"""
1249 1277
1250 1278 # For compatibility checking, we discard the portion of the hg
1251 1279 # version after the + on the assumption that if a "normal
1252 1280 # user" is running a build with a + in it the packager
1253 1281 # probably built from fairly close to a tag and anyone with a
1254 1282 # 'make local' copy of hg (where the version number can be out
1255 1283 # of date) will be clueful enough to notice the implausible
1256 1284 # version number and try updating.
1257 1285 ct = util.versiontuple(n=2)
1258 1286 worst = None, ct, b''
1259 1287 if ui.config(b'ui', b'supportcontact') is None:
1260 1288 for name, mod in extensions.extensions():
1261 1289 # 'testedwith' should be bytes, but not all extensions are ported
1262 1290 # to py3 and we don't want UnicodeException because of that.
1263 1291 testedwith = stringutil.forcebytestr(
1264 1292 getattr(mod, 'testedwith', b'')
1265 1293 )
1266 1294 report = getattr(mod, 'buglink', _(b'the extension author.'))
1267 1295 if not testedwith.strip():
1268 1296 # We found an untested extension. It's likely the culprit.
1269 1297 worst = name, b'unknown', report
1270 1298 break
1271 1299
1272 1300 # Never blame on extensions bundled with Mercurial.
1273 1301 if extensions.ismoduleinternal(mod):
1274 1302 continue
1275 1303
1276 1304 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1277 1305 if ct in tested:
1278 1306 continue
1279 1307
1280 1308 lower = [t for t in tested if t < ct]
1281 1309 nearest = max(lower or tested)
1282 1310 if worst[0] is None or nearest < worst[1]:
1283 1311 worst = name, nearest, report
1284 1312 if worst[0] is not None:
1285 1313 name, testedwith, report = worst
1286 1314 if not isinstance(testedwith, (bytes, str)):
1287 1315 testedwith = b'.'.join(
1288 1316 [stringutil.forcebytestr(c) for c in testedwith]
1289 1317 )
1290 1318 warning = _(
1291 1319 b'** Unknown exception encountered with '
1292 1320 b'possibly-broken third-party extension %s\n'
1293 1321 b'** which supports versions %s of Mercurial.\n'
1294 1322 b'** Please disable %s and try your action again.\n'
1295 1323 b'** If that fixes the bug please report it to %s\n'
1296 1324 ) % (name, testedwith, name, stringutil.forcebytestr(report))
1297 1325 else:
1298 1326 bugtracker = ui.config(b'ui', b'supportcontact')
1299 1327 if bugtracker is None:
1300 1328 bugtracker = _(b"https://mercurial-scm.org/wiki/BugTracker")
1301 1329 warning = (
1302 1330 _(
1303 1331 b"** unknown exception encountered, "
1304 1332 b"please report by visiting\n** "
1305 1333 )
1306 1334 + bugtracker
1307 1335 + b'\n'
1308 1336 )
1309 1337 sysversion = pycompat.sysbytes(sys.version).replace(b'\n', b'')
1310 1338 warning += (
1311 1339 (_(b"** Python %s\n") % sysversion)
1312 1340 + (_(b"** Mercurial Distributed SCM (version %s)\n") % util.version())
1313 1341 + (
1314 1342 _(b"** Extensions loaded: %s\n")
1315 1343 % b", ".join([x[0] for x in extensions.extensions()])
1316 1344 )
1317 1345 )
1318 1346 return warning
1319 1347
1320 1348
1321 1349 def handlecommandexception(ui):
1322 1350 """Produce a warning message for broken commands
1323 1351
1324 1352 Called when handling an exception; the exception is reraised if
1325 1353 this function returns False, ignored otherwise.
1326 1354 """
1327 1355 warning = _exceptionwarning(ui)
1328 1356 ui.log(
1329 1357 b"commandexception",
1330 1358 b"%s\n%s\n",
1331 1359 warning,
1332 1360 pycompat.sysbytes(traceback.format_exc()),
1333 1361 )
1334 1362 ui.warn(warning)
1335 1363 return False # re-raise the exception
@@ -1,282 +1,275 b''
1 1 setup
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > share =
6 6 > [format]
7 7 > exp-share-safe = True
8 8 > EOF
9 9
10 10 prepare source repo
11 11
12 12 $ hg init source
13 13 $ cd source
14 14 $ cat .hg/requires
15 15 exp-sharesafe
16 16 $ cat .hg/store/requires
17 17 dotencode
18 18 fncache
19 19 generaldelta
20 20 revlogv1
21 21 sparserevlog
22 22 store
23 23 $ hg debugrequirements
24 24 dotencode
25 25 exp-sharesafe
26 26 fncache
27 27 generaldelta
28 28 revlogv1
29 29 sparserevlog
30 30 store
31 31
32 32 $ echo a > a
33 33 $ hg ci -Aqm "added a"
34 34 $ echo b > b
35 35 $ hg ci -Aqm "added b"
36 36
37 37 $ HGEDITOR=cat hg config --shared
38 38 abort: repository is not shared; can't use --shared
39 39 [255]
40 40 $ cd ..
41 41
42 42 Create a shared repo and check the requirements are shared and read correctly
43 43 $ hg share source shared1
44 44 updating working directory
45 45 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
46 46 $ cd shared1
47 47 $ cat .hg/requires
48 48 exp-sharesafe
49 49 shared
50 50
51 51 $ hg debugrequirements -R ../source
52 52 dotencode
53 53 exp-sharesafe
54 54 fncache
55 55 generaldelta
56 56 revlogv1
57 57 sparserevlog
58 58 store
59 59
60 60 $ hg debugrequirements
61 61 dotencode
62 62 exp-sharesafe
63 63 fncache
64 64 generaldelta
65 65 revlogv1
66 66 shared
67 67 sparserevlog
68 68 store
69 69
70 70 $ echo c > c
71 71 $ hg ci -Aqm "added c"
72 72
73 73 Check that config of the source repository is also loaded
74 74
75 75 $ hg showconfig ui.curses
76 76 [1]
77 77
78 78 $ echo "[ui]" >> ../source/.hg/hgrc
79 79 $ echo "curses=true" >> ../source/.hg/hgrc
80 80
81 81 $ hg showconfig ui.curses
82 82 true
83 83
84 84 Test that extensions of source repository are also loaded
85 85
86 86 $ hg debugextensions
87 87 share
88 88 $ hg extdiff -p echo
89 89 hg: unknown command 'extdiff'
90 90 'extdiff' is provided by the following extension:
91 91
92 92 extdiff command to allow external programs to compare revisions
93 93
94 94 (use 'hg help extensions' for information on enabling extensions)
95 95 [255]
96 96
97 97 $ echo "[extensions]" >> ../source/.hg/hgrc
98 98 $ echo "extdiff=" >> ../source/.hg/hgrc
99 99
100 100 $ hg debugextensions -R ../source
101 101 extdiff
102 102 share
103 103 $ hg extdiff -R ../source -p echo
104 104
105 BROKEN: the command below does not work but debugextensions says that extension
105 BROKEN: the command below will not work if config of shared source is not loaded
106 on dispatch but debugextensions says that extension
106 107 is loaded
107 108 $ hg debugextensions
108 109 extdiff
109 110 share
110 111
111 BROKEN: extdiff command should work here
112 112 $ hg extdiff -p echo
113 hg: unknown command 'extdiff'
114 'extdiff' is provided by the following extension:
115
116 extdiff command to allow external programs to compare revisions
117
118 (use 'hg help extensions' for information on enabling extensions)
119 [255]
120 113
121 114 However, local .hg/hgrc should override the config set by share source
122 115
123 116 $ echo "[ui]" >> .hg/hgrc
124 117 $ echo "curses=false" >> .hg/hgrc
125 118
126 119 $ hg showconfig ui.curses
127 120 false
128 121
129 122 $ HGEDITOR=cat hg config --shared
130 123 [ui]
131 124 curses=true
132 125 [extensions]
133 126 extdiff=
134 127
135 128 $ HGEDITOR=cat hg config --local
136 129 [ui]
137 130 curses=false
138 131
139 132 Testing that hooks set in source repository also runs in shared repo
140 133
141 134 $ cd ../source
142 135 $ cat <<EOF >> .hg/hgrc
143 136 > [extensions]
144 137 > hooklib=
145 138 > [hooks]
146 139 > pretxnchangegroup.reject_merge_commits = \
147 140 > python:hgext.hooklib.reject_merge_commits.hook
148 141 > EOF
149 142
150 143 $ cd ..
151 144 $ hg clone source cloned
152 145 updating to branch default
153 146 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
154 147 $ cd cloned
155 148 $ hg up 0
156 149 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
157 150 $ echo bar > bar
158 151 $ hg ci -Aqm "added bar"
159 152 $ hg merge
160 153 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
161 154 (branch merge, don't forget to commit)
162 155 $ hg ci -m "merge commit"
163 156
164 157 $ hg push ../source
165 158 pushing to ../source
166 159 searching for changes
167 160 adding changesets
168 161 adding manifests
169 162 adding file changes
170 163 error: pretxnchangegroup.reject_merge_commits hook failed: bcde3522682d rejected as merge on the same branch. Please consider rebase.
171 164 transaction abort!
172 165 rollback completed
173 166 abort: bcde3522682d rejected as merge on the same branch. Please consider rebase.
174 167 [255]
175 168
176 169 $ hg push ../shared1
177 170 pushing to ../shared1
178 171 searching for changes
179 172 adding changesets
180 173 adding manifests
181 174 adding file changes
182 175 error: pretxnchangegroup.reject_merge_commits hook failed: bcde3522682d rejected as merge on the same branch. Please consider rebase.
183 176 transaction abort!
184 177 rollback completed
185 178 abort: bcde3522682d rejected as merge on the same branch. Please consider rebase.
186 179 [255]
187 180
188 181 Test that if share source config is untrusted, we dont read it
189 182
190 183 $ cd ../shared1
191 184
192 185 $ cat << EOF > $TESTTMP/untrusted.py
193 186 > from mercurial import scmutil, util
194 187 > def uisetup(ui):
195 188 > class untrustedui(ui.__class__):
196 189 > def _trusted(self, fp, f):
197 190 > if util.normpath(fp.name).endswith(b'source/.hg/hgrc'):
198 191 > return False
199 192 > return super(untrustedui, self)._trusted(fp, f)
200 193 > ui.__class__ = untrustedui
201 194 > EOF
202 195
203 196 $ hg showconfig hooks
204 197 hooks.pretxnchangegroup.reject_merge_commits=python:hgext.hooklib.reject_merge_commits.hook
205 198
206 199 $ hg showconfig hooks --config extensions.untrusted=$TESTTMP/untrusted.py
207 200 [1]
208 201
209 202 Update the source repository format and check that shared repo works
210 203
211 204 $ cd ../source
212 205
213 206 Disable zstd related tests because its not present on pure version
214 207 #if zstd
215 208 $ echo "[format]" >> .hg/hgrc
216 209 $ echo "revlog-compression=zstd" >> .hg/hgrc
217 210
218 211 $ hg debugupgraderepo --run -q
219 212 upgrade will perform the following actions:
220 213
221 214 requirements
222 215 preserved: dotencode, exp-sharesafe, fncache, generaldelta, revlogv1, sparserevlog, store
223 216 added: revlog-compression-zstd
224 217
225 218 $ hg log -r .
226 219 changeset: 1:5f6d8a4bf34a
227 220 user: test
228 221 date: Thu Jan 01 00:00:00 1970 +0000
229 222 summary: added b
230 223
231 224 #endif
232 225 $ echo "[format]" >> .hg/hgrc
233 226 $ echo "use-persistent-nodemap=True" >> .hg/hgrc
234 227
235 228 $ hg debugupgraderepo --run -q -R ../shared1
236 229 abort: cannot upgrade repository; unsupported source requirement: shared
237 230 [255]
238 231
239 232 $ hg debugupgraderepo --run -q
240 233 upgrade will perform the following actions:
241 234
242 235 requirements
243 236 preserved: dotencode, exp-sharesafe, fncache, generaldelta, revlogv1, sparserevlog, store (no-zstd !)
244 237 preserved: dotencode, exp-sharesafe, fncache, generaldelta, revlog-compression-zstd, revlogv1, sparserevlog, store (zstd !)
245 238 added: persistent-nodemap
246 239
247 240 $ hg log -r .
248 241 changeset: 1:5f6d8a4bf34a
249 242 user: test
250 243 date: Thu Jan 01 00:00:00 1970 +0000
251 244 summary: added b
252 245
253 246
254 247 Shared one should work
255 248 $ cd ../shared1
256 249 $ hg log -r .
257 250 changeset: 2:155349b645be
258 251 tag: tip
259 252 user: test
260 253 date: Thu Jan 01 00:00:00 1970 +0000
261 254 summary: added c
262 255
263 256 Unsharing works
264 257
265 258 $ hg unshare
266 259
267 260 Test that source config is added to the shared one after unshare, and the config
268 261 of current repo is still respected over the config which came from source config
269 262 $ cd ../cloned
270 263 $ hg push ../shared1
271 264 pushing to ../shared1
272 265 searching for changes
273 266 adding changesets
274 267 adding manifests
275 268 adding file changes
276 269 error: pretxnchangegroup.reject_merge_commits hook failed: bcde3522682d rejected as merge on the same branch. Please consider rebase.
277 270 transaction abort!
278 271 rollback completed
279 272 abort: bcde3522682d rejected as merge on the same branch. Please consider rebase.
280 273 [255]
281 274 $ hg showconfig ui.curses -R ../shared1
282 275 false
General Comments 0
You need to be logged in to leave comments. Login now