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