##// END OF EJS Templates
editor: file created for diff action should have .diff suffix...
Michael Bolin -
r34056:3c82b14d default
parent child Browse files
Show More
@@ -1,1792 +1,1794 b''
1 1 # ui.py - user interface bits 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
9 9
10 10 import collections
11 11 import contextlib
12 12 import errno
13 13 import getpass
14 14 import inspect
15 15 import os
16 16 import re
17 17 import signal
18 18 import socket
19 19 import subprocess
20 20 import sys
21 21 import tempfile
22 22 import traceback
23 23
24 24 from .i18n import _
25 25 from .node import hex
26 26
27 27 from . import (
28 28 color,
29 29 config,
30 30 configitems,
31 31 encoding,
32 32 error,
33 33 formatter,
34 34 progress,
35 35 pycompat,
36 36 rcutil,
37 37 scmutil,
38 38 util,
39 39 )
40 40
41 41 urlreq = util.urlreq
42 42
43 43 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
44 44 _keepalnum = ''.join(c for c in map(pycompat.bytechr, range(256))
45 45 if not c.isalnum())
46 46
47 47 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
48 48 tweakrc = """
49 49 [ui]
50 50 # The rollback command is dangerous. As a rule, don't use it.
51 51 rollback = False
52 52
53 53 [commands]
54 54 # Make `hg status` emit cwd-relative paths by default.
55 55 status.relative = yes
56 56
57 57 [diff]
58 58 git = 1
59 59 """
60 60
61 61 samplehgrcs = {
62 62 'user':
63 63 b"""# example user config (see 'hg help config' for more info)
64 64 [ui]
65 65 # name and email, e.g.
66 66 # username = Jane Doe <jdoe@example.com>
67 67 username =
68 68
69 69 # uncomment to disable color in command output
70 70 # (see 'hg help color' for details)
71 71 # color = never
72 72
73 73 # uncomment to disable command output pagination
74 74 # (see 'hg help pager' for details)
75 75 # paginate = never
76 76
77 77 [extensions]
78 78 # uncomment these lines to enable some popular extensions
79 79 # (see 'hg help extensions' for more info)
80 80 #
81 81 # churn =
82 82 """,
83 83
84 84 'cloned':
85 85 b"""# example repository config (see 'hg help config' for more info)
86 86 [paths]
87 87 default = %s
88 88
89 89 # path aliases to other clones of this repo in URLs or filesystem paths
90 90 # (see 'hg help config.paths' for more info)
91 91 #
92 92 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
93 93 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
94 94 # my-clone = /home/jdoe/jdoes-clone
95 95
96 96 [ui]
97 97 # name and email (local to this repository, optional), e.g.
98 98 # username = Jane Doe <jdoe@example.com>
99 99 """,
100 100
101 101 'local':
102 102 b"""# example repository config (see 'hg help config' for more info)
103 103 [paths]
104 104 # path aliases to other clones of this repo in URLs or filesystem paths
105 105 # (see 'hg help config.paths' for more info)
106 106 #
107 107 # default = http://example.com/hg/example-repo
108 108 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
109 109 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
110 110 # my-clone = /home/jdoe/jdoes-clone
111 111
112 112 [ui]
113 113 # name and email (local to this repository, optional), e.g.
114 114 # username = Jane Doe <jdoe@example.com>
115 115 """,
116 116
117 117 'global':
118 118 b"""# example system-wide hg config (see 'hg help config' for more info)
119 119
120 120 [ui]
121 121 # uncomment to disable color in command output
122 122 # (see 'hg help color' for details)
123 123 # color = never
124 124
125 125 # uncomment to disable command output pagination
126 126 # (see 'hg help pager' for details)
127 127 # paginate = never
128 128
129 129 [extensions]
130 130 # uncomment these lines to enable some popular extensions
131 131 # (see 'hg help extensions' for more info)
132 132 #
133 133 # blackbox =
134 134 # churn =
135 135 """,
136 136 }
137 137
138 138
139 139 class httppasswordmgrdbproxy(object):
140 140 """Delays loading urllib2 until it's needed."""
141 141 def __init__(self):
142 142 self._mgr = None
143 143
144 144 def _get_mgr(self):
145 145 if self._mgr is None:
146 146 self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
147 147 return self._mgr
148 148
149 149 def add_password(self, *args, **kwargs):
150 150 return self._get_mgr().add_password(*args, **kwargs)
151 151
152 152 def find_user_password(self, *args, **kwargs):
153 153 return self._get_mgr().find_user_password(*args, **kwargs)
154 154
155 155 def _catchterm(*args):
156 156 raise error.SignalInterrupt
157 157
158 158 # unique object used to detect no default value has been provided when
159 159 # retrieving configuration value.
160 160 _unset = object()
161 161
162 162 class ui(object):
163 163 def __init__(self, src=None):
164 164 """Create a fresh new ui object if no src given
165 165
166 166 Use uimod.ui.load() to create a ui which knows global and user configs.
167 167 In most cases, you should use ui.copy() to create a copy of an existing
168 168 ui object.
169 169 """
170 170 # _buffers: used for temporary capture of output
171 171 self._buffers = []
172 172 # _exithandlers: callbacks run at the end of a request
173 173 self._exithandlers = []
174 174 # 3-tuple describing how each buffer in the stack behaves.
175 175 # Values are (capture stderr, capture subprocesses, apply labels).
176 176 self._bufferstates = []
177 177 # When a buffer is active, defines whether we are expanding labels.
178 178 # This exists to prevent an extra list lookup.
179 179 self._bufferapplylabels = None
180 180 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
181 181 self._reportuntrusted = True
182 182 self._knownconfig = configitems.coreitems
183 183 self._ocfg = config.config() # overlay
184 184 self._tcfg = config.config() # trusted
185 185 self._ucfg = config.config() # untrusted
186 186 self._trustusers = set()
187 187 self._trustgroups = set()
188 188 self.callhooks = True
189 189 # Insecure server connections requested.
190 190 self.insecureconnections = False
191 191 # Blocked time
192 192 self.logblockedtimes = False
193 193 # color mode: see mercurial/color.py for possible value
194 194 self._colormode = None
195 195 self._terminfoparams = {}
196 196 self._styles = {}
197 197
198 198 if src:
199 199 self._exithandlers = src._exithandlers
200 200 self.fout = src.fout
201 201 self.ferr = src.ferr
202 202 self.fin = src.fin
203 203 self.pageractive = src.pageractive
204 204 self._disablepager = src._disablepager
205 205 self._tweaked = src._tweaked
206 206
207 207 self._tcfg = src._tcfg.copy()
208 208 self._ucfg = src._ucfg.copy()
209 209 self._ocfg = src._ocfg.copy()
210 210 self._trustusers = src._trustusers.copy()
211 211 self._trustgroups = src._trustgroups.copy()
212 212 self.environ = src.environ
213 213 self.callhooks = src.callhooks
214 214 self.insecureconnections = src.insecureconnections
215 215 self._colormode = src._colormode
216 216 self._terminfoparams = src._terminfoparams.copy()
217 217 self._styles = src._styles.copy()
218 218
219 219 self.fixconfig()
220 220
221 221 self.httppasswordmgrdb = src.httppasswordmgrdb
222 222 self._blockedtimes = src._blockedtimes
223 223 else:
224 224 self.fout = util.stdout
225 225 self.ferr = util.stderr
226 226 self.fin = util.stdin
227 227 self.pageractive = False
228 228 self._disablepager = False
229 229 self._tweaked = False
230 230
231 231 # shared read-only environment
232 232 self.environ = encoding.environ
233 233
234 234 self.httppasswordmgrdb = httppasswordmgrdbproxy()
235 235 self._blockedtimes = collections.defaultdict(int)
236 236
237 237 allowed = self.configlist('experimental', 'exportableenviron')
238 238 if '*' in allowed:
239 239 self._exportableenviron = self.environ
240 240 else:
241 241 self._exportableenviron = {}
242 242 for k in allowed:
243 243 if k in self.environ:
244 244 self._exportableenviron[k] = self.environ[k]
245 245
246 246 @classmethod
247 247 def load(cls):
248 248 """Create a ui and load global and user configs"""
249 249 u = cls()
250 250 # we always trust global config files and environment variables
251 251 for t, f in rcutil.rccomponents():
252 252 if t == 'path':
253 253 u.readconfig(f, trust=True)
254 254 elif t == 'items':
255 255 sections = set()
256 256 for section, name, value, source in f:
257 257 # do not set u._ocfg
258 258 # XXX clean this up once immutable config object is a thing
259 259 u._tcfg.set(section, name, value, source)
260 260 u._ucfg.set(section, name, value, source)
261 261 sections.add(section)
262 262 for section in sections:
263 263 u.fixconfig(section=section)
264 264 else:
265 265 raise error.ProgrammingError('unknown rctype: %s' % t)
266 266 u._maybetweakdefaults()
267 267 return u
268 268
269 269 def _maybetweakdefaults(self):
270 270 if not self.configbool('ui', 'tweakdefaults'):
271 271 return
272 272 if self._tweaked or self.plain('tweakdefaults'):
273 273 return
274 274
275 275 # Note: it is SUPER IMPORTANT that you set self._tweaked to
276 276 # True *before* any calls to setconfig(), otherwise you'll get
277 277 # infinite recursion between setconfig and this method.
278 278 #
279 279 # TODO: We should extract an inner method in setconfig() to
280 280 # avoid this weirdness.
281 281 self._tweaked = True
282 282 tmpcfg = config.config()
283 283 tmpcfg.parse('<tweakdefaults>', tweakrc)
284 284 for section in tmpcfg:
285 285 for name, value in tmpcfg.items(section):
286 286 if not self.hasconfig(section, name):
287 287 self.setconfig(section, name, value, "<tweakdefaults>")
288 288
289 289 def copy(self):
290 290 return self.__class__(self)
291 291
292 292 def resetstate(self):
293 293 """Clear internal state that shouldn't persist across commands"""
294 294 if self._progbar:
295 295 self._progbar.resetstate() # reset last-print time of progress bar
296 296 self.httppasswordmgrdb = httppasswordmgrdbproxy()
297 297
298 298 @contextlib.contextmanager
299 299 def timeblockedsection(self, key):
300 300 # this is open-coded below - search for timeblockedsection to find them
301 301 starttime = util.timer()
302 302 try:
303 303 yield
304 304 finally:
305 305 self._blockedtimes[key + '_blocked'] += \
306 306 (util.timer() - starttime) * 1000
307 307
308 308 def formatter(self, topic, opts):
309 309 return formatter.formatter(self, self, topic, opts)
310 310
311 311 def _trusted(self, fp, f):
312 312 st = util.fstat(fp)
313 313 if util.isowner(st):
314 314 return True
315 315
316 316 tusers, tgroups = self._trustusers, self._trustgroups
317 317 if '*' in tusers or '*' in tgroups:
318 318 return True
319 319
320 320 user = util.username(st.st_uid)
321 321 group = util.groupname(st.st_gid)
322 322 if user in tusers or group in tgroups or user == util.username():
323 323 return True
324 324
325 325 if self._reportuntrusted:
326 326 self.warn(_('not trusting file %s from untrusted '
327 327 'user %s, group %s\n') % (f, user, group))
328 328 return False
329 329
330 330 def readconfig(self, filename, root=None, trust=False,
331 331 sections=None, remap=None):
332 332 try:
333 333 fp = open(filename, u'rb')
334 334 except IOError:
335 335 if not sections: # ignore unless we were looking for something
336 336 return
337 337 raise
338 338
339 339 cfg = config.config()
340 340 trusted = sections or trust or self._trusted(fp, filename)
341 341
342 342 try:
343 343 cfg.read(filename, fp, sections=sections, remap=remap)
344 344 fp.close()
345 345 except error.ConfigError as inst:
346 346 if trusted:
347 347 raise
348 348 self.warn(_("ignored: %s\n") % str(inst))
349 349
350 350 if self.plain():
351 351 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
352 352 'logtemplate', 'statuscopies', 'style',
353 353 'traceback', 'verbose'):
354 354 if k in cfg['ui']:
355 355 del cfg['ui'][k]
356 356 for k, v in cfg.items('defaults'):
357 357 del cfg['defaults'][k]
358 358 for k, v in cfg.items('commands'):
359 359 del cfg['commands'][k]
360 360 # Don't remove aliases from the configuration if in the exceptionlist
361 361 if self.plain('alias'):
362 362 for k, v in cfg.items('alias'):
363 363 del cfg['alias'][k]
364 364 if self.plain('revsetalias'):
365 365 for k, v in cfg.items('revsetalias'):
366 366 del cfg['revsetalias'][k]
367 367 if self.plain('templatealias'):
368 368 for k, v in cfg.items('templatealias'):
369 369 del cfg['templatealias'][k]
370 370
371 371 if trusted:
372 372 self._tcfg.update(cfg)
373 373 self._tcfg.update(self._ocfg)
374 374 self._ucfg.update(cfg)
375 375 self._ucfg.update(self._ocfg)
376 376
377 377 if root is None:
378 378 root = os.path.expanduser('~')
379 379 self.fixconfig(root=root)
380 380
381 381 def fixconfig(self, root=None, section=None):
382 382 if section in (None, 'paths'):
383 383 # expand vars and ~
384 384 # translate paths relative to root (or home) into absolute paths
385 385 root = root or pycompat.getcwd()
386 386 for c in self._tcfg, self._ucfg, self._ocfg:
387 387 for n, p in c.items('paths'):
388 388 # Ignore sub-options.
389 389 if ':' in n:
390 390 continue
391 391 if not p:
392 392 continue
393 393 if '%%' in p:
394 394 s = self.configsource('paths', n) or 'none'
395 395 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
396 396 % (n, p, s))
397 397 p = p.replace('%%', '%')
398 398 p = util.expandpath(p)
399 399 if not util.hasscheme(p) and not os.path.isabs(p):
400 400 p = os.path.normpath(os.path.join(root, p))
401 401 c.set("paths", n, p)
402 402
403 403 if section in (None, 'ui'):
404 404 # update ui options
405 405 self.debugflag = self.configbool('ui', 'debug')
406 406 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
407 407 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
408 408 if self.verbose and self.quiet:
409 409 self.quiet = self.verbose = False
410 410 self._reportuntrusted = self.debugflag or self.configbool("ui",
411 411 "report_untrusted")
412 412 self.tracebackflag = self.configbool('ui', 'traceback')
413 413 self.logblockedtimes = self.configbool('ui', 'logblockedtimes')
414 414
415 415 if section in (None, 'trusted'):
416 416 # update trust information
417 417 self._trustusers.update(self.configlist('trusted', 'users'))
418 418 self._trustgroups.update(self.configlist('trusted', 'groups'))
419 419
420 420 def backupconfig(self, section, item):
421 421 return (self._ocfg.backup(section, item),
422 422 self._tcfg.backup(section, item),
423 423 self._ucfg.backup(section, item),)
424 424 def restoreconfig(self, data):
425 425 self._ocfg.restore(data[0])
426 426 self._tcfg.restore(data[1])
427 427 self._ucfg.restore(data[2])
428 428
429 429 def setconfig(self, section, name, value, source=''):
430 430 for cfg in (self._ocfg, self._tcfg, self._ucfg):
431 431 cfg.set(section, name, value, source)
432 432 self.fixconfig(section=section)
433 433 self._maybetweakdefaults()
434 434
435 435 def _data(self, untrusted):
436 436 return untrusted and self._ucfg or self._tcfg
437 437
438 438 def configsource(self, section, name, untrusted=False):
439 439 return self._data(untrusted).source(section, name)
440 440
441 441 def config(self, section, name, default=_unset, untrusted=False):
442 442 """return the plain string version of a config"""
443 443 value = self._config(section, name, default=default,
444 444 untrusted=untrusted)
445 445 if value is _unset:
446 446 return None
447 447 return value
448 448
449 449 def _config(self, section, name, default=_unset, untrusted=False):
450 450 value = default
451 451 item = self._knownconfig.get(section, {}).get(name)
452 452 alternates = [(section, name)]
453 453
454 454 if item is not None:
455 455 alternates.extend(item.alias)
456 456
457 457 if default is _unset:
458 458 if item is None:
459 459 value = default
460 460 elif item.default is configitems.dynamicdefault:
461 461 value = None
462 462 msg = "config item requires an explicit default value: '%s.%s'"
463 463 msg %= (section, name)
464 464 self.develwarn(msg, 2, 'warn-config-default')
465 465 elif callable(item.default):
466 466 value = item.default()
467 467 else:
468 468 value = item.default
469 469 elif (item is not None
470 470 and item.default is not configitems.dynamicdefault):
471 471 msg = ("specifying a default value for a registered "
472 472 "config item: '%s.%s' '%s'")
473 473 msg %= (section, name, default)
474 474 self.develwarn(msg, 2, 'warn-config-default')
475 475
476 476 for s, n in alternates:
477 477 candidate = self._data(untrusted).get(s, n, None)
478 478 if candidate is not None:
479 479 value = candidate
480 480 section = s
481 481 name = n
482 482 break
483 483
484 484 if self.debugflag and not untrusted and self._reportuntrusted:
485 485 for s, n in alternates:
486 486 uvalue = self._ucfg.get(s, n)
487 487 if uvalue is not None and uvalue != value:
488 488 self.debug("ignoring untrusted configuration option "
489 489 "%s.%s = %s\n" % (s, n, uvalue))
490 490 return value
491 491
492 492 def configsuboptions(self, section, name, default=_unset, untrusted=False):
493 493 """Get a config option and all sub-options.
494 494
495 495 Some config options have sub-options that are declared with the
496 496 format "key:opt = value". This method is used to return the main
497 497 option and all its declared sub-options.
498 498
499 499 Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
500 500 is a dict of defined sub-options where keys and values are strings.
501 501 """
502 502 main = self.config(section, name, default, untrusted=untrusted)
503 503 data = self._data(untrusted)
504 504 sub = {}
505 505 prefix = '%s:' % name
506 506 for k, v in data.items(section):
507 507 if k.startswith(prefix):
508 508 sub[k[len(prefix):]] = v
509 509
510 510 if self.debugflag and not untrusted and self._reportuntrusted:
511 511 for k, v in sub.items():
512 512 uvalue = self._ucfg.get(section, '%s:%s' % (name, k))
513 513 if uvalue is not None and uvalue != v:
514 514 self.debug('ignoring untrusted configuration option '
515 515 '%s:%s.%s = %s\n' % (section, name, k, uvalue))
516 516
517 517 return main, sub
518 518
519 519 def configpath(self, section, name, default=_unset, untrusted=False):
520 520 'get a path config item, expanded relative to repo root or config file'
521 521 v = self.config(section, name, default, untrusted)
522 522 if v is None:
523 523 return None
524 524 if not os.path.isabs(v) or "://" not in v:
525 525 src = self.configsource(section, name, untrusted)
526 526 if ':' in src:
527 527 base = os.path.dirname(src.rsplit(':')[0])
528 528 v = os.path.join(base, os.path.expanduser(v))
529 529 return v
530 530
531 531 def configbool(self, section, name, default=_unset, untrusted=False):
532 532 """parse a configuration element as a boolean
533 533
534 534 >>> u = ui(); s = 'foo'
535 535 >>> u.setconfig(s, 'true', 'yes')
536 536 >>> u.configbool(s, 'true')
537 537 True
538 538 >>> u.setconfig(s, 'false', 'no')
539 539 >>> u.configbool(s, 'false')
540 540 False
541 541 >>> u.configbool(s, 'unknown')
542 542 False
543 543 >>> u.configbool(s, 'unknown', True)
544 544 True
545 545 >>> u.setconfig(s, 'invalid', 'somevalue')
546 546 >>> u.configbool(s, 'invalid')
547 547 Traceback (most recent call last):
548 548 ...
549 549 ConfigError: foo.invalid is not a boolean ('somevalue')
550 550 """
551 551
552 552 v = self._config(section, name, default, untrusted=untrusted)
553 553 if v is None:
554 554 return v
555 555 if v is _unset:
556 556 if default is _unset:
557 557 return False
558 558 return default
559 559 if isinstance(v, bool):
560 560 return v
561 561 b = util.parsebool(v)
562 562 if b is None:
563 563 raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
564 564 % (section, name, v))
565 565 return b
566 566
567 567 def configwith(self, convert, section, name, default=_unset,
568 568 desc=None, untrusted=False):
569 569 """parse a configuration element with a conversion function
570 570
571 571 >>> u = ui(); s = 'foo'
572 572 >>> u.setconfig(s, 'float1', '42')
573 573 >>> u.configwith(float, s, 'float1')
574 574 42.0
575 575 >>> u.setconfig(s, 'float2', '-4.25')
576 576 >>> u.configwith(float, s, 'float2')
577 577 -4.25
578 578 >>> u.configwith(float, s, 'unknown', 7)
579 579 7.0
580 580 >>> u.setconfig(s, 'invalid', 'somevalue')
581 581 >>> u.configwith(float, s, 'invalid')
582 582 Traceback (most recent call last):
583 583 ...
584 584 ConfigError: foo.invalid is not a valid float ('somevalue')
585 585 >>> u.configwith(float, s, 'invalid', desc='womble')
586 586 Traceback (most recent call last):
587 587 ...
588 588 ConfigError: foo.invalid is not a valid womble ('somevalue')
589 589 """
590 590
591 591 v = self.config(section, name, default, untrusted)
592 592 if v is None:
593 593 return v # do not attempt to convert None
594 594 try:
595 595 return convert(v)
596 596 except (ValueError, error.ParseError):
597 597 if desc is None:
598 598 desc = convert.__name__
599 599 raise error.ConfigError(_("%s.%s is not a valid %s ('%s')")
600 600 % (section, name, desc, v))
601 601
602 602 def configint(self, section, name, default=_unset, untrusted=False):
603 603 """parse a configuration element as an integer
604 604
605 605 >>> u = ui(); s = 'foo'
606 606 >>> u.setconfig(s, 'int1', '42')
607 607 >>> u.configint(s, 'int1')
608 608 42
609 609 >>> u.setconfig(s, 'int2', '-42')
610 610 >>> u.configint(s, 'int2')
611 611 -42
612 612 >>> u.configint(s, 'unknown', 7)
613 613 7
614 614 >>> u.setconfig(s, 'invalid', 'somevalue')
615 615 >>> u.configint(s, 'invalid')
616 616 Traceback (most recent call last):
617 617 ...
618 618 ConfigError: foo.invalid is not a valid integer ('somevalue')
619 619 """
620 620
621 621 return self.configwith(int, section, name, default, 'integer',
622 622 untrusted)
623 623
624 624 def configbytes(self, section, name, default=_unset, untrusted=False):
625 625 """parse a configuration element as a quantity in bytes
626 626
627 627 Units can be specified as b (bytes), k or kb (kilobytes), m or
628 628 mb (megabytes), g or gb (gigabytes).
629 629
630 630 >>> u = ui(); s = 'foo'
631 631 >>> u.setconfig(s, 'val1', '42')
632 632 >>> u.configbytes(s, 'val1')
633 633 42
634 634 >>> u.setconfig(s, 'val2', '42.5 kb')
635 635 >>> u.configbytes(s, 'val2')
636 636 43520
637 637 >>> u.configbytes(s, 'unknown', '7 MB')
638 638 7340032
639 639 >>> u.setconfig(s, 'invalid', 'somevalue')
640 640 >>> u.configbytes(s, 'invalid')
641 641 Traceback (most recent call last):
642 642 ...
643 643 ConfigError: foo.invalid is not a byte quantity ('somevalue')
644 644 """
645 645
646 646 value = self._config(section, name, default, untrusted)
647 647 if value is _unset:
648 648 if default is _unset:
649 649 default = 0
650 650 value = default
651 651 if not isinstance(value, bytes):
652 652 return value
653 653 try:
654 654 return util.sizetoint(value)
655 655 except error.ParseError:
656 656 raise error.ConfigError(_("%s.%s is not a byte quantity ('%s')")
657 657 % (section, name, value))
658 658
659 659 def configlist(self, section, name, default=_unset, untrusted=False):
660 660 """parse a configuration element as a list of comma/space separated
661 661 strings
662 662
663 663 >>> u = ui(); s = 'foo'
664 664 >>> u.setconfig(s, 'list1', 'this,is "a small" ,test')
665 665 >>> u.configlist(s, 'list1')
666 666 ['this', 'is', 'a small', 'test']
667 667 """
668 668 # default is not always a list
669 669 v = self.configwith(config.parselist, section, name, default,
670 670 'list', untrusted)
671 671 if isinstance(v, bytes):
672 672 return config.parselist(v)
673 673 elif v is None:
674 674 return []
675 675 return v
676 676
677 677 def configdate(self, section, name, default=_unset, untrusted=False):
678 678 """parse a configuration element as a tuple of ints
679 679
680 680 >>> u = ui(); s = 'foo'
681 681 >>> u.setconfig(s, 'date', '0 0')
682 682 >>> u.configdate(s, 'date')
683 683 (0, 0)
684 684 """
685 685 if self.config(section, name, default, untrusted):
686 686 return self.configwith(util.parsedate, section, name, default,
687 687 'date', untrusted)
688 688 if default is _unset:
689 689 return None
690 690 return default
691 691
692 692 def hasconfig(self, section, name, untrusted=False):
693 693 return self._data(untrusted).hasitem(section, name)
694 694
695 695 def has_section(self, section, untrusted=False):
696 696 '''tell whether section exists in config.'''
697 697 return section in self._data(untrusted)
698 698
699 699 def configitems(self, section, untrusted=False, ignoresub=False):
700 700 items = self._data(untrusted).items(section)
701 701 if ignoresub:
702 702 newitems = {}
703 703 for k, v in items:
704 704 if ':' not in k:
705 705 newitems[k] = v
706 706 items = newitems.items()
707 707 if self.debugflag and not untrusted and self._reportuntrusted:
708 708 for k, v in self._ucfg.items(section):
709 709 if self._tcfg.get(section, k) != v:
710 710 self.debug("ignoring untrusted configuration option "
711 711 "%s.%s = %s\n" % (section, k, v))
712 712 return items
713 713
714 714 def walkconfig(self, untrusted=False):
715 715 cfg = self._data(untrusted)
716 716 for section in cfg.sections():
717 717 for name, value in self.configitems(section, untrusted):
718 718 yield section, name, value
719 719
720 720 def plain(self, feature=None):
721 721 '''is plain mode active?
722 722
723 723 Plain mode means that all configuration variables which affect
724 724 the behavior and output of Mercurial should be
725 725 ignored. Additionally, the output should be stable,
726 726 reproducible and suitable for use in scripts or applications.
727 727
728 728 The only way to trigger plain mode is by setting either the
729 729 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
730 730
731 731 The return value can either be
732 732 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
733 733 - True otherwise
734 734 '''
735 735 if ('HGPLAIN' not in encoding.environ and
736 736 'HGPLAINEXCEPT' not in encoding.environ):
737 737 return False
738 738 exceptions = encoding.environ.get('HGPLAINEXCEPT',
739 739 '').strip().split(',')
740 740 if feature and exceptions:
741 741 return feature not in exceptions
742 742 return True
743 743
744 744 def username(self):
745 745 """Return default username to be used in commits.
746 746
747 747 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
748 748 and stop searching if one of these is set.
749 749 If not found and ui.askusername is True, ask the user, else use
750 750 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
751 751 """
752 752 user = encoding.environ.get("HGUSER")
753 753 if user is None:
754 754 user = self.config("ui", "username")
755 755 if user is not None:
756 756 user = os.path.expandvars(user)
757 757 if user is None:
758 758 user = encoding.environ.get("EMAIL")
759 759 if user is None and self.configbool("ui", "askusername"):
760 760 user = self.prompt(_("enter a commit username:"), default=None)
761 761 if user is None and not self.interactive():
762 762 try:
763 763 user = '%s@%s' % (util.getuser(), socket.getfqdn())
764 764 self.warn(_("no username found, using '%s' instead\n") % user)
765 765 except KeyError:
766 766 pass
767 767 if not user:
768 768 raise error.Abort(_('no username supplied'),
769 769 hint=_("use 'hg config --edit' "
770 770 'to set your username'))
771 771 if "\n" in user:
772 772 raise error.Abort(_("username %s contains a newline\n")
773 773 % repr(user))
774 774 return user
775 775
776 776 def shortuser(self, user):
777 777 """Return a short representation of a user name or email address."""
778 778 if not self.verbose:
779 779 user = util.shortuser(user)
780 780 return user
781 781
782 782 def expandpath(self, loc, default=None):
783 783 """Return repository location relative to cwd or from [paths]"""
784 784 try:
785 785 p = self.paths.getpath(loc)
786 786 if p:
787 787 return p.rawloc
788 788 except error.RepoError:
789 789 pass
790 790
791 791 if default:
792 792 try:
793 793 p = self.paths.getpath(default)
794 794 if p:
795 795 return p.rawloc
796 796 except error.RepoError:
797 797 pass
798 798
799 799 return loc
800 800
801 801 @util.propertycache
802 802 def paths(self):
803 803 return paths(self)
804 804
805 805 def pushbuffer(self, error=False, subproc=False, labeled=False):
806 806 """install a buffer to capture standard output of the ui object
807 807
808 808 If error is True, the error output will be captured too.
809 809
810 810 If subproc is True, output from subprocesses (typically hooks) will be
811 811 captured too.
812 812
813 813 If labeled is True, any labels associated with buffered
814 814 output will be handled. By default, this has no effect
815 815 on the output returned, but extensions and GUI tools may
816 816 handle this argument and returned styled output. If output
817 817 is being buffered so it can be captured and parsed or
818 818 processed, labeled should not be set to True.
819 819 """
820 820 self._buffers.append([])
821 821 self._bufferstates.append((error, subproc, labeled))
822 822 self._bufferapplylabels = labeled
823 823
824 824 def popbuffer(self):
825 825 '''pop the last buffer and return the buffered output'''
826 826 self._bufferstates.pop()
827 827 if self._bufferstates:
828 828 self._bufferapplylabels = self._bufferstates[-1][2]
829 829 else:
830 830 self._bufferapplylabels = None
831 831
832 832 return "".join(self._buffers.pop())
833 833
834 834 def write(self, *args, **opts):
835 835 '''write args to output
836 836
837 837 By default, this method simply writes to the buffer or stdout.
838 838 Color mode can be set on the UI class to have the output decorated
839 839 with color modifier before being written to stdout.
840 840
841 841 The color used is controlled by an optional keyword argument, "label".
842 842 This should be a string containing label names separated by space.
843 843 Label names take the form of "topic.type". For example, ui.debug()
844 844 issues a label of "ui.debug".
845 845
846 846 When labeling output for a specific command, a label of
847 847 "cmdname.type" is recommended. For example, status issues
848 848 a label of "status.modified" for modified files.
849 849 '''
850 850 if self._buffers and not opts.get('prompt', False):
851 851 if self._bufferapplylabels:
852 852 label = opts.get('label', '')
853 853 self._buffers[-1].extend(self.label(a, label) for a in args)
854 854 else:
855 855 self._buffers[-1].extend(args)
856 856 elif self._colormode == 'win32':
857 857 # windows color printing is its own can of crab, defer to
858 858 # the color module and that is it.
859 859 color.win32print(self, self._write, *args, **opts)
860 860 else:
861 861 msgs = args
862 862 if self._colormode is not None:
863 863 label = opts.get('label', '')
864 864 msgs = [self.label(a, label) for a in args]
865 865 self._write(*msgs, **opts)
866 866
867 867 def _write(self, *msgs, **opts):
868 868 self._progclear()
869 869 # opencode timeblockedsection because this is a critical path
870 870 starttime = util.timer()
871 871 try:
872 872 for a in msgs:
873 873 self.fout.write(a)
874 874 except IOError as err:
875 875 raise error.StdioError(err)
876 876 finally:
877 877 self._blockedtimes['stdio_blocked'] += \
878 878 (util.timer() - starttime) * 1000
879 879
880 880 def write_err(self, *args, **opts):
881 881 self._progclear()
882 882 if self._bufferstates and self._bufferstates[-1][0]:
883 883 self.write(*args, **opts)
884 884 elif self._colormode == 'win32':
885 885 # windows color printing is its own can of crab, defer to
886 886 # the color module and that is it.
887 887 color.win32print(self, self._write_err, *args, **opts)
888 888 else:
889 889 msgs = args
890 890 if self._colormode is not None:
891 891 label = opts.get('label', '')
892 892 msgs = [self.label(a, label) for a in args]
893 893 self._write_err(*msgs, **opts)
894 894
895 895 def _write_err(self, *msgs, **opts):
896 896 try:
897 897 with self.timeblockedsection('stdio'):
898 898 if not getattr(self.fout, 'closed', False):
899 899 self.fout.flush()
900 900 for a in msgs:
901 901 self.ferr.write(a)
902 902 # stderr may be buffered under win32 when redirected to files,
903 903 # including stdout.
904 904 if not getattr(self.ferr, 'closed', False):
905 905 self.ferr.flush()
906 906 except IOError as inst:
907 907 if inst.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
908 908 raise error.StdioError(inst)
909 909
910 910 def flush(self):
911 911 # opencode timeblockedsection because this is a critical path
912 912 starttime = util.timer()
913 913 try:
914 914 try:
915 915 self.fout.flush()
916 916 except IOError as err:
917 917 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
918 918 raise error.StdioError(err)
919 919 finally:
920 920 try:
921 921 self.ferr.flush()
922 922 except IOError as err:
923 923 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
924 924 raise error.StdioError(err)
925 925 finally:
926 926 self._blockedtimes['stdio_blocked'] += \
927 927 (util.timer() - starttime) * 1000
928 928
929 929 def _isatty(self, fh):
930 930 if self.configbool('ui', 'nontty'):
931 931 return False
932 932 return util.isatty(fh)
933 933
934 934 def disablepager(self):
935 935 self._disablepager = True
936 936
937 937 def pager(self, command):
938 938 """Start a pager for subsequent command output.
939 939
940 940 Commands which produce a long stream of output should call
941 941 this function to activate the user's preferred pagination
942 942 mechanism (which may be no pager). Calling this function
943 943 precludes any future use of interactive functionality, such as
944 944 prompting the user or activating curses.
945 945
946 946 Args:
947 947 command: The full, non-aliased name of the command. That is, "log"
948 948 not "history, "summary" not "summ", etc.
949 949 """
950 950 if (self._disablepager
951 951 or self.pageractive):
952 952 # how pager should do is already determined
953 953 return
954 954
955 955 if not command.startswith('internal-always-') and (
956 956 # explicit --pager=on (= 'internal-always-' prefix) should
957 957 # take precedence over disabling factors below
958 958 command in self.configlist('pager', 'ignore')
959 959 or not self.configbool('ui', 'paginate')
960 960 or not self.configbool('pager', 'attend-' + command, True)
961 961 # TODO: if we want to allow HGPLAINEXCEPT=pager,
962 962 # formatted() will need some adjustment.
963 963 or not self.formatted()
964 964 or self.plain()
965 965 or self._buffers
966 966 # TODO: expose debugger-enabled on the UI object
967 967 or '--debugger' in pycompat.sysargv):
968 968 # We only want to paginate if the ui appears to be
969 969 # interactive, the user didn't say HGPLAIN or
970 970 # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
971 971 return
972 972
973 973 pagercmd = self.config('pager', 'pager', rcutil.fallbackpager)
974 974 if not pagercmd:
975 975 return
976 976
977 977 pagerenv = {}
978 978 for name, value in rcutil.defaultpagerenv().items():
979 979 if name not in encoding.environ:
980 980 pagerenv[name] = value
981 981
982 982 self.debug('starting pager for command %r\n' % command)
983 983 self.flush()
984 984
985 985 wasformatted = self.formatted()
986 986 if util.safehasattr(signal, "SIGPIPE"):
987 987 signal.signal(signal.SIGPIPE, _catchterm)
988 988 if self._runpager(pagercmd, pagerenv):
989 989 self.pageractive = True
990 990 # Preserve the formatted-ness of the UI. This is important
991 991 # because we mess with stdout, which might confuse
992 992 # auto-detection of things being formatted.
993 993 self.setconfig('ui', 'formatted', wasformatted, 'pager')
994 994 self.setconfig('ui', 'interactive', False, 'pager')
995 995
996 996 # If pagermode differs from color.mode, reconfigure color now that
997 997 # pageractive is set.
998 998 cm = self._colormode
999 999 if cm != self.config('color', 'pagermode', cm):
1000 1000 color.setup(self)
1001 1001 else:
1002 1002 # If the pager can't be spawned in dispatch when --pager=on is
1003 1003 # given, don't try again when the command runs, to avoid a duplicate
1004 1004 # warning about a missing pager command.
1005 1005 self.disablepager()
1006 1006
1007 1007 def _runpager(self, command, env=None):
1008 1008 """Actually start the pager and set up file descriptors.
1009 1009
1010 1010 This is separate in part so that extensions (like chg) can
1011 1011 override how a pager is invoked.
1012 1012 """
1013 1013 if command == 'cat':
1014 1014 # Save ourselves some work.
1015 1015 return False
1016 1016 # If the command doesn't contain any of these characters, we
1017 1017 # assume it's a binary and exec it directly. This means for
1018 1018 # simple pager command configurations, we can degrade
1019 1019 # gracefully and tell the user about their broken pager.
1020 1020 shell = any(c in command for c in "|&;<>()$`\\\"' \t\n*?[#~=%")
1021 1021
1022 1022 if pycompat.osname == 'nt' and not shell:
1023 1023 # Window's built-in `more` cannot be invoked with shell=False, but
1024 1024 # its `more.com` can. Hide this implementation detail from the
1025 1025 # user so we can also get sane bad PAGER behavior. MSYS has
1026 1026 # `more.exe`, so do a cmd.exe style resolution of the executable to
1027 1027 # determine which one to use.
1028 1028 fullcmd = util.findexe(command)
1029 1029 if not fullcmd:
1030 1030 self.warn(_("missing pager command '%s', skipping pager\n")
1031 1031 % command)
1032 1032 return False
1033 1033
1034 1034 command = fullcmd
1035 1035
1036 1036 try:
1037 1037 pager = subprocess.Popen(
1038 1038 command, shell=shell, bufsize=-1,
1039 1039 close_fds=util.closefds, stdin=subprocess.PIPE,
1040 1040 stdout=util.stdout, stderr=util.stderr,
1041 1041 env=util.shellenviron(env))
1042 1042 except OSError as e:
1043 1043 if e.errno == errno.ENOENT and not shell:
1044 1044 self.warn(_("missing pager command '%s', skipping pager\n")
1045 1045 % command)
1046 1046 return False
1047 1047 raise
1048 1048
1049 1049 # back up original file descriptors
1050 1050 stdoutfd = os.dup(util.stdout.fileno())
1051 1051 stderrfd = os.dup(util.stderr.fileno())
1052 1052
1053 1053 os.dup2(pager.stdin.fileno(), util.stdout.fileno())
1054 1054 if self._isatty(util.stderr):
1055 1055 os.dup2(pager.stdin.fileno(), util.stderr.fileno())
1056 1056
1057 1057 @self.atexit
1058 1058 def killpager():
1059 1059 if util.safehasattr(signal, "SIGINT"):
1060 1060 signal.signal(signal.SIGINT, signal.SIG_IGN)
1061 1061 # restore original fds, closing pager.stdin copies in the process
1062 1062 os.dup2(stdoutfd, util.stdout.fileno())
1063 1063 os.dup2(stderrfd, util.stderr.fileno())
1064 1064 pager.stdin.close()
1065 1065 pager.wait()
1066 1066
1067 1067 return True
1068 1068
1069 1069 def atexit(self, func, *args, **kwargs):
1070 1070 '''register a function to run after dispatching a request
1071 1071
1072 1072 Handlers do not stay registered across request boundaries.'''
1073 1073 self._exithandlers.append((func, args, kwargs))
1074 1074 return func
1075 1075
1076 1076 def interface(self, feature):
1077 1077 """what interface to use for interactive console features?
1078 1078
1079 1079 The interface is controlled by the value of `ui.interface` but also by
1080 1080 the value of feature-specific configuration. For example:
1081 1081
1082 1082 ui.interface.histedit = text
1083 1083 ui.interface.chunkselector = curses
1084 1084
1085 1085 Here the features are "histedit" and "chunkselector".
1086 1086
1087 1087 The configuration above means that the default interfaces for commands
1088 1088 is curses, the interface for histedit is text and the interface for
1089 1089 selecting chunk is crecord (the best curses interface available).
1090 1090
1091 1091 Consider the following example:
1092 1092 ui.interface = curses
1093 1093 ui.interface.histedit = text
1094 1094
1095 1095 Then histedit will use the text interface and chunkselector will use
1096 1096 the default curses interface (crecord at the moment).
1097 1097 """
1098 1098 alldefaults = frozenset(["text", "curses"])
1099 1099
1100 1100 featureinterfaces = {
1101 1101 "chunkselector": [
1102 1102 "text",
1103 1103 "curses",
1104 1104 ]
1105 1105 }
1106 1106
1107 1107 # Feature-specific interface
1108 1108 if feature not in featureinterfaces.keys():
1109 1109 # Programming error, not user error
1110 1110 raise ValueError("Unknown feature requested %s" % feature)
1111 1111
1112 1112 availableinterfaces = frozenset(featureinterfaces[feature])
1113 1113 if alldefaults > availableinterfaces:
1114 1114 # Programming error, not user error. We need a use case to
1115 1115 # define the right thing to do here.
1116 1116 raise ValueError(
1117 1117 "Feature %s does not handle all default interfaces" %
1118 1118 feature)
1119 1119
1120 1120 if self.plain():
1121 1121 return "text"
1122 1122
1123 1123 # Default interface for all the features
1124 1124 defaultinterface = "text"
1125 1125 i = self.config("ui", "interface")
1126 1126 if i in alldefaults:
1127 1127 defaultinterface = i
1128 1128
1129 1129 choseninterface = defaultinterface
1130 1130 f = self.config("ui", "interface.%s" % feature, None)
1131 1131 if f in availableinterfaces:
1132 1132 choseninterface = f
1133 1133
1134 1134 if i is not None and defaultinterface != i:
1135 1135 if f is not None:
1136 1136 self.warn(_("invalid value for ui.interface: %s\n") %
1137 1137 (i,))
1138 1138 else:
1139 1139 self.warn(_("invalid value for ui.interface: %s (using %s)\n") %
1140 1140 (i, choseninterface))
1141 1141 if f is not None and choseninterface != f:
1142 1142 self.warn(_("invalid value for ui.interface.%s: %s (using %s)\n") %
1143 1143 (feature, f, choseninterface))
1144 1144
1145 1145 return choseninterface
1146 1146
1147 1147 def interactive(self):
1148 1148 '''is interactive input allowed?
1149 1149
1150 1150 An interactive session is a session where input can be reasonably read
1151 1151 from `sys.stdin'. If this function returns false, any attempt to read
1152 1152 from stdin should fail with an error, unless a sensible default has been
1153 1153 specified.
1154 1154
1155 1155 Interactiveness is triggered by the value of the `ui.interactive'
1156 1156 configuration variable or - if it is unset - when `sys.stdin' points
1157 1157 to a terminal device.
1158 1158
1159 1159 This function refers to input only; for output, see `ui.formatted()'.
1160 1160 '''
1161 1161 i = self.configbool("ui", "interactive")
1162 1162 if i is None:
1163 1163 # some environments replace stdin without implementing isatty
1164 1164 # usually those are non-interactive
1165 1165 return self._isatty(self.fin)
1166 1166
1167 1167 return i
1168 1168
1169 1169 def termwidth(self):
1170 1170 '''how wide is the terminal in columns?
1171 1171 '''
1172 1172 if 'COLUMNS' in encoding.environ:
1173 1173 try:
1174 1174 return int(encoding.environ['COLUMNS'])
1175 1175 except ValueError:
1176 1176 pass
1177 1177 return scmutil.termsize(self)[0]
1178 1178
1179 1179 def formatted(self):
1180 1180 '''should formatted output be used?
1181 1181
1182 1182 It is often desirable to format the output to suite the output medium.
1183 1183 Examples of this are truncating long lines or colorizing messages.
1184 1184 However, this is not often not desirable when piping output into other
1185 1185 utilities, e.g. `grep'.
1186 1186
1187 1187 Formatted output is triggered by the value of the `ui.formatted'
1188 1188 configuration variable or - if it is unset - when `sys.stdout' points
1189 1189 to a terminal device. Please note that `ui.formatted' should be
1190 1190 considered an implementation detail; it is not intended for use outside
1191 1191 Mercurial or its extensions.
1192 1192
1193 1193 This function refers to output only; for input, see `ui.interactive()'.
1194 1194 This function always returns false when in plain mode, see `ui.plain()'.
1195 1195 '''
1196 1196 if self.plain():
1197 1197 return False
1198 1198
1199 1199 i = self.configbool("ui", "formatted")
1200 1200 if i is None:
1201 1201 # some environments replace stdout without implementing isatty
1202 1202 # usually those are non-interactive
1203 1203 return self._isatty(self.fout)
1204 1204
1205 1205 return i
1206 1206
1207 1207 def _readline(self, prompt=''):
1208 1208 if self._isatty(self.fin):
1209 1209 try:
1210 1210 # magically add command line editing support, where
1211 1211 # available
1212 1212 import readline
1213 1213 # force demandimport to really load the module
1214 1214 readline.read_history_file
1215 1215 # windows sometimes raises something other than ImportError
1216 1216 except Exception:
1217 1217 pass
1218 1218
1219 1219 # call write() so output goes through subclassed implementation
1220 1220 # e.g. color extension on Windows
1221 1221 self.write(prompt, prompt=True)
1222 1222 self.flush()
1223 1223
1224 1224 # prompt ' ' must exist; otherwise readline may delete entire line
1225 1225 # - http://bugs.python.org/issue12833
1226 1226 with self.timeblockedsection('stdio'):
1227 1227 line = util.bytesinput(self.fin, self.fout, r' ')
1228 1228
1229 1229 # When stdin is in binary mode on Windows, it can cause
1230 1230 # raw_input() to emit an extra trailing carriage return
1231 1231 if pycompat.oslinesep == '\r\n' and line and line[-1] == '\r':
1232 1232 line = line[:-1]
1233 1233 return line
1234 1234
1235 1235 def prompt(self, msg, default="y"):
1236 1236 """Prompt user with msg, read response.
1237 1237 If ui is not interactive, the default is returned.
1238 1238 """
1239 1239 if not self.interactive():
1240 1240 self.write(msg, ' ', default or '', "\n")
1241 1241 return default
1242 1242 try:
1243 1243 r = self._readline(self.label(msg, 'ui.prompt'))
1244 1244 if not r:
1245 1245 r = default
1246 1246 if self.configbool('ui', 'promptecho'):
1247 1247 self.write(r, "\n")
1248 1248 return r
1249 1249 except EOFError:
1250 1250 raise error.ResponseExpected()
1251 1251
1252 1252 @staticmethod
1253 1253 def extractchoices(prompt):
1254 1254 """Extract prompt message and list of choices from specified prompt.
1255 1255
1256 1256 This returns tuple "(message, choices)", and "choices" is the
1257 1257 list of tuple "(response character, text without &)".
1258 1258
1259 1259 >>> ui.extractchoices("awake? $$ &Yes $$ &No")
1260 1260 ('awake? ', [('y', 'Yes'), ('n', 'No')])
1261 1261 >>> ui.extractchoices("line\\nbreak? $$ &Yes $$ &No")
1262 1262 ('line\\nbreak? ', [('y', 'Yes'), ('n', 'No')])
1263 1263 >>> ui.extractchoices("want lots of $$money$$?$$Ye&s$$N&o")
1264 1264 ('want lots of $$money$$?', [('s', 'Yes'), ('o', 'No')])
1265 1265 """
1266 1266
1267 1267 # Sadly, the prompt string may have been built with a filename
1268 1268 # containing "$$" so let's try to find the first valid-looking
1269 1269 # prompt to start parsing. Sadly, we also can't rely on
1270 1270 # choices containing spaces, ASCII, or basically anything
1271 1271 # except an ampersand followed by a character.
1272 1272 m = re.match(br'(?s)(.+?)\$\$([^\$]*&[^ \$].*)', prompt)
1273 1273 msg = m.group(1)
1274 1274 choices = [p.strip(' ') for p in m.group(2).split('$$')]
1275 1275 def choicetuple(s):
1276 1276 ampidx = s.index('&')
1277 1277 return s[ampidx + 1:ampidx + 2].lower(), s.replace('&', '', 1)
1278 1278 return (msg, [choicetuple(s) for s in choices])
1279 1279
1280 1280 def promptchoice(self, prompt, default=0):
1281 1281 """Prompt user with a message, read response, and ensure it matches
1282 1282 one of the provided choices. The prompt is formatted as follows:
1283 1283
1284 1284 "would you like fries with that (Yn)? $$ &Yes $$ &No"
1285 1285
1286 1286 The index of the choice is returned. Responses are case
1287 1287 insensitive. If ui is not interactive, the default is
1288 1288 returned.
1289 1289 """
1290 1290
1291 1291 msg, choices = self.extractchoices(prompt)
1292 1292 resps = [r for r, t in choices]
1293 1293 while True:
1294 1294 r = self.prompt(msg, resps[default])
1295 1295 if r.lower() in resps:
1296 1296 return resps.index(r.lower())
1297 1297 self.write(_("unrecognized response\n"))
1298 1298
1299 1299 def getpass(self, prompt=None, default=None):
1300 1300 if not self.interactive():
1301 1301 return default
1302 1302 try:
1303 1303 self.write_err(self.label(prompt or _('password: '), 'ui.prompt'))
1304 1304 # disable getpass() only if explicitly specified. it's still valid
1305 1305 # to interact with tty even if fin is not a tty.
1306 1306 with self.timeblockedsection('stdio'):
1307 1307 if self.configbool('ui', 'nontty'):
1308 1308 l = self.fin.readline()
1309 1309 if not l:
1310 1310 raise EOFError
1311 1311 return l.rstrip('\n')
1312 1312 else:
1313 1313 return getpass.getpass('')
1314 1314 except EOFError:
1315 1315 raise error.ResponseExpected()
1316 1316 def status(self, *msg, **opts):
1317 1317 '''write status message to output (if ui.quiet is False)
1318 1318
1319 1319 This adds an output label of "ui.status".
1320 1320 '''
1321 1321 if not self.quiet:
1322 1322 opts[r'label'] = opts.get(r'label', '') + ' ui.status'
1323 1323 self.write(*msg, **opts)
1324 1324 def warn(self, *msg, **opts):
1325 1325 '''write warning message to output (stderr)
1326 1326
1327 1327 This adds an output label of "ui.warning".
1328 1328 '''
1329 1329 opts[r'label'] = opts.get(r'label', '') + ' ui.warning'
1330 1330 self.write_err(*msg, **opts)
1331 1331 def note(self, *msg, **opts):
1332 1332 '''write note to output (if ui.verbose is True)
1333 1333
1334 1334 This adds an output label of "ui.note".
1335 1335 '''
1336 1336 if self.verbose:
1337 1337 opts[r'label'] = opts.get(r'label', '') + ' ui.note'
1338 1338 self.write(*msg, **opts)
1339 1339 def debug(self, *msg, **opts):
1340 1340 '''write debug message to output (if ui.debugflag is True)
1341 1341
1342 1342 This adds an output label of "ui.debug".
1343 1343 '''
1344 1344 if self.debugflag:
1345 1345 opts[r'label'] = opts.get(r'label', '') + ' ui.debug'
1346 1346 self.write(*msg, **opts)
1347 1347
1348 1348 def edit(self, text, user, extra=None, editform=None, pending=None,
1349 1349 repopath=None, action=None):
1350 1350 if action is None:
1351 1351 self.develwarn('action is None but will soon be a required '
1352 1352 'parameter to ui.edit()')
1353 1353 extra_defaults = {
1354 1354 'prefix': 'editor',
1355 1355 'suffix': '.txt',
1356 1356 }
1357 1357 if extra is not None:
1358 1358 if extra.get('suffix') is not None:
1359 1359 self.develwarn('extra.suffix is not None but will soon be '
1360 1360 'ignored by ui.edit()')
1361 1361 extra_defaults.update(extra)
1362 1362 extra = extra_defaults
1363 1363
1364 if action:
1364 if action == 'diff':
1365 suffix = '.diff'
1366 elif action:
1365 1367 suffix = '.%s.hg.txt' % action
1366 1368 else:
1367 1369 suffix = extra['suffix']
1368 1370
1369 1371 rdir = None
1370 1372 if self.configbool('experimental', 'editortmpinhg'):
1371 1373 rdir = repopath
1372 1374 (fd, name) = tempfile.mkstemp(prefix='hg-' + extra['prefix'] + '-',
1373 1375 suffix=suffix,
1374 1376 dir=rdir)
1375 1377 try:
1376 1378 f = os.fdopen(fd, r'wb')
1377 1379 f.write(util.tonativeeol(text))
1378 1380 f.close()
1379 1381
1380 1382 environ = {'HGUSER': user}
1381 1383 if 'transplant_source' in extra:
1382 1384 environ.update({'HGREVISION': hex(extra['transplant_source'])})
1383 1385 for label in ('intermediate-source', 'source', 'rebase_source'):
1384 1386 if label in extra:
1385 1387 environ.update({'HGREVISION': extra[label]})
1386 1388 break
1387 1389 if editform:
1388 1390 environ.update({'HGEDITFORM': editform})
1389 1391 if pending:
1390 1392 environ.update({'HG_PENDING': pending})
1391 1393
1392 1394 editor = self.geteditor()
1393 1395
1394 1396 self.system("%s \"%s\"" % (editor, name),
1395 1397 environ=environ,
1396 1398 onerr=error.Abort, errprefix=_("edit failed"),
1397 1399 blockedtag='editor')
1398 1400
1399 1401 f = open(name, r'rb')
1400 1402 t = util.fromnativeeol(f.read())
1401 1403 f.close()
1402 1404 finally:
1403 1405 os.unlink(name)
1404 1406
1405 1407 return t
1406 1408
1407 1409 def system(self, cmd, environ=None, cwd=None, onerr=None, errprefix=None,
1408 1410 blockedtag=None):
1409 1411 '''execute shell command with appropriate output stream. command
1410 1412 output will be redirected if fout is not stdout.
1411 1413
1412 1414 if command fails and onerr is None, return status, else raise onerr
1413 1415 object as exception.
1414 1416 '''
1415 1417 if blockedtag is None:
1416 1418 # Long cmds tend to be because of an absolute path on cmd. Keep
1417 1419 # the tail end instead
1418 1420 cmdsuffix = cmd.translate(None, _keepalnum)[-85:]
1419 1421 blockedtag = 'unknown_system_' + cmdsuffix
1420 1422 out = self.fout
1421 1423 if any(s[1] for s in self._bufferstates):
1422 1424 out = self
1423 1425 with self.timeblockedsection(blockedtag):
1424 1426 rc = self._runsystem(cmd, environ=environ, cwd=cwd, out=out)
1425 1427 if rc and onerr:
1426 1428 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
1427 1429 util.explainexit(rc)[0])
1428 1430 if errprefix:
1429 1431 errmsg = '%s: %s' % (errprefix, errmsg)
1430 1432 raise onerr(errmsg)
1431 1433 return rc
1432 1434
1433 1435 def _runsystem(self, cmd, environ, cwd, out):
1434 1436 """actually execute the given shell command (can be overridden by
1435 1437 extensions like chg)"""
1436 1438 return util.system(cmd, environ=environ, cwd=cwd, out=out)
1437 1439
1438 1440 def traceback(self, exc=None, force=False):
1439 1441 '''print exception traceback if traceback printing enabled or forced.
1440 1442 only to call in exception handler. returns true if traceback
1441 1443 printed.'''
1442 1444 if self.tracebackflag or force:
1443 1445 if exc is None:
1444 1446 exc = sys.exc_info()
1445 1447 cause = getattr(exc[1], 'cause', None)
1446 1448
1447 1449 if cause is not None:
1448 1450 causetb = traceback.format_tb(cause[2])
1449 1451 exctb = traceback.format_tb(exc[2])
1450 1452 exconly = traceback.format_exception_only(cause[0], cause[1])
1451 1453
1452 1454 # exclude frame where 'exc' was chained and rethrown from exctb
1453 1455 self.write_err('Traceback (most recent call last):\n',
1454 1456 ''.join(exctb[:-1]),
1455 1457 ''.join(causetb),
1456 1458 ''.join(exconly))
1457 1459 else:
1458 1460 output = traceback.format_exception(exc[0], exc[1], exc[2])
1459 1461 data = r''.join(output)
1460 1462 if pycompat.ispy3:
1461 1463 enc = pycompat.sysstr(encoding.encoding)
1462 1464 data = data.encode(enc, errors=r'replace')
1463 1465 self.write_err(data)
1464 1466 return self.tracebackflag or force
1465 1467
1466 1468 def geteditor(self):
1467 1469 '''return editor to use'''
1468 1470 if pycompat.sysplatform == 'plan9':
1469 1471 # vi is the MIPS instruction simulator on Plan 9. We
1470 1472 # instead default to E to plumb commit messages to
1471 1473 # avoid confusion.
1472 1474 editor = 'E'
1473 1475 else:
1474 1476 editor = 'vi'
1475 1477 return (encoding.environ.get("HGEDITOR") or
1476 1478 self.config("ui", "editor", editor))
1477 1479
1478 1480 @util.propertycache
1479 1481 def _progbar(self):
1480 1482 """setup the progbar singleton to the ui object"""
1481 1483 if (self.quiet or self.debugflag
1482 1484 or self.configbool('progress', 'disable')
1483 1485 or not progress.shouldprint(self)):
1484 1486 return None
1485 1487 return getprogbar(self)
1486 1488
1487 1489 def _progclear(self):
1488 1490 """clear progress bar output if any. use it before any output"""
1489 1491 if '_progbar' not in vars(self): # nothing loaded yet
1490 1492 return
1491 1493 if self._progbar is not None and self._progbar.printed:
1492 1494 self._progbar.clear()
1493 1495
1494 1496 def progress(self, topic, pos, item="", unit="", total=None):
1495 1497 '''show a progress message
1496 1498
1497 1499 By default a textual progress bar will be displayed if an operation
1498 1500 takes too long. 'topic' is the current operation, 'item' is a
1499 1501 non-numeric marker of the current position (i.e. the currently
1500 1502 in-process file), 'pos' is the current numeric position (i.e.
1501 1503 revision, bytes, etc.), unit is a corresponding unit label,
1502 1504 and total is the highest expected pos.
1503 1505
1504 1506 Multiple nested topics may be active at a time.
1505 1507
1506 1508 All topics should be marked closed by setting pos to None at
1507 1509 termination.
1508 1510 '''
1509 1511 if self._progbar is not None:
1510 1512 self._progbar.progress(topic, pos, item=item, unit=unit,
1511 1513 total=total)
1512 1514 if pos is None or not self.configbool('progress', 'debug'):
1513 1515 return
1514 1516
1515 1517 if unit:
1516 1518 unit = ' ' + unit
1517 1519 if item:
1518 1520 item = ' ' + item
1519 1521
1520 1522 if total:
1521 1523 pct = 100.0 * pos / total
1522 1524 self.debug('%s:%s %s/%s%s (%4.2f%%)\n'
1523 1525 % (topic, item, pos, total, unit, pct))
1524 1526 else:
1525 1527 self.debug('%s:%s %s%s\n' % (topic, item, pos, unit))
1526 1528
1527 1529 def log(self, service, *msg, **opts):
1528 1530 '''hook for logging facility extensions
1529 1531
1530 1532 service should be a readily-identifiable subsystem, which will
1531 1533 allow filtering.
1532 1534
1533 1535 *msg should be a newline-terminated format string to log, and
1534 1536 then any values to %-format into that format string.
1535 1537
1536 1538 **opts currently has no defined meanings.
1537 1539 '''
1538 1540
1539 1541 def label(self, msg, label):
1540 1542 '''style msg based on supplied label
1541 1543
1542 1544 If some color mode is enabled, this will add the necessary control
1543 1545 characters to apply such color. In addition, 'debug' color mode adds
1544 1546 markup showing which label affects a piece of text.
1545 1547
1546 1548 ui.write(s, 'label') is equivalent to
1547 1549 ui.write(ui.label(s, 'label')).
1548 1550 '''
1549 1551 if self._colormode is not None:
1550 1552 return color.colorlabel(self, msg, label)
1551 1553 return msg
1552 1554
1553 1555 def develwarn(self, msg, stacklevel=1, config=None):
1554 1556 """issue a developer warning message
1555 1557
1556 1558 Use 'stacklevel' to report the offender some layers further up in the
1557 1559 stack.
1558 1560 """
1559 1561 if not self.configbool('devel', 'all-warnings'):
1560 1562 if config is not None and not self.configbool('devel', config):
1561 1563 return
1562 1564 msg = 'devel-warn: ' + msg
1563 1565 stacklevel += 1 # get in develwarn
1564 1566 if self.tracebackflag:
1565 1567 util.debugstacktrace(msg, stacklevel, self.ferr, self.fout)
1566 1568 self.log('develwarn', '%s at:\n%s' %
1567 1569 (msg, ''.join(util.getstackframes(stacklevel))))
1568 1570 else:
1569 1571 curframe = inspect.currentframe()
1570 1572 calframe = inspect.getouterframes(curframe, 2)
1571 1573 self.write_err('%s at: %s:%s (%s)\n'
1572 1574 % ((msg,) + calframe[stacklevel][1:4]))
1573 1575 self.log('develwarn', '%s at: %s:%s (%s)\n',
1574 1576 msg, *calframe[stacklevel][1:4])
1575 1577 curframe = calframe = None # avoid cycles
1576 1578
1577 1579 def deprecwarn(self, msg, version):
1578 1580 """issue a deprecation warning
1579 1581
1580 1582 - msg: message explaining what is deprecated and how to upgrade,
1581 1583 - version: last version where the API will be supported,
1582 1584 """
1583 1585 if not (self.configbool('devel', 'all-warnings')
1584 1586 or self.configbool('devel', 'deprec-warn')):
1585 1587 return
1586 1588 msg += ("\n(compatibility will be dropped after Mercurial-%s,"
1587 1589 " update your code.)") % version
1588 1590 self.develwarn(msg, stacklevel=2, config='deprec-warn')
1589 1591
1590 1592 def exportableenviron(self):
1591 1593 """The environment variables that are safe to export, e.g. through
1592 1594 hgweb.
1593 1595 """
1594 1596 return self._exportableenviron
1595 1597
1596 1598 @contextlib.contextmanager
1597 1599 def configoverride(self, overrides, source=""):
1598 1600 """Context manager for temporary config overrides
1599 1601 `overrides` must be a dict of the following structure:
1600 1602 {(section, name) : value}"""
1601 1603 backups = {}
1602 1604 try:
1603 1605 for (section, name), value in overrides.items():
1604 1606 backups[(section, name)] = self.backupconfig(section, name)
1605 1607 self.setconfig(section, name, value, source)
1606 1608 yield
1607 1609 finally:
1608 1610 for __, backup in backups.items():
1609 1611 self.restoreconfig(backup)
1610 1612 # just restoring ui.quiet config to the previous value is not enough
1611 1613 # as it does not update ui.quiet class member
1612 1614 if ('ui', 'quiet') in overrides:
1613 1615 self.fixconfig(section='ui')
1614 1616
1615 1617 class paths(dict):
1616 1618 """Represents a collection of paths and their configs.
1617 1619
1618 1620 Data is initially derived from ui instances and the config files they have
1619 1621 loaded.
1620 1622 """
1621 1623 def __init__(self, ui):
1622 1624 dict.__init__(self)
1623 1625
1624 1626 for name, loc in ui.configitems('paths', ignoresub=True):
1625 1627 # No location is the same as not existing.
1626 1628 if not loc:
1627 1629 continue
1628 1630 loc, sub = ui.configsuboptions('paths', name)
1629 1631 self[name] = path(ui, name, rawloc=loc, suboptions=sub)
1630 1632
1631 1633 def getpath(self, name, default=None):
1632 1634 """Return a ``path`` from a string, falling back to default.
1633 1635
1634 1636 ``name`` can be a named path or locations. Locations are filesystem
1635 1637 paths or URIs.
1636 1638
1637 1639 Returns None if ``name`` is not a registered path, a URI, or a local
1638 1640 path to a repo.
1639 1641 """
1640 1642 # Only fall back to default if no path was requested.
1641 1643 if name is None:
1642 1644 if not default:
1643 1645 default = ()
1644 1646 elif not isinstance(default, (tuple, list)):
1645 1647 default = (default,)
1646 1648 for k in default:
1647 1649 try:
1648 1650 return self[k]
1649 1651 except KeyError:
1650 1652 continue
1651 1653 return None
1652 1654
1653 1655 # Most likely empty string.
1654 1656 # This may need to raise in the future.
1655 1657 if not name:
1656 1658 return None
1657 1659
1658 1660 try:
1659 1661 return self[name]
1660 1662 except KeyError:
1661 1663 # Try to resolve as a local path or URI.
1662 1664 try:
1663 1665 # We don't pass sub-options in, so no need to pass ui instance.
1664 1666 return path(None, None, rawloc=name)
1665 1667 except ValueError:
1666 1668 raise error.RepoError(_('repository %s does not exist') %
1667 1669 name)
1668 1670
1669 1671 _pathsuboptions = {}
1670 1672
1671 1673 def pathsuboption(option, attr):
1672 1674 """Decorator used to declare a path sub-option.
1673 1675
1674 1676 Arguments are the sub-option name and the attribute it should set on
1675 1677 ``path`` instances.
1676 1678
1677 1679 The decorated function will receive as arguments a ``ui`` instance,
1678 1680 ``path`` instance, and the string value of this option from the config.
1679 1681 The function should return the value that will be set on the ``path``
1680 1682 instance.
1681 1683
1682 1684 This decorator can be used to perform additional verification of
1683 1685 sub-options and to change the type of sub-options.
1684 1686 """
1685 1687 def register(func):
1686 1688 _pathsuboptions[option] = (attr, func)
1687 1689 return func
1688 1690 return register
1689 1691
1690 1692 @pathsuboption('pushurl', 'pushloc')
1691 1693 def pushurlpathoption(ui, path, value):
1692 1694 u = util.url(value)
1693 1695 # Actually require a URL.
1694 1696 if not u.scheme:
1695 1697 ui.warn(_('(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
1696 1698 return None
1697 1699
1698 1700 # Don't support the #foo syntax in the push URL to declare branch to
1699 1701 # push.
1700 1702 if u.fragment:
1701 1703 ui.warn(_('("#fragment" in paths.%s:pushurl not supported; '
1702 1704 'ignoring)\n') % path.name)
1703 1705 u.fragment = None
1704 1706
1705 1707 return str(u)
1706 1708
1707 1709 @pathsuboption('pushrev', 'pushrev')
1708 1710 def pushrevpathoption(ui, path, value):
1709 1711 return value
1710 1712
1711 1713 class path(object):
1712 1714 """Represents an individual path and its configuration."""
1713 1715
1714 1716 def __init__(self, ui, name, rawloc=None, suboptions=None):
1715 1717 """Construct a path from its config options.
1716 1718
1717 1719 ``ui`` is the ``ui`` instance the path is coming from.
1718 1720 ``name`` is the symbolic name of the path.
1719 1721 ``rawloc`` is the raw location, as defined in the config.
1720 1722 ``pushloc`` is the raw locations pushes should be made to.
1721 1723
1722 1724 If ``name`` is not defined, we require that the location be a) a local
1723 1725 filesystem path with a .hg directory or b) a URL. If not,
1724 1726 ``ValueError`` is raised.
1725 1727 """
1726 1728 if not rawloc:
1727 1729 raise ValueError('rawloc must be defined')
1728 1730
1729 1731 # Locations may define branches via syntax <base>#<branch>.
1730 1732 u = util.url(rawloc)
1731 1733 branch = None
1732 1734 if u.fragment:
1733 1735 branch = u.fragment
1734 1736 u.fragment = None
1735 1737
1736 1738 self.url = u
1737 1739 self.branch = branch
1738 1740
1739 1741 self.name = name
1740 1742 self.rawloc = rawloc
1741 1743 self.loc = '%s' % u
1742 1744
1743 1745 # When given a raw location but not a symbolic name, validate the
1744 1746 # location is valid.
1745 1747 if not name and not u.scheme and not self._isvalidlocalpath(self.loc):
1746 1748 raise ValueError('location is not a URL or path to a local '
1747 1749 'repo: %s' % rawloc)
1748 1750
1749 1751 suboptions = suboptions or {}
1750 1752
1751 1753 # Now process the sub-options. If a sub-option is registered, its
1752 1754 # attribute will always be present. The value will be None if there
1753 1755 # was no valid sub-option.
1754 1756 for suboption, (attr, func) in _pathsuboptions.iteritems():
1755 1757 if suboption not in suboptions:
1756 1758 setattr(self, attr, None)
1757 1759 continue
1758 1760
1759 1761 value = func(ui, self, suboptions[suboption])
1760 1762 setattr(self, attr, value)
1761 1763
1762 1764 def _isvalidlocalpath(self, path):
1763 1765 """Returns True if the given path is a potentially valid repository.
1764 1766 This is its own function so that extensions can change the definition of
1765 1767 'valid' in this case (like when pulling from a git repo into a hg
1766 1768 one)."""
1767 1769 return os.path.isdir(os.path.join(path, '.hg'))
1768 1770
1769 1771 @property
1770 1772 def suboptions(self):
1771 1773 """Return sub-options and their values for this path.
1772 1774
1773 1775 This is intended to be used for presentation purposes.
1774 1776 """
1775 1777 d = {}
1776 1778 for subopt, (attr, _func) in _pathsuboptions.iteritems():
1777 1779 value = getattr(self, attr)
1778 1780 if value is not None:
1779 1781 d[subopt] = value
1780 1782 return d
1781 1783
1782 1784 # we instantiate one globally shared progress bar to avoid
1783 1785 # competing progress bars when multiple UI objects get created
1784 1786 _progresssingleton = None
1785 1787
1786 1788 def getprogbar(ui):
1787 1789 global _progresssingleton
1788 1790 if _progresssingleton is None:
1789 1791 # passing 'ui' object to the singleton is fishy,
1790 1792 # this is how the extension used to work but feel free to rework it.
1791 1793 _progresssingleton = progress.progbar(ui)
1792 1794 return _progresssingleton
@@ -1,35 +1,63 b''
1 1 Test temp file used with an editor has the expected suffix.
2 2
3 3 $ hg init
4 4
5 5 Create an editor that writes its arguments to stdout and set it to $HGEDITOR.
6 6
7 7 $ cat > editor.sh << EOF
8 8 > #!/bin/bash
9 9 > echo "\$@"
10 10 > exit 1
11 11 > EOF
12 12 $ chmod +x editor.sh
13 13 $ hg add editor.sh
14 14 $ HGEDITOR=$TESTTMP/editor.sh
15 15 $ export HGEDITOR
16 16
17 17 Verify that the path for a commit editor has the expected suffix.
18 18
19 19 $ hg commit
20 20 *.commit.hg.txt (glob)
21 21 abort: edit failed: editor.sh exited with status 1
22 22 [255]
23 23
24 24 Verify that the path for a histedit editor has the expected suffix.
25 25
26 26 $ cat >> $HGRCPATH <<EOF
27 27 > [extensions]
28 28 > rebase=
29 29 > histedit=
30 30 > EOF
31 31 $ hg commit --message 'At least one commit for histedit.'
32 32 $ hg histedit
33 33 *.histedit.hg.txt (glob)
34 34 abort: edit failed: editor.sh exited with status 1
35 35 [255]
36
37 Verify that when performing an action that has the side-effect of creating an
38 editor for a diff, the file ends in .diff.
39
40 $ echo 1 > one
41 $ echo 2 > two
42 $ hg add
43 adding one
44 adding two
45 $ hg commit --interactive --config ui.interactive=true --config ui.interface=text << EOF
46 > y
47 > e
48 > q
49 > EOF
50 diff --git a/one b/one
51 new file mode 100644
52 examine changes to 'one'? [Ynesfdaq?] y
53
54 @@ -0,0 +1,1 @@
55 +1
56 record change 1/2 to 'one'? [Ynesfdaq?] e
57
58 *.diff (glob)
59 editor exited with exit code 1
60 record change 1/2 to 'one'? [Ynesfdaq?] q
61
62 abort: user quit
63 [255]
General Comments 0
You need to be logged in to leave comments. Login now