##// END OF EJS Templates
ui: add logging hook
Matt Mackall -
r11984:2db0fccc default
parent child Browse files
Show More
@@ -1,606 +1,615 b''
1 1 # ui.py - user interface bits for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 import errno, getpass, os, socket, sys, tempfile, traceback
10 10 import config, util, error
11 11
12 12 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True,
13 13 '0': False, 'no': False, 'false': False, 'off': False}
14 14
15 15 class ui(object):
16 16 def __init__(self, src=None):
17 17 self._buffers = []
18 18 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
19 19 self._reportuntrusted = True
20 20 self._ocfg = config.config() # overlay
21 21 self._tcfg = config.config() # trusted
22 22 self._ucfg = config.config() # untrusted
23 23 self._trustusers = set()
24 24 self._trustgroups = set()
25 25
26 26 if src:
27 27 self._tcfg = src._tcfg.copy()
28 28 self._ucfg = src._ucfg.copy()
29 29 self._ocfg = src._ocfg.copy()
30 30 self._trustusers = src._trustusers.copy()
31 31 self._trustgroups = src._trustgroups.copy()
32 32 self.environ = src.environ
33 33 self.fixconfig()
34 34 else:
35 35 # shared read-only environment
36 36 self.environ = os.environ
37 37 # we always trust global config files
38 38 for f in util.rcpath():
39 39 self.readconfig(f, trust=True)
40 40
41 41 def copy(self):
42 42 return self.__class__(self)
43 43
44 44 def _is_trusted(self, fp, f):
45 45 st = util.fstat(fp)
46 46 if util.isowner(st):
47 47 return True
48 48
49 49 tusers, tgroups = self._trustusers, self._trustgroups
50 50 if '*' in tusers or '*' in tgroups:
51 51 return True
52 52
53 53 user = util.username(st.st_uid)
54 54 group = util.groupname(st.st_gid)
55 55 if user in tusers or group in tgroups or user == util.username():
56 56 return True
57 57
58 58 if self._reportuntrusted:
59 59 self.warn(_('Not trusting file %s from untrusted '
60 60 'user %s, group %s\n') % (f, user, group))
61 61 return False
62 62
63 63 def readconfig(self, filename, root=None, trust=False,
64 64 sections=None, remap=None):
65 65 try:
66 66 fp = open(filename)
67 67 except IOError:
68 68 if not sections: # ignore unless we were looking for something
69 69 return
70 70 raise
71 71
72 72 cfg = config.config()
73 73 trusted = sections or trust or self._is_trusted(fp, filename)
74 74
75 75 try:
76 76 cfg.read(filename, fp, sections=sections, remap=remap)
77 77 except error.ConfigError, inst:
78 78 if trusted:
79 79 raise
80 80 self.warn(_("Ignored: %s\n") % str(inst))
81 81
82 82 if self.plain():
83 83 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
84 84 'logtemplate', 'style',
85 85 'traceback', 'verbose'):
86 86 if k in cfg['ui']:
87 87 del cfg['ui'][k]
88 88 for k, v in cfg.items('alias'):
89 89 del cfg['alias'][k]
90 90 for k, v in cfg.items('defaults'):
91 91 del cfg['defaults'][k]
92 92
93 93 if trusted:
94 94 self._tcfg.update(cfg)
95 95 self._tcfg.update(self._ocfg)
96 96 self._ucfg.update(cfg)
97 97 self._ucfg.update(self._ocfg)
98 98
99 99 if root is None:
100 100 root = os.path.expanduser('~')
101 101 self.fixconfig(root=root)
102 102
103 103 def fixconfig(self, root=None):
104 104 # translate paths relative to root (or home) into absolute paths
105 105 root = root or os.getcwd()
106 106 for c in self._tcfg, self._ucfg, self._ocfg:
107 107 for n, p in c.items('paths'):
108 108 if p and "://" not in p and not os.path.isabs(p):
109 109 c.set("paths", n, os.path.normpath(os.path.join(root, p)))
110 110
111 111 # update ui options
112 112 self.debugflag = self.configbool('ui', 'debug')
113 113 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
114 114 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
115 115 if self.verbose and self.quiet:
116 116 self.quiet = self.verbose = False
117 117 self._reportuntrusted = self.configbool("ui", "report_untrusted", True)
118 118 self.tracebackflag = self.configbool('ui', 'traceback', False)
119 119
120 120 # update trust information
121 121 self._trustusers.update(self.configlist('trusted', 'users'))
122 122 self._trustgroups.update(self.configlist('trusted', 'groups'))
123 123
124 124 def setconfig(self, section, name, value, overlay=True):
125 125 if overlay:
126 126 self._ocfg.set(section, name, value)
127 127 self._tcfg.set(section, name, value)
128 128 self._ucfg.set(section, name, value)
129 129 self.fixconfig()
130 130
131 131 def _data(self, untrusted):
132 132 return untrusted and self._ucfg or self._tcfg
133 133
134 134 def configsource(self, section, name, untrusted=False):
135 135 return self._data(untrusted).source(section, name) or 'none'
136 136
137 137 def config(self, section, name, default=None, untrusted=False):
138 138 value = self._data(untrusted).get(section, name, default)
139 139 if self.debugflag and not untrusted and self._reportuntrusted:
140 140 uvalue = self._ucfg.get(section, name)
141 141 if uvalue is not None and uvalue != value:
142 142 self.debug(_("ignoring untrusted configuration option "
143 143 "%s.%s = %s\n") % (section, name, uvalue))
144 144 return value
145 145
146 146 def configbool(self, section, name, default=False, untrusted=False):
147 147 v = self.config(section, name, None, untrusted)
148 148 if v is None:
149 149 return default
150 150 if isinstance(v, bool):
151 151 return v
152 152 if v.lower() not in _booleans:
153 153 raise error.ConfigError(_("%s.%s not a boolean ('%s')")
154 154 % (section, name, v))
155 155 return _booleans[v.lower()]
156 156
157 157 def configlist(self, section, name, default=None, untrusted=False):
158 158 """Return a list of comma/space separated strings"""
159 159
160 160 def _parse_plain(parts, s, offset):
161 161 whitespace = False
162 162 while offset < len(s) and (s[offset].isspace() or s[offset] == ','):
163 163 whitespace = True
164 164 offset += 1
165 165 if offset >= len(s):
166 166 return None, parts, offset
167 167 if whitespace:
168 168 parts.append('')
169 169 if s[offset] == '"' and not parts[-1]:
170 170 return _parse_quote, parts, offset + 1
171 171 elif s[offset] == '"' and parts[-1][-1] == '\\':
172 172 parts[-1] = parts[-1][:-1] + s[offset]
173 173 return _parse_plain, parts, offset + 1
174 174 parts[-1] += s[offset]
175 175 return _parse_plain, parts, offset + 1
176 176
177 177 def _parse_quote(parts, s, offset):
178 178 if offset < len(s) and s[offset] == '"': # ""
179 179 parts.append('')
180 180 offset += 1
181 181 while offset < len(s) and (s[offset].isspace() or
182 182 s[offset] == ','):
183 183 offset += 1
184 184 return _parse_plain, parts, offset
185 185
186 186 while offset < len(s) and s[offset] != '"':
187 187 if (s[offset] == '\\' and offset + 1 < len(s)
188 188 and s[offset + 1] == '"'):
189 189 offset += 1
190 190 parts[-1] += '"'
191 191 else:
192 192 parts[-1] += s[offset]
193 193 offset += 1
194 194
195 195 if offset >= len(s):
196 196 real_parts = _configlist(parts[-1])
197 197 if not real_parts:
198 198 parts[-1] = '"'
199 199 else:
200 200 real_parts[0] = '"' + real_parts[0]
201 201 parts = parts[:-1]
202 202 parts.extend(real_parts)
203 203 return None, parts, offset
204 204
205 205 offset += 1
206 206 while offset < len(s) and s[offset] in [' ', ',']:
207 207 offset += 1
208 208
209 209 if offset < len(s):
210 210 if offset + 1 == len(s) and s[offset] == '"':
211 211 parts[-1] += '"'
212 212 offset += 1
213 213 else:
214 214 parts.append('')
215 215 else:
216 216 return None, parts, offset
217 217
218 218 return _parse_plain, parts, offset
219 219
220 220 def _configlist(s):
221 221 s = s.rstrip(' ,')
222 222 if not s:
223 223 return []
224 224 parser, parts, offset = _parse_plain, [''], 0
225 225 while parser:
226 226 parser, parts, offset = parser(parts, s, offset)
227 227 return parts
228 228
229 229 result = self.config(section, name, untrusted=untrusted)
230 230 if result is None:
231 231 result = default or []
232 232 if isinstance(result, basestring):
233 233 result = _configlist(result.lstrip(' ,\n'))
234 234 if result is None:
235 235 result = default or []
236 236 return result
237 237
238 238 def has_section(self, section, untrusted=False):
239 239 '''tell whether section exists in config.'''
240 240 return section in self._data(untrusted)
241 241
242 242 def configitems(self, section, untrusted=False):
243 243 items = self._data(untrusted).items(section)
244 244 if self.debugflag and not untrusted and self._reportuntrusted:
245 245 for k, v in self._ucfg.items(section):
246 246 if self._tcfg.get(section, k) != v:
247 247 self.debug(_("ignoring untrusted configuration option "
248 248 "%s.%s = %s\n") % (section, k, v))
249 249 return items
250 250
251 251 def walkconfig(self, untrusted=False):
252 252 cfg = self._data(untrusted)
253 253 for section in cfg.sections():
254 254 for name, value in self.configitems(section, untrusted):
255 255 yield section, name, str(value).replace('\n', '\\n')
256 256
257 257 def plain(self):
258 258 '''is plain mode active?
259 259
260 260 Plain mode means that all configuration variables which affect the
261 261 behavior and output of Mercurial should be ignored. Additionally, the
262 262 output should be stable, reproducible and suitable for use in scripts or
263 263 applications.
264 264
265 265 The only way to trigger plain mode is by setting the `HGPLAIN'
266 266 environment variable.
267 267 '''
268 268 return 'HGPLAIN' in os.environ
269 269
270 270 def username(self):
271 271 """Return default username to be used in commits.
272 272
273 273 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
274 274 and stop searching if one of these is set.
275 275 If not found and ui.askusername is True, ask the user, else use
276 276 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
277 277 """
278 278 user = os.environ.get("HGUSER")
279 279 if user is None:
280 280 user = self.config("ui", "username")
281 281 if user is not None:
282 282 user = os.path.expandvars(user)
283 283 if user is None:
284 284 user = os.environ.get("EMAIL")
285 285 if user is None and self.configbool("ui", "askusername"):
286 286 user = self.prompt(_("enter a commit username:"), default=None)
287 287 if user is None and not self.interactive():
288 288 try:
289 289 user = '%s@%s' % (util.getuser(), socket.getfqdn())
290 290 self.warn(_("No username found, using '%s' instead\n") % user)
291 291 except KeyError:
292 292 pass
293 293 if not user:
294 294 raise util.Abort(_('no username supplied (see "hg help config")'))
295 295 if "\n" in user:
296 296 raise util.Abort(_("username %s contains a newline\n") % repr(user))
297 297 return user
298 298
299 299 def shortuser(self, user):
300 300 """Return a short representation of a user name or email address."""
301 301 if not self.verbose:
302 302 user = util.shortuser(user)
303 303 return user
304 304
305 305 def _path(self, loc):
306 306 p = self.config('paths', loc)
307 307 if p:
308 308 if '%%' in p:
309 309 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n") %
310 310 (loc, p, self.configsource('paths', loc)))
311 311 p = p.replace('%%', '%')
312 312 p = util.expandpath(p)
313 313 return p
314 314
315 315 def expandpath(self, loc, default=None):
316 316 """Return repository location relative to cwd or from [paths]"""
317 317 if "://" in loc or os.path.isdir(os.path.join(loc, '.hg')):
318 318 return loc
319 319
320 320 path = self._path(loc)
321 321 if not path and default is not None:
322 322 path = self._path(default)
323 323 return path or loc
324 324
325 325 def pushbuffer(self):
326 326 self._buffers.append([])
327 327
328 328 def popbuffer(self, labeled=False):
329 329 '''pop the last buffer and return the buffered output
330 330
331 331 If labeled is True, any labels associated with buffered
332 332 output will be handled. By default, this has no effect
333 333 on the output returned, but extensions and GUI tools may
334 334 handle this argument and returned styled output. If output
335 335 is being buffered so it can be captured and parsed or
336 336 processed, labeled should not be set to True.
337 337 '''
338 338 return "".join(self._buffers.pop())
339 339
340 340 def write(self, *args, **opts):
341 341 '''write args to output
342 342
343 343 By default, this method simply writes to the buffer or stdout,
344 344 but extensions or GUI tools may override this method,
345 345 write_err(), popbuffer(), and label() to style output from
346 346 various parts of hg.
347 347
348 348 An optional keyword argument, "label", can be passed in.
349 349 This should be a string containing label names separated by
350 350 space. Label names take the form of "topic.type". For example,
351 351 ui.debug() issues a label of "ui.debug".
352 352
353 353 When labeling output for a specific command, a label of
354 354 "cmdname.type" is recommended. For example, status issues
355 355 a label of "status.modified" for modified files.
356 356 '''
357 357 if self._buffers:
358 358 self._buffers[-1].extend([str(a) for a in args])
359 359 else:
360 360 for a in args:
361 361 sys.stdout.write(str(a))
362 362
363 363 def write_err(self, *args, **opts):
364 364 try:
365 365 if not getattr(sys.stdout, 'closed', False):
366 366 sys.stdout.flush()
367 367 for a in args:
368 368 sys.stderr.write(str(a))
369 369 # stderr may be buffered under win32 when redirected to files,
370 370 # including stdout.
371 371 if not getattr(sys.stderr, 'closed', False):
372 372 sys.stderr.flush()
373 373 except IOError, inst:
374 374 if inst.errno not in (errno.EPIPE, errno.EIO):
375 375 raise
376 376
377 377 def flush(self):
378 378 try: sys.stdout.flush()
379 379 except: pass
380 380 try: sys.stderr.flush()
381 381 except: pass
382 382
383 383 def interactive(self):
384 384 '''is interactive input allowed?
385 385
386 386 An interactive session is a session where input can be reasonably read
387 387 from `sys.stdin'. If this function returns false, any attempt to read
388 388 from stdin should fail with an error, unless a sensible default has been
389 389 specified.
390 390
391 391 Interactiveness is triggered by the value of the `ui.interactive'
392 392 configuration variable or - if it is unset - when `sys.stdin' points
393 393 to a terminal device.
394 394
395 395 This function refers to input only; for output, see `ui.formatted()'.
396 396 '''
397 397 i = self.configbool("ui", "interactive", None)
398 398 if i is None:
399 399 try:
400 400 return sys.stdin.isatty()
401 401 except AttributeError:
402 402 # some environments replace stdin without implementing isatty
403 403 # usually those are non-interactive
404 404 return False
405 405
406 406 return i
407 407
408 408 def formatted(self):
409 409 '''should formatted output be used?
410 410
411 411 It is often desirable to format the output to suite the output medium.
412 412 Examples of this are truncating long lines or colorizing messages.
413 413 However, this is not often not desirable when piping output into other
414 414 utilities, e.g. `grep'.
415 415
416 416 Formatted output is triggered by the value of the `ui.formatted'
417 417 configuration variable or - if it is unset - when `sys.stdout' points
418 418 to a terminal device. Please note that `ui.formatted' should be
419 419 considered an implementation detail; it is not intended for use outside
420 420 Mercurial or its extensions.
421 421
422 422 This function refers to output only; for input, see `ui.interactive()'.
423 423 This function always returns false when in plain mode, see `ui.plain()'.
424 424 '''
425 425 if self.plain():
426 426 return False
427 427
428 428 i = self.configbool("ui", "formatted", None)
429 429 if i is None:
430 430 try:
431 431 return sys.stdout.isatty()
432 432 except AttributeError:
433 433 # some environments replace stdout without implementing isatty
434 434 # usually those are non-interactive
435 435 return False
436 436
437 437 return i
438 438
439 439 def _readline(self, prompt=''):
440 440 if sys.stdin.isatty():
441 441 try:
442 442 # magically add command line editing support, where
443 443 # available
444 444 import readline
445 445 # force demandimport to really load the module
446 446 readline.read_history_file
447 447 # windows sometimes raises something other than ImportError
448 448 except Exception:
449 449 pass
450 450 line = raw_input(prompt)
451 451 # When stdin is in binary mode on Windows, it can cause
452 452 # raw_input() to emit an extra trailing carriage return
453 453 if os.linesep == '\r\n' and line and line[-1] == '\r':
454 454 line = line[:-1]
455 455 return line
456 456
457 457 def prompt(self, msg, default="y"):
458 458 """Prompt user with msg, read response.
459 459 If ui is not interactive, the default is returned.
460 460 """
461 461 if not self.interactive():
462 462 self.write(msg, ' ', default, "\n")
463 463 return default
464 464 try:
465 465 r = self._readline(msg + ' ')
466 466 if not r:
467 467 return default
468 468 return r
469 469 except EOFError:
470 470 raise util.Abort(_('response expected'))
471 471
472 472 def promptchoice(self, msg, choices, default=0):
473 473 """Prompt user with msg, read response, and ensure it matches
474 474 one of the provided choices. The index of the choice is returned.
475 475 choices is a sequence of acceptable responses with the format:
476 476 ('&None', 'E&xec', 'Sym&link') Responses are case insensitive.
477 477 If ui is not interactive, the default is returned.
478 478 """
479 479 resps = [s[s.index('&')+1].lower() for s in choices]
480 480 while True:
481 481 r = self.prompt(msg, resps[default])
482 482 if r.lower() in resps:
483 483 return resps.index(r.lower())
484 484 self.write(_("unrecognized response\n"))
485 485
486 486 def getpass(self, prompt=None, default=None):
487 487 if not self.interactive():
488 488 return default
489 489 try:
490 490 return getpass.getpass(prompt or _('password: '))
491 491 except EOFError:
492 492 raise util.Abort(_('response expected'))
493 493 def status(self, *msg, **opts):
494 494 '''write status message to output (if ui.quiet is False)
495 495
496 496 This adds an output label of "ui.status".
497 497 '''
498 498 if not self.quiet:
499 499 opts['label'] = opts.get('label', '') + ' ui.status'
500 500 self.write(*msg, **opts)
501 501 def warn(self, *msg, **opts):
502 502 '''write warning message to output (stderr)
503 503
504 504 This adds an output label of "ui.warning".
505 505 '''
506 506 opts['label'] = opts.get('label', '') + ' ui.warning'
507 507 self.write_err(*msg, **opts)
508 508 def note(self, *msg, **opts):
509 509 '''write note to output (if ui.verbose is True)
510 510
511 511 This adds an output label of "ui.note".
512 512 '''
513 513 if self.verbose:
514 514 opts['label'] = opts.get('label', '') + ' ui.note'
515 515 self.write(*msg, **opts)
516 516 def debug(self, *msg, **opts):
517 517 '''write debug message to output (if ui.debugflag is True)
518 518
519 519 This adds an output label of "ui.debug".
520 520 '''
521 521 if self.debugflag:
522 522 opts['label'] = opts.get('label', '') + ' ui.debug'
523 523 self.write(*msg, **opts)
524 524 def edit(self, text, user):
525 525 (fd, name) = tempfile.mkstemp(prefix="hg-editor-", suffix=".txt",
526 526 text=True)
527 527 try:
528 528 f = os.fdopen(fd, "w")
529 529 f.write(text)
530 530 f.close()
531 531
532 532 editor = self.geteditor()
533 533
534 534 util.system("%s \"%s\"" % (editor, name),
535 535 environ={'HGUSER': user},
536 536 onerr=util.Abort, errprefix=_("edit failed"))
537 537
538 538 f = open(name)
539 539 t = f.read()
540 540 f.close()
541 541 finally:
542 542 os.unlink(name)
543 543
544 544 return t
545 545
546 546 def traceback(self, exc=None):
547 547 '''print exception traceback if traceback printing enabled.
548 548 only to call in exception handler. returns true if traceback
549 549 printed.'''
550 550 if self.tracebackflag:
551 551 if exc:
552 552 traceback.print_exception(exc[0], exc[1], exc[2])
553 553 else:
554 554 traceback.print_exc()
555 555 return self.tracebackflag
556 556
557 557 def geteditor(self):
558 558 '''return editor to use'''
559 559 return (os.environ.get("HGEDITOR") or
560 560 self.config("ui", "editor") or
561 561 os.environ.get("VISUAL") or
562 562 os.environ.get("EDITOR", "vi"))
563 563
564 564 def progress(self, topic, pos, item="", unit="", total=None):
565 565 '''show a progress message
566 566
567 567 With stock hg, this is simply a debug message that is hidden
568 568 by default, but with extensions or GUI tools it may be
569 569 visible. 'topic' is the current operation, 'item' is a
570 570 non-numeric marker of the current position (ie the currently
571 571 in-process file), 'pos' is the current numeric position (ie
572 572 revision, bytes, etc.), unit is a corresponding unit label,
573 573 and total is the highest expected pos.
574 574
575 575 Multiple nested topics may be active at a time.
576 576
577 577 All topics should be marked closed by setting pos to None at
578 578 termination.
579 579 '''
580 580
581 581 if pos == None or not self.debugflag:
582 582 return
583 583
584 584 if unit:
585 585 unit = ' ' + unit
586 586 if item:
587 587 item = ' ' + item
588 588
589 589 if total:
590 590 pct = 100.0 * pos / total
591 591 self.debug('%s:%s %s/%s%s (%4.2f%%)\n'
592 592 % (topic, item, pos, total, unit, pct))
593 593 else:
594 594 self.debug('%s:%s %s%s\n' % (topic, item, pos, unit))
595 595
596 def log(self, service, message):
597 '''hook for logging facility extensions
598
599 service should be a readily-identifiable subsystem, which will
600 allow filtering.
601 message should be a newline-terminated string to log.
602 '''
603 pass
604
596 605 def label(self, msg, label):
597 606 '''style msg based on supplied label
598 607
599 608 Like ui.write(), this just returns msg unchanged, but extensions
600 609 and GUI tools can override it to allow styling output without
601 610 writing it.
602 611
603 612 ui.write(s, 'label') is equivalent to
604 613 ui.write(ui.label(s, 'label')).
605 614 '''
606 615 return msg
General Comments 0
You need to be logged in to leave comments. Login now