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