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