##// END OF EJS Templates
typing: disable module attribute warnings for properly conditionalized code...
Matt Harbison -
r47544:52528570 stable
parent child Browse files
Show More
@@ -1,1375 +1,1379 b''
1 # dispatch.py - command dispatching for mercurial
1 # dispatch.py - command dispatching for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import errno
10 import errno
11 import getopt
11 import getopt
12 import io
12 import io
13 import os
13 import os
14 import pdb
14 import pdb
15 import re
15 import re
16 import signal
16 import signal
17 import sys
17 import sys
18 import traceback
18 import traceback
19
19
20
20
21 from .i18n import _
21 from .i18n import _
22 from .pycompat import getattr
22 from .pycompat import getattr
23
23
24 from hgdemandimport import tracing
24 from hgdemandimport import tracing
25
25
26 from . import (
26 from . import (
27 cmdutil,
27 cmdutil,
28 color,
28 color,
29 commands,
29 commands,
30 demandimport,
30 demandimport,
31 encoding,
31 encoding,
32 error,
32 error,
33 extensions,
33 extensions,
34 fancyopts,
34 fancyopts,
35 help,
35 help,
36 hg,
36 hg,
37 hook,
37 hook,
38 localrepo,
38 localrepo,
39 profiling,
39 profiling,
40 pycompat,
40 pycompat,
41 rcutil,
41 rcutil,
42 registrar,
42 registrar,
43 requirements as requirementsmod,
43 requirements as requirementsmod,
44 scmutil,
44 scmutil,
45 ui as uimod,
45 ui as uimod,
46 util,
46 util,
47 vfs,
47 vfs,
48 )
48 )
49
49
50 from .utils import (
50 from .utils import (
51 procutil,
51 procutil,
52 stringutil,
52 stringutil,
53 )
53 )
54
54
55
55
56 class request(object):
56 class request(object):
57 def __init__(
57 def __init__(
58 self,
58 self,
59 args,
59 args,
60 ui=None,
60 ui=None,
61 repo=None,
61 repo=None,
62 fin=None,
62 fin=None,
63 fout=None,
63 fout=None,
64 ferr=None,
64 ferr=None,
65 fmsg=None,
65 fmsg=None,
66 prereposetups=None,
66 prereposetups=None,
67 ):
67 ):
68 self.args = args
68 self.args = args
69 self.ui = ui
69 self.ui = ui
70 self.repo = repo
70 self.repo = repo
71
71
72 # input/output/error streams
72 # input/output/error streams
73 self.fin = fin
73 self.fin = fin
74 self.fout = fout
74 self.fout = fout
75 self.ferr = ferr
75 self.ferr = ferr
76 # separate stream for status/error messages
76 # separate stream for status/error messages
77 self.fmsg = fmsg
77 self.fmsg = fmsg
78
78
79 # remember options pre-parsed by _earlyparseopts()
79 # remember options pre-parsed by _earlyparseopts()
80 self.earlyoptions = {}
80 self.earlyoptions = {}
81
81
82 # reposetups which run before extensions, useful for chg to pre-fill
82 # reposetups which run before extensions, useful for chg to pre-fill
83 # low-level repo state (for example, changelog) before extensions.
83 # low-level repo state (for example, changelog) before extensions.
84 self.prereposetups = prereposetups or []
84 self.prereposetups = prereposetups or []
85
85
86 # store the parsed and canonical command
86 # store the parsed and canonical command
87 self.canonical_command = None
87 self.canonical_command = None
88
88
89 def _runexithandlers(self):
89 def _runexithandlers(self):
90 exc = None
90 exc = None
91 handlers = self.ui._exithandlers
91 handlers = self.ui._exithandlers
92 try:
92 try:
93 while handlers:
93 while handlers:
94 func, args, kwargs = handlers.pop()
94 func, args, kwargs = handlers.pop()
95 try:
95 try:
96 func(*args, **kwargs)
96 func(*args, **kwargs)
97 except: # re-raises below
97 except: # re-raises below
98 if exc is None:
98 if exc is None:
99 exc = sys.exc_info()[1]
99 exc = sys.exc_info()[1]
100 self.ui.warnnoi18n(b'error in exit handlers:\n')
100 self.ui.warnnoi18n(b'error in exit handlers:\n')
101 self.ui.traceback(force=True)
101 self.ui.traceback(force=True)
102 finally:
102 finally:
103 if exc is not None:
103 if exc is not None:
104 raise exc
104 raise exc
105
105
106
106
107 def _flushstdio(ui, err):
107 def _flushstdio(ui, err):
108 status = None
108 status = None
109 # In all cases we try to flush stdio streams.
109 # In all cases we try to flush stdio streams.
110 if util.safehasattr(ui, b'fout'):
110 if util.safehasattr(ui, b'fout'):
111 assert ui is not None # help pytype
111 assert ui is not None # help pytype
112 assert ui.fout is not None # help pytype
112 assert ui.fout is not None # help pytype
113 try:
113 try:
114 ui.fout.flush()
114 ui.fout.flush()
115 except IOError as e:
115 except IOError as e:
116 err = e
116 err = e
117 status = -1
117 status = -1
118
118
119 if util.safehasattr(ui, b'ferr'):
119 if util.safehasattr(ui, b'ferr'):
120 assert ui is not None # help pytype
120 assert ui is not None # help pytype
121 assert ui.ferr is not None # help pytype
121 assert ui.ferr is not None # help pytype
122 try:
122 try:
123 if err is not None and err.errno != errno.EPIPE:
123 if err is not None and err.errno != errno.EPIPE:
124 ui.ferr.write(
124 ui.ferr.write(
125 b'abort: %s\n' % encoding.strtolocal(err.strerror)
125 b'abort: %s\n' % encoding.strtolocal(err.strerror)
126 )
126 )
127 ui.ferr.flush()
127 ui.ferr.flush()
128 # There's not much we can do about an I/O error here. So (possibly)
128 # There's not much we can do about an I/O error here. So (possibly)
129 # change the status code and move on.
129 # change the status code and move on.
130 except IOError:
130 except IOError:
131 status = -1
131 status = -1
132
132
133 return status
133 return status
134
134
135
135
136 def run():
136 def run():
137 """run the command in sys.argv"""
137 """run the command in sys.argv"""
138 try:
138 try:
139 initstdio()
139 initstdio()
140 with tracing.log('parse args into request'):
140 with tracing.log('parse args into request'):
141 req = request(pycompat.sysargv[1:])
141 req = request(pycompat.sysargv[1:])
142
142
143 status = dispatch(req)
143 status = dispatch(req)
144 _silencestdio()
144 _silencestdio()
145 except KeyboardInterrupt:
145 except KeyboardInterrupt:
146 # Catch early/late KeyboardInterrupt as last ditch. Here nothing will
146 # Catch early/late KeyboardInterrupt as last ditch. Here nothing will
147 # be printed to console to avoid another IOError/KeyboardInterrupt.
147 # be printed to console to avoid another IOError/KeyboardInterrupt.
148 status = -1
148 status = -1
149 sys.exit(status & 255)
149 sys.exit(status & 255)
150
150
151
151
152 if pycompat.ispy3:
152 if pycompat.ispy3:
153
153
154 def initstdio():
154 def initstdio():
155 # stdio streams on Python 3 are io.TextIOWrapper instances proxying another
155 # stdio streams on Python 3 are io.TextIOWrapper instances proxying another
156 # buffer. These streams will normalize \n to \r\n by default. Mercurial's
156 # buffer. These streams will normalize \n to \r\n by default. Mercurial's
157 # preferred mechanism for writing output (ui.write()) uses io.BufferedWriter
157 # preferred mechanism for writing output (ui.write()) uses io.BufferedWriter
158 # instances, which write to the underlying stdio file descriptor in binary
158 # instances, which write to the underlying stdio file descriptor in binary
159 # mode. ui.write() uses \n for line endings and no line ending normalization
159 # mode. ui.write() uses \n for line endings and no line ending normalization
160 # is attempted through this interface. This "just works," even if the system
160 # is attempted through this interface. This "just works," even if the system
161 # preferred line ending is not \n.
161 # preferred line ending is not \n.
162 #
162 #
163 # But some parts of Mercurial (e.g. hooks) can still send data to sys.stdout
163 # But some parts of Mercurial (e.g. hooks) can still send data to sys.stdout
164 # and sys.stderr. They will inherit the line ending normalization settings,
164 # and sys.stderr. They will inherit the line ending normalization settings,
165 # potentially causing e.g. \r\n to be emitted. Since emitting \n should
165 # potentially causing e.g. \r\n to be emitted. Since emitting \n should
166 # "just work," here we change the sys.* streams to disable line ending
166 # "just work," here we change the sys.* streams to disable line ending
167 # normalization, ensuring compatibility with our ui type.
167 # normalization, ensuring compatibility with our ui type.
168
168
169 if sys.stdout is not None:
169 if sys.stdout is not None:
170 # write_through is new in Python 3.7.
170 # write_through is new in Python 3.7.
171 kwargs = {
171 kwargs = {
172 "newline": "\n",
172 "newline": "\n",
173 "line_buffering": sys.stdout.line_buffering,
173 "line_buffering": sys.stdout.line_buffering,
174 }
174 }
175 if util.safehasattr(sys.stdout, "write_through"):
175 if util.safehasattr(sys.stdout, "write_through"):
176 # pytype: disable=attribute-error
176 kwargs["write_through"] = sys.stdout.write_through
177 kwargs["write_through"] = sys.stdout.write_through
178 # pytype: enable=attribute-error
177 sys.stdout = io.TextIOWrapper(
179 sys.stdout = io.TextIOWrapper(
178 sys.stdout.buffer,
180 sys.stdout.buffer,
179 sys.stdout.encoding,
181 sys.stdout.encoding,
180 sys.stdout.errors,
182 sys.stdout.errors,
181 **kwargs
183 **kwargs
182 )
184 )
183
185
184 if sys.stderr is not None:
186 if sys.stderr is not None:
185 kwargs = {
187 kwargs = {
186 "newline": "\n",
188 "newline": "\n",
187 "line_buffering": sys.stderr.line_buffering,
189 "line_buffering": sys.stderr.line_buffering,
188 }
190 }
189 if util.safehasattr(sys.stderr, "write_through"):
191 if util.safehasattr(sys.stderr, "write_through"):
192 # pytype: disable=attribute-error
190 kwargs["write_through"] = sys.stderr.write_through
193 kwargs["write_through"] = sys.stderr.write_through
194 # pytype: enable=attribute-error
191 sys.stderr = io.TextIOWrapper(
195 sys.stderr = io.TextIOWrapper(
192 sys.stderr.buffer,
196 sys.stderr.buffer,
193 sys.stderr.encoding,
197 sys.stderr.encoding,
194 sys.stderr.errors,
198 sys.stderr.errors,
195 **kwargs
199 **kwargs
196 )
200 )
197
201
198 if sys.stdin is not None:
202 if sys.stdin is not None:
199 # No write_through on read-only stream.
203 # No write_through on read-only stream.
200 sys.stdin = io.TextIOWrapper(
204 sys.stdin = io.TextIOWrapper(
201 sys.stdin.buffer,
205 sys.stdin.buffer,
202 sys.stdin.encoding,
206 sys.stdin.encoding,
203 sys.stdin.errors,
207 sys.stdin.errors,
204 # None is universal newlines mode.
208 # None is universal newlines mode.
205 newline=None,
209 newline=None,
206 line_buffering=sys.stdin.line_buffering,
210 line_buffering=sys.stdin.line_buffering,
207 )
211 )
208
212
209 def _silencestdio():
213 def _silencestdio():
210 for fp in (sys.stdout, sys.stderr):
214 for fp in (sys.stdout, sys.stderr):
211 if fp is None:
215 if fp is None:
212 continue
216 continue
213 # Check if the file is okay
217 # Check if the file is okay
214 try:
218 try:
215 fp.flush()
219 fp.flush()
216 continue
220 continue
217 except IOError:
221 except IOError:
218 pass
222 pass
219 # Otherwise mark it as closed to silence "Exception ignored in"
223 # Otherwise mark it as closed to silence "Exception ignored in"
220 # message emitted by the interpreter finalizer.
224 # message emitted by the interpreter finalizer.
221 try:
225 try:
222 fp.close()
226 fp.close()
223 except IOError:
227 except IOError:
224 pass
228 pass
225
229
226
230
227 else:
231 else:
228
232
229 def initstdio():
233 def initstdio():
230 for fp in (sys.stdin, sys.stdout, sys.stderr):
234 for fp in (sys.stdin, sys.stdout, sys.stderr):
231 procutil.setbinary(fp)
235 procutil.setbinary(fp)
232
236
233 def _silencestdio():
237 def _silencestdio():
234 pass
238 pass
235
239
236
240
237 def _formatargs(args):
241 def _formatargs(args):
238 return b' '.join(procutil.shellquote(a) for a in args)
242 return b' '.join(procutil.shellquote(a) for a in args)
239
243
240
244
241 def dispatch(req):
245 def dispatch(req):
242 """run the command specified in req.args; returns an integer status code"""
246 """run the command specified in req.args; returns an integer status code"""
243 err = None
247 err = None
244 try:
248 try:
245 status = _rundispatch(req)
249 status = _rundispatch(req)
246 except error.StdioError as e:
250 except error.StdioError as e:
247 err = e
251 err = e
248 status = -1
252 status = -1
249
253
250 ret = _flushstdio(req.ui, err)
254 ret = _flushstdio(req.ui, err)
251 if ret:
255 if ret:
252 status = ret
256 status = ret
253 return status
257 return status
254
258
255
259
256 def _rundispatch(req):
260 def _rundispatch(req):
257 with tracing.log('dispatch._rundispatch'):
261 with tracing.log('dispatch._rundispatch'):
258 if req.ferr:
262 if req.ferr:
259 ferr = req.ferr
263 ferr = req.ferr
260 elif req.ui:
264 elif req.ui:
261 ferr = req.ui.ferr
265 ferr = req.ui.ferr
262 else:
266 else:
263 ferr = procutil.stderr
267 ferr = procutil.stderr
264
268
265 try:
269 try:
266 if not req.ui:
270 if not req.ui:
267 req.ui = uimod.ui.load()
271 req.ui = uimod.ui.load()
268 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
272 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
269 if req.earlyoptions[b'traceback']:
273 if req.earlyoptions[b'traceback']:
270 req.ui.setconfig(b'ui', b'traceback', b'on', b'--traceback')
274 req.ui.setconfig(b'ui', b'traceback', b'on', b'--traceback')
271
275
272 # set ui streams from the request
276 # set ui streams from the request
273 if req.fin:
277 if req.fin:
274 req.ui.fin = req.fin
278 req.ui.fin = req.fin
275 if req.fout:
279 if req.fout:
276 req.ui.fout = req.fout
280 req.ui.fout = req.fout
277 if req.ferr:
281 if req.ferr:
278 req.ui.ferr = req.ferr
282 req.ui.ferr = req.ferr
279 if req.fmsg:
283 if req.fmsg:
280 req.ui.fmsg = req.fmsg
284 req.ui.fmsg = req.fmsg
281 except error.Abort as inst:
285 except error.Abort as inst:
282 ferr.write(inst.format())
286 ferr.write(inst.format())
283 return -1
287 return -1
284
288
285 msg = _formatargs(req.args)
289 msg = _formatargs(req.args)
286 starttime = util.timer()
290 starttime = util.timer()
287 ret = 1 # default of Python exit code on unhandled exception
291 ret = 1 # default of Python exit code on unhandled exception
288 try:
292 try:
289 ret = _runcatch(req) or 0
293 ret = _runcatch(req) or 0
290 except error.ProgrammingError as inst:
294 except error.ProgrammingError as inst:
291 req.ui.error(_(b'** ProgrammingError: %s\n') % inst)
295 req.ui.error(_(b'** ProgrammingError: %s\n') % inst)
292 if inst.hint:
296 if inst.hint:
293 req.ui.error(_(b'** (%s)\n') % inst.hint)
297 req.ui.error(_(b'** (%s)\n') % inst.hint)
294 raise
298 raise
295 except KeyboardInterrupt as inst:
299 except KeyboardInterrupt as inst:
296 try:
300 try:
297 if isinstance(inst, error.SignalInterrupt):
301 if isinstance(inst, error.SignalInterrupt):
298 msg = _(b"killed!\n")
302 msg = _(b"killed!\n")
299 else:
303 else:
300 msg = _(b"interrupted!\n")
304 msg = _(b"interrupted!\n")
301 req.ui.error(msg)
305 req.ui.error(msg)
302 except error.SignalInterrupt:
306 except error.SignalInterrupt:
303 # maybe pager would quit without consuming all the output, and
307 # maybe pager would quit without consuming all the output, and
304 # SIGPIPE was raised. we cannot print anything in this case.
308 # SIGPIPE was raised. we cannot print anything in this case.
305 pass
309 pass
306 except IOError as inst:
310 except IOError as inst:
307 if inst.errno != errno.EPIPE:
311 if inst.errno != errno.EPIPE:
308 raise
312 raise
309 ret = -1
313 ret = -1
310 finally:
314 finally:
311 duration = util.timer() - starttime
315 duration = util.timer() - starttime
312 req.ui.flush() # record blocked times
316 req.ui.flush() # record blocked times
313 if req.ui.logblockedtimes:
317 if req.ui.logblockedtimes:
314 req.ui._blockedtimes[b'command_duration'] = duration * 1000
318 req.ui._blockedtimes[b'command_duration'] = duration * 1000
315 req.ui.log(
319 req.ui.log(
316 b'uiblocked',
320 b'uiblocked',
317 b'ui blocked ms\n',
321 b'ui blocked ms\n',
318 **pycompat.strkwargs(req.ui._blockedtimes)
322 **pycompat.strkwargs(req.ui._blockedtimes)
319 )
323 )
320 return_code = ret & 255
324 return_code = ret & 255
321 req.ui.log(
325 req.ui.log(
322 b"commandfinish",
326 b"commandfinish",
323 b"%s exited %d after %0.2f seconds\n",
327 b"%s exited %d after %0.2f seconds\n",
324 msg,
328 msg,
325 return_code,
329 return_code,
326 duration,
330 duration,
327 return_code=return_code,
331 return_code=return_code,
328 duration=duration,
332 duration=duration,
329 canonical_command=req.canonical_command,
333 canonical_command=req.canonical_command,
330 )
334 )
331 try:
335 try:
332 req._runexithandlers()
336 req._runexithandlers()
333 except: # exiting, so no re-raises
337 except: # exiting, so no re-raises
334 ret = ret or -1
338 ret = ret or -1
335 # do flush again since ui.log() and exit handlers may write to ui
339 # do flush again since ui.log() and exit handlers may write to ui
336 req.ui.flush()
340 req.ui.flush()
337 return ret
341 return ret
338
342
339
343
340 def _runcatch(req):
344 def _runcatch(req):
341 with tracing.log('dispatch._runcatch'):
345 with tracing.log('dispatch._runcatch'):
342
346
343 def catchterm(*args):
347 def catchterm(*args):
344 raise error.SignalInterrupt
348 raise error.SignalInterrupt
345
349
346 ui = req.ui
350 ui = req.ui
347 try:
351 try:
348 for name in b'SIGBREAK', b'SIGHUP', b'SIGTERM':
352 for name in b'SIGBREAK', b'SIGHUP', b'SIGTERM':
349 num = getattr(signal, name, None)
353 num = getattr(signal, name, None)
350 if num:
354 if num:
351 signal.signal(num, catchterm)
355 signal.signal(num, catchterm)
352 except ValueError:
356 except ValueError:
353 pass # happens if called in a thread
357 pass # happens if called in a thread
354
358
355 def _runcatchfunc():
359 def _runcatchfunc():
356 realcmd = None
360 realcmd = None
357 try:
361 try:
358 cmdargs = fancyopts.fancyopts(
362 cmdargs = fancyopts.fancyopts(
359 req.args[:], commands.globalopts, {}
363 req.args[:], commands.globalopts, {}
360 )
364 )
361 cmd = cmdargs[0]
365 cmd = cmdargs[0]
362 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
366 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
363 realcmd = aliases[0]
367 realcmd = aliases[0]
364 except (
368 except (
365 error.UnknownCommand,
369 error.UnknownCommand,
366 error.AmbiguousCommand,
370 error.AmbiguousCommand,
367 IndexError,
371 IndexError,
368 getopt.GetoptError,
372 getopt.GetoptError,
369 ):
373 ):
370 # Don't handle this here. We know the command is
374 # Don't handle this here. We know the command is
371 # invalid, but all we're worried about for now is that
375 # invalid, but all we're worried about for now is that
372 # it's not a command that server operators expect to
376 # it's not a command that server operators expect to
373 # be safe to offer to users in a sandbox.
377 # be safe to offer to users in a sandbox.
374 pass
378 pass
375 if realcmd == b'serve' and b'--stdio' in cmdargs:
379 if realcmd == b'serve' and b'--stdio' in cmdargs:
376 # We want to constrain 'hg serve --stdio' instances pretty
380 # We want to constrain 'hg serve --stdio' instances pretty
377 # closely, as many shared-ssh access tools want to grant
381 # closely, as many shared-ssh access tools want to grant
378 # access to run *only* 'hg -R $repo serve --stdio'. We
382 # access to run *only* 'hg -R $repo serve --stdio'. We
379 # restrict to exactly that set of arguments, and prohibit
383 # restrict to exactly that set of arguments, and prohibit
380 # any repo name that starts with '--' to prevent
384 # any repo name that starts with '--' to prevent
381 # shenanigans wherein a user does something like pass
385 # shenanigans wherein a user does something like pass
382 # --debugger or --config=ui.debugger=1 as a repo
386 # --debugger or --config=ui.debugger=1 as a repo
383 # name. This used to actually run the debugger.
387 # name. This used to actually run the debugger.
384 if (
388 if (
385 len(req.args) != 4
389 len(req.args) != 4
386 or req.args[0] != b'-R'
390 or req.args[0] != b'-R'
387 or req.args[1].startswith(b'--')
391 or req.args[1].startswith(b'--')
388 or req.args[2] != b'serve'
392 or req.args[2] != b'serve'
389 or req.args[3] != b'--stdio'
393 or req.args[3] != b'--stdio'
390 ):
394 ):
391 raise error.Abort(
395 raise error.Abort(
392 _(b'potentially unsafe serve --stdio invocation: %s')
396 _(b'potentially unsafe serve --stdio invocation: %s')
393 % (stringutil.pprint(req.args),)
397 % (stringutil.pprint(req.args),)
394 )
398 )
395
399
396 try:
400 try:
397 debugger = b'pdb'
401 debugger = b'pdb'
398 debugtrace = {b'pdb': pdb.set_trace}
402 debugtrace = {b'pdb': pdb.set_trace}
399 debugmortem = {b'pdb': pdb.post_mortem}
403 debugmortem = {b'pdb': pdb.post_mortem}
400
404
401 # read --config before doing anything else
405 # read --config before doing anything else
402 # (e.g. to change trust settings for reading .hg/hgrc)
406 # (e.g. to change trust settings for reading .hg/hgrc)
403 cfgs = _parseconfig(req.ui, req.earlyoptions[b'config'])
407 cfgs = _parseconfig(req.ui, req.earlyoptions[b'config'])
404
408
405 if req.repo:
409 if req.repo:
406 # copy configs that were passed on the cmdline (--config) to
410 # copy configs that were passed on the cmdline (--config) to
407 # the repo ui
411 # the repo ui
408 for sec, name, val in cfgs:
412 for sec, name, val in cfgs:
409 req.repo.ui.setconfig(
413 req.repo.ui.setconfig(
410 sec, name, val, source=b'--config'
414 sec, name, val, source=b'--config'
411 )
415 )
412
416
413 # developer config: ui.debugger
417 # developer config: ui.debugger
414 debugger = ui.config(b"ui", b"debugger")
418 debugger = ui.config(b"ui", b"debugger")
415 debugmod = pdb
419 debugmod = pdb
416 if not debugger or ui.plain():
420 if not debugger or ui.plain():
417 # if we are in HGPLAIN mode, then disable custom debugging
421 # if we are in HGPLAIN mode, then disable custom debugging
418 debugger = b'pdb'
422 debugger = b'pdb'
419 elif req.earlyoptions[b'debugger']:
423 elif req.earlyoptions[b'debugger']:
420 # This import can be slow for fancy debuggers, so only
424 # This import can be slow for fancy debuggers, so only
421 # do it when absolutely necessary, i.e. when actual
425 # do it when absolutely necessary, i.e. when actual
422 # debugging has been requested
426 # debugging has been requested
423 with demandimport.deactivated():
427 with demandimport.deactivated():
424 try:
428 try:
425 debugmod = __import__(debugger)
429 debugmod = __import__(debugger)
426 except ImportError:
430 except ImportError:
427 pass # Leave debugmod = pdb
431 pass # Leave debugmod = pdb
428
432
429 debugtrace[debugger] = debugmod.set_trace
433 debugtrace[debugger] = debugmod.set_trace
430 debugmortem[debugger] = debugmod.post_mortem
434 debugmortem[debugger] = debugmod.post_mortem
431
435
432 # enter the debugger before command execution
436 # enter the debugger before command execution
433 if req.earlyoptions[b'debugger']:
437 if req.earlyoptions[b'debugger']:
434 ui.warn(
438 ui.warn(
435 _(
439 _(
436 b"entering debugger - "
440 b"entering debugger - "
437 b"type c to continue starting hg or h for help\n"
441 b"type c to continue starting hg or h for help\n"
438 )
442 )
439 )
443 )
440
444
441 if (
445 if (
442 debugger != b'pdb'
446 debugger != b'pdb'
443 and debugtrace[debugger] == debugtrace[b'pdb']
447 and debugtrace[debugger] == debugtrace[b'pdb']
444 ):
448 ):
445 ui.warn(
449 ui.warn(
446 _(
450 _(
447 b"%s debugger specified "
451 b"%s debugger specified "
448 b"but its module was not found\n"
452 b"but its module was not found\n"
449 )
453 )
450 % debugger
454 % debugger
451 )
455 )
452 with demandimport.deactivated():
456 with demandimport.deactivated():
453 debugtrace[debugger]()
457 debugtrace[debugger]()
454 try:
458 try:
455 return _dispatch(req)
459 return _dispatch(req)
456 finally:
460 finally:
457 ui.flush()
461 ui.flush()
458 except: # re-raises
462 except: # re-raises
459 # enter the debugger when we hit an exception
463 # enter the debugger when we hit an exception
460 if req.earlyoptions[b'debugger']:
464 if req.earlyoptions[b'debugger']:
461 traceback.print_exc()
465 traceback.print_exc()
462 debugmortem[debugger](sys.exc_info()[2])
466 debugmortem[debugger](sys.exc_info()[2])
463 raise
467 raise
464
468
465 return _callcatch(ui, _runcatchfunc)
469 return _callcatch(ui, _runcatchfunc)
466
470
467
471
468 def _callcatch(ui, func):
472 def _callcatch(ui, func):
469 """like scmutil.callcatch but handles more high-level exceptions about
473 """like scmutil.callcatch but handles more high-level exceptions about
470 config parsing and commands. besides, use handlecommandexception to handle
474 config parsing and commands. besides, use handlecommandexception to handle
471 uncaught exceptions.
475 uncaught exceptions.
472 """
476 """
473 detailed_exit_code = -1
477 detailed_exit_code = -1
474 try:
478 try:
475 return scmutil.callcatch(ui, func)
479 return scmutil.callcatch(ui, func)
476 except error.AmbiguousCommand as inst:
480 except error.AmbiguousCommand as inst:
477 detailed_exit_code = 10
481 detailed_exit_code = 10
478 ui.warn(
482 ui.warn(
479 _(b"hg: command '%s' is ambiguous:\n %s\n")
483 _(b"hg: command '%s' is ambiguous:\n %s\n")
480 % (inst.prefix, b" ".join(inst.matches))
484 % (inst.prefix, b" ".join(inst.matches))
481 )
485 )
482 except error.CommandError as inst:
486 except error.CommandError as inst:
483 detailed_exit_code = 10
487 detailed_exit_code = 10
484 if inst.command:
488 if inst.command:
485 ui.pager(b'help')
489 ui.pager(b'help')
486 msgbytes = pycompat.bytestr(inst.message)
490 msgbytes = pycompat.bytestr(inst.message)
487 ui.warn(_(b"hg %s: %s\n") % (inst.command, msgbytes))
491 ui.warn(_(b"hg %s: %s\n") % (inst.command, msgbytes))
488 commands.help_(ui, inst.command, full=False, command=True)
492 commands.help_(ui, inst.command, full=False, command=True)
489 else:
493 else:
490 ui.warn(_(b"hg: %s\n") % inst.message)
494 ui.warn(_(b"hg: %s\n") % inst.message)
491 ui.warn(_(b"(use 'hg help -v' for a list of global options)\n"))
495 ui.warn(_(b"(use 'hg help -v' for a list of global options)\n"))
492 except error.UnknownCommand as inst:
496 except error.UnknownCommand as inst:
493 detailed_exit_code = 10
497 detailed_exit_code = 10
494 nocmdmsg = _(b"hg: unknown command '%s'\n") % inst.command
498 nocmdmsg = _(b"hg: unknown command '%s'\n") % inst.command
495 try:
499 try:
496 # check if the command is in a disabled extension
500 # check if the command is in a disabled extension
497 # (but don't check for extensions themselves)
501 # (but don't check for extensions themselves)
498 formatted = help.formattedhelp(
502 formatted = help.formattedhelp(
499 ui, commands, inst.command, unknowncmd=True
503 ui, commands, inst.command, unknowncmd=True
500 )
504 )
501 ui.warn(nocmdmsg)
505 ui.warn(nocmdmsg)
502 ui.write(formatted)
506 ui.write(formatted)
503 except (error.UnknownCommand, error.Abort):
507 except (error.UnknownCommand, error.Abort):
504 suggested = False
508 suggested = False
505 if inst.all_commands:
509 if inst.all_commands:
506 sim = error.getsimilar(inst.all_commands, inst.command)
510 sim = error.getsimilar(inst.all_commands, inst.command)
507 if sim:
511 if sim:
508 ui.warn(nocmdmsg)
512 ui.warn(nocmdmsg)
509 ui.warn(b"(%s)\n" % error.similarity_hint(sim))
513 ui.warn(b"(%s)\n" % error.similarity_hint(sim))
510 suggested = True
514 suggested = True
511 if not suggested:
515 if not suggested:
512 ui.warn(nocmdmsg)
516 ui.warn(nocmdmsg)
513 ui.warn(_(b"(use 'hg help' for a list of commands)\n"))
517 ui.warn(_(b"(use 'hg help' for a list of commands)\n"))
514 except IOError:
518 except IOError:
515 raise
519 raise
516 except KeyboardInterrupt:
520 except KeyboardInterrupt:
517 raise
521 raise
518 except: # probably re-raises
522 except: # probably re-raises
519 if not handlecommandexception(ui):
523 if not handlecommandexception(ui):
520 raise
524 raise
521
525
522 if ui.configbool(b'ui', b'detailed-exit-code'):
526 if ui.configbool(b'ui', b'detailed-exit-code'):
523 return detailed_exit_code
527 return detailed_exit_code
524 else:
528 else:
525 return -1
529 return -1
526
530
527
531
528 def aliasargs(fn, givenargs):
532 def aliasargs(fn, givenargs):
529 args = []
533 args = []
530 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
534 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
531 if not util.safehasattr(fn, b'_origfunc'):
535 if not util.safehasattr(fn, b'_origfunc'):
532 args = getattr(fn, 'args', args)
536 args = getattr(fn, 'args', args)
533 if args:
537 if args:
534 cmd = b' '.join(map(procutil.shellquote, args))
538 cmd = b' '.join(map(procutil.shellquote, args))
535
539
536 nums = []
540 nums = []
537
541
538 def replacer(m):
542 def replacer(m):
539 num = int(m.group(1)) - 1
543 num = int(m.group(1)) - 1
540 nums.append(num)
544 nums.append(num)
541 if num < len(givenargs):
545 if num < len(givenargs):
542 return givenargs[num]
546 return givenargs[num]
543 raise error.InputError(_(b'too few arguments for command alias'))
547 raise error.InputError(_(b'too few arguments for command alias'))
544
548
545 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
549 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
546 givenargs = [x for i, x in enumerate(givenargs) if i not in nums]
550 givenargs = [x for i, x in enumerate(givenargs) if i not in nums]
547 args = pycompat.shlexsplit(cmd)
551 args = pycompat.shlexsplit(cmd)
548 return args + givenargs
552 return args + givenargs
549
553
550
554
551 def aliasinterpolate(name, args, cmd):
555 def aliasinterpolate(name, args, cmd):
552 """interpolate args into cmd for shell aliases
556 """interpolate args into cmd for shell aliases
553
557
554 This also handles $0, $@ and "$@".
558 This also handles $0, $@ and "$@".
555 """
559 """
556 # util.interpolate can't deal with "$@" (with quotes) because it's only
560 # util.interpolate can't deal with "$@" (with quotes) because it's only
557 # built to match prefix + patterns.
561 # built to match prefix + patterns.
558 replacemap = {b'$%d' % (i + 1): arg for i, arg in enumerate(args)}
562 replacemap = {b'$%d' % (i + 1): arg for i, arg in enumerate(args)}
559 replacemap[b'$0'] = name
563 replacemap[b'$0'] = name
560 replacemap[b'$$'] = b'$'
564 replacemap[b'$$'] = b'$'
561 replacemap[b'$@'] = b' '.join(args)
565 replacemap[b'$@'] = b' '.join(args)
562 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
566 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
563 # parameters, separated out into words. Emulate the same behavior here by
567 # parameters, separated out into words. Emulate the same behavior here by
564 # quoting the arguments individually. POSIX shells will then typically
568 # quoting the arguments individually. POSIX shells will then typically
565 # tokenize each argument into exactly one word.
569 # tokenize each argument into exactly one word.
566 replacemap[b'"$@"'] = b' '.join(procutil.shellquote(arg) for arg in args)
570 replacemap[b'"$@"'] = b' '.join(procutil.shellquote(arg) for arg in args)
567 # escape '\$' for regex
571 # escape '\$' for regex
568 regex = b'|'.join(replacemap.keys()).replace(b'$', br'\$')
572 regex = b'|'.join(replacemap.keys()).replace(b'$', br'\$')
569 r = re.compile(regex)
573 r = re.compile(regex)
570 return r.sub(lambda x: replacemap[x.group()], cmd)
574 return r.sub(lambda x: replacemap[x.group()], cmd)
571
575
572
576
573 class cmdalias(object):
577 class cmdalias(object):
574 def __init__(self, ui, name, definition, cmdtable, source):
578 def __init__(self, ui, name, definition, cmdtable, source):
575 self.name = self.cmd = name
579 self.name = self.cmd = name
576 self.cmdname = b''
580 self.cmdname = b''
577 self.definition = definition
581 self.definition = definition
578 self.fn = None
582 self.fn = None
579 self.givenargs = []
583 self.givenargs = []
580 self.opts = []
584 self.opts = []
581 self.help = b''
585 self.help = b''
582 self.badalias = None
586 self.badalias = None
583 self.unknowncmd = False
587 self.unknowncmd = False
584 self.source = source
588 self.source = source
585
589
586 try:
590 try:
587 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
591 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
588 for alias, e in pycompat.iteritems(cmdtable):
592 for alias, e in pycompat.iteritems(cmdtable):
589 if e is entry:
593 if e is entry:
590 self.cmd = alias
594 self.cmd = alias
591 break
595 break
592 self.shadows = True
596 self.shadows = True
593 except error.UnknownCommand:
597 except error.UnknownCommand:
594 self.shadows = False
598 self.shadows = False
595
599
596 if not self.definition:
600 if not self.definition:
597 self.badalias = _(b"no definition for alias '%s'") % self.name
601 self.badalias = _(b"no definition for alias '%s'") % self.name
598 return
602 return
599
603
600 if self.definition.startswith(b'!'):
604 if self.definition.startswith(b'!'):
601 shdef = self.definition[1:]
605 shdef = self.definition[1:]
602 self.shell = True
606 self.shell = True
603
607
604 def fn(ui, *args):
608 def fn(ui, *args):
605 env = {b'HG_ARGS': b' '.join((self.name,) + args)}
609 env = {b'HG_ARGS': b' '.join((self.name,) + args)}
606
610
607 def _checkvar(m):
611 def _checkvar(m):
608 if m.groups()[0] == b'$':
612 if m.groups()[0] == b'$':
609 return m.group()
613 return m.group()
610 elif int(m.groups()[0]) <= len(args):
614 elif int(m.groups()[0]) <= len(args):
611 return m.group()
615 return m.group()
612 else:
616 else:
613 ui.debug(
617 ui.debug(
614 b"No argument found for substitution "
618 b"No argument found for substitution "
615 b"of %i variable in alias '%s' definition.\n"
619 b"of %i variable in alias '%s' definition.\n"
616 % (int(m.groups()[0]), self.name)
620 % (int(m.groups()[0]), self.name)
617 )
621 )
618 return b''
622 return b''
619
623
620 cmd = re.sub(br'\$(\d+|\$)', _checkvar, shdef)
624 cmd = re.sub(br'\$(\d+|\$)', _checkvar, shdef)
621 cmd = aliasinterpolate(self.name, args, cmd)
625 cmd = aliasinterpolate(self.name, args, cmd)
622 return ui.system(
626 return ui.system(
623 cmd, environ=env, blockedtag=b'alias_%s' % self.name
627 cmd, environ=env, blockedtag=b'alias_%s' % self.name
624 )
628 )
625
629
626 self.fn = fn
630 self.fn = fn
627 self.alias = True
631 self.alias = True
628 self._populatehelp(ui, name, shdef, self.fn)
632 self._populatehelp(ui, name, shdef, self.fn)
629 return
633 return
630
634
631 try:
635 try:
632 args = pycompat.shlexsplit(self.definition)
636 args = pycompat.shlexsplit(self.definition)
633 except ValueError as inst:
637 except ValueError as inst:
634 self.badalias = _(b"error in definition for alias '%s': %s") % (
638 self.badalias = _(b"error in definition for alias '%s': %s") % (
635 self.name,
639 self.name,
636 stringutil.forcebytestr(inst),
640 stringutil.forcebytestr(inst),
637 )
641 )
638 return
642 return
639 earlyopts, args = _earlysplitopts(args)
643 earlyopts, args = _earlysplitopts(args)
640 if earlyopts:
644 if earlyopts:
641 self.badalias = _(
645 self.badalias = _(
642 b"error in definition for alias '%s': %s may "
646 b"error in definition for alias '%s': %s may "
643 b"only be given on the command line"
647 b"only be given on the command line"
644 ) % (self.name, b'/'.join(pycompat.ziplist(*earlyopts)[0]))
648 ) % (self.name, b'/'.join(pycompat.ziplist(*earlyopts)[0]))
645 return
649 return
646 self.cmdname = cmd = args.pop(0)
650 self.cmdname = cmd = args.pop(0)
647 self.givenargs = args
651 self.givenargs = args
648
652
649 try:
653 try:
650 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
654 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
651 if len(tableentry) > 2:
655 if len(tableentry) > 2:
652 self.fn, self.opts, cmdhelp = tableentry
656 self.fn, self.opts, cmdhelp = tableentry
653 else:
657 else:
654 self.fn, self.opts = tableentry
658 self.fn, self.opts = tableentry
655 cmdhelp = None
659 cmdhelp = None
656
660
657 self.alias = True
661 self.alias = True
658 self._populatehelp(ui, name, cmd, self.fn, cmdhelp)
662 self._populatehelp(ui, name, cmd, self.fn, cmdhelp)
659
663
660 except error.UnknownCommand:
664 except error.UnknownCommand:
661 self.badalias = _(
665 self.badalias = _(
662 b"alias '%s' resolves to unknown command '%s'"
666 b"alias '%s' resolves to unknown command '%s'"
663 ) % (
667 ) % (
664 self.name,
668 self.name,
665 cmd,
669 cmd,
666 )
670 )
667 self.unknowncmd = True
671 self.unknowncmd = True
668 except error.AmbiguousCommand:
672 except error.AmbiguousCommand:
669 self.badalias = _(
673 self.badalias = _(
670 b"alias '%s' resolves to ambiguous command '%s'"
674 b"alias '%s' resolves to ambiguous command '%s'"
671 ) % (
675 ) % (
672 self.name,
676 self.name,
673 cmd,
677 cmd,
674 )
678 )
675
679
676 def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None):
680 def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None):
677 # confine strings to be passed to i18n.gettext()
681 # confine strings to be passed to i18n.gettext()
678 cfg = {}
682 cfg = {}
679 for k in (b'doc', b'help', b'category'):
683 for k in (b'doc', b'help', b'category'):
680 v = ui.config(b'alias', b'%s:%s' % (name, k), None)
684 v = ui.config(b'alias', b'%s:%s' % (name, k), None)
681 if v is None:
685 if v is None:
682 continue
686 continue
683 if not encoding.isasciistr(v):
687 if not encoding.isasciistr(v):
684 self.badalias = _(
688 self.badalias = _(
685 b"non-ASCII character in alias definition '%s:%s'"
689 b"non-ASCII character in alias definition '%s:%s'"
686 ) % (name, k)
690 ) % (name, k)
687 return
691 return
688 cfg[k] = v
692 cfg[k] = v
689
693
690 self.help = cfg.get(b'help', defaulthelp or b'')
694 self.help = cfg.get(b'help', defaulthelp or b'')
691 if self.help and self.help.startswith(b"hg " + cmd):
695 if self.help and self.help.startswith(b"hg " + cmd):
692 # drop prefix in old-style help lines so hg shows the alias
696 # drop prefix in old-style help lines so hg shows the alias
693 self.help = self.help[4 + len(cmd) :]
697 self.help = self.help[4 + len(cmd) :]
694
698
695 self.owndoc = b'doc' in cfg
699 self.owndoc = b'doc' in cfg
696 doc = cfg.get(b'doc', pycompat.getdoc(fn))
700 doc = cfg.get(b'doc', pycompat.getdoc(fn))
697 if doc is not None:
701 if doc is not None:
698 doc = pycompat.sysstr(doc)
702 doc = pycompat.sysstr(doc)
699 self.__doc__ = doc
703 self.__doc__ = doc
700
704
701 self.helpcategory = cfg.get(
705 self.helpcategory = cfg.get(
702 b'category', registrar.command.CATEGORY_NONE
706 b'category', registrar.command.CATEGORY_NONE
703 )
707 )
704
708
705 @property
709 @property
706 def args(self):
710 def args(self):
707 args = pycompat.maplist(util.expandpath, self.givenargs)
711 args = pycompat.maplist(util.expandpath, self.givenargs)
708 return aliasargs(self.fn, args)
712 return aliasargs(self.fn, args)
709
713
710 def __getattr__(self, name):
714 def __getattr__(self, name):
711 adefaults = {
715 adefaults = {
712 'norepo': True,
716 'norepo': True,
713 'intents': set(),
717 'intents': set(),
714 'optionalrepo': False,
718 'optionalrepo': False,
715 'inferrepo': False,
719 'inferrepo': False,
716 }
720 }
717 if name not in adefaults:
721 if name not in adefaults:
718 raise AttributeError(name)
722 raise AttributeError(name)
719 if self.badalias or util.safehasattr(self, b'shell'):
723 if self.badalias or util.safehasattr(self, b'shell'):
720 return adefaults[name]
724 return adefaults[name]
721 return getattr(self.fn, name)
725 return getattr(self.fn, name)
722
726
723 def __call__(self, ui, *args, **opts):
727 def __call__(self, ui, *args, **opts):
724 if self.badalias:
728 if self.badalias:
725 hint = None
729 hint = None
726 if self.unknowncmd:
730 if self.unknowncmd:
727 try:
731 try:
728 # check if the command is in a disabled extension
732 # check if the command is in a disabled extension
729 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
733 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
730 hint = _(b"'%s' is provided by '%s' extension") % (cmd, ext)
734 hint = _(b"'%s' is provided by '%s' extension") % (cmd, ext)
731 except error.UnknownCommand:
735 except error.UnknownCommand:
732 pass
736 pass
733 raise error.ConfigError(self.badalias, hint=hint)
737 raise error.ConfigError(self.badalias, hint=hint)
734 if self.shadows:
738 if self.shadows:
735 ui.debug(
739 ui.debug(
736 b"alias '%s' shadows command '%s'\n" % (self.name, self.cmdname)
740 b"alias '%s' shadows command '%s'\n" % (self.name, self.cmdname)
737 )
741 )
738
742
739 ui.log(
743 ui.log(
740 b'commandalias',
744 b'commandalias',
741 b"alias '%s' expands to '%s'\n",
745 b"alias '%s' expands to '%s'\n",
742 self.name,
746 self.name,
743 self.definition,
747 self.definition,
744 )
748 )
745 if util.safehasattr(self, b'shell'):
749 if util.safehasattr(self, b'shell'):
746 return self.fn(ui, *args, **opts)
750 return self.fn(ui, *args, **opts)
747 else:
751 else:
748 try:
752 try:
749 return util.checksignature(self.fn)(ui, *args, **opts)
753 return util.checksignature(self.fn)(ui, *args, **opts)
750 except error.SignatureError:
754 except error.SignatureError:
751 args = b' '.join([self.cmdname] + self.args)
755 args = b' '.join([self.cmdname] + self.args)
752 ui.debug(b"alias '%s' expands to '%s'\n" % (self.name, args))
756 ui.debug(b"alias '%s' expands to '%s'\n" % (self.name, args))
753 raise
757 raise
754
758
755
759
756 class lazyaliasentry(object):
760 class lazyaliasentry(object):
757 """like a typical command entry (func, opts, help), but is lazy"""
761 """like a typical command entry (func, opts, help), but is lazy"""
758
762
759 def __init__(self, ui, name, definition, cmdtable, source):
763 def __init__(self, ui, name, definition, cmdtable, source):
760 self.ui = ui
764 self.ui = ui
761 self.name = name
765 self.name = name
762 self.definition = definition
766 self.definition = definition
763 self.cmdtable = cmdtable.copy()
767 self.cmdtable = cmdtable.copy()
764 self.source = source
768 self.source = source
765 self.alias = True
769 self.alias = True
766
770
767 @util.propertycache
771 @util.propertycache
768 def _aliasdef(self):
772 def _aliasdef(self):
769 return cmdalias(
773 return cmdalias(
770 self.ui, self.name, self.definition, self.cmdtable, self.source
774 self.ui, self.name, self.definition, self.cmdtable, self.source
771 )
775 )
772
776
773 def __getitem__(self, n):
777 def __getitem__(self, n):
774 aliasdef = self._aliasdef
778 aliasdef = self._aliasdef
775 if n == 0:
779 if n == 0:
776 return aliasdef
780 return aliasdef
777 elif n == 1:
781 elif n == 1:
778 return aliasdef.opts
782 return aliasdef.opts
779 elif n == 2:
783 elif n == 2:
780 return aliasdef.help
784 return aliasdef.help
781 else:
785 else:
782 raise IndexError
786 raise IndexError
783
787
784 def __iter__(self):
788 def __iter__(self):
785 for i in range(3):
789 for i in range(3):
786 yield self[i]
790 yield self[i]
787
791
788 def __len__(self):
792 def __len__(self):
789 return 3
793 return 3
790
794
791
795
792 def addaliases(ui, cmdtable):
796 def addaliases(ui, cmdtable):
793 # aliases are processed after extensions have been loaded, so they
797 # aliases are processed after extensions have been loaded, so they
794 # may use extension commands. Aliases can also use other alias definitions,
798 # may use extension commands. Aliases can also use other alias definitions,
795 # but only if they have been defined prior to the current definition.
799 # but only if they have been defined prior to the current definition.
796 for alias, definition in ui.configitems(b'alias', ignoresub=True):
800 for alias, definition in ui.configitems(b'alias', ignoresub=True):
797 try:
801 try:
798 if cmdtable[alias].definition == definition:
802 if cmdtable[alias].definition == definition:
799 continue
803 continue
800 except (KeyError, AttributeError):
804 except (KeyError, AttributeError):
801 # definition might not exist or it might not be a cmdalias
805 # definition might not exist or it might not be a cmdalias
802 pass
806 pass
803
807
804 source = ui.configsource(b'alias', alias)
808 source = ui.configsource(b'alias', alias)
805 entry = lazyaliasentry(ui, alias, definition, cmdtable, source)
809 entry = lazyaliasentry(ui, alias, definition, cmdtable, source)
806 cmdtable[alias] = entry
810 cmdtable[alias] = entry
807
811
808
812
809 def _parse(ui, args):
813 def _parse(ui, args):
810 options = {}
814 options = {}
811 cmdoptions = {}
815 cmdoptions = {}
812
816
813 try:
817 try:
814 args = fancyopts.fancyopts(args, commands.globalopts, options)
818 args = fancyopts.fancyopts(args, commands.globalopts, options)
815 except getopt.GetoptError as inst:
819 except getopt.GetoptError as inst:
816 raise error.CommandError(None, stringutil.forcebytestr(inst))
820 raise error.CommandError(None, stringutil.forcebytestr(inst))
817
821
818 if args:
822 if args:
819 cmd, args = args[0], args[1:]
823 cmd, args = args[0], args[1:]
820 aliases, entry = cmdutil.findcmd(
824 aliases, entry = cmdutil.findcmd(
821 cmd, commands.table, ui.configbool(b"ui", b"strict")
825 cmd, commands.table, ui.configbool(b"ui", b"strict")
822 )
826 )
823 cmd = aliases[0]
827 cmd = aliases[0]
824 args = aliasargs(entry[0], args)
828 args = aliasargs(entry[0], args)
825 defaults = ui.config(b"defaults", cmd)
829 defaults = ui.config(b"defaults", cmd)
826 if defaults:
830 if defaults:
827 args = (
831 args = (
828 pycompat.maplist(util.expandpath, pycompat.shlexsplit(defaults))
832 pycompat.maplist(util.expandpath, pycompat.shlexsplit(defaults))
829 + args
833 + args
830 )
834 )
831 c = list(entry[1])
835 c = list(entry[1])
832 else:
836 else:
833 cmd = None
837 cmd = None
834 c = []
838 c = []
835
839
836 # combine global options into local
840 # combine global options into local
837 for o in commands.globalopts:
841 for o in commands.globalopts:
838 c.append((o[0], o[1], options[o[1]], o[3]))
842 c.append((o[0], o[1], options[o[1]], o[3]))
839
843
840 try:
844 try:
841 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
845 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
842 except getopt.GetoptError as inst:
846 except getopt.GetoptError as inst:
843 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
847 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
844
848
845 # separate global options back out
849 # separate global options back out
846 for o in commands.globalopts:
850 for o in commands.globalopts:
847 n = o[1]
851 n = o[1]
848 options[n] = cmdoptions[n]
852 options[n] = cmdoptions[n]
849 del cmdoptions[n]
853 del cmdoptions[n]
850
854
851 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
855 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
852
856
853
857
854 def _parseconfig(ui, config):
858 def _parseconfig(ui, config):
855 """parse the --config options from the command line"""
859 """parse the --config options from the command line"""
856 configs = []
860 configs = []
857
861
858 for cfg in config:
862 for cfg in config:
859 try:
863 try:
860 name, value = [cfgelem.strip() for cfgelem in cfg.split(b'=', 1)]
864 name, value = [cfgelem.strip() for cfgelem in cfg.split(b'=', 1)]
861 section, name = name.split(b'.', 1)
865 section, name = name.split(b'.', 1)
862 if not section or not name:
866 if not section or not name:
863 raise IndexError
867 raise IndexError
864 ui.setconfig(section, name, value, b'--config')
868 ui.setconfig(section, name, value, b'--config')
865 configs.append((section, name, value))
869 configs.append((section, name, value))
866 except (IndexError, ValueError):
870 except (IndexError, ValueError):
867 raise error.InputError(
871 raise error.InputError(
868 _(
872 _(
869 b'malformed --config option: %r '
873 b'malformed --config option: %r '
870 b'(use --config section.name=value)'
874 b'(use --config section.name=value)'
871 )
875 )
872 % pycompat.bytestr(cfg)
876 % pycompat.bytestr(cfg)
873 )
877 )
874
878
875 return configs
879 return configs
876
880
877
881
878 def _earlyparseopts(ui, args):
882 def _earlyparseopts(ui, args):
879 options = {}
883 options = {}
880 fancyopts.fancyopts(
884 fancyopts.fancyopts(
881 args,
885 args,
882 commands.globalopts,
886 commands.globalopts,
883 options,
887 options,
884 gnu=not ui.plain(b'strictflags'),
888 gnu=not ui.plain(b'strictflags'),
885 early=True,
889 early=True,
886 optaliases={b'repository': [b'repo']},
890 optaliases={b'repository': [b'repo']},
887 )
891 )
888 return options
892 return options
889
893
890
894
891 def _earlysplitopts(args):
895 def _earlysplitopts(args):
892 """Split args into a list of possible early options and remainder args"""
896 """Split args into a list of possible early options and remainder args"""
893 shortoptions = b'R:'
897 shortoptions = b'R:'
894 # TODO: perhaps 'debugger' should be included
898 # TODO: perhaps 'debugger' should be included
895 longoptions = [b'cwd=', b'repository=', b'repo=', b'config=']
899 longoptions = [b'cwd=', b'repository=', b'repo=', b'config=']
896 return fancyopts.earlygetopt(
900 return fancyopts.earlygetopt(
897 args, shortoptions, longoptions, gnu=True, keepsep=True
901 args, shortoptions, longoptions, gnu=True, keepsep=True
898 )
902 )
899
903
900
904
901 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
905 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
902 # run pre-hook, and abort if it fails
906 # run pre-hook, and abort if it fails
903 hook.hook(
907 hook.hook(
904 lui,
908 lui,
905 repo,
909 repo,
906 b"pre-%s" % cmd,
910 b"pre-%s" % cmd,
907 True,
911 True,
908 args=b" ".join(fullargs),
912 args=b" ".join(fullargs),
909 pats=cmdpats,
913 pats=cmdpats,
910 opts=cmdoptions,
914 opts=cmdoptions,
911 )
915 )
912 try:
916 try:
913 ret = _runcommand(ui, options, cmd, d)
917 ret = _runcommand(ui, options, cmd, d)
914 # run post-hook, passing command result
918 # run post-hook, passing command result
915 hook.hook(
919 hook.hook(
916 lui,
920 lui,
917 repo,
921 repo,
918 b"post-%s" % cmd,
922 b"post-%s" % cmd,
919 False,
923 False,
920 args=b" ".join(fullargs),
924 args=b" ".join(fullargs),
921 result=ret,
925 result=ret,
922 pats=cmdpats,
926 pats=cmdpats,
923 opts=cmdoptions,
927 opts=cmdoptions,
924 )
928 )
925 except Exception:
929 except Exception:
926 # run failure hook and re-raise
930 # run failure hook and re-raise
927 hook.hook(
931 hook.hook(
928 lui,
932 lui,
929 repo,
933 repo,
930 b"fail-%s" % cmd,
934 b"fail-%s" % cmd,
931 False,
935 False,
932 args=b" ".join(fullargs),
936 args=b" ".join(fullargs),
933 pats=cmdpats,
937 pats=cmdpats,
934 opts=cmdoptions,
938 opts=cmdoptions,
935 )
939 )
936 raise
940 raise
937 return ret
941 return ret
938
942
939
943
940 def _readsharedsourceconfig(ui, path):
944 def _readsharedsourceconfig(ui, path):
941 """if the current repository is shared one, this tries to read
945 """if the current repository is shared one, this tries to read
942 .hg/hgrc of shared source if we are in share-safe mode
946 .hg/hgrc of shared source if we are in share-safe mode
943
947
944 Config read is loaded into the ui object passed
948 Config read is loaded into the ui object passed
945
949
946 This should be called before reading .hg/hgrc or the main repo
950 This should be called before reading .hg/hgrc or the main repo
947 as that overrides config set in shared source"""
951 as that overrides config set in shared source"""
948 try:
952 try:
949 with open(os.path.join(path, b".hg", b"requires"), "rb") as fp:
953 with open(os.path.join(path, b".hg", b"requires"), "rb") as fp:
950 requirements = set(fp.read().splitlines())
954 requirements = set(fp.read().splitlines())
951 if not (
955 if not (
952 requirementsmod.SHARESAFE_REQUIREMENT in requirements
956 requirementsmod.SHARESAFE_REQUIREMENT in requirements
953 and requirementsmod.SHARED_REQUIREMENT in requirements
957 and requirementsmod.SHARED_REQUIREMENT in requirements
954 ):
958 ):
955 return
959 return
956 hgvfs = vfs.vfs(os.path.join(path, b".hg"))
960 hgvfs = vfs.vfs(os.path.join(path, b".hg"))
957 sharedvfs = localrepo._getsharedvfs(hgvfs, requirements)
961 sharedvfs = localrepo._getsharedvfs(hgvfs, requirements)
958 root = sharedvfs.base
962 root = sharedvfs.base
959 ui.readconfig(sharedvfs.join(b"hgrc"), root)
963 ui.readconfig(sharedvfs.join(b"hgrc"), root)
960 except IOError:
964 except IOError:
961 pass
965 pass
962
966
963
967
964 def _getlocal(ui, rpath, wd=None):
968 def _getlocal(ui, rpath, wd=None):
965 """Return (path, local ui object) for the given target path.
969 """Return (path, local ui object) for the given target path.
966
970
967 Takes paths in [cwd]/.hg/hgrc into account."
971 Takes paths in [cwd]/.hg/hgrc into account."
968 """
972 """
969 if wd is None:
973 if wd is None:
970 try:
974 try:
971 wd = encoding.getcwd()
975 wd = encoding.getcwd()
972 except OSError as e:
976 except OSError as e:
973 raise error.Abort(
977 raise error.Abort(
974 _(b"error getting current working directory: %s")
978 _(b"error getting current working directory: %s")
975 % encoding.strtolocal(e.strerror)
979 % encoding.strtolocal(e.strerror)
976 )
980 )
977
981
978 path = cmdutil.findrepo(wd) or b""
982 path = cmdutil.findrepo(wd) or b""
979 if not path:
983 if not path:
980 lui = ui
984 lui = ui
981 else:
985 else:
982 lui = ui.copy()
986 lui = ui.copy()
983 if rcutil.use_repo_hgrc():
987 if rcutil.use_repo_hgrc():
984 _readsharedsourceconfig(lui, path)
988 _readsharedsourceconfig(lui, path)
985 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
989 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
986 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
990 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
987
991
988 if rpath:
992 if rpath:
989 path = lui.expandpath(rpath)
993 path = lui.expandpath(rpath)
990 lui = ui.copy()
994 lui = ui.copy()
991 if rcutil.use_repo_hgrc():
995 if rcutil.use_repo_hgrc():
992 _readsharedsourceconfig(lui, path)
996 _readsharedsourceconfig(lui, path)
993 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
997 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
994 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
998 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
995
999
996 return path, lui
1000 return path, lui
997
1001
998
1002
999 def _checkshellalias(lui, ui, args):
1003 def _checkshellalias(lui, ui, args):
1000 """Return the function to run the shell alias, if it is required"""
1004 """Return the function to run the shell alias, if it is required"""
1001 options = {}
1005 options = {}
1002
1006
1003 try:
1007 try:
1004 args = fancyopts.fancyopts(args, commands.globalopts, options)
1008 args = fancyopts.fancyopts(args, commands.globalopts, options)
1005 except getopt.GetoptError:
1009 except getopt.GetoptError:
1006 return
1010 return
1007
1011
1008 if not args:
1012 if not args:
1009 return
1013 return
1010
1014
1011 cmdtable = commands.table
1015 cmdtable = commands.table
1012
1016
1013 cmd = args[0]
1017 cmd = args[0]
1014 try:
1018 try:
1015 strict = ui.configbool(b"ui", b"strict")
1019 strict = ui.configbool(b"ui", b"strict")
1016 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
1020 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
1017 except (error.AmbiguousCommand, error.UnknownCommand):
1021 except (error.AmbiguousCommand, error.UnknownCommand):
1018 return
1022 return
1019
1023
1020 cmd = aliases[0]
1024 cmd = aliases[0]
1021 fn = entry[0]
1025 fn = entry[0]
1022
1026
1023 if cmd and util.safehasattr(fn, b'shell'):
1027 if cmd and util.safehasattr(fn, b'shell'):
1024 # shell alias shouldn't receive early options which are consumed by hg
1028 # shell alias shouldn't receive early options which are consumed by hg
1025 _earlyopts, args = _earlysplitopts(args)
1029 _earlyopts, args = _earlysplitopts(args)
1026 d = lambda: fn(ui, *args[1:])
1030 d = lambda: fn(ui, *args[1:])
1027 return lambda: runcommand(
1031 return lambda: runcommand(
1028 lui, None, cmd, args[:1], ui, options, d, [], {}
1032 lui, None, cmd, args[:1], ui, options, d, [], {}
1029 )
1033 )
1030
1034
1031
1035
1032 def _dispatch(req):
1036 def _dispatch(req):
1033 args = req.args
1037 args = req.args
1034 ui = req.ui
1038 ui = req.ui
1035
1039
1036 # check for cwd
1040 # check for cwd
1037 cwd = req.earlyoptions[b'cwd']
1041 cwd = req.earlyoptions[b'cwd']
1038 if cwd:
1042 if cwd:
1039 os.chdir(cwd)
1043 os.chdir(cwd)
1040
1044
1041 rpath = req.earlyoptions[b'repository']
1045 rpath = req.earlyoptions[b'repository']
1042 path, lui = _getlocal(ui, rpath)
1046 path, lui = _getlocal(ui, rpath)
1043
1047
1044 uis = {ui, lui}
1048 uis = {ui, lui}
1045
1049
1046 if req.repo:
1050 if req.repo:
1047 uis.add(req.repo.ui)
1051 uis.add(req.repo.ui)
1048
1052
1049 if (
1053 if (
1050 req.earlyoptions[b'verbose']
1054 req.earlyoptions[b'verbose']
1051 or req.earlyoptions[b'debug']
1055 or req.earlyoptions[b'debug']
1052 or req.earlyoptions[b'quiet']
1056 or req.earlyoptions[b'quiet']
1053 ):
1057 ):
1054 for opt in (b'verbose', b'debug', b'quiet'):
1058 for opt in (b'verbose', b'debug', b'quiet'):
1055 val = pycompat.bytestr(bool(req.earlyoptions[opt]))
1059 val = pycompat.bytestr(bool(req.earlyoptions[opt]))
1056 for ui_ in uis:
1060 for ui_ in uis:
1057 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1061 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1058
1062
1059 if req.earlyoptions[b'profile']:
1063 if req.earlyoptions[b'profile']:
1060 for ui_ in uis:
1064 for ui_ in uis:
1061 ui_.setconfig(b'profiling', b'enabled', b'true', b'--profile')
1065 ui_.setconfig(b'profiling', b'enabled', b'true', b'--profile')
1062
1066
1063 profile = lui.configbool(b'profiling', b'enabled')
1067 profile = lui.configbool(b'profiling', b'enabled')
1064 with profiling.profile(lui, enabled=profile) as profiler:
1068 with profiling.profile(lui, enabled=profile) as profiler:
1065 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
1069 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
1066 # reposetup
1070 # reposetup
1067 extensions.loadall(lui)
1071 extensions.loadall(lui)
1068 # Propagate any changes to lui.__class__ by extensions
1072 # Propagate any changes to lui.__class__ by extensions
1069 ui.__class__ = lui.__class__
1073 ui.__class__ = lui.__class__
1070
1074
1071 # (uisetup and extsetup are handled in extensions.loadall)
1075 # (uisetup and extsetup are handled in extensions.loadall)
1072
1076
1073 # (reposetup is handled in hg.repository)
1077 # (reposetup is handled in hg.repository)
1074
1078
1075 addaliases(lui, commands.table)
1079 addaliases(lui, commands.table)
1076
1080
1077 # All aliases and commands are completely defined, now.
1081 # All aliases and commands are completely defined, now.
1078 # Check abbreviation/ambiguity of shell alias.
1082 # Check abbreviation/ambiguity of shell alias.
1079 shellaliasfn = _checkshellalias(lui, ui, args)
1083 shellaliasfn = _checkshellalias(lui, ui, args)
1080 if shellaliasfn:
1084 if shellaliasfn:
1081 # no additional configs will be set, set up the ui instances
1085 # no additional configs will be set, set up the ui instances
1082 for ui_ in uis:
1086 for ui_ in uis:
1083 extensions.populateui(ui_)
1087 extensions.populateui(ui_)
1084 return shellaliasfn()
1088 return shellaliasfn()
1085
1089
1086 # check for fallback encoding
1090 # check for fallback encoding
1087 fallback = lui.config(b'ui', b'fallbackencoding')
1091 fallback = lui.config(b'ui', b'fallbackencoding')
1088 if fallback:
1092 if fallback:
1089 encoding.fallbackencoding = fallback
1093 encoding.fallbackencoding = fallback
1090
1094
1091 fullargs = args
1095 fullargs = args
1092 cmd, func, args, options, cmdoptions = _parse(lui, args)
1096 cmd, func, args, options, cmdoptions = _parse(lui, args)
1093
1097
1094 # store the canonical command name in request object for later access
1098 # store the canonical command name in request object for later access
1095 req.canonical_command = cmd
1099 req.canonical_command = cmd
1096
1100
1097 if options[b"config"] != req.earlyoptions[b"config"]:
1101 if options[b"config"] != req.earlyoptions[b"config"]:
1098 raise error.InputError(_(b"option --config may not be abbreviated"))
1102 raise error.InputError(_(b"option --config may not be abbreviated"))
1099 if options[b"cwd"] != req.earlyoptions[b"cwd"]:
1103 if options[b"cwd"] != req.earlyoptions[b"cwd"]:
1100 raise error.InputError(_(b"option --cwd may not be abbreviated"))
1104 raise error.InputError(_(b"option --cwd may not be abbreviated"))
1101 if options[b"repository"] != req.earlyoptions[b"repository"]:
1105 if options[b"repository"] != req.earlyoptions[b"repository"]:
1102 raise error.InputError(
1106 raise error.InputError(
1103 _(
1107 _(
1104 b"option -R has to be separated from other options (e.g. not "
1108 b"option -R has to be separated from other options (e.g. not "
1105 b"-qR) and --repository may only be abbreviated as --repo"
1109 b"-qR) and --repository may only be abbreviated as --repo"
1106 )
1110 )
1107 )
1111 )
1108 if options[b"debugger"] != req.earlyoptions[b"debugger"]:
1112 if options[b"debugger"] != req.earlyoptions[b"debugger"]:
1109 raise error.InputError(
1113 raise error.InputError(
1110 _(b"option --debugger may not be abbreviated")
1114 _(b"option --debugger may not be abbreviated")
1111 )
1115 )
1112 # don't validate --profile/--traceback, which can be enabled from now
1116 # don't validate --profile/--traceback, which can be enabled from now
1113
1117
1114 if options[b"encoding"]:
1118 if options[b"encoding"]:
1115 encoding.encoding = options[b"encoding"]
1119 encoding.encoding = options[b"encoding"]
1116 if options[b"encodingmode"]:
1120 if options[b"encodingmode"]:
1117 encoding.encodingmode = options[b"encodingmode"]
1121 encoding.encodingmode = options[b"encodingmode"]
1118 if options[b"time"]:
1122 if options[b"time"]:
1119
1123
1120 def get_times():
1124 def get_times():
1121 t = os.times()
1125 t = os.times()
1122 if t[4] == 0.0:
1126 if t[4] == 0.0:
1123 # Windows leaves this as zero, so use time.perf_counter()
1127 # Windows leaves this as zero, so use time.perf_counter()
1124 t = (t[0], t[1], t[2], t[3], util.timer())
1128 t = (t[0], t[1], t[2], t[3], util.timer())
1125 return t
1129 return t
1126
1130
1127 s = get_times()
1131 s = get_times()
1128
1132
1129 def print_time():
1133 def print_time():
1130 t = get_times()
1134 t = get_times()
1131 ui.warn(
1135 ui.warn(
1132 _(b"time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n")
1136 _(b"time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n")
1133 % (
1137 % (
1134 t[4] - s[4],
1138 t[4] - s[4],
1135 t[0] - s[0],
1139 t[0] - s[0],
1136 t[2] - s[2],
1140 t[2] - s[2],
1137 t[1] - s[1],
1141 t[1] - s[1],
1138 t[3] - s[3],
1142 t[3] - s[3],
1139 )
1143 )
1140 )
1144 )
1141
1145
1142 ui.atexit(print_time)
1146 ui.atexit(print_time)
1143 if options[b"profile"]:
1147 if options[b"profile"]:
1144 profiler.start()
1148 profiler.start()
1145
1149
1146 # if abbreviated version of this were used, take them in account, now
1150 # if abbreviated version of this were used, take them in account, now
1147 if options[b'verbose'] or options[b'debug'] or options[b'quiet']:
1151 if options[b'verbose'] or options[b'debug'] or options[b'quiet']:
1148 for opt in (b'verbose', b'debug', b'quiet'):
1152 for opt in (b'verbose', b'debug', b'quiet'):
1149 if options[opt] == req.earlyoptions[opt]:
1153 if options[opt] == req.earlyoptions[opt]:
1150 continue
1154 continue
1151 val = pycompat.bytestr(bool(options[opt]))
1155 val = pycompat.bytestr(bool(options[opt]))
1152 for ui_ in uis:
1156 for ui_ in uis:
1153 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1157 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1154
1158
1155 if options[b'traceback']:
1159 if options[b'traceback']:
1156 for ui_ in uis:
1160 for ui_ in uis:
1157 ui_.setconfig(b'ui', b'traceback', b'on', b'--traceback')
1161 ui_.setconfig(b'ui', b'traceback', b'on', b'--traceback')
1158
1162
1159 if options[b'noninteractive']:
1163 if options[b'noninteractive']:
1160 for ui_ in uis:
1164 for ui_ in uis:
1161 ui_.setconfig(b'ui', b'interactive', b'off', b'-y')
1165 ui_.setconfig(b'ui', b'interactive', b'off', b'-y')
1162
1166
1163 if cmdoptions.get(b'insecure', False):
1167 if cmdoptions.get(b'insecure', False):
1164 for ui_ in uis:
1168 for ui_ in uis:
1165 ui_.insecureconnections = True
1169 ui_.insecureconnections = True
1166
1170
1167 # setup color handling before pager, because setting up pager
1171 # setup color handling before pager, because setting up pager
1168 # might cause incorrect console information
1172 # might cause incorrect console information
1169 coloropt = options[b'color']
1173 coloropt = options[b'color']
1170 for ui_ in uis:
1174 for ui_ in uis:
1171 if coloropt:
1175 if coloropt:
1172 ui_.setconfig(b'ui', b'color', coloropt, b'--color')
1176 ui_.setconfig(b'ui', b'color', coloropt, b'--color')
1173 color.setup(ui_)
1177 color.setup(ui_)
1174
1178
1175 if stringutil.parsebool(options[b'pager']):
1179 if stringutil.parsebool(options[b'pager']):
1176 # ui.pager() expects 'internal-always-' prefix in this case
1180 # ui.pager() expects 'internal-always-' prefix in this case
1177 ui.pager(b'internal-always-' + cmd)
1181 ui.pager(b'internal-always-' + cmd)
1178 elif options[b'pager'] != b'auto':
1182 elif options[b'pager'] != b'auto':
1179 for ui_ in uis:
1183 for ui_ in uis:
1180 ui_.disablepager()
1184 ui_.disablepager()
1181
1185
1182 # configs are fully loaded, set up the ui instances
1186 # configs are fully loaded, set up the ui instances
1183 for ui_ in uis:
1187 for ui_ in uis:
1184 extensions.populateui(ui_)
1188 extensions.populateui(ui_)
1185
1189
1186 if options[b'version']:
1190 if options[b'version']:
1187 return commands.version_(ui)
1191 return commands.version_(ui)
1188 if options[b'help']:
1192 if options[b'help']:
1189 return commands.help_(ui, cmd, command=cmd is not None)
1193 return commands.help_(ui, cmd, command=cmd is not None)
1190 elif not cmd:
1194 elif not cmd:
1191 return commands.help_(ui, b'shortlist')
1195 return commands.help_(ui, b'shortlist')
1192
1196
1193 repo = None
1197 repo = None
1194 cmdpats = args[:]
1198 cmdpats = args[:]
1195 assert func is not None # help out pytype
1199 assert func is not None # help out pytype
1196 if not func.norepo:
1200 if not func.norepo:
1197 # use the repo from the request only if we don't have -R
1201 # use the repo from the request only if we don't have -R
1198 if not rpath and not cwd:
1202 if not rpath and not cwd:
1199 repo = req.repo
1203 repo = req.repo
1200
1204
1201 if repo:
1205 if repo:
1202 # set the descriptors of the repo ui to those of ui
1206 # set the descriptors of the repo ui to those of ui
1203 repo.ui.fin = ui.fin
1207 repo.ui.fin = ui.fin
1204 repo.ui.fout = ui.fout
1208 repo.ui.fout = ui.fout
1205 repo.ui.ferr = ui.ferr
1209 repo.ui.ferr = ui.ferr
1206 repo.ui.fmsg = ui.fmsg
1210 repo.ui.fmsg = ui.fmsg
1207 else:
1211 else:
1208 try:
1212 try:
1209 repo = hg.repository(
1213 repo = hg.repository(
1210 ui,
1214 ui,
1211 path=path,
1215 path=path,
1212 presetupfuncs=req.prereposetups,
1216 presetupfuncs=req.prereposetups,
1213 intents=func.intents,
1217 intents=func.intents,
1214 )
1218 )
1215 if not repo.local():
1219 if not repo.local():
1216 raise error.InputError(
1220 raise error.InputError(
1217 _(b"repository '%s' is not local") % path
1221 _(b"repository '%s' is not local") % path
1218 )
1222 )
1219 repo.ui.setconfig(
1223 repo.ui.setconfig(
1220 b"bundle", b"mainreporoot", repo.root, b'repo'
1224 b"bundle", b"mainreporoot", repo.root, b'repo'
1221 )
1225 )
1222 except error.RequirementError:
1226 except error.RequirementError:
1223 raise
1227 raise
1224 except error.RepoError:
1228 except error.RepoError:
1225 if rpath: # invalid -R path
1229 if rpath: # invalid -R path
1226 raise
1230 raise
1227 if not func.optionalrepo:
1231 if not func.optionalrepo:
1228 if func.inferrepo and args and not path:
1232 if func.inferrepo and args and not path:
1229 # try to infer -R from command args
1233 # try to infer -R from command args
1230 repos = pycompat.maplist(cmdutil.findrepo, args)
1234 repos = pycompat.maplist(cmdutil.findrepo, args)
1231 guess = repos[0]
1235 guess = repos[0]
1232 if guess and repos.count(guess) == len(repos):
1236 if guess and repos.count(guess) == len(repos):
1233 req.args = [b'--repository', guess] + fullargs
1237 req.args = [b'--repository', guess] + fullargs
1234 req.earlyoptions[b'repository'] = guess
1238 req.earlyoptions[b'repository'] = guess
1235 return _dispatch(req)
1239 return _dispatch(req)
1236 if not path:
1240 if not path:
1237 raise error.InputError(
1241 raise error.InputError(
1238 _(
1242 _(
1239 b"no repository found in"
1243 b"no repository found in"
1240 b" '%s' (.hg not found)"
1244 b" '%s' (.hg not found)"
1241 )
1245 )
1242 % encoding.getcwd()
1246 % encoding.getcwd()
1243 )
1247 )
1244 raise
1248 raise
1245 if repo:
1249 if repo:
1246 ui = repo.ui
1250 ui = repo.ui
1247 if options[b'hidden']:
1251 if options[b'hidden']:
1248 repo = repo.unfiltered()
1252 repo = repo.unfiltered()
1249 args.insert(0, repo)
1253 args.insert(0, repo)
1250 elif rpath:
1254 elif rpath:
1251 ui.warn(_(b"warning: --repository ignored\n"))
1255 ui.warn(_(b"warning: --repository ignored\n"))
1252
1256
1253 msg = _formatargs(fullargs)
1257 msg = _formatargs(fullargs)
1254 ui.log(b"command", b'%s\n', msg)
1258 ui.log(b"command", b'%s\n', msg)
1255 strcmdopt = pycompat.strkwargs(cmdoptions)
1259 strcmdopt = pycompat.strkwargs(cmdoptions)
1256 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
1260 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
1257 try:
1261 try:
1258 return runcommand(
1262 return runcommand(
1259 lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions
1263 lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions
1260 )
1264 )
1261 finally:
1265 finally:
1262 if repo and repo != req.repo:
1266 if repo and repo != req.repo:
1263 repo.close()
1267 repo.close()
1264
1268
1265
1269
1266 def _runcommand(ui, options, cmd, cmdfunc):
1270 def _runcommand(ui, options, cmd, cmdfunc):
1267 """Run a command function, possibly with profiling enabled."""
1271 """Run a command function, possibly with profiling enabled."""
1268 try:
1272 try:
1269 with tracing.log("Running %s command" % cmd):
1273 with tracing.log("Running %s command" % cmd):
1270 return cmdfunc()
1274 return cmdfunc()
1271 except error.SignatureError:
1275 except error.SignatureError:
1272 raise error.CommandError(cmd, _(b'invalid arguments'))
1276 raise error.CommandError(cmd, _(b'invalid arguments'))
1273
1277
1274
1278
1275 def _exceptionwarning(ui):
1279 def _exceptionwarning(ui):
1276 """Produce a warning message for the current active exception"""
1280 """Produce a warning message for the current active exception"""
1277
1281
1278 # For compatibility checking, we discard the portion of the hg
1282 # For compatibility checking, we discard the portion of the hg
1279 # version after the + on the assumption that if a "normal
1283 # version after the + on the assumption that if a "normal
1280 # user" is running a build with a + in it the packager
1284 # user" is running a build with a + in it the packager
1281 # probably built from fairly close to a tag and anyone with a
1285 # probably built from fairly close to a tag and anyone with a
1282 # 'make local' copy of hg (where the version number can be out
1286 # 'make local' copy of hg (where the version number can be out
1283 # of date) will be clueful enough to notice the implausible
1287 # of date) will be clueful enough to notice the implausible
1284 # version number and try updating.
1288 # version number and try updating.
1285 ct = util.versiontuple(n=2)
1289 ct = util.versiontuple(n=2)
1286 worst = None, ct, b'', b''
1290 worst = None, ct, b'', b''
1287 if ui.config(b'ui', b'supportcontact') is None:
1291 if ui.config(b'ui', b'supportcontact') is None:
1288 for name, mod in extensions.extensions():
1292 for name, mod in extensions.extensions():
1289 # 'testedwith' should be bytes, but not all extensions are ported
1293 # 'testedwith' should be bytes, but not all extensions are ported
1290 # to py3 and we don't want UnicodeException because of that.
1294 # to py3 and we don't want UnicodeException because of that.
1291 testedwith = stringutil.forcebytestr(
1295 testedwith = stringutil.forcebytestr(
1292 getattr(mod, 'testedwith', b'')
1296 getattr(mod, 'testedwith', b'')
1293 )
1297 )
1294 version = extensions.moduleversion(mod)
1298 version = extensions.moduleversion(mod)
1295 report = getattr(mod, 'buglink', _(b'the extension author.'))
1299 report = getattr(mod, 'buglink', _(b'the extension author.'))
1296 if not testedwith.strip():
1300 if not testedwith.strip():
1297 # We found an untested extension. It's likely the culprit.
1301 # We found an untested extension. It's likely the culprit.
1298 worst = name, b'unknown', report, version
1302 worst = name, b'unknown', report, version
1299 break
1303 break
1300
1304
1301 # Never blame on extensions bundled with Mercurial.
1305 # Never blame on extensions bundled with Mercurial.
1302 if extensions.ismoduleinternal(mod):
1306 if extensions.ismoduleinternal(mod):
1303 continue
1307 continue
1304
1308
1305 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1309 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1306 if ct in tested:
1310 if ct in tested:
1307 continue
1311 continue
1308
1312
1309 lower = [t for t in tested if t < ct]
1313 lower = [t for t in tested if t < ct]
1310 nearest = max(lower or tested)
1314 nearest = max(lower or tested)
1311 if worst[0] is None or nearest < worst[1]:
1315 if worst[0] is None or nearest < worst[1]:
1312 worst = name, nearest, report, version
1316 worst = name, nearest, report, version
1313 if worst[0] is not None:
1317 if worst[0] is not None:
1314 name, testedwith, report, version = worst
1318 name, testedwith, report, version = worst
1315 if not isinstance(testedwith, (bytes, str)):
1319 if not isinstance(testedwith, (bytes, str)):
1316 testedwith = b'.'.join(
1320 testedwith = b'.'.join(
1317 [stringutil.forcebytestr(c) for c in testedwith]
1321 [stringutil.forcebytestr(c) for c in testedwith]
1318 )
1322 )
1319 extver = version or _(b"(version N/A)")
1323 extver = version or _(b"(version N/A)")
1320 warning = _(
1324 warning = _(
1321 b'** Unknown exception encountered with '
1325 b'** Unknown exception encountered with '
1322 b'possibly-broken third-party extension "%s" %s\n'
1326 b'possibly-broken third-party extension "%s" %s\n'
1323 b'** which supports versions %s of Mercurial.\n'
1327 b'** which supports versions %s of Mercurial.\n'
1324 b'** Please disable "%s" and try your action again.\n'
1328 b'** Please disable "%s" and try your action again.\n'
1325 b'** If that fixes the bug please report it to %s\n'
1329 b'** If that fixes the bug please report it to %s\n'
1326 ) % (name, extver, testedwith, name, stringutil.forcebytestr(report))
1330 ) % (name, extver, testedwith, name, stringutil.forcebytestr(report))
1327 else:
1331 else:
1328 bugtracker = ui.config(b'ui', b'supportcontact')
1332 bugtracker = ui.config(b'ui', b'supportcontact')
1329 if bugtracker is None:
1333 if bugtracker is None:
1330 bugtracker = _(b"https://mercurial-scm.org/wiki/BugTracker")
1334 bugtracker = _(b"https://mercurial-scm.org/wiki/BugTracker")
1331 warning = (
1335 warning = (
1332 _(
1336 _(
1333 b"** unknown exception encountered, "
1337 b"** unknown exception encountered, "
1334 b"please report by visiting\n** "
1338 b"please report by visiting\n** "
1335 )
1339 )
1336 + bugtracker
1340 + bugtracker
1337 + b'\n'
1341 + b'\n'
1338 )
1342 )
1339 sysversion = pycompat.sysbytes(sys.version).replace(b'\n', b'')
1343 sysversion = pycompat.sysbytes(sys.version).replace(b'\n', b'')
1340
1344
1341 def ext_with_ver(x):
1345 def ext_with_ver(x):
1342 ext = x[0]
1346 ext = x[0]
1343 ver = extensions.moduleversion(x[1])
1347 ver = extensions.moduleversion(x[1])
1344 if ver:
1348 if ver:
1345 ext += b' ' + ver
1349 ext += b' ' + ver
1346 return ext
1350 return ext
1347
1351
1348 warning += (
1352 warning += (
1349 (_(b"** Python %s\n") % sysversion)
1353 (_(b"** Python %s\n") % sysversion)
1350 + (_(b"** Mercurial Distributed SCM (version %s)\n") % util.version())
1354 + (_(b"** Mercurial Distributed SCM (version %s)\n") % util.version())
1351 + (
1355 + (
1352 _(b"** Extensions loaded: %s\n")
1356 _(b"** Extensions loaded: %s\n")
1353 % b", ".join(
1357 % b", ".join(
1354 [ext_with_ver(x) for x in sorted(extensions.extensions())]
1358 [ext_with_ver(x) for x in sorted(extensions.extensions())]
1355 )
1359 )
1356 )
1360 )
1357 )
1361 )
1358 return warning
1362 return warning
1359
1363
1360
1364
1361 def handlecommandexception(ui):
1365 def handlecommandexception(ui):
1362 """Produce a warning message for broken commands
1366 """Produce a warning message for broken commands
1363
1367
1364 Called when handling an exception; the exception is reraised if
1368 Called when handling an exception; the exception is reraised if
1365 this function returns False, ignored otherwise.
1369 this function returns False, ignored otherwise.
1366 """
1370 """
1367 warning = _exceptionwarning(ui)
1371 warning = _exceptionwarning(ui)
1368 ui.log(
1372 ui.log(
1369 b"commandexception",
1373 b"commandexception",
1370 b"%s\n%s\n",
1374 b"%s\n%s\n",
1371 warning,
1375 warning,
1372 pycompat.sysbytes(traceback.format_exc()),
1376 pycompat.sysbytes(traceback.format_exc()),
1373 )
1377 )
1374 ui.warn(warning)
1378 ui.warn(warning)
1375 return False # re-raise the exception
1379 return False # re-raise the exception
@@ -1,305 +1,305 b''
1 # osutil.py - pure Python version of osutil.c
1 # osutil.py - pure Python version of osutil.c
2 #
2 #
3 # Copyright 2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import, division
8 from __future__ import absolute_import, division
9
9
10 import ctypes
10 import ctypes
11 import ctypes.util
11 import ctypes.util
12 import os
12 import os
13 import socket
13 import socket
14 import stat as statmod
14 import stat as statmod
15
15
16 from ..pycompat import getattr
16 from ..pycompat import getattr
17 from .. import (
17 from .. import (
18 encoding,
18 encoding,
19 pycompat,
19 pycompat,
20 )
20 )
21
21
22
22
23 def _mode_to_kind(mode):
23 def _mode_to_kind(mode):
24 if statmod.S_ISREG(mode):
24 if statmod.S_ISREG(mode):
25 return statmod.S_IFREG
25 return statmod.S_IFREG
26 if statmod.S_ISDIR(mode):
26 if statmod.S_ISDIR(mode):
27 return statmod.S_IFDIR
27 return statmod.S_IFDIR
28 if statmod.S_ISLNK(mode):
28 if statmod.S_ISLNK(mode):
29 return statmod.S_IFLNK
29 return statmod.S_IFLNK
30 if statmod.S_ISBLK(mode):
30 if statmod.S_ISBLK(mode):
31 return statmod.S_IFBLK
31 return statmod.S_IFBLK
32 if statmod.S_ISCHR(mode):
32 if statmod.S_ISCHR(mode):
33 return statmod.S_IFCHR
33 return statmod.S_IFCHR
34 if statmod.S_ISFIFO(mode):
34 if statmod.S_ISFIFO(mode):
35 return statmod.S_IFIFO
35 return statmod.S_IFIFO
36 if statmod.S_ISSOCK(mode):
36 if statmod.S_ISSOCK(mode):
37 return statmod.S_IFSOCK
37 return statmod.S_IFSOCK
38 return mode
38 return mode
39
39
40
40
41 def listdir(path, stat=False, skip=None):
41 def listdir(path, stat=False, skip=None):
42 """listdir(path, stat=False) -> list_of_tuples
42 """listdir(path, stat=False) -> list_of_tuples
43
43
44 Return a sorted list containing information about the entries
44 Return a sorted list containing information about the entries
45 in the directory.
45 in the directory.
46
46
47 If stat is True, each element is a 3-tuple:
47 If stat is True, each element is a 3-tuple:
48
48
49 (name, type, stat object)
49 (name, type, stat object)
50
50
51 Otherwise, each element is a 2-tuple:
51 Otherwise, each element is a 2-tuple:
52
52
53 (name, type)
53 (name, type)
54 """
54 """
55 result = []
55 result = []
56 prefix = path
56 prefix = path
57 if not prefix.endswith(pycompat.ossep):
57 if not prefix.endswith(pycompat.ossep):
58 prefix += pycompat.ossep
58 prefix += pycompat.ossep
59 names = os.listdir(path)
59 names = os.listdir(path)
60 names.sort()
60 names.sort()
61 for fn in names:
61 for fn in names:
62 st = os.lstat(prefix + fn)
62 st = os.lstat(prefix + fn)
63 if fn == skip and statmod.S_ISDIR(st.st_mode):
63 if fn == skip and statmod.S_ISDIR(st.st_mode):
64 return []
64 return []
65 if stat:
65 if stat:
66 result.append((fn, _mode_to_kind(st.st_mode), st))
66 result.append((fn, _mode_to_kind(st.st_mode), st))
67 else:
67 else:
68 result.append((fn, _mode_to_kind(st.st_mode)))
68 result.append((fn, _mode_to_kind(st.st_mode)))
69 return result
69 return result
70
70
71
71
72 if not pycompat.iswindows:
72 if not pycompat.iswindows:
73 posixfile = open
73 posixfile = open
74
74
75 _SCM_RIGHTS = 0x01
75 _SCM_RIGHTS = 0x01
76 _socklen_t = ctypes.c_uint
76 _socklen_t = ctypes.c_uint
77
77
78 if pycompat.sysplatform.startswith(b'linux'):
78 if pycompat.sysplatform.startswith(b'linux'):
79 # socket.h says "the type should be socklen_t but the definition of
79 # socket.h says "the type should be socklen_t but the definition of
80 # the kernel is incompatible with this."
80 # the kernel is incompatible with this."
81 _cmsg_len_t = ctypes.c_size_t
81 _cmsg_len_t = ctypes.c_size_t
82 _msg_controllen_t = ctypes.c_size_t
82 _msg_controllen_t = ctypes.c_size_t
83 _msg_iovlen_t = ctypes.c_size_t
83 _msg_iovlen_t = ctypes.c_size_t
84 else:
84 else:
85 _cmsg_len_t = _socklen_t
85 _cmsg_len_t = _socklen_t
86 _msg_controllen_t = _socklen_t
86 _msg_controllen_t = _socklen_t
87 _msg_iovlen_t = ctypes.c_int
87 _msg_iovlen_t = ctypes.c_int
88
88
89 class _iovec(ctypes.Structure):
89 class _iovec(ctypes.Structure):
90 _fields_ = [
90 _fields_ = [
91 (u'iov_base', ctypes.c_void_p),
91 (u'iov_base', ctypes.c_void_p),
92 (u'iov_len', ctypes.c_size_t),
92 (u'iov_len', ctypes.c_size_t),
93 ]
93 ]
94
94
95 class _msghdr(ctypes.Structure):
95 class _msghdr(ctypes.Structure):
96 _fields_ = [
96 _fields_ = [
97 (u'msg_name', ctypes.c_void_p),
97 (u'msg_name', ctypes.c_void_p),
98 (u'msg_namelen', _socklen_t),
98 (u'msg_namelen', _socklen_t),
99 (u'msg_iov', ctypes.POINTER(_iovec)),
99 (u'msg_iov', ctypes.POINTER(_iovec)),
100 (u'msg_iovlen', _msg_iovlen_t),
100 (u'msg_iovlen', _msg_iovlen_t),
101 (u'msg_control', ctypes.c_void_p),
101 (u'msg_control', ctypes.c_void_p),
102 (u'msg_controllen', _msg_controllen_t),
102 (u'msg_controllen', _msg_controllen_t),
103 (u'msg_flags', ctypes.c_int),
103 (u'msg_flags', ctypes.c_int),
104 ]
104 ]
105
105
106 class _cmsghdr(ctypes.Structure):
106 class _cmsghdr(ctypes.Structure):
107 _fields_ = [
107 _fields_ = [
108 (u'cmsg_len', _cmsg_len_t),
108 (u'cmsg_len', _cmsg_len_t),
109 (u'cmsg_level', ctypes.c_int),
109 (u'cmsg_level', ctypes.c_int),
110 (u'cmsg_type', ctypes.c_int),
110 (u'cmsg_type', ctypes.c_int),
111 (u'cmsg_data', ctypes.c_ubyte * 0),
111 (u'cmsg_data', ctypes.c_ubyte * 0),
112 ]
112 ]
113
113
114 _libc = ctypes.CDLL(ctypes.util.find_library(u'c'), use_errno=True)
114 _libc = ctypes.CDLL(ctypes.util.find_library(u'c'), use_errno=True)
115 _recvmsg = getattr(_libc, 'recvmsg', None)
115 _recvmsg = getattr(_libc, 'recvmsg', None)
116 if _recvmsg:
116 if _recvmsg:
117 _recvmsg.restype = getattr(ctypes, 'c_ssize_t', ctypes.c_long)
117 _recvmsg.restype = getattr(ctypes, 'c_ssize_t', ctypes.c_long)
118 _recvmsg.argtypes = (
118 _recvmsg.argtypes = (
119 ctypes.c_int,
119 ctypes.c_int,
120 ctypes.POINTER(_msghdr),
120 ctypes.POINTER(_msghdr),
121 ctypes.c_int,
121 ctypes.c_int,
122 )
122 )
123 else:
123 else:
124 # recvmsg isn't always provided by libc; such systems are unsupported
124 # recvmsg isn't always provided by libc; such systems are unsupported
125 def _recvmsg(sockfd, msg, flags):
125 def _recvmsg(sockfd, msg, flags):
126 raise NotImplementedError(b'unsupported platform')
126 raise NotImplementedError(b'unsupported platform')
127
127
128 def _CMSG_FIRSTHDR(msgh):
128 def _CMSG_FIRSTHDR(msgh):
129 if msgh.msg_controllen < ctypes.sizeof(_cmsghdr):
129 if msgh.msg_controllen < ctypes.sizeof(_cmsghdr):
130 return
130 return
131 cmsgptr = ctypes.cast(msgh.msg_control, ctypes.POINTER(_cmsghdr))
131 cmsgptr = ctypes.cast(msgh.msg_control, ctypes.POINTER(_cmsghdr))
132 return cmsgptr.contents
132 return cmsgptr.contents
133
133
134 # The pure version is less portable than the native version because the
134 # The pure version is less portable than the native version because the
135 # handling of socket ancillary data heavily depends on C preprocessor.
135 # handling of socket ancillary data heavily depends on C preprocessor.
136 # Also, some length fields are wrongly typed in Linux kernel.
136 # Also, some length fields are wrongly typed in Linux kernel.
137 def recvfds(sockfd):
137 def recvfds(sockfd):
138 """receive list of file descriptors via socket"""
138 """receive list of file descriptors via socket"""
139 dummy = (ctypes.c_ubyte * 1)()
139 dummy = (ctypes.c_ubyte * 1)()
140 iov = _iovec(ctypes.cast(dummy, ctypes.c_void_p), ctypes.sizeof(dummy))
140 iov = _iovec(ctypes.cast(dummy, ctypes.c_void_p), ctypes.sizeof(dummy))
141 cbuf = ctypes.create_string_buffer(256)
141 cbuf = ctypes.create_string_buffer(256)
142 msgh = _msghdr(
142 msgh = _msghdr(
143 None,
143 None,
144 0,
144 0,
145 ctypes.pointer(iov),
145 ctypes.pointer(iov),
146 1,
146 1,
147 ctypes.cast(cbuf, ctypes.c_void_p),
147 ctypes.cast(cbuf, ctypes.c_void_p),
148 ctypes.sizeof(cbuf),
148 ctypes.sizeof(cbuf),
149 0,
149 0,
150 )
150 )
151 r = _recvmsg(sockfd, ctypes.byref(msgh), 0)
151 r = _recvmsg(sockfd, ctypes.byref(msgh), 0)
152 if r < 0:
152 if r < 0:
153 e = ctypes.get_errno()
153 e = ctypes.get_errno()
154 raise OSError(e, os.strerror(e))
154 raise OSError(e, os.strerror(e))
155 # assumes that the first cmsg has fds because it isn't easy to write
155 # assumes that the first cmsg has fds because it isn't easy to write
156 # portable CMSG_NXTHDR() with ctypes.
156 # portable CMSG_NXTHDR() with ctypes.
157 cmsg = _CMSG_FIRSTHDR(msgh)
157 cmsg = _CMSG_FIRSTHDR(msgh)
158 if not cmsg:
158 if not cmsg:
159 return []
159 return []
160 if (
160 if (
161 cmsg.cmsg_level != socket.SOL_SOCKET
161 cmsg.cmsg_level != socket.SOL_SOCKET
162 or cmsg.cmsg_type != _SCM_RIGHTS
162 or cmsg.cmsg_type != _SCM_RIGHTS
163 ):
163 ):
164 return []
164 return []
165 rfds = ctypes.cast(cmsg.cmsg_data, ctypes.POINTER(ctypes.c_int))
165 rfds = ctypes.cast(cmsg.cmsg_data, ctypes.POINTER(ctypes.c_int))
166 rfdscount = (
166 rfdscount = (
167 cmsg.cmsg_len - _cmsghdr.cmsg_data.offset
167 cmsg.cmsg_len - _cmsghdr.cmsg_data.offset
168 ) // ctypes.sizeof(ctypes.c_int)
168 ) // ctypes.sizeof(ctypes.c_int)
169 return [rfds[i] for i in pycompat.xrange(rfdscount)]
169 return [rfds[i] for i in pycompat.xrange(rfdscount)]
170
170
171
171
172 else:
172 else:
173 import msvcrt
173 import msvcrt
174
174
175 _kernel32 = ctypes.windll.kernel32
175 _kernel32 = ctypes.windll.kernel32 # pytype: disable=module-attr
176
176
177 _DWORD = ctypes.c_ulong
177 _DWORD = ctypes.c_ulong
178 _LPCSTR = _LPSTR = ctypes.c_char_p
178 _LPCSTR = _LPSTR = ctypes.c_char_p
179 _HANDLE = ctypes.c_void_p
179 _HANDLE = ctypes.c_void_p
180
180
181 _INVALID_HANDLE_VALUE = _HANDLE(-1).value
181 _INVALID_HANDLE_VALUE = _HANDLE(-1).value
182
182
183 # CreateFile
183 # CreateFile
184 _FILE_SHARE_READ = 0x00000001
184 _FILE_SHARE_READ = 0x00000001
185 _FILE_SHARE_WRITE = 0x00000002
185 _FILE_SHARE_WRITE = 0x00000002
186 _FILE_SHARE_DELETE = 0x00000004
186 _FILE_SHARE_DELETE = 0x00000004
187
187
188 _CREATE_ALWAYS = 2
188 _CREATE_ALWAYS = 2
189 _OPEN_EXISTING = 3
189 _OPEN_EXISTING = 3
190 _OPEN_ALWAYS = 4
190 _OPEN_ALWAYS = 4
191
191
192 _GENERIC_READ = 0x80000000
192 _GENERIC_READ = 0x80000000
193 _GENERIC_WRITE = 0x40000000
193 _GENERIC_WRITE = 0x40000000
194
194
195 _FILE_ATTRIBUTE_NORMAL = 0x80
195 _FILE_ATTRIBUTE_NORMAL = 0x80
196
196
197 # open_osfhandle flags
197 # open_osfhandle flags
198 _O_RDONLY = 0x0000
198 _O_RDONLY = 0x0000
199 _O_RDWR = 0x0002
199 _O_RDWR = 0x0002
200 _O_APPEND = 0x0008
200 _O_APPEND = 0x0008
201
201
202 _O_TEXT = 0x4000
202 _O_TEXT = 0x4000
203 _O_BINARY = 0x8000
203 _O_BINARY = 0x8000
204
204
205 # types of parameters of C functions used (required by pypy)
205 # types of parameters of C functions used (required by pypy)
206
206
207 _kernel32.CreateFileA.argtypes = [
207 _kernel32.CreateFileA.argtypes = [
208 _LPCSTR,
208 _LPCSTR,
209 _DWORD,
209 _DWORD,
210 _DWORD,
210 _DWORD,
211 ctypes.c_void_p,
211 ctypes.c_void_p,
212 _DWORD,
212 _DWORD,
213 _DWORD,
213 _DWORD,
214 _HANDLE,
214 _HANDLE,
215 ]
215 ]
216 _kernel32.CreateFileA.restype = _HANDLE
216 _kernel32.CreateFileA.restype = _HANDLE
217
217
218 def _raiseioerror(name):
218 def _raiseioerror(name):
219 err = ctypes.WinError()
219 err = ctypes.WinError() # pytype: disable=module-attr
220 raise IOError(
220 raise IOError(
221 err.errno, '%s: %s' % (encoding.strfromlocal(name), err.strerror)
221 err.errno, '%s: %s' % (encoding.strfromlocal(name), err.strerror)
222 )
222 )
223
223
224 class posixfile(object):
224 class posixfile(object):
225 """a file object aiming for POSIX-like semantics
225 """a file object aiming for POSIX-like semantics
226
226
227 CPython's open() returns a file that was opened *without* setting the
227 CPython's open() returns a file that was opened *without* setting the
228 _FILE_SHARE_DELETE flag, which causes rename and unlink to abort.
228 _FILE_SHARE_DELETE flag, which causes rename and unlink to abort.
229 This even happens if any hardlinked copy of the file is in open state.
229 This even happens if any hardlinked copy of the file is in open state.
230 We set _FILE_SHARE_DELETE here, so files opened with posixfile can be
230 We set _FILE_SHARE_DELETE here, so files opened with posixfile can be
231 renamed and deleted while they are held open.
231 renamed and deleted while they are held open.
232 Note that if a file opened with posixfile is unlinked, the file
232 Note that if a file opened with posixfile is unlinked, the file
233 remains but cannot be opened again or be recreated under the same name,
233 remains but cannot be opened again or be recreated under the same name,
234 until all reading processes have closed the file."""
234 until all reading processes have closed the file."""
235
235
236 def __init__(self, name, mode=b'r', bufsize=-1):
236 def __init__(self, name, mode=b'r', bufsize=-1):
237 if b'b' in mode:
237 if b'b' in mode:
238 flags = _O_BINARY
238 flags = _O_BINARY
239 else:
239 else:
240 flags = _O_TEXT
240 flags = _O_TEXT
241
241
242 m0 = mode[0:1]
242 m0 = mode[0:1]
243 if m0 == b'r' and b'+' not in mode:
243 if m0 == b'r' and b'+' not in mode:
244 flags |= _O_RDONLY
244 flags |= _O_RDONLY
245 access = _GENERIC_READ
245 access = _GENERIC_READ
246 else:
246 else:
247 # work around http://support.microsoft.com/kb/899149 and
247 # work around http://support.microsoft.com/kb/899149 and
248 # set _O_RDWR for 'w' and 'a', even if mode has no '+'
248 # set _O_RDWR for 'w' and 'a', even if mode has no '+'
249 flags |= _O_RDWR
249 flags |= _O_RDWR
250 access = _GENERIC_READ | _GENERIC_WRITE
250 access = _GENERIC_READ | _GENERIC_WRITE
251
251
252 if m0 == b'r':
252 if m0 == b'r':
253 creation = _OPEN_EXISTING
253 creation = _OPEN_EXISTING
254 elif m0 == b'w':
254 elif m0 == b'w':
255 creation = _CREATE_ALWAYS
255 creation = _CREATE_ALWAYS
256 elif m0 == b'a':
256 elif m0 == b'a':
257 creation = _OPEN_ALWAYS
257 creation = _OPEN_ALWAYS
258 flags |= _O_APPEND
258 flags |= _O_APPEND
259 else:
259 else:
260 raise ValueError("invalid mode: %s" % pycompat.sysstr(mode))
260 raise ValueError("invalid mode: %s" % pycompat.sysstr(mode))
261
261
262 fh = _kernel32.CreateFileA(
262 fh = _kernel32.CreateFileA(
263 name,
263 name,
264 access,
264 access,
265 _FILE_SHARE_READ | _FILE_SHARE_WRITE | _FILE_SHARE_DELETE,
265 _FILE_SHARE_READ | _FILE_SHARE_WRITE | _FILE_SHARE_DELETE,
266 None,
266 None,
267 creation,
267 creation,
268 _FILE_ATTRIBUTE_NORMAL,
268 _FILE_ATTRIBUTE_NORMAL,
269 None,
269 None,
270 )
270 )
271 if fh == _INVALID_HANDLE_VALUE:
271 if fh == _INVALID_HANDLE_VALUE:
272 _raiseioerror(name)
272 _raiseioerror(name)
273
273
274 fd = msvcrt.open_osfhandle(fh, flags)
274 fd = msvcrt.open_osfhandle(fh, flags) # pytype: disable=module-attr
275 if fd == -1:
275 if fd == -1:
276 _kernel32.CloseHandle(fh)
276 _kernel32.CloseHandle(fh)
277 _raiseioerror(name)
277 _raiseioerror(name)
278
278
279 f = os.fdopen(fd, pycompat.sysstr(mode), bufsize)
279 f = os.fdopen(fd, pycompat.sysstr(mode), bufsize)
280 # unfortunately, f.name is '<fdopen>' at this point -- so we store
280 # unfortunately, f.name is '<fdopen>' at this point -- so we store
281 # the name on this wrapper. We cannot just assign to f.name,
281 # the name on this wrapper. We cannot just assign to f.name,
282 # because that attribute is read-only.
282 # because that attribute is read-only.
283 object.__setattr__(self, 'name', name)
283 object.__setattr__(self, 'name', name)
284 object.__setattr__(self, '_file', f)
284 object.__setattr__(self, '_file', f)
285
285
286 def __iter__(self):
286 def __iter__(self):
287 return self._file
287 return self._file
288
288
289 def __getattr__(self, name):
289 def __getattr__(self, name):
290 return getattr(self._file, name)
290 return getattr(self._file, name)
291
291
292 def __setattr__(self, name, value):
292 def __setattr__(self, name, value):
293 """mimics the read-only attributes of Python file objects
293 """mimics the read-only attributes of Python file objects
294 by raising 'TypeError: readonly attribute' if someone tries:
294 by raising 'TypeError: readonly attribute' if someone tries:
295 f = posixfile('foo.txt')
295 f = posixfile('foo.txt')
296 f.name = 'bla'
296 f.name = 'bla'
297 """
297 """
298 return self._file.__setattr__(name, value)
298 return self._file.__setattr__(name, value)
299
299
300 def __enter__(self):
300 def __enter__(self):
301 self._file.__enter__()
301 self._file.__enter__()
302 return self
302 return self
303
303
304 def __exit__(self, exc_type, exc_value, exc_tb):
304 def __exit__(self, exc_type, exc_value, exc_tb):
305 return self._file.__exit__(exc_type, exc_value, exc_tb)
305 return self._file.__exit__(exc_type, exc_value, exc_tb)
@@ -1,836 +1,838 b''
1 # sslutil.py - SSL handling for mercurial
1 # sslutil.py - SSL handling for mercurial
2 #
2 #
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import re
14 import re
15 import ssl
15 import ssl
16
16
17 from .i18n import _
17 from .i18n import _
18 from .pycompat import getattr
18 from .pycompat import getattr
19 from .node import hex
19 from .node import hex
20 from . import (
20 from . import (
21 encoding,
21 encoding,
22 error,
22 error,
23 pycompat,
23 pycompat,
24 util,
24 util,
25 )
25 )
26 from .utils import (
26 from .utils import (
27 hashutil,
27 hashutil,
28 resourceutil,
28 resourceutil,
29 stringutil,
29 stringutil,
30 )
30 )
31
31
32 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
32 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
33 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
33 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
34 # all exposed via the "ssl" module.
34 # all exposed via the "ssl" module.
35 #
35 #
36 # We require in setup.py the presence of ssl.SSLContext, which indicates modern
36 # We require in setup.py the presence of ssl.SSLContext, which indicates modern
37 # SSL/TLS support.
37 # SSL/TLS support.
38
38
39 configprotocols = {
39 configprotocols = {
40 b'tls1.0',
40 b'tls1.0',
41 b'tls1.1',
41 b'tls1.1',
42 b'tls1.2',
42 b'tls1.2',
43 }
43 }
44
44
45 hassni = getattr(ssl, 'HAS_SNI', False)
45 hassni = getattr(ssl, 'HAS_SNI', False)
46
46
47 # ssl.HAS_TLSv1* are preferred to check support but they were added in Python
47 # ssl.HAS_TLSv1* are preferred to check support but they were added in Python
48 # 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
48 # 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
49 # (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
49 # (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
50 # were defined only if compiled against a OpenSSL version with TLS 1.1 / 1.2
50 # were defined only if compiled against a OpenSSL version with TLS 1.1 / 1.2
51 # support. At the mentioned commit, they were unconditionally defined.
51 # support. At the mentioned commit, they were unconditionally defined.
52 supportedprotocols = set()
52 supportedprotocols = set()
53 if getattr(ssl, 'HAS_TLSv1', util.safehasattr(ssl, 'PROTOCOL_TLSv1')):
53 if getattr(ssl, 'HAS_TLSv1', util.safehasattr(ssl, 'PROTOCOL_TLSv1')):
54 supportedprotocols.add(b'tls1.0')
54 supportedprotocols.add(b'tls1.0')
55 if getattr(ssl, 'HAS_TLSv1_1', util.safehasattr(ssl, 'PROTOCOL_TLSv1_1')):
55 if getattr(ssl, 'HAS_TLSv1_1', util.safehasattr(ssl, 'PROTOCOL_TLSv1_1')):
56 supportedprotocols.add(b'tls1.1')
56 supportedprotocols.add(b'tls1.1')
57 if getattr(ssl, 'HAS_TLSv1_2', util.safehasattr(ssl, 'PROTOCOL_TLSv1_2')):
57 if getattr(ssl, 'HAS_TLSv1_2', util.safehasattr(ssl, 'PROTOCOL_TLSv1_2')):
58 supportedprotocols.add(b'tls1.2')
58 supportedprotocols.add(b'tls1.2')
59
59
60
60
61 def _hostsettings(ui, hostname):
61 def _hostsettings(ui, hostname):
62 """Obtain security settings for a hostname.
62 """Obtain security settings for a hostname.
63
63
64 Returns a dict of settings relevant to that hostname.
64 Returns a dict of settings relevant to that hostname.
65 """
65 """
66 bhostname = pycompat.bytesurl(hostname)
66 bhostname = pycompat.bytesurl(hostname)
67 s = {
67 s = {
68 # Whether we should attempt to load default/available CA certs
68 # Whether we should attempt to load default/available CA certs
69 # if an explicit ``cafile`` is not defined.
69 # if an explicit ``cafile`` is not defined.
70 b'allowloaddefaultcerts': True,
70 b'allowloaddefaultcerts': True,
71 # List of 2-tuple of (hash algorithm, hash).
71 # List of 2-tuple of (hash algorithm, hash).
72 b'certfingerprints': [],
72 b'certfingerprints': [],
73 # Path to file containing concatenated CA certs. Used by
73 # Path to file containing concatenated CA certs. Used by
74 # SSLContext.load_verify_locations().
74 # SSLContext.load_verify_locations().
75 b'cafile': None,
75 b'cafile': None,
76 # Whether certificate verification should be disabled.
76 # Whether certificate verification should be disabled.
77 b'disablecertverification': False,
77 b'disablecertverification': False,
78 # Whether the legacy [hostfingerprints] section has data for this host.
78 # Whether the legacy [hostfingerprints] section has data for this host.
79 b'legacyfingerprint': False,
79 b'legacyfingerprint': False,
80 # String representation of minimum protocol to be used for UI
80 # String representation of minimum protocol to be used for UI
81 # presentation.
81 # presentation.
82 b'minimumprotocol': None,
82 b'minimumprotocol': None,
83 # ssl.CERT_* constant used by SSLContext.verify_mode.
83 # ssl.CERT_* constant used by SSLContext.verify_mode.
84 b'verifymode': None,
84 b'verifymode': None,
85 # OpenSSL Cipher List to use (instead of default).
85 # OpenSSL Cipher List to use (instead of default).
86 b'ciphers': None,
86 b'ciphers': None,
87 }
87 }
88
88
89 # Allow minimum TLS protocol to be specified in the config.
89 # Allow minimum TLS protocol to be specified in the config.
90 def validateprotocol(protocol, key):
90 def validateprotocol(protocol, key):
91 if protocol not in configprotocols:
91 if protocol not in configprotocols:
92 raise error.Abort(
92 raise error.Abort(
93 _(b'unsupported protocol from hostsecurity.%s: %s')
93 _(b'unsupported protocol from hostsecurity.%s: %s')
94 % (key, protocol),
94 % (key, protocol),
95 hint=_(b'valid protocols: %s')
95 hint=_(b'valid protocols: %s')
96 % b' '.join(sorted(configprotocols)),
96 % b' '.join(sorted(configprotocols)),
97 )
97 )
98
98
99 # We default to TLS 1.1+ because TLS 1.0 has known vulnerabilities (like
99 # We default to TLS 1.1+ because TLS 1.0 has known vulnerabilities (like
100 # BEAST and POODLE). We allow users to downgrade to TLS 1.0+ via config
100 # BEAST and POODLE). We allow users to downgrade to TLS 1.0+ via config
101 # options in case a legacy server is encountered.
101 # options in case a legacy server is encountered.
102
102
103 # setup.py checks that TLS 1.1 or TLS 1.2 is present, so the following
103 # setup.py checks that TLS 1.1 or TLS 1.2 is present, so the following
104 # assert should not fail.
104 # assert should not fail.
105 assert supportedprotocols - {b'tls1.0'}
105 assert supportedprotocols - {b'tls1.0'}
106 defaultminimumprotocol = b'tls1.1'
106 defaultminimumprotocol = b'tls1.1'
107
107
108 key = b'minimumprotocol'
108 key = b'minimumprotocol'
109 minimumprotocol = ui.config(b'hostsecurity', key, defaultminimumprotocol)
109 minimumprotocol = ui.config(b'hostsecurity', key, defaultminimumprotocol)
110 validateprotocol(minimumprotocol, key)
110 validateprotocol(minimumprotocol, key)
111
111
112 key = b'%s:minimumprotocol' % bhostname
112 key = b'%s:minimumprotocol' % bhostname
113 minimumprotocol = ui.config(b'hostsecurity', key, minimumprotocol)
113 minimumprotocol = ui.config(b'hostsecurity', key, minimumprotocol)
114 validateprotocol(minimumprotocol, key)
114 validateprotocol(minimumprotocol, key)
115
115
116 # If --insecure is used, we allow the use of TLS 1.0 despite config options.
116 # If --insecure is used, we allow the use of TLS 1.0 despite config options.
117 # We always print a "connection security to %s is disabled..." message when
117 # We always print a "connection security to %s is disabled..." message when
118 # --insecure is used. So no need to print anything more here.
118 # --insecure is used. So no need to print anything more here.
119 if ui.insecureconnections:
119 if ui.insecureconnections:
120 minimumprotocol = b'tls1.0'
120 minimumprotocol = b'tls1.0'
121
121
122 s[b'minimumprotocol'] = minimumprotocol
122 s[b'minimumprotocol'] = minimumprotocol
123
123
124 ciphers = ui.config(b'hostsecurity', b'ciphers')
124 ciphers = ui.config(b'hostsecurity', b'ciphers')
125 ciphers = ui.config(b'hostsecurity', b'%s:ciphers' % bhostname, ciphers)
125 ciphers = ui.config(b'hostsecurity', b'%s:ciphers' % bhostname, ciphers)
126 s[b'ciphers'] = ciphers
126 s[b'ciphers'] = ciphers
127
127
128 # Look for fingerprints in [hostsecurity] section. Value is a list
128 # Look for fingerprints in [hostsecurity] section. Value is a list
129 # of <alg>:<fingerprint> strings.
129 # of <alg>:<fingerprint> strings.
130 fingerprints = ui.configlist(
130 fingerprints = ui.configlist(
131 b'hostsecurity', b'%s:fingerprints' % bhostname
131 b'hostsecurity', b'%s:fingerprints' % bhostname
132 )
132 )
133 for fingerprint in fingerprints:
133 for fingerprint in fingerprints:
134 if not (fingerprint.startswith((b'sha1:', b'sha256:', b'sha512:'))):
134 if not (fingerprint.startswith((b'sha1:', b'sha256:', b'sha512:'))):
135 raise error.Abort(
135 raise error.Abort(
136 _(b'invalid fingerprint for %s: %s') % (bhostname, fingerprint),
136 _(b'invalid fingerprint for %s: %s') % (bhostname, fingerprint),
137 hint=_(b'must begin with "sha1:", "sha256:", or "sha512:"'),
137 hint=_(b'must begin with "sha1:", "sha256:", or "sha512:"'),
138 )
138 )
139
139
140 alg, fingerprint = fingerprint.split(b':', 1)
140 alg, fingerprint = fingerprint.split(b':', 1)
141 fingerprint = fingerprint.replace(b':', b'').lower()
141 fingerprint = fingerprint.replace(b':', b'').lower()
142 s[b'certfingerprints'].append((alg, fingerprint))
142 s[b'certfingerprints'].append((alg, fingerprint))
143
143
144 # Fingerprints from [hostfingerprints] are always SHA-1.
144 # Fingerprints from [hostfingerprints] are always SHA-1.
145 for fingerprint in ui.configlist(b'hostfingerprints', bhostname):
145 for fingerprint in ui.configlist(b'hostfingerprints', bhostname):
146 fingerprint = fingerprint.replace(b':', b'').lower()
146 fingerprint = fingerprint.replace(b':', b'').lower()
147 s[b'certfingerprints'].append((b'sha1', fingerprint))
147 s[b'certfingerprints'].append((b'sha1', fingerprint))
148 s[b'legacyfingerprint'] = True
148 s[b'legacyfingerprint'] = True
149
149
150 # If a host cert fingerprint is defined, it is the only thing that
150 # If a host cert fingerprint is defined, it is the only thing that
151 # matters. No need to validate CA certs.
151 # matters. No need to validate CA certs.
152 if s[b'certfingerprints']:
152 if s[b'certfingerprints']:
153 s[b'verifymode'] = ssl.CERT_NONE
153 s[b'verifymode'] = ssl.CERT_NONE
154 s[b'allowloaddefaultcerts'] = False
154 s[b'allowloaddefaultcerts'] = False
155
155
156 # If --insecure is used, don't take CAs into consideration.
156 # If --insecure is used, don't take CAs into consideration.
157 elif ui.insecureconnections:
157 elif ui.insecureconnections:
158 s[b'disablecertverification'] = True
158 s[b'disablecertverification'] = True
159 s[b'verifymode'] = ssl.CERT_NONE
159 s[b'verifymode'] = ssl.CERT_NONE
160 s[b'allowloaddefaultcerts'] = False
160 s[b'allowloaddefaultcerts'] = False
161
161
162 if ui.configbool(b'devel', b'disableloaddefaultcerts'):
162 if ui.configbool(b'devel', b'disableloaddefaultcerts'):
163 s[b'allowloaddefaultcerts'] = False
163 s[b'allowloaddefaultcerts'] = False
164
164
165 # If both fingerprints and a per-host ca file are specified, issue a warning
165 # If both fingerprints and a per-host ca file are specified, issue a warning
166 # because users should not be surprised about what security is or isn't
166 # because users should not be surprised about what security is or isn't
167 # being performed.
167 # being performed.
168 cafile = ui.config(b'hostsecurity', b'%s:verifycertsfile' % bhostname)
168 cafile = ui.config(b'hostsecurity', b'%s:verifycertsfile' % bhostname)
169 if s[b'certfingerprints'] and cafile:
169 if s[b'certfingerprints'] and cafile:
170 ui.warn(
170 ui.warn(
171 _(
171 _(
172 b'(hostsecurity.%s:verifycertsfile ignored when host '
172 b'(hostsecurity.%s:verifycertsfile ignored when host '
173 b'fingerprints defined; using host fingerprints for '
173 b'fingerprints defined; using host fingerprints for '
174 b'verification)\n'
174 b'verification)\n'
175 )
175 )
176 % bhostname
176 % bhostname
177 )
177 )
178
178
179 # Try to hook up CA certificate validation unless something above
179 # Try to hook up CA certificate validation unless something above
180 # makes it not necessary.
180 # makes it not necessary.
181 if s[b'verifymode'] is None:
181 if s[b'verifymode'] is None:
182 # Look at per-host ca file first.
182 # Look at per-host ca file first.
183 if cafile:
183 if cafile:
184 cafile = util.expandpath(cafile)
184 cafile = util.expandpath(cafile)
185 if not os.path.exists(cafile):
185 if not os.path.exists(cafile):
186 raise error.Abort(
186 raise error.Abort(
187 _(b'path specified by %s does not exist: %s')
187 _(b'path specified by %s does not exist: %s')
188 % (
188 % (
189 b'hostsecurity.%s:verifycertsfile' % (bhostname,),
189 b'hostsecurity.%s:verifycertsfile' % (bhostname,),
190 cafile,
190 cafile,
191 )
191 )
192 )
192 )
193 s[b'cafile'] = cafile
193 s[b'cafile'] = cafile
194 else:
194 else:
195 # Find global certificates file in config.
195 # Find global certificates file in config.
196 cafile = ui.config(b'web', b'cacerts')
196 cafile = ui.config(b'web', b'cacerts')
197
197
198 if cafile:
198 if cafile:
199 cafile = util.expandpath(cafile)
199 cafile = util.expandpath(cafile)
200 if not os.path.exists(cafile):
200 if not os.path.exists(cafile):
201 raise error.Abort(
201 raise error.Abort(
202 _(b'could not find web.cacerts: %s') % cafile
202 _(b'could not find web.cacerts: %s') % cafile
203 )
203 )
204 elif s[b'allowloaddefaultcerts']:
204 elif s[b'allowloaddefaultcerts']:
205 # CAs not defined in config. Try to find system bundles.
205 # CAs not defined in config. Try to find system bundles.
206 cafile = _defaultcacerts(ui)
206 cafile = _defaultcacerts(ui)
207 if cafile:
207 if cafile:
208 ui.debug(b'using %s for CA file\n' % cafile)
208 ui.debug(b'using %s for CA file\n' % cafile)
209
209
210 s[b'cafile'] = cafile
210 s[b'cafile'] = cafile
211
211
212 # Require certificate validation if CA certs are being loaded and
212 # Require certificate validation if CA certs are being loaded and
213 # verification hasn't been disabled above.
213 # verification hasn't been disabled above.
214 if cafile or s[b'allowloaddefaultcerts']:
214 if cafile or s[b'allowloaddefaultcerts']:
215 s[b'verifymode'] = ssl.CERT_REQUIRED
215 s[b'verifymode'] = ssl.CERT_REQUIRED
216 else:
216 else:
217 # At this point we don't have a fingerprint, aren't being
217 # At this point we don't have a fingerprint, aren't being
218 # explicitly insecure, and can't load CA certs. Connecting
218 # explicitly insecure, and can't load CA certs. Connecting
219 # is insecure. We allow the connection and abort during
219 # is insecure. We allow the connection and abort during
220 # validation (once we have the fingerprint to print to the
220 # validation (once we have the fingerprint to print to the
221 # user).
221 # user).
222 s[b'verifymode'] = ssl.CERT_NONE
222 s[b'verifymode'] = ssl.CERT_NONE
223
223
224 assert s[b'verifymode'] is not None
224 assert s[b'verifymode'] is not None
225
225
226 return s
226 return s
227
227
228
228
229 def commonssloptions(minimumprotocol):
229 def commonssloptions(minimumprotocol):
230 """Return SSLContext options common to servers and clients."""
230 """Return SSLContext options common to servers and clients."""
231 if minimumprotocol not in configprotocols:
231 if minimumprotocol not in configprotocols:
232 raise ValueError(b'protocol value not supported: %s' % minimumprotocol)
232 raise ValueError(b'protocol value not supported: %s' % minimumprotocol)
233
233
234 # SSLv2 and SSLv3 are broken. We ban them outright.
234 # SSLv2 and SSLv3 are broken. We ban them outright.
235 options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
235 options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
236
236
237 if minimumprotocol == b'tls1.0':
237 if minimumprotocol == b'tls1.0':
238 # Defaults above are to use TLS 1.0+
238 # Defaults above are to use TLS 1.0+
239 pass
239 pass
240 elif minimumprotocol == b'tls1.1':
240 elif minimumprotocol == b'tls1.1':
241 options |= ssl.OP_NO_TLSv1
241 options |= ssl.OP_NO_TLSv1
242 elif minimumprotocol == b'tls1.2':
242 elif minimumprotocol == b'tls1.2':
243 options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
243 options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
244 else:
244 else:
245 raise error.Abort(_(b'this should not happen'))
245 raise error.Abort(_(b'this should not happen'))
246
246
247 # Prevent CRIME.
247 # Prevent CRIME.
248 # There is no guarantee this attribute is defined on the module.
248 # There is no guarantee this attribute is defined on the module.
249 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
249 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
250
250
251 return options
251 return options
252
252
253
253
254 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
254 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
255 """Add SSL/TLS to a socket.
255 """Add SSL/TLS to a socket.
256
256
257 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
257 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
258 choices based on what security options are available.
258 choices based on what security options are available.
259
259
260 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
260 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
261 the following additional arguments:
261 the following additional arguments:
262
262
263 * serverhostname - The expected hostname of the remote server. If the
263 * serverhostname - The expected hostname of the remote server. If the
264 server (and client) support SNI, this tells the server which certificate
264 server (and client) support SNI, this tells the server which certificate
265 to use.
265 to use.
266 """
266 """
267 if not serverhostname:
267 if not serverhostname:
268 raise error.Abort(_(b'serverhostname argument is required'))
268 raise error.Abort(_(b'serverhostname argument is required'))
269
269
270 if b'SSLKEYLOGFILE' in encoding.environ:
270 if b'SSLKEYLOGFILE' in encoding.environ:
271 try:
271 try:
272 import sslkeylog # pytype: disable=import-error
272 import sslkeylog # pytype: disable=import-error
273
273
274 sslkeylog.set_keylog(
274 sslkeylog.set_keylog(
275 pycompat.fsdecode(encoding.environ[b'SSLKEYLOGFILE'])
275 pycompat.fsdecode(encoding.environ[b'SSLKEYLOGFILE'])
276 )
276 )
277 ui.warnnoi18n(
277 ui.warnnoi18n(
278 b'sslkeylog enabled by SSLKEYLOGFILE environment variable\n'
278 b'sslkeylog enabled by SSLKEYLOGFILE environment variable\n'
279 )
279 )
280 except ImportError:
280 except ImportError:
281 ui.warnnoi18n(
281 ui.warnnoi18n(
282 b'sslkeylog module missing, '
282 b'sslkeylog module missing, '
283 b'but SSLKEYLOGFILE set in environment\n'
283 b'but SSLKEYLOGFILE set in environment\n'
284 )
284 )
285
285
286 for f in (keyfile, certfile):
286 for f in (keyfile, certfile):
287 if f and not os.path.exists(f):
287 if f and not os.path.exists(f):
288 raise error.Abort(
288 raise error.Abort(
289 _(b'certificate file (%s) does not exist; cannot connect to %s')
289 _(b'certificate file (%s) does not exist; cannot connect to %s')
290 % (f, pycompat.bytesurl(serverhostname)),
290 % (f, pycompat.bytesurl(serverhostname)),
291 hint=_(
291 hint=_(
292 b'restore missing file or fix references '
292 b'restore missing file or fix references '
293 b'in Mercurial config'
293 b'in Mercurial config'
294 ),
294 ),
295 )
295 )
296
296
297 settings = _hostsettings(ui, serverhostname)
297 settings = _hostsettings(ui, serverhostname)
298
298
299 # We can't use ssl.create_default_context() because it calls
299 # We can't use ssl.create_default_context() because it calls
300 # load_default_certs() unless CA arguments are passed to it. We want to
300 # load_default_certs() unless CA arguments are passed to it. We want to
301 # have explicit control over CA loading because implicitly loading
301 # have explicit control over CA loading because implicitly loading
302 # CAs may undermine the user's intent. For example, a user may define a CA
302 # CAs may undermine the user's intent. For example, a user may define a CA
303 # bundle with a specific CA cert removed. If the system/default CA bundle
303 # bundle with a specific CA cert removed. If the system/default CA bundle
304 # is loaded and contains that removed CA, you've just undone the user's
304 # is loaded and contains that removed CA, you've just undone the user's
305 # choice.
305 # choice.
306 #
306 #
307 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
307 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
308 # ends support, including TLS protocols. commonssloptions() restricts the
308 # ends support, including TLS protocols. commonssloptions() restricts the
309 # set of allowed protocols.
309 # set of allowed protocols.
310 sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
310 sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
311 sslcontext.options |= commonssloptions(settings[b'minimumprotocol'])
311 sslcontext.options |= commonssloptions(settings[b'minimumprotocol'])
312 sslcontext.verify_mode = settings[b'verifymode']
312 sslcontext.verify_mode = settings[b'verifymode']
313
313
314 if settings[b'ciphers']:
314 if settings[b'ciphers']:
315 try:
315 try:
316 sslcontext.set_ciphers(pycompat.sysstr(settings[b'ciphers']))
316 sslcontext.set_ciphers(pycompat.sysstr(settings[b'ciphers']))
317 except ssl.SSLError as e:
317 except ssl.SSLError as e:
318 raise error.Abort(
318 raise error.Abort(
319 _(b'could not set ciphers: %s')
319 _(b'could not set ciphers: %s')
320 % stringutil.forcebytestr(e.args[0]),
320 % stringutil.forcebytestr(e.args[0]),
321 hint=_(b'change cipher string (%s) in config')
321 hint=_(b'change cipher string (%s) in config')
322 % settings[b'ciphers'],
322 % settings[b'ciphers'],
323 )
323 )
324
324
325 if certfile is not None:
325 if certfile is not None:
326
326
327 def password():
327 def password():
328 f = keyfile or certfile
328 f = keyfile or certfile
329 return ui.getpass(_(b'passphrase for %s: ') % f, b'')
329 return ui.getpass(_(b'passphrase for %s: ') % f, b'')
330
330
331 sslcontext.load_cert_chain(certfile, keyfile, password)
331 sslcontext.load_cert_chain(certfile, keyfile, password)
332
332
333 if settings[b'cafile'] is not None:
333 if settings[b'cafile'] is not None:
334 try:
334 try:
335 sslcontext.load_verify_locations(cafile=settings[b'cafile'])
335 sslcontext.load_verify_locations(cafile=settings[b'cafile'])
336 except ssl.SSLError as e:
336 except ssl.SSLError as e:
337 if len(e.args) == 1: # pypy has different SSLError args
337 if len(e.args) == 1: # pypy has different SSLError args
338 msg = e.args[0]
338 msg = e.args[0]
339 else:
339 else:
340 msg = e.args[1]
340 msg = e.args[1]
341 raise error.Abort(
341 raise error.Abort(
342 _(b'error loading CA file %s: %s')
342 _(b'error loading CA file %s: %s')
343 % (settings[b'cafile'], stringutil.forcebytestr(msg)),
343 % (settings[b'cafile'], stringutil.forcebytestr(msg)),
344 hint=_(b'file is empty or malformed?'),
344 hint=_(b'file is empty or malformed?'),
345 )
345 )
346 caloaded = True
346 caloaded = True
347 elif settings[b'allowloaddefaultcerts']:
347 elif settings[b'allowloaddefaultcerts']:
348 # This is a no-op on old Python.
348 # This is a no-op on old Python.
349 sslcontext.load_default_certs()
349 sslcontext.load_default_certs()
350 caloaded = True
350 caloaded = True
351 else:
351 else:
352 caloaded = False
352 caloaded = False
353
353
354 try:
354 try:
355 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
355 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
356 except ssl.SSLError as e:
356 except ssl.SSLError as e:
357 # If we're doing certificate verification and no CA certs are loaded,
357 # If we're doing certificate verification and no CA certs are loaded,
358 # that is almost certainly the reason why verification failed. Provide
358 # that is almost certainly the reason why verification failed. Provide
359 # a hint to the user.
359 # a hint to the user.
360 # The exception handler is here to handle bugs around cert attributes:
360 # The exception handler is here to handle bugs around cert attributes:
361 # https://bugs.python.org/issue20916#msg213479. (See issues5313.)
361 # https://bugs.python.org/issue20916#msg213479. (See issues5313.)
362 # When the main 20916 bug occurs, 'sslcontext.get_ca_certs()' is a
362 # When the main 20916 bug occurs, 'sslcontext.get_ca_certs()' is a
363 # non-empty list, but the following conditional is otherwise True.
363 # non-empty list, but the following conditional is otherwise True.
364 try:
364 try:
365 if (
365 if (
366 caloaded
366 caloaded
367 and settings[b'verifymode'] == ssl.CERT_REQUIRED
367 and settings[b'verifymode'] == ssl.CERT_REQUIRED
368 and not sslcontext.get_ca_certs()
368 and not sslcontext.get_ca_certs()
369 ):
369 ):
370 ui.warn(
370 ui.warn(
371 _(
371 _(
372 b'(an attempt was made to load CA certificates but '
372 b'(an attempt was made to load CA certificates but '
373 b'none were loaded; see '
373 b'none were loaded; see '
374 b'https://mercurial-scm.org/wiki/SecureConnections '
374 b'https://mercurial-scm.org/wiki/SecureConnections '
375 b'for how to configure Mercurial to avoid this '
375 b'for how to configure Mercurial to avoid this '
376 b'error)\n'
376 b'error)\n'
377 )
377 )
378 )
378 )
379 except ssl.SSLError:
379 except ssl.SSLError:
380 pass
380 pass
381
381
382 # Try to print more helpful error messages for known failures.
382 # Try to print more helpful error messages for known failures.
383 if util.safehasattr(e, b'reason'):
383 if util.safehasattr(e, b'reason'):
384 # This error occurs when the client and server don't share a
384 # This error occurs when the client and server don't share a
385 # common/supported SSL/TLS protocol. We've disabled SSLv2 and SSLv3
385 # common/supported SSL/TLS protocol. We've disabled SSLv2 and SSLv3
386 # outright. Hopefully the reason for this error is that we require
386 # outright. Hopefully the reason for this error is that we require
387 # TLS 1.1+ and the server only supports TLS 1.0. Whatever the
387 # TLS 1.1+ and the server only supports TLS 1.0. Whatever the
388 # reason, try to emit an actionable warning.
388 # reason, try to emit an actionable warning.
389 if e.reason == 'UNSUPPORTED_PROTOCOL':
389 if e.reason == 'UNSUPPORTED_PROTOCOL':
390 # We attempted TLS 1.0+.
390 # We attempted TLS 1.0+.
391 if settings[b'minimumprotocol'] == b'tls1.0':
391 if settings[b'minimumprotocol'] == b'tls1.0':
392 # We support more than just TLS 1.0+. If this happens,
392 # We support more than just TLS 1.0+. If this happens,
393 # the likely scenario is either the client or the server
393 # the likely scenario is either the client or the server
394 # is really old. (e.g. server doesn't support TLS 1.0+ or
394 # is really old. (e.g. server doesn't support TLS 1.0+ or
395 # client doesn't support modern TLS versions introduced
395 # client doesn't support modern TLS versions introduced
396 # several years from when this comment was written).
396 # several years from when this comment was written).
397 if supportedprotocols != {b'tls1.0'}:
397 if supportedprotocols != {b'tls1.0'}:
398 ui.warn(
398 ui.warn(
399 _(
399 _(
400 b'(could not communicate with %s using security '
400 b'(could not communicate with %s using security '
401 b'protocols %s; if you are using a modern Mercurial '
401 b'protocols %s; if you are using a modern Mercurial '
402 b'version, consider contacting the operator of this '
402 b'version, consider contacting the operator of this '
403 b'server; see '
403 b'server; see '
404 b'https://mercurial-scm.org/wiki/SecureConnections '
404 b'https://mercurial-scm.org/wiki/SecureConnections '
405 b'for more info)\n'
405 b'for more info)\n'
406 )
406 )
407 % (
407 % (
408 pycompat.bytesurl(serverhostname),
408 pycompat.bytesurl(serverhostname),
409 b', '.join(sorted(supportedprotocols)),
409 b', '.join(sorted(supportedprotocols)),
410 )
410 )
411 )
411 )
412 else:
412 else:
413 ui.warn(
413 ui.warn(
414 _(
414 _(
415 b'(could not communicate with %s using TLS 1.0; the '
415 b'(could not communicate with %s using TLS 1.0; the '
416 b'likely cause of this is the server no longer '
416 b'likely cause of this is the server no longer '
417 b'supports TLS 1.0 because it has known security '
417 b'supports TLS 1.0 because it has known security '
418 b'vulnerabilities; see '
418 b'vulnerabilities; see '
419 b'https://mercurial-scm.org/wiki/SecureConnections '
419 b'https://mercurial-scm.org/wiki/SecureConnections '
420 b'for more info)\n'
420 b'for more info)\n'
421 )
421 )
422 % pycompat.bytesurl(serverhostname)
422 % pycompat.bytesurl(serverhostname)
423 )
423 )
424 else:
424 else:
425 # We attempted TLS 1.1+. We can only get here if the client
425 # We attempted TLS 1.1+. We can only get here if the client
426 # supports the configured protocol. So the likely reason is
426 # supports the configured protocol. So the likely reason is
427 # the client wants better security than the server can
427 # the client wants better security than the server can
428 # offer.
428 # offer.
429 ui.warn(
429 ui.warn(
430 _(
430 _(
431 b'(could not negotiate a common security protocol (%s+) '
431 b'(could not negotiate a common security protocol (%s+) '
432 b'with %s; the likely cause is Mercurial is configured '
432 b'with %s; the likely cause is Mercurial is configured '
433 b'to be more secure than the server can support)\n'
433 b'to be more secure than the server can support)\n'
434 )
434 )
435 % (
435 % (
436 settings[b'minimumprotocol'],
436 settings[b'minimumprotocol'],
437 pycompat.bytesurl(serverhostname),
437 pycompat.bytesurl(serverhostname),
438 )
438 )
439 )
439 )
440 ui.warn(
440 ui.warn(
441 _(
441 _(
442 b'(consider contacting the operator of this '
442 b'(consider contacting the operator of this '
443 b'server and ask them to support modern TLS '
443 b'server and ask them to support modern TLS '
444 b'protocol versions; or, set '
444 b'protocol versions; or, set '
445 b'hostsecurity.%s:minimumprotocol=tls1.0 to allow '
445 b'hostsecurity.%s:minimumprotocol=tls1.0 to allow '
446 b'use of legacy, less secure protocols when '
446 b'use of legacy, less secure protocols when '
447 b'communicating with this server)\n'
447 b'communicating with this server)\n'
448 )
448 )
449 % pycompat.bytesurl(serverhostname)
449 % pycompat.bytesurl(serverhostname)
450 )
450 )
451 ui.warn(
451 ui.warn(
452 _(
452 _(
453 b'(see https://mercurial-scm.org/wiki/SecureConnections '
453 b'(see https://mercurial-scm.org/wiki/SecureConnections '
454 b'for more info)\n'
454 b'for more info)\n'
455 )
455 )
456 )
456 )
457
457
458 elif e.reason == 'CERTIFICATE_VERIFY_FAILED' and pycompat.iswindows:
458 elif e.reason == 'CERTIFICATE_VERIFY_FAILED' and pycompat.iswindows:
459
459
460 ui.warn(
460 ui.warn(
461 _(
461 _(
462 b'(the full certificate chain may not be available '
462 b'(the full certificate chain may not be available '
463 b'locally; see "hg help debugssl")\n'
463 b'locally; see "hg help debugssl")\n'
464 )
464 )
465 )
465 )
466 raise
466 raise
467
467
468 # check if wrap_socket failed silently because socket had been
468 # check if wrap_socket failed silently because socket had been
469 # closed
469 # closed
470 # - see http://bugs.python.org/issue13721
470 # - see http://bugs.python.org/issue13721
471 if not sslsocket.cipher():
471 if not sslsocket.cipher():
472 raise error.SecurityError(_(b'ssl connection failed'))
472 raise error.SecurityError(_(b'ssl connection failed'))
473
473
474 sslsocket._hgstate = {
474 sslsocket._hgstate = {
475 b'caloaded': caloaded,
475 b'caloaded': caloaded,
476 b'hostname': serverhostname,
476 b'hostname': serverhostname,
477 b'settings': settings,
477 b'settings': settings,
478 b'ui': ui,
478 b'ui': ui,
479 }
479 }
480
480
481 return sslsocket
481 return sslsocket
482
482
483
483
484 def wrapserversocket(
484 def wrapserversocket(
485 sock, ui, certfile=None, keyfile=None, cafile=None, requireclientcert=False
485 sock, ui, certfile=None, keyfile=None, cafile=None, requireclientcert=False
486 ):
486 ):
487 """Wrap a socket for use by servers.
487 """Wrap a socket for use by servers.
488
488
489 ``certfile`` and ``keyfile`` specify the files containing the certificate's
489 ``certfile`` and ``keyfile`` specify the files containing the certificate's
490 public and private keys, respectively. Both keys can be defined in the same
490 public and private keys, respectively. Both keys can be defined in the same
491 file via ``certfile`` (the private key must come first in the file).
491 file via ``certfile`` (the private key must come first in the file).
492
492
493 ``cafile`` defines the path to certificate authorities.
493 ``cafile`` defines the path to certificate authorities.
494
494
495 ``requireclientcert`` specifies whether to require client certificates.
495 ``requireclientcert`` specifies whether to require client certificates.
496
496
497 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
497 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
498 """
498 """
499 # This function is not used much by core Mercurial, so the error messaging
499 # This function is not used much by core Mercurial, so the error messaging
500 # doesn't have to be as detailed as for wrapsocket().
500 # doesn't have to be as detailed as for wrapsocket().
501 for f in (certfile, keyfile, cafile):
501 for f in (certfile, keyfile, cafile):
502 if f and not os.path.exists(f):
502 if f and not os.path.exists(f):
503 raise error.Abort(
503 raise error.Abort(
504 _(b'referenced certificate file (%s) does not exist') % f
504 _(b'referenced certificate file (%s) does not exist') % f
505 )
505 )
506
506
507 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
507 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
508 # ends support, including TLS protocols. commonssloptions() restricts the
508 # ends support, including TLS protocols. commonssloptions() restricts the
509 # set of allowed protocols.
509 # set of allowed protocols.
510 protocol = ssl.PROTOCOL_SSLv23
510 protocol = ssl.PROTOCOL_SSLv23
511 options = commonssloptions(b'tls1.0')
511 options = commonssloptions(b'tls1.0')
512
512
513 # This config option is intended for use in tests only. It is a giant
513 # This config option is intended for use in tests only. It is a giant
514 # footgun to kill security. Don't define it.
514 # footgun to kill security. Don't define it.
515 exactprotocol = ui.config(b'devel', b'serverexactprotocol')
515 exactprotocol = ui.config(b'devel', b'serverexactprotocol')
516 if exactprotocol == b'tls1.0':
516 if exactprotocol == b'tls1.0':
517 if b'tls1.0' not in supportedprotocols:
517 if b'tls1.0' not in supportedprotocols:
518 raise error.Abort(_(b'TLS 1.0 not supported by this Python'))
518 raise error.Abort(_(b'TLS 1.0 not supported by this Python'))
519 protocol = ssl.PROTOCOL_TLSv1
519 protocol = ssl.PROTOCOL_TLSv1
520 elif exactprotocol == b'tls1.1':
520 elif exactprotocol == b'tls1.1':
521 if b'tls1.1' not in supportedprotocols:
521 if b'tls1.1' not in supportedprotocols:
522 raise error.Abort(_(b'TLS 1.1 not supported by this Python'))
522 raise error.Abort(_(b'TLS 1.1 not supported by this Python'))
523 protocol = ssl.PROTOCOL_TLSv1_1
523 protocol = ssl.PROTOCOL_TLSv1_1
524 elif exactprotocol == b'tls1.2':
524 elif exactprotocol == b'tls1.2':
525 if b'tls1.2' not in supportedprotocols:
525 if b'tls1.2' not in supportedprotocols:
526 raise error.Abort(_(b'TLS 1.2 not supported by this Python'))
526 raise error.Abort(_(b'TLS 1.2 not supported by this Python'))
527 protocol = ssl.PROTOCOL_TLSv1_2
527 protocol = ssl.PROTOCOL_TLSv1_2
528 elif exactprotocol:
528 elif exactprotocol:
529 raise error.Abort(
529 raise error.Abort(
530 _(b'invalid value for serverexactprotocol: %s') % exactprotocol
530 _(b'invalid value for serverexactprotocol: %s') % exactprotocol
531 )
531 )
532
532
533 # We /could/ use create_default_context() here since it doesn't load
533 # We /could/ use create_default_context() here since it doesn't load
534 # CAs when configured for client auth. However, it is hard-coded to
534 # CAs when configured for client auth. However, it is hard-coded to
535 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
535 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
536 sslcontext = ssl.SSLContext(protocol)
536 sslcontext = ssl.SSLContext(protocol)
537 sslcontext.options |= options
537 sslcontext.options |= options
538
538
539 # Improve forward secrecy.
539 # Improve forward secrecy.
540 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
540 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
541 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
541 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
542
542
543 # Use the list of more secure ciphers if found in the ssl module.
543 # Use the list of more secure ciphers if found in the ssl module.
544 if util.safehasattr(ssl, b'_RESTRICTED_SERVER_CIPHERS'):
544 if util.safehasattr(ssl, b'_RESTRICTED_SERVER_CIPHERS'):
545 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
545 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
546 # pytype: disable=module-attr
546 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
547 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
548 # pytype: enable=module-attr
547
549
548 if requireclientcert:
550 if requireclientcert:
549 sslcontext.verify_mode = ssl.CERT_REQUIRED
551 sslcontext.verify_mode = ssl.CERT_REQUIRED
550 else:
552 else:
551 sslcontext.verify_mode = ssl.CERT_NONE
553 sslcontext.verify_mode = ssl.CERT_NONE
552
554
553 if certfile or keyfile:
555 if certfile or keyfile:
554 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
556 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
555
557
556 if cafile:
558 if cafile:
557 sslcontext.load_verify_locations(cafile=cafile)
559 sslcontext.load_verify_locations(cafile=cafile)
558
560
559 return sslcontext.wrap_socket(sock, server_side=True)
561 return sslcontext.wrap_socket(sock, server_side=True)
560
562
561
563
562 class wildcarderror(Exception):
564 class wildcarderror(Exception):
563 """Represents an error parsing wildcards in DNS name."""
565 """Represents an error parsing wildcards in DNS name."""
564
566
565
567
566 def _dnsnamematch(dn, hostname, maxwildcards=1):
568 def _dnsnamematch(dn, hostname, maxwildcards=1):
567 """Match DNS names according RFC 6125 section 6.4.3.
569 """Match DNS names according RFC 6125 section 6.4.3.
568
570
569 This code is effectively copied from CPython's ssl._dnsname_match.
571 This code is effectively copied from CPython's ssl._dnsname_match.
570
572
571 Returns a bool indicating whether the expected hostname matches
573 Returns a bool indicating whether the expected hostname matches
572 the value in ``dn``.
574 the value in ``dn``.
573 """
575 """
574 pats = []
576 pats = []
575 if not dn:
577 if not dn:
576 return False
578 return False
577 dn = pycompat.bytesurl(dn)
579 dn = pycompat.bytesurl(dn)
578 hostname = pycompat.bytesurl(hostname)
580 hostname = pycompat.bytesurl(hostname)
579
581
580 pieces = dn.split(b'.')
582 pieces = dn.split(b'.')
581 leftmost = pieces[0]
583 leftmost = pieces[0]
582 remainder = pieces[1:]
584 remainder = pieces[1:]
583 wildcards = leftmost.count(b'*')
585 wildcards = leftmost.count(b'*')
584 if wildcards > maxwildcards:
586 if wildcards > maxwildcards:
585 raise wildcarderror(
587 raise wildcarderror(
586 _(b'too many wildcards in certificate DNS name: %s') % dn
588 _(b'too many wildcards in certificate DNS name: %s') % dn
587 )
589 )
588
590
589 # speed up common case w/o wildcards
591 # speed up common case w/o wildcards
590 if not wildcards:
592 if not wildcards:
591 return dn.lower() == hostname.lower()
593 return dn.lower() == hostname.lower()
592
594
593 # RFC 6125, section 6.4.3, subitem 1.
595 # RFC 6125, section 6.4.3, subitem 1.
594 # The client SHOULD NOT attempt to match a presented identifier in which
596 # The client SHOULD NOT attempt to match a presented identifier in which
595 # the wildcard character comprises a label other than the left-most label.
597 # the wildcard character comprises a label other than the left-most label.
596 if leftmost == b'*':
598 if leftmost == b'*':
597 # When '*' is a fragment by itself, it matches a non-empty dotless
599 # When '*' is a fragment by itself, it matches a non-empty dotless
598 # fragment.
600 # fragment.
599 pats.append(b'[^.]+')
601 pats.append(b'[^.]+')
600 elif leftmost.startswith(b'xn--') or hostname.startswith(b'xn--'):
602 elif leftmost.startswith(b'xn--') or hostname.startswith(b'xn--'):
601 # RFC 6125, section 6.4.3, subitem 3.
603 # RFC 6125, section 6.4.3, subitem 3.
602 # The client SHOULD NOT attempt to match a presented identifier
604 # The client SHOULD NOT attempt to match a presented identifier
603 # where the wildcard character is embedded within an A-label or
605 # where the wildcard character is embedded within an A-label or
604 # U-label of an internationalized domain name.
606 # U-label of an internationalized domain name.
605 pats.append(stringutil.reescape(leftmost))
607 pats.append(stringutil.reescape(leftmost))
606 else:
608 else:
607 # Otherwise, '*' matches any dotless string, e.g. www*
609 # Otherwise, '*' matches any dotless string, e.g. www*
608 pats.append(stringutil.reescape(leftmost).replace(br'\*', b'[^.]*'))
610 pats.append(stringutil.reescape(leftmost).replace(br'\*', b'[^.]*'))
609
611
610 # add the remaining fragments, ignore any wildcards
612 # add the remaining fragments, ignore any wildcards
611 for frag in remainder:
613 for frag in remainder:
612 pats.append(stringutil.reescape(frag))
614 pats.append(stringutil.reescape(frag))
613
615
614 pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE)
616 pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE)
615 return pat.match(hostname) is not None
617 return pat.match(hostname) is not None
616
618
617
619
618 def _verifycert(cert, hostname):
620 def _verifycert(cert, hostname):
619 """Verify that cert (in socket.getpeercert() format) matches hostname.
621 """Verify that cert (in socket.getpeercert() format) matches hostname.
620 CRLs is not handled.
622 CRLs is not handled.
621
623
622 Returns error message if any problems are found and None on success.
624 Returns error message if any problems are found and None on success.
623 """
625 """
624 if not cert:
626 if not cert:
625 return _(b'no certificate received')
627 return _(b'no certificate received')
626
628
627 dnsnames = []
629 dnsnames = []
628 san = cert.get('subjectAltName', [])
630 san = cert.get('subjectAltName', [])
629 for key, value in san:
631 for key, value in san:
630 if key == 'DNS':
632 if key == 'DNS':
631 try:
633 try:
632 if _dnsnamematch(value, hostname):
634 if _dnsnamematch(value, hostname):
633 return
635 return
634 except wildcarderror as e:
636 except wildcarderror as e:
635 return stringutil.forcebytestr(e.args[0])
637 return stringutil.forcebytestr(e.args[0])
636
638
637 dnsnames.append(value)
639 dnsnames.append(value)
638
640
639 if not dnsnames:
641 if not dnsnames:
640 # The subject is only checked when there is no DNS in subjectAltName.
642 # The subject is only checked when there is no DNS in subjectAltName.
641 for sub in cert.get('subject', []):
643 for sub in cert.get('subject', []):
642 for key, value in sub:
644 for key, value in sub:
643 # According to RFC 2818 the most specific Common Name must
645 # According to RFC 2818 the most specific Common Name must
644 # be used.
646 # be used.
645 if key == 'commonName':
647 if key == 'commonName':
646 # 'subject' entries are unicode.
648 # 'subject' entries are unicode.
647 try:
649 try:
648 value = value.encode('ascii')
650 value = value.encode('ascii')
649 except UnicodeEncodeError:
651 except UnicodeEncodeError:
650 return _(b'IDN in certificate not supported')
652 return _(b'IDN in certificate not supported')
651
653
652 try:
654 try:
653 if _dnsnamematch(value, hostname):
655 if _dnsnamematch(value, hostname):
654 return
656 return
655 except wildcarderror as e:
657 except wildcarderror as e:
656 return stringutil.forcebytestr(e.args[0])
658 return stringutil.forcebytestr(e.args[0])
657
659
658 dnsnames.append(value)
660 dnsnames.append(value)
659
661
660 dnsnames = [pycompat.bytesurl(d) for d in dnsnames]
662 dnsnames = [pycompat.bytesurl(d) for d in dnsnames]
661 if len(dnsnames) > 1:
663 if len(dnsnames) > 1:
662 return _(b'certificate is for %s') % b', '.join(dnsnames)
664 return _(b'certificate is for %s') % b', '.join(dnsnames)
663 elif len(dnsnames) == 1:
665 elif len(dnsnames) == 1:
664 return _(b'certificate is for %s') % dnsnames[0]
666 return _(b'certificate is for %s') % dnsnames[0]
665 else:
667 else:
666 return _(b'no commonName or subjectAltName found in certificate')
668 return _(b'no commonName or subjectAltName found in certificate')
667
669
668
670
669 def _plainapplepython():
671 def _plainapplepython():
670 """return true if this seems to be a pure Apple Python that
672 """return true if this seems to be a pure Apple Python that
671 * is unfrozen and presumably has the whole mercurial module in the file
673 * is unfrozen and presumably has the whole mercurial module in the file
672 system
674 system
673 * presumably is an Apple Python that uses Apple OpenSSL which has patches
675 * presumably is an Apple Python that uses Apple OpenSSL which has patches
674 for using system certificate store CAs in addition to the provided
676 for using system certificate store CAs in addition to the provided
675 cacerts file
677 cacerts file
676 """
678 """
677 if (
679 if (
678 not pycompat.isdarwin
680 not pycompat.isdarwin
679 or resourceutil.mainfrozen()
681 or resourceutil.mainfrozen()
680 or not pycompat.sysexecutable
682 or not pycompat.sysexecutable
681 ):
683 ):
682 return False
684 return False
683 exe = os.path.realpath(pycompat.sysexecutable).lower()
685 exe = os.path.realpath(pycompat.sysexecutable).lower()
684 return exe.startswith(b'/usr/bin/python') or exe.startswith(
686 return exe.startswith(b'/usr/bin/python') or exe.startswith(
685 b'/system/library/frameworks/python.framework/'
687 b'/system/library/frameworks/python.framework/'
686 )
688 )
687
689
688
690
689 def _defaultcacerts(ui):
691 def _defaultcacerts(ui):
690 """return path to default CA certificates or None.
692 """return path to default CA certificates or None.
691
693
692 It is assumed this function is called when the returned certificates
694 It is assumed this function is called when the returned certificates
693 file will actually be used to validate connections. Therefore this
695 file will actually be used to validate connections. Therefore this
694 function may print warnings or debug messages assuming this usage.
696 function may print warnings or debug messages assuming this usage.
695
697
696 We don't print a message when the Python is able to load default
698 We don't print a message when the Python is able to load default
697 CA certs because this scenario is detected at socket connect time.
699 CA certs because this scenario is detected at socket connect time.
698 """
700 """
699 # The "certifi" Python package provides certificates. If it is installed
701 # The "certifi" Python package provides certificates. If it is installed
700 # and usable, assume the user intends it to be used and use it.
702 # and usable, assume the user intends it to be used and use it.
701 try:
703 try:
702 import certifi
704 import certifi
703
705
704 certs = certifi.where()
706 certs = certifi.where()
705 if os.path.exists(certs):
707 if os.path.exists(certs):
706 ui.debug(b'using ca certificates from certifi\n')
708 ui.debug(b'using ca certificates from certifi\n')
707 return pycompat.fsencode(certs)
709 return pycompat.fsencode(certs)
708 except (ImportError, AttributeError):
710 except (ImportError, AttributeError):
709 pass
711 pass
710
712
711 # Apple's OpenSSL has patches that allow a specially constructed certificate
713 # Apple's OpenSSL has patches that allow a specially constructed certificate
712 # to load the system CA store. If we're running on Apple Python, use this
714 # to load the system CA store. If we're running on Apple Python, use this
713 # trick.
715 # trick.
714 if _plainapplepython():
716 if _plainapplepython():
715 dummycert = os.path.join(
717 dummycert = os.path.join(
716 os.path.dirname(pycompat.fsencode(__file__)), b'dummycert.pem'
718 os.path.dirname(pycompat.fsencode(__file__)), b'dummycert.pem'
717 )
719 )
718 if os.path.exists(dummycert):
720 if os.path.exists(dummycert):
719 return dummycert
721 return dummycert
720
722
721 return None
723 return None
722
724
723
725
724 def validatesocket(sock):
726 def validatesocket(sock):
725 """Validate a socket meets security requirements.
727 """Validate a socket meets security requirements.
726
728
727 The passed socket must have been created with ``wrapsocket()``.
729 The passed socket must have been created with ``wrapsocket()``.
728 """
730 """
729 shost = sock._hgstate[b'hostname']
731 shost = sock._hgstate[b'hostname']
730 host = pycompat.bytesurl(shost)
732 host = pycompat.bytesurl(shost)
731 ui = sock._hgstate[b'ui']
733 ui = sock._hgstate[b'ui']
732 settings = sock._hgstate[b'settings']
734 settings = sock._hgstate[b'settings']
733
735
734 try:
736 try:
735 peercert = sock.getpeercert(True)
737 peercert = sock.getpeercert(True)
736 peercert2 = sock.getpeercert()
738 peercert2 = sock.getpeercert()
737 except AttributeError:
739 except AttributeError:
738 raise error.SecurityError(_(b'%s ssl connection error') % host)
740 raise error.SecurityError(_(b'%s ssl connection error') % host)
739
741
740 if not peercert:
742 if not peercert:
741 raise error.SecurityError(
743 raise error.SecurityError(
742 _(b'%s certificate error: no certificate received') % host
744 _(b'%s certificate error: no certificate received') % host
743 )
745 )
744
746
745 if settings[b'disablecertverification']:
747 if settings[b'disablecertverification']:
746 # We don't print the certificate fingerprint because it shouldn't
748 # We don't print the certificate fingerprint because it shouldn't
747 # be necessary: if the user requested certificate verification be
749 # be necessary: if the user requested certificate verification be
748 # disabled, they presumably already saw a message about the inability
750 # disabled, they presumably already saw a message about the inability
749 # to verify the certificate and this message would have printed the
751 # to verify the certificate and this message would have printed the
750 # fingerprint. So printing the fingerprint here adds little to no
752 # fingerprint. So printing the fingerprint here adds little to no
751 # value.
753 # value.
752 ui.warn(
754 ui.warn(
753 _(
755 _(
754 b'warning: connection security to %s is disabled per current '
756 b'warning: connection security to %s is disabled per current '
755 b'settings; communication is susceptible to eavesdropping '
757 b'settings; communication is susceptible to eavesdropping '
756 b'and tampering\n'
758 b'and tampering\n'
757 )
759 )
758 % host
760 % host
759 )
761 )
760 return
762 return
761
763
762 # If a certificate fingerprint is pinned, use it and only it to
764 # If a certificate fingerprint is pinned, use it and only it to
763 # validate the remote cert.
765 # validate the remote cert.
764 peerfingerprints = {
766 peerfingerprints = {
765 b'sha1': hex(hashutil.sha1(peercert).digest()),
767 b'sha1': hex(hashutil.sha1(peercert).digest()),
766 b'sha256': hex(hashlib.sha256(peercert).digest()),
768 b'sha256': hex(hashlib.sha256(peercert).digest()),
767 b'sha512': hex(hashlib.sha512(peercert).digest()),
769 b'sha512': hex(hashlib.sha512(peercert).digest()),
768 }
770 }
769
771
770 def fmtfingerprint(s):
772 def fmtfingerprint(s):
771 return b':'.join([s[x : x + 2] for x in range(0, len(s), 2)])
773 return b':'.join([s[x : x + 2] for x in range(0, len(s), 2)])
772
774
773 nicefingerprint = b'sha256:%s' % fmtfingerprint(peerfingerprints[b'sha256'])
775 nicefingerprint = b'sha256:%s' % fmtfingerprint(peerfingerprints[b'sha256'])
774
776
775 if settings[b'certfingerprints']:
777 if settings[b'certfingerprints']:
776 for hash, fingerprint in settings[b'certfingerprints']:
778 for hash, fingerprint in settings[b'certfingerprints']:
777 if peerfingerprints[hash].lower() == fingerprint:
779 if peerfingerprints[hash].lower() == fingerprint:
778 ui.debug(
780 ui.debug(
779 b'%s certificate matched fingerprint %s:%s\n'
781 b'%s certificate matched fingerprint %s:%s\n'
780 % (host, hash, fmtfingerprint(fingerprint))
782 % (host, hash, fmtfingerprint(fingerprint))
781 )
783 )
782 if settings[b'legacyfingerprint']:
784 if settings[b'legacyfingerprint']:
783 ui.warn(
785 ui.warn(
784 _(
786 _(
785 b'(SHA-1 fingerprint for %s found in legacy '
787 b'(SHA-1 fingerprint for %s found in legacy '
786 b'[hostfingerprints] section; '
788 b'[hostfingerprints] section; '
787 b'if you trust this fingerprint, remove the old '
789 b'if you trust this fingerprint, remove the old '
788 b'SHA-1 fingerprint from [hostfingerprints] and '
790 b'SHA-1 fingerprint from [hostfingerprints] and '
789 b'add the following entry to the new '
791 b'add the following entry to the new '
790 b'[hostsecurity] section: %s:fingerprints=%s)\n'
792 b'[hostsecurity] section: %s:fingerprints=%s)\n'
791 )
793 )
792 % (host, host, nicefingerprint)
794 % (host, host, nicefingerprint)
793 )
795 )
794 return
796 return
795
797
796 # Pinned fingerprint didn't match. This is a fatal error.
798 # Pinned fingerprint didn't match. This is a fatal error.
797 if settings[b'legacyfingerprint']:
799 if settings[b'legacyfingerprint']:
798 section = b'hostfingerprint'
800 section = b'hostfingerprint'
799 nice = fmtfingerprint(peerfingerprints[b'sha1'])
801 nice = fmtfingerprint(peerfingerprints[b'sha1'])
800 else:
802 else:
801 section = b'hostsecurity'
803 section = b'hostsecurity'
802 nice = b'%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
804 nice = b'%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
803 raise error.SecurityError(
805 raise error.SecurityError(
804 _(b'certificate for %s has unexpected fingerprint %s')
806 _(b'certificate for %s has unexpected fingerprint %s')
805 % (host, nice),
807 % (host, nice),
806 hint=_(b'check %s configuration') % section,
808 hint=_(b'check %s configuration') % section,
807 )
809 )
808
810
809 # Security is enabled but no CAs are loaded. We can't establish trust
811 # Security is enabled but no CAs are loaded. We can't establish trust
810 # for the cert so abort.
812 # for the cert so abort.
811 if not sock._hgstate[b'caloaded']:
813 if not sock._hgstate[b'caloaded']:
812 raise error.SecurityError(
814 raise error.SecurityError(
813 _(
815 _(
814 b'unable to verify security of %s (no loaded CA certificates); '
816 b'unable to verify security of %s (no loaded CA certificates); '
815 b'refusing to connect'
817 b'refusing to connect'
816 )
818 )
817 % host,
819 % host,
818 hint=_(
820 hint=_(
819 b'see https://mercurial-scm.org/wiki/SecureConnections for '
821 b'see https://mercurial-scm.org/wiki/SecureConnections for '
820 b'how to configure Mercurial to avoid this error or set '
822 b'how to configure Mercurial to avoid this error or set '
821 b'hostsecurity.%s:fingerprints=%s to trust this server'
823 b'hostsecurity.%s:fingerprints=%s to trust this server'
822 )
824 )
823 % (host, nicefingerprint),
825 % (host, nicefingerprint),
824 )
826 )
825
827
826 msg = _verifycert(peercert2, shost)
828 msg = _verifycert(peercert2, shost)
827 if msg:
829 if msg:
828 raise error.SecurityError(
830 raise error.SecurityError(
829 _(b'%s certificate error: %s') % (host, msg),
831 _(b'%s certificate error: %s') % (host, msg),
830 hint=_(
832 hint=_(
831 b'set hostsecurity.%s:certfingerprints=%s '
833 b'set hostsecurity.%s:certfingerprints=%s '
832 b'config setting or use --insecure to connect '
834 b'config setting or use --insecure to connect '
833 b'insecurely'
835 b'insecurely'
834 )
836 )
835 % (host, nicefingerprint),
837 % (host, nicefingerprint),
836 )
838 )
General Comments 0
You need to be logged in to leave comments. Login now