##// END OF EJS Templates
procutil: bulk-replace util.std* to point to new module
Yuya Nishihara -
r37137:d4a2e0d5 default
parent child Browse files
Show More
@@ -1,594 +1,598 b''
1 # chgserver.py - command server extension for cHg
1 # chgserver.py - command server extension for cHg
2 #
2 #
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
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 """command server extension for cHg
8 """command server extension for cHg
9
9
10 'S' channel (read/write)
10 'S' channel (read/write)
11 propagate ui.system() request to client
11 propagate ui.system() request to client
12
12
13 'attachio' command
13 'attachio' command
14 attach client's stdio passed by sendmsg()
14 attach client's stdio passed by sendmsg()
15
15
16 'chdir' command
16 'chdir' command
17 change current directory
17 change current directory
18
18
19 'setenv' command
19 'setenv' command
20 replace os.environ completely
20 replace os.environ completely
21
21
22 'setumask' command
22 'setumask' command
23 set umask
23 set umask
24
24
25 'validate' command
25 'validate' command
26 reload the config and check if the server is up to date
26 reload the config and check if the server is up to date
27
27
28 Config
28 Config
29 ------
29 ------
30
30
31 ::
31 ::
32
32
33 [chgserver]
33 [chgserver]
34 # how long (in seconds) should an idle chg server exit
34 # how long (in seconds) should an idle chg server exit
35 idletimeout = 3600
35 idletimeout = 3600
36
36
37 # whether to skip config or env change checks
37 # whether to skip config or env change checks
38 skiphash = False
38 skiphash = False
39 """
39 """
40
40
41 from __future__ import absolute_import
41 from __future__ import absolute_import
42
42
43 import hashlib
43 import hashlib
44 import inspect
44 import inspect
45 import os
45 import os
46 import re
46 import re
47 import socket
47 import socket
48 import stat
48 import stat
49 import struct
49 import struct
50 import time
50 import time
51
51
52 from .i18n import _
52 from .i18n import _
53
53
54 from . import (
54 from . import (
55 commandserver,
55 commandserver,
56 encoding,
56 encoding,
57 error,
57 error,
58 extensions,
58 extensions,
59 node,
59 node,
60 pycompat,
60 pycompat,
61 util,
61 util,
62 )
62 )
63
63
64 from .utils import (
65 procutil,
66 )
67
64 _log = commandserver.log
68 _log = commandserver.log
65
69
66 def _hashlist(items):
70 def _hashlist(items):
67 """return sha1 hexdigest for a list"""
71 """return sha1 hexdigest for a list"""
68 return node.hex(hashlib.sha1(str(items)).digest())
72 return node.hex(hashlib.sha1(str(items)).digest())
69
73
70 # sensitive config sections affecting confighash
74 # sensitive config sections affecting confighash
71 _configsections = [
75 _configsections = [
72 'alias', # affects global state commands.table
76 'alias', # affects global state commands.table
73 'eol', # uses setconfig('eol', ...)
77 'eol', # uses setconfig('eol', ...)
74 'extdiff', # uisetup will register new commands
78 'extdiff', # uisetup will register new commands
75 'extensions',
79 'extensions',
76 ]
80 ]
77
81
78 _configsectionitems = [
82 _configsectionitems = [
79 ('commands', 'show.aliasprefix'), # show.py reads it in extsetup
83 ('commands', 'show.aliasprefix'), # show.py reads it in extsetup
80 ]
84 ]
81
85
82 # sensitive environment variables affecting confighash
86 # sensitive environment variables affecting confighash
83 _envre = re.compile(r'''\A(?:
87 _envre = re.compile(r'''\A(?:
84 CHGHG
88 CHGHG
85 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
89 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
86 |HG(?:ENCODING|PLAIN).*
90 |HG(?:ENCODING|PLAIN).*
87 |LANG(?:UAGE)?
91 |LANG(?:UAGE)?
88 |LC_.*
92 |LC_.*
89 |LD_.*
93 |LD_.*
90 |PATH
94 |PATH
91 |PYTHON.*
95 |PYTHON.*
92 |TERM(?:INFO)?
96 |TERM(?:INFO)?
93 |TZ
97 |TZ
94 )\Z''', re.X)
98 )\Z''', re.X)
95
99
96 def _confighash(ui):
100 def _confighash(ui):
97 """return a quick hash for detecting config/env changes
101 """return a quick hash for detecting config/env changes
98
102
99 confighash is the hash of sensitive config items and environment variables.
103 confighash is the hash of sensitive config items and environment variables.
100
104
101 for chgserver, it is designed that once confighash changes, the server is
105 for chgserver, it is designed that once confighash changes, the server is
102 not qualified to serve its client and should redirect the client to a new
106 not qualified to serve its client and should redirect the client to a new
103 server. different from mtimehash, confighash change will not mark the
107 server. different from mtimehash, confighash change will not mark the
104 server outdated and exit since the user can have different configs at the
108 server outdated and exit since the user can have different configs at the
105 same time.
109 same time.
106 """
110 """
107 sectionitems = []
111 sectionitems = []
108 for section in _configsections:
112 for section in _configsections:
109 sectionitems.append(ui.configitems(section))
113 sectionitems.append(ui.configitems(section))
110 for section, item in _configsectionitems:
114 for section, item in _configsectionitems:
111 sectionitems.append(ui.config(section, item))
115 sectionitems.append(ui.config(section, item))
112 sectionhash = _hashlist(sectionitems)
116 sectionhash = _hashlist(sectionitems)
113 # If $CHGHG is set, the change to $HG should not trigger a new chg server
117 # If $CHGHG is set, the change to $HG should not trigger a new chg server
114 if 'CHGHG' in encoding.environ:
118 if 'CHGHG' in encoding.environ:
115 ignored = {'HG'}
119 ignored = {'HG'}
116 else:
120 else:
117 ignored = set()
121 ignored = set()
118 envitems = [(k, v) for k, v in encoding.environ.iteritems()
122 envitems = [(k, v) for k, v in encoding.environ.iteritems()
119 if _envre.match(k) and k not in ignored]
123 if _envre.match(k) and k not in ignored]
120 envhash = _hashlist(sorted(envitems))
124 envhash = _hashlist(sorted(envitems))
121 return sectionhash[:6] + envhash[:6]
125 return sectionhash[:6] + envhash[:6]
122
126
123 def _getmtimepaths(ui):
127 def _getmtimepaths(ui):
124 """get a list of paths that should be checked to detect change
128 """get a list of paths that should be checked to detect change
125
129
126 The list will include:
130 The list will include:
127 - extensions (will not cover all files for complex extensions)
131 - extensions (will not cover all files for complex extensions)
128 - mercurial/__version__.py
132 - mercurial/__version__.py
129 - python binary
133 - python binary
130 """
134 """
131 modules = [m for n, m in extensions.extensions(ui)]
135 modules = [m for n, m in extensions.extensions(ui)]
132 try:
136 try:
133 from . import __version__
137 from . import __version__
134 modules.append(__version__)
138 modules.append(__version__)
135 except ImportError:
139 except ImportError:
136 pass
140 pass
137 files = [pycompat.sysexecutable]
141 files = [pycompat.sysexecutable]
138 for m in modules:
142 for m in modules:
139 try:
143 try:
140 files.append(inspect.getabsfile(m))
144 files.append(inspect.getabsfile(m))
141 except TypeError:
145 except TypeError:
142 pass
146 pass
143 return sorted(set(files))
147 return sorted(set(files))
144
148
145 def _mtimehash(paths):
149 def _mtimehash(paths):
146 """return a quick hash for detecting file changes
150 """return a quick hash for detecting file changes
147
151
148 mtimehash calls stat on given paths and calculate a hash based on size and
152 mtimehash calls stat on given paths and calculate a hash based on size and
149 mtime of each file. mtimehash does not read file content because reading is
153 mtime of each file. mtimehash does not read file content because reading is
150 expensive. therefore it's not 100% reliable for detecting content changes.
154 expensive. therefore it's not 100% reliable for detecting content changes.
151 it's possible to return different hashes for same file contents.
155 it's possible to return different hashes for same file contents.
152 it's also possible to return a same hash for different file contents for
156 it's also possible to return a same hash for different file contents for
153 some carefully crafted situation.
157 some carefully crafted situation.
154
158
155 for chgserver, it is designed that once mtimehash changes, the server is
159 for chgserver, it is designed that once mtimehash changes, the server is
156 considered outdated immediately and should no longer provide service.
160 considered outdated immediately and should no longer provide service.
157
161
158 mtimehash is not included in confighash because we only know the paths of
162 mtimehash is not included in confighash because we only know the paths of
159 extensions after importing them (there is imp.find_module but that faces
163 extensions after importing them (there is imp.find_module but that faces
160 race conditions). We need to calculate confighash without importing.
164 race conditions). We need to calculate confighash without importing.
161 """
165 """
162 def trystat(path):
166 def trystat(path):
163 try:
167 try:
164 st = os.stat(path)
168 st = os.stat(path)
165 return (st[stat.ST_MTIME], st.st_size)
169 return (st[stat.ST_MTIME], st.st_size)
166 except OSError:
170 except OSError:
167 # could be ENOENT, EPERM etc. not fatal in any case
171 # could be ENOENT, EPERM etc. not fatal in any case
168 pass
172 pass
169 return _hashlist(map(trystat, paths))[:12]
173 return _hashlist(map(trystat, paths))[:12]
170
174
171 class hashstate(object):
175 class hashstate(object):
172 """a structure storing confighash, mtimehash, paths used for mtimehash"""
176 """a structure storing confighash, mtimehash, paths used for mtimehash"""
173 def __init__(self, confighash, mtimehash, mtimepaths):
177 def __init__(self, confighash, mtimehash, mtimepaths):
174 self.confighash = confighash
178 self.confighash = confighash
175 self.mtimehash = mtimehash
179 self.mtimehash = mtimehash
176 self.mtimepaths = mtimepaths
180 self.mtimepaths = mtimepaths
177
181
178 @staticmethod
182 @staticmethod
179 def fromui(ui, mtimepaths=None):
183 def fromui(ui, mtimepaths=None):
180 if mtimepaths is None:
184 if mtimepaths is None:
181 mtimepaths = _getmtimepaths(ui)
185 mtimepaths = _getmtimepaths(ui)
182 confighash = _confighash(ui)
186 confighash = _confighash(ui)
183 mtimehash = _mtimehash(mtimepaths)
187 mtimehash = _mtimehash(mtimepaths)
184 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
188 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
185 return hashstate(confighash, mtimehash, mtimepaths)
189 return hashstate(confighash, mtimehash, mtimepaths)
186
190
187 def _newchgui(srcui, csystem, attachio):
191 def _newchgui(srcui, csystem, attachio):
188 class chgui(srcui.__class__):
192 class chgui(srcui.__class__):
189 def __init__(self, src=None):
193 def __init__(self, src=None):
190 super(chgui, self).__init__(src)
194 super(chgui, self).__init__(src)
191 if src:
195 if src:
192 self._csystem = getattr(src, '_csystem', csystem)
196 self._csystem = getattr(src, '_csystem', csystem)
193 else:
197 else:
194 self._csystem = csystem
198 self._csystem = csystem
195
199
196 def _runsystem(self, cmd, environ, cwd, out):
200 def _runsystem(self, cmd, environ, cwd, out):
197 # fallback to the original system method if the output needs to be
201 # fallback to the original system method if the output needs to be
198 # captured (to self._buffers), or the output stream is not stdout
202 # captured (to self._buffers), or the output stream is not stdout
199 # (e.g. stderr, cStringIO), because the chg client is not aware of
203 # (e.g. stderr, cStringIO), because the chg client is not aware of
200 # these situations and will behave differently (write to stdout).
204 # these situations and will behave differently (write to stdout).
201 if (out is not self.fout
205 if (out is not self.fout
202 or not util.safehasattr(self.fout, 'fileno')
206 or not util.safehasattr(self.fout, 'fileno')
203 or self.fout.fileno() != util.stdout.fileno()):
207 or self.fout.fileno() != procutil.stdout.fileno()):
204 return util.system(cmd, environ=environ, cwd=cwd, out=out)
208 return util.system(cmd, environ=environ, cwd=cwd, out=out)
205 self.flush()
209 self.flush()
206 return self._csystem(cmd, util.shellenviron(environ), cwd)
210 return self._csystem(cmd, util.shellenviron(environ), cwd)
207
211
208 def _runpager(self, cmd, env=None):
212 def _runpager(self, cmd, env=None):
209 self._csystem(cmd, util.shellenviron(env), type='pager',
213 self._csystem(cmd, util.shellenviron(env), type='pager',
210 cmdtable={'attachio': attachio})
214 cmdtable={'attachio': attachio})
211 return True
215 return True
212
216
213 return chgui(srcui)
217 return chgui(srcui)
214
218
215 def _loadnewui(srcui, args):
219 def _loadnewui(srcui, args):
216 from . import dispatch # avoid cycle
220 from . import dispatch # avoid cycle
217
221
218 newui = srcui.__class__.load()
222 newui = srcui.__class__.load()
219 for a in ['fin', 'fout', 'ferr', 'environ']:
223 for a in ['fin', 'fout', 'ferr', 'environ']:
220 setattr(newui, a, getattr(srcui, a))
224 setattr(newui, a, getattr(srcui, a))
221 if util.safehasattr(srcui, '_csystem'):
225 if util.safehasattr(srcui, '_csystem'):
222 newui._csystem = srcui._csystem
226 newui._csystem = srcui._csystem
223
227
224 # command line args
228 # command line args
225 options = dispatch._earlyparseopts(newui, args)
229 options = dispatch._earlyparseopts(newui, args)
226 dispatch._parseconfig(newui, options['config'])
230 dispatch._parseconfig(newui, options['config'])
227
231
228 # stolen from tortoisehg.util.copydynamicconfig()
232 # stolen from tortoisehg.util.copydynamicconfig()
229 for section, name, value in srcui.walkconfig():
233 for section, name, value in srcui.walkconfig():
230 source = srcui.configsource(section, name)
234 source = srcui.configsource(section, name)
231 if ':' in source or source == '--config' or source.startswith('$'):
235 if ':' in source or source == '--config' or source.startswith('$'):
232 # path:line or command line, or environ
236 # path:line or command line, or environ
233 continue
237 continue
234 newui.setconfig(section, name, value, source)
238 newui.setconfig(section, name, value, source)
235
239
236 # load wd and repo config, copied from dispatch.py
240 # load wd and repo config, copied from dispatch.py
237 cwd = options['cwd']
241 cwd = options['cwd']
238 cwd = cwd and os.path.realpath(cwd) or None
242 cwd = cwd and os.path.realpath(cwd) or None
239 rpath = options['repository']
243 rpath = options['repository']
240 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
244 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
241
245
242 return (newui, newlui)
246 return (newui, newlui)
243
247
244 class channeledsystem(object):
248 class channeledsystem(object):
245 """Propagate ui.system() request in the following format:
249 """Propagate ui.system() request in the following format:
246
250
247 payload length (unsigned int),
251 payload length (unsigned int),
248 type, '\0',
252 type, '\0',
249 cmd, '\0',
253 cmd, '\0',
250 cwd, '\0',
254 cwd, '\0',
251 envkey, '=', val, '\0',
255 envkey, '=', val, '\0',
252 ...
256 ...
253 envkey, '=', val
257 envkey, '=', val
254
258
255 if type == 'system', waits for:
259 if type == 'system', waits for:
256
260
257 exitcode length (unsigned int),
261 exitcode length (unsigned int),
258 exitcode (int)
262 exitcode (int)
259
263
260 if type == 'pager', repetitively waits for a command name ending with '\n'
264 if type == 'pager', repetitively waits for a command name ending with '\n'
261 and executes it defined by cmdtable, or exits the loop if the command name
265 and executes it defined by cmdtable, or exits the loop if the command name
262 is empty.
266 is empty.
263 """
267 """
264 def __init__(self, in_, out, channel):
268 def __init__(self, in_, out, channel):
265 self.in_ = in_
269 self.in_ = in_
266 self.out = out
270 self.out = out
267 self.channel = channel
271 self.channel = channel
268
272
269 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
273 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
270 args = [type, util.quotecommand(cmd), os.path.abspath(cwd or '.')]
274 args = [type, util.quotecommand(cmd), os.path.abspath(cwd or '.')]
271 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
275 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
272 data = '\0'.join(args)
276 data = '\0'.join(args)
273 self.out.write(struct.pack('>cI', self.channel, len(data)))
277 self.out.write(struct.pack('>cI', self.channel, len(data)))
274 self.out.write(data)
278 self.out.write(data)
275 self.out.flush()
279 self.out.flush()
276
280
277 if type == 'system':
281 if type == 'system':
278 length = self.in_.read(4)
282 length = self.in_.read(4)
279 length, = struct.unpack('>I', length)
283 length, = struct.unpack('>I', length)
280 if length != 4:
284 if length != 4:
281 raise error.Abort(_('invalid response'))
285 raise error.Abort(_('invalid response'))
282 rc, = struct.unpack('>i', self.in_.read(4))
286 rc, = struct.unpack('>i', self.in_.read(4))
283 return rc
287 return rc
284 elif type == 'pager':
288 elif type == 'pager':
285 while True:
289 while True:
286 cmd = self.in_.readline()[:-1]
290 cmd = self.in_.readline()[:-1]
287 if not cmd:
291 if not cmd:
288 break
292 break
289 if cmdtable and cmd in cmdtable:
293 if cmdtable and cmd in cmdtable:
290 _log('pager subcommand: %s' % cmd)
294 _log('pager subcommand: %s' % cmd)
291 cmdtable[cmd]()
295 cmdtable[cmd]()
292 else:
296 else:
293 raise error.Abort(_('unexpected command: %s') % cmd)
297 raise error.Abort(_('unexpected command: %s') % cmd)
294 else:
298 else:
295 raise error.ProgrammingError('invalid S channel type: %s' % type)
299 raise error.ProgrammingError('invalid S channel type: %s' % type)
296
300
297 _iochannels = [
301 _iochannels = [
298 # server.ch, ui.fp, mode
302 # server.ch, ui.fp, mode
299 ('cin', 'fin', r'rb'),
303 ('cin', 'fin', r'rb'),
300 ('cout', 'fout', r'wb'),
304 ('cout', 'fout', r'wb'),
301 ('cerr', 'ferr', r'wb'),
305 ('cerr', 'ferr', r'wb'),
302 ]
306 ]
303
307
304 class chgcmdserver(commandserver.server):
308 class chgcmdserver(commandserver.server):
305 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
309 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
306 super(chgcmdserver, self).__init__(
310 super(chgcmdserver, self).__init__(
307 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
311 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
308 repo, fin, fout)
312 repo, fin, fout)
309 self.clientsock = sock
313 self.clientsock = sock
310 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
314 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
311 self.hashstate = hashstate
315 self.hashstate = hashstate
312 self.baseaddress = baseaddress
316 self.baseaddress = baseaddress
313 if hashstate is not None:
317 if hashstate is not None:
314 self.capabilities = self.capabilities.copy()
318 self.capabilities = self.capabilities.copy()
315 self.capabilities['validate'] = chgcmdserver.validate
319 self.capabilities['validate'] = chgcmdserver.validate
316
320
317 def cleanup(self):
321 def cleanup(self):
318 super(chgcmdserver, self).cleanup()
322 super(chgcmdserver, self).cleanup()
319 # dispatch._runcatch() does not flush outputs if exception is not
323 # dispatch._runcatch() does not flush outputs if exception is not
320 # handled by dispatch._dispatch()
324 # handled by dispatch._dispatch()
321 self.ui.flush()
325 self.ui.flush()
322 self._restoreio()
326 self._restoreio()
323
327
324 def attachio(self):
328 def attachio(self):
325 """Attach to client's stdio passed via unix domain socket; all
329 """Attach to client's stdio passed via unix domain socket; all
326 channels except cresult will no longer be used
330 channels except cresult will no longer be used
327 """
331 """
328 # tell client to sendmsg() with 1-byte payload, which makes it
332 # tell client to sendmsg() with 1-byte payload, which makes it
329 # distinctive from "attachio\n" command consumed by client.read()
333 # distinctive from "attachio\n" command consumed by client.read()
330 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
334 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
331 clientfds = util.recvfds(self.clientsock.fileno())
335 clientfds = util.recvfds(self.clientsock.fileno())
332 _log('received fds: %r\n' % clientfds)
336 _log('received fds: %r\n' % clientfds)
333
337
334 ui = self.ui
338 ui = self.ui
335 ui.flush()
339 ui.flush()
336 first = self._saveio()
340 first = self._saveio()
337 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
341 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
338 assert fd > 0
342 assert fd > 0
339 fp = getattr(ui, fn)
343 fp = getattr(ui, fn)
340 os.dup2(fd, fp.fileno())
344 os.dup2(fd, fp.fileno())
341 os.close(fd)
345 os.close(fd)
342 if not first:
346 if not first:
343 continue
347 continue
344 # reset buffering mode when client is first attached. as we want
348 # reset buffering mode when client is first attached. as we want
345 # to see output immediately on pager, the mode stays unchanged
349 # to see output immediately on pager, the mode stays unchanged
346 # when client re-attached. ferr is unchanged because it should
350 # when client re-attached. ferr is unchanged because it should
347 # be unbuffered no matter if it is a tty or not.
351 # be unbuffered no matter if it is a tty or not.
348 if fn == 'ferr':
352 if fn == 'ferr':
349 newfp = fp
353 newfp = fp
350 else:
354 else:
351 # make it line buffered explicitly because the default is
355 # make it line buffered explicitly because the default is
352 # decided on first write(), where fout could be a pager.
356 # decided on first write(), where fout could be a pager.
353 if fp.isatty():
357 if fp.isatty():
354 bufsize = 1 # line buffered
358 bufsize = 1 # line buffered
355 else:
359 else:
356 bufsize = -1 # system default
360 bufsize = -1 # system default
357 newfp = os.fdopen(fp.fileno(), mode, bufsize)
361 newfp = os.fdopen(fp.fileno(), mode, bufsize)
358 setattr(ui, fn, newfp)
362 setattr(ui, fn, newfp)
359 setattr(self, cn, newfp)
363 setattr(self, cn, newfp)
360
364
361 self.cresult.write(struct.pack('>i', len(clientfds)))
365 self.cresult.write(struct.pack('>i', len(clientfds)))
362
366
363 def _saveio(self):
367 def _saveio(self):
364 if self._oldios:
368 if self._oldios:
365 return False
369 return False
366 ui = self.ui
370 ui = self.ui
367 for cn, fn, _mode in _iochannels:
371 for cn, fn, _mode in _iochannels:
368 ch = getattr(self, cn)
372 ch = getattr(self, cn)
369 fp = getattr(ui, fn)
373 fp = getattr(ui, fn)
370 fd = os.dup(fp.fileno())
374 fd = os.dup(fp.fileno())
371 self._oldios.append((ch, fp, fd))
375 self._oldios.append((ch, fp, fd))
372 return True
376 return True
373
377
374 def _restoreio(self):
378 def _restoreio(self):
375 ui = self.ui
379 ui = self.ui
376 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
380 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
377 newfp = getattr(ui, fn)
381 newfp = getattr(ui, fn)
378 # close newfp while it's associated with client; otherwise it
382 # close newfp while it's associated with client; otherwise it
379 # would be closed when newfp is deleted
383 # would be closed when newfp is deleted
380 if newfp is not fp:
384 if newfp is not fp:
381 newfp.close()
385 newfp.close()
382 # restore original fd: fp is open again
386 # restore original fd: fp is open again
383 os.dup2(fd, fp.fileno())
387 os.dup2(fd, fp.fileno())
384 os.close(fd)
388 os.close(fd)
385 setattr(self, cn, ch)
389 setattr(self, cn, ch)
386 setattr(ui, fn, fp)
390 setattr(ui, fn, fp)
387 del self._oldios[:]
391 del self._oldios[:]
388
392
389 def validate(self):
393 def validate(self):
390 """Reload the config and check if the server is up to date
394 """Reload the config and check if the server is up to date
391
395
392 Read a list of '\0' separated arguments.
396 Read a list of '\0' separated arguments.
393 Write a non-empty list of '\0' separated instruction strings or '\0'
397 Write a non-empty list of '\0' separated instruction strings or '\0'
394 if the list is empty.
398 if the list is empty.
395 An instruction string could be either:
399 An instruction string could be either:
396 - "unlink $path", the client should unlink the path to stop the
400 - "unlink $path", the client should unlink the path to stop the
397 outdated server.
401 outdated server.
398 - "redirect $path", the client should attempt to connect to $path
402 - "redirect $path", the client should attempt to connect to $path
399 first. If it does not work, start a new server. It implies
403 first. If it does not work, start a new server. It implies
400 "reconnect".
404 "reconnect".
401 - "exit $n", the client should exit directly with code n.
405 - "exit $n", the client should exit directly with code n.
402 This may happen if we cannot parse the config.
406 This may happen if we cannot parse the config.
403 - "reconnect", the client should close the connection and
407 - "reconnect", the client should close the connection and
404 reconnect.
408 reconnect.
405 If neither "reconnect" nor "redirect" is included in the instruction
409 If neither "reconnect" nor "redirect" is included in the instruction
406 list, the client can continue with this server after completing all
410 list, the client can continue with this server after completing all
407 the instructions.
411 the instructions.
408 """
412 """
409 from . import dispatch # avoid cycle
413 from . import dispatch # avoid cycle
410
414
411 args = self._readlist()
415 args = self._readlist()
412 try:
416 try:
413 self.ui, lui = _loadnewui(self.ui, args)
417 self.ui, lui = _loadnewui(self.ui, args)
414 except error.ParseError as inst:
418 except error.ParseError as inst:
415 dispatch._formatparse(self.ui.warn, inst)
419 dispatch._formatparse(self.ui.warn, inst)
416 self.ui.flush()
420 self.ui.flush()
417 self.cresult.write('exit 255')
421 self.cresult.write('exit 255')
418 return
422 return
419 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
423 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
420 insts = []
424 insts = []
421 if newhash.mtimehash != self.hashstate.mtimehash:
425 if newhash.mtimehash != self.hashstate.mtimehash:
422 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
426 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
423 insts.append('unlink %s' % addr)
427 insts.append('unlink %s' % addr)
424 # mtimehash is empty if one or more extensions fail to load.
428 # mtimehash is empty if one or more extensions fail to load.
425 # to be compatible with hg, still serve the client this time.
429 # to be compatible with hg, still serve the client this time.
426 if self.hashstate.mtimehash:
430 if self.hashstate.mtimehash:
427 insts.append('reconnect')
431 insts.append('reconnect')
428 if newhash.confighash != self.hashstate.confighash:
432 if newhash.confighash != self.hashstate.confighash:
429 addr = _hashaddress(self.baseaddress, newhash.confighash)
433 addr = _hashaddress(self.baseaddress, newhash.confighash)
430 insts.append('redirect %s' % addr)
434 insts.append('redirect %s' % addr)
431 _log('validate: %s\n' % insts)
435 _log('validate: %s\n' % insts)
432 self.cresult.write('\0'.join(insts) or '\0')
436 self.cresult.write('\0'.join(insts) or '\0')
433
437
434 def chdir(self):
438 def chdir(self):
435 """Change current directory
439 """Change current directory
436
440
437 Note that the behavior of --cwd option is bit different from this.
441 Note that the behavior of --cwd option is bit different from this.
438 It does not affect --config parameter.
442 It does not affect --config parameter.
439 """
443 """
440 path = self._readstr()
444 path = self._readstr()
441 if not path:
445 if not path:
442 return
446 return
443 _log('chdir to %r\n' % path)
447 _log('chdir to %r\n' % path)
444 os.chdir(path)
448 os.chdir(path)
445
449
446 def setumask(self):
450 def setumask(self):
447 """Change umask"""
451 """Change umask"""
448 mask = struct.unpack('>I', self._read(4))[0]
452 mask = struct.unpack('>I', self._read(4))[0]
449 _log('setumask %r\n' % mask)
453 _log('setumask %r\n' % mask)
450 os.umask(mask)
454 os.umask(mask)
451
455
452 def runcommand(self):
456 def runcommand(self):
453 return super(chgcmdserver, self).runcommand()
457 return super(chgcmdserver, self).runcommand()
454
458
455 def setenv(self):
459 def setenv(self):
456 """Clear and update os.environ
460 """Clear and update os.environ
457
461
458 Note that not all variables can make an effect on the running process.
462 Note that not all variables can make an effect on the running process.
459 """
463 """
460 l = self._readlist()
464 l = self._readlist()
461 try:
465 try:
462 newenv = dict(s.split('=', 1) for s in l)
466 newenv = dict(s.split('=', 1) for s in l)
463 except ValueError:
467 except ValueError:
464 raise ValueError('unexpected value in setenv request')
468 raise ValueError('unexpected value in setenv request')
465 _log('setenv: %r\n' % sorted(newenv.keys()))
469 _log('setenv: %r\n' % sorted(newenv.keys()))
466 encoding.environ.clear()
470 encoding.environ.clear()
467 encoding.environ.update(newenv)
471 encoding.environ.update(newenv)
468
472
469 capabilities = commandserver.server.capabilities.copy()
473 capabilities = commandserver.server.capabilities.copy()
470 capabilities.update({'attachio': attachio,
474 capabilities.update({'attachio': attachio,
471 'chdir': chdir,
475 'chdir': chdir,
472 'runcommand': runcommand,
476 'runcommand': runcommand,
473 'setenv': setenv,
477 'setenv': setenv,
474 'setumask': setumask})
478 'setumask': setumask})
475
479
476 if util.safehasattr(util, 'setprocname'):
480 if util.safehasattr(util, 'setprocname'):
477 def setprocname(self):
481 def setprocname(self):
478 """Change process title"""
482 """Change process title"""
479 name = self._readstr()
483 name = self._readstr()
480 _log('setprocname: %r\n' % name)
484 _log('setprocname: %r\n' % name)
481 util.setprocname(name)
485 util.setprocname(name)
482 capabilities['setprocname'] = setprocname
486 capabilities['setprocname'] = setprocname
483
487
484 def _tempaddress(address):
488 def _tempaddress(address):
485 return '%s.%d.tmp' % (address, os.getpid())
489 return '%s.%d.tmp' % (address, os.getpid())
486
490
487 def _hashaddress(address, hashstr):
491 def _hashaddress(address, hashstr):
488 # if the basename of address contains '.', use only the left part. this
492 # if the basename of address contains '.', use only the left part. this
489 # makes it possible for the client to pass 'server.tmp$PID' and follow by
493 # makes it possible for the client to pass 'server.tmp$PID' and follow by
490 # an atomic rename to avoid locking when spawning new servers.
494 # an atomic rename to avoid locking when spawning new servers.
491 dirname, basename = os.path.split(address)
495 dirname, basename = os.path.split(address)
492 basename = basename.split('.', 1)[0]
496 basename = basename.split('.', 1)[0]
493 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
497 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
494
498
495 class chgunixservicehandler(object):
499 class chgunixservicehandler(object):
496 """Set of operations for chg services"""
500 """Set of operations for chg services"""
497
501
498 pollinterval = 1 # [sec]
502 pollinterval = 1 # [sec]
499
503
500 def __init__(self, ui):
504 def __init__(self, ui):
501 self.ui = ui
505 self.ui = ui
502 self._idletimeout = ui.configint('chgserver', 'idletimeout')
506 self._idletimeout = ui.configint('chgserver', 'idletimeout')
503 self._lastactive = time.time()
507 self._lastactive = time.time()
504
508
505 def bindsocket(self, sock, address):
509 def bindsocket(self, sock, address):
506 self._inithashstate(address)
510 self._inithashstate(address)
507 self._checkextensions()
511 self._checkextensions()
508 self._bind(sock)
512 self._bind(sock)
509 self._createsymlink()
513 self._createsymlink()
510 # no "listening at" message should be printed to simulate hg behavior
514 # no "listening at" message should be printed to simulate hg behavior
511
515
512 def _inithashstate(self, address):
516 def _inithashstate(self, address):
513 self._baseaddress = address
517 self._baseaddress = address
514 if self.ui.configbool('chgserver', 'skiphash'):
518 if self.ui.configbool('chgserver', 'skiphash'):
515 self._hashstate = None
519 self._hashstate = None
516 self._realaddress = address
520 self._realaddress = address
517 return
521 return
518 self._hashstate = hashstate.fromui(self.ui)
522 self._hashstate = hashstate.fromui(self.ui)
519 self._realaddress = _hashaddress(address, self._hashstate.confighash)
523 self._realaddress = _hashaddress(address, self._hashstate.confighash)
520
524
521 def _checkextensions(self):
525 def _checkextensions(self):
522 if not self._hashstate:
526 if not self._hashstate:
523 return
527 return
524 if extensions.notloaded():
528 if extensions.notloaded():
525 # one or more extensions failed to load. mtimehash becomes
529 # one or more extensions failed to load. mtimehash becomes
526 # meaningless because we do not know the paths of those extensions.
530 # meaningless because we do not know the paths of those extensions.
527 # set mtimehash to an illegal hash value to invalidate the server.
531 # set mtimehash to an illegal hash value to invalidate the server.
528 self._hashstate.mtimehash = ''
532 self._hashstate.mtimehash = ''
529
533
530 def _bind(self, sock):
534 def _bind(self, sock):
531 # use a unique temp address so we can stat the file and do ownership
535 # use a unique temp address so we can stat the file and do ownership
532 # check later
536 # check later
533 tempaddress = _tempaddress(self._realaddress)
537 tempaddress = _tempaddress(self._realaddress)
534 util.bindunixsocket(sock, tempaddress)
538 util.bindunixsocket(sock, tempaddress)
535 self._socketstat = os.stat(tempaddress)
539 self._socketstat = os.stat(tempaddress)
536 sock.listen(socket.SOMAXCONN)
540 sock.listen(socket.SOMAXCONN)
537 # rename will replace the old socket file if exists atomically. the
541 # rename will replace the old socket file if exists atomically. the
538 # old server will detect ownership change and exit.
542 # old server will detect ownership change and exit.
539 util.rename(tempaddress, self._realaddress)
543 util.rename(tempaddress, self._realaddress)
540
544
541 def _createsymlink(self):
545 def _createsymlink(self):
542 if self._baseaddress == self._realaddress:
546 if self._baseaddress == self._realaddress:
543 return
547 return
544 tempaddress = _tempaddress(self._baseaddress)
548 tempaddress = _tempaddress(self._baseaddress)
545 os.symlink(os.path.basename(self._realaddress), tempaddress)
549 os.symlink(os.path.basename(self._realaddress), tempaddress)
546 util.rename(tempaddress, self._baseaddress)
550 util.rename(tempaddress, self._baseaddress)
547
551
548 def _issocketowner(self):
552 def _issocketowner(self):
549 try:
553 try:
550 st = os.stat(self._realaddress)
554 st = os.stat(self._realaddress)
551 return (st.st_ino == self._socketstat.st_ino and
555 return (st.st_ino == self._socketstat.st_ino and
552 st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME])
556 st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME])
553 except OSError:
557 except OSError:
554 return False
558 return False
555
559
556 def unlinksocket(self, address):
560 def unlinksocket(self, address):
557 if not self._issocketowner():
561 if not self._issocketowner():
558 return
562 return
559 # it is possible to have a race condition here that we may
563 # it is possible to have a race condition here that we may
560 # remove another server's socket file. but that's okay
564 # remove another server's socket file. but that's okay
561 # since that server will detect and exit automatically and
565 # since that server will detect and exit automatically and
562 # the client will start a new server on demand.
566 # the client will start a new server on demand.
563 util.tryunlink(self._realaddress)
567 util.tryunlink(self._realaddress)
564
568
565 def shouldexit(self):
569 def shouldexit(self):
566 if not self._issocketowner():
570 if not self._issocketowner():
567 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
571 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
568 return True
572 return True
569 if time.time() - self._lastactive > self._idletimeout:
573 if time.time() - self._lastactive > self._idletimeout:
570 self.ui.debug('being idle too long. exiting.\n')
574 self.ui.debug('being idle too long. exiting.\n')
571 return True
575 return True
572 return False
576 return False
573
577
574 def newconnection(self):
578 def newconnection(self):
575 self._lastactive = time.time()
579 self._lastactive = time.time()
576
580
577 def createcmdserver(self, repo, conn, fin, fout):
581 def createcmdserver(self, repo, conn, fin, fout):
578 return chgcmdserver(self.ui, repo, fin, fout, conn,
582 return chgcmdserver(self.ui, repo, fin, fout, conn,
579 self._hashstate, self._baseaddress)
583 self._hashstate, self._baseaddress)
580
584
581 def chgunixservice(ui, repo, opts):
585 def chgunixservice(ui, repo, opts):
582 # CHGINTERNALMARK is set by chg client. It is an indication of things are
586 # CHGINTERNALMARK is set by chg client. It is an indication of things are
583 # started by chg so other code can do things accordingly, like disabling
587 # started by chg so other code can do things accordingly, like disabling
584 # demandimport or detecting chg client started by chg client. When executed
588 # demandimport or detecting chg client started by chg client. When executed
585 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
589 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
586 # environ cleaner.
590 # environ cleaner.
587 if 'CHGINTERNALMARK' in encoding.environ:
591 if 'CHGINTERNALMARK' in encoding.environ:
588 del encoding.environ['CHGINTERNALMARK']
592 del encoding.environ['CHGINTERNALMARK']
589
593
590 if repo:
594 if repo:
591 # one chgserver can serve multiple repos. drop repo information
595 # one chgserver can serve multiple repos. drop repo information
592 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
596 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
593 h = chgunixservicehandler(ui)
597 h = chgunixservicehandler(ui)
594 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
598 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,556 +1,559 b''
1 # commandserver.py - communicate with Mercurial's API over a pipe
1 # commandserver.py - communicate with Mercurial's API over a pipe
2 #
2 #
3 # Copyright Matt Mackall <mpm@selenic.com>
3 # Copyright 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
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import gc
11 import gc
12 import os
12 import os
13 import random
13 import random
14 import signal
14 import signal
15 import socket
15 import socket
16 import struct
16 import struct
17 import traceback
17 import traceback
18
18
19 try:
19 try:
20 import selectors
20 import selectors
21 selectors.BaseSelector
21 selectors.BaseSelector
22 except ImportError:
22 except ImportError:
23 from .thirdparty import selectors2 as selectors
23 from .thirdparty import selectors2 as selectors
24
24
25 from .i18n import _
25 from .i18n import _
26 from . import (
26 from . import (
27 encoding,
27 encoding,
28 error,
28 error,
29 pycompat,
29 pycompat,
30 util,
30 util,
31 )
31 )
32 from .utils import (
33 procutil,
34 )
32
35
33 logfile = None
36 logfile = None
34
37
35 def log(*args):
38 def log(*args):
36 if not logfile:
39 if not logfile:
37 return
40 return
38
41
39 for a in args:
42 for a in args:
40 logfile.write(str(a))
43 logfile.write(str(a))
41
44
42 logfile.flush()
45 logfile.flush()
43
46
44 class channeledoutput(object):
47 class channeledoutput(object):
45 """
48 """
46 Write data to out in the following format:
49 Write data to out in the following format:
47
50
48 data length (unsigned int),
51 data length (unsigned int),
49 data
52 data
50 """
53 """
51 def __init__(self, out, channel):
54 def __init__(self, out, channel):
52 self.out = out
55 self.out = out
53 self.channel = channel
56 self.channel = channel
54
57
55 @property
58 @property
56 def name(self):
59 def name(self):
57 return '<%c-channel>' % self.channel
60 return '<%c-channel>' % self.channel
58
61
59 def write(self, data):
62 def write(self, data):
60 if not data:
63 if not data:
61 return
64 return
62 # single write() to guarantee the same atomicity as the underlying file
65 # single write() to guarantee the same atomicity as the underlying file
63 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
66 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
64 self.out.flush()
67 self.out.flush()
65
68
66 def __getattr__(self, attr):
69 def __getattr__(self, attr):
67 if attr in ('isatty', 'fileno', 'tell', 'seek'):
70 if attr in ('isatty', 'fileno', 'tell', 'seek'):
68 raise AttributeError(attr)
71 raise AttributeError(attr)
69 return getattr(self.out, attr)
72 return getattr(self.out, attr)
70
73
71 class channeledinput(object):
74 class channeledinput(object):
72 """
75 """
73 Read data from in_.
76 Read data from in_.
74
77
75 Requests for input are written to out in the following format:
78 Requests for input are written to out in the following format:
76 channel identifier - 'I' for plain input, 'L' line based (1 byte)
79 channel identifier - 'I' for plain input, 'L' line based (1 byte)
77 how many bytes to send at most (unsigned int),
80 how many bytes to send at most (unsigned int),
78
81
79 The client replies with:
82 The client replies with:
80 data length (unsigned int), 0 meaning EOF
83 data length (unsigned int), 0 meaning EOF
81 data
84 data
82 """
85 """
83
86
84 maxchunksize = 4 * 1024
87 maxchunksize = 4 * 1024
85
88
86 def __init__(self, in_, out, channel):
89 def __init__(self, in_, out, channel):
87 self.in_ = in_
90 self.in_ = in_
88 self.out = out
91 self.out = out
89 self.channel = channel
92 self.channel = channel
90
93
91 @property
94 @property
92 def name(self):
95 def name(self):
93 return '<%c-channel>' % self.channel
96 return '<%c-channel>' % self.channel
94
97
95 def read(self, size=-1):
98 def read(self, size=-1):
96 if size < 0:
99 if size < 0:
97 # if we need to consume all the clients input, ask for 4k chunks
100 # if we need to consume all the clients input, ask for 4k chunks
98 # so the pipe doesn't fill up risking a deadlock
101 # so the pipe doesn't fill up risking a deadlock
99 size = self.maxchunksize
102 size = self.maxchunksize
100 s = self._read(size, self.channel)
103 s = self._read(size, self.channel)
101 buf = s
104 buf = s
102 while s:
105 while s:
103 s = self._read(size, self.channel)
106 s = self._read(size, self.channel)
104 buf += s
107 buf += s
105
108
106 return buf
109 return buf
107 else:
110 else:
108 return self._read(size, self.channel)
111 return self._read(size, self.channel)
109
112
110 def _read(self, size, channel):
113 def _read(self, size, channel):
111 if not size:
114 if not size:
112 return ''
115 return ''
113 assert size > 0
116 assert size > 0
114
117
115 # tell the client we need at most size bytes
118 # tell the client we need at most size bytes
116 self.out.write(struct.pack('>cI', channel, size))
119 self.out.write(struct.pack('>cI', channel, size))
117 self.out.flush()
120 self.out.flush()
118
121
119 length = self.in_.read(4)
122 length = self.in_.read(4)
120 length = struct.unpack('>I', length)[0]
123 length = struct.unpack('>I', length)[0]
121 if not length:
124 if not length:
122 return ''
125 return ''
123 else:
126 else:
124 return self.in_.read(length)
127 return self.in_.read(length)
125
128
126 def readline(self, size=-1):
129 def readline(self, size=-1):
127 if size < 0:
130 if size < 0:
128 size = self.maxchunksize
131 size = self.maxchunksize
129 s = self._read(size, 'L')
132 s = self._read(size, 'L')
130 buf = s
133 buf = s
131 # keep asking for more until there's either no more or
134 # keep asking for more until there's either no more or
132 # we got a full line
135 # we got a full line
133 while s and s[-1] != '\n':
136 while s and s[-1] != '\n':
134 s = self._read(size, 'L')
137 s = self._read(size, 'L')
135 buf += s
138 buf += s
136
139
137 return buf
140 return buf
138 else:
141 else:
139 return self._read(size, 'L')
142 return self._read(size, 'L')
140
143
141 def __iter__(self):
144 def __iter__(self):
142 return self
145 return self
143
146
144 def next(self):
147 def next(self):
145 l = self.readline()
148 l = self.readline()
146 if not l:
149 if not l:
147 raise StopIteration
150 raise StopIteration
148 return l
151 return l
149
152
150 def __getattr__(self, attr):
153 def __getattr__(self, attr):
151 if attr in ('isatty', 'fileno', 'tell', 'seek'):
154 if attr in ('isatty', 'fileno', 'tell', 'seek'):
152 raise AttributeError(attr)
155 raise AttributeError(attr)
153 return getattr(self.in_, attr)
156 return getattr(self.in_, attr)
154
157
155 class server(object):
158 class server(object):
156 """
159 """
157 Listens for commands on fin, runs them and writes the output on a channel
160 Listens for commands on fin, runs them and writes the output on a channel
158 based stream to fout.
161 based stream to fout.
159 """
162 """
160 def __init__(self, ui, repo, fin, fout):
163 def __init__(self, ui, repo, fin, fout):
161 self.cwd = pycompat.getcwd()
164 self.cwd = pycompat.getcwd()
162
165
163 # developer config: cmdserver.log
166 # developer config: cmdserver.log
164 logpath = ui.config("cmdserver", "log")
167 logpath = ui.config("cmdserver", "log")
165 if logpath:
168 if logpath:
166 global logfile
169 global logfile
167 if logpath == '-':
170 if logpath == '-':
168 # write log on a special 'd' (debug) channel
171 # write log on a special 'd' (debug) channel
169 logfile = channeledoutput(fout, 'd')
172 logfile = channeledoutput(fout, 'd')
170 else:
173 else:
171 logfile = open(logpath, 'a')
174 logfile = open(logpath, 'a')
172
175
173 if repo:
176 if repo:
174 # the ui here is really the repo ui so take its baseui so we don't
177 # the ui here is really the repo ui so take its baseui so we don't
175 # end up with its local configuration
178 # end up with its local configuration
176 self.ui = repo.baseui
179 self.ui = repo.baseui
177 self.repo = repo
180 self.repo = repo
178 self.repoui = repo.ui
181 self.repoui = repo.ui
179 else:
182 else:
180 self.ui = ui
183 self.ui = ui
181 self.repo = self.repoui = None
184 self.repo = self.repoui = None
182
185
183 self.cerr = channeledoutput(fout, 'e')
186 self.cerr = channeledoutput(fout, 'e')
184 self.cout = channeledoutput(fout, 'o')
187 self.cout = channeledoutput(fout, 'o')
185 self.cin = channeledinput(fin, fout, 'I')
188 self.cin = channeledinput(fin, fout, 'I')
186 self.cresult = channeledoutput(fout, 'r')
189 self.cresult = channeledoutput(fout, 'r')
187
190
188 self.client = fin
191 self.client = fin
189
192
190 def cleanup(self):
193 def cleanup(self):
191 """release and restore resources taken during server session"""
194 """release and restore resources taken during server session"""
192
195
193 def _read(self, size):
196 def _read(self, size):
194 if not size:
197 if not size:
195 return ''
198 return ''
196
199
197 data = self.client.read(size)
200 data = self.client.read(size)
198
201
199 # is the other end closed?
202 # is the other end closed?
200 if not data:
203 if not data:
201 raise EOFError
204 raise EOFError
202
205
203 return data
206 return data
204
207
205 def _readstr(self):
208 def _readstr(self):
206 """read a string from the channel
209 """read a string from the channel
207
210
208 format:
211 format:
209 data length (uint32), data
212 data length (uint32), data
210 """
213 """
211 length = struct.unpack('>I', self._read(4))[0]
214 length = struct.unpack('>I', self._read(4))[0]
212 if not length:
215 if not length:
213 return ''
216 return ''
214 return self._read(length)
217 return self._read(length)
215
218
216 def _readlist(self):
219 def _readlist(self):
217 """read a list of NULL separated strings from the channel"""
220 """read a list of NULL separated strings from the channel"""
218 s = self._readstr()
221 s = self._readstr()
219 if s:
222 if s:
220 return s.split('\0')
223 return s.split('\0')
221 else:
224 else:
222 return []
225 return []
223
226
224 def runcommand(self):
227 def runcommand(self):
225 """ reads a list of \0 terminated arguments, executes
228 """ reads a list of \0 terminated arguments, executes
226 and writes the return code to the result channel """
229 and writes the return code to the result channel """
227 from . import dispatch # avoid cycle
230 from . import dispatch # avoid cycle
228
231
229 args = self._readlist()
232 args = self._readlist()
230
233
231 # copy the uis so changes (e.g. --config or --verbose) don't
234 # copy the uis so changes (e.g. --config or --verbose) don't
232 # persist between requests
235 # persist between requests
233 copiedui = self.ui.copy()
236 copiedui = self.ui.copy()
234 uis = [copiedui]
237 uis = [copiedui]
235 if self.repo:
238 if self.repo:
236 self.repo.baseui = copiedui
239 self.repo.baseui = copiedui
237 # clone ui without using ui.copy because this is protected
240 # clone ui without using ui.copy because this is protected
238 repoui = self.repoui.__class__(self.repoui)
241 repoui = self.repoui.__class__(self.repoui)
239 repoui.copy = copiedui.copy # redo copy protection
242 repoui.copy = copiedui.copy # redo copy protection
240 uis.append(repoui)
243 uis.append(repoui)
241 self.repo.ui = self.repo.dirstate._ui = repoui
244 self.repo.ui = self.repo.dirstate._ui = repoui
242 self.repo.invalidateall()
245 self.repo.invalidateall()
243
246
244 for ui in uis:
247 for ui in uis:
245 ui.resetstate()
248 ui.resetstate()
246 # any kind of interaction must use server channels, but chg may
249 # any kind of interaction must use server channels, but chg may
247 # replace channels by fully functional tty files. so nontty is
250 # replace channels by fully functional tty files. so nontty is
248 # enforced only if cin is a channel.
251 # enforced only if cin is a channel.
249 if not util.safehasattr(self.cin, 'fileno'):
252 if not util.safehasattr(self.cin, 'fileno'):
250 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
253 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
251
254
252 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
255 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
253 self.cout, self.cerr)
256 self.cout, self.cerr)
254
257
255 try:
258 try:
256 ret = (dispatch.dispatch(req) or 0) & 255 # might return None
259 ret = (dispatch.dispatch(req) or 0) & 255 # might return None
257 self.cresult.write(struct.pack('>i', int(ret)))
260 self.cresult.write(struct.pack('>i', int(ret)))
258 finally:
261 finally:
259 # restore old cwd
262 # restore old cwd
260 if '--cwd' in args:
263 if '--cwd' in args:
261 os.chdir(self.cwd)
264 os.chdir(self.cwd)
262
265
263 def getencoding(self):
266 def getencoding(self):
264 """ writes the current encoding to the result channel """
267 """ writes the current encoding to the result channel """
265 self.cresult.write(encoding.encoding)
268 self.cresult.write(encoding.encoding)
266
269
267 def serveone(self):
270 def serveone(self):
268 cmd = self.client.readline()[:-1]
271 cmd = self.client.readline()[:-1]
269 if cmd:
272 if cmd:
270 handler = self.capabilities.get(cmd)
273 handler = self.capabilities.get(cmd)
271 if handler:
274 if handler:
272 handler(self)
275 handler(self)
273 else:
276 else:
274 # clients are expected to check what commands are supported by
277 # clients are expected to check what commands are supported by
275 # looking at the servers capabilities
278 # looking at the servers capabilities
276 raise error.Abort(_('unknown command %s') % cmd)
279 raise error.Abort(_('unknown command %s') % cmd)
277
280
278 return cmd != ''
281 return cmd != ''
279
282
280 capabilities = {'runcommand': runcommand,
283 capabilities = {'runcommand': runcommand,
281 'getencoding': getencoding}
284 'getencoding': getencoding}
282
285
283 def serve(self):
286 def serve(self):
284 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
287 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
285 hellomsg += '\n'
288 hellomsg += '\n'
286 hellomsg += 'encoding: ' + encoding.encoding
289 hellomsg += 'encoding: ' + encoding.encoding
287 hellomsg += '\n'
290 hellomsg += '\n'
288 hellomsg += 'pid: %d' % util.getpid()
291 hellomsg += 'pid: %d' % util.getpid()
289 if util.safehasattr(os, 'getpgid'):
292 if util.safehasattr(os, 'getpgid'):
290 hellomsg += '\n'
293 hellomsg += '\n'
291 hellomsg += 'pgid: %d' % os.getpgid(0)
294 hellomsg += 'pgid: %d' % os.getpgid(0)
292
295
293 # write the hello msg in -one- chunk
296 # write the hello msg in -one- chunk
294 self.cout.write(hellomsg)
297 self.cout.write(hellomsg)
295
298
296 try:
299 try:
297 while self.serveone():
300 while self.serveone():
298 pass
301 pass
299 except EOFError:
302 except EOFError:
300 # we'll get here if the client disconnected while we were reading
303 # we'll get here if the client disconnected while we were reading
301 # its request
304 # its request
302 return 1
305 return 1
303
306
304 return 0
307 return 0
305
308
306 def _protectio(ui):
309 def _protectio(ui):
307 """ duplicates streams and redirect original to null if ui uses stdio """
310 """ duplicates streams and redirect original to null if ui uses stdio """
308 ui.flush()
311 ui.flush()
309 newfiles = []
312 newfiles = []
310 nullfd = os.open(os.devnull, os.O_RDWR)
313 nullfd = os.open(os.devnull, os.O_RDWR)
311 for f, sysf, mode in [(ui.fin, util.stdin, r'rb'),
314 for f, sysf, mode in [(ui.fin, procutil.stdin, r'rb'),
312 (ui.fout, util.stdout, r'wb')]:
315 (ui.fout, procutil.stdout, r'wb')]:
313 if f is sysf:
316 if f is sysf:
314 newfd = os.dup(f.fileno())
317 newfd = os.dup(f.fileno())
315 os.dup2(nullfd, f.fileno())
318 os.dup2(nullfd, f.fileno())
316 f = os.fdopen(newfd, mode)
319 f = os.fdopen(newfd, mode)
317 newfiles.append(f)
320 newfiles.append(f)
318 os.close(nullfd)
321 os.close(nullfd)
319 return tuple(newfiles)
322 return tuple(newfiles)
320
323
321 def _restoreio(ui, fin, fout):
324 def _restoreio(ui, fin, fout):
322 """ restores streams from duplicated ones """
325 """ restores streams from duplicated ones """
323 ui.flush()
326 ui.flush()
324 for f, uif in [(fin, ui.fin), (fout, ui.fout)]:
327 for f, uif in [(fin, ui.fin), (fout, ui.fout)]:
325 if f is not uif:
328 if f is not uif:
326 os.dup2(f.fileno(), uif.fileno())
329 os.dup2(f.fileno(), uif.fileno())
327 f.close()
330 f.close()
328
331
329 class pipeservice(object):
332 class pipeservice(object):
330 def __init__(self, ui, repo, opts):
333 def __init__(self, ui, repo, opts):
331 self.ui = ui
334 self.ui = ui
332 self.repo = repo
335 self.repo = repo
333
336
334 def init(self):
337 def init(self):
335 pass
338 pass
336
339
337 def run(self):
340 def run(self):
338 ui = self.ui
341 ui = self.ui
339 # redirect stdio to null device so that broken extensions or in-process
342 # redirect stdio to null device so that broken extensions or in-process
340 # hooks will never cause corruption of channel protocol.
343 # hooks will never cause corruption of channel protocol.
341 fin, fout = _protectio(ui)
344 fin, fout = _protectio(ui)
342 try:
345 try:
343 sv = server(ui, self.repo, fin, fout)
346 sv = server(ui, self.repo, fin, fout)
344 return sv.serve()
347 return sv.serve()
345 finally:
348 finally:
346 sv.cleanup()
349 sv.cleanup()
347 _restoreio(ui, fin, fout)
350 _restoreio(ui, fin, fout)
348
351
349 def _initworkerprocess():
352 def _initworkerprocess():
350 # use a different process group from the master process, in order to:
353 # use a different process group from the master process, in order to:
351 # 1. make the current process group no longer "orphaned" (because the
354 # 1. make the current process group no longer "orphaned" (because the
352 # parent of this process is in a different process group while
355 # parent of this process is in a different process group while
353 # remains in a same session)
356 # remains in a same session)
354 # according to POSIX 2.2.2.52, orphaned process group will ignore
357 # according to POSIX 2.2.2.52, orphaned process group will ignore
355 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
358 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
356 # cause trouble for things like ncurses.
359 # cause trouble for things like ncurses.
357 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
360 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
358 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
361 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
359 # processes like ssh will be killed properly, without affecting
362 # processes like ssh will be killed properly, without affecting
360 # unrelated processes.
363 # unrelated processes.
361 os.setpgid(0, 0)
364 os.setpgid(0, 0)
362 # change random state otherwise forked request handlers would have a
365 # change random state otherwise forked request handlers would have a
363 # same state inherited from parent.
366 # same state inherited from parent.
364 random.seed()
367 random.seed()
365
368
366 def _serverequest(ui, repo, conn, createcmdserver):
369 def _serverequest(ui, repo, conn, createcmdserver):
367 fin = conn.makefile('rb')
370 fin = conn.makefile('rb')
368 fout = conn.makefile('wb')
371 fout = conn.makefile('wb')
369 sv = None
372 sv = None
370 try:
373 try:
371 sv = createcmdserver(repo, conn, fin, fout)
374 sv = createcmdserver(repo, conn, fin, fout)
372 try:
375 try:
373 sv.serve()
376 sv.serve()
374 # handle exceptions that may be raised by command server. most of
377 # handle exceptions that may be raised by command server. most of
375 # known exceptions are caught by dispatch.
378 # known exceptions are caught by dispatch.
376 except error.Abort as inst:
379 except error.Abort as inst:
377 ui.warn(_('abort: %s\n') % inst)
380 ui.warn(_('abort: %s\n') % inst)
378 except IOError as inst:
381 except IOError as inst:
379 if inst.errno != errno.EPIPE:
382 if inst.errno != errno.EPIPE:
380 raise
383 raise
381 except KeyboardInterrupt:
384 except KeyboardInterrupt:
382 pass
385 pass
383 finally:
386 finally:
384 sv.cleanup()
387 sv.cleanup()
385 except: # re-raises
388 except: # re-raises
386 # also write traceback to error channel. otherwise client cannot
389 # also write traceback to error channel. otherwise client cannot
387 # see it because it is written to server's stderr by default.
390 # see it because it is written to server's stderr by default.
388 if sv:
391 if sv:
389 cerr = sv.cerr
392 cerr = sv.cerr
390 else:
393 else:
391 cerr = channeledoutput(fout, 'e')
394 cerr = channeledoutput(fout, 'e')
392 traceback.print_exc(file=cerr)
395 traceback.print_exc(file=cerr)
393 raise
396 raise
394 finally:
397 finally:
395 fin.close()
398 fin.close()
396 try:
399 try:
397 fout.close() # implicit flush() may cause another EPIPE
400 fout.close() # implicit flush() may cause another EPIPE
398 except IOError as inst:
401 except IOError as inst:
399 if inst.errno != errno.EPIPE:
402 if inst.errno != errno.EPIPE:
400 raise
403 raise
401
404
402 class unixservicehandler(object):
405 class unixservicehandler(object):
403 """Set of pluggable operations for unix-mode services
406 """Set of pluggable operations for unix-mode services
404
407
405 Almost all methods except for createcmdserver() are called in the main
408 Almost all methods except for createcmdserver() are called in the main
406 process. You can't pass mutable resource back from createcmdserver().
409 process. You can't pass mutable resource back from createcmdserver().
407 """
410 """
408
411
409 pollinterval = None
412 pollinterval = None
410
413
411 def __init__(self, ui):
414 def __init__(self, ui):
412 self.ui = ui
415 self.ui = ui
413
416
414 def bindsocket(self, sock, address):
417 def bindsocket(self, sock, address):
415 util.bindunixsocket(sock, address)
418 util.bindunixsocket(sock, address)
416 sock.listen(socket.SOMAXCONN)
419 sock.listen(socket.SOMAXCONN)
417 self.ui.status(_('listening at %s\n') % address)
420 self.ui.status(_('listening at %s\n') % address)
418 self.ui.flush() # avoid buffering of status message
421 self.ui.flush() # avoid buffering of status message
419
422
420 def unlinksocket(self, address):
423 def unlinksocket(self, address):
421 os.unlink(address)
424 os.unlink(address)
422
425
423 def shouldexit(self):
426 def shouldexit(self):
424 """True if server should shut down; checked per pollinterval"""
427 """True if server should shut down; checked per pollinterval"""
425 return False
428 return False
426
429
427 def newconnection(self):
430 def newconnection(self):
428 """Called when main process notices new connection"""
431 """Called when main process notices new connection"""
429
432
430 def createcmdserver(self, repo, conn, fin, fout):
433 def createcmdserver(self, repo, conn, fin, fout):
431 """Create new command server instance; called in the process that
434 """Create new command server instance; called in the process that
432 serves for the current connection"""
435 serves for the current connection"""
433 return server(self.ui, repo, fin, fout)
436 return server(self.ui, repo, fin, fout)
434
437
435 class unixforkingservice(object):
438 class unixforkingservice(object):
436 """
439 """
437 Listens on unix domain socket and forks server per connection
440 Listens on unix domain socket and forks server per connection
438 """
441 """
439
442
440 def __init__(self, ui, repo, opts, handler=None):
443 def __init__(self, ui, repo, opts, handler=None):
441 self.ui = ui
444 self.ui = ui
442 self.repo = repo
445 self.repo = repo
443 self.address = opts['address']
446 self.address = opts['address']
444 if not util.safehasattr(socket, 'AF_UNIX'):
447 if not util.safehasattr(socket, 'AF_UNIX'):
445 raise error.Abort(_('unsupported platform'))
448 raise error.Abort(_('unsupported platform'))
446 if not self.address:
449 if not self.address:
447 raise error.Abort(_('no socket path specified with --address'))
450 raise error.Abort(_('no socket path specified with --address'))
448 self._servicehandler = handler or unixservicehandler(ui)
451 self._servicehandler = handler or unixservicehandler(ui)
449 self._sock = None
452 self._sock = None
450 self._oldsigchldhandler = None
453 self._oldsigchldhandler = None
451 self._workerpids = set() # updated by signal handler; do not iterate
454 self._workerpids = set() # updated by signal handler; do not iterate
452 self._socketunlinked = None
455 self._socketunlinked = None
453
456
454 def init(self):
457 def init(self):
455 self._sock = socket.socket(socket.AF_UNIX)
458 self._sock = socket.socket(socket.AF_UNIX)
456 self._servicehandler.bindsocket(self._sock, self.address)
459 self._servicehandler.bindsocket(self._sock, self.address)
457 if util.safehasattr(util, 'unblocksignal'):
460 if util.safehasattr(util, 'unblocksignal'):
458 util.unblocksignal(signal.SIGCHLD)
461 util.unblocksignal(signal.SIGCHLD)
459 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
462 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
460 self._oldsigchldhandler = o
463 self._oldsigchldhandler = o
461 self._socketunlinked = False
464 self._socketunlinked = False
462
465
463 def _unlinksocket(self):
466 def _unlinksocket(self):
464 if not self._socketunlinked:
467 if not self._socketunlinked:
465 self._servicehandler.unlinksocket(self.address)
468 self._servicehandler.unlinksocket(self.address)
466 self._socketunlinked = True
469 self._socketunlinked = True
467
470
468 def _cleanup(self):
471 def _cleanup(self):
469 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
472 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
470 self._sock.close()
473 self._sock.close()
471 self._unlinksocket()
474 self._unlinksocket()
472 # don't kill child processes as they have active clients, just wait
475 # don't kill child processes as they have active clients, just wait
473 self._reapworkers(0)
476 self._reapworkers(0)
474
477
475 def run(self):
478 def run(self):
476 try:
479 try:
477 self._mainloop()
480 self._mainloop()
478 finally:
481 finally:
479 self._cleanup()
482 self._cleanup()
480
483
481 def _mainloop(self):
484 def _mainloop(self):
482 exiting = False
485 exiting = False
483 h = self._servicehandler
486 h = self._servicehandler
484 selector = selectors.DefaultSelector()
487 selector = selectors.DefaultSelector()
485 selector.register(self._sock, selectors.EVENT_READ)
488 selector.register(self._sock, selectors.EVENT_READ)
486 while True:
489 while True:
487 if not exiting and h.shouldexit():
490 if not exiting and h.shouldexit():
488 # clients can no longer connect() to the domain socket, so
491 # clients can no longer connect() to the domain socket, so
489 # we stop queuing new requests.
492 # we stop queuing new requests.
490 # for requests that are queued (connect()-ed, but haven't been
493 # for requests that are queued (connect()-ed, but haven't been
491 # accept()-ed), handle them before exit. otherwise, clients
494 # accept()-ed), handle them before exit. otherwise, clients
492 # waiting for recv() will receive ECONNRESET.
495 # waiting for recv() will receive ECONNRESET.
493 self._unlinksocket()
496 self._unlinksocket()
494 exiting = True
497 exiting = True
495 ready = selector.select(timeout=h.pollinterval)
498 ready = selector.select(timeout=h.pollinterval)
496 if not ready:
499 if not ready:
497 # only exit if we completed all queued requests
500 # only exit if we completed all queued requests
498 if exiting:
501 if exiting:
499 break
502 break
500 continue
503 continue
501 try:
504 try:
502 conn, _addr = self._sock.accept()
505 conn, _addr = self._sock.accept()
503 except socket.error as inst:
506 except socket.error as inst:
504 if inst.args[0] == errno.EINTR:
507 if inst.args[0] == errno.EINTR:
505 continue
508 continue
506 raise
509 raise
507
510
508 pid = os.fork()
511 pid = os.fork()
509 if pid:
512 if pid:
510 try:
513 try:
511 self.ui.debug('forked worker process (pid=%d)\n' % pid)
514 self.ui.debug('forked worker process (pid=%d)\n' % pid)
512 self._workerpids.add(pid)
515 self._workerpids.add(pid)
513 h.newconnection()
516 h.newconnection()
514 finally:
517 finally:
515 conn.close() # release handle in parent process
518 conn.close() # release handle in parent process
516 else:
519 else:
517 try:
520 try:
518 self._runworker(conn)
521 self._runworker(conn)
519 conn.close()
522 conn.close()
520 os._exit(0)
523 os._exit(0)
521 except: # never return, hence no re-raises
524 except: # never return, hence no re-raises
522 try:
525 try:
523 self.ui.traceback(force=True)
526 self.ui.traceback(force=True)
524 finally:
527 finally:
525 os._exit(255)
528 os._exit(255)
526 selector.close()
529 selector.close()
527
530
528 def _sigchldhandler(self, signal, frame):
531 def _sigchldhandler(self, signal, frame):
529 self._reapworkers(os.WNOHANG)
532 self._reapworkers(os.WNOHANG)
530
533
531 def _reapworkers(self, options):
534 def _reapworkers(self, options):
532 while self._workerpids:
535 while self._workerpids:
533 try:
536 try:
534 pid, _status = os.waitpid(-1, options)
537 pid, _status = os.waitpid(-1, options)
535 except OSError as inst:
538 except OSError as inst:
536 if inst.errno == errno.EINTR:
539 if inst.errno == errno.EINTR:
537 continue
540 continue
538 if inst.errno != errno.ECHILD:
541 if inst.errno != errno.ECHILD:
539 raise
542 raise
540 # no child processes at all (reaped by other waitpid()?)
543 # no child processes at all (reaped by other waitpid()?)
541 self._workerpids.clear()
544 self._workerpids.clear()
542 return
545 return
543 if pid == 0:
546 if pid == 0:
544 # no waitable child processes
547 # no waitable child processes
545 return
548 return
546 self.ui.debug('worker process exited (pid=%d)\n' % pid)
549 self.ui.debug('worker process exited (pid=%d)\n' % pid)
547 self._workerpids.discard(pid)
550 self._workerpids.discard(pid)
548
551
549 def _runworker(self, conn):
552 def _runworker(self, conn):
550 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
553 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
551 _initworkerprocess()
554 _initworkerprocess()
552 h = self._servicehandler
555 h = self._servicehandler
553 try:
556 try:
554 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
557 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
555 finally:
558 finally:
556 gc.collect() # trigger __del__ since worker process uses os._exit
559 gc.collect() # trigger __del__ since worker process uses os._exit
@@ -1,1029 +1,1030 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 difflib
10 import difflib
11 import errno
11 import errno
12 import getopt
12 import getopt
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 time
18 import time
19 import traceback
19 import traceback
20
20
21
21
22 from .i18n import _
22 from .i18n import _
23
23
24 from . import (
24 from . import (
25 cmdutil,
25 cmdutil,
26 color,
26 color,
27 commands,
27 commands,
28 demandimport,
28 demandimport,
29 encoding,
29 encoding,
30 error,
30 error,
31 extensions,
31 extensions,
32 fancyopts,
32 fancyopts,
33 help,
33 help,
34 hg,
34 hg,
35 hook,
35 hook,
36 profiling,
36 profiling,
37 pycompat,
37 pycompat,
38 registrar,
38 registrar,
39 scmutil,
39 scmutil,
40 ui as uimod,
40 ui as uimod,
41 util,
41 util,
42 )
42 )
43
43
44 from .utils import (
44 from .utils import (
45 procutil,
45 stringutil,
46 stringutil,
46 )
47 )
47
48
48 unrecoverablewrite = registrar.command.unrecoverablewrite
49 unrecoverablewrite = registrar.command.unrecoverablewrite
49
50
50 class request(object):
51 class request(object):
51 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
52 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
52 ferr=None, prereposetups=None):
53 ferr=None, prereposetups=None):
53 self.args = args
54 self.args = args
54 self.ui = ui
55 self.ui = ui
55 self.repo = repo
56 self.repo = repo
56
57
57 # input/output/error streams
58 # input/output/error streams
58 self.fin = fin
59 self.fin = fin
59 self.fout = fout
60 self.fout = fout
60 self.ferr = ferr
61 self.ferr = ferr
61
62
62 # remember options pre-parsed by _earlyparseopts()
63 # remember options pre-parsed by _earlyparseopts()
63 self.earlyoptions = {}
64 self.earlyoptions = {}
64
65
65 # reposetups which run before extensions, useful for chg to pre-fill
66 # reposetups which run before extensions, useful for chg to pre-fill
66 # low-level repo state (for example, changelog) before extensions.
67 # low-level repo state (for example, changelog) before extensions.
67 self.prereposetups = prereposetups or []
68 self.prereposetups = prereposetups or []
68
69
69 def _runexithandlers(self):
70 def _runexithandlers(self):
70 exc = None
71 exc = None
71 handlers = self.ui._exithandlers
72 handlers = self.ui._exithandlers
72 try:
73 try:
73 while handlers:
74 while handlers:
74 func, args, kwargs = handlers.pop()
75 func, args, kwargs = handlers.pop()
75 try:
76 try:
76 func(*args, **kwargs)
77 func(*args, **kwargs)
77 except: # re-raises below
78 except: # re-raises below
78 if exc is None:
79 if exc is None:
79 exc = sys.exc_info()[1]
80 exc = sys.exc_info()[1]
80 self.ui.warn(('error in exit handlers:\n'))
81 self.ui.warn(('error in exit handlers:\n'))
81 self.ui.traceback(force=True)
82 self.ui.traceback(force=True)
82 finally:
83 finally:
83 if exc is not None:
84 if exc is not None:
84 raise exc
85 raise exc
85
86
86 def run():
87 def run():
87 "run the command in sys.argv"
88 "run the command in sys.argv"
88 _initstdio()
89 _initstdio()
89 req = request(pycompat.sysargv[1:])
90 req = request(pycompat.sysargv[1:])
90 err = None
91 err = None
91 try:
92 try:
92 status = (dispatch(req) or 0)
93 status = (dispatch(req) or 0)
93 except error.StdioError as e:
94 except error.StdioError as e:
94 err = e
95 err = e
95 status = -1
96 status = -1
96 if util.safehasattr(req.ui, 'fout'):
97 if util.safehasattr(req.ui, 'fout'):
97 try:
98 try:
98 req.ui.fout.flush()
99 req.ui.fout.flush()
99 except IOError as e:
100 except IOError as e:
100 err = e
101 err = e
101 status = -1
102 status = -1
102 if util.safehasattr(req.ui, 'ferr'):
103 if util.safehasattr(req.ui, 'ferr'):
103 try:
104 try:
104 if err is not None and err.errno != errno.EPIPE:
105 if err is not None and err.errno != errno.EPIPE:
105 req.ui.ferr.write('abort: %s\n' %
106 req.ui.ferr.write('abort: %s\n' %
106 encoding.strtolocal(err.strerror))
107 encoding.strtolocal(err.strerror))
107 req.ui.ferr.flush()
108 req.ui.ferr.flush()
108 # There's not much we can do about an I/O error here. So (possibly)
109 # There's not much we can do about an I/O error here. So (possibly)
109 # change the status code and move on.
110 # change the status code and move on.
110 except IOError:
111 except IOError:
111 status = -1
112 status = -1
112
113
113 _silencestdio()
114 _silencestdio()
114 sys.exit(status & 255)
115 sys.exit(status & 255)
115
116
116 if pycompat.ispy3:
117 if pycompat.ispy3:
117 def _initstdio():
118 def _initstdio():
118 pass
119 pass
119
120
120 def _silencestdio():
121 def _silencestdio():
121 for fp in (sys.stdout, sys.stderr):
122 for fp in (sys.stdout, sys.stderr):
122 # Check if the file is okay
123 # Check if the file is okay
123 try:
124 try:
124 fp.flush()
125 fp.flush()
125 continue
126 continue
126 except IOError:
127 except IOError:
127 pass
128 pass
128 # Otherwise mark it as closed to silence "Exception ignored in"
129 # Otherwise mark it as closed to silence "Exception ignored in"
129 # message emitted by the interpreter finalizer. Be careful to
130 # message emitted by the interpreter finalizer. Be careful to
130 # not close util.stdout, which may be a fdopen-ed file object and
131 # not close procutil.stdout, which may be a fdopen-ed file object
131 # its close() actually closes the underlying file descriptor.
132 # and its close() actually closes the underlying file descriptor.
132 try:
133 try:
133 fp.close()
134 fp.close()
134 except IOError:
135 except IOError:
135 pass
136 pass
136 else:
137 else:
137 def _initstdio():
138 def _initstdio():
138 for fp in (sys.stdin, sys.stdout, sys.stderr):
139 for fp in (sys.stdin, sys.stdout, sys.stderr):
139 util.setbinary(fp)
140 util.setbinary(fp)
140
141
141 def _silencestdio():
142 def _silencestdio():
142 pass
143 pass
143
144
144 def _getsimilar(symbols, value):
145 def _getsimilar(symbols, value):
145 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
146 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
146 # The cutoff for similarity here is pretty arbitrary. It should
147 # The cutoff for similarity here is pretty arbitrary. It should
147 # probably be investigated and tweaked.
148 # probably be investigated and tweaked.
148 return [s for s in symbols if sim(s) > 0.6]
149 return [s for s in symbols if sim(s) > 0.6]
149
150
150 def _reportsimilar(write, similar):
151 def _reportsimilar(write, similar):
151 if len(similar) == 1:
152 if len(similar) == 1:
152 write(_("(did you mean %s?)\n") % similar[0])
153 write(_("(did you mean %s?)\n") % similar[0])
153 elif similar:
154 elif similar:
154 ss = ", ".join(sorted(similar))
155 ss = ", ".join(sorted(similar))
155 write(_("(did you mean one of %s?)\n") % ss)
156 write(_("(did you mean one of %s?)\n") % ss)
156
157
157 def _formatparse(write, inst):
158 def _formatparse(write, inst):
158 similar = []
159 similar = []
159 if isinstance(inst, error.UnknownIdentifier):
160 if isinstance(inst, error.UnknownIdentifier):
160 # make sure to check fileset first, as revset can invoke fileset
161 # make sure to check fileset first, as revset can invoke fileset
161 similar = _getsimilar(inst.symbols, inst.function)
162 similar = _getsimilar(inst.symbols, inst.function)
162 if len(inst.args) > 1:
163 if len(inst.args) > 1:
163 write(_("hg: parse error at %s: %s\n") %
164 write(_("hg: parse error at %s: %s\n") %
164 (pycompat.bytestr(inst.args[1]), inst.args[0]))
165 (pycompat.bytestr(inst.args[1]), inst.args[0]))
165 if inst.args[0].startswith(' '):
166 if inst.args[0].startswith(' '):
166 write(_("unexpected leading whitespace\n"))
167 write(_("unexpected leading whitespace\n"))
167 else:
168 else:
168 write(_("hg: parse error: %s\n") % inst.args[0])
169 write(_("hg: parse error: %s\n") % inst.args[0])
169 _reportsimilar(write, similar)
170 _reportsimilar(write, similar)
170 if inst.hint:
171 if inst.hint:
171 write(_("(%s)\n") % inst.hint)
172 write(_("(%s)\n") % inst.hint)
172
173
173 def _formatargs(args):
174 def _formatargs(args):
174 return ' '.join(util.shellquote(a) for a in args)
175 return ' '.join(util.shellquote(a) for a in args)
175
176
176 def dispatch(req):
177 def dispatch(req):
177 "run the command specified in req.args"
178 "run the command specified in req.args"
178 if req.ferr:
179 if req.ferr:
179 ferr = req.ferr
180 ferr = req.ferr
180 elif req.ui:
181 elif req.ui:
181 ferr = req.ui.ferr
182 ferr = req.ui.ferr
182 else:
183 else:
183 ferr = util.stderr
184 ferr = procutil.stderr
184
185
185 try:
186 try:
186 if not req.ui:
187 if not req.ui:
187 req.ui = uimod.ui.load()
188 req.ui = uimod.ui.load()
188 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
189 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
189 if req.earlyoptions['traceback']:
190 if req.earlyoptions['traceback']:
190 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
191 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
191
192
192 # set ui streams from the request
193 # set ui streams from the request
193 if req.fin:
194 if req.fin:
194 req.ui.fin = req.fin
195 req.ui.fin = req.fin
195 if req.fout:
196 if req.fout:
196 req.ui.fout = req.fout
197 req.ui.fout = req.fout
197 if req.ferr:
198 if req.ferr:
198 req.ui.ferr = req.ferr
199 req.ui.ferr = req.ferr
199 except error.Abort as inst:
200 except error.Abort as inst:
200 ferr.write(_("abort: %s\n") % inst)
201 ferr.write(_("abort: %s\n") % inst)
201 if inst.hint:
202 if inst.hint:
202 ferr.write(_("(%s)\n") % inst.hint)
203 ferr.write(_("(%s)\n") % inst.hint)
203 return -1
204 return -1
204 except error.ParseError as inst:
205 except error.ParseError as inst:
205 _formatparse(ferr.write, inst)
206 _formatparse(ferr.write, inst)
206 return -1
207 return -1
207
208
208 msg = _formatargs(req.args)
209 msg = _formatargs(req.args)
209 starttime = util.timer()
210 starttime = util.timer()
210 ret = None
211 ret = None
211 try:
212 try:
212 ret = _runcatch(req)
213 ret = _runcatch(req)
213 except error.ProgrammingError as inst:
214 except error.ProgrammingError as inst:
214 req.ui.warn(_('** ProgrammingError: %s\n') % inst)
215 req.ui.warn(_('** ProgrammingError: %s\n') % inst)
215 if inst.hint:
216 if inst.hint:
216 req.ui.warn(_('** (%s)\n') % inst.hint)
217 req.ui.warn(_('** (%s)\n') % inst.hint)
217 raise
218 raise
218 except KeyboardInterrupt as inst:
219 except KeyboardInterrupt as inst:
219 try:
220 try:
220 if isinstance(inst, error.SignalInterrupt):
221 if isinstance(inst, error.SignalInterrupt):
221 msg = _("killed!\n")
222 msg = _("killed!\n")
222 else:
223 else:
223 msg = _("interrupted!\n")
224 msg = _("interrupted!\n")
224 req.ui.warn(msg)
225 req.ui.warn(msg)
225 except error.SignalInterrupt:
226 except error.SignalInterrupt:
226 # maybe pager would quit without consuming all the output, and
227 # maybe pager would quit without consuming all the output, and
227 # SIGPIPE was raised. we cannot print anything in this case.
228 # SIGPIPE was raised. we cannot print anything in this case.
228 pass
229 pass
229 except IOError as inst:
230 except IOError as inst:
230 if inst.errno != errno.EPIPE:
231 if inst.errno != errno.EPIPE:
231 raise
232 raise
232 ret = -1
233 ret = -1
233 finally:
234 finally:
234 duration = util.timer() - starttime
235 duration = util.timer() - starttime
235 req.ui.flush()
236 req.ui.flush()
236 if req.ui.logblockedtimes:
237 if req.ui.logblockedtimes:
237 req.ui._blockedtimes['command_duration'] = duration * 1000
238 req.ui._blockedtimes['command_duration'] = duration * 1000
238 req.ui.log('uiblocked', 'ui blocked ms',
239 req.ui.log('uiblocked', 'ui blocked ms',
239 **pycompat.strkwargs(req.ui._blockedtimes))
240 **pycompat.strkwargs(req.ui._blockedtimes))
240 req.ui.log("commandfinish", "%s exited %d after %0.2f seconds\n",
241 req.ui.log("commandfinish", "%s exited %d after %0.2f seconds\n",
241 msg, ret or 0, duration)
242 msg, ret or 0, duration)
242 try:
243 try:
243 req._runexithandlers()
244 req._runexithandlers()
244 except: # exiting, so no re-raises
245 except: # exiting, so no re-raises
245 ret = ret or -1
246 ret = ret or -1
246 return ret
247 return ret
247
248
248 def _runcatch(req):
249 def _runcatch(req):
249 def catchterm(*args):
250 def catchterm(*args):
250 raise error.SignalInterrupt
251 raise error.SignalInterrupt
251
252
252 ui = req.ui
253 ui = req.ui
253 try:
254 try:
254 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
255 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
255 num = getattr(signal, name, None)
256 num = getattr(signal, name, None)
256 if num:
257 if num:
257 signal.signal(num, catchterm)
258 signal.signal(num, catchterm)
258 except ValueError:
259 except ValueError:
259 pass # happens if called in a thread
260 pass # happens if called in a thread
260
261
261 def _runcatchfunc():
262 def _runcatchfunc():
262 realcmd = None
263 realcmd = None
263 try:
264 try:
264 cmdargs = fancyopts.fancyopts(req.args[:], commands.globalopts, {})
265 cmdargs = fancyopts.fancyopts(req.args[:], commands.globalopts, {})
265 cmd = cmdargs[0]
266 cmd = cmdargs[0]
266 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
267 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
267 realcmd = aliases[0]
268 realcmd = aliases[0]
268 except (error.UnknownCommand, error.AmbiguousCommand,
269 except (error.UnknownCommand, error.AmbiguousCommand,
269 IndexError, getopt.GetoptError):
270 IndexError, getopt.GetoptError):
270 # Don't handle this here. We know the command is
271 # Don't handle this here. We know the command is
271 # invalid, but all we're worried about for now is that
272 # invalid, but all we're worried about for now is that
272 # it's not a command that server operators expect to
273 # it's not a command that server operators expect to
273 # be safe to offer to users in a sandbox.
274 # be safe to offer to users in a sandbox.
274 pass
275 pass
275 if realcmd == 'serve' and '--stdio' in cmdargs:
276 if realcmd == 'serve' and '--stdio' in cmdargs:
276 # We want to constrain 'hg serve --stdio' instances pretty
277 # We want to constrain 'hg serve --stdio' instances pretty
277 # closely, as many shared-ssh access tools want to grant
278 # closely, as many shared-ssh access tools want to grant
278 # access to run *only* 'hg -R $repo serve --stdio'. We
279 # access to run *only* 'hg -R $repo serve --stdio'. We
279 # restrict to exactly that set of arguments, and prohibit
280 # restrict to exactly that set of arguments, and prohibit
280 # any repo name that starts with '--' to prevent
281 # any repo name that starts with '--' to prevent
281 # shenanigans wherein a user does something like pass
282 # shenanigans wherein a user does something like pass
282 # --debugger or --config=ui.debugger=1 as a repo
283 # --debugger or --config=ui.debugger=1 as a repo
283 # name. This used to actually run the debugger.
284 # name. This used to actually run the debugger.
284 if (len(req.args) != 4 or
285 if (len(req.args) != 4 or
285 req.args[0] != '-R' or
286 req.args[0] != '-R' or
286 req.args[1].startswith('--') or
287 req.args[1].startswith('--') or
287 req.args[2] != 'serve' or
288 req.args[2] != 'serve' or
288 req.args[3] != '--stdio'):
289 req.args[3] != '--stdio'):
289 raise error.Abort(
290 raise error.Abort(
290 _('potentially unsafe serve --stdio invocation: %r') %
291 _('potentially unsafe serve --stdio invocation: %r') %
291 (req.args,))
292 (req.args,))
292
293
293 try:
294 try:
294 debugger = 'pdb'
295 debugger = 'pdb'
295 debugtrace = {
296 debugtrace = {
296 'pdb': pdb.set_trace
297 'pdb': pdb.set_trace
297 }
298 }
298 debugmortem = {
299 debugmortem = {
299 'pdb': pdb.post_mortem
300 'pdb': pdb.post_mortem
300 }
301 }
301
302
302 # read --config before doing anything else
303 # read --config before doing anything else
303 # (e.g. to change trust settings for reading .hg/hgrc)
304 # (e.g. to change trust settings for reading .hg/hgrc)
304 cfgs = _parseconfig(req.ui, req.earlyoptions['config'])
305 cfgs = _parseconfig(req.ui, req.earlyoptions['config'])
305
306
306 if req.repo:
307 if req.repo:
307 # copy configs that were passed on the cmdline (--config) to
308 # copy configs that were passed on the cmdline (--config) to
308 # the repo ui
309 # the repo ui
309 for sec, name, val in cfgs:
310 for sec, name, val in cfgs:
310 req.repo.ui.setconfig(sec, name, val, source='--config')
311 req.repo.ui.setconfig(sec, name, val, source='--config')
311
312
312 # developer config: ui.debugger
313 # developer config: ui.debugger
313 debugger = ui.config("ui", "debugger")
314 debugger = ui.config("ui", "debugger")
314 debugmod = pdb
315 debugmod = pdb
315 if not debugger or ui.plain():
316 if not debugger or ui.plain():
316 # if we are in HGPLAIN mode, then disable custom debugging
317 # if we are in HGPLAIN mode, then disable custom debugging
317 debugger = 'pdb'
318 debugger = 'pdb'
318 elif req.earlyoptions['debugger']:
319 elif req.earlyoptions['debugger']:
319 # This import can be slow for fancy debuggers, so only
320 # This import can be slow for fancy debuggers, so only
320 # do it when absolutely necessary, i.e. when actual
321 # do it when absolutely necessary, i.e. when actual
321 # debugging has been requested
322 # debugging has been requested
322 with demandimport.deactivated():
323 with demandimport.deactivated():
323 try:
324 try:
324 debugmod = __import__(debugger)
325 debugmod = __import__(debugger)
325 except ImportError:
326 except ImportError:
326 pass # Leave debugmod = pdb
327 pass # Leave debugmod = pdb
327
328
328 debugtrace[debugger] = debugmod.set_trace
329 debugtrace[debugger] = debugmod.set_trace
329 debugmortem[debugger] = debugmod.post_mortem
330 debugmortem[debugger] = debugmod.post_mortem
330
331
331 # enter the debugger before command execution
332 # enter the debugger before command execution
332 if req.earlyoptions['debugger']:
333 if req.earlyoptions['debugger']:
333 ui.warn(_("entering debugger - "
334 ui.warn(_("entering debugger - "
334 "type c to continue starting hg or h for help\n"))
335 "type c to continue starting hg or h for help\n"))
335
336
336 if (debugger != 'pdb' and
337 if (debugger != 'pdb' and
337 debugtrace[debugger] == debugtrace['pdb']):
338 debugtrace[debugger] == debugtrace['pdb']):
338 ui.warn(_("%s debugger specified "
339 ui.warn(_("%s debugger specified "
339 "but its module was not found\n") % debugger)
340 "but its module was not found\n") % debugger)
340 with demandimport.deactivated():
341 with demandimport.deactivated():
341 debugtrace[debugger]()
342 debugtrace[debugger]()
342 try:
343 try:
343 return _dispatch(req)
344 return _dispatch(req)
344 finally:
345 finally:
345 ui.flush()
346 ui.flush()
346 except: # re-raises
347 except: # re-raises
347 # enter the debugger when we hit an exception
348 # enter the debugger when we hit an exception
348 if req.earlyoptions['debugger']:
349 if req.earlyoptions['debugger']:
349 traceback.print_exc()
350 traceback.print_exc()
350 debugmortem[debugger](sys.exc_info()[2])
351 debugmortem[debugger](sys.exc_info()[2])
351 raise
352 raise
352
353
353 return _callcatch(ui, _runcatchfunc)
354 return _callcatch(ui, _runcatchfunc)
354
355
355 def _callcatch(ui, func):
356 def _callcatch(ui, func):
356 """like scmutil.callcatch but handles more high-level exceptions about
357 """like scmutil.callcatch but handles more high-level exceptions about
357 config parsing and commands. besides, use handlecommandexception to handle
358 config parsing and commands. besides, use handlecommandexception to handle
358 uncaught exceptions.
359 uncaught exceptions.
359 """
360 """
360 try:
361 try:
361 return scmutil.callcatch(ui, func)
362 return scmutil.callcatch(ui, func)
362 except error.AmbiguousCommand as inst:
363 except error.AmbiguousCommand as inst:
363 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
364 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
364 (inst.args[0], " ".join(inst.args[1])))
365 (inst.args[0], " ".join(inst.args[1])))
365 except error.CommandError as inst:
366 except error.CommandError as inst:
366 if inst.args[0]:
367 if inst.args[0]:
367 ui.pager('help')
368 ui.pager('help')
368 msgbytes = pycompat.bytestr(inst.args[1])
369 msgbytes = pycompat.bytestr(inst.args[1])
369 ui.warn(_("hg %s: %s\n") % (inst.args[0], msgbytes))
370 ui.warn(_("hg %s: %s\n") % (inst.args[0], msgbytes))
370 commands.help_(ui, inst.args[0], full=False, command=True)
371 commands.help_(ui, inst.args[0], full=False, command=True)
371 else:
372 else:
372 ui.pager('help')
373 ui.pager('help')
373 ui.warn(_("hg: %s\n") % inst.args[1])
374 ui.warn(_("hg: %s\n") % inst.args[1])
374 commands.help_(ui, 'shortlist')
375 commands.help_(ui, 'shortlist')
375 except error.ParseError as inst:
376 except error.ParseError as inst:
376 _formatparse(ui.warn, inst)
377 _formatparse(ui.warn, inst)
377 return -1
378 return -1
378 except error.UnknownCommand as inst:
379 except error.UnknownCommand as inst:
379 nocmdmsg = _("hg: unknown command '%s'\n") % inst.args[0]
380 nocmdmsg = _("hg: unknown command '%s'\n") % inst.args[0]
380 try:
381 try:
381 # check if the command is in a disabled extension
382 # check if the command is in a disabled extension
382 # (but don't check for extensions themselves)
383 # (but don't check for extensions themselves)
383 formatted = help.formattedhelp(ui, commands, inst.args[0],
384 formatted = help.formattedhelp(ui, commands, inst.args[0],
384 unknowncmd=True)
385 unknowncmd=True)
385 ui.warn(nocmdmsg)
386 ui.warn(nocmdmsg)
386 ui.write(formatted)
387 ui.write(formatted)
387 except (error.UnknownCommand, error.Abort):
388 except (error.UnknownCommand, error.Abort):
388 suggested = False
389 suggested = False
389 if len(inst.args) == 2:
390 if len(inst.args) == 2:
390 sim = _getsimilar(inst.args[1], inst.args[0])
391 sim = _getsimilar(inst.args[1], inst.args[0])
391 if sim:
392 if sim:
392 ui.warn(nocmdmsg)
393 ui.warn(nocmdmsg)
393 _reportsimilar(ui.warn, sim)
394 _reportsimilar(ui.warn, sim)
394 suggested = True
395 suggested = True
395 if not suggested:
396 if not suggested:
396 ui.pager('help')
397 ui.pager('help')
397 ui.warn(nocmdmsg)
398 ui.warn(nocmdmsg)
398 commands.help_(ui, 'shortlist')
399 commands.help_(ui, 'shortlist')
399 except IOError:
400 except IOError:
400 raise
401 raise
401 except KeyboardInterrupt:
402 except KeyboardInterrupt:
402 raise
403 raise
403 except: # probably re-raises
404 except: # probably re-raises
404 if not handlecommandexception(ui):
405 if not handlecommandexception(ui):
405 raise
406 raise
406
407
407 return -1
408 return -1
408
409
409 def aliasargs(fn, givenargs):
410 def aliasargs(fn, givenargs):
410 args = []
411 args = []
411 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
412 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
412 if not util.safehasattr(fn, '_origfunc'):
413 if not util.safehasattr(fn, '_origfunc'):
413 args = getattr(fn, 'args', args)
414 args = getattr(fn, 'args', args)
414 if args:
415 if args:
415 cmd = ' '.join(map(util.shellquote, args))
416 cmd = ' '.join(map(util.shellquote, args))
416
417
417 nums = []
418 nums = []
418 def replacer(m):
419 def replacer(m):
419 num = int(m.group(1)) - 1
420 num = int(m.group(1)) - 1
420 nums.append(num)
421 nums.append(num)
421 if num < len(givenargs):
422 if num < len(givenargs):
422 return givenargs[num]
423 return givenargs[num]
423 raise error.Abort(_('too few arguments for command alias'))
424 raise error.Abort(_('too few arguments for command alias'))
424 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
425 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
425 givenargs = [x for i, x in enumerate(givenargs)
426 givenargs = [x for i, x in enumerate(givenargs)
426 if i not in nums]
427 if i not in nums]
427 args = pycompat.shlexsplit(cmd)
428 args = pycompat.shlexsplit(cmd)
428 return args + givenargs
429 return args + givenargs
429
430
430 def aliasinterpolate(name, args, cmd):
431 def aliasinterpolate(name, args, cmd):
431 '''interpolate args into cmd for shell aliases
432 '''interpolate args into cmd for shell aliases
432
433
433 This also handles $0, $@ and "$@".
434 This also handles $0, $@ and "$@".
434 '''
435 '''
435 # util.interpolate can't deal with "$@" (with quotes) because it's only
436 # util.interpolate can't deal with "$@" (with quotes) because it's only
436 # built to match prefix + patterns.
437 # built to match prefix + patterns.
437 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
438 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
438 replacemap['$0'] = name
439 replacemap['$0'] = name
439 replacemap['$$'] = '$'
440 replacemap['$$'] = '$'
440 replacemap['$@'] = ' '.join(args)
441 replacemap['$@'] = ' '.join(args)
441 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
442 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
442 # parameters, separated out into words. Emulate the same behavior here by
443 # parameters, separated out into words. Emulate the same behavior here by
443 # quoting the arguments individually. POSIX shells will then typically
444 # quoting the arguments individually. POSIX shells will then typically
444 # tokenize each argument into exactly one word.
445 # tokenize each argument into exactly one word.
445 replacemap['"$@"'] = ' '.join(util.shellquote(arg) for arg in args)
446 replacemap['"$@"'] = ' '.join(util.shellquote(arg) for arg in args)
446 # escape '\$' for regex
447 # escape '\$' for regex
447 regex = '|'.join(replacemap.keys()).replace('$', br'\$')
448 regex = '|'.join(replacemap.keys()).replace('$', br'\$')
448 r = re.compile(regex)
449 r = re.compile(regex)
449 return r.sub(lambda x: replacemap[x.group()], cmd)
450 return r.sub(lambda x: replacemap[x.group()], cmd)
450
451
451 class cmdalias(object):
452 class cmdalias(object):
452 def __init__(self, name, definition, cmdtable, source):
453 def __init__(self, name, definition, cmdtable, source):
453 self.name = self.cmd = name
454 self.name = self.cmd = name
454 self.cmdname = ''
455 self.cmdname = ''
455 self.definition = definition
456 self.definition = definition
456 self.fn = None
457 self.fn = None
457 self.givenargs = []
458 self.givenargs = []
458 self.opts = []
459 self.opts = []
459 self.help = ''
460 self.help = ''
460 self.badalias = None
461 self.badalias = None
461 self.unknowncmd = False
462 self.unknowncmd = False
462 self.source = source
463 self.source = source
463
464
464 try:
465 try:
465 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
466 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
466 for alias, e in cmdtable.iteritems():
467 for alias, e in cmdtable.iteritems():
467 if e is entry:
468 if e is entry:
468 self.cmd = alias
469 self.cmd = alias
469 break
470 break
470 self.shadows = True
471 self.shadows = True
471 except error.UnknownCommand:
472 except error.UnknownCommand:
472 self.shadows = False
473 self.shadows = False
473
474
474 if not self.definition:
475 if not self.definition:
475 self.badalias = _("no definition for alias '%s'") % self.name
476 self.badalias = _("no definition for alias '%s'") % self.name
476 return
477 return
477
478
478 if self.definition.startswith('!'):
479 if self.definition.startswith('!'):
479 self.shell = True
480 self.shell = True
480 def fn(ui, *args):
481 def fn(ui, *args):
481 env = {'HG_ARGS': ' '.join((self.name,) + args)}
482 env = {'HG_ARGS': ' '.join((self.name,) + args)}
482 def _checkvar(m):
483 def _checkvar(m):
483 if m.groups()[0] == '$':
484 if m.groups()[0] == '$':
484 return m.group()
485 return m.group()
485 elif int(m.groups()[0]) <= len(args):
486 elif int(m.groups()[0]) <= len(args):
486 return m.group()
487 return m.group()
487 else:
488 else:
488 ui.debug("No argument found for substitution "
489 ui.debug("No argument found for substitution "
489 "of %i variable in alias '%s' definition.\n"
490 "of %i variable in alias '%s' definition.\n"
490 % (int(m.groups()[0]), self.name))
491 % (int(m.groups()[0]), self.name))
491 return ''
492 return ''
492 cmd = re.sub(br'\$(\d+|\$)', _checkvar, self.definition[1:])
493 cmd = re.sub(br'\$(\d+|\$)', _checkvar, self.definition[1:])
493 cmd = aliasinterpolate(self.name, args, cmd)
494 cmd = aliasinterpolate(self.name, args, cmd)
494 return ui.system(cmd, environ=env,
495 return ui.system(cmd, environ=env,
495 blockedtag='alias_%s' % self.name)
496 blockedtag='alias_%s' % self.name)
496 self.fn = fn
497 self.fn = fn
497 return
498 return
498
499
499 try:
500 try:
500 args = pycompat.shlexsplit(self.definition)
501 args = pycompat.shlexsplit(self.definition)
501 except ValueError as inst:
502 except ValueError as inst:
502 self.badalias = (_("error in definition for alias '%s': %s")
503 self.badalias = (_("error in definition for alias '%s': %s")
503 % (self.name, stringutil.forcebytestr(inst)))
504 % (self.name, stringutil.forcebytestr(inst)))
504 return
505 return
505 earlyopts, args = _earlysplitopts(args)
506 earlyopts, args = _earlysplitopts(args)
506 if earlyopts:
507 if earlyopts:
507 self.badalias = (_("error in definition for alias '%s': %s may "
508 self.badalias = (_("error in definition for alias '%s': %s may "
508 "only be given on the command line")
509 "only be given on the command line")
509 % (self.name, '/'.join(pycompat.ziplist(*earlyopts)
510 % (self.name, '/'.join(pycompat.ziplist(*earlyopts)
510 [0])))
511 [0])))
511 return
512 return
512 self.cmdname = cmd = args.pop(0)
513 self.cmdname = cmd = args.pop(0)
513 self.givenargs = args
514 self.givenargs = args
514
515
515 try:
516 try:
516 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
517 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
517 if len(tableentry) > 2:
518 if len(tableentry) > 2:
518 self.fn, self.opts, self.help = tableentry
519 self.fn, self.opts, self.help = tableentry
519 else:
520 else:
520 self.fn, self.opts = tableentry
521 self.fn, self.opts = tableentry
521
522
522 if self.help.startswith("hg " + cmd):
523 if self.help.startswith("hg " + cmd):
523 # drop prefix in old-style help lines so hg shows the alias
524 # drop prefix in old-style help lines so hg shows the alias
524 self.help = self.help[4 + len(cmd):]
525 self.help = self.help[4 + len(cmd):]
525 self.__doc__ = self.fn.__doc__
526 self.__doc__ = self.fn.__doc__
526
527
527 except error.UnknownCommand:
528 except error.UnknownCommand:
528 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
529 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
529 % (self.name, cmd))
530 % (self.name, cmd))
530 self.unknowncmd = True
531 self.unknowncmd = True
531 except error.AmbiguousCommand:
532 except error.AmbiguousCommand:
532 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
533 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
533 % (self.name, cmd))
534 % (self.name, cmd))
534
535
535 @property
536 @property
536 def args(self):
537 def args(self):
537 args = pycompat.maplist(util.expandpath, self.givenargs)
538 args = pycompat.maplist(util.expandpath, self.givenargs)
538 return aliasargs(self.fn, args)
539 return aliasargs(self.fn, args)
539
540
540 def __getattr__(self, name):
541 def __getattr__(self, name):
541 adefaults = {r'norepo': True, r'cmdtype': unrecoverablewrite,
542 adefaults = {r'norepo': True, r'cmdtype': unrecoverablewrite,
542 r'optionalrepo': False, r'inferrepo': False}
543 r'optionalrepo': False, r'inferrepo': False}
543 if name not in adefaults:
544 if name not in adefaults:
544 raise AttributeError(name)
545 raise AttributeError(name)
545 if self.badalias or util.safehasattr(self, 'shell'):
546 if self.badalias or util.safehasattr(self, 'shell'):
546 return adefaults[name]
547 return adefaults[name]
547 return getattr(self.fn, name)
548 return getattr(self.fn, name)
548
549
549 def __call__(self, ui, *args, **opts):
550 def __call__(self, ui, *args, **opts):
550 if self.badalias:
551 if self.badalias:
551 hint = None
552 hint = None
552 if self.unknowncmd:
553 if self.unknowncmd:
553 try:
554 try:
554 # check if the command is in a disabled extension
555 # check if the command is in a disabled extension
555 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
556 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
556 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
557 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
557 except error.UnknownCommand:
558 except error.UnknownCommand:
558 pass
559 pass
559 raise error.Abort(self.badalias, hint=hint)
560 raise error.Abort(self.badalias, hint=hint)
560 if self.shadows:
561 if self.shadows:
561 ui.debug("alias '%s' shadows command '%s'\n" %
562 ui.debug("alias '%s' shadows command '%s'\n" %
562 (self.name, self.cmdname))
563 (self.name, self.cmdname))
563
564
564 ui.log('commandalias', "alias '%s' expands to '%s'\n",
565 ui.log('commandalias', "alias '%s' expands to '%s'\n",
565 self.name, self.definition)
566 self.name, self.definition)
566 if util.safehasattr(self, 'shell'):
567 if util.safehasattr(self, 'shell'):
567 return self.fn(ui, *args, **opts)
568 return self.fn(ui, *args, **opts)
568 else:
569 else:
569 try:
570 try:
570 return util.checksignature(self.fn)(ui, *args, **opts)
571 return util.checksignature(self.fn)(ui, *args, **opts)
571 except error.SignatureError:
572 except error.SignatureError:
572 args = ' '.join([self.cmdname] + self.args)
573 args = ' '.join([self.cmdname] + self.args)
573 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
574 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
574 raise
575 raise
575
576
576 class lazyaliasentry(object):
577 class lazyaliasentry(object):
577 """like a typical command entry (func, opts, help), but is lazy"""
578 """like a typical command entry (func, opts, help), but is lazy"""
578
579
579 def __init__(self, name, definition, cmdtable, source):
580 def __init__(self, name, definition, cmdtable, source):
580 self.name = name
581 self.name = name
581 self.definition = definition
582 self.definition = definition
582 self.cmdtable = cmdtable.copy()
583 self.cmdtable = cmdtable.copy()
583 self.source = source
584 self.source = source
584
585
585 @util.propertycache
586 @util.propertycache
586 def _aliasdef(self):
587 def _aliasdef(self):
587 return cmdalias(self.name, self.definition, self.cmdtable, self.source)
588 return cmdalias(self.name, self.definition, self.cmdtable, self.source)
588
589
589 def __getitem__(self, n):
590 def __getitem__(self, n):
590 aliasdef = self._aliasdef
591 aliasdef = self._aliasdef
591 if n == 0:
592 if n == 0:
592 return aliasdef
593 return aliasdef
593 elif n == 1:
594 elif n == 1:
594 return aliasdef.opts
595 return aliasdef.opts
595 elif n == 2:
596 elif n == 2:
596 return aliasdef.help
597 return aliasdef.help
597 else:
598 else:
598 raise IndexError
599 raise IndexError
599
600
600 def __iter__(self):
601 def __iter__(self):
601 for i in range(3):
602 for i in range(3):
602 yield self[i]
603 yield self[i]
603
604
604 def __len__(self):
605 def __len__(self):
605 return 3
606 return 3
606
607
607 def addaliases(ui, cmdtable):
608 def addaliases(ui, cmdtable):
608 # aliases are processed after extensions have been loaded, so they
609 # aliases are processed after extensions have been loaded, so they
609 # may use extension commands. Aliases can also use other alias definitions,
610 # may use extension commands. Aliases can also use other alias definitions,
610 # but only if they have been defined prior to the current definition.
611 # but only if they have been defined prior to the current definition.
611 for alias, definition in ui.configitems('alias'):
612 for alias, definition in ui.configitems('alias'):
612 try:
613 try:
613 if cmdtable[alias].definition == definition:
614 if cmdtable[alias].definition == definition:
614 continue
615 continue
615 except (KeyError, AttributeError):
616 except (KeyError, AttributeError):
616 # definition might not exist or it might not be a cmdalias
617 # definition might not exist or it might not be a cmdalias
617 pass
618 pass
618
619
619 source = ui.configsource('alias', alias)
620 source = ui.configsource('alias', alias)
620 entry = lazyaliasentry(alias, definition, cmdtable, source)
621 entry = lazyaliasentry(alias, definition, cmdtable, source)
621 cmdtable[alias] = entry
622 cmdtable[alias] = entry
622
623
623 def _parse(ui, args):
624 def _parse(ui, args):
624 options = {}
625 options = {}
625 cmdoptions = {}
626 cmdoptions = {}
626
627
627 try:
628 try:
628 args = fancyopts.fancyopts(args, commands.globalopts, options)
629 args = fancyopts.fancyopts(args, commands.globalopts, options)
629 except getopt.GetoptError as inst:
630 except getopt.GetoptError as inst:
630 raise error.CommandError(None, stringutil.forcebytestr(inst))
631 raise error.CommandError(None, stringutil.forcebytestr(inst))
631
632
632 if args:
633 if args:
633 cmd, args = args[0], args[1:]
634 cmd, args = args[0], args[1:]
634 aliases, entry = cmdutil.findcmd(cmd, commands.table,
635 aliases, entry = cmdutil.findcmd(cmd, commands.table,
635 ui.configbool("ui", "strict"))
636 ui.configbool("ui", "strict"))
636 cmd = aliases[0]
637 cmd = aliases[0]
637 args = aliasargs(entry[0], args)
638 args = aliasargs(entry[0], args)
638 defaults = ui.config("defaults", cmd)
639 defaults = ui.config("defaults", cmd)
639 if defaults:
640 if defaults:
640 args = pycompat.maplist(
641 args = pycompat.maplist(
641 util.expandpath, pycompat.shlexsplit(defaults)) + args
642 util.expandpath, pycompat.shlexsplit(defaults)) + args
642 c = list(entry[1])
643 c = list(entry[1])
643 else:
644 else:
644 cmd = None
645 cmd = None
645 c = []
646 c = []
646
647
647 # combine global options into local
648 # combine global options into local
648 for o in commands.globalopts:
649 for o in commands.globalopts:
649 c.append((o[0], o[1], options[o[1]], o[3]))
650 c.append((o[0], o[1], options[o[1]], o[3]))
650
651
651 try:
652 try:
652 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
653 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
653 except getopt.GetoptError as inst:
654 except getopt.GetoptError as inst:
654 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
655 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
655
656
656 # separate global options back out
657 # separate global options back out
657 for o in commands.globalopts:
658 for o in commands.globalopts:
658 n = o[1]
659 n = o[1]
659 options[n] = cmdoptions[n]
660 options[n] = cmdoptions[n]
660 del cmdoptions[n]
661 del cmdoptions[n]
661
662
662 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
663 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
663
664
664 def _parseconfig(ui, config):
665 def _parseconfig(ui, config):
665 """parse the --config options from the command line"""
666 """parse the --config options from the command line"""
666 configs = []
667 configs = []
667
668
668 for cfg in config:
669 for cfg in config:
669 try:
670 try:
670 name, value = [cfgelem.strip()
671 name, value = [cfgelem.strip()
671 for cfgelem in cfg.split('=', 1)]
672 for cfgelem in cfg.split('=', 1)]
672 section, name = name.split('.', 1)
673 section, name = name.split('.', 1)
673 if not section or not name:
674 if not section or not name:
674 raise IndexError
675 raise IndexError
675 ui.setconfig(section, name, value, '--config')
676 ui.setconfig(section, name, value, '--config')
676 configs.append((section, name, value))
677 configs.append((section, name, value))
677 except (IndexError, ValueError):
678 except (IndexError, ValueError):
678 raise error.Abort(_('malformed --config option: %r '
679 raise error.Abort(_('malformed --config option: %r '
679 '(use --config section.name=value)')
680 '(use --config section.name=value)')
680 % pycompat.bytestr(cfg))
681 % pycompat.bytestr(cfg))
681
682
682 return configs
683 return configs
683
684
684 def _earlyparseopts(ui, args):
685 def _earlyparseopts(ui, args):
685 options = {}
686 options = {}
686 fancyopts.fancyopts(args, commands.globalopts, options,
687 fancyopts.fancyopts(args, commands.globalopts, options,
687 gnu=not ui.plain('strictflags'), early=True,
688 gnu=not ui.plain('strictflags'), early=True,
688 optaliases={'repository': ['repo']})
689 optaliases={'repository': ['repo']})
689 return options
690 return options
690
691
691 def _earlysplitopts(args):
692 def _earlysplitopts(args):
692 """Split args into a list of possible early options and remainder args"""
693 """Split args into a list of possible early options and remainder args"""
693 shortoptions = 'R:'
694 shortoptions = 'R:'
694 # TODO: perhaps 'debugger' should be included
695 # TODO: perhaps 'debugger' should be included
695 longoptions = ['cwd=', 'repository=', 'repo=', 'config=']
696 longoptions = ['cwd=', 'repository=', 'repo=', 'config=']
696 return fancyopts.earlygetopt(args, shortoptions, longoptions,
697 return fancyopts.earlygetopt(args, shortoptions, longoptions,
697 gnu=True, keepsep=True)
698 gnu=True, keepsep=True)
698
699
699 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
700 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
700 # run pre-hook, and abort if it fails
701 # run pre-hook, and abort if it fails
701 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
702 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
702 pats=cmdpats, opts=cmdoptions)
703 pats=cmdpats, opts=cmdoptions)
703 try:
704 try:
704 ret = _runcommand(ui, options, cmd, d)
705 ret = _runcommand(ui, options, cmd, d)
705 # run post-hook, passing command result
706 # run post-hook, passing command result
706 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
707 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
707 result=ret, pats=cmdpats, opts=cmdoptions)
708 result=ret, pats=cmdpats, opts=cmdoptions)
708 except Exception:
709 except Exception:
709 # run failure hook and re-raise
710 # run failure hook and re-raise
710 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
711 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
711 pats=cmdpats, opts=cmdoptions)
712 pats=cmdpats, opts=cmdoptions)
712 raise
713 raise
713 return ret
714 return ret
714
715
715 def _getlocal(ui, rpath, wd=None):
716 def _getlocal(ui, rpath, wd=None):
716 """Return (path, local ui object) for the given target path.
717 """Return (path, local ui object) for the given target path.
717
718
718 Takes paths in [cwd]/.hg/hgrc into account."
719 Takes paths in [cwd]/.hg/hgrc into account."
719 """
720 """
720 if wd is None:
721 if wd is None:
721 try:
722 try:
722 wd = pycompat.getcwd()
723 wd = pycompat.getcwd()
723 except OSError as e:
724 except OSError as e:
724 raise error.Abort(_("error getting current working directory: %s") %
725 raise error.Abort(_("error getting current working directory: %s") %
725 encoding.strtolocal(e.strerror))
726 encoding.strtolocal(e.strerror))
726 path = cmdutil.findrepo(wd) or ""
727 path = cmdutil.findrepo(wd) or ""
727 if not path:
728 if not path:
728 lui = ui
729 lui = ui
729 else:
730 else:
730 lui = ui.copy()
731 lui = ui.copy()
731 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
732 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
732
733
733 if rpath:
734 if rpath:
734 path = lui.expandpath(rpath)
735 path = lui.expandpath(rpath)
735 lui = ui.copy()
736 lui = ui.copy()
736 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
737 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
737
738
738 return path, lui
739 return path, lui
739
740
740 def _checkshellalias(lui, ui, args):
741 def _checkshellalias(lui, ui, args):
741 """Return the function to run the shell alias, if it is required"""
742 """Return the function to run the shell alias, if it is required"""
742 options = {}
743 options = {}
743
744
744 try:
745 try:
745 args = fancyopts.fancyopts(args, commands.globalopts, options)
746 args = fancyopts.fancyopts(args, commands.globalopts, options)
746 except getopt.GetoptError:
747 except getopt.GetoptError:
747 return
748 return
748
749
749 if not args:
750 if not args:
750 return
751 return
751
752
752 cmdtable = commands.table
753 cmdtable = commands.table
753
754
754 cmd = args[0]
755 cmd = args[0]
755 try:
756 try:
756 strict = ui.configbool("ui", "strict")
757 strict = ui.configbool("ui", "strict")
757 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
758 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
758 except (error.AmbiguousCommand, error.UnknownCommand):
759 except (error.AmbiguousCommand, error.UnknownCommand):
759 return
760 return
760
761
761 cmd = aliases[0]
762 cmd = aliases[0]
762 fn = entry[0]
763 fn = entry[0]
763
764
764 if cmd and util.safehasattr(fn, 'shell'):
765 if cmd and util.safehasattr(fn, 'shell'):
765 # shell alias shouldn't receive early options which are consumed by hg
766 # shell alias shouldn't receive early options which are consumed by hg
766 _earlyopts, args = _earlysplitopts(args)
767 _earlyopts, args = _earlysplitopts(args)
767 d = lambda: fn(ui, *args[1:])
768 d = lambda: fn(ui, *args[1:])
768 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
769 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
769 [], {})
770 [], {})
770
771
771 def _dispatch(req):
772 def _dispatch(req):
772 args = req.args
773 args = req.args
773 ui = req.ui
774 ui = req.ui
774
775
775 # check for cwd
776 # check for cwd
776 cwd = req.earlyoptions['cwd']
777 cwd = req.earlyoptions['cwd']
777 if cwd:
778 if cwd:
778 os.chdir(cwd)
779 os.chdir(cwd)
779
780
780 rpath = req.earlyoptions['repository']
781 rpath = req.earlyoptions['repository']
781 path, lui = _getlocal(ui, rpath)
782 path, lui = _getlocal(ui, rpath)
782
783
783 uis = {ui, lui}
784 uis = {ui, lui}
784
785
785 if req.repo:
786 if req.repo:
786 uis.add(req.repo.ui)
787 uis.add(req.repo.ui)
787
788
788 if req.earlyoptions['profile']:
789 if req.earlyoptions['profile']:
789 for ui_ in uis:
790 for ui_ in uis:
790 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
791 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
791
792
792 profile = lui.configbool('profiling', 'enabled')
793 profile = lui.configbool('profiling', 'enabled')
793 with profiling.profile(lui, enabled=profile) as profiler:
794 with profiling.profile(lui, enabled=profile) as profiler:
794 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
795 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
795 # reposetup
796 # reposetup
796 extensions.loadall(lui)
797 extensions.loadall(lui)
797 # Propagate any changes to lui.__class__ by extensions
798 # Propagate any changes to lui.__class__ by extensions
798 ui.__class__ = lui.__class__
799 ui.__class__ = lui.__class__
799
800
800 # (uisetup and extsetup are handled in extensions.loadall)
801 # (uisetup and extsetup are handled in extensions.loadall)
801
802
802 # (reposetup is handled in hg.repository)
803 # (reposetup is handled in hg.repository)
803
804
804 addaliases(lui, commands.table)
805 addaliases(lui, commands.table)
805
806
806 # All aliases and commands are completely defined, now.
807 # All aliases and commands are completely defined, now.
807 # Check abbreviation/ambiguity of shell alias.
808 # Check abbreviation/ambiguity of shell alias.
808 shellaliasfn = _checkshellalias(lui, ui, args)
809 shellaliasfn = _checkshellalias(lui, ui, args)
809 if shellaliasfn:
810 if shellaliasfn:
810 return shellaliasfn()
811 return shellaliasfn()
811
812
812 # check for fallback encoding
813 # check for fallback encoding
813 fallback = lui.config('ui', 'fallbackencoding')
814 fallback = lui.config('ui', 'fallbackencoding')
814 if fallback:
815 if fallback:
815 encoding.fallbackencoding = fallback
816 encoding.fallbackencoding = fallback
816
817
817 fullargs = args
818 fullargs = args
818 cmd, func, args, options, cmdoptions = _parse(lui, args)
819 cmd, func, args, options, cmdoptions = _parse(lui, args)
819
820
820 if options["config"] != req.earlyoptions["config"]:
821 if options["config"] != req.earlyoptions["config"]:
821 raise error.Abort(_("option --config may not be abbreviated!"))
822 raise error.Abort(_("option --config may not be abbreviated!"))
822 if options["cwd"] != req.earlyoptions["cwd"]:
823 if options["cwd"] != req.earlyoptions["cwd"]:
823 raise error.Abort(_("option --cwd may not be abbreviated!"))
824 raise error.Abort(_("option --cwd may not be abbreviated!"))
824 if options["repository"] != req.earlyoptions["repository"]:
825 if options["repository"] != req.earlyoptions["repository"]:
825 raise error.Abort(_(
826 raise error.Abort(_(
826 "option -R has to be separated from other options (e.g. not "
827 "option -R has to be separated from other options (e.g. not "
827 "-qR) and --repository may only be abbreviated as --repo!"))
828 "-qR) and --repository may only be abbreviated as --repo!"))
828 if options["debugger"] != req.earlyoptions["debugger"]:
829 if options["debugger"] != req.earlyoptions["debugger"]:
829 raise error.Abort(_("option --debugger may not be abbreviated!"))
830 raise error.Abort(_("option --debugger may not be abbreviated!"))
830 # don't validate --profile/--traceback, which can be enabled from now
831 # don't validate --profile/--traceback, which can be enabled from now
831
832
832 if options["encoding"]:
833 if options["encoding"]:
833 encoding.encoding = options["encoding"]
834 encoding.encoding = options["encoding"]
834 if options["encodingmode"]:
835 if options["encodingmode"]:
835 encoding.encodingmode = options["encodingmode"]
836 encoding.encodingmode = options["encodingmode"]
836 if options["time"]:
837 if options["time"]:
837 def get_times():
838 def get_times():
838 t = os.times()
839 t = os.times()
839 if t[4] == 0.0:
840 if t[4] == 0.0:
840 # Windows leaves this as zero, so use time.clock()
841 # Windows leaves this as zero, so use time.clock()
841 t = (t[0], t[1], t[2], t[3], time.clock())
842 t = (t[0], t[1], t[2], t[3], time.clock())
842 return t
843 return t
843 s = get_times()
844 s = get_times()
844 def print_time():
845 def print_time():
845 t = get_times()
846 t = get_times()
846 ui.warn(
847 ui.warn(
847 _("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
848 _("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
848 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
849 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
849 ui.atexit(print_time)
850 ui.atexit(print_time)
850 if options["profile"]:
851 if options["profile"]:
851 profiler.start()
852 profiler.start()
852
853
853 if options['verbose'] or options['debug'] or options['quiet']:
854 if options['verbose'] or options['debug'] or options['quiet']:
854 for opt in ('verbose', 'debug', 'quiet'):
855 for opt in ('verbose', 'debug', 'quiet'):
855 val = pycompat.bytestr(bool(options[opt]))
856 val = pycompat.bytestr(bool(options[opt]))
856 for ui_ in uis:
857 for ui_ in uis:
857 ui_.setconfig('ui', opt, val, '--' + opt)
858 ui_.setconfig('ui', opt, val, '--' + opt)
858
859
859 if options['traceback']:
860 if options['traceback']:
860 for ui_ in uis:
861 for ui_ in uis:
861 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
862 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
862
863
863 if options['noninteractive']:
864 if options['noninteractive']:
864 for ui_ in uis:
865 for ui_ in uis:
865 ui_.setconfig('ui', 'interactive', 'off', '-y')
866 ui_.setconfig('ui', 'interactive', 'off', '-y')
866
867
867 if cmdoptions.get('insecure', False):
868 if cmdoptions.get('insecure', False):
868 for ui_ in uis:
869 for ui_ in uis:
869 ui_.insecureconnections = True
870 ui_.insecureconnections = True
870
871
871 # setup color handling before pager, because setting up pager
872 # setup color handling before pager, because setting up pager
872 # might cause incorrect console information
873 # might cause incorrect console information
873 coloropt = options['color']
874 coloropt = options['color']
874 for ui_ in uis:
875 for ui_ in uis:
875 if coloropt:
876 if coloropt:
876 ui_.setconfig('ui', 'color', coloropt, '--color')
877 ui_.setconfig('ui', 'color', coloropt, '--color')
877 color.setup(ui_)
878 color.setup(ui_)
878
879
879 if stringutil.parsebool(options['pager']):
880 if stringutil.parsebool(options['pager']):
880 # ui.pager() expects 'internal-always-' prefix in this case
881 # ui.pager() expects 'internal-always-' prefix in this case
881 ui.pager('internal-always-' + cmd)
882 ui.pager('internal-always-' + cmd)
882 elif options['pager'] != 'auto':
883 elif options['pager'] != 'auto':
883 for ui_ in uis:
884 for ui_ in uis:
884 ui_.disablepager()
885 ui_.disablepager()
885
886
886 if options['version']:
887 if options['version']:
887 return commands.version_(ui)
888 return commands.version_(ui)
888 if options['help']:
889 if options['help']:
889 return commands.help_(ui, cmd, command=cmd is not None)
890 return commands.help_(ui, cmd, command=cmd is not None)
890 elif not cmd:
891 elif not cmd:
891 return commands.help_(ui, 'shortlist')
892 return commands.help_(ui, 'shortlist')
892
893
893 repo = None
894 repo = None
894 cmdpats = args[:]
895 cmdpats = args[:]
895 if not func.norepo:
896 if not func.norepo:
896 # use the repo from the request only if we don't have -R
897 # use the repo from the request only if we don't have -R
897 if not rpath and not cwd:
898 if not rpath and not cwd:
898 repo = req.repo
899 repo = req.repo
899
900
900 if repo:
901 if repo:
901 # set the descriptors of the repo ui to those of ui
902 # set the descriptors of the repo ui to those of ui
902 repo.ui.fin = ui.fin
903 repo.ui.fin = ui.fin
903 repo.ui.fout = ui.fout
904 repo.ui.fout = ui.fout
904 repo.ui.ferr = ui.ferr
905 repo.ui.ferr = ui.ferr
905 else:
906 else:
906 try:
907 try:
907 repo = hg.repository(ui, path=path,
908 repo = hg.repository(ui, path=path,
908 presetupfuncs=req.prereposetups)
909 presetupfuncs=req.prereposetups)
909 if not repo.local():
910 if not repo.local():
910 raise error.Abort(_("repository '%s' is not local")
911 raise error.Abort(_("repository '%s' is not local")
911 % path)
912 % path)
912 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
913 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
913 'repo')
914 'repo')
914 except error.RequirementError:
915 except error.RequirementError:
915 raise
916 raise
916 except error.RepoError:
917 except error.RepoError:
917 if rpath: # invalid -R path
918 if rpath: # invalid -R path
918 raise
919 raise
919 if not func.optionalrepo:
920 if not func.optionalrepo:
920 if func.inferrepo and args and not path:
921 if func.inferrepo and args and not path:
921 # try to infer -R from command args
922 # try to infer -R from command args
922 repos = pycompat.maplist(cmdutil.findrepo, args)
923 repos = pycompat.maplist(cmdutil.findrepo, args)
923 guess = repos[0]
924 guess = repos[0]
924 if guess and repos.count(guess) == len(repos):
925 if guess and repos.count(guess) == len(repos):
925 req.args = ['--repository', guess] + fullargs
926 req.args = ['--repository', guess] + fullargs
926 req.earlyoptions['repository'] = guess
927 req.earlyoptions['repository'] = guess
927 return _dispatch(req)
928 return _dispatch(req)
928 if not path:
929 if not path:
929 raise error.RepoError(_("no repository found in"
930 raise error.RepoError(_("no repository found in"
930 " '%s' (.hg not found)")
931 " '%s' (.hg not found)")
931 % pycompat.getcwd())
932 % pycompat.getcwd())
932 raise
933 raise
933 if repo:
934 if repo:
934 ui = repo.ui
935 ui = repo.ui
935 if options['hidden']:
936 if options['hidden']:
936 repo = repo.unfiltered()
937 repo = repo.unfiltered()
937 args.insert(0, repo)
938 args.insert(0, repo)
938 elif rpath:
939 elif rpath:
939 ui.warn(_("warning: --repository ignored\n"))
940 ui.warn(_("warning: --repository ignored\n"))
940
941
941 msg = _formatargs(fullargs)
942 msg = _formatargs(fullargs)
942 ui.log("command", '%s\n', msg)
943 ui.log("command", '%s\n', msg)
943 strcmdopt = pycompat.strkwargs(cmdoptions)
944 strcmdopt = pycompat.strkwargs(cmdoptions)
944 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
945 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
945 try:
946 try:
946 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
947 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
947 cmdpats, cmdoptions)
948 cmdpats, cmdoptions)
948 finally:
949 finally:
949 if repo and repo != req.repo:
950 if repo and repo != req.repo:
950 repo.close()
951 repo.close()
951
952
952 def _runcommand(ui, options, cmd, cmdfunc):
953 def _runcommand(ui, options, cmd, cmdfunc):
953 """Run a command function, possibly with profiling enabled."""
954 """Run a command function, possibly with profiling enabled."""
954 try:
955 try:
955 return cmdfunc()
956 return cmdfunc()
956 except error.SignatureError:
957 except error.SignatureError:
957 raise error.CommandError(cmd, _('invalid arguments'))
958 raise error.CommandError(cmd, _('invalid arguments'))
958
959
959 def _exceptionwarning(ui):
960 def _exceptionwarning(ui):
960 """Produce a warning message for the current active exception"""
961 """Produce a warning message for the current active exception"""
961
962
962 # For compatibility checking, we discard the portion of the hg
963 # For compatibility checking, we discard the portion of the hg
963 # version after the + on the assumption that if a "normal
964 # version after the + on the assumption that if a "normal
964 # user" is running a build with a + in it the packager
965 # user" is running a build with a + in it the packager
965 # probably built from fairly close to a tag and anyone with a
966 # probably built from fairly close to a tag and anyone with a
966 # 'make local' copy of hg (where the version number can be out
967 # 'make local' copy of hg (where the version number can be out
967 # of date) will be clueful enough to notice the implausible
968 # of date) will be clueful enough to notice the implausible
968 # version number and try updating.
969 # version number and try updating.
969 ct = util.versiontuple(n=2)
970 ct = util.versiontuple(n=2)
970 worst = None, ct, ''
971 worst = None, ct, ''
971 if ui.config('ui', 'supportcontact') is None:
972 if ui.config('ui', 'supportcontact') is None:
972 for name, mod in extensions.extensions():
973 for name, mod in extensions.extensions():
973 # 'testedwith' should be bytes, but not all extensions are ported
974 # 'testedwith' should be bytes, but not all extensions are ported
974 # to py3 and we don't want UnicodeException because of that.
975 # to py3 and we don't want UnicodeException because of that.
975 testedwith = stringutil.forcebytestr(getattr(mod, 'testedwith', ''))
976 testedwith = stringutil.forcebytestr(getattr(mod, 'testedwith', ''))
976 report = getattr(mod, 'buglink', _('the extension author.'))
977 report = getattr(mod, 'buglink', _('the extension author.'))
977 if not testedwith.strip():
978 if not testedwith.strip():
978 # We found an untested extension. It's likely the culprit.
979 # We found an untested extension. It's likely the culprit.
979 worst = name, 'unknown', report
980 worst = name, 'unknown', report
980 break
981 break
981
982
982 # Never blame on extensions bundled with Mercurial.
983 # Never blame on extensions bundled with Mercurial.
983 if extensions.ismoduleinternal(mod):
984 if extensions.ismoduleinternal(mod):
984 continue
985 continue
985
986
986 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
987 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
987 if ct in tested:
988 if ct in tested:
988 continue
989 continue
989
990
990 lower = [t for t in tested if t < ct]
991 lower = [t for t in tested if t < ct]
991 nearest = max(lower or tested)
992 nearest = max(lower or tested)
992 if worst[0] is None or nearest < worst[1]:
993 if worst[0] is None or nearest < worst[1]:
993 worst = name, nearest, report
994 worst = name, nearest, report
994 if worst[0] is not None:
995 if worst[0] is not None:
995 name, testedwith, report = worst
996 name, testedwith, report = worst
996 if not isinstance(testedwith, (bytes, str)):
997 if not isinstance(testedwith, (bytes, str)):
997 testedwith = '.'.join([stringutil.forcebytestr(c)
998 testedwith = '.'.join([stringutil.forcebytestr(c)
998 for c in testedwith])
999 for c in testedwith])
999 warning = (_('** Unknown exception encountered with '
1000 warning = (_('** Unknown exception encountered with '
1000 'possibly-broken third-party extension %s\n'
1001 'possibly-broken third-party extension %s\n'
1001 '** which supports versions %s of Mercurial.\n'
1002 '** which supports versions %s of Mercurial.\n'
1002 '** Please disable %s and try your action again.\n'
1003 '** Please disable %s and try your action again.\n'
1003 '** If that fixes the bug please report it to %s\n')
1004 '** If that fixes the bug please report it to %s\n')
1004 % (name, testedwith, name, report))
1005 % (name, testedwith, name, report))
1005 else:
1006 else:
1006 bugtracker = ui.config('ui', 'supportcontact')
1007 bugtracker = ui.config('ui', 'supportcontact')
1007 if bugtracker is None:
1008 if bugtracker is None:
1008 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
1009 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
1009 warning = (_("** unknown exception encountered, "
1010 warning = (_("** unknown exception encountered, "
1010 "please report by visiting\n** ") + bugtracker + '\n')
1011 "please report by visiting\n** ") + bugtracker + '\n')
1011 sysversion = pycompat.sysbytes(sys.version).replace('\n', '')
1012 sysversion = pycompat.sysbytes(sys.version).replace('\n', '')
1012 warning += ((_("** Python %s\n") % sysversion) +
1013 warning += ((_("** Python %s\n") % sysversion) +
1013 (_("** Mercurial Distributed SCM (version %s)\n") %
1014 (_("** Mercurial Distributed SCM (version %s)\n") %
1014 util.version()) +
1015 util.version()) +
1015 (_("** Extensions loaded: %s\n") %
1016 (_("** Extensions loaded: %s\n") %
1016 ", ".join([x[0] for x in extensions.extensions()])))
1017 ", ".join([x[0] for x in extensions.extensions()])))
1017 return warning
1018 return warning
1018
1019
1019 def handlecommandexception(ui):
1020 def handlecommandexception(ui):
1020 """Produce a warning message for broken commands
1021 """Produce a warning message for broken commands
1021
1022
1022 Called when handling an exception; the exception is reraised if
1023 Called when handling an exception; the exception is reraised if
1023 this function returns False, ignored otherwise.
1024 this function returns False, ignored otherwise.
1024 """
1025 """
1025 warning = _exceptionwarning(ui)
1026 warning = _exceptionwarning(ui)
1026 ui.log("commandexception", "%s\n%s\n", warning,
1027 ui.log("commandexception", "%s\n%s\n", warning,
1027 pycompat.sysbytes(traceback.format_exc()))
1028 pycompat.sysbytes(traceback.format_exc()))
1028 ui.warn(warning)
1029 ui.warn(warning)
1029 return False # re-raise the exception
1030 return False # re-raise the exception
@@ -1,90 +1,94 b''
1 # hgweb/wsgicgi.py - CGI->WSGI translator
1 # hgweb/wsgicgi.py - CGI->WSGI translator
2 #
2 #
3 # Copyright 2006 Eric Hopper <hopper@omnifarious.org>
3 # Copyright 2006 Eric Hopper <hopper@omnifarious.org>
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 # This was originally copied from the public domain code at
8 # This was originally copied from the public domain code at
9 # http://www.python.org/dev/peps/pep-0333/#the-server-gateway-side
9 # http://www.python.org/dev/peps/pep-0333/#the-server-gateway-side
10
10
11 from __future__ import absolute_import
11 from __future__ import absolute_import
12
12
13 from .. import (
13 from .. import (
14 encoding,
14 encoding,
15 util,
15 util,
16 )
16 )
17
17
18 from ..utils import (
19 procutil,
20 )
21
18 from . import (
22 from . import (
19 common,
23 common,
20 )
24 )
21
25
22 def launch(application):
26 def launch(application):
23 util.setbinary(util.stdin)
27 util.setbinary(procutil.stdin)
24 util.setbinary(util.stdout)
28 util.setbinary(procutil.stdout)
25
29
26 environ = dict(encoding.environ.iteritems())
30 environ = dict(encoding.environ.iteritems())
27 environ.setdefault(r'PATH_INFO', '')
31 environ.setdefault(r'PATH_INFO', '')
28 if environ.get(r'SERVER_SOFTWARE', r'').startswith(r'Microsoft-IIS'):
32 if environ.get(r'SERVER_SOFTWARE', r'').startswith(r'Microsoft-IIS'):
29 # IIS includes script_name in PATH_INFO
33 # IIS includes script_name in PATH_INFO
30 scriptname = environ[r'SCRIPT_NAME']
34 scriptname = environ[r'SCRIPT_NAME']
31 if environ[r'PATH_INFO'].startswith(scriptname):
35 if environ[r'PATH_INFO'].startswith(scriptname):
32 environ[r'PATH_INFO'] = environ[r'PATH_INFO'][len(scriptname):]
36 environ[r'PATH_INFO'] = environ[r'PATH_INFO'][len(scriptname):]
33
37
34 stdin = util.stdin
38 stdin = procutil.stdin
35 if environ.get(r'HTTP_EXPECT', r'').lower() == r'100-continue':
39 if environ.get(r'HTTP_EXPECT', r'').lower() == r'100-continue':
36 stdin = common.continuereader(stdin, util.stdout.write)
40 stdin = common.continuereader(stdin, procutil.stdout.write)
37
41
38 environ[r'wsgi.input'] = stdin
42 environ[r'wsgi.input'] = stdin
39 environ[r'wsgi.errors'] = util.stderr
43 environ[r'wsgi.errors'] = procutil.stderr
40 environ[r'wsgi.version'] = (1, 0)
44 environ[r'wsgi.version'] = (1, 0)
41 environ[r'wsgi.multithread'] = False
45 environ[r'wsgi.multithread'] = False
42 environ[r'wsgi.multiprocess'] = True
46 environ[r'wsgi.multiprocess'] = True
43 environ[r'wsgi.run_once'] = True
47 environ[r'wsgi.run_once'] = True
44
48
45 if environ.get(r'HTTPS', r'off').lower() in (r'on', r'1', r'yes'):
49 if environ.get(r'HTTPS', r'off').lower() in (r'on', r'1', r'yes'):
46 environ[r'wsgi.url_scheme'] = r'https'
50 environ[r'wsgi.url_scheme'] = r'https'
47 else:
51 else:
48 environ[r'wsgi.url_scheme'] = r'http'
52 environ[r'wsgi.url_scheme'] = r'http'
49
53
50 headers_set = []
54 headers_set = []
51 headers_sent = []
55 headers_sent = []
52 out = util.stdout
56 out = procutil.stdout
53
57
54 def write(data):
58 def write(data):
55 if not headers_set:
59 if not headers_set:
56 raise AssertionError("write() before start_response()")
60 raise AssertionError("write() before start_response()")
57
61
58 elif not headers_sent:
62 elif not headers_sent:
59 # Before the first output, send the stored headers
63 # Before the first output, send the stored headers
60 status, response_headers = headers_sent[:] = headers_set
64 status, response_headers = headers_sent[:] = headers_set
61 out.write('Status: %s\r\n' % status)
65 out.write('Status: %s\r\n' % status)
62 for header in response_headers:
66 for header in response_headers:
63 out.write('%s: %s\r\n' % header)
67 out.write('%s: %s\r\n' % header)
64 out.write('\r\n')
68 out.write('\r\n')
65
69
66 out.write(data)
70 out.write(data)
67 out.flush()
71 out.flush()
68
72
69 def start_response(status, response_headers, exc_info=None):
73 def start_response(status, response_headers, exc_info=None):
70 if exc_info:
74 if exc_info:
71 try:
75 try:
72 if headers_sent:
76 if headers_sent:
73 # Re-raise original exception if headers sent
77 # Re-raise original exception if headers sent
74 raise exc_info[0](exc_info[1], exc_info[2])
78 raise exc_info[0](exc_info[1], exc_info[2])
75 finally:
79 finally:
76 exc_info = None # avoid dangling circular ref
80 exc_info = None # avoid dangling circular ref
77 elif headers_set:
81 elif headers_set:
78 raise AssertionError("Headers already set!")
82 raise AssertionError("Headers already set!")
79
83
80 headers_set[:] = [status, response_headers]
84 headers_set[:] = [status, response_headers]
81 return write
85 return write
82
86
83 content = application(environ, start_response)
87 content = application(environ, start_response)
84 try:
88 try:
85 for chunk in content:
89 for chunk in content:
86 write(chunk)
90 write(chunk)
87 if not headers_sent:
91 if not headers_sent:
88 write('') # send headers now if body was empty
92 write('') # send headers now if body was empty
89 finally:
93 finally:
90 getattr(content, 'close', lambda: None)()
94 getattr(content, 'close', lambda: None)()
@@ -1,279 +1,282 b''
1 # hook.py - hook support for mercurial
1 # hook.py - hook support for mercurial
2 #
2 #
3 # Copyright 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 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
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import sys
11 import sys
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 demandimport,
15 demandimport,
16 encoding,
16 encoding,
17 error,
17 error,
18 extensions,
18 extensions,
19 pycompat,
19 pycompat,
20 util,
20 util,
21 )
21 )
22 from .utils import (
23 procutil,
24 )
22
25
23 def _pythonhook(ui, repo, htype, hname, funcname, args, throw):
26 def _pythonhook(ui, repo, htype, hname, funcname, args, throw):
24 '''call python hook. hook is callable object, looked up as
27 '''call python hook. hook is callable object, looked up as
25 name in python module. if callable returns "true", hook
28 name in python module. if callable returns "true", hook
26 fails, else passes. if hook raises exception, treated as
29 fails, else passes. if hook raises exception, treated as
27 hook failure. exception propagates if throw is "true".
30 hook failure. exception propagates if throw is "true".
28
31
29 reason for "true" meaning "hook failed" is so that
32 reason for "true" meaning "hook failed" is so that
30 unmodified commands (e.g. mercurial.commands.update) can
33 unmodified commands (e.g. mercurial.commands.update) can
31 be run as hooks without wrappers to convert return values.'''
34 be run as hooks without wrappers to convert return values.'''
32
35
33 if callable(funcname):
36 if callable(funcname):
34 obj = funcname
37 obj = funcname
35 funcname = pycompat.sysbytes(obj.__module__ + r"." + obj.__name__)
38 funcname = pycompat.sysbytes(obj.__module__ + r"." + obj.__name__)
36 else:
39 else:
37 d = funcname.rfind('.')
40 d = funcname.rfind('.')
38 if d == -1:
41 if d == -1:
39 raise error.HookLoadError(
42 raise error.HookLoadError(
40 _('%s hook is invalid: "%s" not in a module')
43 _('%s hook is invalid: "%s" not in a module')
41 % (hname, funcname))
44 % (hname, funcname))
42 modname = funcname[:d]
45 modname = funcname[:d]
43 oldpaths = sys.path
46 oldpaths = sys.path
44 if util.mainfrozen():
47 if util.mainfrozen():
45 # binary installs require sys.path manipulation
48 # binary installs require sys.path manipulation
46 modpath, modfile = os.path.split(modname)
49 modpath, modfile = os.path.split(modname)
47 if modpath and modfile:
50 if modpath and modfile:
48 sys.path = sys.path[:] + [modpath]
51 sys.path = sys.path[:] + [modpath]
49 modname = modfile
52 modname = modfile
50 with demandimport.deactivated():
53 with demandimport.deactivated():
51 try:
54 try:
52 obj = __import__(pycompat.sysstr(modname))
55 obj = __import__(pycompat.sysstr(modname))
53 except (ImportError, SyntaxError):
56 except (ImportError, SyntaxError):
54 e1 = sys.exc_info()
57 e1 = sys.exc_info()
55 try:
58 try:
56 # extensions are loaded with hgext_ prefix
59 # extensions are loaded with hgext_ prefix
57 obj = __import__(r"hgext_%s" % pycompat.sysstr(modname))
60 obj = __import__(r"hgext_%s" % pycompat.sysstr(modname))
58 except (ImportError, SyntaxError):
61 except (ImportError, SyntaxError):
59 e2 = sys.exc_info()
62 e2 = sys.exc_info()
60 if ui.tracebackflag:
63 if ui.tracebackflag:
61 ui.warn(_('exception from first failed import '
64 ui.warn(_('exception from first failed import '
62 'attempt:\n'))
65 'attempt:\n'))
63 ui.traceback(e1)
66 ui.traceback(e1)
64 if ui.tracebackflag:
67 if ui.tracebackflag:
65 ui.warn(_('exception from second failed import '
68 ui.warn(_('exception from second failed import '
66 'attempt:\n'))
69 'attempt:\n'))
67 ui.traceback(e2)
70 ui.traceback(e2)
68
71
69 if not ui.tracebackflag:
72 if not ui.tracebackflag:
70 tracebackhint = _(
73 tracebackhint = _(
71 'run with --traceback for stack trace')
74 'run with --traceback for stack trace')
72 else:
75 else:
73 tracebackhint = None
76 tracebackhint = None
74 raise error.HookLoadError(
77 raise error.HookLoadError(
75 _('%s hook is invalid: import of "%s" failed') %
78 _('%s hook is invalid: import of "%s" failed') %
76 (hname, modname), hint=tracebackhint)
79 (hname, modname), hint=tracebackhint)
77 sys.path = oldpaths
80 sys.path = oldpaths
78 try:
81 try:
79 for p in funcname.split('.')[1:]:
82 for p in funcname.split('.')[1:]:
80 obj = getattr(obj, p)
83 obj = getattr(obj, p)
81 except AttributeError:
84 except AttributeError:
82 raise error.HookLoadError(
85 raise error.HookLoadError(
83 _('%s hook is invalid: "%s" is not defined')
86 _('%s hook is invalid: "%s" is not defined')
84 % (hname, funcname))
87 % (hname, funcname))
85 if not callable(obj):
88 if not callable(obj):
86 raise error.HookLoadError(
89 raise error.HookLoadError(
87 _('%s hook is invalid: "%s" is not callable')
90 _('%s hook is invalid: "%s" is not callable')
88 % (hname, funcname))
91 % (hname, funcname))
89
92
90 ui.note(_("calling hook %s: %s\n") % (hname, funcname))
93 ui.note(_("calling hook %s: %s\n") % (hname, funcname))
91 starttime = util.timer()
94 starttime = util.timer()
92
95
93 try:
96 try:
94 r = obj(ui=ui, repo=repo, hooktype=htype, **pycompat.strkwargs(args))
97 r = obj(ui=ui, repo=repo, hooktype=htype, **pycompat.strkwargs(args))
95 except Exception as exc:
98 except Exception as exc:
96 if isinstance(exc, error.Abort):
99 if isinstance(exc, error.Abort):
97 ui.warn(_('error: %s hook failed: %s\n') %
100 ui.warn(_('error: %s hook failed: %s\n') %
98 (hname, exc.args[0]))
101 (hname, exc.args[0]))
99 else:
102 else:
100 ui.warn(_('error: %s hook raised an exception: '
103 ui.warn(_('error: %s hook raised an exception: '
101 '%s\n') % (hname, encoding.strtolocal(str(exc))))
104 '%s\n') % (hname, encoding.strtolocal(str(exc))))
102 if throw:
105 if throw:
103 raise
106 raise
104 if not ui.tracebackflag:
107 if not ui.tracebackflag:
105 ui.warn(_('(run with --traceback for stack trace)\n'))
108 ui.warn(_('(run with --traceback for stack trace)\n'))
106 ui.traceback()
109 ui.traceback()
107 return True, True
110 return True, True
108 finally:
111 finally:
109 duration = util.timer() - starttime
112 duration = util.timer() - starttime
110 ui.log('pythonhook', 'pythonhook-%s: %s finished in %0.2f seconds\n',
113 ui.log('pythonhook', 'pythonhook-%s: %s finished in %0.2f seconds\n',
111 htype, funcname, duration)
114 htype, funcname, duration)
112 if r:
115 if r:
113 if throw:
116 if throw:
114 raise error.HookAbort(_('%s hook failed') % hname)
117 raise error.HookAbort(_('%s hook failed') % hname)
115 ui.warn(_('warning: %s hook failed\n') % hname)
118 ui.warn(_('warning: %s hook failed\n') % hname)
116 return r, False
119 return r, False
117
120
118 def _exthook(ui, repo, htype, name, cmd, args, throw):
121 def _exthook(ui, repo, htype, name, cmd, args, throw):
119 ui.note(_("running hook %s: %s\n") % (name, cmd))
122 ui.note(_("running hook %s: %s\n") % (name, cmd))
120
123
121 starttime = util.timer()
124 starttime = util.timer()
122 env = {}
125 env = {}
123
126
124 # make in-memory changes visible to external process
127 # make in-memory changes visible to external process
125 if repo is not None:
128 if repo is not None:
126 tr = repo.currenttransaction()
129 tr = repo.currenttransaction()
127 repo.dirstate.write(tr)
130 repo.dirstate.write(tr)
128 if tr and tr.writepending():
131 if tr and tr.writepending():
129 env['HG_PENDING'] = repo.root
132 env['HG_PENDING'] = repo.root
130 env['HG_HOOKTYPE'] = htype
133 env['HG_HOOKTYPE'] = htype
131 env['HG_HOOKNAME'] = name
134 env['HG_HOOKNAME'] = name
132
135
133 for k, v in args.iteritems():
136 for k, v in args.iteritems():
134 if callable(v):
137 if callable(v):
135 v = v()
138 v = v()
136 if isinstance(v, dict):
139 if isinstance(v, dict):
137 # make the dictionary element order stable across Python
140 # make the dictionary element order stable across Python
138 # implementations
141 # implementations
139 v = ('{' +
142 v = ('{' +
140 ', '.join('%r: %r' % i for i in sorted(v.iteritems())) +
143 ', '.join('%r: %r' % i for i in sorted(v.iteritems())) +
141 '}')
144 '}')
142 env['HG_' + k.upper()] = v
145 env['HG_' + k.upper()] = v
143
146
144 if repo:
147 if repo:
145 cwd = repo.root
148 cwd = repo.root
146 else:
149 else:
147 cwd = pycompat.getcwd()
150 cwd = pycompat.getcwd()
148 r = ui.system(cmd, environ=env, cwd=cwd, blockedtag='exthook-%s' % (name,))
151 r = ui.system(cmd, environ=env, cwd=cwd, blockedtag='exthook-%s' % (name,))
149
152
150 duration = util.timer() - starttime
153 duration = util.timer() - starttime
151 ui.log('exthook', 'exthook-%s: %s finished in %0.2f seconds\n',
154 ui.log('exthook', 'exthook-%s: %s finished in %0.2f seconds\n',
152 name, cmd, duration)
155 name, cmd, duration)
153 if r:
156 if r:
154 desc, r = util.explainexit(r)
157 desc, r = util.explainexit(r)
155 if throw:
158 if throw:
156 raise error.HookAbort(_('%s hook %s') % (name, desc))
159 raise error.HookAbort(_('%s hook %s') % (name, desc))
157 ui.warn(_('warning: %s hook %s\n') % (name, desc))
160 ui.warn(_('warning: %s hook %s\n') % (name, desc))
158 return r
161 return r
159
162
160 # represent an untrusted hook command
163 # represent an untrusted hook command
161 _fromuntrusted = object()
164 _fromuntrusted = object()
162
165
163 def _allhooks(ui):
166 def _allhooks(ui):
164 """return a list of (hook-id, cmd) pairs sorted by priority"""
167 """return a list of (hook-id, cmd) pairs sorted by priority"""
165 hooks = _hookitems(ui)
168 hooks = _hookitems(ui)
166 # Be careful in this section, propagating the real commands from untrusted
169 # Be careful in this section, propagating the real commands from untrusted
167 # sources would create a security vulnerability, make sure anything altered
170 # sources would create a security vulnerability, make sure anything altered
168 # in that section uses "_fromuntrusted" as its command.
171 # in that section uses "_fromuntrusted" as its command.
169 untrustedhooks = _hookitems(ui, _untrusted=True)
172 untrustedhooks = _hookitems(ui, _untrusted=True)
170 for name, value in untrustedhooks.items():
173 for name, value in untrustedhooks.items():
171 trustedvalue = hooks.get(name, (None, None, name, _fromuntrusted))
174 trustedvalue = hooks.get(name, (None, None, name, _fromuntrusted))
172 if value != trustedvalue:
175 if value != trustedvalue:
173 (lp, lo, lk, lv) = trustedvalue
176 (lp, lo, lk, lv) = trustedvalue
174 hooks[name] = (lp, lo, lk, _fromuntrusted)
177 hooks[name] = (lp, lo, lk, _fromuntrusted)
175 # (end of the security sensitive section)
178 # (end of the security sensitive section)
176 return [(k, v) for p, o, k, v in sorted(hooks.values())]
179 return [(k, v) for p, o, k, v in sorted(hooks.values())]
177
180
178 def _hookitems(ui, _untrusted=False):
181 def _hookitems(ui, _untrusted=False):
179 """return all hooks items ready to be sorted"""
182 """return all hooks items ready to be sorted"""
180 hooks = {}
183 hooks = {}
181 for name, cmd in ui.configitems('hooks', untrusted=_untrusted):
184 for name, cmd in ui.configitems('hooks', untrusted=_untrusted):
182 if not name.startswith('priority'):
185 if not name.startswith('priority'):
183 priority = ui.configint('hooks', 'priority.%s' % name, 0)
186 priority = ui.configint('hooks', 'priority.%s' % name, 0)
184 hooks[name] = (-priority, len(hooks), name, cmd)
187 hooks[name] = (-priority, len(hooks), name, cmd)
185 return hooks
188 return hooks
186
189
187 _redirect = False
190 _redirect = False
188 def redirect(state):
191 def redirect(state):
189 global _redirect
192 global _redirect
190 _redirect = state
193 _redirect = state
191
194
192 def hashook(ui, htype):
195 def hashook(ui, htype):
193 """return True if a hook is configured for 'htype'"""
196 """return True if a hook is configured for 'htype'"""
194 if not ui.callhooks:
197 if not ui.callhooks:
195 return False
198 return False
196 for hname, cmd in _allhooks(ui):
199 for hname, cmd in _allhooks(ui):
197 if hname.split('.')[0] == htype and cmd:
200 if hname.split('.')[0] == htype and cmd:
198 return True
201 return True
199 return False
202 return False
200
203
201 def hook(ui, repo, htype, throw=False, **args):
204 def hook(ui, repo, htype, throw=False, **args):
202 if not ui.callhooks:
205 if not ui.callhooks:
203 return False
206 return False
204
207
205 hooks = []
208 hooks = []
206 for hname, cmd in _allhooks(ui):
209 for hname, cmd in _allhooks(ui):
207 if hname.split('.')[0] == htype and cmd:
210 if hname.split('.')[0] == htype and cmd:
208 hooks.append((hname, cmd))
211 hooks.append((hname, cmd))
209
212
210 res = runhooks(ui, repo, htype, hooks, throw=throw, **args)
213 res = runhooks(ui, repo, htype, hooks, throw=throw, **args)
211 r = False
214 r = False
212 for hname, cmd in hooks:
215 for hname, cmd in hooks:
213 r = res[hname][0] or r
216 r = res[hname][0] or r
214 return r
217 return r
215
218
216 def runhooks(ui, repo, htype, hooks, throw=False, **args):
219 def runhooks(ui, repo, htype, hooks, throw=False, **args):
217 args = pycompat.byteskwargs(args)
220 args = pycompat.byteskwargs(args)
218 res = {}
221 res = {}
219 oldstdout = -1
222 oldstdout = -1
220
223
221 try:
224 try:
222 for hname, cmd in hooks:
225 for hname, cmd in hooks:
223 if oldstdout == -1 and _redirect:
226 if oldstdout == -1 and _redirect:
224 try:
227 try:
225 stdoutno = util.stdout.fileno()
228 stdoutno = procutil.stdout.fileno()
226 stderrno = util.stderr.fileno()
229 stderrno = procutil.stderr.fileno()
227 # temporarily redirect stdout to stderr, if possible
230 # temporarily redirect stdout to stderr, if possible
228 if stdoutno >= 0 and stderrno >= 0:
231 if stdoutno >= 0 and stderrno >= 0:
229 util.stdout.flush()
232 procutil.stdout.flush()
230 oldstdout = os.dup(stdoutno)
233 oldstdout = os.dup(stdoutno)
231 os.dup2(stderrno, stdoutno)
234 os.dup2(stderrno, stdoutno)
232 except (OSError, AttributeError):
235 except (OSError, AttributeError):
233 # files seem to be bogus, give up on redirecting (WSGI, etc)
236 # files seem to be bogus, give up on redirecting (WSGI, etc)
234 pass
237 pass
235
238
236 if cmd is _fromuntrusted:
239 if cmd is _fromuntrusted:
237 if throw:
240 if throw:
238 raise error.HookAbort(
241 raise error.HookAbort(
239 _('untrusted hook %s not executed') % hname,
242 _('untrusted hook %s not executed') % hname,
240 hint = _("see 'hg help config.trusted'"))
243 hint = _("see 'hg help config.trusted'"))
241 ui.warn(_('warning: untrusted hook %s not executed\n') % hname)
244 ui.warn(_('warning: untrusted hook %s not executed\n') % hname)
242 r = 1
245 r = 1
243 raised = False
246 raised = False
244 elif callable(cmd):
247 elif callable(cmd):
245 r, raised = _pythonhook(ui, repo, htype, hname, cmd, args,
248 r, raised = _pythonhook(ui, repo, htype, hname, cmd, args,
246 throw)
249 throw)
247 elif cmd.startswith('python:'):
250 elif cmd.startswith('python:'):
248 if cmd.count(':') >= 2:
251 if cmd.count(':') >= 2:
249 path, cmd = cmd[7:].rsplit(':', 1)
252 path, cmd = cmd[7:].rsplit(':', 1)
250 path = util.expandpath(path)
253 path = util.expandpath(path)
251 if repo:
254 if repo:
252 path = os.path.join(repo.root, path)
255 path = os.path.join(repo.root, path)
253 try:
256 try:
254 mod = extensions.loadpath(path, 'hghook.%s' % hname)
257 mod = extensions.loadpath(path, 'hghook.%s' % hname)
255 except Exception:
258 except Exception:
256 ui.write(_("loading %s hook failed:\n") % hname)
259 ui.write(_("loading %s hook failed:\n") % hname)
257 raise
260 raise
258 hookfn = getattr(mod, cmd)
261 hookfn = getattr(mod, cmd)
259 else:
262 else:
260 hookfn = cmd[7:].strip()
263 hookfn = cmd[7:].strip()
261 r, raised = _pythonhook(ui, repo, htype, hname, hookfn, args,
264 r, raised = _pythonhook(ui, repo, htype, hname, hookfn, args,
262 throw)
265 throw)
263 else:
266 else:
264 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
267 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
265 raised = False
268 raised = False
266
269
267 res[hname] = r, raised
270 res[hname] = r, raised
268 finally:
271 finally:
269 # The stderr is fully buffered on Windows when connected to a pipe.
272 # The stderr is fully buffered on Windows when connected to a pipe.
270 # A forcible flush is required to make small stderr data in the
273 # A forcible flush is required to make small stderr data in the
271 # remote side available to the client immediately.
274 # remote side available to the client immediately.
272 util.stderr.flush()
275 procutil.stderr.flush()
273
276
274 if _redirect and oldstdout >= 0:
277 if _redirect and oldstdout >= 0:
275 util.stdout.flush() # write hook output to stderr fd
278 procutil.stdout.flush() # write hook output to stderr fd
276 os.dup2(oldstdout, stdoutno)
279 os.dup2(oldstdout, stdoutno)
277 os.close(oldstdout)
280 os.close(oldstdout)
278
281
279 return res
282 return res
@@ -1,727 +1,730 b''
1 # This library is free software; you can redistribute it and/or
1 # This library is free software; you can redistribute it and/or
2 # modify it under the terms of the GNU Lesser General Public
2 # modify it under the terms of the GNU Lesser General Public
3 # License as published by the Free Software Foundation; either
3 # License as published by the Free Software Foundation; either
4 # version 2.1 of the License, or (at your option) any later version.
4 # version 2.1 of the License, or (at your option) any later version.
5 #
5 #
6 # This library is distributed in the hope that it will be useful,
6 # This library is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 # Lesser General Public License for more details.
9 # Lesser General Public License for more details.
10 #
10 #
11 # You should have received a copy of the GNU Lesser General Public
11 # You should have received a copy of the GNU Lesser General Public
12 # License along with this library; if not, see
12 # License along with this library; if not, see
13 # <http://www.gnu.org/licenses/>.
13 # <http://www.gnu.org/licenses/>.
14
14
15 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
15 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
16 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
16 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
17
17
18 # Modified by Benoit Boissinot:
18 # Modified by Benoit Boissinot:
19 # - fix for digest auth (inspired from urllib2.py @ Python v2.4)
19 # - fix for digest auth (inspired from urllib2.py @ Python v2.4)
20 # Modified by Dirkjan Ochtman:
20 # Modified by Dirkjan Ochtman:
21 # - import md5 function from a local util module
21 # - import md5 function from a local util module
22 # Modified by Augie Fackler:
22 # Modified by Augie Fackler:
23 # - add safesend method and use it to prevent broken pipe errors
23 # - add safesend method and use it to prevent broken pipe errors
24 # on large POST requests
24 # on large POST requests
25
25
26 """An HTTP handler for urllib2 that supports HTTP 1.1 and keepalive.
26 """An HTTP handler for urllib2 that supports HTTP 1.1 and keepalive.
27
27
28 >>> import urllib2
28 >>> import urllib2
29 >>> from keepalive import HTTPHandler
29 >>> from keepalive import HTTPHandler
30 >>> keepalive_handler = HTTPHandler()
30 >>> keepalive_handler = HTTPHandler()
31 >>> opener = urlreq.buildopener(keepalive_handler)
31 >>> opener = urlreq.buildopener(keepalive_handler)
32 >>> urlreq.installopener(opener)
32 >>> urlreq.installopener(opener)
33 >>>
33 >>>
34 >>> fo = urlreq.urlopen('http://www.python.org')
34 >>> fo = urlreq.urlopen('http://www.python.org')
35
35
36 If a connection to a given host is requested, and all of the existing
36 If a connection to a given host is requested, and all of the existing
37 connections are still in use, another connection will be opened. If
37 connections are still in use, another connection will be opened. If
38 the handler tries to use an existing connection but it fails in some
38 the handler tries to use an existing connection but it fails in some
39 way, it will be closed and removed from the pool.
39 way, it will be closed and removed from the pool.
40
40
41 To remove the handler, simply re-run build_opener with no arguments, and
41 To remove the handler, simply re-run build_opener with no arguments, and
42 install that opener.
42 install that opener.
43
43
44 You can explicitly close connections by using the close_connection()
44 You can explicitly close connections by using the close_connection()
45 method of the returned file-like object (described below) or you can
45 method of the returned file-like object (described below) or you can
46 use the handler methods:
46 use the handler methods:
47
47
48 close_connection(host)
48 close_connection(host)
49 close_all()
49 close_all()
50 open_connections()
50 open_connections()
51
51
52 NOTE: using the close_connection and close_all methods of the handler
52 NOTE: using the close_connection and close_all methods of the handler
53 should be done with care when using multiple threads.
53 should be done with care when using multiple threads.
54 * there is nothing that prevents another thread from creating new
54 * there is nothing that prevents another thread from creating new
55 connections immediately after connections are closed
55 connections immediately after connections are closed
56 * no checks are done to prevent in-use connections from being closed
56 * no checks are done to prevent in-use connections from being closed
57
57
58 >>> keepalive_handler.close_all()
58 >>> keepalive_handler.close_all()
59
59
60 EXTRA ATTRIBUTES AND METHODS
60 EXTRA ATTRIBUTES AND METHODS
61
61
62 Upon a status of 200, the object returned has a few additional
62 Upon a status of 200, the object returned has a few additional
63 attributes and methods, which should not be used if you want to
63 attributes and methods, which should not be used if you want to
64 remain consistent with the normal urllib2-returned objects:
64 remain consistent with the normal urllib2-returned objects:
65
65
66 close_connection() - close the connection to the host
66 close_connection() - close the connection to the host
67 readlines() - you know, readlines()
67 readlines() - you know, readlines()
68 status - the return status (i.e. 404)
68 status - the return status (i.e. 404)
69 reason - english translation of status (i.e. 'File not found')
69 reason - english translation of status (i.e. 'File not found')
70
70
71 If you want the best of both worlds, use this inside an
71 If you want the best of both worlds, use this inside an
72 AttributeError-catching try:
72 AttributeError-catching try:
73
73
74 >>> try: status = fo.status
74 >>> try: status = fo.status
75 >>> except AttributeError: status = None
75 >>> except AttributeError: status = None
76
76
77 Unfortunately, these are ONLY there if status == 200, so it's not
77 Unfortunately, these are ONLY there if status == 200, so it's not
78 easy to distinguish between non-200 responses. The reason is that
78 easy to distinguish between non-200 responses. The reason is that
79 urllib2 tries to do clever things with error codes 301, 302, 401,
79 urllib2 tries to do clever things with error codes 301, 302, 401,
80 and 407, and it wraps the object upon return.
80 and 407, and it wraps the object upon return.
81 """
81 """
82
82
83 # $Id: keepalive.py,v 1.14 2006/04/04 21:00:32 mstenner Exp $
83 # $Id: keepalive.py,v 1.14 2006/04/04 21:00:32 mstenner Exp $
84
84
85 from __future__ import absolute_import, print_function
85 from __future__ import absolute_import, print_function
86
86
87 import errno
87 import errno
88 import hashlib
88 import hashlib
89 import socket
89 import socket
90 import sys
90 import sys
91 import threading
91 import threading
92
92
93 from .i18n import _
93 from .i18n import _
94 from . import (
94 from . import (
95 node,
95 node,
96 pycompat,
96 pycompat,
97 urllibcompat,
97 urllibcompat,
98 util,
98 util,
99 )
99 )
100 from .utils import (
101 procutil,
102 )
100
103
101 httplib = util.httplib
104 httplib = util.httplib
102 urlerr = util.urlerr
105 urlerr = util.urlerr
103 urlreq = util.urlreq
106 urlreq = util.urlreq
104
107
105 DEBUG = None
108 DEBUG = None
106
109
107 class ConnectionManager(object):
110 class ConnectionManager(object):
108 """
111 """
109 The connection manager must be able to:
112 The connection manager must be able to:
110 * keep track of all existing
113 * keep track of all existing
111 """
114 """
112 def __init__(self):
115 def __init__(self):
113 self._lock = threading.Lock()
116 self._lock = threading.Lock()
114 self._hostmap = {} # map hosts to a list of connections
117 self._hostmap = {} # map hosts to a list of connections
115 self._connmap = {} # map connections to host
118 self._connmap = {} # map connections to host
116 self._readymap = {} # map connection to ready state
119 self._readymap = {} # map connection to ready state
117
120
118 def add(self, host, connection, ready):
121 def add(self, host, connection, ready):
119 self._lock.acquire()
122 self._lock.acquire()
120 try:
123 try:
121 if host not in self._hostmap:
124 if host not in self._hostmap:
122 self._hostmap[host] = []
125 self._hostmap[host] = []
123 self._hostmap[host].append(connection)
126 self._hostmap[host].append(connection)
124 self._connmap[connection] = host
127 self._connmap[connection] = host
125 self._readymap[connection] = ready
128 self._readymap[connection] = ready
126 finally:
129 finally:
127 self._lock.release()
130 self._lock.release()
128
131
129 def remove(self, connection):
132 def remove(self, connection):
130 self._lock.acquire()
133 self._lock.acquire()
131 try:
134 try:
132 try:
135 try:
133 host = self._connmap[connection]
136 host = self._connmap[connection]
134 except KeyError:
137 except KeyError:
135 pass
138 pass
136 else:
139 else:
137 del self._connmap[connection]
140 del self._connmap[connection]
138 del self._readymap[connection]
141 del self._readymap[connection]
139 self._hostmap[host].remove(connection)
142 self._hostmap[host].remove(connection)
140 if not self._hostmap[host]:
143 if not self._hostmap[host]:
141 del self._hostmap[host]
144 del self._hostmap[host]
142 finally:
145 finally:
143 self._lock.release()
146 self._lock.release()
144
147
145 def set_ready(self, connection, ready):
148 def set_ready(self, connection, ready):
146 try:
149 try:
147 self._readymap[connection] = ready
150 self._readymap[connection] = ready
148 except KeyError:
151 except KeyError:
149 pass
152 pass
150
153
151 def get_ready_conn(self, host):
154 def get_ready_conn(self, host):
152 conn = None
155 conn = None
153 self._lock.acquire()
156 self._lock.acquire()
154 try:
157 try:
155 if host in self._hostmap:
158 if host in self._hostmap:
156 for c in self._hostmap[host]:
159 for c in self._hostmap[host]:
157 if self._readymap[c]:
160 if self._readymap[c]:
158 self._readymap[c] = 0
161 self._readymap[c] = 0
159 conn = c
162 conn = c
160 break
163 break
161 finally:
164 finally:
162 self._lock.release()
165 self._lock.release()
163 return conn
166 return conn
164
167
165 def get_all(self, host=None):
168 def get_all(self, host=None):
166 if host:
169 if host:
167 return list(self._hostmap.get(host, []))
170 return list(self._hostmap.get(host, []))
168 else:
171 else:
169 return dict(self._hostmap)
172 return dict(self._hostmap)
170
173
171 class KeepAliveHandler(object):
174 class KeepAliveHandler(object):
172 def __init__(self):
175 def __init__(self):
173 self._cm = ConnectionManager()
176 self._cm = ConnectionManager()
174
177
175 #### Connection Management
178 #### Connection Management
176 def open_connections(self):
179 def open_connections(self):
177 """return a list of connected hosts and the number of connections
180 """return a list of connected hosts and the number of connections
178 to each. [('foo.com:80', 2), ('bar.org', 1)]"""
181 to each. [('foo.com:80', 2), ('bar.org', 1)]"""
179 return [(host, len(li)) for (host, li) in self._cm.get_all().items()]
182 return [(host, len(li)) for (host, li) in self._cm.get_all().items()]
180
183
181 def close_connection(self, host):
184 def close_connection(self, host):
182 """close connection(s) to <host>
185 """close connection(s) to <host>
183 host is the host:port spec, as in 'www.cnn.com:8080' as passed in.
186 host is the host:port spec, as in 'www.cnn.com:8080' as passed in.
184 no error occurs if there is no connection to that host."""
187 no error occurs if there is no connection to that host."""
185 for h in self._cm.get_all(host):
188 for h in self._cm.get_all(host):
186 self._cm.remove(h)
189 self._cm.remove(h)
187 h.close()
190 h.close()
188
191
189 def close_all(self):
192 def close_all(self):
190 """close all open connections"""
193 """close all open connections"""
191 for host, conns in self._cm.get_all().iteritems():
194 for host, conns in self._cm.get_all().iteritems():
192 for h in conns:
195 for h in conns:
193 self._cm.remove(h)
196 self._cm.remove(h)
194 h.close()
197 h.close()
195
198
196 def _request_closed(self, request, host, connection):
199 def _request_closed(self, request, host, connection):
197 """tells us that this request is now closed and that the
200 """tells us that this request is now closed and that the
198 connection is ready for another request"""
201 connection is ready for another request"""
199 self._cm.set_ready(connection, 1)
202 self._cm.set_ready(connection, 1)
200
203
201 def _remove_connection(self, host, connection, close=0):
204 def _remove_connection(self, host, connection, close=0):
202 if close:
205 if close:
203 connection.close()
206 connection.close()
204 self._cm.remove(connection)
207 self._cm.remove(connection)
205
208
206 #### Transaction Execution
209 #### Transaction Execution
207 def http_open(self, req):
210 def http_open(self, req):
208 return self.do_open(HTTPConnection, req)
211 return self.do_open(HTTPConnection, req)
209
212
210 def do_open(self, http_class, req):
213 def do_open(self, http_class, req):
211 host = urllibcompat.gethost(req)
214 host = urllibcompat.gethost(req)
212 if not host:
215 if not host:
213 raise urlerr.urlerror('no host given')
216 raise urlerr.urlerror('no host given')
214
217
215 try:
218 try:
216 h = self._cm.get_ready_conn(host)
219 h = self._cm.get_ready_conn(host)
217 while h:
220 while h:
218 r = self._reuse_connection(h, req, host)
221 r = self._reuse_connection(h, req, host)
219
222
220 # if this response is non-None, then it worked and we're
223 # if this response is non-None, then it worked and we're
221 # done. Break out, skipping the else block.
224 # done. Break out, skipping the else block.
222 if r:
225 if r:
223 break
226 break
224
227
225 # connection is bad - possibly closed by server
228 # connection is bad - possibly closed by server
226 # discard it and ask for the next free connection
229 # discard it and ask for the next free connection
227 h.close()
230 h.close()
228 self._cm.remove(h)
231 self._cm.remove(h)
229 h = self._cm.get_ready_conn(host)
232 h = self._cm.get_ready_conn(host)
230 else:
233 else:
231 # no (working) free connections were found. Create a new one.
234 # no (working) free connections were found. Create a new one.
232 h = http_class(host)
235 h = http_class(host)
233 if DEBUG:
236 if DEBUG:
234 DEBUG.info("creating new connection to %s (%d)",
237 DEBUG.info("creating new connection to %s (%d)",
235 host, id(h))
238 host, id(h))
236 self._cm.add(host, h, 0)
239 self._cm.add(host, h, 0)
237 self._start_transaction(h, req)
240 self._start_transaction(h, req)
238 r = h.getresponse()
241 r = h.getresponse()
239 # The string form of BadStatusLine is the status line. Add some context
242 # The string form of BadStatusLine is the status line. Add some context
240 # to make the error message slightly more useful.
243 # to make the error message slightly more useful.
241 except httplib.BadStatusLine as err:
244 except httplib.BadStatusLine as err:
242 raise urlerr.urlerror(
245 raise urlerr.urlerror(
243 _('bad HTTP status line: %s') % pycompat.sysbytes(err.line))
246 _('bad HTTP status line: %s') % pycompat.sysbytes(err.line))
244 except (socket.error, httplib.HTTPException) as err:
247 except (socket.error, httplib.HTTPException) as err:
245 raise urlerr.urlerror(err)
248 raise urlerr.urlerror(err)
246
249
247 # if not a persistent connection, don't try to reuse it
250 # if not a persistent connection, don't try to reuse it
248 if r.will_close:
251 if r.will_close:
249 self._cm.remove(h)
252 self._cm.remove(h)
250
253
251 if DEBUG:
254 if DEBUG:
252 DEBUG.info("STATUS: %s, %s", r.status, r.reason)
255 DEBUG.info("STATUS: %s, %s", r.status, r.reason)
253 r._handler = self
256 r._handler = self
254 r._host = host
257 r._host = host
255 r._url = req.get_full_url()
258 r._url = req.get_full_url()
256 r._connection = h
259 r._connection = h
257 r.code = r.status
260 r.code = r.status
258 r.headers = r.msg
261 r.headers = r.msg
259 r.msg = r.reason
262 r.msg = r.reason
260
263
261 return r
264 return r
262
265
263 def _reuse_connection(self, h, req, host):
266 def _reuse_connection(self, h, req, host):
264 """start the transaction with a re-used connection
267 """start the transaction with a re-used connection
265 return a response object (r) upon success or None on failure.
268 return a response object (r) upon success or None on failure.
266 This DOES not close or remove bad connections in cases where
269 This DOES not close or remove bad connections in cases where
267 it returns. However, if an unexpected exception occurs, it
270 it returns. However, if an unexpected exception occurs, it
268 will close and remove the connection before re-raising.
271 will close and remove the connection before re-raising.
269 """
272 """
270 try:
273 try:
271 self._start_transaction(h, req)
274 self._start_transaction(h, req)
272 r = h.getresponse()
275 r = h.getresponse()
273 # note: just because we got something back doesn't mean it
276 # note: just because we got something back doesn't mean it
274 # worked. We'll check the version below, too.
277 # worked. We'll check the version below, too.
275 except (socket.error, httplib.HTTPException):
278 except (socket.error, httplib.HTTPException):
276 r = None
279 r = None
277 except: # re-raises
280 except: # re-raises
278 # adding this block just in case we've missed
281 # adding this block just in case we've missed
279 # something we will still raise the exception, but
282 # something we will still raise the exception, but
280 # lets try and close the connection and remove it
283 # lets try and close the connection and remove it
281 # first. We previously got into a nasty loop
284 # first. We previously got into a nasty loop
282 # where an exception was uncaught, and so the
285 # where an exception was uncaught, and so the
283 # connection stayed open. On the next try, the
286 # connection stayed open. On the next try, the
284 # same exception was raised, etc. The trade-off is
287 # same exception was raised, etc. The trade-off is
285 # that it's now possible this call will raise
288 # that it's now possible this call will raise
286 # a DIFFERENT exception
289 # a DIFFERENT exception
287 if DEBUG:
290 if DEBUG:
288 DEBUG.error("unexpected exception - closing "
291 DEBUG.error("unexpected exception - closing "
289 "connection to %s (%d)", host, id(h))
292 "connection to %s (%d)", host, id(h))
290 self._cm.remove(h)
293 self._cm.remove(h)
291 h.close()
294 h.close()
292 raise
295 raise
293
296
294 if r is None or r.version == 9:
297 if r is None or r.version == 9:
295 # httplib falls back to assuming HTTP 0.9 if it gets a
298 # httplib falls back to assuming HTTP 0.9 if it gets a
296 # bad header back. This is most likely to happen if
299 # bad header back. This is most likely to happen if
297 # the socket has been closed by the server since we
300 # the socket has been closed by the server since we
298 # last used the connection.
301 # last used the connection.
299 if DEBUG:
302 if DEBUG:
300 DEBUG.info("failed to re-use connection to %s (%d)",
303 DEBUG.info("failed to re-use connection to %s (%d)",
301 host, id(h))
304 host, id(h))
302 r = None
305 r = None
303 else:
306 else:
304 if DEBUG:
307 if DEBUG:
305 DEBUG.info("re-using connection to %s (%d)", host, id(h))
308 DEBUG.info("re-using connection to %s (%d)", host, id(h))
306
309
307 return r
310 return r
308
311
309 def _start_transaction(self, h, req):
312 def _start_transaction(self, h, req):
310 # What follows mostly reimplements HTTPConnection.request()
313 # What follows mostly reimplements HTTPConnection.request()
311 # except it adds self.parent.addheaders in the mix and sends headers
314 # except it adds self.parent.addheaders in the mix and sends headers
312 # in a deterministic order (to make testing easier).
315 # in a deterministic order (to make testing easier).
313 headers = util.sortdict(self.parent.addheaders)
316 headers = util.sortdict(self.parent.addheaders)
314 headers.update(sorted(req.headers.items()))
317 headers.update(sorted(req.headers.items()))
315 headers.update(sorted(req.unredirected_hdrs.items()))
318 headers.update(sorted(req.unredirected_hdrs.items()))
316 headers = util.sortdict((n.lower(), v) for n, v in headers.items())
319 headers = util.sortdict((n.lower(), v) for n, v in headers.items())
317 skipheaders = {}
320 skipheaders = {}
318 for n in ('host', 'accept-encoding'):
321 for n in ('host', 'accept-encoding'):
319 if n in headers:
322 if n in headers:
320 skipheaders['skip_' + n.replace('-', '_')] = 1
323 skipheaders['skip_' + n.replace('-', '_')] = 1
321 try:
324 try:
322 if urllibcompat.hasdata(req):
325 if urllibcompat.hasdata(req):
323 data = urllibcompat.getdata(req)
326 data = urllibcompat.getdata(req)
324 h.putrequest(
327 h.putrequest(
325 req.get_method(), urllibcompat.getselector(req),
328 req.get_method(), urllibcompat.getselector(req),
326 **pycompat.strkwargs(skipheaders))
329 **pycompat.strkwargs(skipheaders))
327 if r'content-type' not in headers:
330 if r'content-type' not in headers:
328 h.putheader(r'Content-type',
331 h.putheader(r'Content-type',
329 r'application/x-www-form-urlencoded')
332 r'application/x-www-form-urlencoded')
330 if r'content-length' not in headers:
333 if r'content-length' not in headers:
331 h.putheader(r'Content-length', r'%d' % len(data))
334 h.putheader(r'Content-length', r'%d' % len(data))
332 else:
335 else:
333 h.putrequest(
336 h.putrequest(
334 req.get_method(), urllibcompat.getselector(req),
337 req.get_method(), urllibcompat.getselector(req),
335 **pycompat.strkwargs(skipheaders))
338 **pycompat.strkwargs(skipheaders))
336 except socket.error as err:
339 except socket.error as err:
337 raise urlerr.urlerror(err)
340 raise urlerr.urlerror(err)
338 for k, v in headers.items():
341 for k, v in headers.items():
339 h.putheader(k, v)
342 h.putheader(k, v)
340 h.endheaders()
343 h.endheaders()
341 if urllibcompat.hasdata(req):
344 if urllibcompat.hasdata(req):
342 h.send(data)
345 h.send(data)
343
346
344 class HTTPHandler(KeepAliveHandler, urlreq.httphandler):
347 class HTTPHandler(KeepAliveHandler, urlreq.httphandler):
345 pass
348 pass
346
349
347 class HTTPResponse(httplib.HTTPResponse):
350 class HTTPResponse(httplib.HTTPResponse):
348 # we need to subclass HTTPResponse in order to
351 # we need to subclass HTTPResponse in order to
349 # 1) add readline() and readlines() methods
352 # 1) add readline() and readlines() methods
350 # 2) add close_connection() methods
353 # 2) add close_connection() methods
351 # 3) add info() and geturl() methods
354 # 3) add info() and geturl() methods
352
355
353 # in order to add readline(), read must be modified to deal with a
356 # in order to add readline(), read must be modified to deal with a
354 # buffer. example: readline must read a buffer and then spit back
357 # buffer. example: readline must read a buffer and then spit back
355 # one line at a time. The only real alternative is to read one
358 # one line at a time. The only real alternative is to read one
356 # BYTE at a time (ick). Once something has been read, it can't be
359 # BYTE at a time (ick). Once something has been read, it can't be
357 # put back (ok, maybe it can, but that's even uglier than this),
360 # put back (ok, maybe it can, but that's even uglier than this),
358 # so if you THEN do a normal read, you must first take stuff from
361 # so if you THEN do a normal read, you must first take stuff from
359 # the buffer.
362 # the buffer.
360
363
361 # the read method wraps the original to accommodate buffering,
364 # the read method wraps the original to accommodate buffering,
362 # although read() never adds to the buffer.
365 # although read() never adds to the buffer.
363 # Both readline and readlines have been stolen with almost no
366 # Both readline and readlines have been stolen with almost no
364 # modification from socket.py
367 # modification from socket.py
365
368
366
369
367 def __init__(self, sock, debuglevel=0, strict=0, method=None):
370 def __init__(self, sock, debuglevel=0, strict=0, method=None):
368 extrakw = {}
371 extrakw = {}
369 if not pycompat.ispy3:
372 if not pycompat.ispy3:
370 extrakw[r'strict'] = True
373 extrakw[r'strict'] = True
371 extrakw[r'buffering'] = True
374 extrakw[r'buffering'] = True
372 httplib.HTTPResponse.__init__(self, sock, debuglevel=debuglevel,
375 httplib.HTTPResponse.__init__(self, sock, debuglevel=debuglevel,
373 method=method, **extrakw)
376 method=method, **extrakw)
374 self.fileno = sock.fileno
377 self.fileno = sock.fileno
375 self.code = None
378 self.code = None
376 self._rbuf = ''
379 self._rbuf = ''
377 self._rbufsize = 8096
380 self._rbufsize = 8096
378 self._handler = None # inserted by the handler later
381 self._handler = None # inserted by the handler later
379 self._host = None # (same)
382 self._host = None # (same)
380 self._url = None # (same)
383 self._url = None # (same)
381 self._connection = None # (same)
384 self._connection = None # (same)
382
385
383 _raw_read = httplib.HTTPResponse.read
386 _raw_read = httplib.HTTPResponse.read
384
387
385 def close(self):
388 def close(self):
386 if self.fp:
389 if self.fp:
387 self.fp.close()
390 self.fp.close()
388 self.fp = None
391 self.fp = None
389 if self._handler:
392 if self._handler:
390 self._handler._request_closed(self, self._host,
393 self._handler._request_closed(self, self._host,
391 self._connection)
394 self._connection)
392
395
393 def close_connection(self):
396 def close_connection(self):
394 self._handler._remove_connection(self._host, self._connection, close=1)
397 self._handler._remove_connection(self._host, self._connection, close=1)
395 self.close()
398 self.close()
396
399
397 def info(self):
400 def info(self):
398 return self.headers
401 return self.headers
399
402
400 def geturl(self):
403 def geturl(self):
401 return self._url
404 return self._url
402
405
403 def read(self, amt=None):
406 def read(self, amt=None):
404 # the _rbuf test is only in this first if for speed. It's not
407 # the _rbuf test is only in this first if for speed. It's not
405 # logically necessary
408 # logically necessary
406 if self._rbuf and amt is not None:
409 if self._rbuf and amt is not None:
407 L = len(self._rbuf)
410 L = len(self._rbuf)
408 if amt > L:
411 if amt > L:
409 amt -= L
412 amt -= L
410 else:
413 else:
411 s = self._rbuf[:amt]
414 s = self._rbuf[:amt]
412 self._rbuf = self._rbuf[amt:]
415 self._rbuf = self._rbuf[amt:]
413 return s
416 return s
414
417
415 s = self._rbuf + self._raw_read(amt)
418 s = self._rbuf + self._raw_read(amt)
416 self._rbuf = ''
419 self._rbuf = ''
417 return s
420 return s
418
421
419 # stolen from Python SVN #68532 to fix issue1088
422 # stolen from Python SVN #68532 to fix issue1088
420 def _read_chunked(self, amt):
423 def _read_chunked(self, amt):
421 chunk_left = self.chunk_left
424 chunk_left = self.chunk_left
422 parts = []
425 parts = []
423
426
424 while True:
427 while True:
425 if chunk_left is None:
428 if chunk_left is None:
426 line = self.fp.readline()
429 line = self.fp.readline()
427 i = line.find(';')
430 i = line.find(';')
428 if i >= 0:
431 if i >= 0:
429 line = line[:i] # strip chunk-extensions
432 line = line[:i] # strip chunk-extensions
430 try:
433 try:
431 chunk_left = int(line, 16)
434 chunk_left = int(line, 16)
432 except ValueError:
435 except ValueError:
433 # close the connection as protocol synchronization is
436 # close the connection as protocol synchronization is
434 # probably lost
437 # probably lost
435 self.close()
438 self.close()
436 raise httplib.IncompleteRead(''.join(parts))
439 raise httplib.IncompleteRead(''.join(parts))
437 if chunk_left == 0:
440 if chunk_left == 0:
438 break
441 break
439 if amt is None:
442 if amt is None:
440 parts.append(self._safe_read(chunk_left))
443 parts.append(self._safe_read(chunk_left))
441 elif amt < chunk_left:
444 elif amt < chunk_left:
442 parts.append(self._safe_read(amt))
445 parts.append(self._safe_read(amt))
443 self.chunk_left = chunk_left - amt
446 self.chunk_left = chunk_left - amt
444 return ''.join(parts)
447 return ''.join(parts)
445 elif amt == chunk_left:
448 elif amt == chunk_left:
446 parts.append(self._safe_read(amt))
449 parts.append(self._safe_read(amt))
447 self._safe_read(2) # toss the CRLF at the end of the chunk
450 self._safe_read(2) # toss the CRLF at the end of the chunk
448 self.chunk_left = None
451 self.chunk_left = None
449 return ''.join(parts)
452 return ''.join(parts)
450 else:
453 else:
451 parts.append(self._safe_read(chunk_left))
454 parts.append(self._safe_read(chunk_left))
452 amt -= chunk_left
455 amt -= chunk_left
453
456
454 # we read the whole chunk, get another
457 # we read the whole chunk, get another
455 self._safe_read(2) # toss the CRLF at the end of the chunk
458 self._safe_read(2) # toss the CRLF at the end of the chunk
456 chunk_left = None
459 chunk_left = None
457
460
458 # read and discard trailer up to the CRLF terminator
461 # read and discard trailer up to the CRLF terminator
459 ### note: we shouldn't have any trailers!
462 ### note: we shouldn't have any trailers!
460 while True:
463 while True:
461 line = self.fp.readline()
464 line = self.fp.readline()
462 if not line:
465 if not line:
463 # a vanishingly small number of sites EOF without
466 # a vanishingly small number of sites EOF without
464 # sending the trailer
467 # sending the trailer
465 break
468 break
466 if line == '\r\n':
469 if line == '\r\n':
467 break
470 break
468
471
469 # we read everything; close the "file"
472 # we read everything; close the "file"
470 self.close()
473 self.close()
471
474
472 return ''.join(parts)
475 return ''.join(parts)
473
476
474 def readline(self):
477 def readline(self):
475 # Fast path for a line is already available in read buffer.
478 # Fast path for a line is already available in read buffer.
476 i = self._rbuf.find('\n')
479 i = self._rbuf.find('\n')
477 if i >= 0:
480 if i >= 0:
478 i += 1
481 i += 1
479 line = self._rbuf[:i]
482 line = self._rbuf[:i]
480 self._rbuf = self._rbuf[i:]
483 self._rbuf = self._rbuf[i:]
481 return line
484 return line
482
485
483 # No newline in local buffer. Read until we find one.
486 # No newline in local buffer. Read until we find one.
484 chunks = [self._rbuf]
487 chunks = [self._rbuf]
485 i = -1
488 i = -1
486 readsize = self._rbufsize
489 readsize = self._rbufsize
487 while True:
490 while True:
488 new = self._raw_read(readsize)
491 new = self._raw_read(readsize)
489 if not new:
492 if not new:
490 break
493 break
491
494
492 chunks.append(new)
495 chunks.append(new)
493 i = new.find('\n')
496 i = new.find('\n')
494 if i >= 0:
497 if i >= 0:
495 break
498 break
496
499
497 # We either have exhausted the stream or have a newline in chunks[-1].
500 # We either have exhausted the stream or have a newline in chunks[-1].
498
501
499 # EOF
502 # EOF
500 if i == -1:
503 if i == -1:
501 self._rbuf = ''
504 self._rbuf = ''
502 return ''.join(chunks)
505 return ''.join(chunks)
503
506
504 i += 1
507 i += 1
505 self._rbuf = chunks[-1][i:]
508 self._rbuf = chunks[-1][i:]
506 chunks[-1] = chunks[-1][:i]
509 chunks[-1] = chunks[-1][:i]
507 return ''.join(chunks)
510 return ''.join(chunks)
508
511
509 def readlines(self, sizehint=0):
512 def readlines(self, sizehint=0):
510 total = 0
513 total = 0
511 list = []
514 list = []
512 while True:
515 while True:
513 line = self.readline()
516 line = self.readline()
514 if not line:
517 if not line:
515 break
518 break
516 list.append(line)
519 list.append(line)
517 total += len(line)
520 total += len(line)
518 if sizehint and total >= sizehint:
521 if sizehint and total >= sizehint:
519 break
522 break
520 return list
523 return list
521
524
522 def safesend(self, str):
525 def safesend(self, str):
523 """Send `str' to the server.
526 """Send `str' to the server.
524
527
525 Shamelessly ripped off from httplib to patch a bad behavior.
528 Shamelessly ripped off from httplib to patch a bad behavior.
526 """
529 """
527 # _broken_pipe_resp is an attribute we set in this function
530 # _broken_pipe_resp is an attribute we set in this function
528 # if the socket is closed while we're sending data but
531 # if the socket is closed while we're sending data but
529 # the server sent us a response before hanging up.
532 # the server sent us a response before hanging up.
530 # In that case, we want to pretend to send the rest of the
533 # In that case, we want to pretend to send the rest of the
531 # outgoing data, and then let the user use getresponse()
534 # outgoing data, and then let the user use getresponse()
532 # (which we wrap) to get this last response before
535 # (which we wrap) to get this last response before
533 # opening a new socket.
536 # opening a new socket.
534 if getattr(self, '_broken_pipe_resp', None) is not None:
537 if getattr(self, '_broken_pipe_resp', None) is not None:
535 return
538 return
536
539
537 if self.sock is None:
540 if self.sock is None:
538 if self.auto_open:
541 if self.auto_open:
539 self.connect()
542 self.connect()
540 else:
543 else:
541 raise httplib.NotConnected
544 raise httplib.NotConnected
542
545
543 # send the data to the server. if we get a broken pipe, then close
546 # send the data to the server. if we get a broken pipe, then close
544 # the socket. we want to reconnect when somebody tries to send again.
547 # the socket. we want to reconnect when somebody tries to send again.
545 #
548 #
546 # NOTE: we DO propagate the error, though, because we cannot simply
549 # NOTE: we DO propagate the error, though, because we cannot simply
547 # ignore the error... the caller will know if they can retry.
550 # ignore the error... the caller will know if they can retry.
548 if self.debuglevel > 0:
551 if self.debuglevel > 0:
549 print("send:", repr(str))
552 print("send:", repr(str))
550 try:
553 try:
551 blocksize = 8192
554 blocksize = 8192
552 read = getattr(str, 'read', None)
555 read = getattr(str, 'read', None)
553 if read is not None:
556 if read is not None:
554 if self.debuglevel > 0:
557 if self.debuglevel > 0:
555 print("sending a read()able")
558 print("sending a read()able")
556 data = read(blocksize)
559 data = read(blocksize)
557 while data:
560 while data:
558 self.sock.sendall(data)
561 self.sock.sendall(data)
559 data = read(blocksize)
562 data = read(blocksize)
560 else:
563 else:
561 self.sock.sendall(str)
564 self.sock.sendall(str)
562 except socket.error as v:
565 except socket.error as v:
563 reraise = True
566 reraise = True
564 if v[0] == errno.EPIPE: # Broken pipe
567 if v[0] == errno.EPIPE: # Broken pipe
565 if self._HTTPConnection__state == httplib._CS_REQ_SENT:
568 if self._HTTPConnection__state == httplib._CS_REQ_SENT:
566 self._broken_pipe_resp = None
569 self._broken_pipe_resp = None
567 self._broken_pipe_resp = self.getresponse()
570 self._broken_pipe_resp = self.getresponse()
568 reraise = False
571 reraise = False
569 self.close()
572 self.close()
570 if reraise:
573 if reraise:
571 raise
574 raise
572
575
573 def wrapgetresponse(cls):
576 def wrapgetresponse(cls):
574 """Wraps getresponse in cls with a broken-pipe sane version.
577 """Wraps getresponse in cls with a broken-pipe sane version.
575 """
578 """
576 def safegetresponse(self):
579 def safegetresponse(self):
577 # In safesend() we might set the _broken_pipe_resp
580 # In safesend() we might set the _broken_pipe_resp
578 # attribute, in which case the socket has already
581 # attribute, in which case the socket has already
579 # been closed and we just need to give them the response
582 # been closed and we just need to give them the response
580 # back. Otherwise, we use the normal response path.
583 # back. Otherwise, we use the normal response path.
581 r = getattr(self, '_broken_pipe_resp', None)
584 r = getattr(self, '_broken_pipe_resp', None)
582 if r is not None:
585 if r is not None:
583 return r
586 return r
584 return cls.getresponse(self)
587 return cls.getresponse(self)
585 safegetresponse.__doc__ = cls.getresponse.__doc__
588 safegetresponse.__doc__ = cls.getresponse.__doc__
586 return safegetresponse
589 return safegetresponse
587
590
588 class HTTPConnection(httplib.HTTPConnection):
591 class HTTPConnection(httplib.HTTPConnection):
589 # use the modified response class
592 # use the modified response class
590 response_class = HTTPResponse
593 response_class = HTTPResponse
591 send = safesend
594 send = safesend
592 getresponse = wrapgetresponse(httplib.HTTPConnection)
595 getresponse = wrapgetresponse(httplib.HTTPConnection)
593
596
594
597
595 #########################################################################
598 #########################################################################
596 ##### TEST FUNCTIONS
599 ##### TEST FUNCTIONS
597 #########################################################################
600 #########################################################################
598
601
599
602
600 def continuity(url):
603 def continuity(url):
601 md5 = hashlib.md5
604 md5 = hashlib.md5
602 format = '%25s: %s'
605 format = '%25s: %s'
603
606
604 # first fetch the file with the normal http handler
607 # first fetch the file with the normal http handler
605 opener = urlreq.buildopener()
608 opener = urlreq.buildopener()
606 urlreq.installopener(opener)
609 urlreq.installopener(opener)
607 fo = urlreq.urlopen(url)
610 fo = urlreq.urlopen(url)
608 foo = fo.read()
611 foo = fo.read()
609 fo.close()
612 fo.close()
610 m = md5(foo)
613 m = md5(foo)
611 print(format % ('normal urllib', node.hex(m.digest())))
614 print(format % ('normal urllib', node.hex(m.digest())))
612
615
613 # now install the keepalive handler and try again
616 # now install the keepalive handler and try again
614 opener = urlreq.buildopener(HTTPHandler())
617 opener = urlreq.buildopener(HTTPHandler())
615 urlreq.installopener(opener)
618 urlreq.installopener(opener)
616
619
617 fo = urlreq.urlopen(url)
620 fo = urlreq.urlopen(url)
618 foo = fo.read()
621 foo = fo.read()
619 fo.close()
622 fo.close()
620 m = md5(foo)
623 m = md5(foo)
621 print(format % ('keepalive read', node.hex(m.digest())))
624 print(format % ('keepalive read', node.hex(m.digest())))
622
625
623 fo = urlreq.urlopen(url)
626 fo = urlreq.urlopen(url)
624 foo = ''
627 foo = ''
625 while True:
628 while True:
626 f = fo.readline()
629 f = fo.readline()
627 if f:
630 if f:
628 foo = foo + f
631 foo = foo + f
629 else:
632 else:
630 break
633 break
631 fo.close()
634 fo.close()
632 m = md5(foo)
635 m = md5(foo)
633 print(format % ('keepalive readline', node.hex(m.digest())))
636 print(format % ('keepalive readline', node.hex(m.digest())))
634
637
635 def comp(N, url):
638 def comp(N, url):
636 print(' making %i connections to:\n %s' % (N, url))
639 print(' making %i connections to:\n %s' % (N, url))
637
640
638 util.stdout.write(' first using the normal urllib handlers')
641 procutil.stdout.write(' first using the normal urllib handlers')
639 # first use normal opener
642 # first use normal opener
640 opener = urlreq.buildopener()
643 opener = urlreq.buildopener()
641 urlreq.installopener(opener)
644 urlreq.installopener(opener)
642 t1 = fetch(N, url)
645 t1 = fetch(N, url)
643 print(' TIME: %.3f s' % t1)
646 print(' TIME: %.3f s' % t1)
644
647
645 util.stdout.write(' now using the keepalive handler ')
648 procutil.stdout.write(' now using the keepalive handler ')
646 # now install the keepalive handler and try again
649 # now install the keepalive handler and try again
647 opener = urlreq.buildopener(HTTPHandler())
650 opener = urlreq.buildopener(HTTPHandler())
648 urlreq.installopener(opener)
651 urlreq.installopener(opener)
649 t2 = fetch(N, url)
652 t2 = fetch(N, url)
650 print(' TIME: %.3f s' % t2)
653 print(' TIME: %.3f s' % t2)
651 print(' improvement factor: %.2f' % (t1 / t2))
654 print(' improvement factor: %.2f' % (t1 / t2))
652
655
653 def fetch(N, url, delay=0):
656 def fetch(N, url, delay=0):
654 import time
657 import time
655 lens = []
658 lens = []
656 starttime = time.time()
659 starttime = time.time()
657 for i in range(N):
660 for i in range(N):
658 if delay and i > 0:
661 if delay and i > 0:
659 time.sleep(delay)
662 time.sleep(delay)
660 fo = urlreq.urlopen(url)
663 fo = urlreq.urlopen(url)
661 foo = fo.read()
664 foo = fo.read()
662 fo.close()
665 fo.close()
663 lens.append(len(foo))
666 lens.append(len(foo))
664 diff = time.time() - starttime
667 diff = time.time() - starttime
665
668
666 j = 0
669 j = 0
667 for i in lens[1:]:
670 for i in lens[1:]:
668 j = j + 1
671 j = j + 1
669 if not i == lens[0]:
672 if not i == lens[0]:
670 print("WARNING: inconsistent length on read %i: %i" % (j, i))
673 print("WARNING: inconsistent length on read %i: %i" % (j, i))
671
674
672 return diff
675 return diff
673
676
674 def test_timeout(url):
677 def test_timeout(url):
675 global DEBUG
678 global DEBUG
676 dbbackup = DEBUG
679 dbbackup = DEBUG
677 class FakeLogger(object):
680 class FakeLogger(object):
678 def debug(self, msg, *args):
681 def debug(self, msg, *args):
679 print(msg % args)
682 print(msg % args)
680 info = warning = error = debug
683 info = warning = error = debug
681 DEBUG = FakeLogger()
684 DEBUG = FakeLogger()
682 print(" fetching the file to establish a connection")
685 print(" fetching the file to establish a connection")
683 fo = urlreq.urlopen(url)
686 fo = urlreq.urlopen(url)
684 data1 = fo.read()
687 data1 = fo.read()
685 fo.close()
688 fo.close()
686
689
687 i = 20
690 i = 20
688 print(" waiting %i seconds for the server to close the connection" % i)
691 print(" waiting %i seconds for the server to close the connection" % i)
689 while i > 0:
692 while i > 0:
690 util.stdout.write('\r %2i' % i)
693 procutil.stdout.write('\r %2i' % i)
691 util.stdout.flush()
694 procutil.stdout.flush()
692 time.sleep(1)
695 time.sleep(1)
693 i -= 1
696 i -= 1
694 util.stderr.write('\r')
697 procutil.stderr.write('\r')
695
698
696 print(" fetching the file a second time")
699 print(" fetching the file a second time")
697 fo = urlreq.urlopen(url)
700 fo = urlreq.urlopen(url)
698 data2 = fo.read()
701 data2 = fo.read()
699 fo.close()
702 fo.close()
700
703
701 if data1 == data2:
704 if data1 == data2:
702 print(' data are identical')
705 print(' data are identical')
703 else:
706 else:
704 print(' ERROR: DATA DIFFER')
707 print(' ERROR: DATA DIFFER')
705
708
706 DEBUG = dbbackup
709 DEBUG = dbbackup
707
710
708
711
709 def test(url, N=10):
712 def test(url, N=10):
710 print("performing continuity test (making sure stuff isn't corrupted)")
713 print("performing continuity test (making sure stuff isn't corrupted)")
711 continuity(url)
714 continuity(url)
712 print('')
715 print('')
713 print("performing speed comparison")
716 print("performing speed comparison")
714 comp(N, url)
717 comp(N, url)
715 print('')
718 print('')
716 print("performing dropped-connection check")
719 print("performing dropped-connection check")
717 test_timeout(url)
720 test_timeout(url)
718
721
719 if __name__ == '__main__':
722 if __name__ == '__main__':
720 import time
723 import time
721 try:
724 try:
722 N = int(sys.argv[1])
725 N = int(sys.argv[1])
723 url = sys.argv[2]
726 url = sys.argv[2]
724 except (IndexError, ValueError):
727 except (IndexError, ValueError):
725 print("%s <integer> <url>" % sys.argv[0])
728 print("%s <integer> <url>" % sys.argv[0])
726 else:
729 else:
727 test(url, N)
730 test(url, N)
@@ -1,170 +1,174 b''
1 # server.py - utility and factory of server
1 # server.py - utility and factory of server
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
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import tempfile
11 import tempfile
12
12
13 from .i18n import _
13 from .i18n import _
14
14
15 from . import (
15 from . import (
16 chgserver,
16 chgserver,
17 cmdutil,
17 cmdutil,
18 commandserver,
18 commandserver,
19 error,
19 error,
20 hgweb,
20 hgweb,
21 pycompat,
21 pycompat,
22 util,
22 util,
23 )
23 )
24
24
25 from .utils import (
26 procutil,
27 )
28
25 def runservice(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
29 def runservice(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
26 runargs=None, appendpid=False):
30 runargs=None, appendpid=False):
27 '''Run a command as a service.'''
31 '''Run a command as a service.'''
28
32
29 def writepid(pid):
33 def writepid(pid):
30 if opts['pid_file']:
34 if opts['pid_file']:
31 if appendpid:
35 if appendpid:
32 mode = 'ab'
36 mode = 'ab'
33 else:
37 else:
34 mode = 'wb'
38 mode = 'wb'
35 fp = open(opts['pid_file'], mode)
39 fp = open(opts['pid_file'], mode)
36 fp.write('%d\n' % pid)
40 fp.write('%d\n' % pid)
37 fp.close()
41 fp.close()
38
42
39 if opts['daemon'] and not opts['daemon_postexec']:
43 if opts['daemon'] and not opts['daemon_postexec']:
40 # Signal child process startup with file removal
44 # Signal child process startup with file removal
41 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
45 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
42 os.close(lockfd)
46 os.close(lockfd)
43 try:
47 try:
44 if not runargs:
48 if not runargs:
45 runargs = util.hgcmd() + pycompat.sysargv[1:]
49 runargs = util.hgcmd() + pycompat.sysargv[1:]
46 runargs.append('--daemon-postexec=unlink:%s' % lockpath)
50 runargs.append('--daemon-postexec=unlink:%s' % lockpath)
47 # Don't pass --cwd to the child process, because we've already
51 # Don't pass --cwd to the child process, because we've already
48 # changed directory.
52 # changed directory.
49 for i in xrange(1, len(runargs)):
53 for i in xrange(1, len(runargs)):
50 if runargs[i].startswith('--cwd='):
54 if runargs[i].startswith('--cwd='):
51 del runargs[i]
55 del runargs[i]
52 break
56 break
53 elif runargs[i].startswith('--cwd'):
57 elif runargs[i].startswith('--cwd'):
54 del runargs[i:i + 2]
58 del runargs[i:i + 2]
55 break
59 break
56 def condfn():
60 def condfn():
57 return not os.path.exists(lockpath)
61 return not os.path.exists(lockpath)
58 pid = util.rundetached(runargs, condfn)
62 pid = util.rundetached(runargs, condfn)
59 if pid < 0:
63 if pid < 0:
60 raise error.Abort(_('child process failed to start'))
64 raise error.Abort(_('child process failed to start'))
61 writepid(pid)
65 writepid(pid)
62 finally:
66 finally:
63 util.tryunlink(lockpath)
67 util.tryunlink(lockpath)
64 if parentfn:
68 if parentfn:
65 return parentfn(pid)
69 return parentfn(pid)
66 else:
70 else:
67 return
71 return
68
72
69 if initfn:
73 if initfn:
70 initfn()
74 initfn()
71
75
72 if not opts['daemon']:
76 if not opts['daemon']:
73 writepid(util.getpid())
77 writepid(util.getpid())
74
78
75 if opts['daemon_postexec']:
79 if opts['daemon_postexec']:
76 try:
80 try:
77 os.setsid()
81 os.setsid()
78 except AttributeError:
82 except AttributeError:
79 pass
83 pass
80 for inst in opts['daemon_postexec']:
84 for inst in opts['daemon_postexec']:
81 if inst.startswith('unlink:'):
85 if inst.startswith('unlink:'):
82 lockpath = inst[7:]
86 lockpath = inst[7:]
83 os.unlink(lockpath)
87 os.unlink(lockpath)
84 elif inst.startswith('chdir:'):
88 elif inst.startswith('chdir:'):
85 os.chdir(inst[6:])
89 os.chdir(inst[6:])
86 elif inst != 'none':
90 elif inst != 'none':
87 raise error.Abort(_('invalid value for --daemon-postexec: %s')
91 raise error.Abort(_('invalid value for --daemon-postexec: %s')
88 % inst)
92 % inst)
89 util.hidewindow()
93 util.hidewindow()
90 util.stdout.flush()
94 procutil.stdout.flush()
91 util.stderr.flush()
95 procutil.stderr.flush()
92
96
93 nullfd = os.open(os.devnull, os.O_RDWR)
97 nullfd = os.open(os.devnull, os.O_RDWR)
94 logfilefd = nullfd
98 logfilefd = nullfd
95 if logfile:
99 if logfile:
96 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND,
100 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND,
97 0o666)
101 0o666)
98 os.dup2(nullfd, 0)
102 os.dup2(nullfd, 0)
99 os.dup2(logfilefd, 1)
103 os.dup2(logfilefd, 1)
100 os.dup2(logfilefd, 2)
104 os.dup2(logfilefd, 2)
101 if nullfd not in (0, 1, 2):
105 if nullfd not in (0, 1, 2):
102 os.close(nullfd)
106 os.close(nullfd)
103 if logfile and logfilefd not in (0, 1, 2):
107 if logfile and logfilefd not in (0, 1, 2):
104 os.close(logfilefd)
108 os.close(logfilefd)
105
109
106 if runfn:
110 if runfn:
107 return runfn()
111 return runfn()
108
112
109 _cmdservicemap = {
113 _cmdservicemap = {
110 'chgunix': chgserver.chgunixservice,
114 'chgunix': chgserver.chgunixservice,
111 'pipe': commandserver.pipeservice,
115 'pipe': commandserver.pipeservice,
112 'unix': commandserver.unixforkingservice,
116 'unix': commandserver.unixforkingservice,
113 }
117 }
114
118
115 def _createcmdservice(ui, repo, opts):
119 def _createcmdservice(ui, repo, opts):
116 mode = opts['cmdserver']
120 mode = opts['cmdserver']
117 try:
121 try:
118 return _cmdservicemap[mode](ui, repo, opts)
122 return _cmdservicemap[mode](ui, repo, opts)
119 except KeyError:
123 except KeyError:
120 raise error.Abort(_('unknown mode %s') % mode)
124 raise error.Abort(_('unknown mode %s') % mode)
121
125
122 def _createhgwebservice(ui, repo, opts):
126 def _createhgwebservice(ui, repo, opts):
123 # this way we can check if something was given in the command-line
127 # this way we can check if something was given in the command-line
124 if opts.get('port'):
128 if opts.get('port'):
125 opts['port'] = util.getport(opts.get('port'))
129 opts['port'] = util.getport(opts.get('port'))
126
130
127 alluis = {ui}
131 alluis = {ui}
128 if repo:
132 if repo:
129 baseui = repo.baseui
133 baseui = repo.baseui
130 alluis.update([repo.baseui, repo.ui])
134 alluis.update([repo.baseui, repo.ui])
131 else:
135 else:
132 baseui = ui
136 baseui = ui
133 webconf = opts.get('web_conf') or opts.get('webdir_conf')
137 webconf = opts.get('web_conf') or opts.get('webdir_conf')
134 if webconf:
138 if webconf:
135 if opts.get('subrepos'):
139 if opts.get('subrepos'):
136 raise error.Abort(_('--web-conf cannot be used with --subrepos'))
140 raise error.Abort(_('--web-conf cannot be used with --subrepos'))
137
141
138 # load server settings (e.g. web.port) to "copied" ui, which allows
142 # load server settings (e.g. web.port) to "copied" ui, which allows
139 # hgwebdir to reload webconf cleanly
143 # hgwebdir to reload webconf cleanly
140 servui = ui.copy()
144 servui = ui.copy()
141 servui.readconfig(webconf, sections=['web'])
145 servui.readconfig(webconf, sections=['web'])
142 alluis.add(servui)
146 alluis.add(servui)
143 elif opts.get('subrepos'):
147 elif opts.get('subrepos'):
144 servui = ui
148 servui = ui
145
149
146 # If repo is None, hgweb.createapp() already raises a proper abort
150 # If repo is None, hgweb.createapp() already raises a proper abort
147 # message as long as webconf is None.
151 # message as long as webconf is None.
148 if repo:
152 if repo:
149 webconf = dict()
153 webconf = dict()
150 cmdutil.addwebdirpath(repo, "", webconf)
154 cmdutil.addwebdirpath(repo, "", webconf)
151 else:
155 else:
152 servui = ui
156 servui = ui
153
157
154 optlist = ("name templates style address port prefix ipv6"
158 optlist = ("name templates style address port prefix ipv6"
155 " accesslog errorlog certificate encoding")
159 " accesslog errorlog certificate encoding")
156 for o in optlist.split():
160 for o in optlist.split():
157 val = opts.get(o, '')
161 val = opts.get(o, '')
158 if val in (None, ''): # should check against default options instead
162 if val in (None, ''): # should check against default options instead
159 continue
163 continue
160 for u in alluis:
164 for u in alluis:
161 u.setconfig("web", o, val, 'serve')
165 u.setconfig("web", o, val, 'serve')
162
166
163 app = hgweb.createapp(baseui, repo, webconf)
167 app = hgweb.createapp(baseui, repo, webconf)
164 return hgweb.httpservice(servui, app, opts)
168 return hgweb.httpservice(servui, app, opts)
165
169
166 def createservice(ui, repo, opts):
170 def createservice(ui, repo, opts):
167 if opts["cmdserver"]:
171 if opts["cmdserver"]:
168 return _createcmdservice(ui, repo, opts)
172 return _createcmdservice(ui, repo, opts)
169 else:
173 else:
170 return _createhgwebservice(ui, repo, opts)
174 return _createhgwebservice(ui, repo, opts)
@@ -1,1872 +1,1873 b''
1 # ui.py - user interface bits for mercurial
1 # ui.py - user interface bits 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
8 from __future__ import absolute_import
9
9
10 import collections
10 import collections
11 import contextlib
11 import contextlib
12 import errno
12 import errno
13 import getpass
13 import getpass
14 import inspect
14 import inspect
15 import os
15 import os
16 import re
16 import re
17 import signal
17 import signal
18 import socket
18 import socket
19 import subprocess
19 import subprocess
20 import sys
20 import sys
21 import tempfile
21 import tempfile
22 import traceback
22 import traceback
23
23
24 from .i18n import _
24 from .i18n import _
25 from .node import hex
25 from .node import hex
26
26
27 from . import (
27 from . import (
28 color,
28 color,
29 config,
29 config,
30 configitems,
30 configitems,
31 encoding,
31 encoding,
32 error,
32 error,
33 formatter,
33 formatter,
34 progress,
34 progress,
35 pycompat,
35 pycompat,
36 rcutil,
36 rcutil,
37 scmutil,
37 scmutil,
38 util,
38 util,
39 )
39 )
40 from .utils import (
40 from .utils import (
41 dateutil,
41 dateutil,
42 procutil,
42 stringutil,
43 stringutil,
43 )
44 )
44
45
45 urlreq = util.urlreq
46 urlreq = util.urlreq
46
47
47 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
48 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
48 _keepalnum = ''.join(c for c in map(pycompat.bytechr, range(256))
49 _keepalnum = ''.join(c for c in map(pycompat.bytechr, range(256))
49 if not c.isalnum())
50 if not c.isalnum())
50
51
51 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
52 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
52 tweakrc = b"""
53 tweakrc = b"""
53 [ui]
54 [ui]
54 # The rollback command is dangerous. As a rule, don't use it.
55 # The rollback command is dangerous. As a rule, don't use it.
55 rollback = False
56 rollback = False
56 # Make `hg status` report copy information
57 # Make `hg status` report copy information
57 statuscopies = yes
58 statuscopies = yes
58 # Prefer curses UIs when available. Revert to plain-text with `text`.
59 # Prefer curses UIs when available. Revert to plain-text with `text`.
59 interface = curses
60 interface = curses
60
61
61 [commands]
62 [commands]
62 # Make `hg status` emit cwd-relative paths by default.
63 # Make `hg status` emit cwd-relative paths by default.
63 status.relative = yes
64 status.relative = yes
64 # Refuse to perform an `hg update` that would cause a file content merge
65 # Refuse to perform an `hg update` that would cause a file content merge
65 update.check = noconflict
66 update.check = noconflict
66 # Show conflicts information in `hg status`
67 # Show conflicts information in `hg status`
67 status.verbose = True
68 status.verbose = True
68 # Skip the bisect state in conflicts information in `hg status`
69 # Skip the bisect state in conflicts information in `hg status`
69 status.skipstates = bisect
70 status.skipstates = bisect
70
71
71 [diff]
72 [diff]
72 git = 1
73 git = 1
73 showfunc = 1
74 showfunc = 1
74 """
75 """
75
76
76 samplehgrcs = {
77 samplehgrcs = {
77 'user':
78 'user':
78 b"""# example user config (see 'hg help config' for more info)
79 b"""# example user config (see 'hg help config' for more info)
79 [ui]
80 [ui]
80 # name and email, e.g.
81 # name and email, e.g.
81 # username = Jane Doe <jdoe@example.com>
82 # username = Jane Doe <jdoe@example.com>
82 username =
83 username =
83
84
84 # We recommend enabling tweakdefaults to get slight improvements to
85 # We recommend enabling tweakdefaults to get slight improvements to
85 # the UI over time. Make sure to set HGPLAIN in the environment when
86 # the UI over time. Make sure to set HGPLAIN in the environment when
86 # writing scripts!
87 # writing scripts!
87 # tweakdefaults = True
88 # tweakdefaults = True
88
89
89 # uncomment to disable color in command output
90 # uncomment to disable color in command output
90 # (see 'hg help color' for details)
91 # (see 'hg help color' for details)
91 # color = never
92 # color = never
92
93
93 # uncomment to disable command output pagination
94 # uncomment to disable command output pagination
94 # (see 'hg help pager' for details)
95 # (see 'hg help pager' for details)
95 # paginate = never
96 # paginate = never
96
97
97 [extensions]
98 [extensions]
98 # uncomment these lines to enable some popular extensions
99 # uncomment these lines to enable some popular extensions
99 # (see 'hg help extensions' for more info)
100 # (see 'hg help extensions' for more info)
100 #
101 #
101 # churn =
102 # churn =
102 """,
103 """,
103
104
104 'cloned':
105 'cloned':
105 b"""# example repository config (see 'hg help config' for more info)
106 b"""# example repository config (see 'hg help config' for more info)
106 [paths]
107 [paths]
107 default = %s
108 default = %s
108
109
109 # path aliases to other clones of this repo in URLs or filesystem paths
110 # path aliases to other clones of this repo in URLs or filesystem paths
110 # (see 'hg help config.paths' for more info)
111 # (see 'hg help config.paths' for more info)
111 #
112 #
112 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
113 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
113 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
114 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
114 # my-clone = /home/jdoe/jdoes-clone
115 # my-clone = /home/jdoe/jdoes-clone
115
116
116 [ui]
117 [ui]
117 # name and email (local to this repository, optional), e.g.
118 # name and email (local to this repository, optional), e.g.
118 # username = Jane Doe <jdoe@example.com>
119 # username = Jane Doe <jdoe@example.com>
119 """,
120 """,
120
121
121 'local':
122 'local':
122 b"""# example repository config (see 'hg help config' for more info)
123 b"""# example repository config (see 'hg help config' for more info)
123 [paths]
124 [paths]
124 # path aliases to other clones of this repo in URLs or filesystem paths
125 # path aliases to other clones of this repo in URLs or filesystem paths
125 # (see 'hg help config.paths' for more info)
126 # (see 'hg help config.paths' for more info)
126 #
127 #
127 # default = http://example.com/hg/example-repo
128 # default = http://example.com/hg/example-repo
128 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
129 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
129 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
130 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
130 # my-clone = /home/jdoe/jdoes-clone
131 # my-clone = /home/jdoe/jdoes-clone
131
132
132 [ui]
133 [ui]
133 # name and email (local to this repository, optional), e.g.
134 # name and email (local to this repository, optional), e.g.
134 # username = Jane Doe <jdoe@example.com>
135 # username = Jane Doe <jdoe@example.com>
135 """,
136 """,
136
137
137 'global':
138 'global':
138 b"""# example system-wide hg config (see 'hg help config' for more info)
139 b"""# example system-wide hg config (see 'hg help config' for more info)
139
140
140 [ui]
141 [ui]
141 # uncomment to disable color in command output
142 # uncomment to disable color in command output
142 # (see 'hg help color' for details)
143 # (see 'hg help color' for details)
143 # color = never
144 # color = never
144
145
145 # uncomment to disable command output pagination
146 # uncomment to disable command output pagination
146 # (see 'hg help pager' for details)
147 # (see 'hg help pager' for details)
147 # paginate = never
148 # paginate = never
148
149
149 [extensions]
150 [extensions]
150 # uncomment these lines to enable some popular extensions
151 # uncomment these lines to enable some popular extensions
151 # (see 'hg help extensions' for more info)
152 # (see 'hg help extensions' for more info)
152 #
153 #
153 # blackbox =
154 # blackbox =
154 # churn =
155 # churn =
155 """,
156 """,
156 }
157 }
157
158
158 def _maybestrurl(maybebytes):
159 def _maybestrurl(maybebytes):
159 return util.rapply(pycompat.strurl, maybebytes)
160 return util.rapply(pycompat.strurl, maybebytes)
160
161
161 def _maybebytesurl(maybestr):
162 def _maybebytesurl(maybestr):
162 return util.rapply(pycompat.bytesurl, maybestr)
163 return util.rapply(pycompat.bytesurl, maybestr)
163
164
164 class httppasswordmgrdbproxy(object):
165 class httppasswordmgrdbproxy(object):
165 """Delays loading urllib2 until it's needed."""
166 """Delays loading urllib2 until it's needed."""
166 def __init__(self):
167 def __init__(self):
167 self._mgr = None
168 self._mgr = None
168
169
169 def _get_mgr(self):
170 def _get_mgr(self):
170 if self._mgr is None:
171 if self._mgr is None:
171 self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
172 self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
172 return self._mgr
173 return self._mgr
173
174
174 def add_password(self, realm, uris, user, passwd):
175 def add_password(self, realm, uris, user, passwd):
175 return self._get_mgr().add_password(
176 return self._get_mgr().add_password(
176 _maybestrurl(realm), _maybestrurl(uris),
177 _maybestrurl(realm), _maybestrurl(uris),
177 _maybestrurl(user), _maybestrurl(passwd))
178 _maybestrurl(user), _maybestrurl(passwd))
178
179
179 def find_user_password(self, realm, uri):
180 def find_user_password(self, realm, uri):
180 mgr = self._get_mgr()
181 mgr = self._get_mgr()
181 return _maybebytesurl(mgr.find_user_password(_maybestrurl(realm),
182 return _maybebytesurl(mgr.find_user_password(_maybestrurl(realm),
182 _maybestrurl(uri)))
183 _maybestrurl(uri)))
183
184
184 def _catchterm(*args):
185 def _catchterm(*args):
185 raise error.SignalInterrupt
186 raise error.SignalInterrupt
186
187
187 # unique object used to detect no default value has been provided when
188 # unique object used to detect no default value has been provided when
188 # retrieving configuration value.
189 # retrieving configuration value.
189 _unset = object()
190 _unset = object()
190
191
191 # _reqexithandlers: callbacks run at the end of a request
192 # _reqexithandlers: callbacks run at the end of a request
192 _reqexithandlers = []
193 _reqexithandlers = []
193
194
194 class ui(object):
195 class ui(object):
195 def __init__(self, src=None):
196 def __init__(self, src=None):
196 """Create a fresh new ui object if no src given
197 """Create a fresh new ui object if no src given
197
198
198 Use uimod.ui.load() to create a ui which knows global and user configs.
199 Use uimod.ui.load() to create a ui which knows global and user configs.
199 In most cases, you should use ui.copy() to create a copy of an existing
200 In most cases, you should use ui.copy() to create a copy of an existing
200 ui object.
201 ui object.
201 """
202 """
202 # _buffers: used for temporary capture of output
203 # _buffers: used for temporary capture of output
203 self._buffers = []
204 self._buffers = []
204 # 3-tuple describing how each buffer in the stack behaves.
205 # 3-tuple describing how each buffer in the stack behaves.
205 # Values are (capture stderr, capture subprocesses, apply labels).
206 # Values are (capture stderr, capture subprocesses, apply labels).
206 self._bufferstates = []
207 self._bufferstates = []
207 # When a buffer is active, defines whether we are expanding labels.
208 # When a buffer is active, defines whether we are expanding labels.
208 # This exists to prevent an extra list lookup.
209 # This exists to prevent an extra list lookup.
209 self._bufferapplylabels = None
210 self._bufferapplylabels = None
210 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
211 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
211 self._reportuntrusted = True
212 self._reportuntrusted = True
212 self._knownconfig = configitems.coreitems
213 self._knownconfig = configitems.coreitems
213 self._ocfg = config.config() # overlay
214 self._ocfg = config.config() # overlay
214 self._tcfg = config.config() # trusted
215 self._tcfg = config.config() # trusted
215 self._ucfg = config.config() # untrusted
216 self._ucfg = config.config() # untrusted
216 self._trustusers = set()
217 self._trustusers = set()
217 self._trustgroups = set()
218 self._trustgroups = set()
218 self.callhooks = True
219 self.callhooks = True
219 # Insecure server connections requested.
220 # Insecure server connections requested.
220 self.insecureconnections = False
221 self.insecureconnections = False
221 # Blocked time
222 # Blocked time
222 self.logblockedtimes = False
223 self.logblockedtimes = False
223 # color mode: see mercurial/color.py for possible value
224 # color mode: see mercurial/color.py for possible value
224 self._colormode = None
225 self._colormode = None
225 self._terminfoparams = {}
226 self._terminfoparams = {}
226 self._styles = {}
227 self._styles = {}
227
228
228 if src:
229 if src:
229 self.fout = src.fout
230 self.fout = src.fout
230 self.ferr = src.ferr
231 self.ferr = src.ferr
231 self.fin = src.fin
232 self.fin = src.fin
232 self.pageractive = src.pageractive
233 self.pageractive = src.pageractive
233 self._disablepager = src._disablepager
234 self._disablepager = src._disablepager
234 self._tweaked = src._tweaked
235 self._tweaked = src._tweaked
235
236
236 self._tcfg = src._tcfg.copy()
237 self._tcfg = src._tcfg.copy()
237 self._ucfg = src._ucfg.copy()
238 self._ucfg = src._ucfg.copy()
238 self._ocfg = src._ocfg.copy()
239 self._ocfg = src._ocfg.copy()
239 self._trustusers = src._trustusers.copy()
240 self._trustusers = src._trustusers.copy()
240 self._trustgroups = src._trustgroups.copy()
241 self._trustgroups = src._trustgroups.copy()
241 self.environ = src.environ
242 self.environ = src.environ
242 self.callhooks = src.callhooks
243 self.callhooks = src.callhooks
243 self.insecureconnections = src.insecureconnections
244 self.insecureconnections = src.insecureconnections
244 self._colormode = src._colormode
245 self._colormode = src._colormode
245 self._terminfoparams = src._terminfoparams.copy()
246 self._terminfoparams = src._terminfoparams.copy()
246 self._styles = src._styles.copy()
247 self._styles = src._styles.copy()
247
248
248 self.fixconfig()
249 self.fixconfig()
249
250
250 self.httppasswordmgrdb = src.httppasswordmgrdb
251 self.httppasswordmgrdb = src.httppasswordmgrdb
251 self._blockedtimes = src._blockedtimes
252 self._blockedtimes = src._blockedtimes
252 else:
253 else:
253 self.fout = util.stdout
254 self.fout = procutil.stdout
254 self.ferr = util.stderr
255 self.ferr = procutil.stderr
255 self.fin = util.stdin
256 self.fin = procutil.stdin
256 self.pageractive = False
257 self.pageractive = False
257 self._disablepager = False
258 self._disablepager = False
258 self._tweaked = False
259 self._tweaked = False
259
260
260 # shared read-only environment
261 # shared read-only environment
261 self.environ = encoding.environ
262 self.environ = encoding.environ
262
263
263 self.httppasswordmgrdb = httppasswordmgrdbproxy()
264 self.httppasswordmgrdb = httppasswordmgrdbproxy()
264 self._blockedtimes = collections.defaultdict(int)
265 self._blockedtimes = collections.defaultdict(int)
265
266
266 allowed = self.configlist('experimental', 'exportableenviron')
267 allowed = self.configlist('experimental', 'exportableenviron')
267 if '*' in allowed:
268 if '*' in allowed:
268 self._exportableenviron = self.environ
269 self._exportableenviron = self.environ
269 else:
270 else:
270 self._exportableenviron = {}
271 self._exportableenviron = {}
271 for k in allowed:
272 for k in allowed:
272 if k in self.environ:
273 if k in self.environ:
273 self._exportableenviron[k] = self.environ[k]
274 self._exportableenviron[k] = self.environ[k]
274
275
275 @classmethod
276 @classmethod
276 def load(cls):
277 def load(cls):
277 """Create a ui and load global and user configs"""
278 """Create a ui and load global and user configs"""
278 u = cls()
279 u = cls()
279 # we always trust global config files and environment variables
280 # we always trust global config files and environment variables
280 for t, f in rcutil.rccomponents():
281 for t, f in rcutil.rccomponents():
281 if t == 'path':
282 if t == 'path':
282 u.readconfig(f, trust=True)
283 u.readconfig(f, trust=True)
283 elif t == 'items':
284 elif t == 'items':
284 sections = set()
285 sections = set()
285 for section, name, value, source in f:
286 for section, name, value, source in f:
286 # do not set u._ocfg
287 # do not set u._ocfg
287 # XXX clean this up once immutable config object is a thing
288 # XXX clean this up once immutable config object is a thing
288 u._tcfg.set(section, name, value, source)
289 u._tcfg.set(section, name, value, source)
289 u._ucfg.set(section, name, value, source)
290 u._ucfg.set(section, name, value, source)
290 sections.add(section)
291 sections.add(section)
291 for section in sections:
292 for section in sections:
292 u.fixconfig(section=section)
293 u.fixconfig(section=section)
293 else:
294 else:
294 raise error.ProgrammingError('unknown rctype: %s' % t)
295 raise error.ProgrammingError('unknown rctype: %s' % t)
295 u._maybetweakdefaults()
296 u._maybetweakdefaults()
296 return u
297 return u
297
298
298 def _maybetweakdefaults(self):
299 def _maybetweakdefaults(self):
299 if not self.configbool('ui', 'tweakdefaults'):
300 if not self.configbool('ui', 'tweakdefaults'):
300 return
301 return
301 if self._tweaked or self.plain('tweakdefaults'):
302 if self._tweaked or self.plain('tweakdefaults'):
302 return
303 return
303
304
304 # Note: it is SUPER IMPORTANT that you set self._tweaked to
305 # Note: it is SUPER IMPORTANT that you set self._tweaked to
305 # True *before* any calls to setconfig(), otherwise you'll get
306 # True *before* any calls to setconfig(), otherwise you'll get
306 # infinite recursion between setconfig and this method.
307 # infinite recursion between setconfig and this method.
307 #
308 #
308 # TODO: We should extract an inner method in setconfig() to
309 # TODO: We should extract an inner method in setconfig() to
309 # avoid this weirdness.
310 # avoid this weirdness.
310 self._tweaked = True
311 self._tweaked = True
311 tmpcfg = config.config()
312 tmpcfg = config.config()
312 tmpcfg.parse('<tweakdefaults>', tweakrc)
313 tmpcfg.parse('<tweakdefaults>', tweakrc)
313 for section in tmpcfg:
314 for section in tmpcfg:
314 for name, value in tmpcfg.items(section):
315 for name, value in tmpcfg.items(section):
315 if not self.hasconfig(section, name):
316 if not self.hasconfig(section, name):
316 self.setconfig(section, name, value, "<tweakdefaults>")
317 self.setconfig(section, name, value, "<tweakdefaults>")
317
318
318 def copy(self):
319 def copy(self):
319 return self.__class__(self)
320 return self.__class__(self)
320
321
321 def resetstate(self):
322 def resetstate(self):
322 """Clear internal state that shouldn't persist across commands"""
323 """Clear internal state that shouldn't persist across commands"""
323 if self._progbar:
324 if self._progbar:
324 self._progbar.resetstate() # reset last-print time of progress bar
325 self._progbar.resetstate() # reset last-print time of progress bar
325 self.httppasswordmgrdb = httppasswordmgrdbproxy()
326 self.httppasswordmgrdb = httppasswordmgrdbproxy()
326
327
327 @contextlib.contextmanager
328 @contextlib.contextmanager
328 def timeblockedsection(self, key):
329 def timeblockedsection(self, key):
329 # this is open-coded below - search for timeblockedsection to find them
330 # this is open-coded below - search for timeblockedsection to find them
330 starttime = util.timer()
331 starttime = util.timer()
331 try:
332 try:
332 yield
333 yield
333 finally:
334 finally:
334 self._blockedtimes[key + '_blocked'] += \
335 self._blockedtimes[key + '_blocked'] += \
335 (util.timer() - starttime) * 1000
336 (util.timer() - starttime) * 1000
336
337
337 def formatter(self, topic, opts):
338 def formatter(self, topic, opts):
338 return formatter.formatter(self, self, topic, opts)
339 return formatter.formatter(self, self, topic, opts)
339
340
340 def _trusted(self, fp, f):
341 def _trusted(self, fp, f):
341 st = util.fstat(fp)
342 st = util.fstat(fp)
342 if util.isowner(st):
343 if util.isowner(st):
343 return True
344 return True
344
345
345 tusers, tgroups = self._trustusers, self._trustgroups
346 tusers, tgroups = self._trustusers, self._trustgroups
346 if '*' in tusers or '*' in tgroups:
347 if '*' in tusers or '*' in tgroups:
347 return True
348 return True
348
349
349 user = util.username(st.st_uid)
350 user = util.username(st.st_uid)
350 group = util.groupname(st.st_gid)
351 group = util.groupname(st.st_gid)
351 if user in tusers or group in tgroups or user == util.username():
352 if user in tusers or group in tgroups or user == util.username():
352 return True
353 return True
353
354
354 if self._reportuntrusted:
355 if self._reportuntrusted:
355 self.warn(_('not trusting file %s from untrusted '
356 self.warn(_('not trusting file %s from untrusted '
356 'user %s, group %s\n') % (f, user, group))
357 'user %s, group %s\n') % (f, user, group))
357 return False
358 return False
358
359
359 def readconfig(self, filename, root=None, trust=False,
360 def readconfig(self, filename, root=None, trust=False,
360 sections=None, remap=None):
361 sections=None, remap=None):
361 try:
362 try:
362 fp = open(filename, u'rb')
363 fp = open(filename, u'rb')
363 except IOError:
364 except IOError:
364 if not sections: # ignore unless we were looking for something
365 if not sections: # ignore unless we were looking for something
365 return
366 return
366 raise
367 raise
367
368
368 cfg = config.config()
369 cfg = config.config()
369 trusted = sections or trust or self._trusted(fp, filename)
370 trusted = sections or trust or self._trusted(fp, filename)
370
371
371 try:
372 try:
372 cfg.read(filename, fp, sections=sections, remap=remap)
373 cfg.read(filename, fp, sections=sections, remap=remap)
373 fp.close()
374 fp.close()
374 except error.ConfigError as inst:
375 except error.ConfigError as inst:
375 if trusted:
376 if trusted:
376 raise
377 raise
377 self.warn(_("ignored: %s\n") % stringutil.forcebytestr(inst))
378 self.warn(_("ignored: %s\n") % stringutil.forcebytestr(inst))
378
379
379 if self.plain():
380 if self.plain():
380 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
381 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
381 'logtemplate', 'statuscopies', 'style',
382 'logtemplate', 'statuscopies', 'style',
382 'traceback', 'verbose'):
383 'traceback', 'verbose'):
383 if k in cfg['ui']:
384 if k in cfg['ui']:
384 del cfg['ui'][k]
385 del cfg['ui'][k]
385 for k, v in cfg.items('defaults'):
386 for k, v in cfg.items('defaults'):
386 del cfg['defaults'][k]
387 del cfg['defaults'][k]
387 for k, v in cfg.items('commands'):
388 for k, v in cfg.items('commands'):
388 del cfg['commands'][k]
389 del cfg['commands'][k]
389 # Don't remove aliases from the configuration if in the exceptionlist
390 # Don't remove aliases from the configuration if in the exceptionlist
390 if self.plain('alias'):
391 if self.plain('alias'):
391 for k, v in cfg.items('alias'):
392 for k, v in cfg.items('alias'):
392 del cfg['alias'][k]
393 del cfg['alias'][k]
393 if self.plain('revsetalias'):
394 if self.plain('revsetalias'):
394 for k, v in cfg.items('revsetalias'):
395 for k, v in cfg.items('revsetalias'):
395 del cfg['revsetalias'][k]
396 del cfg['revsetalias'][k]
396 if self.plain('templatealias'):
397 if self.plain('templatealias'):
397 for k, v in cfg.items('templatealias'):
398 for k, v in cfg.items('templatealias'):
398 del cfg['templatealias'][k]
399 del cfg['templatealias'][k]
399
400
400 if trusted:
401 if trusted:
401 self._tcfg.update(cfg)
402 self._tcfg.update(cfg)
402 self._tcfg.update(self._ocfg)
403 self._tcfg.update(self._ocfg)
403 self._ucfg.update(cfg)
404 self._ucfg.update(cfg)
404 self._ucfg.update(self._ocfg)
405 self._ucfg.update(self._ocfg)
405
406
406 if root is None:
407 if root is None:
407 root = os.path.expanduser('~')
408 root = os.path.expanduser('~')
408 self.fixconfig(root=root)
409 self.fixconfig(root=root)
409
410
410 def fixconfig(self, root=None, section=None):
411 def fixconfig(self, root=None, section=None):
411 if section in (None, 'paths'):
412 if section in (None, 'paths'):
412 # expand vars and ~
413 # expand vars and ~
413 # translate paths relative to root (or home) into absolute paths
414 # translate paths relative to root (or home) into absolute paths
414 root = root or pycompat.getcwd()
415 root = root or pycompat.getcwd()
415 for c in self._tcfg, self._ucfg, self._ocfg:
416 for c in self._tcfg, self._ucfg, self._ocfg:
416 for n, p in c.items('paths'):
417 for n, p in c.items('paths'):
417 # Ignore sub-options.
418 # Ignore sub-options.
418 if ':' in n:
419 if ':' in n:
419 continue
420 continue
420 if not p:
421 if not p:
421 continue
422 continue
422 if '%%' in p:
423 if '%%' in p:
423 s = self.configsource('paths', n) or 'none'
424 s = self.configsource('paths', n) or 'none'
424 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
425 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
425 % (n, p, s))
426 % (n, p, s))
426 p = p.replace('%%', '%')
427 p = p.replace('%%', '%')
427 p = util.expandpath(p)
428 p = util.expandpath(p)
428 if not util.hasscheme(p) and not os.path.isabs(p):
429 if not util.hasscheme(p) and not os.path.isabs(p):
429 p = os.path.normpath(os.path.join(root, p))
430 p = os.path.normpath(os.path.join(root, p))
430 c.set("paths", n, p)
431 c.set("paths", n, p)
431
432
432 if section in (None, 'ui'):
433 if section in (None, 'ui'):
433 # update ui options
434 # update ui options
434 self.debugflag = self.configbool('ui', 'debug')
435 self.debugflag = self.configbool('ui', 'debug')
435 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
436 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
436 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
437 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
437 if self.verbose and self.quiet:
438 if self.verbose and self.quiet:
438 self.quiet = self.verbose = False
439 self.quiet = self.verbose = False
439 self._reportuntrusted = self.debugflag or self.configbool("ui",
440 self._reportuntrusted = self.debugflag or self.configbool("ui",
440 "report_untrusted")
441 "report_untrusted")
441 self.tracebackflag = self.configbool('ui', 'traceback')
442 self.tracebackflag = self.configbool('ui', 'traceback')
442 self.logblockedtimes = self.configbool('ui', 'logblockedtimes')
443 self.logblockedtimes = self.configbool('ui', 'logblockedtimes')
443
444
444 if section in (None, 'trusted'):
445 if section in (None, 'trusted'):
445 # update trust information
446 # update trust information
446 self._trustusers.update(self.configlist('trusted', 'users'))
447 self._trustusers.update(self.configlist('trusted', 'users'))
447 self._trustgroups.update(self.configlist('trusted', 'groups'))
448 self._trustgroups.update(self.configlist('trusted', 'groups'))
448
449
449 def backupconfig(self, section, item):
450 def backupconfig(self, section, item):
450 return (self._ocfg.backup(section, item),
451 return (self._ocfg.backup(section, item),
451 self._tcfg.backup(section, item),
452 self._tcfg.backup(section, item),
452 self._ucfg.backup(section, item),)
453 self._ucfg.backup(section, item),)
453 def restoreconfig(self, data):
454 def restoreconfig(self, data):
454 self._ocfg.restore(data[0])
455 self._ocfg.restore(data[0])
455 self._tcfg.restore(data[1])
456 self._tcfg.restore(data[1])
456 self._ucfg.restore(data[2])
457 self._ucfg.restore(data[2])
457
458
458 def setconfig(self, section, name, value, source=''):
459 def setconfig(self, section, name, value, source=''):
459 for cfg in (self._ocfg, self._tcfg, self._ucfg):
460 for cfg in (self._ocfg, self._tcfg, self._ucfg):
460 cfg.set(section, name, value, source)
461 cfg.set(section, name, value, source)
461 self.fixconfig(section=section)
462 self.fixconfig(section=section)
462 self._maybetweakdefaults()
463 self._maybetweakdefaults()
463
464
464 def _data(self, untrusted):
465 def _data(self, untrusted):
465 return untrusted and self._ucfg or self._tcfg
466 return untrusted and self._ucfg or self._tcfg
466
467
467 def configsource(self, section, name, untrusted=False):
468 def configsource(self, section, name, untrusted=False):
468 return self._data(untrusted).source(section, name)
469 return self._data(untrusted).source(section, name)
469
470
470 def config(self, section, name, default=_unset, untrusted=False):
471 def config(self, section, name, default=_unset, untrusted=False):
471 """return the plain string version of a config"""
472 """return the plain string version of a config"""
472 value = self._config(section, name, default=default,
473 value = self._config(section, name, default=default,
473 untrusted=untrusted)
474 untrusted=untrusted)
474 if value is _unset:
475 if value is _unset:
475 return None
476 return None
476 return value
477 return value
477
478
478 def _config(self, section, name, default=_unset, untrusted=False):
479 def _config(self, section, name, default=_unset, untrusted=False):
479 value = itemdefault = default
480 value = itemdefault = default
480 item = self._knownconfig.get(section, {}).get(name)
481 item = self._knownconfig.get(section, {}).get(name)
481 alternates = [(section, name)]
482 alternates = [(section, name)]
482
483
483 if item is not None:
484 if item is not None:
484 alternates.extend(item.alias)
485 alternates.extend(item.alias)
485 if callable(item.default):
486 if callable(item.default):
486 itemdefault = item.default()
487 itemdefault = item.default()
487 else:
488 else:
488 itemdefault = item.default
489 itemdefault = item.default
489 else:
490 else:
490 msg = ("accessing unregistered config item: '%s.%s'")
491 msg = ("accessing unregistered config item: '%s.%s'")
491 msg %= (section, name)
492 msg %= (section, name)
492 self.develwarn(msg, 2, 'warn-config-unknown')
493 self.develwarn(msg, 2, 'warn-config-unknown')
493
494
494 if default is _unset:
495 if default is _unset:
495 if item is None:
496 if item is None:
496 value = default
497 value = default
497 elif item.default is configitems.dynamicdefault:
498 elif item.default is configitems.dynamicdefault:
498 value = None
499 value = None
499 msg = "config item requires an explicit default value: '%s.%s'"
500 msg = "config item requires an explicit default value: '%s.%s'"
500 msg %= (section, name)
501 msg %= (section, name)
501 self.develwarn(msg, 2, 'warn-config-default')
502 self.develwarn(msg, 2, 'warn-config-default')
502 else:
503 else:
503 value = itemdefault
504 value = itemdefault
504 elif (item is not None
505 elif (item is not None
505 and item.default is not configitems.dynamicdefault
506 and item.default is not configitems.dynamicdefault
506 and default != itemdefault):
507 and default != itemdefault):
507 msg = ("specifying a mismatched default value for a registered "
508 msg = ("specifying a mismatched default value for a registered "
508 "config item: '%s.%s' '%s'")
509 "config item: '%s.%s' '%s'")
509 msg %= (section, name, pycompat.bytestr(default))
510 msg %= (section, name, pycompat.bytestr(default))
510 self.develwarn(msg, 2, 'warn-config-default')
511 self.develwarn(msg, 2, 'warn-config-default')
511
512
512 for s, n in alternates:
513 for s, n in alternates:
513 candidate = self._data(untrusted).get(s, n, None)
514 candidate = self._data(untrusted).get(s, n, None)
514 if candidate is not None:
515 if candidate is not None:
515 value = candidate
516 value = candidate
516 section = s
517 section = s
517 name = n
518 name = n
518 break
519 break
519
520
520 if self.debugflag and not untrusted and self._reportuntrusted:
521 if self.debugflag and not untrusted and self._reportuntrusted:
521 for s, n in alternates:
522 for s, n in alternates:
522 uvalue = self._ucfg.get(s, n)
523 uvalue = self._ucfg.get(s, n)
523 if uvalue is not None and uvalue != value:
524 if uvalue is not None and uvalue != value:
524 self.debug("ignoring untrusted configuration option "
525 self.debug("ignoring untrusted configuration option "
525 "%s.%s = %s\n" % (s, n, uvalue))
526 "%s.%s = %s\n" % (s, n, uvalue))
526 return value
527 return value
527
528
528 def configsuboptions(self, section, name, default=_unset, untrusted=False):
529 def configsuboptions(self, section, name, default=_unset, untrusted=False):
529 """Get a config option and all sub-options.
530 """Get a config option and all sub-options.
530
531
531 Some config options have sub-options that are declared with the
532 Some config options have sub-options that are declared with the
532 format "key:opt = value". This method is used to return the main
533 format "key:opt = value". This method is used to return the main
533 option and all its declared sub-options.
534 option and all its declared sub-options.
534
535
535 Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
536 Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
536 is a dict of defined sub-options where keys and values are strings.
537 is a dict of defined sub-options where keys and values are strings.
537 """
538 """
538 main = self.config(section, name, default, untrusted=untrusted)
539 main = self.config(section, name, default, untrusted=untrusted)
539 data = self._data(untrusted)
540 data = self._data(untrusted)
540 sub = {}
541 sub = {}
541 prefix = '%s:' % name
542 prefix = '%s:' % name
542 for k, v in data.items(section):
543 for k, v in data.items(section):
543 if k.startswith(prefix):
544 if k.startswith(prefix):
544 sub[k[len(prefix):]] = v
545 sub[k[len(prefix):]] = v
545
546
546 if self.debugflag and not untrusted and self._reportuntrusted:
547 if self.debugflag and not untrusted and self._reportuntrusted:
547 for k, v in sub.items():
548 for k, v in sub.items():
548 uvalue = self._ucfg.get(section, '%s:%s' % (name, k))
549 uvalue = self._ucfg.get(section, '%s:%s' % (name, k))
549 if uvalue is not None and uvalue != v:
550 if uvalue is not None and uvalue != v:
550 self.debug('ignoring untrusted configuration option '
551 self.debug('ignoring untrusted configuration option '
551 '%s:%s.%s = %s\n' % (section, name, k, uvalue))
552 '%s:%s.%s = %s\n' % (section, name, k, uvalue))
552
553
553 return main, sub
554 return main, sub
554
555
555 def configpath(self, section, name, default=_unset, untrusted=False):
556 def configpath(self, section, name, default=_unset, untrusted=False):
556 'get a path config item, expanded relative to repo root or config file'
557 'get a path config item, expanded relative to repo root or config file'
557 v = self.config(section, name, default, untrusted)
558 v = self.config(section, name, default, untrusted)
558 if v is None:
559 if v is None:
559 return None
560 return None
560 if not os.path.isabs(v) or "://" not in v:
561 if not os.path.isabs(v) or "://" not in v:
561 src = self.configsource(section, name, untrusted)
562 src = self.configsource(section, name, untrusted)
562 if ':' in src:
563 if ':' in src:
563 base = os.path.dirname(src.rsplit(':')[0])
564 base = os.path.dirname(src.rsplit(':')[0])
564 v = os.path.join(base, os.path.expanduser(v))
565 v = os.path.join(base, os.path.expanduser(v))
565 return v
566 return v
566
567
567 def configbool(self, section, name, default=_unset, untrusted=False):
568 def configbool(self, section, name, default=_unset, untrusted=False):
568 """parse a configuration element as a boolean
569 """parse a configuration element as a boolean
569
570
570 >>> u = ui(); s = b'foo'
571 >>> u = ui(); s = b'foo'
571 >>> u.setconfig(s, b'true', b'yes')
572 >>> u.setconfig(s, b'true', b'yes')
572 >>> u.configbool(s, b'true')
573 >>> u.configbool(s, b'true')
573 True
574 True
574 >>> u.setconfig(s, b'false', b'no')
575 >>> u.setconfig(s, b'false', b'no')
575 >>> u.configbool(s, b'false')
576 >>> u.configbool(s, b'false')
576 False
577 False
577 >>> u.configbool(s, b'unknown')
578 >>> u.configbool(s, b'unknown')
578 False
579 False
579 >>> u.configbool(s, b'unknown', True)
580 >>> u.configbool(s, b'unknown', True)
580 True
581 True
581 >>> u.setconfig(s, b'invalid', b'somevalue')
582 >>> u.setconfig(s, b'invalid', b'somevalue')
582 >>> u.configbool(s, b'invalid')
583 >>> u.configbool(s, b'invalid')
583 Traceback (most recent call last):
584 Traceback (most recent call last):
584 ...
585 ...
585 ConfigError: foo.invalid is not a boolean ('somevalue')
586 ConfigError: foo.invalid is not a boolean ('somevalue')
586 """
587 """
587
588
588 v = self._config(section, name, default, untrusted=untrusted)
589 v = self._config(section, name, default, untrusted=untrusted)
589 if v is None:
590 if v is None:
590 return v
591 return v
591 if v is _unset:
592 if v is _unset:
592 if default is _unset:
593 if default is _unset:
593 return False
594 return False
594 return default
595 return default
595 if isinstance(v, bool):
596 if isinstance(v, bool):
596 return v
597 return v
597 b = stringutil.parsebool(v)
598 b = stringutil.parsebool(v)
598 if b is None:
599 if b is None:
599 raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
600 raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
600 % (section, name, v))
601 % (section, name, v))
601 return b
602 return b
602
603
603 def configwith(self, convert, section, name, default=_unset,
604 def configwith(self, convert, section, name, default=_unset,
604 desc=None, untrusted=False):
605 desc=None, untrusted=False):
605 """parse a configuration element with a conversion function
606 """parse a configuration element with a conversion function
606
607
607 >>> u = ui(); s = b'foo'
608 >>> u = ui(); s = b'foo'
608 >>> u.setconfig(s, b'float1', b'42')
609 >>> u.setconfig(s, b'float1', b'42')
609 >>> u.configwith(float, s, b'float1')
610 >>> u.configwith(float, s, b'float1')
610 42.0
611 42.0
611 >>> u.setconfig(s, b'float2', b'-4.25')
612 >>> u.setconfig(s, b'float2', b'-4.25')
612 >>> u.configwith(float, s, b'float2')
613 >>> u.configwith(float, s, b'float2')
613 -4.25
614 -4.25
614 >>> u.configwith(float, s, b'unknown', 7)
615 >>> u.configwith(float, s, b'unknown', 7)
615 7.0
616 7.0
616 >>> u.setconfig(s, b'invalid', b'somevalue')
617 >>> u.setconfig(s, b'invalid', b'somevalue')
617 >>> u.configwith(float, s, b'invalid')
618 >>> u.configwith(float, s, b'invalid')
618 Traceback (most recent call last):
619 Traceback (most recent call last):
619 ...
620 ...
620 ConfigError: foo.invalid is not a valid float ('somevalue')
621 ConfigError: foo.invalid is not a valid float ('somevalue')
621 >>> u.configwith(float, s, b'invalid', desc=b'womble')
622 >>> u.configwith(float, s, b'invalid', desc=b'womble')
622 Traceback (most recent call last):
623 Traceback (most recent call last):
623 ...
624 ...
624 ConfigError: foo.invalid is not a valid womble ('somevalue')
625 ConfigError: foo.invalid is not a valid womble ('somevalue')
625 """
626 """
626
627
627 v = self.config(section, name, default, untrusted)
628 v = self.config(section, name, default, untrusted)
628 if v is None:
629 if v is None:
629 return v # do not attempt to convert None
630 return v # do not attempt to convert None
630 try:
631 try:
631 return convert(v)
632 return convert(v)
632 except (ValueError, error.ParseError):
633 except (ValueError, error.ParseError):
633 if desc is None:
634 if desc is None:
634 desc = pycompat.sysbytes(convert.__name__)
635 desc = pycompat.sysbytes(convert.__name__)
635 raise error.ConfigError(_("%s.%s is not a valid %s ('%s')")
636 raise error.ConfigError(_("%s.%s is not a valid %s ('%s')")
636 % (section, name, desc, v))
637 % (section, name, desc, v))
637
638
638 def configint(self, section, name, default=_unset, untrusted=False):
639 def configint(self, section, name, default=_unset, untrusted=False):
639 """parse a configuration element as an integer
640 """parse a configuration element as an integer
640
641
641 >>> u = ui(); s = b'foo'
642 >>> u = ui(); s = b'foo'
642 >>> u.setconfig(s, b'int1', b'42')
643 >>> u.setconfig(s, b'int1', b'42')
643 >>> u.configint(s, b'int1')
644 >>> u.configint(s, b'int1')
644 42
645 42
645 >>> u.setconfig(s, b'int2', b'-42')
646 >>> u.setconfig(s, b'int2', b'-42')
646 >>> u.configint(s, b'int2')
647 >>> u.configint(s, b'int2')
647 -42
648 -42
648 >>> u.configint(s, b'unknown', 7)
649 >>> u.configint(s, b'unknown', 7)
649 7
650 7
650 >>> u.setconfig(s, b'invalid', b'somevalue')
651 >>> u.setconfig(s, b'invalid', b'somevalue')
651 >>> u.configint(s, b'invalid')
652 >>> u.configint(s, b'invalid')
652 Traceback (most recent call last):
653 Traceback (most recent call last):
653 ...
654 ...
654 ConfigError: foo.invalid is not a valid integer ('somevalue')
655 ConfigError: foo.invalid is not a valid integer ('somevalue')
655 """
656 """
656
657
657 return self.configwith(int, section, name, default, 'integer',
658 return self.configwith(int, section, name, default, 'integer',
658 untrusted)
659 untrusted)
659
660
660 def configbytes(self, section, name, default=_unset, untrusted=False):
661 def configbytes(self, section, name, default=_unset, untrusted=False):
661 """parse a configuration element as a quantity in bytes
662 """parse a configuration element as a quantity in bytes
662
663
663 Units can be specified as b (bytes), k or kb (kilobytes), m or
664 Units can be specified as b (bytes), k or kb (kilobytes), m or
664 mb (megabytes), g or gb (gigabytes).
665 mb (megabytes), g or gb (gigabytes).
665
666
666 >>> u = ui(); s = b'foo'
667 >>> u = ui(); s = b'foo'
667 >>> u.setconfig(s, b'val1', b'42')
668 >>> u.setconfig(s, b'val1', b'42')
668 >>> u.configbytes(s, b'val1')
669 >>> u.configbytes(s, b'val1')
669 42
670 42
670 >>> u.setconfig(s, b'val2', b'42.5 kb')
671 >>> u.setconfig(s, b'val2', b'42.5 kb')
671 >>> u.configbytes(s, b'val2')
672 >>> u.configbytes(s, b'val2')
672 43520
673 43520
673 >>> u.configbytes(s, b'unknown', b'7 MB')
674 >>> u.configbytes(s, b'unknown', b'7 MB')
674 7340032
675 7340032
675 >>> u.setconfig(s, b'invalid', b'somevalue')
676 >>> u.setconfig(s, b'invalid', b'somevalue')
676 >>> u.configbytes(s, b'invalid')
677 >>> u.configbytes(s, b'invalid')
677 Traceback (most recent call last):
678 Traceback (most recent call last):
678 ...
679 ...
679 ConfigError: foo.invalid is not a byte quantity ('somevalue')
680 ConfigError: foo.invalid is not a byte quantity ('somevalue')
680 """
681 """
681
682
682 value = self._config(section, name, default, untrusted)
683 value = self._config(section, name, default, untrusted)
683 if value is _unset:
684 if value is _unset:
684 if default is _unset:
685 if default is _unset:
685 default = 0
686 default = 0
686 value = default
687 value = default
687 if not isinstance(value, bytes):
688 if not isinstance(value, bytes):
688 return value
689 return value
689 try:
690 try:
690 return util.sizetoint(value)
691 return util.sizetoint(value)
691 except error.ParseError:
692 except error.ParseError:
692 raise error.ConfigError(_("%s.%s is not a byte quantity ('%s')")
693 raise error.ConfigError(_("%s.%s is not a byte quantity ('%s')")
693 % (section, name, value))
694 % (section, name, value))
694
695
695 def configlist(self, section, name, default=_unset, untrusted=False):
696 def configlist(self, section, name, default=_unset, untrusted=False):
696 """parse a configuration element as a list of comma/space separated
697 """parse a configuration element as a list of comma/space separated
697 strings
698 strings
698
699
699 >>> u = ui(); s = b'foo'
700 >>> u = ui(); s = b'foo'
700 >>> u.setconfig(s, b'list1', b'this,is "a small" ,test')
701 >>> u.setconfig(s, b'list1', b'this,is "a small" ,test')
701 >>> u.configlist(s, b'list1')
702 >>> u.configlist(s, b'list1')
702 ['this', 'is', 'a small', 'test']
703 ['this', 'is', 'a small', 'test']
703 >>> u.setconfig(s, b'list2', b'this, is "a small" , test ')
704 >>> u.setconfig(s, b'list2', b'this, is "a small" , test ')
704 >>> u.configlist(s, b'list2')
705 >>> u.configlist(s, b'list2')
705 ['this', 'is', 'a small', 'test']
706 ['this', 'is', 'a small', 'test']
706 """
707 """
707 # default is not always a list
708 # default is not always a list
708 v = self.configwith(config.parselist, section, name, default,
709 v = self.configwith(config.parselist, section, name, default,
709 'list', untrusted)
710 'list', untrusted)
710 if isinstance(v, bytes):
711 if isinstance(v, bytes):
711 return config.parselist(v)
712 return config.parselist(v)
712 elif v is None:
713 elif v is None:
713 return []
714 return []
714 return v
715 return v
715
716
716 def configdate(self, section, name, default=_unset, untrusted=False):
717 def configdate(self, section, name, default=_unset, untrusted=False):
717 """parse a configuration element as a tuple of ints
718 """parse a configuration element as a tuple of ints
718
719
719 >>> u = ui(); s = b'foo'
720 >>> u = ui(); s = b'foo'
720 >>> u.setconfig(s, b'date', b'0 0')
721 >>> u.setconfig(s, b'date', b'0 0')
721 >>> u.configdate(s, b'date')
722 >>> u.configdate(s, b'date')
722 (0, 0)
723 (0, 0)
723 """
724 """
724 if self.config(section, name, default, untrusted):
725 if self.config(section, name, default, untrusted):
725 return self.configwith(dateutil.parsedate, section, name, default,
726 return self.configwith(dateutil.parsedate, section, name, default,
726 'date', untrusted)
727 'date', untrusted)
727 if default is _unset:
728 if default is _unset:
728 return None
729 return None
729 return default
730 return default
730
731
731 def hasconfig(self, section, name, untrusted=False):
732 def hasconfig(self, section, name, untrusted=False):
732 return self._data(untrusted).hasitem(section, name)
733 return self._data(untrusted).hasitem(section, name)
733
734
734 def has_section(self, section, untrusted=False):
735 def has_section(self, section, untrusted=False):
735 '''tell whether section exists in config.'''
736 '''tell whether section exists in config.'''
736 return section in self._data(untrusted)
737 return section in self._data(untrusted)
737
738
738 def configitems(self, section, untrusted=False, ignoresub=False):
739 def configitems(self, section, untrusted=False, ignoresub=False):
739 items = self._data(untrusted).items(section)
740 items = self._data(untrusted).items(section)
740 if ignoresub:
741 if ignoresub:
741 newitems = {}
742 newitems = {}
742 for k, v in items:
743 for k, v in items:
743 if ':' not in k:
744 if ':' not in k:
744 newitems[k] = v
745 newitems[k] = v
745 items = list(newitems.iteritems())
746 items = list(newitems.iteritems())
746 if self.debugflag and not untrusted and self._reportuntrusted:
747 if self.debugflag and not untrusted and self._reportuntrusted:
747 for k, v in self._ucfg.items(section):
748 for k, v in self._ucfg.items(section):
748 if self._tcfg.get(section, k) != v:
749 if self._tcfg.get(section, k) != v:
749 self.debug("ignoring untrusted configuration option "
750 self.debug("ignoring untrusted configuration option "
750 "%s.%s = %s\n" % (section, k, v))
751 "%s.%s = %s\n" % (section, k, v))
751 return items
752 return items
752
753
753 def walkconfig(self, untrusted=False):
754 def walkconfig(self, untrusted=False):
754 cfg = self._data(untrusted)
755 cfg = self._data(untrusted)
755 for section in cfg.sections():
756 for section in cfg.sections():
756 for name, value in self.configitems(section, untrusted):
757 for name, value in self.configitems(section, untrusted):
757 yield section, name, value
758 yield section, name, value
758
759
759 def plain(self, feature=None):
760 def plain(self, feature=None):
760 '''is plain mode active?
761 '''is plain mode active?
761
762
762 Plain mode means that all configuration variables which affect
763 Plain mode means that all configuration variables which affect
763 the behavior and output of Mercurial should be
764 the behavior and output of Mercurial should be
764 ignored. Additionally, the output should be stable,
765 ignored. Additionally, the output should be stable,
765 reproducible and suitable for use in scripts or applications.
766 reproducible and suitable for use in scripts or applications.
766
767
767 The only way to trigger plain mode is by setting either the
768 The only way to trigger plain mode is by setting either the
768 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
769 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
769
770
770 The return value can either be
771 The return value can either be
771 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
772 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
772 - False if feature is disabled by default and not included in HGPLAIN
773 - False if feature is disabled by default and not included in HGPLAIN
773 - True otherwise
774 - True otherwise
774 '''
775 '''
775 if ('HGPLAIN' not in encoding.environ and
776 if ('HGPLAIN' not in encoding.environ and
776 'HGPLAINEXCEPT' not in encoding.environ):
777 'HGPLAINEXCEPT' not in encoding.environ):
777 return False
778 return False
778 exceptions = encoding.environ.get('HGPLAINEXCEPT',
779 exceptions = encoding.environ.get('HGPLAINEXCEPT',
779 '').strip().split(',')
780 '').strip().split(',')
780 # TODO: add support for HGPLAIN=+feature,-feature syntax
781 # TODO: add support for HGPLAIN=+feature,-feature syntax
781 if '+strictflags' not in encoding.environ.get('HGPLAIN', '').split(','):
782 if '+strictflags' not in encoding.environ.get('HGPLAIN', '').split(','):
782 exceptions.append('strictflags')
783 exceptions.append('strictflags')
783 if feature and exceptions:
784 if feature and exceptions:
784 return feature not in exceptions
785 return feature not in exceptions
785 return True
786 return True
786
787
787 def username(self, acceptempty=False):
788 def username(self, acceptempty=False):
788 """Return default username to be used in commits.
789 """Return default username to be used in commits.
789
790
790 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
791 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
791 and stop searching if one of these is set.
792 and stop searching if one of these is set.
792 If not found and acceptempty is True, returns None.
793 If not found and acceptempty is True, returns None.
793 If not found and ui.askusername is True, ask the user, else use
794 If not found and ui.askusername is True, ask the user, else use
794 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
795 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
795 If no username could be found, raise an Abort error.
796 If no username could be found, raise an Abort error.
796 """
797 """
797 user = encoding.environ.get("HGUSER")
798 user = encoding.environ.get("HGUSER")
798 if user is None:
799 if user is None:
799 user = self.config("ui", "username")
800 user = self.config("ui", "username")
800 if user is not None:
801 if user is not None:
801 user = os.path.expandvars(user)
802 user = os.path.expandvars(user)
802 if user is None:
803 if user is None:
803 user = encoding.environ.get("EMAIL")
804 user = encoding.environ.get("EMAIL")
804 if user is None and acceptempty:
805 if user is None and acceptempty:
805 return user
806 return user
806 if user is None and self.configbool("ui", "askusername"):
807 if user is None and self.configbool("ui", "askusername"):
807 user = self.prompt(_("enter a commit username:"), default=None)
808 user = self.prompt(_("enter a commit username:"), default=None)
808 if user is None and not self.interactive():
809 if user is None and not self.interactive():
809 try:
810 try:
810 user = '%s@%s' % (util.getuser(),
811 user = '%s@%s' % (util.getuser(),
811 encoding.strtolocal(socket.getfqdn()))
812 encoding.strtolocal(socket.getfqdn()))
812 self.warn(_("no username found, using '%s' instead\n") % user)
813 self.warn(_("no username found, using '%s' instead\n") % user)
813 except KeyError:
814 except KeyError:
814 pass
815 pass
815 if not user:
816 if not user:
816 raise error.Abort(_('no username supplied'),
817 raise error.Abort(_('no username supplied'),
817 hint=_("use 'hg config --edit' "
818 hint=_("use 'hg config --edit' "
818 'to set your username'))
819 'to set your username'))
819 if "\n" in user:
820 if "\n" in user:
820 raise error.Abort(_("username %r contains a newline\n")
821 raise error.Abort(_("username %r contains a newline\n")
821 % pycompat.bytestr(user))
822 % pycompat.bytestr(user))
822 return user
823 return user
823
824
824 def shortuser(self, user):
825 def shortuser(self, user):
825 """Return a short representation of a user name or email address."""
826 """Return a short representation of a user name or email address."""
826 if not self.verbose:
827 if not self.verbose:
827 user = stringutil.shortuser(user)
828 user = stringutil.shortuser(user)
828 return user
829 return user
829
830
830 def expandpath(self, loc, default=None):
831 def expandpath(self, loc, default=None):
831 """Return repository location relative to cwd or from [paths]"""
832 """Return repository location relative to cwd or from [paths]"""
832 try:
833 try:
833 p = self.paths.getpath(loc)
834 p = self.paths.getpath(loc)
834 if p:
835 if p:
835 return p.rawloc
836 return p.rawloc
836 except error.RepoError:
837 except error.RepoError:
837 pass
838 pass
838
839
839 if default:
840 if default:
840 try:
841 try:
841 p = self.paths.getpath(default)
842 p = self.paths.getpath(default)
842 if p:
843 if p:
843 return p.rawloc
844 return p.rawloc
844 except error.RepoError:
845 except error.RepoError:
845 pass
846 pass
846
847
847 return loc
848 return loc
848
849
849 @util.propertycache
850 @util.propertycache
850 def paths(self):
851 def paths(self):
851 return paths(self)
852 return paths(self)
852
853
853 def pushbuffer(self, error=False, subproc=False, labeled=False):
854 def pushbuffer(self, error=False, subproc=False, labeled=False):
854 """install a buffer to capture standard output of the ui object
855 """install a buffer to capture standard output of the ui object
855
856
856 If error is True, the error output will be captured too.
857 If error is True, the error output will be captured too.
857
858
858 If subproc is True, output from subprocesses (typically hooks) will be
859 If subproc is True, output from subprocesses (typically hooks) will be
859 captured too.
860 captured too.
860
861
861 If labeled is True, any labels associated with buffered
862 If labeled is True, any labels associated with buffered
862 output will be handled. By default, this has no effect
863 output will be handled. By default, this has no effect
863 on the output returned, but extensions and GUI tools may
864 on the output returned, but extensions and GUI tools may
864 handle this argument and returned styled output. If output
865 handle this argument and returned styled output. If output
865 is being buffered so it can be captured and parsed or
866 is being buffered so it can be captured and parsed or
866 processed, labeled should not be set to True.
867 processed, labeled should not be set to True.
867 """
868 """
868 self._buffers.append([])
869 self._buffers.append([])
869 self._bufferstates.append((error, subproc, labeled))
870 self._bufferstates.append((error, subproc, labeled))
870 self._bufferapplylabels = labeled
871 self._bufferapplylabels = labeled
871
872
872 def popbuffer(self):
873 def popbuffer(self):
873 '''pop the last buffer and return the buffered output'''
874 '''pop the last buffer and return the buffered output'''
874 self._bufferstates.pop()
875 self._bufferstates.pop()
875 if self._bufferstates:
876 if self._bufferstates:
876 self._bufferapplylabels = self._bufferstates[-1][2]
877 self._bufferapplylabels = self._bufferstates[-1][2]
877 else:
878 else:
878 self._bufferapplylabels = None
879 self._bufferapplylabels = None
879
880
880 return "".join(self._buffers.pop())
881 return "".join(self._buffers.pop())
881
882
882 def canwritewithoutlabels(self):
883 def canwritewithoutlabels(self):
883 '''check if write skips the label'''
884 '''check if write skips the label'''
884 if self._buffers and not self._bufferapplylabels:
885 if self._buffers and not self._bufferapplylabels:
885 return True
886 return True
886 return self._colormode is None
887 return self._colormode is None
887
888
888 def canbatchlabeledwrites(self):
889 def canbatchlabeledwrites(self):
889 '''check if write calls with labels are batchable'''
890 '''check if write calls with labels are batchable'''
890 # Windows color printing is special, see ``write``.
891 # Windows color printing is special, see ``write``.
891 return self._colormode != 'win32'
892 return self._colormode != 'win32'
892
893
893 def write(self, *args, **opts):
894 def write(self, *args, **opts):
894 '''write args to output
895 '''write args to output
895
896
896 By default, this method simply writes to the buffer or stdout.
897 By default, this method simply writes to the buffer or stdout.
897 Color mode can be set on the UI class to have the output decorated
898 Color mode can be set on the UI class to have the output decorated
898 with color modifier before being written to stdout.
899 with color modifier before being written to stdout.
899
900
900 The color used is controlled by an optional keyword argument, "label".
901 The color used is controlled by an optional keyword argument, "label".
901 This should be a string containing label names separated by space.
902 This should be a string containing label names separated by space.
902 Label names take the form of "topic.type". For example, ui.debug()
903 Label names take the form of "topic.type". For example, ui.debug()
903 issues a label of "ui.debug".
904 issues a label of "ui.debug".
904
905
905 When labeling output for a specific command, a label of
906 When labeling output for a specific command, a label of
906 "cmdname.type" is recommended. For example, status issues
907 "cmdname.type" is recommended. For example, status issues
907 a label of "status.modified" for modified files.
908 a label of "status.modified" for modified files.
908 '''
909 '''
909 if self._buffers:
910 if self._buffers:
910 if self._bufferapplylabels:
911 if self._bufferapplylabels:
911 label = opts.get(r'label', '')
912 label = opts.get(r'label', '')
912 self._buffers[-1].extend(self.label(a, label) for a in args)
913 self._buffers[-1].extend(self.label(a, label) for a in args)
913 else:
914 else:
914 self._buffers[-1].extend(args)
915 self._buffers[-1].extend(args)
915 else:
916 else:
916 self._writenobuf(*args, **opts)
917 self._writenobuf(*args, **opts)
917
918
918 def _writenobuf(self, *args, **opts):
919 def _writenobuf(self, *args, **opts):
919 if self._colormode == 'win32':
920 if self._colormode == 'win32':
920 # windows color printing is its own can of crab, defer to
921 # windows color printing is its own can of crab, defer to
921 # the color module and that is it.
922 # the color module and that is it.
922 color.win32print(self, self._write, *args, **opts)
923 color.win32print(self, self._write, *args, **opts)
923 else:
924 else:
924 msgs = args
925 msgs = args
925 if self._colormode is not None:
926 if self._colormode is not None:
926 label = opts.get(r'label', '')
927 label = opts.get(r'label', '')
927 msgs = [self.label(a, label) for a in args]
928 msgs = [self.label(a, label) for a in args]
928 self._write(*msgs, **opts)
929 self._write(*msgs, **opts)
929
930
930 def _write(self, *msgs, **opts):
931 def _write(self, *msgs, **opts):
931 self._progclear()
932 self._progclear()
932 # opencode timeblockedsection because this is a critical path
933 # opencode timeblockedsection because this is a critical path
933 starttime = util.timer()
934 starttime = util.timer()
934 try:
935 try:
935 self.fout.write(''.join(msgs))
936 self.fout.write(''.join(msgs))
936 except IOError as err:
937 except IOError as err:
937 raise error.StdioError(err)
938 raise error.StdioError(err)
938 finally:
939 finally:
939 self._blockedtimes['stdio_blocked'] += \
940 self._blockedtimes['stdio_blocked'] += \
940 (util.timer() - starttime) * 1000
941 (util.timer() - starttime) * 1000
941
942
942 def write_err(self, *args, **opts):
943 def write_err(self, *args, **opts):
943 self._progclear()
944 self._progclear()
944 if self._bufferstates and self._bufferstates[-1][0]:
945 if self._bufferstates and self._bufferstates[-1][0]:
945 self.write(*args, **opts)
946 self.write(*args, **opts)
946 elif self._colormode == 'win32':
947 elif self._colormode == 'win32':
947 # windows color printing is its own can of crab, defer to
948 # windows color printing is its own can of crab, defer to
948 # the color module and that is it.
949 # the color module and that is it.
949 color.win32print(self, self._write_err, *args, **opts)
950 color.win32print(self, self._write_err, *args, **opts)
950 else:
951 else:
951 msgs = args
952 msgs = args
952 if self._colormode is not None:
953 if self._colormode is not None:
953 label = opts.get(r'label', '')
954 label = opts.get(r'label', '')
954 msgs = [self.label(a, label) for a in args]
955 msgs = [self.label(a, label) for a in args]
955 self._write_err(*msgs, **opts)
956 self._write_err(*msgs, **opts)
956
957
957 def _write_err(self, *msgs, **opts):
958 def _write_err(self, *msgs, **opts):
958 try:
959 try:
959 with self.timeblockedsection('stdio'):
960 with self.timeblockedsection('stdio'):
960 if not getattr(self.fout, 'closed', False):
961 if not getattr(self.fout, 'closed', False):
961 self.fout.flush()
962 self.fout.flush()
962 for a in msgs:
963 for a in msgs:
963 self.ferr.write(a)
964 self.ferr.write(a)
964 # stderr may be buffered under win32 when redirected to files,
965 # stderr may be buffered under win32 when redirected to files,
965 # including stdout.
966 # including stdout.
966 if not getattr(self.ferr, 'closed', False):
967 if not getattr(self.ferr, 'closed', False):
967 self.ferr.flush()
968 self.ferr.flush()
968 except IOError as inst:
969 except IOError as inst:
969 if inst.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
970 if inst.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
970 raise error.StdioError(inst)
971 raise error.StdioError(inst)
971
972
972 def flush(self):
973 def flush(self):
973 # opencode timeblockedsection because this is a critical path
974 # opencode timeblockedsection because this is a critical path
974 starttime = util.timer()
975 starttime = util.timer()
975 try:
976 try:
976 try:
977 try:
977 self.fout.flush()
978 self.fout.flush()
978 except IOError as err:
979 except IOError as err:
979 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
980 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
980 raise error.StdioError(err)
981 raise error.StdioError(err)
981 finally:
982 finally:
982 try:
983 try:
983 self.ferr.flush()
984 self.ferr.flush()
984 except IOError as err:
985 except IOError as err:
985 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
986 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
986 raise error.StdioError(err)
987 raise error.StdioError(err)
987 finally:
988 finally:
988 self._blockedtimes['stdio_blocked'] += \
989 self._blockedtimes['stdio_blocked'] += \
989 (util.timer() - starttime) * 1000
990 (util.timer() - starttime) * 1000
990
991
991 def _isatty(self, fh):
992 def _isatty(self, fh):
992 if self.configbool('ui', 'nontty'):
993 if self.configbool('ui', 'nontty'):
993 return False
994 return False
994 return util.isatty(fh)
995 return util.isatty(fh)
995
996
996 def disablepager(self):
997 def disablepager(self):
997 self._disablepager = True
998 self._disablepager = True
998
999
999 def pager(self, command):
1000 def pager(self, command):
1000 """Start a pager for subsequent command output.
1001 """Start a pager for subsequent command output.
1001
1002
1002 Commands which produce a long stream of output should call
1003 Commands which produce a long stream of output should call
1003 this function to activate the user's preferred pagination
1004 this function to activate the user's preferred pagination
1004 mechanism (which may be no pager). Calling this function
1005 mechanism (which may be no pager). Calling this function
1005 precludes any future use of interactive functionality, such as
1006 precludes any future use of interactive functionality, such as
1006 prompting the user or activating curses.
1007 prompting the user or activating curses.
1007
1008
1008 Args:
1009 Args:
1009 command: The full, non-aliased name of the command. That is, "log"
1010 command: The full, non-aliased name of the command. That is, "log"
1010 not "history, "summary" not "summ", etc.
1011 not "history, "summary" not "summ", etc.
1011 """
1012 """
1012 if (self._disablepager
1013 if (self._disablepager
1013 or self.pageractive):
1014 or self.pageractive):
1014 # how pager should do is already determined
1015 # how pager should do is already determined
1015 return
1016 return
1016
1017
1017 if not command.startswith('internal-always-') and (
1018 if not command.startswith('internal-always-') and (
1018 # explicit --pager=on (= 'internal-always-' prefix) should
1019 # explicit --pager=on (= 'internal-always-' prefix) should
1019 # take precedence over disabling factors below
1020 # take precedence over disabling factors below
1020 command in self.configlist('pager', 'ignore')
1021 command in self.configlist('pager', 'ignore')
1021 or not self.configbool('ui', 'paginate')
1022 or not self.configbool('ui', 'paginate')
1022 or not self.configbool('pager', 'attend-' + command, True)
1023 or not self.configbool('pager', 'attend-' + command, True)
1023 # TODO: if we want to allow HGPLAINEXCEPT=pager,
1024 # TODO: if we want to allow HGPLAINEXCEPT=pager,
1024 # formatted() will need some adjustment.
1025 # formatted() will need some adjustment.
1025 or not self.formatted()
1026 or not self.formatted()
1026 or self.plain()
1027 or self.plain()
1027 or self._buffers
1028 or self._buffers
1028 # TODO: expose debugger-enabled on the UI object
1029 # TODO: expose debugger-enabled on the UI object
1029 or '--debugger' in pycompat.sysargv):
1030 or '--debugger' in pycompat.sysargv):
1030 # We only want to paginate if the ui appears to be
1031 # We only want to paginate if the ui appears to be
1031 # interactive, the user didn't say HGPLAIN or
1032 # interactive, the user didn't say HGPLAIN or
1032 # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
1033 # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
1033 return
1034 return
1034
1035
1035 pagercmd = self.config('pager', 'pager', rcutil.fallbackpager)
1036 pagercmd = self.config('pager', 'pager', rcutil.fallbackpager)
1036 if not pagercmd:
1037 if not pagercmd:
1037 return
1038 return
1038
1039
1039 pagerenv = {}
1040 pagerenv = {}
1040 for name, value in rcutil.defaultpagerenv().items():
1041 for name, value in rcutil.defaultpagerenv().items():
1041 if name not in encoding.environ:
1042 if name not in encoding.environ:
1042 pagerenv[name] = value
1043 pagerenv[name] = value
1043
1044
1044 self.debug('starting pager for command %r\n' % command)
1045 self.debug('starting pager for command %r\n' % command)
1045 self.flush()
1046 self.flush()
1046
1047
1047 wasformatted = self.formatted()
1048 wasformatted = self.formatted()
1048 if util.safehasattr(signal, "SIGPIPE"):
1049 if util.safehasattr(signal, "SIGPIPE"):
1049 signal.signal(signal.SIGPIPE, _catchterm)
1050 signal.signal(signal.SIGPIPE, _catchterm)
1050 if self._runpager(pagercmd, pagerenv):
1051 if self._runpager(pagercmd, pagerenv):
1051 self.pageractive = True
1052 self.pageractive = True
1052 # Preserve the formatted-ness of the UI. This is important
1053 # Preserve the formatted-ness of the UI. This is important
1053 # because we mess with stdout, which might confuse
1054 # because we mess with stdout, which might confuse
1054 # auto-detection of things being formatted.
1055 # auto-detection of things being formatted.
1055 self.setconfig('ui', 'formatted', wasformatted, 'pager')
1056 self.setconfig('ui', 'formatted', wasformatted, 'pager')
1056 self.setconfig('ui', 'interactive', False, 'pager')
1057 self.setconfig('ui', 'interactive', False, 'pager')
1057
1058
1058 # If pagermode differs from color.mode, reconfigure color now that
1059 # If pagermode differs from color.mode, reconfigure color now that
1059 # pageractive is set.
1060 # pageractive is set.
1060 cm = self._colormode
1061 cm = self._colormode
1061 if cm != self.config('color', 'pagermode', cm):
1062 if cm != self.config('color', 'pagermode', cm):
1062 color.setup(self)
1063 color.setup(self)
1063 else:
1064 else:
1064 # If the pager can't be spawned in dispatch when --pager=on is
1065 # If the pager can't be spawned in dispatch when --pager=on is
1065 # given, don't try again when the command runs, to avoid a duplicate
1066 # given, don't try again when the command runs, to avoid a duplicate
1066 # warning about a missing pager command.
1067 # warning about a missing pager command.
1067 self.disablepager()
1068 self.disablepager()
1068
1069
1069 def _runpager(self, command, env=None):
1070 def _runpager(self, command, env=None):
1070 """Actually start the pager and set up file descriptors.
1071 """Actually start the pager and set up file descriptors.
1071
1072
1072 This is separate in part so that extensions (like chg) can
1073 This is separate in part so that extensions (like chg) can
1073 override how a pager is invoked.
1074 override how a pager is invoked.
1074 """
1075 """
1075 if command == 'cat':
1076 if command == 'cat':
1076 # Save ourselves some work.
1077 # Save ourselves some work.
1077 return False
1078 return False
1078 # If the command doesn't contain any of these characters, we
1079 # If the command doesn't contain any of these characters, we
1079 # assume it's a binary and exec it directly. This means for
1080 # assume it's a binary and exec it directly. This means for
1080 # simple pager command configurations, we can degrade
1081 # simple pager command configurations, we can degrade
1081 # gracefully and tell the user about their broken pager.
1082 # gracefully and tell the user about their broken pager.
1082 shell = any(c in command for c in "|&;<>()$`\\\"' \t\n*?[#~=%")
1083 shell = any(c in command for c in "|&;<>()$`\\\"' \t\n*?[#~=%")
1083
1084
1084 if pycompat.iswindows and not shell:
1085 if pycompat.iswindows and not shell:
1085 # Window's built-in `more` cannot be invoked with shell=False, but
1086 # Window's built-in `more` cannot be invoked with shell=False, but
1086 # its `more.com` can. Hide this implementation detail from the
1087 # its `more.com` can. Hide this implementation detail from the
1087 # user so we can also get sane bad PAGER behavior. MSYS has
1088 # user so we can also get sane bad PAGER behavior. MSYS has
1088 # `more.exe`, so do a cmd.exe style resolution of the executable to
1089 # `more.exe`, so do a cmd.exe style resolution of the executable to
1089 # determine which one to use.
1090 # determine which one to use.
1090 fullcmd = util.findexe(command)
1091 fullcmd = util.findexe(command)
1091 if not fullcmd:
1092 if not fullcmd:
1092 self.warn(_("missing pager command '%s', skipping pager\n")
1093 self.warn(_("missing pager command '%s', skipping pager\n")
1093 % command)
1094 % command)
1094 return False
1095 return False
1095
1096
1096 command = fullcmd
1097 command = fullcmd
1097
1098
1098 try:
1099 try:
1099 pager = subprocess.Popen(
1100 pager = subprocess.Popen(
1100 command, shell=shell, bufsize=-1,
1101 command, shell=shell, bufsize=-1,
1101 close_fds=util.closefds, stdin=subprocess.PIPE,
1102 close_fds=util.closefds, stdin=subprocess.PIPE,
1102 stdout=util.stdout, stderr=util.stderr,
1103 stdout=procutil.stdout, stderr=procutil.stderr,
1103 env=util.shellenviron(env))
1104 env=util.shellenviron(env))
1104 except OSError as e:
1105 except OSError as e:
1105 if e.errno == errno.ENOENT and not shell:
1106 if e.errno == errno.ENOENT and not shell:
1106 self.warn(_("missing pager command '%s', skipping pager\n")
1107 self.warn(_("missing pager command '%s', skipping pager\n")
1107 % command)
1108 % command)
1108 return False
1109 return False
1109 raise
1110 raise
1110
1111
1111 # back up original file descriptors
1112 # back up original file descriptors
1112 stdoutfd = os.dup(util.stdout.fileno())
1113 stdoutfd = os.dup(procutil.stdout.fileno())
1113 stderrfd = os.dup(util.stderr.fileno())
1114 stderrfd = os.dup(procutil.stderr.fileno())
1114
1115
1115 os.dup2(pager.stdin.fileno(), util.stdout.fileno())
1116 os.dup2(pager.stdin.fileno(), procutil.stdout.fileno())
1116 if self._isatty(util.stderr):
1117 if self._isatty(procutil.stderr):
1117 os.dup2(pager.stdin.fileno(), util.stderr.fileno())
1118 os.dup2(pager.stdin.fileno(), procutil.stderr.fileno())
1118
1119
1119 @self.atexit
1120 @self.atexit
1120 def killpager():
1121 def killpager():
1121 if util.safehasattr(signal, "SIGINT"):
1122 if util.safehasattr(signal, "SIGINT"):
1122 signal.signal(signal.SIGINT, signal.SIG_IGN)
1123 signal.signal(signal.SIGINT, signal.SIG_IGN)
1123 # restore original fds, closing pager.stdin copies in the process
1124 # restore original fds, closing pager.stdin copies in the process
1124 os.dup2(stdoutfd, util.stdout.fileno())
1125 os.dup2(stdoutfd, procutil.stdout.fileno())
1125 os.dup2(stderrfd, util.stderr.fileno())
1126 os.dup2(stderrfd, procutil.stderr.fileno())
1126 pager.stdin.close()
1127 pager.stdin.close()
1127 pager.wait()
1128 pager.wait()
1128
1129
1129 return True
1130 return True
1130
1131
1131 @property
1132 @property
1132 def _exithandlers(self):
1133 def _exithandlers(self):
1133 return _reqexithandlers
1134 return _reqexithandlers
1134
1135
1135 def atexit(self, func, *args, **kwargs):
1136 def atexit(self, func, *args, **kwargs):
1136 '''register a function to run after dispatching a request
1137 '''register a function to run after dispatching a request
1137
1138
1138 Handlers do not stay registered across request boundaries.'''
1139 Handlers do not stay registered across request boundaries.'''
1139 self._exithandlers.append((func, args, kwargs))
1140 self._exithandlers.append((func, args, kwargs))
1140 return func
1141 return func
1141
1142
1142 def interface(self, feature):
1143 def interface(self, feature):
1143 """what interface to use for interactive console features?
1144 """what interface to use for interactive console features?
1144
1145
1145 The interface is controlled by the value of `ui.interface` but also by
1146 The interface is controlled by the value of `ui.interface` but also by
1146 the value of feature-specific configuration. For example:
1147 the value of feature-specific configuration. For example:
1147
1148
1148 ui.interface.histedit = text
1149 ui.interface.histedit = text
1149 ui.interface.chunkselector = curses
1150 ui.interface.chunkselector = curses
1150
1151
1151 Here the features are "histedit" and "chunkselector".
1152 Here the features are "histedit" and "chunkselector".
1152
1153
1153 The configuration above means that the default interfaces for commands
1154 The configuration above means that the default interfaces for commands
1154 is curses, the interface for histedit is text and the interface for
1155 is curses, the interface for histedit is text and the interface for
1155 selecting chunk is crecord (the best curses interface available).
1156 selecting chunk is crecord (the best curses interface available).
1156
1157
1157 Consider the following example:
1158 Consider the following example:
1158 ui.interface = curses
1159 ui.interface = curses
1159 ui.interface.histedit = text
1160 ui.interface.histedit = text
1160
1161
1161 Then histedit will use the text interface and chunkselector will use
1162 Then histedit will use the text interface and chunkselector will use
1162 the default curses interface (crecord at the moment).
1163 the default curses interface (crecord at the moment).
1163 """
1164 """
1164 alldefaults = frozenset(["text", "curses"])
1165 alldefaults = frozenset(["text", "curses"])
1165
1166
1166 featureinterfaces = {
1167 featureinterfaces = {
1167 "chunkselector": [
1168 "chunkselector": [
1168 "text",
1169 "text",
1169 "curses",
1170 "curses",
1170 ]
1171 ]
1171 }
1172 }
1172
1173
1173 # Feature-specific interface
1174 # Feature-specific interface
1174 if feature not in featureinterfaces.keys():
1175 if feature not in featureinterfaces.keys():
1175 # Programming error, not user error
1176 # Programming error, not user error
1176 raise ValueError("Unknown feature requested %s" % feature)
1177 raise ValueError("Unknown feature requested %s" % feature)
1177
1178
1178 availableinterfaces = frozenset(featureinterfaces[feature])
1179 availableinterfaces = frozenset(featureinterfaces[feature])
1179 if alldefaults > availableinterfaces:
1180 if alldefaults > availableinterfaces:
1180 # Programming error, not user error. We need a use case to
1181 # Programming error, not user error. We need a use case to
1181 # define the right thing to do here.
1182 # define the right thing to do here.
1182 raise ValueError(
1183 raise ValueError(
1183 "Feature %s does not handle all default interfaces" %
1184 "Feature %s does not handle all default interfaces" %
1184 feature)
1185 feature)
1185
1186
1186 if self.plain():
1187 if self.plain():
1187 return "text"
1188 return "text"
1188
1189
1189 # Default interface for all the features
1190 # Default interface for all the features
1190 defaultinterface = "text"
1191 defaultinterface = "text"
1191 i = self.config("ui", "interface")
1192 i = self.config("ui", "interface")
1192 if i in alldefaults:
1193 if i in alldefaults:
1193 defaultinterface = i
1194 defaultinterface = i
1194
1195
1195 choseninterface = defaultinterface
1196 choseninterface = defaultinterface
1196 f = self.config("ui", "interface.%s" % feature)
1197 f = self.config("ui", "interface.%s" % feature)
1197 if f in availableinterfaces:
1198 if f in availableinterfaces:
1198 choseninterface = f
1199 choseninterface = f
1199
1200
1200 if i is not None and defaultinterface != i:
1201 if i is not None and defaultinterface != i:
1201 if f is not None:
1202 if f is not None:
1202 self.warn(_("invalid value for ui.interface: %s\n") %
1203 self.warn(_("invalid value for ui.interface: %s\n") %
1203 (i,))
1204 (i,))
1204 else:
1205 else:
1205 self.warn(_("invalid value for ui.interface: %s (using %s)\n") %
1206 self.warn(_("invalid value for ui.interface: %s (using %s)\n") %
1206 (i, choseninterface))
1207 (i, choseninterface))
1207 if f is not None and choseninterface != f:
1208 if f is not None and choseninterface != f:
1208 self.warn(_("invalid value for ui.interface.%s: %s (using %s)\n") %
1209 self.warn(_("invalid value for ui.interface.%s: %s (using %s)\n") %
1209 (feature, f, choseninterface))
1210 (feature, f, choseninterface))
1210
1211
1211 return choseninterface
1212 return choseninterface
1212
1213
1213 def interactive(self):
1214 def interactive(self):
1214 '''is interactive input allowed?
1215 '''is interactive input allowed?
1215
1216
1216 An interactive session is a session where input can be reasonably read
1217 An interactive session is a session where input can be reasonably read
1217 from `sys.stdin'. If this function returns false, any attempt to read
1218 from `sys.stdin'. If this function returns false, any attempt to read
1218 from stdin should fail with an error, unless a sensible default has been
1219 from stdin should fail with an error, unless a sensible default has been
1219 specified.
1220 specified.
1220
1221
1221 Interactiveness is triggered by the value of the `ui.interactive'
1222 Interactiveness is triggered by the value of the `ui.interactive'
1222 configuration variable or - if it is unset - when `sys.stdin' points
1223 configuration variable or - if it is unset - when `sys.stdin' points
1223 to a terminal device.
1224 to a terminal device.
1224
1225
1225 This function refers to input only; for output, see `ui.formatted()'.
1226 This function refers to input only; for output, see `ui.formatted()'.
1226 '''
1227 '''
1227 i = self.configbool("ui", "interactive")
1228 i = self.configbool("ui", "interactive")
1228 if i is None:
1229 if i is None:
1229 # some environments replace stdin without implementing isatty
1230 # some environments replace stdin without implementing isatty
1230 # usually those are non-interactive
1231 # usually those are non-interactive
1231 return self._isatty(self.fin)
1232 return self._isatty(self.fin)
1232
1233
1233 return i
1234 return i
1234
1235
1235 def termwidth(self):
1236 def termwidth(self):
1236 '''how wide is the terminal in columns?
1237 '''how wide is the terminal in columns?
1237 '''
1238 '''
1238 if 'COLUMNS' in encoding.environ:
1239 if 'COLUMNS' in encoding.environ:
1239 try:
1240 try:
1240 return int(encoding.environ['COLUMNS'])
1241 return int(encoding.environ['COLUMNS'])
1241 except ValueError:
1242 except ValueError:
1242 pass
1243 pass
1243 return scmutil.termsize(self)[0]
1244 return scmutil.termsize(self)[0]
1244
1245
1245 def formatted(self):
1246 def formatted(self):
1246 '''should formatted output be used?
1247 '''should formatted output be used?
1247
1248
1248 It is often desirable to format the output to suite the output medium.
1249 It is often desirable to format the output to suite the output medium.
1249 Examples of this are truncating long lines or colorizing messages.
1250 Examples of this are truncating long lines or colorizing messages.
1250 However, this is not often not desirable when piping output into other
1251 However, this is not often not desirable when piping output into other
1251 utilities, e.g. `grep'.
1252 utilities, e.g. `grep'.
1252
1253
1253 Formatted output is triggered by the value of the `ui.formatted'
1254 Formatted output is triggered by the value of the `ui.formatted'
1254 configuration variable or - if it is unset - when `sys.stdout' points
1255 configuration variable or - if it is unset - when `sys.stdout' points
1255 to a terminal device. Please note that `ui.formatted' should be
1256 to a terminal device. Please note that `ui.formatted' should be
1256 considered an implementation detail; it is not intended for use outside
1257 considered an implementation detail; it is not intended for use outside
1257 Mercurial or its extensions.
1258 Mercurial or its extensions.
1258
1259
1259 This function refers to output only; for input, see `ui.interactive()'.
1260 This function refers to output only; for input, see `ui.interactive()'.
1260 This function always returns false when in plain mode, see `ui.plain()'.
1261 This function always returns false when in plain mode, see `ui.plain()'.
1261 '''
1262 '''
1262 if self.plain():
1263 if self.plain():
1263 return False
1264 return False
1264
1265
1265 i = self.configbool("ui", "formatted")
1266 i = self.configbool("ui", "formatted")
1266 if i is None:
1267 if i is None:
1267 # some environments replace stdout without implementing isatty
1268 # some environments replace stdout without implementing isatty
1268 # usually those are non-interactive
1269 # usually those are non-interactive
1269 return self._isatty(self.fout)
1270 return self._isatty(self.fout)
1270
1271
1271 return i
1272 return i
1272
1273
1273 def _readline(self):
1274 def _readline(self):
1274 # Replacing stdin/stdout temporarily is a hard problem on Python 3
1275 # Replacing stdin/stdout temporarily is a hard problem on Python 3
1275 # because they have to be text streams with *no buffering*. Instead,
1276 # because they have to be text streams with *no buffering*. Instead,
1276 # we use rawinput() only if call_readline() will be invoked by
1277 # we use rawinput() only if call_readline() will be invoked by
1277 # PyOS_Readline(), so no I/O will be made at Python layer.
1278 # PyOS_Readline(), so no I/O will be made at Python layer.
1278 usereadline = (self._isatty(self.fin) and self._isatty(self.fout)
1279 usereadline = (self._isatty(self.fin) and self._isatty(self.fout)
1279 and util.isstdin(self.fin) and util.isstdout(self.fout))
1280 and util.isstdin(self.fin) and util.isstdout(self.fout))
1280 if usereadline:
1281 if usereadline:
1281 try:
1282 try:
1282 # magically add command line editing support, where
1283 # magically add command line editing support, where
1283 # available
1284 # available
1284 import readline
1285 import readline
1285 # force demandimport to really load the module
1286 # force demandimport to really load the module
1286 readline.read_history_file
1287 readline.read_history_file
1287 # windows sometimes raises something other than ImportError
1288 # windows sometimes raises something other than ImportError
1288 except Exception:
1289 except Exception:
1289 usereadline = False
1290 usereadline = False
1290
1291
1291 # prompt ' ' must exist; otherwise readline may delete entire line
1292 # prompt ' ' must exist; otherwise readline may delete entire line
1292 # - http://bugs.python.org/issue12833
1293 # - http://bugs.python.org/issue12833
1293 with self.timeblockedsection('stdio'):
1294 with self.timeblockedsection('stdio'):
1294 if usereadline:
1295 if usereadline:
1295 line = encoding.strtolocal(pycompat.rawinput(r' '))
1296 line = encoding.strtolocal(pycompat.rawinput(r' '))
1296 # When stdin is in binary mode on Windows, it can cause
1297 # When stdin is in binary mode on Windows, it can cause
1297 # raw_input() to emit an extra trailing carriage return
1298 # raw_input() to emit an extra trailing carriage return
1298 if pycompat.oslinesep == b'\r\n' and line.endswith(b'\r'):
1299 if pycompat.oslinesep == b'\r\n' and line.endswith(b'\r'):
1299 line = line[:-1]
1300 line = line[:-1]
1300 else:
1301 else:
1301 self.fout.write(b' ')
1302 self.fout.write(b' ')
1302 self.fout.flush()
1303 self.fout.flush()
1303 line = self.fin.readline()
1304 line = self.fin.readline()
1304 if not line:
1305 if not line:
1305 raise EOFError
1306 raise EOFError
1306 line = line.rstrip(pycompat.oslinesep)
1307 line = line.rstrip(pycompat.oslinesep)
1307
1308
1308 return line
1309 return line
1309
1310
1310 def prompt(self, msg, default="y"):
1311 def prompt(self, msg, default="y"):
1311 """Prompt user with msg, read response.
1312 """Prompt user with msg, read response.
1312 If ui is not interactive, the default is returned.
1313 If ui is not interactive, the default is returned.
1313 """
1314 """
1314 if not self.interactive():
1315 if not self.interactive():
1315 self.write(msg, ' ', default or '', "\n")
1316 self.write(msg, ' ', default or '', "\n")
1316 return default
1317 return default
1317 self._writenobuf(msg, label='ui.prompt')
1318 self._writenobuf(msg, label='ui.prompt')
1318 self.flush()
1319 self.flush()
1319 try:
1320 try:
1320 r = self._readline()
1321 r = self._readline()
1321 if not r:
1322 if not r:
1322 r = default
1323 r = default
1323 if self.configbool('ui', 'promptecho'):
1324 if self.configbool('ui', 'promptecho'):
1324 self.write(r, "\n")
1325 self.write(r, "\n")
1325 return r
1326 return r
1326 except EOFError:
1327 except EOFError:
1327 raise error.ResponseExpected()
1328 raise error.ResponseExpected()
1328
1329
1329 @staticmethod
1330 @staticmethod
1330 def extractchoices(prompt):
1331 def extractchoices(prompt):
1331 """Extract prompt message and list of choices from specified prompt.
1332 """Extract prompt message and list of choices from specified prompt.
1332
1333
1333 This returns tuple "(message, choices)", and "choices" is the
1334 This returns tuple "(message, choices)", and "choices" is the
1334 list of tuple "(response character, text without &)".
1335 list of tuple "(response character, text without &)".
1335
1336
1336 >>> ui.extractchoices(b"awake? $$ &Yes $$ &No")
1337 >>> ui.extractchoices(b"awake? $$ &Yes $$ &No")
1337 ('awake? ', [('y', 'Yes'), ('n', 'No')])
1338 ('awake? ', [('y', 'Yes'), ('n', 'No')])
1338 >>> ui.extractchoices(b"line\\nbreak? $$ &Yes $$ &No")
1339 >>> ui.extractchoices(b"line\\nbreak? $$ &Yes $$ &No")
1339 ('line\\nbreak? ', [('y', 'Yes'), ('n', 'No')])
1340 ('line\\nbreak? ', [('y', 'Yes'), ('n', 'No')])
1340 >>> ui.extractchoices(b"want lots of $$money$$?$$Ye&s$$N&o")
1341 >>> ui.extractchoices(b"want lots of $$money$$?$$Ye&s$$N&o")
1341 ('want lots of $$money$$?', [('s', 'Yes'), ('o', 'No')])
1342 ('want lots of $$money$$?', [('s', 'Yes'), ('o', 'No')])
1342 """
1343 """
1343
1344
1344 # Sadly, the prompt string may have been built with a filename
1345 # Sadly, the prompt string may have been built with a filename
1345 # containing "$$" so let's try to find the first valid-looking
1346 # containing "$$" so let's try to find the first valid-looking
1346 # prompt to start parsing. Sadly, we also can't rely on
1347 # prompt to start parsing. Sadly, we also can't rely on
1347 # choices containing spaces, ASCII, or basically anything
1348 # choices containing spaces, ASCII, or basically anything
1348 # except an ampersand followed by a character.
1349 # except an ampersand followed by a character.
1349 m = re.match(br'(?s)(.+?)\$\$([^\$]*&[^ \$].*)', prompt)
1350 m = re.match(br'(?s)(.+?)\$\$([^\$]*&[^ \$].*)', prompt)
1350 msg = m.group(1)
1351 msg = m.group(1)
1351 choices = [p.strip(' ') for p in m.group(2).split('$$')]
1352 choices = [p.strip(' ') for p in m.group(2).split('$$')]
1352 def choicetuple(s):
1353 def choicetuple(s):
1353 ampidx = s.index('&')
1354 ampidx = s.index('&')
1354 return s[ampidx + 1:ampidx + 2].lower(), s.replace('&', '', 1)
1355 return s[ampidx + 1:ampidx + 2].lower(), s.replace('&', '', 1)
1355 return (msg, [choicetuple(s) for s in choices])
1356 return (msg, [choicetuple(s) for s in choices])
1356
1357
1357 def promptchoice(self, prompt, default=0):
1358 def promptchoice(self, prompt, default=0):
1358 """Prompt user with a message, read response, and ensure it matches
1359 """Prompt user with a message, read response, and ensure it matches
1359 one of the provided choices. The prompt is formatted as follows:
1360 one of the provided choices. The prompt is formatted as follows:
1360
1361
1361 "would you like fries with that (Yn)? $$ &Yes $$ &No"
1362 "would you like fries with that (Yn)? $$ &Yes $$ &No"
1362
1363
1363 The index of the choice is returned. Responses are case
1364 The index of the choice is returned. Responses are case
1364 insensitive. If ui is not interactive, the default is
1365 insensitive. If ui is not interactive, the default is
1365 returned.
1366 returned.
1366 """
1367 """
1367
1368
1368 msg, choices = self.extractchoices(prompt)
1369 msg, choices = self.extractchoices(prompt)
1369 resps = [r for r, t in choices]
1370 resps = [r for r, t in choices]
1370 while True:
1371 while True:
1371 r = self.prompt(msg, resps[default])
1372 r = self.prompt(msg, resps[default])
1372 if r.lower() in resps:
1373 if r.lower() in resps:
1373 return resps.index(r.lower())
1374 return resps.index(r.lower())
1374 self.write(_("unrecognized response\n"))
1375 self.write(_("unrecognized response\n"))
1375
1376
1376 def getpass(self, prompt=None, default=None):
1377 def getpass(self, prompt=None, default=None):
1377 if not self.interactive():
1378 if not self.interactive():
1378 return default
1379 return default
1379 try:
1380 try:
1380 self.write_err(self.label(prompt or _('password: '), 'ui.prompt'))
1381 self.write_err(self.label(prompt or _('password: '), 'ui.prompt'))
1381 # disable getpass() only if explicitly specified. it's still valid
1382 # disable getpass() only if explicitly specified. it's still valid
1382 # to interact with tty even if fin is not a tty.
1383 # to interact with tty even if fin is not a tty.
1383 with self.timeblockedsection('stdio'):
1384 with self.timeblockedsection('stdio'):
1384 if self.configbool('ui', 'nontty'):
1385 if self.configbool('ui', 'nontty'):
1385 l = self.fin.readline()
1386 l = self.fin.readline()
1386 if not l:
1387 if not l:
1387 raise EOFError
1388 raise EOFError
1388 return l.rstrip('\n')
1389 return l.rstrip('\n')
1389 else:
1390 else:
1390 return getpass.getpass('')
1391 return getpass.getpass('')
1391 except EOFError:
1392 except EOFError:
1392 raise error.ResponseExpected()
1393 raise error.ResponseExpected()
1393 def status(self, *msg, **opts):
1394 def status(self, *msg, **opts):
1394 '''write status message to output (if ui.quiet is False)
1395 '''write status message to output (if ui.quiet is False)
1395
1396
1396 This adds an output label of "ui.status".
1397 This adds an output label of "ui.status".
1397 '''
1398 '''
1398 if not self.quiet:
1399 if not self.quiet:
1399 opts[r'label'] = opts.get(r'label', '') + ' ui.status'
1400 opts[r'label'] = opts.get(r'label', '') + ' ui.status'
1400 self.write(*msg, **opts)
1401 self.write(*msg, **opts)
1401 def warn(self, *msg, **opts):
1402 def warn(self, *msg, **opts):
1402 '''write warning message to output (stderr)
1403 '''write warning message to output (stderr)
1403
1404
1404 This adds an output label of "ui.warning".
1405 This adds an output label of "ui.warning".
1405 '''
1406 '''
1406 opts[r'label'] = opts.get(r'label', '') + ' ui.warning'
1407 opts[r'label'] = opts.get(r'label', '') + ' ui.warning'
1407 self.write_err(*msg, **opts)
1408 self.write_err(*msg, **opts)
1408 def note(self, *msg, **opts):
1409 def note(self, *msg, **opts):
1409 '''write note to output (if ui.verbose is True)
1410 '''write note to output (if ui.verbose is True)
1410
1411
1411 This adds an output label of "ui.note".
1412 This adds an output label of "ui.note".
1412 '''
1413 '''
1413 if self.verbose:
1414 if self.verbose:
1414 opts[r'label'] = opts.get(r'label', '') + ' ui.note'
1415 opts[r'label'] = opts.get(r'label', '') + ' ui.note'
1415 self.write(*msg, **opts)
1416 self.write(*msg, **opts)
1416 def debug(self, *msg, **opts):
1417 def debug(self, *msg, **opts):
1417 '''write debug message to output (if ui.debugflag is True)
1418 '''write debug message to output (if ui.debugflag is True)
1418
1419
1419 This adds an output label of "ui.debug".
1420 This adds an output label of "ui.debug".
1420 '''
1421 '''
1421 if self.debugflag:
1422 if self.debugflag:
1422 opts[r'label'] = opts.get(r'label', '') + ' ui.debug'
1423 opts[r'label'] = opts.get(r'label', '') + ' ui.debug'
1423 self.write(*msg, **opts)
1424 self.write(*msg, **opts)
1424
1425
1425 def edit(self, text, user, extra=None, editform=None, pending=None,
1426 def edit(self, text, user, extra=None, editform=None, pending=None,
1426 repopath=None, action=None):
1427 repopath=None, action=None):
1427 if action is None:
1428 if action is None:
1428 self.develwarn('action is None but will soon be a required '
1429 self.develwarn('action is None but will soon be a required '
1429 'parameter to ui.edit()')
1430 'parameter to ui.edit()')
1430 extra_defaults = {
1431 extra_defaults = {
1431 'prefix': 'editor',
1432 'prefix': 'editor',
1432 'suffix': '.txt',
1433 'suffix': '.txt',
1433 }
1434 }
1434 if extra is not None:
1435 if extra is not None:
1435 if extra.get('suffix') is not None:
1436 if extra.get('suffix') is not None:
1436 self.develwarn('extra.suffix is not None but will soon be '
1437 self.develwarn('extra.suffix is not None but will soon be '
1437 'ignored by ui.edit()')
1438 'ignored by ui.edit()')
1438 extra_defaults.update(extra)
1439 extra_defaults.update(extra)
1439 extra = extra_defaults
1440 extra = extra_defaults
1440
1441
1441 if action == 'diff':
1442 if action == 'diff':
1442 suffix = '.diff'
1443 suffix = '.diff'
1443 elif action:
1444 elif action:
1444 suffix = '.%s.hg.txt' % action
1445 suffix = '.%s.hg.txt' % action
1445 else:
1446 else:
1446 suffix = extra['suffix']
1447 suffix = extra['suffix']
1447
1448
1448 rdir = None
1449 rdir = None
1449 if self.configbool('experimental', 'editortmpinhg'):
1450 if self.configbool('experimental', 'editortmpinhg'):
1450 rdir = repopath
1451 rdir = repopath
1451 (fd, name) = tempfile.mkstemp(prefix='hg-' + extra['prefix'] + '-',
1452 (fd, name) = tempfile.mkstemp(prefix='hg-' + extra['prefix'] + '-',
1452 suffix=suffix,
1453 suffix=suffix,
1453 dir=rdir)
1454 dir=rdir)
1454 try:
1455 try:
1455 f = os.fdopen(fd, r'wb')
1456 f = os.fdopen(fd, r'wb')
1456 f.write(util.tonativeeol(text))
1457 f.write(util.tonativeeol(text))
1457 f.close()
1458 f.close()
1458
1459
1459 environ = {'HGUSER': user}
1460 environ = {'HGUSER': user}
1460 if 'transplant_source' in extra:
1461 if 'transplant_source' in extra:
1461 environ.update({'HGREVISION': hex(extra['transplant_source'])})
1462 environ.update({'HGREVISION': hex(extra['transplant_source'])})
1462 for label in ('intermediate-source', 'source', 'rebase_source'):
1463 for label in ('intermediate-source', 'source', 'rebase_source'):
1463 if label in extra:
1464 if label in extra:
1464 environ.update({'HGREVISION': extra[label]})
1465 environ.update({'HGREVISION': extra[label]})
1465 break
1466 break
1466 if editform:
1467 if editform:
1467 environ.update({'HGEDITFORM': editform})
1468 environ.update({'HGEDITFORM': editform})
1468 if pending:
1469 if pending:
1469 environ.update({'HG_PENDING': pending})
1470 environ.update({'HG_PENDING': pending})
1470
1471
1471 editor = self.geteditor()
1472 editor = self.geteditor()
1472
1473
1473 self.system("%s \"%s\"" % (editor, name),
1474 self.system("%s \"%s\"" % (editor, name),
1474 environ=environ,
1475 environ=environ,
1475 onerr=error.Abort, errprefix=_("edit failed"),
1476 onerr=error.Abort, errprefix=_("edit failed"),
1476 blockedtag='editor')
1477 blockedtag='editor')
1477
1478
1478 f = open(name, r'rb')
1479 f = open(name, r'rb')
1479 t = util.fromnativeeol(f.read())
1480 t = util.fromnativeeol(f.read())
1480 f.close()
1481 f.close()
1481 finally:
1482 finally:
1482 os.unlink(name)
1483 os.unlink(name)
1483
1484
1484 return t
1485 return t
1485
1486
1486 def system(self, cmd, environ=None, cwd=None, onerr=None, errprefix=None,
1487 def system(self, cmd, environ=None, cwd=None, onerr=None, errprefix=None,
1487 blockedtag=None):
1488 blockedtag=None):
1488 '''execute shell command with appropriate output stream. command
1489 '''execute shell command with appropriate output stream. command
1489 output will be redirected if fout is not stdout.
1490 output will be redirected if fout is not stdout.
1490
1491
1491 if command fails and onerr is None, return status, else raise onerr
1492 if command fails and onerr is None, return status, else raise onerr
1492 object as exception.
1493 object as exception.
1493 '''
1494 '''
1494 if blockedtag is None:
1495 if blockedtag is None:
1495 # Long cmds tend to be because of an absolute path on cmd. Keep
1496 # Long cmds tend to be because of an absolute path on cmd. Keep
1496 # the tail end instead
1497 # the tail end instead
1497 cmdsuffix = cmd.translate(None, _keepalnum)[-85:]
1498 cmdsuffix = cmd.translate(None, _keepalnum)[-85:]
1498 blockedtag = 'unknown_system_' + cmdsuffix
1499 blockedtag = 'unknown_system_' + cmdsuffix
1499 out = self.fout
1500 out = self.fout
1500 if any(s[1] for s in self._bufferstates):
1501 if any(s[1] for s in self._bufferstates):
1501 out = self
1502 out = self
1502 with self.timeblockedsection(blockedtag):
1503 with self.timeblockedsection(blockedtag):
1503 rc = self._runsystem(cmd, environ=environ, cwd=cwd, out=out)
1504 rc = self._runsystem(cmd, environ=environ, cwd=cwd, out=out)
1504 if rc and onerr:
1505 if rc and onerr:
1505 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
1506 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
1506 util.explainexit(rc)[0])
1507 util.explainexit(rc)[0])
1507 if errprefix:
1508 if errprefix:
1508 errmsg = '%s: %s' % (errprefix, errmsg)
1509 errmsg = '%s: %s' % (errprefix, errmsg)
1509 raise onerr(errmsg)
1510 raise onerr(errmsg)
1510 return rc
1511 return rc
1511
1512
1512 def _runsystem(self, cmd, environ, cwd, out):
1513 def _runsystem(self, cmd, environ, cwd, out):
1513 """actually execute the given shell command (can be overridden by
1514 """actually execute the given shell command (can be overridden by
1514 extensions like chg)"""
1515 extensions like chg)"""
1515 return util.system(cmd, environ=environ, cwd=cwd, out=out)
1516 return util.system(cmd, environ=environ, cwd=cwd, out=out)
1516
1517
1517 def traceback(self, exc=None, force=False):
1518 def traceback(self, exc=None, force=False):
1518 '''print exception traceback if traceback printing enabled or forced.
1519 '''print exception traceback if traceback printing enabled or forced.
1519 only to call in exception handler. returns true if traceback
1520 only to call in exception handler. returns true if traceback
1520 printed.'''
1521 printed.'''
1521 if self.tracebackflag or force:
1522 if self.tracebackflag or force:
1522 if exc is None:
1523 if exc is None:
1523 exc = sys.exc_info()
1524 exc = sys.exc_info()
1524 cause = getattr(exc[1], 'cause', None)
1525 cause = getattr(exc[1], 'cause', None)
1525
1526
1526 if cause is not None:
1527 if cause is not None:
1527 causetb = traceback.format_tb(cause[2])
1528 causetb = traceback.format_tb(cause[2])
1528 exctb = traceback.format_tb(exc[2])
1529 exctb = traceback.format_tb(exc[2])
1529 exconly = traceback.format_exception_only(cause[0], cause[1])
1530 exconly = traceback.format_exception_only(cause[0], cause[1])
1530
1531
1531 # exclude frame where 'exc' was chained and rethrown from exctb
1532 # exclude frame where 'exc' was chained and rethrown from exctb
1532 self.write_err('Traceback (most recent call last):\n',
1533 self.write_err('Traceback (most recent call last):\n',
1533 ''.join(exctb[:-1]),
1534 ''.join(exctb[:-1]),
1534 ''.join(causetb),
1535 ''.join(causetb),
1535 ''.join(exconly))
1536 ''.join(exconly))
1536 else:
1537 else:
1537 output = traceback.format_exception(exc[0], exc[1], exc[2])
1538 output = traceback.format_exception(exc[0], exc[1], exc[2])
1538 self.write_err(encoding.strtolocal(r''.join(output)))
1539 self.write_err(encoding.strtolocal(r''.join(output)))
1539 return self.tracebackflag or force
1540 return self.tracebackflag or force
1540
1541
1541 def geteditor(self):
1542 def geteditor(self):
1542 '''return editor to use'''
1543 '''return editor to use'''
1543 if pycompat.sysplatform == 'plan9':
1544 if pycompat.sysplatform == 'plan9':
1544 # vi is the MIPS instruction simulator on Plan 9. We
1545 # vi is the MIPS instruction simulator on Plan 9. We
1545 # instead default to E to plumb commit messages to
1546 # instead default to E to plumb commit messages to
1546 # avoid confusion.
1547 # avoid confusion.
1547 editor = 'E'
1548 editor = 'E'
1548 else:
1549 else:
1549 editor = 'vi'
1550 editor = 'vi'
1550 return (encoding.environ.get("HGEDITOR") or
1551 return (encoding.environ.get("HGEDITOR") or
1551 self.config("ui", "editor", editor))
1552 self.config("ui", "editor", editor))
1552
1553
1553 @util.propertycache
1554 @util.propertycache
1554 def _progbar(self):
1555 def _progbar(self):
1555 """setup the progbar singleton to the ui object"""
1556 """setup the progbar singleton to the ui object"""
1556 if (self.quiet or self.debugflag
1557 if (self.quiet or self.debugflag
1557 or self.configbool('progress', 'disable')
1558 or self.configbool('progress', 'disable')
1558 or not progress.shouldprint(self)):
1559 or not progress.shouldprint(self)):
1559 return None
1560 return None
1560 return getprogbar(self)
1561 return getprogbar(self)
1561
1562
1562 def _progclear(self):
1563 def _progclear(self):
1563 """clear progress bar output if any. use it before any output"""
1564 """clear progress bar output if any. use it before any output"""
1564 if not haveprogbar(): # nothing loaded yet
1565 if not haveprogbar(): # nothing loaded yet
1565 return
1566 return
1566 if self._progbar is not None and self._progbar.printed:
1567 if self._progbar is not None and self._progbar.printed:
1567 self._progbar.clear()
1568 self._progbar.clear()
1568
1569
1569 def progress(self, topic, pos, item="", unit="", total=None):
1570 def progress(self, topic, pos, item="", unit="", total=None):
1570 '''show a progress message
1571 '''show a progress message
1571
1572
1572 By default a textual progress bar will be displayed if an operation
1573 By default a textual progress bar will be displayed if an operation
1573 takes too long. 'topic' is the current operation, 'item' is a
1574 takes too long. 'topic' is the current operation, 'item' is a
1574 non-numeric marker of the current position (i.e. the currently
1575 non-numeric marker of the current position (i.e. the currently
1575 in-process file), 'pos' is the current numeric position (i.e.
1576 in-process file), 'pos' is the current numeric position (i.e.
1576 revision, bytes, etc.), unit is a corresponding unit label,
1577 revision, bytes, etc.), unit is a corresponding unit label,
1577 and total is the highest expected pos.
1578 and total is the highest expected pos.
1578
1579
1579 Multiple nested topics may be active at a time.
1580 Multiple nested topics may be active at a time.
1580
1581
1581 All topics should be marked closed by setting pos to None at
1582 All topics should be marked closed by setting pos to None at
1582 termination.
1583 termination.
1583 '''
1584 '''
1584 if self._progbar is not None:
1585 if self._progbar is not None:
1585 self._progbar.progress(topic, pos, item=item, unit=unit,
1586 self._progbar.progress(topic, pos, item=item, unit=unit,
1586 total=total)
1587 total=total)
1587 if pos is None or not self.configbool('progress', 'debug'):
1588 if pos is None or not self.configbool('progress', 'debug'):
1588 return
1589 return
1589
1590
1590 if unit:
1591 if unit:
1591 unit = ' ' + unit
1592 unit = ' ' + unit
1592 if item:
1593 if item:
1593 item = ' ' + item
1594 item = ' ' + item
1594
1595
1595 if total:
1596 if total:
1596 pct = 100.0 * pos / total
1597 pct = 100.0 * pos / total
1597 self.debug('%s:%s %d/%d%s (%4.2f%%)\n'
1598 self.debug('%s:%s %d/%d%s (%4.2f%%)\n'
1598 % (topic, item, pos, total, unit, pct))
1599 % (topic, item, pos, total, unit, pct))
1599 else:
1600 else:
1600 self.debug('%s:%s %d%s\n' % (topic, item, pos, unit))
1601 self.debug('%s:%s %d%s\n' % (topic, item, pos, unit))
1601
1602
1602 def log(self, service, *msg, **opts):
1603 def log(self, service, *msg, **opts):
1603 '''hook for logging facility extensions
1604 '''hook for logging facility extensions
1604
1605
1605 service should be a readily-identifiable subsystem, which will
1606 service should be a readily-identifiable subsystem, which will
1606 allow filtering.
1607 allow filtering.
1607
1608
1608 *msg should be a newline-terminated format string to log, and
1609 *msg should be a newline-terminated format string to log, and
1609 then any values to %-format into that format string.
1610 then any values to %-format into that format string.
1610
1611
1611 **opts currently has no defined meanings.
1612 **opts currently has no defined meanings.
1612 '''
1613 '''
1613
1614
1614 def label(self, msg, label):
1615 def label(self, msg, label):
1615 '''style msg based on supplied label
1616 '''style msg based on supplied label
1616
1617
1617 If some color mode is enabled, this will add the necessary control
1618 If some color mode is enabled, this will add the necessary control
1618 characters to apply such color. In addition, 'debug' color mode adds
1619 characters to apply such color. In addition, 'debug' color mode adds
1619 markup showing which label affects a piece of text.
1620 markup showing which label affects a piece of text.
1620
1621
1621 ui.write(s, 'label') is equivalent to
1622 ui.write(s, 'label') is equivalent to
1622 ui.write(ui.label(s, 'label')).
1623 ui.write(ui.label(s, 'label')).
1623 '''
1624 '''
1624 if self._colormode is not None:
1625 if self._colormode is not None:
1625 return color.colorlabel(self, msg, label)
1626 return color.colorlabel(self, msg, label)
1626 return msg
1627 return msg
1627
1628
1628 def develwarn(self, msg, stacklevel=1, config=None):
1629 def develwarn(self, msg, stacklevel=1, config=None):
1629 """issue a developer warning message
1630 """issue a developer warning message
1630
1631
1631 Use 'stacklevel' to report the offender some layers further up in the
1632 Use 'stacklevel' to report the offender some layers further up in the
1632 stack.
1633 stack.
1633 """
1634 """
1634 if not self.configbool('devel', 'all-warnings'):
1635 if not self.configbool('devel', 'all-warnings'):
1635 if config is None or not self.configbool('devel', config):
1636 if config is None or not self.configbool('devel', config):
1636 return
1637 return
1637 msg = 'devel-warn: ' + msg
1638 msg = 'devel-warn: ' + msg
1638 stacklevel += 1 # get in develwarn
1639 stacklevel += 1 # get in develwarn
1639 if self.tracebackflag:
1640 if self.tracebackflag:
1640 util.debugstacktrace(msg, stacklevel, self.ferr, self.fout)
1641 util.debugstacktrace(msg, stacklevel, self.ferr, self.fout)
1641 self.log('develwarn', '%s at:\n%s' %
1642 self.log('develwarn', '%s at:\n%s' %
1642 (msg, ''.join(util.getstackframes(stacklevel))))
1643 (msg, ''.join(util.getstackframes(stacklevel))))
1643 else:
1644 else:
1644 curframe = inspect.currentframe()
1645 curframe = inspect.currentframe()
1645 calframe = inspect.getouterframes(curframe, 2)
1646 calframe = inspect.getouterframes(curframe, 2)
1646 fname, lineno, fmsg = calframe[stacklevel][1:4]
1647 fname, lineno, fmsg = calframe[stacklevel][1:4]
1647 fname, fmsg = pycompat.sysbytes(fname), pycompat.sysbytes(fmsg)
1648 fname, fmsg = pycompat.sysbytes(fname), pycompat.sysbytes(fmsg)
1648 self.write_err('%s at: %s:%d (%s)\n'
1649 self.write_err('%s at: %s:%d (%s)\n'
1649 % (msg, fname, lineno, fmsg))
1650 % (msg, fname, lineno, fmsg))
1650 self.log('develwarn', '%s at: %s:%d (%s)\n',
1651 self.log('develwarn', '%s at: %s:%d (%s)\n',
1651 msg, fname, lineno, fmsg)
1652 msg, fname, lineno, fmsg)
1652 curframe = calframe = None # avoid cycles
1653 curframe = calframe = None # avoid cycles
1653
1654
1654 def deprecwarn(self, msg, version, stacklevel=2):
1655 def deprecwarn(self, msg, version, stacklevel=2):
1655 """issue a deprecation warning
1656 """issue a deprecation warning
1656
1657
1657 - msg: message explaining what is deprecated and how to upgrade,
1658 - msg: message explaining what is deprecated and how to upgrade,
1658 - version: last version where the API will be supported,
1659 - version: last version where the API will be supported,
1659 """
1660 """
1660 if not (self.configbool('devel', 'all-warnings')
1661 if not (self.configbool('devel', 'all-warnings')
1661 or self.configbool('devel', 'deprec-warn')):
1662 or self.configbool('devel', 'deprec-warn')):
1662 return
1663 return
1663 msg += ("\n(compatibility will be dropped after Mercurial-%s,"
1664 msg += ("\n(compatibility will be dropped after Mercurial-%s,"
1664 " update your code.)") % version
1665 " update your code.)") % version
1665 self.develwarn(msg, stacklevel=stacklevel, config='deprec-warn')
1666 self.develwarn(msg, stacklevel=stacklevel, config='deprec-warn')
1666
1667
1667 def exportableenviron(self):
1668 def exportableenviron(self):
1668 """The environment variables that are safe to export, e.g. through
1669 """The environment variables that are safe to export, e.g. through
1669 hgweb.
1670 hgweb.
1670 """
1671 """
1671 return self._exportableenviron
1672 return self._exportableenviron
1672
1673
1673 @contextlib.contextmanager
1674 @contextlib.contextmanager
1674 def configoverride(self, overrides, source=""):
1675 def configoverride(self, overrides, source=""):
1675 """Context manager for temporary config overrides
1676 """Context manager for temporary config overrides
1676 `overrides` must be a dict of the following structure:
1677 `overrides` must be a dict of the following structure:
1677 {(section, name) : value}"""
1678 {(section, name) : value}"""
1678 backups = {}
1679 backups = {}
1679 try:
1680 try:
1680 for (section, name), value in overrides.items():
1681 for (section, name), value in overrides.items():
1681 backups[(section, name)] = self.backupconfig(section, name)
1682 backups[(section, name)] = self.backupconfig(section, name)
1682 self.setconfig(section, name, value, source)
1683 self.setconfig(section, name, value, source)
1683 yield
1684 yield
1684 finally:
1685 finally:
1685 for __, backup in backups.items():
1686 for __, backup in backups.items():
1686 self.restoreconfig(backup)
1687 self.restoreconfig(backup)
1687 # just restoring ui.quiet config to the previous value is not enough
1688 # just restoring ui.quiet config to the previous value is not enough
1688 # as it does not update ui.quiet class member
1689 # as it does not update ui.quiet class member
1689 if ('ui', 'quiet') in overrides:
1690 if ('ui', 'quiet') in overrides:
1690 self.fixconfig(section='ui')
1691 self.fixconfig(section='ui')
1691
1692
1692 class paths(dict):
1693 class paths(dict):
1693 """Represents a collection of paths and their configs.
1694 """Represents a collection of paths and their configs.
1694
1695
1695 Data is initially derived from ui instances and the config files they have
1696 Data is initially derived from ui instances and the config files they have
1696 loaded.
1697 loaded.
1697 """
1698 """
1698 def __init__(self, ui):
1699 def __init__(self, ui):
1699 dict.__init__(self)
1700 dict.__init__(self)
1700
1701
1701 for name, loc in ui.configitems('paths', ignoresub=True):
1702 for name, loc in ui.configitems('paths', ignoresub=True):
1702 # No location is the same as not existing.
1703 # No location is the same as not existing.
1703 if not loc:
1704 if not loc:
1704 continue
1705 continue
1705 loc, sub = ui.configsuboptions('paths', name)
1706 loc, sub = ui.configsuboptions('paths', name)
1706 self[name] = path(ui, name, rawloc=loc, suboptions=sub)
1707 self[name] = path(ui, name, rawloc=loc, suboptions=sub)
1707
1708
1708 def getpath(self, name, default=None):
1709 def getpath(self, name, default=None):
1709 """Return a ``path`` from a string, falling back to default.
1710 """Return a ``path`` from a string, falling back to default.
1710
1711
1711 ``name`` can be a named path or locations. Locations are filesystem
1712 ``name`` can be a named path or locations. Locations are filesystem
1712 paths or URIs.
1713 paths or URIs.
1713
1714
1714 Returns None if ``name`` is not a registered path, a URI, or a local
1715 Returns None if ``name`` is not a registered path, a URI, or a local
1715 path to a repo.
1716 path to a repo.
1716 """
1717 """
1717 # Only fall back to default if no path was requested.
1718 # Only fall back to default if no path was requested.
1718 if name is None:
1719 if name is None:
1719 if not default:
1720 if not default:
1720 default = ()
1721 default = ()
1721 elif not isinstance(default, (tuple, list)):
1722 elif not isinstance(default, (tuple, list)):
1722 default = (default,)
1723 default = (default,)
1723 for k in default:
1724 for k in default:
1724 try:
1725 try:
1725 return self[k]
1726 return self[k]
1726 except KeyError:
1727 except KeyError:
1727 continue
1728 continue
1728 return None
1729 return None
1729
1730
1730 # Most likely empty string.
1731 # Most likely empty string.
1731 # This may need to raise in the future.
1732 # This may need to raise in the future.
1732 if not name:
1733 if not name:
1733 return None
1734 return None
1734
1735
1735 try:
1736 try:
1736 return self[name]
1737 return self[name]
1737 except KeyError:
1738 except KeyError:
1738 # Try to resolve as a local path or URI.
1739 # Try to resolve as a local path or URI.
1739 try:
1740 try:
1740 # We don't pass sub-options in, so no need to pass ui instance.
1741 # We don't pass sub-options in, so no need to pass ui instance.
1741 return path(None, None, rawloc=name)
1742 return path(None, None, rawloc=name)
1742 except ValueError:
1743 except ValueError:
1743 raise error.RepoError(_('repository %s does not exist') %
1744 raise error.RepoError(_('repository %s does not exist') %
1744 name)
1745 name)
1745
1746
1746 _pathsuboptions = {}
1747 _pathsuboptions = {}
1747
1748
1748 def pathsuboption(option, attr):
1749 def pathsuboption(option, attr):
1749 """Decorator used to declare a path sub-option.
1750 """Decorator used to declare a path sub-option.
1750
1751
1751 Arguments are the sub-option name and the attribute it should set on
1752 Arguments are the sub-option name and the attribute it should set on
1752 ``path`` instances.
1753 ``path`` instances.
1753
1754
1754 The decorated function will receive as arguments a ``ui`` instance,
1755 The decorated function will receive as arguments a ``ui`` instance,
1755 ``path`` instance, and the string value of this option from the config.
1756 ``path`` instance, and the string value of this option from the config.
1756 The function should return the value that will be set on the ``path``
1757 The function should return the value that will be set on the ``path``
1757 instance.
1758 instance.
1758
1759
1759 This decorator can be used to perform additional verification of
1760 This decorator can be used to perform additional verification of
1760 sub-options and to change the type of sub-options.
1761 sub-options and to change the type of sub-options.
1761 """
1762 """
1762 def register(func):
1763 def register(func):
1763 _pathsuboptions[option] = (attr, func)
1764 _pathsuboptions[option] = (attr, func)
1764 return func
1765 return func
1765 return register
1766 return register
1766
1767
1767 @pathsuboption('pushurl', 'pushloc')
1768 @pathsuboption('pushurl', 'pushloc')
1768 def pushurlpathoption(ui, path, value):
1769 def pushurlpathoption(ui, path, value):
1769 u = util.url(value)
1770 u = util.url(value)
1770 # Actually require a URL.
1771 # Actually require a URL.
1771 if not u.scheme:
1772 if not u.scheme:
1772 ui.warn(_('(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
1773 ui.warn(_('(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
1773 return None
1774 return None
1774
1775
1775 # Don't support the #foo syntax in the push URL to declare branch to
1776 # Don't support the #foo syntax in the push URL to declare branch to
1776 # push.
1777 # push.
1777 if u.fragment:
1778 if u.fragment:
1778 ui.warn(_('("#fragment" in paths.%s:pushurl not supported; '
1779 ui.warn(_('("#fragment" in paths.%s:pushurl not supported; '
1779 'ignoring)\n') % path.name)
1780 'ignoring)\n') % path.name)
1780 u.fragment = None
1781 u.fragment = None
1781
1782
1782 return bytes(u)
1783 return bytes(u)
1783
1784
1784 @pathsuboption('pushrev', 'pushrev')
1785 @pathsuboption('pushrev', 'pushrev')
1785 def pushrevpathoption(ui, path, value):
1786 def pushrevpathoption(ui, path, value):
1786 return value
1787 return value
1787
1788
1788 class path(object):
1789 class path(object):
1789 """Represents an individual path and its configuration."""
1790 """Represents an individual path and its configuration."""
1790
1791
1791 def __init__(self, ui, name, rawloc=None, suboptions=None):
1792 def __init__(self, ui, name, rawloc=None, suboptions=None):
1792 """Construct a path from its config options.
1793 """Construct a path from its config options.
1793
1794
1794 ``ui`` is the ``ui`` instance the path is coming from.
1795 ``ui`` is the ``ui`` instance the path is coming from.
1795 ``name`` is the symbolic name of the path.
1796 ``name`` is the symbolic name of the path.
1796 ``rawloc`` is the raw location, as defined in the config.
1797 ``rawloc`` is the raw location, as defined in the config.
1797 ``pushloc`` is the raw locations pushes should be made to.
1798 ``pushloc`` is the raw locations pushes should be made to.
1798
1799
1799 If ``name`` is not defined, we require that the location be a) a local
1800 If ``name`` is not defined, we require that the location be a) a local
1800 filesystem path with a .hg directory or b) a URL. If not,
1801 filesystem path with a .hg directory or b) a URL. If not,
1801 ``ValueError`` is raised.
1802 ``ValueError`` is raised.
1802 """
1803 """
1803 if not rawloc:
1804 if not rawloc:
1804 raise ValueError('rawloc must be defined')
1805 raise ValueError('rawloc must be defined')
1805
1806
1806 # Locations may define branches via syntax <base>#<branch>.
1807 # Locations may define branches via syntax <base>#<branch>.
1807 u = util.url(rawloc)
1808 u = util.url(rawloc)
1808 branch = None
1809 branch = None
1809 if u.fragment:
1810 if u.fragment:
1810 branch = u.fragment
1811 branch = u.fragment
1811 u.fragment = None
1812 u.fragment = None
1812
1813
1813 self.url = u
1814 self.url = u
1814 self.branch = branch
1815 self.branch = branch
1815
1816
1816 self.name = name
1817 self.name = name
1817 self.rawloc = rawloc
1818 self.rawloc = rawloc
1818 self.loc = '%s' % u
1819 self.loc = '%s' % u
1819
1820
1820 # When given a raw location but not a symbolic name, validate the
1821 # When given a raw location but not a symbolic name, validate the
1821 # location is valid.
1822 # location is valid.
1822 if not name and not u.scheme and not self._isvalidlocalpath(self.loc):
1823 if not name and not u.scheme and not self._isvalidlocalpath(self.loc):
1823 raise ValueError('location is not a URL or path to a local '
1824 raise ValueError('location is not a URL or path to a local '
1824 'repo: %s' % rawloc)
1825 'repo: %s' % rawloc)
1825
1826
1826 suboptions = suboptions or {}
1827 suboptions = suboptions or {}
1827
1828
1828 # Now process the sub-options. If a sub-option is registered, its
1829 # Now process the sub-options. If a sub-option is registered, its
1829 # attribute will always be present. The value will be None if there
1830 # attribute will always be present. The value will be None if there
1830 # was no valid sub-option.
1831 # was no valid sub-option.
1831 for suboption, (attr, func) in _pathsuboptions.iteritems():
1832 for suboption, (attr, func) in _pathsuboptions.iteritems():
1832 if suboption not in suboptions:
1833 if suboption not in suboptions:
1833 setattr(self, attr, None)
1834 setattr(self, attr, None)
1834 continue
1835 continue
1835
1836
1836 value = func(ui, self, suboptions[suboption])
1837 value = func(ui, self, suboptions[suboption])
1837 setattr(self, attr, value)
1838 setattr(self, attr, value)
1838
1839
1839 def _isvalidlocalpath(self, path):
1840 def _isvalidlocalpath(self, path):
1840 """Returns True if the given path is a potentially valid repository.
1841 """Returns True if the given path is a potentially valid repository.
1841 This is its own function so that extensions can change the definition of
1842 This is its own function so that extensions can change the definition of
1842 'valid' in this case (like when pulling from a git repo into a hg
1843 'valid' in this case (like when pulling from a git repo into a hg
1843 one)."""
1844 one)."""
1844 return os.path.isdir(os.path.join(path, '.hg'))
1845 return os.path.isdir(os.path.join(path, '.hg'))
1845
1846
1846 @property
1847 @property
1847 def suboptions(self):
1848 def suboptions(self):
1848 """Return sub-options and their values for this path.
1849 """Return sub-options and their values for this path.
1849
1850
1850 This is intended to be used for presentation purposes.
1851 This is intended to be used for presentation purposes.
1851 """
1852 """
1852 d = {}
1853 d = {}
1853 for subopt, (attr, _func) in _pathsuboptions.iteritems():
1854 for subopt, (attr, _func) in _pathsuboptions.iteritems():
1854 value = getattr(self, attr)
1855 value = getattr(self, attr)
1855 if value is not None:
1856 if value is not None:
1856 d[subopt] = value
1857 d[subopt] = value
1857 return d
1858 return d
1858
1859
1859 # we instantiate one globally shared progress bar to avoid
1860 # we instantiate one globally shared progress bar to avoid
1860 # competing progress bars when multiple UI objects get created
1861 # competing progress bars when multiple UI objects get created
1861 _progresssingleton = None
1862 _progresssingleton = None
1862
1863
1863 def getprogbar(ui):
1864 def getprogbar(ui):
1864 global _progresssingleton
1865 global _progresssingleton
1865 if _progresssingleton is None:
1866 if _progresssingleton is None:
1866 # passing 'ui' object to the singleton is fishy,
1867 # passing 'ui' object to the singleton is fishy,
1867 # this is how the extension used to work but feel free to rework it.
1868 # this is how the extension used to work but feel free to rework it.
1868 _progresssingleton = progress.progbar(ui)
1869 _progresssingleton = progress.progbar(ui)
1869 return _progresssingleton
1870 return _progresssingleton
1870
1871
1871 def haveprogbar():
1872 def haveprogbar():
1872 return _progresssingleton is not None
1873 return _progresssingleton is not None
@@ -1,1139 +1,1140 b''
1 # wireproto.py - generic wire protocol support functions
1 # wireproto.py - generic wire protocol support functions
2 #
2 #
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2010 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
8 from __future__ import absolute_import
9
9
10 import hashlib
10 import hashlib
11 import os
11 import os
12 import tempfile
12 import tempfile
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import (
15 from .node import (
16 bin,
16 bin,
17 hex,
17 hex,
18 nullid,
18 nullid,
19 )
19 )
20
20
21 from . import (
21 from . import (
22 bundle2,
22 bundle2,
23 changegroup as changegroupmod,
23 changegroup as changegroupmod,
24 discovery,
24 discovery,
25 encoding,
25 encoding,
26 error,
26 error,
27 exchange,
27 exchange,
28 peer,
28 peer,
29 pushkey as pushkeymod,
29 pushkey as pushkeymod,
30 pycompat,
30 pycompat,
31 repository,
31 repository,
32 streamclone,
32 streamclone,
33 util,
33 util,
34 wireprototypes,
34 wireprototypes,
35 )
35 )
36
36
37 from .utils import (
37 from .utils import (
38 procutil,
38 stringutil,
39 stringutil,
39 )
40 )
40
41
41 urlerr = util.urlerr
42 urlerr = util.urlerr
42 urlreq = util.urlreq
43 urlreq = util.urlreq
43
44
44 bytesresponse = wireprototypes.bytesresponse
45 bytesresponse = wireprototypes.bytesresponse
45 ooberror = wireprototypes.ooberror
46 ooberror = wireprototypes.ooberror
46 pushres = wireprototypes.pushres
47 pushres = wireprototypes.pushres
47 pusherr = wireprototypes.pusherr
48 pusherr = wireprototypes.pusherr
48 streamres = wireprototypes.streamres
49 streamres = wireprototypes.streamres
49 streamres_legacy = wireprototypes.streamreslegacy
50 streamres_legacy = wireprototypes.streamreslegacy
50
51
51 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
52 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
52 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
53 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
53 'IncompatibleClient')
54 'IncompatibleClient')
54 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
55 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
55
56
56 class remoteiterbatcher(peer.iterbatcher):
57 class remoteiterbatcher(peer.iterbatcher):
57 def __init__(self, remote):
58 def __init__(self, remote):
58 super(remoteiterbatcher, self).__init__()
59 super(remoteiterbatcher, self).__init__()
59 self._remote = remote
60 self._remote = remote
60
61
61 def __getattr__(self, name):
62 def __getattr__(self, name):
62 # Validate this method is batchable, since submit() only supports
63 # Validate this method is batchable, since submit() only supports
63 # batchable methods.
64 # batchable methods.
64 fn = getattr(self._remote, name)
65 fn = getattr(self._remote, name)
65 if not getattr(fn, 'batchable', None):
66 if not getattr(fn, 'batchable', None):
66 raise error.ProgrammingError('Attempted to batch a non-batchable '
67 raise error.ProgrammingError('Attempted to batch a non-batchable '
67 'call to %r' % name)
68 'call to %r' % name)
68
69
69 return super(remoteiterbatcher, self).__getattr__(name)
70 return super(remoteiterbatcher, self).__getattr__(name)
70
71
71 def submit(self):
72 def submit(self):
72 """Break the batch request into many patch calls and pipeline them.
73 """Break the batch request into many patch calls and pipeline them.
73
74
74 This is mostly valuable over http where request sizes can be
75 This is mostly valuable over http where request sizes can be
75 limited, but can be used in other places as well.
76 limited, but can be used in other places as well.
76 """
77 """
77 # 2-tuple of (command, arguments) that represents what will be
78 # 2-tuple of (command, arguments) that represents what will be
78 # sent over the wire.
79 # sent over the wire.
79 requests = []
80 requests = []
80
81
81 # 4-tuple of (command, final future, @batchable generator, remote
82 # 4-tuple of (command, final future, @batchable generator, remote
82 # future).
83 # future).
83 results = []
84 results = []
84
85
85 for command, args, opts, finalfuture in self.calls:
86 for command, args, opts, finalfuture in self.calls:
86 mtd = getattr(self._remote, command)
87 mtd = getattr(self._remote, command)
87 batchable = mtd.batchable(mtd.__self__, *args, **opts)
88 batchable = mtd.batchable(mtd.__self__, *args, **opts)
88
89
89 commandargs, fremote = next(batchable)
90 commandargs, fremote = next(batchable)
90 assert fremote
91 assert fremote
91 requests.append((command, commandargs))
92 requests.append((command, commandargs))
92 results.append((command, finalfuture, batchable, fremote))
93 results.append((command, finalfuture, batchable, fremote))
93
94
94 if requests:
95 if requests:
95 self._resultiter = self._remote._submitbatch(requests)
96 self._resultiter = self._remote._submitbatch(requests)
96
97
97 self._results = results
98 self._results = results
98
99
99 def results(self):
100 def results(self):
100 for command, finalfuture, batchable, remotefuture in self._results:
101 for command, finalfuture, batchable, remotefuture in self._results:
101 # Get the raw result, set it in the remote future, feed it
102 # Get the raw result, set it in the remote future, feed it
102 # back into the @batchable generator so it can be decoded, and
103 # back into the @batchable generator so it can be decoded, and
103 # set the result on the final future to this value.
104 # set the result on the final future to this value.
104 remoteresult = next(self._resultiter)
105 remoteresult = next(self._resultiter)
105 remotefuture.set(remoteresult)
106 remotefuture.set(remoteresult)
106 finalfuture.set(next(batchable))
107 finalfuture.set(next(batchable))
107
108
108 # Verify our @batchable generators only emit 2 values.
109 # Verify our @batchable generators only emit 2 values.
109 try:
110 try:
110 next(batchable)
111 next(batchable)
111 except StopIteration:
112 except StopIteration:
112 pass
113 pass
113 else:
114 else:
114 raise error.ProgrammingError('%s @batchable generator emitted '
115 raise error.ProgrammingError('%s @batchable generator emitted '
115 'unexpected value count' % command)
116 'unexpected value count' % command)
116
117
117 yield finalfuture.value
118 yield finalfuture.value
118
119
119 # Forward a couple of names from peer to make wireproto interactions
120 # Forward a couple of names from peer to make wireproto interactions
120 # slightly more sensible.
121 # slightly more sensible.
121 batchable = peer.batchable
122 batchable = peer.batchable
122 future = peer.future
123 future = peer.future
123
124
124 # list of nodes encoding / decoding
125 # list of nodes encoding / decoding
125
126
126 def decodelist(l, sep=' '):
127 def decodelist(l, sep=' '):
127 if l:
128 if l:
128 return [bin(v) for v in l.split(sep)]
129 return [bin(v) for v in l.split(sep)]
129 return []
130 return []
130
131
131 def encodelist(l, sep=' '):
132 def encodelist(l, sep=' '):
132 try:
133 try:
133 return sep.join(map(hex, l))
134 return sep.join(map(hex, l))
134 except TypeError:
135 except TypeError:
135 raise
136 raise
136
137
137 # batched call argument encoding
138 # batched call argument encoding
138
139
139 def escapearg(plain):
140 def escapearg(plain):
140 return (plain
141 return (plain
141 .replace(':', ':c')
142 .replace(':', ':c')
142 .replace(',', ':o')
143 .replace(',', ':o')
143 .replace(';', ':s')
144 .replace(';', ':s')
144 .replace('=', ':e'))
145 .replace('=', ':e'))
145
146
146 def unescapearg(escaped):
147 def unescapearg(escaped):
147 return (escaped
148 return (escaped
148 .replace(':e', '=')
149 .replace(':e', '=')
149 .replace(':s', ';')
150 .replace(':s', ';')
150 .replace(':o', ',')
151 .replace(':o', ',')
151 .replace(':c', ':'))
152 .replace(':c', ':'))
152
153
153 def encodebatchcmds(req):
154 def encodebatchcmds(req):
154 """Return a ``cmds`` argument value for the ``batch`` command."""
155 """Return a ``cmds`` argument value for the ``batch`` command."""
155 cmds = []
156 cmds = []
156 for op, argsdict in req:
157 for op, argsdict in req:
157 # Old servers didn't properly unescape argument names. So prevent
158 # Old servers didn't properly unescape argument names. So prevent
158 # the sending of argument names that may not be decoded properly by
159 # the sending of argument names that may not be decoded properly by
159 # servers.
160 # servers.
160 assert all(escapearg(k) == k for k in argsdict)
161 assert all(escapearg(k) == k for k in argsdict)
161
162
162 args = ','.join('%s=%s' % (escapearg(k), escapearg(v))
163 args = ','.join('%s=%s' % (escapearg(k), escapearg(v))
163 for k, v in argsdict.iteritems())
164 for k, v in argsdict.iteritems())
164 cmds.append('%s %s' % (op, args))
165 cmds.append('%s %s' % (op, args))
165
166
166 return ';'.join(cmds)
167 return ';'.join(cmds)
167
168
168 # mapping of options accepted by getbundle and their types
169 # mapping of options accepted by getbundle and their types
169 #
170 #
170 # Meant to be extended by extensions. It is extensions responsibility to ensure
171 # Meant to be extended by extensions. It is extensions responsibility to ensure
171 # such options are properly processed in exchange.getbundle.
172 # such options are properly processed in exchange.getbundle.
172 #
173 #
173 # supported types are:
174 # supported types are:
174 #
175 #
175 # :nodes: list of binary nodes
176 # :nodes: list of binary nodes
176 # :csv: list of comma-separated values
177 # :csv: list of comma-separated values
177 # :scsv: list of comma-separated values return as set
178 # :scsv: list of comma-separated values return as set
178 # :plain: string with no transformation needed.
179 # :plain: string with no transformation needed.
179 gboptsmap = {'heads': 'nodes',
180 gboptsmap = {'heads': 'nodes',
180 'bookmarks': 'boolean',
181 'bookmarks': 'boolean',
181 'common': 'nodes',
182 'common': 'nodes',
182 'obsmarkers': 'boolean',
183 'obsmarkers': 'boolean',
183 'phases': 'boolean',
184 'phases': 'boolean',
184 'bundlecaps': 'scsv',
185 'bundlecaps': 'scsv',
185 'listkeys': 'csv',
186 'listkeys': 'csv',
186 'cg': 'boolean',
187 'cg': 'boolean',
187 'cbattempted': 'boolean',
188 'cbattempted': 'boolean',
188 'stream': 'boolean',
189 'stream': 'boolean',
189 }
190 }
190
191
191 # client side
192 # client side
192
193
193 class wirepeer(repository.legacypeer):
194 class wirepeer(repository.legacypeer):
194 """Client-side interface for communicating with a peer repository.
195 """Client-side interface for communicating with a peer repository.
195
196
196 Methods commonly call wire protocol commands of the same name.
197 Methods commonly call wire protocol commands of the same name.
197
198
198 See also httppeer.py and sshpeer.py for protocol-specific
199 See also httppeer.py and sshpeer.py for protocol-specific
199 implementations of this interface.
200 implementations of this interface.
200 """
201 """
201 # Begin of basewirepeer interface.
202 # Begin of basewirepeer interface.
202
203
203 def iterbatch(self):
204 def iterbatch(self):
204 return remoteiterbatcher(self)
205 return remoteiterbatcher(self)
205
206
206 @batchable
207 @batchable
207 def lookup(self, key):
208 def lookup(self, key):
208 self.requirecap('lookup', _('look up remote revision'))
209 self.requirecap('lookup', _('look up remote revision'))
209 f = future()
210 f = future()
210 yield {'key': encoding.fromlocal(key)}, f
211 yield {'key': encoding.fromlocal(key)}, f
211 d = f.value
212 d = f.value
212 success, data = d[:-1].split(" ", 1)
213 success, data = d[:-1].split(" ", 1)
213 if int(success):
214 if int(success):
214 yield bin(data)
215 yield bin(data)
215 else:
216 else:
216 self._abort(error.RepoError(data))
217 self._abort(error.RepoError(data))
217
218
218 @batchable
219 @batchable
219 def heads(self):
220 def heads(self):
220 f = future()
221 f = future()
221 yield {}, f
222 yield {}, f
222 d = f.value
223 d = f.value
223 try:
224 try:
224 yield decodelist(d[:-1])
225 yield decodelist(d[:-1])
225 except ValueError:
226 except ValueError:
226 self._abort(error.ResponseError(_("unexpected response:"), d))
227 self._abort(error.ResponseError(_("unexpected response:"), d))
227
228
228 @batchable
229 @batchable
229 def known(self, nodes):
230 def known(self, nodes):
230 f = future()
231 f = future()
231 yield {'nodes': encodelist(nodes)}, f
232 yield {'nodes': encodelist(nodes)}, f
232 d = f.value
233 d = f.value
233 try:
234 try:
234 yield [bool(int(b)) for b in d]
235 yield [bool(int(b)) for b in d]
235 except ValueError:
236 except ValueError:
236 self._abort(error.ResponseError(_("unexpected response:"), d))
237 self._abort(error.ResponseError(_("unexpected response:"), d))
237
238
238 @batchable
239 @batchable
239 def branchmap(self):
240 def branchmap(self):
240 f = future()
241 f = future()
241 yield {}, f
242 yield {}, f
242 d = f.value
243 d = f.value
243 try:
244 try:
244 branchmap = {}
245 branchmap = {}
245 for branchpart in d.splitlines():
246 for branchpart in d.splitlines():
246 branchname, branchheads = branchpart.split(' ', 1)
247 branchname, branchheads = branchpart.split(' ', 1)
247 branchname = encoding.tolocal(urlreq.unquote(branchname))
248 branchname = encoding.tolocal(urlreq.unquote(branchname))
248 branchheads = decodelist(branchheads)
249 branchheads = decodelist(branchheads)
249 branchmap[branchname] = branchheads
250 branchmap[branchname] = branchheads
250 yield branchmap
251 yield branchmap
251 except TypeError:
252 except TypeError:
252 self._abort(error.ResponseError(_("unexpected response:"), d))
253 self._abort(error.ResponseError(_("unexpected response:"), d))
253
254
254 @batchable
255 @batchable
255 def listkeys(self, namespace):
256 def listkeys(self, namespace):
256 if not self.capable('pushkey'):
257 if not self.capable('pushkey'):
257 yield {}, None
258 yield {}, None
258 f = future()
259 f = future()
259 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
260 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
260 yield {'namespace': encoding.fromlocal(namespace)}, f
261 yield {'namespace': encoding.fromlocal(namespace)}, f
261 d = f.value
262 d = f.value
262 self.ui.debug('received listkey for "%s": %i bytes\n'
263 self.ui.debug('received listkey for "%s": %i bytes\n'
263 % (namespace, len(d)))
264 % (namespace, len(d)))
264 yield pushkeymod.decodekeys(d)
265 yield pushkeymod.decodekeys(d)
265
266
266 @batchable
267 @batchable
267 def pushkey(self, namespace, key, old, new):
268 def pushkey(self, namespace, key, old, new):
268 if not self.capable('pushkey'):
269 if not self.capable('pushkey'):
269 yield False, None
270 yield False, None
270 f = future()
271 f = future()
271 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
272 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
272 yield {'namespace': encoding.fromlocal(namespace),
273 yield {'namespace': encoding.fromlocal(namespace),
273 'key': encoding.fromlocal(key),
274 'key': encoding.fromlocal(key),
274 'old': encoding.fromlocal(old),
275 'old': encoding.fromlocal(old),
275 'new': encoding.fromlocal(new)}, f
276 'new': encoding.fromlocal(new)}, f
276 d = f.value
277 d = f.value
277 d, output = d.split('\n', 1)
278 d, output = d.split('\n', 1)
278 try:
279 try:
279 d = bool(int(d))
280 d = bool(int(d))
280 except ValueError:
281 except ValueError:
281 raise error.ResponseError(
282 raise error.ResponseError(
282 _('push failed (unexpected response):'), d)
283 _('push failed (unexpected response):'), d)
283 for l in output.splitlines(True):
284 for l in output.splitlines(True):
284 self.ui.status(_('remote: '), l)
285 self.ui.status(_('remote: '), l)
285 yield d
286 yield d
286
287
287 def stream_out(self):
288 def stream_out(self):
288 return self._callstream('stream_out')
289 return self._callstream('stream_out')
289
290
290 def getbundle(self, source, **kwargs):
291 def getbundle(self, source, **kwargs):
291 kwargs = pycompat.byteskwargs(kwargs)
292 kwargs = pycompat.byteskwargs(kwargs)
292 self.requirecap('getbundle', _('look up remote changes'))
293 self.requirecap('getbundle', _('look up remote changes'))
293 opts = {}
294 opts = {}
294 bundlecaps = kwargs.get('bundlecaps')
295 bundlecaps = kwargs.get('bundlecaps')
295 if bundlecaps is not None:
296 if bundlecaps is not None:
296 kwargs['bundlecaps'] = sorted(bundlecaps)
297 kwargs['bundlecaps'] = sorted(bundlecaps)
297 else:
298 else:
298 bundlecaps = () # kwargs could have it to None
299 bundlecaps = () # kwargs could have it to None
299 for key, value in kwargs.iteritems():
300 for key, value in kwargs.iteritems():
300 if value is None:
301 if value is None:
301 continue
302 continue
302 keytype = gboptsmap.get(key)
303 keytype = gboptsmap.get(key)
303 if keytype is None:
304 if keytype is None:
304 raise error.ProgrammingError(
305 raise error.ProgrammingError(
305 'Unexpectedly None keytype for key %s' % key)
306 'Unexpectedly None keytype for key %s' % key)
306 elif keytype == 'nodes':
307 elif keytype == 'nodes':
307 value = encodelist(value)
308 value = encodelist(value)
308 elif keytype in ('csv', 'scsv'):
309 elif keytype in ('csv', 'scsv'):
309 value = ','.join(value)
310 value = ','.join(value)
310 elif keytype == 'boolean':
311 elif keytype == 'boolean':
311 value = '%i' % bool(value)
312 value = '%i' % bool(value)
312 elif keytype != 'plain':
313 elif keytype != 'plain':
313 raise KeyError('unknown getbundle option type %s'
314 raise KeyError('unknown getbundle option type %s'
314 % keytype)
315 % keytype)
315 opts[key] = value
316 opts[key] = value
316 f = self._callcompressable("getbundle", **pycompat.strkwargs(opts))
317 f = self._callcompressable("getbundle", **pycompat.strkwargs(opts))
317 if any((cap.startswith('HG2') for cap in bundlecaps)):
318 if any((cap.startswith('HG2') for cap in bundlecaps)):
318 return bundle2.getunbundler(self.ui, f)
319 return bundle2.getunbundler(self.ui, f)
319 else:
320 else:
320 return changegroupmod.cg1unpacker(f, 'UN')
321 return changegroupmod.cg1unpacker(f, 'UN')
321
322
322 def unbundle(self, cg, heads, url):
323 def unbundle(self, cg, heads, url):
323 '''Send cg (a readable file-like object representing the
324 '''Send cg (a readable file-like object representing the
324 changegroup to push, typically a chunkbuffer object) to the
325 changegroup to push, typically a chunkbuffer object) to the
325 remote server as a bundle.
326 remote server as a bundle.
326
327
327 When pushing a bundle10 stream, return an integer indicating the
328 When pushing a bundle10 stream, return an integer indicating the
328 result of the push (see changegroup.apply()).
329 result of the push (see changegroup.apply()).
329
330
330 When pushing a bundle20 stream, return a bundle20 stream.
331 When pushing a bundle20 stream, return a bundle20 stream.
331
332
332 `url` is the url the client thinks it's pushing to, which is
333 `url` is the url the client thinks it's pushing to, which is
333 visible to hooks.
334 visible to hooks.
334 '''
335 '''
335
336
336 if heads != ['force'] and self.capable('unbundlehash'):
337 if heads != ['force'] and self.capable('unbundlehash'):
337 heads = encodelist(['hashed',
338 heads = encodelist(['hashed',
338 hashlib.sha1(''.join(sorted(heads))).digest()])
339 hashlib.sha1(''.join(sorted(heads))).digest()])
339 else:
340 else:
340 heads = encodelist(heads)
341 heads = encodelist(heads)
341
342
342 if util.safehasattr(cg, 'deltaheader'):
343 if util.safehasattr(cg, 'deltaheader'):
343 # this a bundle10, do the old style call sequence
344 # this a bundle10, do the old style call sequence
344 ret, output = self._callpush("unbundle", cg, heads=heads)
345 ret, output = self._callpush("unbundle", cg, heads=heads)
345 if ret == "":
346 if ret == "":
346 raise error.ResponseError(
347 raise error.ResponseError(
347 _('push failed:'), output)
348 _('push failed:'), output)
348 try:
349 try:
349 ret = int(ret)
350 ret = int(ret)
350 except ValueError:
351 except ValueError:
351 raise error.ResponseError(
352 raise error.ResponseError(
352 _('push failed (unexpected response):'), ret)
353 _('push failed (unexpected response):'), ret)
353
354
354 for l in output.splitlines(True):
355 for l in output.splitlines(True):
355 self.ui.status(_('remote: '), l)
356 self.ui.status(_('remote: '), l)
356 else:
357 else:
357 # bundle2 push. Send a stream, fetch a stream.
358 # bundle2 push. Send a stream, fetch a stream.
358 stream = self._calltwowaystream('unbundle', cg, heads=heads)
359 stream = self._calltwowaystream('unbundle', cg, heads=heads)
359 ret = bundle2.getunbundler(self.ui, stream)
360 ret = bundle2.getunbundler(self.ui, stream)
360 return ret
361 return ret
361
362
362 # End of basewirepeer interface.
363 # End of basewirepeer interface.
363
364
364 # Begin of baselegacywirepeer interface.
365 # Begin of baselegacywirepeer interface.
365
366
366 def branches(self, nodes):
367 def branches(self, nodes):
367 n = encodelist(nodes)
368 n = encodelist(nodes)
368 d = self._call("branches", nodes=n)
369 d = self._call("branches", nodes=n)
369 try:
370 try:
370 br = [tuple(decodelist(b)) for b in d.splitlines()]
371 br = [tuple(decodelist(b)) for b in d.splitlines()]
371 return br
372 return br
372 except ValueError:
373 except ValueError:
373 self._abort(error.ResponseError(_("unexpected response:"), d))
374 self._abort(error.ResponseError(_("unexpected response:"), d))
374
375
375 def between(self, pairs):
376 def between(self, pairs):
376 batch = 8 # avoid giant requests
377 batch = 8 # avoid giant requests
377 r = []
378 r = []
378 for i in xrange(0, len(pairs), batch):
379 for i in xrange(0, len(pairs), batch):
379 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
380 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
380 d = self._call("between", pairs=n)
381 d = self._call("between", pairs=n)
381 try:
382 try:
382 r.extend(l and decodelist(l) or [] for l in d.splitlines())
383 r.extend(l and decodelist(l) or [] for l in d.splitlines())
383 except ValueError:
384 except ValueError:
384 self._abort(error.ResponseError(_("unexpected response:"), d))
385 self._abort(error.ResponseError(_("unexpected response:"), d))
385 return r
386 return r
386
387
387 def changegroup(self, nodes, kind):
388 def changegroup(self, nodes, kind):
388 n = encodelist(nodes)
389 n = encodelist(nodes)
389 f = self._callcompressable("changegroup", roots=n)
390 f = self._callcompressable("changegroup", roots=n)
390 return changegroupmod.cg1unpacker(f, 'UN')
391 return changegroupmod.cg1unpacker(f, 'UN')
391
392
392 def changegroupsubset(self, bases, heads, kind):
393 def changegroupsubset(self, bases, heads, kind):
393 self.requirecap('changegroupsubset', _('look up remote changes'))
394 self.requirecap('changegroupsubset', _('look up remote changes'))
394 bases = encodelist(bases)
395 bases = encodelist(bases)
395 heads = encodelist(heads)
396 heads = encodelist(heads)
396 f = self._callcompressable("changegroupsubset",
397 f = self._callcompressable("changegroupsubset",
397 bases=bases, heads=heads)
398 bases=bases, heads=heads)
398 return changegroupmod.cg1unpacker(f, 'UN')
399 return changegroupmod.cg1unpacker(f, 'UN')
399
400
400 # End of baselegacywirepeer interface.
401 # End of baselegacywirepeer interface.
401
402
402 def _submitbatch(self, req):
403 def _submitbatch(self, req):
403 """run batch request <req> on the server
404 """run batch request <req> on the server
404
405
405 Returns an iterator of the raw responses from the server.
406 Returns an iterator of the raw responses from the server.
406 """
407 """
407 ui = self.ui
408 ui = self.ui
408 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
409 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
409 ui.debug('devel-peer-request: batched-content\n')
410 ui.debug('devel-peer-request: batched-content\n')
410 for op, args in req:
411 for op, args in req:
411 msg = 'devel-peer-request: - %s (%d arguments)\n'
412 msg = 'devel-peer-request: - %s (%d arguments)\n'
412 ui.debug(msg % (op, len(args)))
413 ui.debug(msg % (op, len(args)))
413
414
414 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
415 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
415 chunk = rsp.read(1024)
416 chunk = rsp.read(1024)
416 work = [chunk]
417 work = [chunk]
417 while chunk:
418 while chunk:
418 while ';' not in chunk and chunk:
419 while ';' not in chunk and chunk:
419 chunk = rsp.read(1024)
420 chunk = rsp.read(1024)
420 work.append(chunk)
421 work.append(chunk)
421 merged = ''.join(work)
422 merged = ''.join(work)
422 while ';' in merged:
423 while ';' in merged:
423 one, merged = merged.split(';', 1)
424 one, merged = merged.split(';', 1)
424 yield unescapearg(one)
425 yield unescapearg(one)
425 chunk = rsp.read(1024)
426 chunk = rsp.read(1024)
426 work = [merged, chunk]
427 work = [merged, chunk]
427 yield unescapearg(''.join(work))
428 yield unescapearg(''.join(work))
428
429
429 def _submitone(self, op, args):
430 def _submitone(self, op, args):
430 return self._call(op, **pycompat.strkwargs(args))
431 return self._call(op, **pycompat.strkwargs(args))
431
432
432 def debugwireargs(self, one, two, three=None, four=None, five=None):
433 def debugwireargs(self, one, two, three=None, four=None, five=None):
433 # don't pass optional arguments left at their default value
434 # don't pass optional arguments left at their default value
434 opts = {}
435 opts = {}
435 if three is not None:
436 if three is not None:
436 opts[r'three'] = three
437 opts[r'three'] = three
437 if four is not None:
438 if four is not None:
438 opts[r'four'] = four
439 opts[r'four'] = four
439 return self._call('debugwireargs', one=one, two=two, **opts)
440 return self._call('debugwireargs', one=one, two=two, **opts)
440
441
441 def _call(self, cmd, **args):
442 def _call(self, cmd, **args):
442 """execute <cmd> on the server
443 """execute <cmd> on the server
443
444
444 The command is expected to return a simple string.
445 The command is expected to return a simple string.
445
446
446 returns the server reply as a string."""
447 returns the server reply as a string."""
447 raise NotImplementedError()
448 raise NotImplementedError()
448
449
449 def _callstream(self, cmd, **args):
450 def _callstream(self, cmd, **args):
450 """execute <cmd> on the server
451 """execute <cmd> on the server
451
452
452 The command is expected to return a stream. Note that if the
453 The command is expected to return a stream. Note that if the
453 command doesn't return a stream, _callstream behaves
454 command doesn't return a stream, _callstream behaves
454 differently for ssh and http peers.
455 differently for ssh and http peers.
455
456
456 returns the server reply as a file like object.
457 returns the server reply as a file like object.
457 """
458 """
458 raise NotImplementedError()
459 raise NotImplementedError()
459
460
460 def _callcompressable(self, cmd, **args):
461 def _callcompressable(self, cmd, **args):
461 """execute <cmd> on the server
462 """execute <cmd> on the server
462
463
463 The command is expected to return a stream.
464 The command is expected to return a stream.
464
465
465 The stream may have been compressed in some implementations. This
466 The stream may have been compressed in some implementations. This
466 function takes care of the decompression. This is the only difference
467 function takes care of the decompression. This is the only difference
467 with _callstream.
468 with _callstream.
468
469
469 returns the server reply as a file like object.
470 returns the server reply as a file like object.
470 """
471 """
471 raise NotImplementedError()
472 raise NotImplementedError()
472
473
473 def _callpush(self, cmd, fp, **args):
474 def _callpush(self, cmd, fp, **args):
474 """execute a <cmd> on server
475 """execute a <cmd> on server
475
476
476 The command is expected to be related to a push. Push has a special
477 The command is expected to be related to a push. Push has a special
477 return method.
478 return method.
478
479
479 returns the server reply as a (ret, output) tuple. ret is either
480 returns the server reply as a (ret, output) tuple. ret is either
480 empty (error) or a stringified int.
481 empty (error) or a stringified int.
481 """
482 """
482 raise NotImplementedError()
483 raise NotImplementedError()
483
484
484 def _calltwowaystream(self, cmd, fp, **args):
485 def _calltwowaystream(self, cmd, fp, **args):
485 """execute <cmd> on server
486 """execute <cmd> on server
486
487
487 The command will send a stream to the server and get a stream in reply.
488 The command will send a stream to the server and get a stream in reply.
488 """
489 """
489 raise NotImplementedError()
490 raise NotImplementedError()
490
491
491 def _abort(self, exception):
492 def _abort(self, exception):
492 """clearly abort the wire protocol connection and raise the exception
493 """clearly abort the wire protocol connection and raise the exception
493 """
494 """
494 raise NotImplementedError()
495 raise NotImplementedError()
495
496
496 # server side
497 # server side
497
498
498 # wire protocol command can either return a string or one of these classes.
499 # wire protocol command can either return a string or one of these classes.
499
500
500 def getdispatchrepo(repo, proto, command):
501 def getdispatchrepo(repo, proto, command):
501 """Obtain the repo used for processing wire protocol commands.
502 """Obtain the repo used for processing wire protocol commands.
502
503
503 The intent of this function is to serve as a monkeypatch point for
504 The intent of this function is to serve as a monkeypatch point for
504 extensions that need commands to operate on different repo views under
505 extensions that need commands to operate on different repo views under
505 specialized circumstances.
506 specialized circumstances.
506 """
507 """
507 return repo.filtered('served')
508 return repo.filtered('served')
508
509
509 def dispatch(repo, proto, command):
510 def dispatch(repo, proto, command):
510 repo = getdispatchrepo(repo, proto, command)
511 repo = getdispatchrepo(repo, proto, command)
511 func, spec = commands[command]
512 func, spec = commands[command]
512 args = proto.getargs(spec)
513 args = proto.getargs(spec)
513 return func(repo, proto, *args)
514 return func(repo, proto, *args)
514
515
515 def options(cmd, keys, others):
516 def options(cmd, keys, others):
516 opts = {}
517 opts = {}
517 for k in keys:
518 for k in keys:
518 if k in others:
519 if k in others:
519 opts[k] = others[k]
520 opts[k] = others[k]
520 del others[k]
521 del others[k]
521 if others:
522 if others:
522 util.stderr.write("warning: %s ignored unexpected arguments %s\n"
523 procutil.stderr.write("warning: %s ignored unexpected arguments %s\n"
523 % (cmd, ",".join(others)))
524 % (cmd, ",".join(others)))
524 return opts
525 return opts
525
526
526 def bundle1allowed(repo, action):
527 def bundle1allowed(repo, action):
527 """Whether a bundle1 operation is allowed from the server.
528 """Whether a bundle1 operation is allowed from the server.
528
529
529 Priority is:
530 Priority is:
530
531
531 1. server.bundle1gd.<action> (if generaldelta active)
532 1. server.bundle1gd.<action> (if generaldelta active)
532 2. server.bundle1.<action>
533 2. server.bundle1.<action>
533 3. server.bundle1gd (if generaldelta active)
534 3. server.bundle1gd (if generaldelta active)
534 4. server.bundle1
535 4. server.bundle1
535 """
536 """
536 ui = repo.ui
537 ui = repo.ui
537 gd = 'generaldelta' in repo.requirements
538 gd = 'generaldelta' in repo.requirements
538
539
539 if gd:
540 if gd:
540 v = ui.configbool('server', 'bundle1gd.%s' % action)
541 v = ui.configbool('server', 'bundle1gd.%s' % action)
541 if v is not None:
542 if v is not None:
542 return v
543 return v
543
544
544 v = ui.configbool('server', 'bundle1.%s' % action)
545 v = ui.configbool('server', 'bundle1.%s' % action)
545 if v is not None:
546 if v is not None:
546 return v
547 return v
547
548
548 if gd:
549 if gd:
549 v = ui.configbool('server', 'bundle1gd')
550 v = ui.configbool('server', 'bundle1gd')
550 if v is not None:
551 if v is not None:
551 return v
552 return v
552
553
553 return ui.configbool('server', 'bundle1')
554 return ui.configbool('server', 'bundle1')
554
555
555 def supportedcompengines(ui, role):
556 def supportedcompengines(ui, role):
556 """Obtain the list of supported compression engines for a request."""
557 """Obtain the list of supported compression engines for a request."""
557 assert role in (util.CLIENTROLE, util.SERVERROLE)
558 assert role in (util.CLIENTROLE, util.SERVERROLE)
558
559
559 compengines = util.compengines.supportedwireengines(role)
560 compengines = util.compengines.supportedwireengines(role)
560
561
561 # Allow config to override default list and ordering.
562 # Allow config to override default list and ordering.
562 if role == util.SERVERROLE:
563 if role == util.SERVERROLE:
563 configengines = ui.configlist('server', 'compressionengines')
564 configengines = ui.configlist('server', 'compressionengines')
564 config = 'server.compressionengines'
565 config = 'server.compressionengines'
565 else:
566 else:
566 # This is currently implemented mainly to facilitate testing. In most
567 # This is currently implemented mainly to facilitate testing. In most
567 # cases, the server should be in charge of choosing a compression engine
568 # cases, the server should be in charge of choosing a compression engine
568 # because a server has the most to lose from a sub-optimal choice. (e.g.
569 # because a server has the most to lose from a sub-optimal choice. (e.g.
569 # CPU DoS due to an expensive engine or a network DoS due to poor
570 # CPU DoS due to an expensive engine or a network DoS due to poor
570 # compression ratio).
571 # compression ratio).
571 configengines = ui.configlist('experimental',
572 configengines = ui.configlist('experimental',
572 'clientcompressionengines')
573 'clientcompressionengines')
573 config = 'experimental.clientcompressionengines'
574 config = 'experimental.clientcompressionengines'
574
575
575 # No explicit config. Filter out the ones that aren't supposed to be
576 # No explicit config. Filter out the ones that aren't supposed to be
576 # advertised and return default ordering.
577 # advertised and return default ordering.
577 if not configengines:
578 if not configengines:
578 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
579 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
579 return [e for e in compengines
580 return [e for e in compengines
580 if getattr(e.wireprotosupport(), attr) > 0]
581 if getattr(e.wireprotosupport(), attr) > 0]
581
582
582 # If compression engines are listed in the config, assume there is a good
583 # If compression engines are listed in the config, assume there is a good
583 # reason for it (like server operators wanting to achieve specific
584 # reason for it (like server operators wanting to achieve specific
584 # performance characteristics). So fail fast if the config references
585 # performance characteristics). So fail fast if the config references
585 # unusable compression engines.
586 # unusable compression engines.
586 validnames = set(e.name() for e in compengines)
587 validnames = set(e.name() for e in compengines)
587 invalidnames = set(e for e in configengines if e not in validnames)
588 invalidnames = set(e for e in configengines if e not in validnames)
588 if invalidnames:
589 if invalidnames:
589 raise error.Abort(_('invalid compression engine defined in %s: %s') %
590 raise error.Abort(_('invalid compression engine defined in %s: %s') %
590 (config, ', '.join(sorted(invalidnames))))
591 (config, ', '.join(sorted(invalidnames))))
591
592
592 compengines = [e for e in compengines if e.name() in configengines]
593 compengines = [e for e in compengines if e.name() in configengines]
593 compengines = sorted(compengines,
594 compengines = sorted(compengines,
594 key=lambda e: configengines.index(e.name()))
595 key=lambda e: configengines.index(e.name()))
595
596
596 if not compengines:
597 if not compengines:
597 raise error.Abort(_('%s config option does not specify any known '
598 raise error.Abort(_('%s config option does not specify any known '
598 'compression engines') % config,
599 'compression engines') % config,
599 hint=_('usable compression engines: %s') %
600 hint=_('usable compression engines: %s') %
600 ', '.sorted(validnames))
601 ', '.sorted(validnames))
601
602
602 return compengines
603 return compengines
603
604
604 class commandentry(object):
605 class commandentry(object):
605 """Represents a declared wire protocol command."""
606 """Represents a declared wire protocol command."""
606 def __init__(self, func, args='', transports=None,
607 def __init__(self, func, args='', transports=None,
607 permission='push'):
608 permission='push'):
608 self.func = func
609 self.func = func
609 self.args = args
610 self.args = args
610 self.transports = transports or set()
611 self.transports = transports or set()
611 self.permission = permission
612 self.permission = permission
612
613
613 def _merge(self, func, args):
614 def _merge(self, func, args):
614 """Merge this instance with an incoming 2-tuple.
615 """Merge this instance with an incoming 2-tuple.
615
616
616 This is called when a caller using the old 2-tuple API attempts
617 This is called when a caller using the old 2-tuple API attempts
617 to replace an instance. The incoming values are merged with
618 to replace an instance. The incoming values are merged with
618 data not captured by the 2-tuple and a new instance containing
619 data not captured by the 2-tuple and a new instance containing
619 the union of the two objects is returned.
620 the union of the two objects is returned.
620 """
621 """
621 return commandentry(func, args=args, transports=set(self.transports),
622 return commandentry(func, args=args, transports=set(self.transports),
622 permission=self.permission)
623 permission=self.permission)
623
624
624 # Old code treats instances as 2-tuples. So expose that interface.
625 # Old code treats instances as 2-tuples. So expose that interface.
625 def __iter__(self):
626 def __iter__(self):
626 yield self.func
627 yield self.func
627 yield self.args
628 yield self.args
628
629
629 def __getitem__(self, i):
630 def __getitem__(self, i):
630 if i == 0:
631 if i == 0:
631 return self.func
632 return self.func
632 elif i == 1:
633 elif i == 1:
633 return self.args
634 return self.args
634 else:
635 else:
635 raise IndexError('can only access elements 0 and 1')
636 raise IndexError('can only access elements 0 and 1')
636
637
637 class commanddict(dict):
638 class commanddict(dict):
638 """Container for registered wire protocol commands.
639 """Container for registered wire protocol commands.
639
640
640 It behaves like a dict. But __setitem__ is overwritten to allow silent
641 It behaves like a dict. But __setitem__ is overwritten to allow silent
641 coercion of values from 2-tuples for API compatibility.
642 coercion of values from 2-tuples for API compatibility.
642 """
643 """
643 def __setitem__(self, k, v):
644 def __setitem__(self, k, v):
644 if isinstance(v, commandentry):
645 if isinstance(v, commandentry):
645 pass
646 pass
646 # Cast 2-tuples to commandentry instances.
647 # Cast 2-tuples to commandentry instances.
647 elif isinstance(v, tuple):
648 elif isinstance(v, tuple):
648 if len(v) != 2:
649 if len(v) != 2:
649 raise ValueError('command tuples must have exactly 2 elements')
650 raise ValueError('command tuples must have exactly 2 elements')
650
651
651 # It is common for extensions to wrap wire protocol commands via
652 # It is common for extensions to wrap wire protocol commands via
652 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
653 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
653 # doing this aren't aware of the new API that uses objects to store
654 # doing this aren't aware of the new API that uses objects to store
654 # command entries, we automatically merge old state with new.
655 # command entries, we automatically merge old state with new.
655 if k in self:
656 if k in self:
656 v = self[k]._merge(v[0], v[1])
657 v = self[k]._merge(v[0], v[1])
657 else:
658 else:
658 # Use default values from @wireprotocommand.
659 # Use default values from @wireprotocommand.
659 v = commandentry(v[0], args=v[1],
660 v = commandentry(v[0], args=v[1],
660 transports=set(wireprototypes.TRANSPORTS),
661 transports=set(wireprototypes.TRANSPORTS),
661 permission='push')
662 permission='push')
662 else:
663 else:
663 raise ValueError('command entries must be commandentry instances '
664 raise ValueError('command entries must be commandentry instances '
664 'or 2-tuples')
665 'or 2-tuples')
665
666
666 return super(commanddict, self).__setitem__(k, v)
667 return super(commanddict, self).__setitem__(k, v)
667
668
668 def commandavailable(self, command, proto):
669 def commandavailable(self, command, proto):
669 """Determine if a command is available for the requested protocol."""
670 """Determine if a command is available for the requested protocol."""
670 assert proto.name in wireprototypes.TRANSPORTS
671 assert proto.name in wireprototypes.TRANSPORTS
671
672
672 entry = self.get(command)
673 entry = self.get(command)
673
674
674 if not entry:
675 if not entry:
675 return False
676 return False
676
677
677 if proto.name not in entry.transports:
678 if proto.name not in entry.transports:
678 return False
679 return False
679
680
680 return True
681 return True
681
682
682 # Constants specifying which transports a wire protocol command should be
683 # Constants specifying which transports a wire protocol command should be
683 # available on. For use with @wireprotocommand.
684 # available on. For use with @wireprotocommand.
684 POLICY_ALL = 'all'
685 POLICY_ALL = 'all'
685 POLICY_V1_ONLY = 'v1-only'
686 POLICY_V1_ONLY = 'v1-only'
686 POLICY_V2_ONLY = 'v2-only'
687 POLICY_V2_ONLY = 'v2-only'
687
688
688 commands = commanddict()
689 commands = commanddict()
689
690
690 def wireprotocommand(name, args='', transportpolicy=POLICY_ALL,
691 def wireprotocommand(name, args='', transportpolicy=POLICY_ALL,
691 permission='push'):
692 permission='push'):
692 """Decorator to declare a wire protocol command.
693 """Decorator to declare a wire protocol command.
693
694
694 ``name`` is the name of the wire protocol command being provided.
695 ``name`` is the name of the wire protocol command being provided.
695
696
696 ``args`` is a space-delimited list of named arguments that the command
697 ``args`` is a space-delimited list of named arguments that the command
697 accepts. ``*`` is a special value that says to accept all arguments.
698 accepts. ``*`` is a special value that says to accept all arguments.
698
699
699 ``transportpolicy`` is a POLICY_* constant denoting which transports
700 ``transportpolicy`` is a POLICY_* constant denoting which transports
700 this wire protocol command should be exposed to. By default, commands
701 this wire protocol command should be exposed to. By default, commands
701 are exposed to all wire protocol transports.
702 are exposed to all wire protocol transports.
702
703
703 ``permission`` defines the permission type needed to run this command.
704 ``permission`` defines the permission type needed to run this command.
704 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
705 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
705 respectively. Default is to assume command requires ``push`` permissions
706 respectively. Default is to assume command requires ``push`` permissions
706 because otherwise commands not declaring their permissions could modify
707 because otherwise commands not declaring their permissions could modify
707 a repository that is supposed to be read-only.
708 a repository that is supposed to be read-only.
708 """
709 """
709 if transportpolicy == POLICY_ALL:
710 if transportpolicy == POLICY_ALL:
710 transports = set(wireprototypes.TRANSPORTS)
711 transports = set(wireprototypes.TRANSPORTS)
711 elif transportpolicy == POLICY_V1_ONLY:
712 elif transportpolicy == POLICY_V1_ONLY:
712 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
713 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
713 if v['version'] == 1}
714 if v['version'] == 1}
714 elif transportpolicy == POLICY_V2_ONLY:
715 elif transportpolicy == POLICY_V2_ONLY:
715 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
716 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
716 if v['version'] == 2}
717 if v['version'] == 2}
717 else:
718 else:
718 raise error.ProgrammingError('invalid transport policy value: %s' %
719 raise error.ProgrammingError('invalid transport policy value: %s' %
719 transportpolicy)
720 transportpolicy)
720
721
721 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
722 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
722 # SSHv2.
723 # SSHv2.
723 # TODO undo this hack when SSH is using the unified frame protocol.
724 # TODO undo this hack when SSH is using the unified frame protocol.
724 if name == b'batch':
725 if name == b'batch':
725 transports.add(wireprototypes.SSHV2)
726 transports.add(wireprototypes.SSHV2)
726
727
727 if permission not in ('push', 'pull'):
728 if permission not in ('push', 'pull'):
728 raise error.ProgrammingError('invalid wire protocol permission; '
729 raise error.ProgrammingError('invalid wire protocol permission; '
729 'got %s; expected "push" or "pull"' %
730 'got %s; expected "push" or "pull"' %
730 permission)
731 permission)
731
732
732 def register(func):
733 def register(func):
733 commands[name] = commandentry(func, args=args, transports=transports,
734 commands[name] = commandentry(func, args=args, transports=transports,
734 permission=permission)
735 permission=permission)
735 return func
736 return func
736 return register
737 return register
737
738
738 # TODO define a more appropriate permissions type to use for this.
739 # TODO define a more appropriate permissions type to use for this.
739 @wireprotocommand('batch', 'cmds *', permission='pull',
740 @wireprotocommand('batch', 'cmds *', permission='pull',
740 transportpolicy=POLICY_V1_ONLY)
741 transportpolicy=POLICY_V1_ONLY)
741 def batch(repo, proto, cmds, others):
742 def batch(repo, proto, cmds, others):
742 repo = repo.filtered("served")
743 repo = repo.filtered("served")
743 res = []
744 res = []
744 for pair in cmds.split(';'):
745 for pair in cmds.split(';'):
745 op, args = pair.split(' ', 1)
746 op, args = pair.split(' ', 1)
746 vals = {}
747 vals = {}
747 for a in args.split(','):
748 for a in args.split(','):
748 if a:
749 if a:
749 n, v = a.split('=')
750 n, v = a.split('=')
750 vals[unescapearg(n)] = unescapearg(v)
751 vals[unescapearg(n)] = unescapearg(v)
751 func, spec = commands[op]
752 func, spec = commands[op]
752
753
753 # Validate that client has permissions to perform this command.
754 # Validate that client has permissions to perform this command.
754 perm = commands[op].permission
755 perm = commands[op].permission
755 assert perm in ('push', 'pull')
756 assert perm in ('push', 'pull')
756 proto.checkperm(perm)
757 proto.checkperm(perm)
757
758
758 if spec:
759 if spec:
759 keys = spec.split()
760 keys = spec.split()
760 data = {}
761 data = {}
761 for k in keys:
762 for k in keys:
762 if k == '*':
763 if k == '*':
763 star = {}
764 star = {}
764 for key in vals.keys():
765 for key in vals.keys():
765 if key not in keys:
766 if key not in keys:
766 star[key] = vals[key]
767 star[key] = vals[key]
767 data['*'] = star
768 data['*'] = star
768 else:
769 else:
769 data[k] = vals[k]
770 data[k] = vals[k]
770 result = func(repo, proto, *[data[k] for k in keys])
771 result = func(repo, proto, *[data[k] for k in keys])
771 else:
772 else:
772 result = func(repo, proto)
773 result = func(repo, proto)
773 if isinstance(result, ooberror):
774 if isinstance(result, ooberror):
774 return result
775 return result
775
776
776 # For now, all batchable commands must return bytesresponse or
777 # For now, all batchable commands must return bytesresponse or
777 # raw bytes (for backwards compatibility).
778 # raw bytes (for backwards compatibility).
778 assert isinstance(result, (bytesresponse, bytes))
779 assert isinstance(result, (bytesresponse, bytes))
779 if isinstance(result, bytesresponse):
780 if isinstance(result, bytesresponse):
780 result = result.data
781 result = result.data
781 res.append(escapearg(result))
782 res.append(escapearg(result))
782
783
783 return bytesresponse(';'.join(res))
784 return bytesresponse(';'.join(res))
784
785
785 @wireprotocommand('between', 'pairs', transportpolicy=POLICY_V1_ONLY,
786 @wireprotocommand('between', 'pairs', transportpolicy=POLICY_V1_ONLY,
786 permission='pull')
787 permission='pull')
787 def between(repo, proto, pairs):
788 def between(repo, proto, pairs):
788 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
789 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
789 r = []
790 r = []
790 for b in repo.between(pairs):
791 for b in repo.between(pairs):
791 r.append(encodelist(b) + "\n")
792 r.append(encodelist(b) + "\n")
792
793
793 return bytesresponse(''.join(r))
794 return bytesresponse(''.join(r))
794
795
795 @wireprotocommand('branchmap', permission='pull')
796 @wireprotocommand('branchmap', permission='pull')
796 def branchmap(repo, proto):
797 def branchmap(repo, proto):
797 branchmap = repo.branchmap()
798 branchmap = repo.branchmap()
798 heads = []
799 heads = []
799 for branch, nodes in branchmap.iteritems():
800 for branch, nodes in branchmap.iteritems():
800 branchname = urlreq.quote(encoding.fromlocal(branch))
801 branchname = urlreq.quote(encoding.fromlocal(branch))
801 branchnodes = encodelist(nodes)
802 branchnodes = encodelist(nodes)
802 heads.append('%s %s' % (branchname, branchnodes))
803 heads.append('%s %s' % (branchname, branchnodes))
803
804
804 return bytesresponse('\n'.join(heads))
805 return bytesresponse('\n'.join(heads))
805
806
806 @wireprotocommand('branches', 'nodes', transportpolicy=POLICY_V1_ONLY,
807 @wireprotocommand('branches', 'nodes', transportpolicy=POLICY_V1_ONLY,
807 permission='pull')
808 permission='pull')
808 def branches(repo, proto, nodes):
809 def branches(repo, proto, nodes):
809 nodes = decodelist(nodes)
810 nodes = decodelist(nodes)
810 r = []
811 r = []
811 for b in repo.branches(nodes):
812 for b in repo.branches(nodes):
812 r.append(encodelist(b) + "\n")
813 r.append(encodelist(b) + "\n")
813
814
814 return bytesresponse(''.join(r))
815 return bytesresponse(''.join(r))
815
816
816 @wireprotocommand('clonebundles', '', permission='pull')
817 @wireprotocommand('clonebundles', '', permission='pull')
817 def clonebundles(repo, proto):
818 def clonebundles(repo, proto):
818 """Server command for returning info for available bundles to seed clones.
819 """Server command for returning info for available bundles to seed clones.
819
820
820 Clients will parse this response and determine what bundle to fetch.
821 Clients will parse this response and determine what bundle to fetch.
821
822
822 Extensions may wrap this command to filter or dynamically emit data
823 Extensions may wrap this command to filter or dynamically emit data
823 depending on the request. e.g. you could advertise URLs for the closest
824 depending on the request. e.g. you could advertise URLs for the closest
824 data center given the client's IP address.
825 data center given the client's IP address.
825 """
826 """
826 return bytesresponse(repo.vfs.tryread('clonebundles.manifest'))
827 return bytesresponse(repo.vfs.tryread('clonebundles.manifest'))
827
828
828 wireprotocaps = ['lookup', 'branchmap', 'pushkey',
829 wireprotocaps = ['lookup', 'branchmap', 'pushkey',
829 'known', 'getbundle', 'unbundlehash']
830 'known', 'getbundle', 'unbundlehash']
830
831
831 def _capabilities(repo, proto):
832 def _capabilities(repo, proto):
832 """return a list of capabilities for a repo
833 """return a list of capabilities for a repo
833
834
834 This function exists to allow extensions to easily wrap capabilities
835 This function exists to allow extensions to easily wrap capabilities
835 computation
836 computation
836
837
837 - returns a lists: easy to alter
838 - returns a lists: easy to alter
838 - change done here will be propagated to both `capabilities` and `hello`
839 - change done here will be propagated to both `capabilities` and `hello`
839 command without any other action needed.
840 command without any other action needed.
840 """
841 """
841 # copy to prevent modification of the global list
842 # copy to prevent modification of the global list
842 caps = list(wireprotocaps)
843 caps = list(wireprotocaps)
843
844
844 # Command of same name as capability isn't exposed to version 1 of
845 # Command of same name as capability isn't exposed to version 1 of
845 # transports. So conditionally add it.
846 # transports. So conditionally add it.
846 if commands.commandavailable('changegroupsubset', proto):
847 if commands.commandavailable('changegroupsubset', proto):
847 caps.append('changegroupsubset')
848 caps.append('changegroupsubset')
848
849
849 if streamclone.allowservergeneration(repo):
850 if streamclone.allowservergeneration(repo):
850 if repo.ui.configbool('server', 'preferuncompressed'):
851 if repo.ui.configbool('server', 'preferuncompressed'):
851 caps.append('stream-preferred')
852 caps.append('stream-preferred')
852 requiredformats = repo.requirements & repo.supportedformats
853 requiredformats = repo.requirements & repo.supportedformats
853 # if our local revlogs are just revlogv1, add 'stream' cap
854 # if our local revlogs are just revlogv1, add 'stream' cap
854 if not requiredformats - {'revlogv1'}:
855 if not requiredformats - {'revlogv1'}:
855 caps.append('stream')
856 caps.append('stream')
856 # otherwise, add 'streamreqs' detailing our local revlog format
857 # otherwise, add 'streamreqs' detailing our local revlog format
857 else:
858 else:
858 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
859 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
859 if repo.ui.configbool('experimental', 'bundle2-advertise'):
860 if repo.ui.configbool('experimental', 'bundle2-advertise'):
860 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
861 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
861 caps.append('bundle2=' + urlreq.quote(capsblob))
862 caps.append('bundle2=' + urlreq.quote(capsblob))
862 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
863 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
863
864
864 return proto.addcapabilities(repo, caps)
865 return proto.addcapabilities(repo, caps)
865
866
866 # If you are writing an extension and consider wrapping this function. Wrap
867 # If you are writing an extension and consider wrapping this function. Wrap
867 # `_capabilities` instead.
868 # `_capabilities` instead.
868 @wireprotocommand('capabilities', permission='pull')
869 @wireprotocommand('capabilities', permission='pull')
869 def capabilities(repo, proto):
870 def capabilities(repo, proto):
870 return bytesresponse(' '.join(_capabilities(repo, proto)))
871 return bytesresponse(' '.join(_capabilities(repo, proto)))
871
872
872 @wireprotocommand('changegroup', 'roots', transportpolicy=POLICY_V1_ONLY,
873 @wireprotocommand('changegroup', 'roots', transportpolicy=POLICY_V1_ONLY,
873 permission='pull')
874 permission='pull')
874 def changegroup(repo, proto, roots):
875 def changegroup(repo, proto, roots):
875 nodes = decodelist(roots)
876 nodes = decodelist(roots)
876 outgoing = discovery.outgoing(repo, missingroots=nodes,
877 outgoing = discovery.outgoing(repo, missingroots=nodes,
877 missingheads=repo.heads())
878 missingheads=repo.heads())
878 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
879 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
879 gen = iter(lambda: cg.read(32768), '')
880 gen = iter(lambda: cg.read(32768), '')
880 return streamres(gen=gen)
881 return streamres(gen=gen)
881
882
882 @wireprotocommand('changegroupsubset', 'bases heads',
883 @wireprotocommand('changegroupsubset', 'bases heads',
883 transportpolicy=POLICY_V1_ONLY,
884 transportpolicy=POLICY_V1_ONLY,
884 permission='pull')
885 permission='pull')
885 def changegroupsubset(repo, proto, bases, heads):
886 def changegroupsubset(repo, proto, bases, heads):
886 bases = decodelist(bases)
887 bases = decodelist(bases)
887 heads = decodelist(heads)
888 heads = decodelist(heads)
888 outgoing = discovery.outgoing(repo, missingroots=bases,
889 outgoing = discovery.outgoing(repo, missingroots=bases,
889 missingheads=heads)
890 missingheads=heads)
890 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
891 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
891 gen = iter(lambda: cg.read(32768), '')
892 gen = iter(lambda: cg.read(32768), '')
892 return streamres(gen=gen)
893 return streamres(gen=gen)
893
894
894 @wireprotocommand('debugwireargs', 'one two *',
895 @wireprotocommand('debugwireargs', 'one two *',
895 permission='pull')
896 permission='pull')
896 def debugwireargs(repo, proto, one, two, others):
897 def debugwireargs(repo, proto, one, two, others):
897 # only accept optional args from the known set
898 # only accept optional args from the known set
898 opts = options('debugwireargs', ['three', 'four'], others)
899 opts = options('debugwireargs', ['three', 'four'], others)
899 return bytesresponse(repo.debugwireargs(one, two,
900 return bytesresponse(repo.debugwireargs(one, two,
900 **pycompat.strkwargs(opts)))
901 **pycompat.strkwargs(opts)))
901
902
902 @wireprotocommand('getbundle', '*', permission='pull')
903 @wireprotocommand('getbundle', '*', permission='pull')
903 def getbundle(repo, proto, others):
904 def getbundle(repo, proto, others):
904 opts = options('getbundle', gboptsmap.keys(), others)
905 opts = options('getbundle', gboptsmap.keys(), others)
905 for k, v in opts.iteritems():
906 for k, v in opts.iteritems():
906 keytype = gboptsmap[k]
907 keytype = gboptsmap[k]
907 if keytype == 'nodes':
908 if keytype == 'nodes':
908 opts[k] = decodelist(v)
909 opts[k] = decodelist(v)
909 elif keytype == 'csv':
910 elif keytype == 'csv':
910 opts[k] = list(v.split(','))
911 opts[k] = list(v.split(','))
911 elif keytype == 'scsv':
912 elif keytype == 'scsv':
912 opts[k] = set(v.split(','))
913 opts[k] = set(v.split(','))
913 elif keytype == 'boolean':
914 elif keytype == 'boolean':
914 # Client should serialize False as '0', which is a non-empty string
915 # Client should serialize False as '0', which is a non-empty string
915 # so it evaluates as a True bool.
916 # so it evaluates as a True bool.
916 if v == '0':
917 if v == '0':
917 opts[k] = False
918 opts[k] = False
918 else:
919 else:
919 opts[k] = bool(v)
920 opts[k] = bool(v)
920 elif keytype != 'plain':
921 elif keytype != 'plain':
921 raise KeyError('unknown getbundle option type %s'
922 raise KeyError('unknown getbundle option type %s'
922 % keytype)
923 % keytype)
923
924
924 if not bundle1allowed(repo, 'pull'):
925 if not bundle1allowed(repo, 'pull'):
925 if not exchange.bundle2requested(opts.get('bundlecaps')):
926 if not exchange.bundle2requested(opts.get('bundlecaps')):
926 if proto.name == 'http-v1':
927 if proto.name == 'http-v1':
927 return ooberror(bundle2required)
928 return ooberror(bundle2required)
928 raise error.Abort(bundle2requiredmain,
929 raise error.Abort(bundle2requiredmain,
929 hint=bundle2requiredhint)
930 hint=bundle2requiredhint)
930
931
931 prefercompressed = True
932 prefercompressed = True
932
933
933 try:
934 try:
934 if repo.ui.configbool('server', 'disablefullbundle'):
935 if repo.ui.configbool('server', 'disablefullbundle'):
935 # Check to see if this is a full clone.
936 # Check to see if this is a full clone.
936 clheads = set(repo.changelog.heads())
937 clheads = set(repo.changelog.heads())
937 changegroup = opts.get('cg', True)
938 changegroup = opts.get('cg', True)
938 heads = set(opts.get('heads', set()))
939 heads = set(opts.get('heads', set()))
939 common = set(opts.get('common', set()))
940 common = set(opts.get('common', set()))
940 common.discard(nullid)
941 common.discard(nullid)
941 if changegroup and not common and clheads == heads:
942 if changegroup and not common and clheads == heads:
942 raise error.Abort(
943 raise error.Abort(
943 _('server has pull-based clones disabled'),
944 _('server has pull-based clones disabled'),
944 hint=_('remove --pull if specified or upgrade Mercurial'))
945 hint=_('remove --pull if specified or upgrade Mercurial'))
945
946
946 info, chunks = exchange.getbundlechunks(repo, 'serve',
947 info, chunks = exchange.getbundlechunks(repo, 'serve',
947 **pycompat.strkwargs(opts))
948 **pycompat.strkwargs(opts))
948 prefercompressed = info.get('prefercompressed', True)
949 prefercompressed = info.get('prefercompressed', True)
949 except error.Abort as exc:
950 except error.Abort as exc:
950 # cleanly forward Abort error to the client
951 # cleanly forward Abort error to the client
951 if not exchange.bundle2requested(opts.get('bundlecaps')):
952 if not exchange.bundle2requested(opts.get('bundlecaps')):
952 if proto.name == 'http-v1':
953 if proto.name == 'http-v1':
953 return ooberror(pycompat.bytestr(exc) + '\n')
954 return ooberror(pycompat.bytestr(exc) + '\n')
954 raise # cannot do better for bundle1 + ssh
955 raise # cannot do better for bundle1 + ssh
955 # bundle2 request expect a bundle2 reply
956 # bundle2 request expect a bundle2 reply
956 bundler = bundle2.bundle20(repo.ui)
957 bundler = bundle2.bundle20(repo.ui)
957 manargs = [('message', pycompat.bytestr(exc))]
958 manargs = [('message', pycompat.bytestr(exc))]
958 advargs = []
959 advargs = []
959 if exc.hint is not None:
960 if exc.hint is not None:
960 advargs.append(('hint', exc.hint))
961 advargs.append(('hint', exc.hint))
961 bundler.addpart(bundle2.bundlepart('error:abort',
962 bundler.addpart(bundle2.bundlepart('error:abort',
962 manargs, advargs))
963 manargs, advargs))
963 chunks = bundler.getchunks()
964 chunks = bundler.getchunks()
964 prefercompressed = False
965 prefercompressed = False
965
966
966 return streamres(gen=chunks, prefer_uncompressed=not prefercompressed)
967 return streamres(gen=chunks, prefer_uncompressed=not prefercompressed)
967
968
968 @wireprotocommand('heads', permission='pull')
969 @wireprotocommand('heads', permission='pull')
969 def heads(repo, proto):
970 def heads(repo, proto):
970 h = repo.heads()
971 h = repo.heads()
971 return bytesresponse(encodelist(h) + '\n')
972 return bytesresponse(encodelist(h) + '\n')
972
973
973 @wireprotocommand('hello', permission='pull')
974 @wireprotocommand('hello', permission='pull')
974 def hello(repo, proto):
975 def hello(repo, proto):
975 """Called as part of SSH handshake to obtain server info.
976 """Called as part of SSH handshake to obtain server info.
976
977
977 Returns a list of lines describing interesting things about the
978 Returns a list of lines describing interesting things about the
978 server, in an RFC822-like format.
979 server, in an RFC822-like format.
979
980
980 Currently, the only one defined is ``capabilities``, which consists of a
981 Currently, the only one defined is ``capabilities``, which consists of a
981 line of space separated tokens describing server abilities:
982 line of space separated tokens describing server abilities:
982
983
983 capabilities: <token0> <token1> <token2>
984 capabilities: <token0> <token1> <token2>
984 """
985 """
985 caps = capabilities(repo, proto).data
986 caps = capabilities(repo, proto).data
986 return bytesresponse('capabilities: %s\n' % caps)
987 return bytesresponse('capabilities: %s\n' % caps)
987
988
988 @wireprotocommand('listkeys', 'namespace', permission='pull')
989 @wireprotocommand('listkeys', 'namespace', permission='pull')
989 def listkeys(repo, proto, namespace):
990 def listkeys(repo, proto, namespace):
990 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
991 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
991 return bytesresponse(pushkeymod.encodekeys(d))
992 return bytesresponse(pushkeymod.encodekeys(d))
992
993
993 @wireprotocommand('lookup', 'key', permission='pull')
994 @wireprotocommand('lookup', 'key', permission='pull')
994 def lookup(repo, proto, key):
995 def lookup(repo, proto, key):
995 try:
996 try:
996 k = encoding.tolocal(key)
997 k = encoding.tolocal(key)
997 c = repo[k]
998 c = repo[k]
998 r = c.hex()
999 r = c.hex()
999 success = 1
1000 success = 1
1000 except Exception as inst:
1001 except Exception as inst:
1001 r = stringutil.forcebytestr(inst)
1002 r = stringutil.forcebytestr(inst)
1002 success = 0
1003 success = 0
1003 return bytesresponse('%d %s\n' % (success, r))
1004 return bytesresponse('%d %s\n' % (success, r))
1004
1005
1005 @wireprotocommand('known', 'nodes *', permission='pull')
1006 @wireprotocommand('known', 'nodes *', permission='pull')
1006 def known(repo, proto, nodes, others):
1007 def known(repo, proto, nodes, others):
1007 v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
1008 v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
1008 return bytesresponse(v)
1009 return bytesresponse(v)
1009
1010
1010 @wireprotocommand('pushkey', 'namespace key old new', permission='push')
1011 @wireprotocommand('pushkey', 'namespace key old new', permission='push')
1011 def pushkey(repo, proto, namespace, key, old, new):
1012 def pushkey(repo, proto, namespace, key, old, new):
1012 # compatibility with pre-1.8 clients which were accidentally
1013 # compatibility with pre-1.8 clients which were accidentally
1013 # sending raw binary nodes rather than utf-8-encoded hex
1014 # sending raw binary nodes rather than utf-8-encoded hex
1014 if len(new) == 20 and stringutil.escapestr(new) != new:
1015 if len(new) == 20 and stringutil.escapestr(new) != new:
1015 # looks like it could be a binary node
1016 # looks like it could be a binary node
1016 try:
1017 try:
1017 new.decode('utf-8')
1018 new.decode('utf-8')
1018 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
1019 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
1019 except UnicodeDecodeError:
1020 except UnicodeDecodeError:
1020 pass # binary, leave unmodified
1021 pass # binary, leave unmodified
1021 else:
1022 else:
1022 new = encoding.tolocal(new) # normal path
1023 new = encoding.tolocal(new) # normal path
1023
1024
1024 with proto.mayberedirectstdio() as output:
1025 with proto.mayberedirectstdio() as output:
1025 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
1026 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
1026 encoding.tolocal(old), new) or False
1027 encoding.tolocal(old), new) or False
1027
1028
1028 output = output.getvalue() if output else ''
1029 output = output.getvalue() if output else ''
1029 return bytesresponse('%d\n%s' % (int(r), output))
1030 return bytesresponse('%d\n%s' % (int(r), output))
1030
1031
1031 @wireprotocommand('stream_out', permission='pull')
1032 @wireprotocommand('stream_out', permission='pull')
1032 def stream(repo, proto):
1033 def stream(repo, proto):
1033 '''If the server supports streaming clone, it advertises the "stream"
1034 '''If the server supports streaming clone, it advertises the "stream"
1034 capability with a value representing the version and flags of the repo
1035 capability with a value representing the version and flags of the repo
1035 it is serving. Client checks to see if it understands the format.
1036 it is serving. Client checks to see if it understands the format.
1036 '''
1037 '''
1037 return streamres_legacy(streamclone.generatev1wireproto(repo))
1038 return streamres_legacy(streamclone.generatev1wireproto(repo))
1038
1039
1039 @wireprotocommand('unbundle', 'heads', permission='push')
1040 @wireprotocommand('unbundle', 'heads', permission='push')
1040 def unbundle(repo, proto, heads):
1041 def unbundle(repo, proto, heads):
1041 their_heads = decodelist(heads)
1042 their_heads = decodelist(heads)
1042
1043
1043 with proto.mayberedirectstdio() as output:
1044 with proto.mayberedirectstdio() as output:
1044 try:
1045 try:
1045 exchange.check_heads(repo, their_heads, 'preparing changes')
1046 exchange.check_heads(repo, their_heads, 'preparing changes')
1046
1047
1047 # write bundle data to temporary file because it can be big
1048 # write bundle data to temporary file because it can be big
1048 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1049 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1049 fp = os.fdopen(fd, r'wb+')
1050 fp = os.fdopen(fd, r'wb+')
1050 r = 0
1051 r = 0
1051 try:
1052 try:
1052 proto.forwardpayload(fp)
1053 proto.forwardpayload(fp)
1053 fp.seek(0)
1054 fp.seek(0)
1054 gen = exchange.readbundle(repo.ui, fp, None)
1055 gen = exchange.readbundle(repo.ui, fp, None)
1055 if (isinstance(gen, changegroupmod.cg1unpacker)
1056 if (isinstance(gen, changegroupmod.cg1unpacker)
1056 and not bundle1allowed(repo, 'push')):
1057 and not bundle1allowed(repo, 'push')):
1057 if proto.name == 'http-v1':
1058 if proto.name == 'http-v1':
1058 # need to special case http because stderr do not get to
1059 # need to special case http because stderr do not get to
1059 # the http client on failed push so we need to abuse
1060 # the http client on failed push so we need to abuse
1060 # some other error type to make sure the message get to
1061 # some other error type to make sure the message get to
1061 # the user.
1062 # the user.
1062 return ooberror(bundle2required)
1063 return ooberror(bundle2required)
1063 raise error.Abort(bundle2requiredmain,
1064 raise error.Abort(bundle2requiredmain,
1064 hint=bundle2requiredhint)
1065 hint=bundle2requiredhint)
1065
1066
1066 r = exchange.unbundle(repo, gen, their_heads, 'serve',
1067 r = exchange.unbundle(repo, gen, their_heads, 'serve',
1067 proto.client())
1068 proto.client())
1068 if util.safehasattr(r, 'addpart'):
1069 if util.safehasattr(r, 'addpart'):
1069 # The return looks streamable, we are in the bundle2 case
1070 # The return looks streamable, we are in the bundle2 case
1070 # and should return a stream.
1071 # and should return a stream.
1071 return streamres_legacy(gen=r.getchunks())
1072 return streamres_legacy(gen=r.getchunks())
1072 return pushres(r, output.getvalue() if output else '')
1073 return pushres(r, output.getvalue() if output else '')
1073
1074
1074 finally:
1075 finally:
1075 fp.close()
1076 fp.close()
1076 os.unlink(tempname)
1077 os.unlink(tempname)
1077
1078
1078 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
1079 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
1079 # handle non-bundle2 case first
1080 # handle non-bundle2 case first
1080 if not getattr(exc, 'duringunbundle2', False):
1081 if not getattr(exc, 'duringunbundle2', False):
1081 try:
1082 try:
1082 raise
1083 raise
1083 except error.Abort:
1084 except error.Abort:
1084 # The old code we moved used util.stderr directly.
1085 # The old code we moved used procutil.stderr directly.
1085 # We did not change it to minimise code change.
1086 # We did not change it to minimise code change.
1086 # This need to be moved to something proper.
1087 # This need to be moved to something proper.
1087 # Feel free to do it.
1088 # Feel free to do it.
1088 util.stderr.write("abort: %s\n" % exc)
1089 procutil.stderr.write("abort: %s\n" % exc)
1089 if exc.hint is not None:
1090 if exc.hint is not None:
1090 util.stderr.write("(%s)\n" % exc.hint)
1091 procutil.stderr.write("(%s)\n" % exc.hint)
1091 util.stderr.flush()
1092 procutil.stderr.flush()
1092 return pushres(0, output.getvalue() if output else '')
1093 return pushres(0, output.getvalue() if output else '')
1093 except error.PushRaced:
1094 except error.PushRaced:
1094 return pusherr(pycompat.bytestr(exc),
1095 return pusherr(pycompat.bytestr(exc),
1095 output.getvalue() if output else '')
1096 output.getvalue() if output else '')
1096
1097
1097 bundler = bundle2.bundle20(repo.ui)
1098 bundler = bundle2.bundle20(repo.ui)
1098 for out in getattr(exc, '_bundle2salvagedoutput', ()):
1099 for out in getattr(exc, '_bundle2salvagedoutput', ()):
1099 bundler.addpart(out)
1100 bundler.addpart(out)
1100 try:
1101 try:
1101 try:
1102 try:
1102 raise
1103 raise
1103 except error.PushkeyFailed as exc:
1104 except error.PushkeyFailed as exc:
1104 # check client caps
1105 # check client caps
1105 remotecaps = getattr(exc, '_replycaps', None)
1106 remotecaps = getattr(exc, '_replycaps', None)
1106 if (remotecaps is not None
1107 if (remotecaps is not None
1107 and 'pushkey' not in remotecaps.get('error', ())):
1108 and 'pushkey' not in remotecaps.get('error', ())):
1108 # no support remote side, fallback to Abort handler.
1109 # no support remote side, fallback to Abort handler.
1109 raise
1110 raise
1110 part = bundler.newpart('error:pushkey')
1111 part = bundler.newpart('error:pushkey')
1111 part.addparam('in-reply-to', exc.partid)
1112 part.addparam('in-reply-to', exc.partid)
1112 if exc.namespace is not None:
1113 if exc.namespace is not None:
1113 part.addparam('namespace', exc.namespace,
1114 part.addparam('namespace', exc.namespace,
1114 mandatory=False)
1115 mandatory=False)
1115 if exc.key is not None:
1116 if exc.key is not None:
1116 part.addparam('key', exc.key, mandatory=False)
1117 part.addparam('key', exc.key, mandatory=False)
1117 if exc.new is not None:
1118 if exc.new is not None:
1118 part.addparam('new', exc.new, mandatory=False)
1119 part.addparam('new', exc.new, mandatory=False)
1119 if exc.old is not None:
1120 if exc.old is not None:
1120 part.addparam('old', exc.old, mandatory=False)
1121 part.addparam('old', exc.old, mandatory=False)
1121 if exc.ret is not None:
1122 if exc.ret is not None:
1122 part.addparam('ret', exc.ret, mandatory=False)
1123 part.addparam('ret', exc.ret, mandatory=False)
1123 except error.BundleValueError as exc:
1124 except error.BundleValueError as exc:
1124 errpart = bundler.newpart('error:unsupportedcontent')
1125 errpart = bundler.newpart('error:unsupportedcontent')
1125 if exc.parttype is not None:
1126 if exc.parttype is not None:
1126 errpart.addparam('parttype', exc.parttype)
1127 errpart.addparam('parttype', exc.parttype)
1127 if exc.params:
1128 if exc.params:
1128 errpart.addparam('params', '\0'.join(exc.params))
1129 errpart.addparam('params', '\0'.join(exc.params))
1129 except error.Abort as exc:
1130 except error.Abort as exc:
1130 manargs = [('message', stringutil.forcebytestr(exc))]
1131 manargs = [('message', stringutil.forcebytestr(exc))]
1131 advargs = []
1132 advargs = []
1132 if exc.hint is not None:
1133 if exc.hint is not None:
1133 advargs.append(('hint', exc.hint))
1134 advargs.append(('hint', exc.hint))
1134 bundler.addpart(bundle2.bundlepart('error:abort',
1135 bundler.addpart(bundle2.bundlepart('error:abort',
1135 manargs, advargs))
1136 manargs, advargs))
1136 except error.PushRaced as exc:
1137 except error.PushRaced as exc:
1137 bundler.newpart('error:pushraced',
1138 bundler.newpart('error:pushraced',
1138 [('message', stringutil.forcebytestr(exc))])
1139 [('message', stringutil.forcebytestr(exc))])
1139 return streamres_legacy(gen=bundler.getchunks())
1140 return streamres_legacy(gen=bundler.getchunks())
@@ -1,1806 +1,1809 b''
1 Set up a repo
1 Set up a repo
2
2
3 $ cat <<EOF >> $HGRCPATH
3 $ cat <<EOF >> $HGRCPATH
4 > [ui]
4 > [ui]
5 > interactive = true
5 > interactive = true
6 > [extensions]
6 > [extensions]
7 > record =
7 > record =
8 > EOF
8 > EOF
9
9
10 $ hg init a
10 $ hg init a
11 $ cd a
11 $ cd a
12
12
13 Select no files
13 Select no files
14
14
15 $ touch empty-rw
15 $ touch empty-rw
16 $ hg add empty-rw
16 $ hg add empty-rw
17
17
18 $ hg record --config ui.interactive=false
18 $ hg record --config ui.interactive=false
19 abort: running non-interactively, use commit instead
19 abort: running non-interactively, use commit instead
20 [255]
20 [255]
21 $ hg commit -i --config ui.interactive=false
21 $ hg commit -i --config ui.interactive=false
22 abort: running non-interactively
22 abort: running non-interactively
23 [255]
23 [255]
24 $ hg commit -i empty-rw<<EOF
24 $ hg commit -i empty-rw<<EOF
25 > n
25 > n
26 > EOF
26 > EOF
27 diff --git a/empty-rw b/empty-rw
27 diff --git a/empty-rw b/empty-rw
28 new file mode 100644
28 new file mode 100644
29 examine changes to 'empty-rw'? [Ynesfdaq?] n
29 examine changes to 'empty-rw'? [Ynesfdaq?] n
30
30
31 no changes to record
31 no changes to record
32 [1]
32 [1]
33
33
34 $ hg tip -p
34 $ hg tip -p
35 changeset: -1:000000000000
35 changeset: -1:000000000000
36 tag: tip
36 tag: tip
37 user:
37 user:
38 date: Thu Jan 01 00:00:00 1970 +0000
38 date: Thu Jan 01 00:00:00 1970 +0000
39
39
40
40
41
41
42 Select files but no hunks
42 Select files but no hunks
43
43
44 $ hg commit -i empty-rw<<EOF
44 $ hg commit -i empty-rw<<EOF
45 > y
45 > y
46 > n
46 > n
47 > EOF
47 > EOF
48 diff --git a/empty-rw b/empty-rw
48 diff --git a/empty-rw b/empty-rw
49 new file mode 100644
49 new file mode 100644
50 examine changes to 'empty-rw'? [Ynesfdaq?] y
50 examine changes to 'empty-rw'? [Ynesfdaq?] y
51
51
52 abort: empty commit message
52 abort: empty commit message
53 [255]
53 [255]
54
54
55 $ hg tip -p
55 $ hg tip -p
56 changeset: -1:000000000000
56 changeset: -1:000000000000
57 tag: tip
57 tag: tip
58 user:
58 user:
59 date: Thu Jan 01 00:00:00 1970 +0000
59 date: Thu Jan 01 00:00:00 1970 +0000
60
60
61
61
62
62
63 Abort for untracked
63 Abort for untracked
64
64
65 $ touch untracked
65 $ touch untracked
66 $ hg commit -i -m should-fail empty-rw untracked
66 $ hg commit -i -m should-fail empty-rw untracked
67 abort: untracked: file not tracked!
67 abort: untracked: file not tracked!
68 [255]
68 [255]
69 $ rm untracked
69 $ rm untracked
70
70
71 Record empty file
71 Record empty file
72
72
73 $ hg commit -i -d '0 0' -m empty empty-rw<<EOF
73 $ hg commit -i -d '0 0' -m empty empty-rw<<EOF
74 > y
74 > y
75 > y
75 > y
76 > EOF
76 > EOF
77 diff --git a/empty-rw b/empty-rw
77 diff --git a/empty-rw b/empty-rw
78 new file mode 100644
78 new file mode 100644
79 examine changes to 'empty-rw'? [Ynesfdaq?] y
79 examine changes to 'empty-rw'? [Ynesfdaq?] y
80
80
81
81
82 $ hg tip -p
82 $ hg tip -p
83 changeset: 0:c0708cf4e46e
83 changeset: 0:c0708cf4e46e
84 tag: tip
84 tag: tip
85 user: test
85 user: test
86 date: Thu Jan 01 00:00:00 1970 +0000
86 date: Thu Jan 01 00:00:00 1970 +0000
87 summary: empty
87 summary: empty
88
88
89
89
90
90
91 Summary shows we updated to the new cset
91 Summary shows we updated to the new cset
92
92
93 $ hg summary
93 $ hg summary
94 parent: 0:c0708cf4e46e tip
94 parent: 0:c0708cf4e46e tip
95 empty
95 empty
96 branch: default
96 branch: default
97 commit: (clean)
97 commit: (clean)
98 update: (current)
98 update: (current)
99 phases: 1 draft
99 phases: 1 draft
100
100
101 Rename empty file
101 Rename empty file
102
102
103 $ hg mv empty-rw empty-rename
103 $ hg mv empty-rw empty-rename
104 $ hg commit -i -d '1 0' -m rename<<EOF
104 $ hg commit -i -d '1 0' -m rename<<EOF
105 > y
105 > y
106 > EOF
106 > EOF
107 diff --git a/empty-rw b/empty-rename
107 diff --git a/empty-rw b/empty-rename
108 rename from empty-rw
108 rename from empty-rw
109 rename to empty-rename
109 rename to empty-rename
110 examine changes to 'empty-rw' and 'empty-rename'? [Ynesfdaq?] y
110 examine changes to 'empty-rw' and 'empty-rename'? [Ynesfdaq?] y
111
111
112
112
113 $ hg tip -p
113 $ hg tip -p
114 changeset: 1:d695e8dcb197
114 changeset: 1:d695e8dcb197
115 tag: tip
115 tag: tip
116 user: test
116 user: test
117 date: Thu Jan 01 00:00:01 1970 +0000
117 date: Thu Jan 01 00:00:01 1970 +0000
118 summary: rename
118 summary: rename
119
119
120
120
121
121
122 Copy empty file
122 Copy empty file
123
123
124 $ hg cp empty-rename empty-copy
124 $ hg cp empty-rename empty-copy
125 $ hg commit -i -d '2 0' -m copy<<EOF
125 $ hg commit -i -d '2 0' -m copy<<EOF
126 > y
126 > y
127 > EOF
127 > EOF
128 diff --git a/empty-rename b/empty-copy
128 diff --git a/empty-rename b/empty-copy
129 copy from empty-rename
129 copy from empty-rename
130 copy to empty-copy
130 copy to empty-copy
131 examine changes to 'empty-rename' and 'empty-copy'? [Ynesfdaq?] y
131 examine changes to 'empty-rename' and 'empty-copy'? [Ynesfdaq?] y
132
132
133
133
134 $ hg tip -p
134 $ hg tip -p
135 changeset: 2:1d4b90bea524
135 changeset: 2:1d4b90bea524
136 tag: tip
136 tag: tip
137 user: test
137 user: test
138 date: Thu Jan 01 00:00:02 1970 +0000
138 date: Thu Jan 01 00:00:02 1970 +0000
139 summary: copy
139 summary: copy
140
140
141
141
142
142
143 Delete empty file
143 Delete empty file
144
144
145 $ hg rm empty-copy
145 $ hg rm empty-copy
146 $ hg commit -i -d '3 0' -m delete<<EOF
146 $ hg commit -i -d '3 0' -m delete<<EOF
147 > y
147 > y
148 > EOF
148 > EOF
149 diff --git a/empty-copy b/empty-copy
149 diff --git a/empty-copy b/empty-copy
150 deleted file mode 100644
150 deleted file mode 100644
151 examine changes to 'empty-copy'? [Ynesfdaq?] y
151 examine changes to 'empty-copy'? [Ynesfdaq?] y
152
152
153
153
154 $ hg tip -p
154 $ hg tip -p
155 changeset: 3:b39a238f01a1
155 changeset: 3:b39a238f01a1
156 tag: tip
156 tag: tip
157 user: test
157 user: test
158 date: Thu Jan 01 00:00:03 1970 +0000
158 date: Thu Jan 01 00:00:03 1970 +0000
159 summary: delete
159 summary: delete
160
160
161
161
162
162
163 Add binary file
163 Add binary file
164
164
165 $ hg bundle --type v1 --base -2 tip.bundle
165 $ hg bundle --type v1 --base -2 tip.bundle
166 1 changesets found
166 1 changesets found
167 $ hg add tip.bundle
167 $ hg add tip.bundle
168 $ hg commit -i -d '4 0' -m binary<<EOF
168 $ hg commit -i -d '4 0' -m binary<<EOF
169 > y
169 > y
170 > EOF
170 > EOF
171 diff --git a/tip.bundle b/tip.bundle
171 diff --git a/tip.bundle b/tip.bundle
172 new file mode 100644
172 new file mode 100644
173 this is a binary file
173 this is a binary file
174 examine changes to 'tip.bundle'? [Ynesfdaq?] y
174 examine changes to 'tip.bundle'? [Ynesfdaq?] y
175
175
176
176
177 $ hg tip -p
177 $ hg tip -p
178 changeset: 4:ad816da3711e
178 changeset: 4:ad816da3711e
179 tag: tip
179 tag: tip
180 user: test
180 user: test
181 date: Thu Jan 01 00:00:04 1970 +0000
181 date: Thu Jan 01 00:00:04 1970 +0000
182 summary: binary
182 summary: binary
183
183
184 diff -r b39a238f01a1 -r ad816da3711e tip.bundle
184 diff -r b39a238f01a1 -r ad816da3711e tip.bundle
185 Binary file tip.bundle has changed
185 Binary file tip.bundle has changed
186
186
187
187
188 Change binary file
188 Change binary file
189
189
190 $ hg bundle --base -2 --type v1 tip.bundle
190 $ hg bundle --base -2 --type v1 tip.bundle
191 1 changesets found
191 1 changesets found
192 $ hg commit -i -d '5 0' -m binary-change<<EOF
192 $ hg commit -i -d '5 0' -m binary-change<<EOF
193 > y
193 > y
194 > EOF
194 > EOF
195 diff --git a/tip.bundle b/tip.bundle
195 diff --git a/tip.bundle b/tip.bundle
196 this modifies a binary file (all or nothing)
196 this modifies a binary file (all or nothing)
197 examine changes to 'tip.bundle'? [Ynesfdaq?] y
197 examine changes to 'tip.bundle'? [Ynesfdaq?] y
198
198
199
199
200 $ hg tip -p
200 $ hg tip -p
201 changeset: 5:dccd6f3eb485
201 changeset: 5:dccd6f3eb485
202 tag: tip
202 tag: tip
203 user: test
203 user: test
204 date: Thu Jan 01 00:00:05 1970 +0000
204 date: Thu Jan 01 00:00:05 1970 +0000
205 summary: binary-change
205 summary: binary-change
206
206
207 diff -r ad816da3711e -r dccd6f3eb485 tip.bundle
207 diff -r ad816da3711e -r dccd6f3eb485 tip.bundle
208 Binary file tip.bundle has changed
208 Binary file tip.bundle has changed
209
209
210
210
211 Rename and change binary file
211 Rename and change binary file
212
212
213 $ hg mv tip.bundle top.bundle
213 $ hg mv tip.bundle top.bundle
214 $ hg bundle --base -2 --type v1 top.bundle
214 $ hg bundle --base -2 --type v1 top.bundle
215 1 changesets found
215 1 changesets found
216 $ hg commit -i -d '6 0' -m binary-change-rename<<EOF
216 $ hg commit -i -d '6 0' -m binary-change-rename<<EOF
217 > y
217 > y
218 > EOF
218 > EOF
219 diff --git a/tip.bundle b/top.bundle
219 diff --git a/tip.bundle b/top.bundle
220 rename from tip.bundle
220 rename from tip.bundle
221 rename to top.bundle
221 rename to top.bundle
222 this modifies a binary file (all or nothing)
222 this modifies a binary file (all or nothing)
223 examine changes to 'tip.bundle' and 'top.bundle'? [Ynesfdaq?] y
223 examine changes to 'tip.bundle' and 'top.bundle'? [Ynesfdaq?] y
224
224
225
225
226 $ hg tip -p
226 $ hg tip -p
227 changeset: 6:7fa44105f5b3
227 changeset: 6:7fa44105f5b3
228 tag: tip
228 tag: tip
229 user: test
229 user: test
230 date: Thu Jan 01 00:00:06 1970 +0000
230 date: Thu Jan 01 00:00:06 1970 +0000
231 summary: binary-change-rename
231 summary: binary-change-rename
232
232
233 diff -r dccd6f3eb485 -r 7fa44105f5b3 tip.bundle
233 diff -r dccd6f3eb485 -r 7fa44105f5b3 tip.bundle
234 Binary file tip.bundle has changed
234 Binary file tip.bundle has changed
235 diff -r dccd6f3eb485 -r 7fa44105f5b3 top.bundle
235 diff -r dccd6f3eb485 -r 7fa44105f5b3 top.bundle
236 Binary file top.bundle has changed
236 Binary file top.bundle has changed
237
237
238
238
239 Add plain file
239 Add plain file
240
240
241 $ for i in 1 2 3 4 5 6 7 8 9 10; do
241 $ for i in 1 2 3 4 5 6 7 8 9 10; do
242 > echo $i >> plain
242 > echo $i >> plain
243 > done
243 > done
244
244
245 $ hg add plain
245 $ hg add plain
246 $ hg commit -i -d '7 0' -m plain plain<<EOF
246 $ hg commit -i -d '7 0' -m plain plain<<EOF
247 > y
247 > y
248 > y
248 > y
249 > EOF
249 > EOF
250 diff --git a/plain b/plain
250 diff --git a/plain b/plain
251 new file mode 100644
251 new file mode 100644
252 examine changes to 'plain'? [Ynesfdaq?] y
252 examine changes to 'plain'? [Ynesfdaq?] y
253
253
254 @@ -0,0 +1,10 @@
254 @@ -0,0 +1,10 @@
255 +1
255 +1
256 +2
256 +2
257 +3
257 +3
258 +4
258 +4
259 +5
259 +5
260 +6
260 +6
261 +7
261 +7
262 +8
262 +8
263 +9
263 +9
264 +10
264 +10
265 record this change to 'plain'? [Ynesfdaq?] y
265 record this change to 'plain'? [Ynesfdaq?] y
266
266
267 $ hg tip -p
267 $ hg tip -p
268 changeset: 7:11fb457c1be4
268 changeset: 7:11fb457c1be4
269 tag: tip
269 tag: tip
270 user: test
270 user: test
271 date: Thu Jan 01 00:00:07 1970 +0000
271 date: Thu Jan 01 00:00:07 1970 +0000
272 summary: plain
272 summary: plain
273
273
274 diff -r 7fa44105f5b3 -r 11fb457c1be4 plain
274 diff -r 7fa44105f5b3 -r 11fb457c1be4 plain
275 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
275 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
276 +++ b/plain Thu Jan 01 00:00:07 1970 +0000
276 +++ b/plain Thu Jan 01 00:00:07 1970 +0000
277 @@ -0,0 +1,10 @@
277 @@ -0,0 +1,10 @@
278 +1
278 +1
279 +2
279 +2
280 +3
280 +3
281 +4
281 +4
282 +5
282 +5
283 +6
283 +6
284 +7
284 +7
285 +8
285 +8
286 +9
286 +9
287 +10
287 +10
288
288
289 Modify end of plain file with username unset
289 Modify end of plain file with username unset
290
290
291 $ echo 11 >> plain
291 $ echo 11 >> plain
292 $ unset HGUSER
292 $ unset HGUSER
293 $ hg commit -i --config ui.username= -d '8 0' -m end plain
293 $ hg commit -i --config ui.username= -d '8 0' -m end plain
294 abort: no username supplied
294 abort: no username supplied
295 (use 'hg config --edit' to set your username)
295 (use 'hg config --edit' to set your username)
296 [255]
296 [255]
297
297
298
298
299 Modify end of plain file, also test that diffopts are accounted for
299 Modify end of plain file, also test that diffopts are accounted for
300
300
301 $ HGUSER="test"
301 $ HGUSER="test"
302 $ export HGUSER
302 $ export HGUSER
303 $ hg commit -i --config diff.showfunc=true -d '8 0' -m end plain <<EOF
303 $ hg commit -i --config diff.showfunc=true -d '8 0' -m end plain <<EOF
304 > y
304 > y
305 > y
305 > y
306 > EOF
306 > EOF
307 diff --git a/plain b/plain
307 diff --git a/plain b/plain
308 1 hunks, 1 lines changed
308 1 hunks, 1 lines changed
309 examine changes to 'plain'? [Ynesfdaq?] y
309 examine changes to 'plain'? [Ynesfdaq?] y
310
310
311 @@ -8,3 +8,4 @@ 7
311 @@ -8,3 +8,4 @@ 7
312 8
312 8
313 9
313 9
314 10
314 10
315 +11
315 +11
316 record this change to 'plain'? [Ynesfdaq?] y
316 record this change to 'plain'? [Ynesfdaq?] y
317
317
318
318
319 Modify end of plain file, no EOL
319 Modify end of plain file, no EOL
320
320
321 $ hg tip --template '{node}' >> plain
321 $ hg tip --template '{node}' >> plain
322 $ hg commit -i -d '9 0' -m noeol plain <<EOF
322 $ hg commit -i -d '9 0' -m noeol plain <<EOF
323 > y
323 > y
324 > y
324 > y
325 > EOF
325 > EOF
326 diff --git a/plain b/plain
326 diff --git a/plain b/plain
327 1 hunks, 1 lines changed
327 1 hunks, 1 lines changed
328 examine changes to 'plain'? [Ynesfdaq?] y
328 examine changes to 'plain'? [Ynesfdaq?] y
329
329
330 @@ -9,3 +9,4 @@ 8
330 @@ -9,3 +9,4 @@ 8
331 9
331 9
332 10
332 10
333 11
333 11
334 +7264f99c5f5ff3261504828afa4fb4d406c3af54
334 +7264f99c5f5ff3261504828afa4fb4d406c3af54
335 \ No newline at end of file
335 \ No newline at end of file
336 record this change to 'plain'? [Ynesfdaq?] y
336 record this change to 'plain'? [Ynesfdaq?] y
337
337
338
338
339 Record showfunc should preserve function across sections
339 Record showfunc should preserve function across sections
340
340
341 $ cat > f1.py <<EOF
341 $ cat > f1.py <<EOF
342 > def annotate(ui, repo, *pats, **opts):
342 > def annotate(ui, repo, *pats, **opts):
343 > """show changeset information by line for each file
343 > """show changeset information by line for each file
344 >
344 >
345 > List changes in files, showing the revision id responsible for
345 > List changes in files, showing the revision id responsible for
346 > each line.
346 > each line.
347 >
347 >
348 > This command is useful for discovering when a change was made and
348 > This command is useful for discovering when a change was made and
349 > by whom.
349 > by whom.
350 >
350 >
351 > If you include -f/-u/-d, the revision number is suppressed unless
351 > If you include -f/-u/-d, the revision number is suppressed unless
352 > you also include -the revision number is suppressed unless
352 > you also include -the revision number is suppressed unless
353 > you also include -n.
353 > you also include -n.
354 >
354 >
355 > Without the -a/--text option, annotate will avoid processing files
355 > Without the -a/--text option, annotate will avoid processing files
356 > it detects as binary. With -a, annotate will annotate the file
356 > it detects as binary. With -a, annotate will annotate the file
357 > anyway, although the results will probably be neither useful
357 > anyway, although the results will probably be neither useful
358 > nor desirable.
358 > nor desirable.
359 >
359 >
360 > Returns 0 on success.
360 > Returns 0 on success.
361 > """
361 > """
362 > return 0
362 > return 0
363 > def archive(ui, repo, dest, **opts):
363 > def archive(ui, repo, dest, **opts):
364 > '''create an unversioned archive of a repository revision
364 > '''create an unversioned archive of a repository revision
365 >
365 >
366 > By default, the revision used is the parent of the working
366 > By default, the revision used is the parent of the working
367 > directory; use -r/--rev to specify a different revision.
367 > directory; use -r/--rev to specify a different revision.
368 >
368 >
369 > The archive type is automatically detected based on file
369 > The archive type is automatically detected based on file
370 > extension (to override, use -t/--type).
370 > extension (to override, use -t/--type).
371 >
371 >
372 > .. container:: verbose
372 > .. container:: verbose
373 >
373 >
374 > Valid types are:
374 > Valid types are:
375 > EOF
375 > EOF
376 $ hg add f1.py
376 $ hg add f1.py
377 $ hg commit -m funcs
377 $ hg commit -m funcs
378 $ cat > f1.py <<EOF
378 $ cat > f1.py <<EOF
379 > def annotate(ui, repo, *pats, **opts):
379 > def annotate(ui, repo, *pats, **opts):
380 > """show changeset information by line for each file
380 > """show changeset information by line for each file
381 >
381 >
382 > List changes in files, showing the revision id responsible for
382 > List changes in files, showing the revision id responsible for
383 > each line
383 > each line
384 >
384 >
385 > This command is useful for discovering when a change was made and
385 > This command is useful for discovering when a change was made and
386 > by whom.
386 > by whom.
387 >
387 >
388 > Without the -a/--text option, annotate will avoid processing files
388 > Without the -a/--text option, annotate will avoid processing files
389 > it detects as binary. With -a, annotate will annotate the file
389 > it detects as binary. With -a, annotate will annotate the file
390 > anyway, although the results will probably be neither useful
390 > anyway, although the results will probably be neither useful
391 > nor desirable.
391 > nor desirable.
392 >
392 >
393 > Returns 0 on success.
393 > Returns 0 on success.
394 > """
394 > """
395 > return 0
395 > return 0
396 > def archive(ui, repo, dest, **opts):
396 > def archive(ui, repo, dest, **opts):
397 > '''create an unversioned archive of a repository revision
397 > '''create an unversioned archive of a repository revision
398 >
398 >
399 > By default, the revision used is the parent of the working
399 > By default, the revision used is the parent of the working
400 > directory; use -r/--rev to specify a different revision.
400 > directory; use -r/--rev to specify a different revision.
401 >
401 >
402 > The archive type is automatically detected based on file
402 > The archive type is automatically detected based on file
403 > extension (or override using -t/--type).
403 > extension (or override using -t/--type).
404 >
404 >
405 > .. container:: verbose
405 > .. container:: verbose
406 >
406 >
407 > Valid types are:
407 > Valid types are:
408 > EOF
408 > EOF
409 $ hg commit -i -m interactive <<EOF
409 $ hg commit -i -m interactive <<EOF
410 > y
410 > y
411 > y
411 > y
412 > y
412 > y
413 > y
413 > y
414 > EOF
414 > EOF
415 diff --git a/f1.py b/f1.py
415 diff --git a/f1.py b/f1.py
416 3 hunks, 6 lines changed
416 3 hunks, 6 lines changed
417 examine changes to 'f1.py'? [Ynesfdaq?] y
417 examine changes to 'f1.py'? [Ynesfdaq?] y
418
418
419 @@ -2,8 +2,8 @@ def annotate(ui, repo, *pats, **opts):
419 @@ -2,8 +2,8 @@ def annotate(ui, repo, *pats, **opts):
420 """show changeset information by line for each file
420 """show changeset information by line for each file
421
421
422 List changes in files, showing the revision id responsible for
422 List changes in files, showing the revision id responsible for
423 - each line.
423 - each line.
424 + each line
424 + each line
425
425
426 This command is useful for discovering when a change was made and
426 This command is useful for discovering when a change was made and
427 by whom.
427 by whom.
428
428
429 record change 1/3 to 'f1.py'? [Ynesfdaq?] y
429 record change 1/3 to 'f1.py'? [Ynesfdaq?] y
430
430
431 @@ -6,11 +6,7 @@ def annotate(ui, repo, *pats, **opts):
431 @@ -6,11 +6,7 @@ def annotate(ui, repo, *pats, **opts):
432
432
433 This command is useful for discovering when a change was made and
433 This command is useful for discovering when a change was made and
434 by whom.
434 by whom.
435
435
436 - If you include -f/-u/-d, the revision number is suppressed unless
436 - If you include -f/-u/-d, the revision number is suppressed unless
437 - you also include -the revision number is suppressed unless
437 - you also include -the revision number is suppressed unless
438 - you also include -n.
438 - you also include -n.
439 -
439 -
440 Without the -a/--text option, annotate will avoid processing files
440 Without the -a/--text option, annotate will avoid processing files
441 it detects as binary. With -a, annotate will annotate the file
441 it detects as binary. With -a, annotate will annotate the file
442 anyway, although the results will probably be neither useful
442 anyway, although the results will probably be neither useful
443 record change 2/3 to 'f1.py'? [Ynesfdaq?] y
443 record change 2/3 to 'f1.py'? [Ynesfdaq?] y
444
444
445 @@ -26,7 +22,7 @@ def archive(ui, repo, dest, **opts):
445 @@ -26,7 +22,7 @@ def archive(ui, repo, dest, **opts):
446 directory; use -r/--rev to specify a different revision.
446 directory; use -r/--rev to specify a different revision.
447
447
448 The archive type is automatically detected based on file
448 The archive type is automatically detected based on file
449 - extension (to override, use -t/--type).
449 - extension (to override, use -t/--type).
450 + extension (or override using -t/--type).
450 + extension (or override using -t/--type).
451
451
452 .. container:: verbose
452 .. container:: verbose
453
453
454 record change 3/3 to 'f1.py'? [Ynesfdaq?] y
454 record change 3/3 to 'f1.py'? [Ynesfdaq?] y
455
455
456
456
457 Modify end of plain file, add EOL
457 Modify end of plain file, add EOL
458
458
459 $ echo >> plain
459 $ echo >> plain
460 $ echo 1 > plain2
460 $ echo 1 > plain2
461 $ hg add plain2
461 $ hg add plain2
462 $ hg commit -i -d '10 0' -m eol plain plain2 <<EOF
462 $ hg commit -i -d '10 0' -m eol plain plain2 <<EOF
463 > y
463 > y
464 > y
464 > y
465 > y
465 > y
466 > y
466 > y
467 > EOF
467 > EOF
468 diff --git a/plain b/plain
468 diff --git a/plain b/plain
469 1 hunks, 1 lines changed
469 1 hunks, 1 lines changed
470 examine changes to 'plain'? [Ynesfdaq?] y
470 examine changes to 'plain'? [Ynesfdaq?] y
471
471
472 @@ -9,4 +9,4 @@ 8
472 @@ -9,4 +9,4 @@ 8
473 9
473 9
474 10
474 10
475 11
475 11
476 -7264f99c5f5ff3261504828afa4fb4d406c3af54
476 -7264f99c5f5ff3261504828afa4fb4d406c3af54
477 \ No newline at end of file
477 \ No newline at end of file
478 +7264f99c5f5ff3261504828afa4fb4d406c3af54
478 +7264f99c5f5ff3261504828afa4fb4d406c3af54
479 record change 1/2 to 'plain'? [Ynesfdaq?] y
479 record change 1/2 to 'plain'? [Ynesfdaq?] y
480
480
481 diff --git a/plain2 b/plain2
481 diff --git a/plain2 b/plain2
482 new file mode 100644
482 new file mode 100644
483 examine changes to 'plain2'? [Ynesfdaq?] y
483 examine changes to 'plain2'? [Ynesfdaq?] y
484
484
485 @@ -0,0 +1,1 @@
485 @@ -0,0 +1,1 @@
486 +1
486 +1
487 record change 2/2 to 'plain2'? [Ynesfdaq?] y
487 record change 2/2 to 'plain2'? [Ynesfdaq?] y
488
488
489 Modify beginning, trim end, record both, add another file to test
489 Modify beginning, trim end, record both, add another file to test
490 changes numbering
490 changes numbering
491
491
492 $ rm plain
492 $ rm plain
493 $ for i in 2 2 3 4 5 6 7 8 9 10; do
493 $ for i in 2 2 3 4 5 6 7 8 9 10; do
494 > echo $i >> plain
494 > echo $i >> plain
495 > done
495 > done
496 $ echo 2 >> plain2
496 $ echo 2 >> plain2
497
497
498 $ hg commit -i -d '10 0' -m begin-and-end plain plain2 <<EOF
498 $ hg commit -i -d '10 0' -m begin-and-end plain plain2 <<EOF
499 > y
499 > y
500 > y
500 > y
501 > y
501 > y
502 > y
502 > y
503 > y
503 > y
504 > EOF
504 > EOF
505 diff --git a/plain b/plain
505 diff --git a/plain b/plain
506 2 hunks, 3 lines changed
506 2 hunks, 3 lines changed
507 examine changes to 'plain'? [Ynesfdaq?] y
507 examine changes to 'plain'? [Ynesfdaq?] y
508
508
509 @@ -1,4 +1,4 @@
509 @@ -1,4 +1,4 @@
510 -1
510 -1
511 +2
511 +2
512 2
512 2
513 3
513 3
514 4
514 4
515 record change 1/3 to 'plain'? [Ynesfdaq?] y
515 record change 1/3 to 'plain'? [Ynesfdaq?] y
516
516
517 @@ -8,5 +8,3 @@ 7
517 @@ -8,5 +8,3 @@ 7
518 8
518 8
519 9
519 9
520 10
520 10
521 -11
521 -11
522 -7264f99c5f5ff3261504828afa4fb4d406c3af54
522 -7264f99c5f5ff3261504828afa4fb4d406c3af54
523 record change 2/3 to 'plain'? [Ynesfdaq?] y
523 record change 2/3 to 'plain'? [Ynesfdaq?] y
524
524
525 diff --git a/plain2 b/plain2
525 diff --git a/plain2 b/plain2
526 1 hunks, 1 lines changed
526 1 hunks, 1 lines changed
527 examine changes to 'plain2'? [Ynesfdaq?] y
527 examine changes to 'plain2'? [Ynesfdaq?] y
528
528
529 @@ -1,1 +1,2 @@
529 @@ -1,1 +1,2 @@
530 1
530 1
531 +2
531 +2
532 record change 3/3 to 'plain2'? [Ynesfdaq?] y
532 record change 3/3 to 'plain2'? [Ynesfdaq?] y
533
533
534
534
535 $ hg tip -p
535 $ hg tip -p
536 changeset: 13:f941910cff62
536 changeset: 13:f941910cff62
537 tag: tip
537 tag: tip
538 user: test
538 user: test
539 date: Thu Jan 01 00:00:10 1970 +0000
539 date: Thu Jan 01 00:00:10 1970 +0000
540 summary: begin-and-end
540 summary: begin-and-end
541
541
542 diff -r 33abe24d946c -r f941910cff62 plain
542 diff -r 33abe24d946c -r f941910cff62 plain
543 --- a/plain Thu Jan 01 00:00:10 1970 +0000
543 --- a/plain Thu Jan 01 00:00:10 1970 +0000
544 +++ b/plain Thu Jan 01 00:00:10 1970 +0000
544 +++ b/plain Thu Jan 01 00:00:10 1970 +0000
545 @@ -1,4 +1,4 @@
545 @@ -1,4 +1,4 @@
546 -1
546 -1
547 +2
547 +2
548 2
548 2
549 3
549 3
550 4
550 4
551 @@ -8,5 +8,3 @@
551 @@ -8,5 +8,3 @@
552 8
552 8
553 9
553 9
554 10
554 10
555 -11
555 -11
556 -7264f99c5f5ff3261504828afa4fb4d406c3af54
556 -7264f99c5f5ff3261504828afa4fb4d406c3af54
557 diff -r 33abe24d946c -r f941910cff62 plain2
557 diff -r 33abe24d946c -r f941910cff62 plain2
558 --- a/plain2 Thu Jan 01 00:00:10 1970 +0000
558 --- a/plain2 Thu Jan 01 00:00:10 1970 +0000
559 +++ b/plain2 Thu Jan 01 00:00:10 1970 +0000
559 +++ b/plain2 Thu Jan 01 00:00:10 1970 +0000
560 @@ -1,1 +1,2 @@
560 @@ -1,1 +1,2 @@
561 1
561 1
562 +2
562 +2
563
563
564
564
565 Trim beginning, modify end
565 Trim beginning, modify end
566
566
567 $ rm plain
567 $ rm plain
568 > for i in 4 5 6 7 8 9 10.new; do
568 > for i in 4 5 6 7 8 9 10.new; do
569 > echo $i >> plain
569 > echo $i >> plain
570 > done
570 > done
571
571
572 Record end
572 Record end
573
573
574 $ hg commit -i -d '11 0' -m end-only plain <<EOF
574 $ hg commit -i -d '11 0' -m end-only plain <<EOF
575 > y
575 > y
576 > n
576 > n
577 > y
577 > y
578 > EOF
578 > EOF
579 diff --git a/plain b/plain
579 diff --git a/plain b/plain
580 2 hunks, 4 lines changed
580 2 hunks, 4 lines changed
581 examine changes to 'plain'? [Ynesfdaq?] y
581 examine changes to 'plain'? [Ynesfdaq?] y
582
582
583 @@ -1,9 +1,6 @@
583 @@ -1,9 +1,6 @@
584 -2
584 -2
585 -2
585 -2
586 -3
586 -3
587 4
587 4
588 5
588 5
589 6
589 6
590 7
590 7
591 8
591 8
592 9
592 9
593 record change 1/2 to 'plain'? [Ynesfdaq?] n
593 record change 1/2 to 'plain'? [Ynesfdaq?] n
594
594
595 @@ -4,7 +1,7 @@
595 @@ -4,7 +1,7 @@
596 4
596 4
597 5
597 5
598 6
598 6
599 7
599 7
600 8
600 8
601 9
601 9
602 -10
602 -10
603 +10.new
603 +10.new
604 record change 2/2 to 'plain'? [Ynesfdaq?] y
604 record change 2/2 to 'plain'? [Ynesfdaq?] y
605
605
606
606
607 $ hg tip -p
607 $ hg tip -p
608 changeset: 14:4915f538659b
608 changeset: 14:4915f538659b
609 tag: tip
609 tag: tip
610 user: test
610 user: test
611 date: Thu Jan 01 00:00:11 1970 +0000
611 date: Thu Jan 01 00:00:11 1970 +0000
612 summary: end-only
612 summary: end-only
613
613
614 diff -r f941910cff62 -r 4915f538659b plain
614 diff -r f941910cff62 -r 4915f538659b plain
615 --- a/plain Thu Jan 01 00:00:10 1970 +0000
615 --- a/plain Thu Jan 01 00:00:10 1970 +0000
616 +++ b/plain Thu Jan 01 00:00:11 1970 +0000
616 +++ b/plain Thu Jan 01 00:00:11 1970 +0000
617 @@ -7,4 +7,4 @@
617 @@ -7,4 +7,4 @@
618 7
618 7
619 8
619 8
620 9
620 9
621 -10
621 -10
622 +10.new
622 +10.new
623
623
624
624
625 Record beginning
625 Record beginning
626
626
627 $ hg commit -i -d '12 0' -m begin-only plain <<EOF
627 $ hg commit -i -d '12 0' -m begin-only plain <<EOF
628 > y
628 > y
629 > y
629 > y
630 > EOF
630 > EOF
631 diff --git a/plain b/plain
631 diff --git a/plain b/plain
632 1 hunks, 3 lines changed
632 1 hunks, 3 lines changed
633 examine changes to 'plain'? [Ynesfdaq?] y
633 examine changes to 'plain'? [Ynesfdaq?] y
634
634
635 @@ -1,6 +1,3 @@
635 @@ -1,6 +1,3 @@
636 -2
636 -2
637 -2
637 -2
638 -3
638 -3
639 4
639 4
640 5
640 5
641 6
641 6
642 record this change to 'plain'? [Ynesfdaq?] y
642 record this change to 'plain'? [Ynesfdaq?] y
643
643
644
644
645 $ hg tip -p
645 $ hg tip -p
646 changeset: 15:1b1f93d4b94b
646 changeset: 15:1b1f93d4b94b
647 tag: tip
647 tag: tip
648 user: test
648 user: test
649 date: Thu Jan 01 00:00:12 1970 +0000
649 date: Thu Jan 01 00:00:12 1970 +0000
650 summary: begin-only
650 summary: begin-only
651
651
652 diff -r 4915f538659b -r 1b1f93d4b94b plain
652 diff -r 4915f538659b -r 1b1f93d4b94b plain
653 --- a/plain Thu Jan 01 00:00:11 1970 +0000
653 --- a/plain Thu Jan 01 00:00:11 1970 +0000
654 +++ b/plain Thu Jan 01 00:00:12 1970 +0000
654 +++ b/plain Thu Jan 01 00:00:12 1970 +0000
655 @@ -1,6 +1,3 @@
655 @@ -1,6 +1,3 @@
656 -2
656 -2
657 -2
657 -2
658 -3
658 -3
659 4
659 4
660 5
660 5
661 6
661 6
662
662
663
663
664 Add to beginning, trim from end
664 Add to beginning, trim from end
665
665
666 $ rm plain
666 $ rm plain
667 $ for i in 1 2 3 4 5 6 7 8 9; do
667 $ for i in 1 2 3 4 5 6 7 8 9; do
668 > echo $i >> plain
668 > echo $i >> plain
669 > done
669 > done
670
670
671 Record end
671 Record end
672
672
673 $ hg commit -i --traceback -d '13 0' -m end-again plain<<EOF
673 $ hg commit -i --traceback -d '13 0' -m end-again plain<<EOF
674 > y
674 > y
675 > n
675 > n
676 > y
676 > y
677 > EOF
677 > EOF
678 diff --git a/plain b/plain
678 diff --git a/plain b/plain
679 2 hunks, 4 lines changed
679 2 hunks, 4 lines changed
680 examine changes to 'plain'? [Ynesfdaq?] y
680 examine changes to 'plain'? [Ynesfdaq?] y
681
681
682 @@ -1,6 +1,9 @@
682 @@ -1,6 +1,9 @@
683 +1
683 +1
684 +2
684 +2
685 +3
685 +3
686 4
686 4
687 5
687 5
688 6
688 6
689 7
689 7
690 8
690 8
691 9
691 9
692 record change 1/2 to 'plain'? [Ynesfdaq?] n
692 record change 1/2 to 'plain'? [Ynesfdaq?] n
693
693
694 @@ -1,7 +4,6 @@
694 @@ -1,7 +4,6 @@
695 4
695 4
696 5
696 5
697 6
697 6
698 7
698 7
699 8
699 8
700 9
700 9
701 -10.new
701 -10.new
702 record change 2/2 to 'plain'? [Ynesfdaq?] y
702 record change 2/2 to 'plain'? [Ynesfdaq?] y
703
703
704
704
705 Add to beginning, middle, end
705 Add to beginning, middle, end
706
706
707 $ rm plain
707 $ rm plain
708 $ for i in 1 2 3 4 5 5.new 5.reallynew 6 7 8 9 10 11; do
708 $ for i in 1 2 3 4 5 5.new 5.reallynew 6 7 8 9 10 11; do
709 > echo $i >> plain
709 > echo $i >> plain
710 > done
710 > done
711
711
712 Record beginning, middle, and test that format-breaking diffopts are ignored
712 Record beginning, middle, and test that format-breaking diffopts are ignored
713
713
714 $ hg commit -i --config diff.noprefix=True -d '14 0' -m middle-only plain <<EOF
714 $ hg commit -i --config diff.noprefix=True -d '14 0' -m middle-only plain <<EOF
715 > y
715 > y
716 > y
716 > y
717 > y
717 > y
718 > n
718 > n
719 > EOF
719 > EOF
720 diff --git a/plain b/plain
720 diff --git a/plain b/plain
721 3 hunks, 7 lines changed
721 3 hunks, 7 lines changed
722 examine changes to 'plain'? [Ynesfdaq?] y
722 examine changes to 'plain'? [Ynesfdaq?] y
723
723
724 @@ -1,2 +1,5 @@
724 @@ -1,2 +1,5 @@
725 +1
725 +1
726 +2
726 +2
727 +3
727 +3
728 4
728 4
729 5
729 5
730 record change 1/3 to 'plain'? [Ynesfdaq?] y
730 record change 1/3 to 'plain'? [Ynesfdaq?] y
731
731
732 @@ -1,6 +4,8 @@
732 @@ -1,6 +4,8 @@
733 4
733 4
734 5
734 5
735 +5.new
735 +5.new
736 +5.reallynew
736 +5.reallynew
737 6
737 6
738 7
738 7
739 8
739 8
740 9
740 9
741 record change 2/3 to 'plain'? [Ynesfdaq?] y
741 record change 2/3 to 'plain'? [Ynesfdaq?] y
742
742
743 @@ -3,4 +8,6 @@
743 @@ -3,4 +8,6 @@
744 6
744 6
745 7
745 7
746 8
746 8
747 9
747 9
748 +10
748 +10
749 +11
749 +11
750 record change 3/3 to 'plain'? [Ynesfdaq?] n
750 record change 3/3 to 'plain'? [Ynesfdaq?] n
751
751
752
752
753 $ hg tip -p
753 $ hg tip -p
754 changeset: 17:41cf3f5c55ae
754 changeset: 17:41cf3f5c55ae
755 tag: tip
755 tag: tip
756 user: test
756 user: test
757 date: Thu Jan 01 00:00:14 1970 +0000
757 date: Thu Jan 01 00:00:14 1970 +0000
758 summary: middle-only
758 summary: middle-only
759
759
760 diff -r a69d252246e1 -r 41cf3f5c55ae plain
760 diff -r a69d252246e1 -r 41cf3f5c55ae plain
761 --- a/plain Thu Jan 01 00:00:13 1970 +0000
761 --- a/plain Thu Jan 01 00:00:13 1970 +0000
762 +++ b/plain Thu Jan 01 00:00:14 1970 +0000
762 +++ b/plain Thu Jan 01 00:00:14 1970 +0000
763 @@ -1,5 +1,10 @@
763 @@ -1,5 +1,10 @@
764 +1
764 +1
765 +2
765 +2
766 +3
766 +3
767 4
767 4
768 5
768 5
769 +5.new
769 +5.new
770 +5.reallynew
770 +5.reallynew
771 6
771 6
772 7
772 7
773 8
773 8
774
774
775
775
776 Record end
776 Record end
777
777
778 $ hg commit -i -d '15 0' -m end-only plain <<EOF
778 $ hg commit -i -d '15 0' -m end-only plain <<EOF
779 > y
779 > y
780 > y
780 > y
781 > EOF
781 > EOF
782 diff --git a/plain b/plain
782 diff --git a/plain b/plain
783 1 hunks, 2 lines changed
783 1 hunks, 2 lines changed
784 examine changes to 'plain'? [Ynesfdaq?] y
784 examine changes to 'plain'? [Ynesfdaq?] y
785
785
786 @@ -9,3 +9,5 @@ 6
786 @@ -9,3 +9,5 @@ 6
787 7
787 7
788 8
788 8
789 9
789 9
790 +10
790 +10
791 +11
791 +11
792 record this change to 'plain'? [Ynesfdaq?] y
792 record this change to 'plain'? [Ynesfdaq?] y
793
793
794
794
795 $ hg tip -p
795 $ hg tip -p
796 changeset: 18:58a72f46bc24
796 changeset: 18:58a72f46bc24
797 tag: tip
797 tag: tip
798 user: test
798 user: test
799 date: Thu Jan 01 00:00:15 1970 +0000
799 date: Thu Jan 01 00:00:15 1970 +0000
800 summary: end-only
800 summary: end-only
801
801
802 diff -r 41cf3f5c55ae -r 58a72f46bc24 plain
802 diff -r 41cf3f5c55ae -r 58a72f46bc24 plain
803 --- a/plain Thu Jan 01 00:00:14 1970 +0000
803 --- a/plain Thu Jan 01 00:00:14 1970 +0000
804 +++ b/plain Thu Jan 01 00:00:15 1970 +0000
804 +++ b/plain Thu Jan 01 00:00:15 1970 +0000
805 @@ -9,3 +9,5 @@
805 @@ -9,3 +9,5 @@
806 7
806 7
807 8
807 8
808 9
808 9
809 +10
809 +10
810 +11
810 +11
811
811
812
812
813 $ mkdir subdir
813 $ mkdir subdir
814 $ cd subdir
814 $ cd subdir
815 $ echo a > a
815 $ echo a > a
816 $ hg ci -d '16 0' -Amsubdir
816 $ hg ci -d '16 0' -Amsubdir
817 adding subdir/a
817 adding subdir/a
818
818
819 $ echo a >> a
819 $ echo a >> a
820 $ hg commit -i -d '16 0' -m subdir-change a <<EOF
820 $ hg commit -i -d '16 0' -m subdir-change a <<EOF
821 > y
821 > y
822 > y
822 > y
823 > EOF
823 > EOF
824 diff --git a/subdir/a b/subdir/a
824 diff --git a/subdir/a b/subdir/a
825 1 hunks, 1 lines changed
825 1 hunks, 1 lines changed
826 examine changes to 'subdir/a'? [Ynesfdaq?] y
826 examine changes to 'subdir/a'? [Ynesfdaq?] y
827
827
828 @@ -1,1 +1,2 @@
828 @@ -1,1 +1,2 @@
829 a
829 a
830 +a
830 +a
831 record this change to 'subdir/a'? [Ynesfdaq?] y
831 record this change to 'subdir/a'? [Ynesfdaq?] y
832
832
833
833
834 $ hg tip -p
834 $ hg tip -p
835 changeset: 20:e0f6b99f6c49
835 changeset: 20:e0f6b99f6c49
836 tag: tip
836 tag: tip
837 user: test
837 user: test
838 date: Thu Jan 01 00:00:16 1970 +0000
838 date: Thu Jan 01 00:00:16 1970 +0000
839 summary: subdir-change
839 summary: subdir-change
840
840
841 diff -r abd26b51de37 -r e0f6b99f6c49 subdir/a
841 diff -r abd26b51de37 -r e0f6b99f6c49 subdir/a
842 --- a/subdir/a Thu Jan 01 00:00:16 1970 +0000
842 --- a/subdir/a Thu Jan 01 00:00:16 1970 +0000
843 +++ b/subdir/a Thu Jan 01 00:00:16 1970 +0000
843 +++ b/subdir/a Thu Jan 01 00:00:16 1970 +0000
844 @@ -1,1 +1,2 @@
844 @@ -1,1 +1,2 @@
845 a
845 a
846 +a
846 +a
847
847
848
848
849 $ echo a > f1
849 $ echo a > f1
850 $ echo b > f2
850 $ echo b > f2
851 $ hg add f1 f2
851 $ hg add f1 f2
852
852
853 $ hg ci -mz -d '17 0'
853 $ hg ci -mz -d '17 0'
854
854
855 $ echo a >> f1
855 $ echo a >> f1
856 $ echo b >> f2
856 $ echo b >> f2
857
857
858 Help, quit
858 Help, quit
859
859
860 $ hg commit -i <<EOF
860 $ hg commit -i <<EOF
861 > ?
861 > ?
862 > q
862 > q
863 > EOF
863 > EOF
864 diff --git a/subdir/f1 b/subdir/f1
864 diff --git a/subdir/f1 b/subdir/f1
865 1 hunks, 1 lines changed
865 1 hunks, 1 lines changed
866 examine changes to 'subdir/f1'? [Ynesfdaq?] ?
866 examine changes to 'subdir/f1'? [Ynesfdaq?] ?
867
867
868 y - yes, record this change
868 y - yes, record this change
869 n - no, skip this change
869 n - no, skip this change
870 e - edit this change manually
870 e - edit this change manually
871 s - skip remaining changes to this file
871 s - skip remaining changes to this file
872 f - record remaining changes to this file
872 f - record remaining changes to this file
873 d - done, skip remaining changes and files
873 d - done, skip remaining changes and files
874 a - record all changes to all remaining files
874 a - record all changes to all remaining files
875 q - quit, recording no changes
875 q - quit, recording no changes
876 ? - ? (display help)
876 ? - ? (display help)
877 examine changes to 'subdir/f1'? [Ynesfdaq?] q
877 examine changes to 'subdir/f1'? [Ynesfdaq?] q
878
878
879 abort: user quit
879 abort: user quit
880 [255]
880 [255]
881
881
882 #if gettext
882 #if gettext
883
883
884 Test translated help message
884 Test translated help message
885
885
886 str.lower() instead of encoding.lower(str) on translated message might
886 str.lower() instead of encoding.lower(str) on translated message might
887 make message meaningless, because some encoding uses 0x41(A) - 0x5a(Z)
887 make message meaningless, because some encoding uses 0x41(A) - 0x5a(Z)
888 as the second or later byte of multi-byte character.
888 as the second or later byte of multi-byte character.
889
889
890 For example, "\x8bL\x98^" (translation of "record" in ja_JP.cp932)
890 For example, "\x8bL\x98^" (translation of "record" in ja_JP.cp932)
891 contains 0x4c (L). str.lower() replaces 0x4c(L) by 0x6c(l) and this
891 contains 0x4c (L). str.lower() replaces 0x4c(L) by 0x6c(l) and this
892 replacement makes message meaningless.
892 replacement makes message meaningless.
893
893
894 This tests that translated help message is lower()-ed correctly.
894 This tests that translated help message is lower()-ed correctly.
895
895
896 $ LANGUAGE=ja
896 $ LANGUAGE=ja
897 $ export LANGUAGE
897 $ export LANGUAGE
898
898
899 $ cat > $TESTTMP/escape.py <<EOF
899 $ cat > $TESTTMP/escape.py <<EOF
900 > from __future__ import absolute_import
900 > from __future__ import absolute_import
901 > from mercurial import (
901 > from mercurial import (
902 > pycompat,
902 > pycompat,
903 > util,
903 > )
904 > from mercurial.utils import (
905 > procutil,
904 > )
906 > )
905 > def escape(c):
907 > def escape(c):
906 > o = ord(c)
908 > o = ord(c)
907 > if o < 0x80:
909 > if o < 0x80:
908 > return c
910 > return c
909 > else:
911 > else:
910 > return br'\x%02x' % o # escape char setting MSB
912 > return br'\x%02x' % o # escape char setting MSB
911 > for l in util.stdin:
913 > for l in procutil.stdin:
912 > util.stdout.write(b''.join(escape(c) for c in pycompat.iterbytestr(l)))
914 > procutil.stdout.write(
915 > b''.join(escape(c) for c in pycompat.iterbytestr(l)))
913 > EOF
916 > EOF
914
917
915 $ hg commit -i --encoding cp932 2>&1 <<EOF | $PYTHON $TESTTMP/escape.py | grep '^y - '
918 $ hg commit -i --encoding cp932 2>&1 <<EOF | $PYTHON $TESTTMP/escape.py | grep '^y - '
916 > ?
919 > ?
917 > q
920 > q
918 > EOF
921 > EOF
919 y - \x82\xb1\x82\xcc\x95\xcf\x8dX\x82\xf0\x8bL\x98^(yes)
922 y - \x82\xb1\x82\xcc\x95\xcf\x8dX\x82\xf0\x8bL\x98^(yes)
920
923
921 $ LANGUAGE=
924 $ LANGUAGE=
922 #endif
925 #endif
923
926
924 Skip
927 Skip
925
928
926 $ hg commit -i <<EOF
929 $ hg commit -i <<EOF
927 > s
930 > s
928 > EOF
931 > EOF
929 diff --git a/subdir/f1 b/subdir/f1
932 diff --git a/subdir/f1 b/subdir/f1
930 1 hunks, 1 lines changed
933 1 hunks, 1 lines changed
931 examine changes to 'subdir/f1'? [Ynesfdaq?] s
934 examine changes to 'subdir/f1'? [Ynesfdaq?] s
932
935
933 diff --git a/subdir/f2 b/subdir/f2
936 diff --git a/subdir/f2 b/subdir/f2
934 1 hunks, 1 lines changed
937 1 hunks, 1 lines changed
935 examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected
938 examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected
936 [255]
939 [255]
937
940
938 No
941 No
939
942
940 $ hg commit -i <<EOF
943 $ hg commit -i <<EOF
941 > n
944 > n
942 > EOF
945 > EOF
943 diff --git a/subdir/f1 b/subdir/f1
946 diff --git a/subdir/f1 b/subdir/f1
944 1 hunks, 1 lines changed
947 1 hunks, 1 lines changed
945 examine changes to 'subdir/f1'? [Ynesfdaq?] n
948 examine changes to 'subdir/f1'? [Ynesfdaq?] n
946
949
947 diff --git a/subdir/f2 b/subdir/f2
950 diff --git a/subdir/f2 b/subdir/f2
948 1 hunks, 1 lines changed
951 1 hunks, 1 lines changed
949 examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected
952 examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected
950 [255]
953 [255]
951
954
952 f, quit
955 f, quit
953
956
954 $ hg commit -i <<EOF
957 $ hg commit -i <<EOF
955 > f
958 > f
956 > q
959 > q
957 > EOF
960 > EOF
958 diff --git a/subdir/f1 b/subdir/f1
961 diff --git a/subdir/f1 b/subdir/f1
959 1 hunks, 1 lines changed
962 1 hunks, 1 lines changed
960 examine changes to 'subdir/f1'? [Ynesfdaq?] f
963 examine changes to 'subdir/f1'? [Ynesfdaq?] f
961
964
962 diff --git a/subdir/f2 b/subdir/f2
965 diff --git a/subdir/f2 b/subdir/f2
963 1 hunks, 1 lines changed
966 1 hunks, 1 lines changed
964 examine changes to 'subdir/f2'? [Ynesfdaq?] q
967 examine changes to 'subdir/f2'? [Ynesfdaq?] q
965
968
966 abort: user quit
969 abort: user quit
967 [255]
970 [255]
968
971
969 s, all
972 s, all
970
973
971 $ hg commit -i -d '18 0' -mx <<EOF
974 $ hg commit -i -d '18 0' -mx <<EOF
972 > s
975 > s
973 > a
976 > a
974 > EOF
977 > EOF
975 diff --git a/subdir/f1 b/subdir/f1
978 diff --git a/subdir/f1 b/subdir/f1
976 1 hunks, 1 lines changed
979 1 hunks, 1 lines changed
977 examine changes to 'subdir/f1'? [Ynesfdaq?] s
980 examine changes to 'subdir/f1'? [Ynesfdaq?] s
978
981
979 diff --git a/subdir/f2 b/subdir/f2
982 diff --git a/subdir/f2 b/subdir/f2
980 1 hunks, 1 lines changed
983 1 hunks, 1 lines changed
981 examine changes to 'subdir/f2'? [Ynesfdaq?] a
984 examine changes to 'subdir/f2'? [Ynesfdaq?] a
982
985
983
986
984 $ hg tip -p
987 $ hg tip -p
985 changeset: 22:6afbbefacf35
988 changeset: 22:6afbbefacf35
986 tag: tip
989 tag: tip
987 user: test
990 user: test
988 date: Thu Jan 01 00:00:18 1970 +0000
991 date: Thu Jan 01 00:00:18 1970 +0000
989 summary: x
992 summary: x
990
993
991 diff -r b73c401c693c -r 6afbbefacf35 subdir/f2
994 diff -r b73c401c693c -r 6afbbefacf35 subdir/f2
992 --- a/subdir/f2 Thu Jan 01 00:00:17 1970 +0000
995 --- a/subdir/f2 Thu Jan 01 00:00:17 1970 +0000
993 +++ b/subdir/f2 Thu Jan 01 00:00:18 1970 +0000
996 +++ b/subdir/f2 Thu Jan 01 00:00:18 1970 +0000
994 @@ -1,1 +1,2 @@
997 @@ -1,1 +1,2 @@
995 b
998 b
996 +b
999 +b
997
1000
998
1001
999 f
1002 f
1000
1003
1001 $ hg commit -i -d '19 0' -my <<EOF
1004 $ hg commit -i -d '19 0' -my <<EOF
1002 > f
1005 > f
1003 > EOF
1006 > EOF
1004 diff --git a/subdir/f1 b/subdir/f1
1007 diff --git a/subdir/f1 b/subdir/f1
1005 1 hunks, 1 lines changed
1008 1 hunks, 1 lines changed
1006 examine changes to 'subdir/f1'? [Ynesfdaq?] f
1009 examine changes to 'subdir/f1'? [Ynesfdaq?] f
1007
1010
1008
1011
1009 $ hg tip -p
1012 $ hg tip -p
1010 changeset: 23:715028a33949
1013 changeset: 23:715028a33949
1011 tag: tip
1014 tag: tip
1012 user: test
1015 user: test
1013 date: Thu Jan 01 00:00:19 1970 +0000
1016 date: Thu Jan 01 00:00:19 1970 +0000
1014 summary: y
1017 summary: y
1015
1018
1016 diff -r 6afbbefacf35 -r 715028a33949 subdir/f1
1019 diff -r 6afbbefacf35 -r 715028a33949 subdir/f1
1017 --- a/subdir/f1 Thu Jan 01 00:00:18 1970 +0000
1020 --- a/subdir/f1 Thu Jan 01 00:00:18 1970 +0000
1018 +++ b/subdir/f1 Thu Jan 01 00:00:19 1970 +0000
1021 +++ b/subdir/f1 Thu Jan 01 00:00:19 1970 +0000
1019 @@ -1,1 +1,2 @@
1022 @@ -1,1 +1,2 @@
1020 a
1023 a
1021 +a
1024 +a
1022
1025
1023
1026
1024 #if execbit
1027 #if execbit
1025
1028
1026 Preserve chmod +x
1029 Preserve chmod +x
1027
1030
1028 $ chmod +x f1
1031 $ chmod +x f1
1029 $ echo a >> f1
1032 $ echo a >> f1
1030 $ hg commit -i -d '20 0' -mz <<EOF
1033 $ hg commit -i -d '20 0' -mz <<EOF
1031 > y
1034 > y
1032 > y
1035 > y
1033 > y
1036 > y
1034 > EOF
1037 > EOF
1035 diff --git a/subdir/f1 b/subdir/f1
1038 diff --git a/subdir/f1 b/subdir/f1
1036 old mode 100644
1039 old mode 100644
1037 new mode 100755
1040 new mode 100755
1038 1 hunks, 1 lines changed
1041 1 hunks, 1 lines changed
1039 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1042 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1040
1043
1041 @@ -1,2 +1,3 @@
1044 @@ -1,2 +1,3 @@
1042 a
1045 a
1043 a
1046 a
1044 +a
1047 +a
1045 record this change to 'subdir/f1'? [Ynesfdaq?] y
1048 record this change to 'subdir/f1'? [Ynesfdaq?] y
1046
1049
1047
1050
1048 $ hg tip --config diff.git=True -p
1051 $ hg tip --config diff.git=True -p
1049 changeset: 24:db967c1e5884
1052 changeset: 24:db967c1e5884
1050 tag: tip
1053 tag: tip
1051 user: test
1054 user: test
1052 date: Thu Jan 01 00:00:20 1970 +0000
1055 date: Thu Jan 01 00:00:20 1970 +0000
1053 summary: z
1056 summary: z
1054
1057
1055 diff --git a/subdir/f1 b/subdir/f1
1058 diff --git a/subdir/f1 b/subdir/f1
1056 old mode 100644
1059 old mode 100644
1057 new mode 100755
1060 new mode 100755
1058 --- a/subdir/f1
1061 --- a/subdir/f1
1059 +++ b/subdir/f1
1062 +++ b/subdir/f1
1060 @@ -1,2 +1,3 @@
1063 @@ -1,2 +1,3 @@
1061 a
1064 a
1062 a
1065 a
1063 +a
1066 +a
1064
1067
1065
1068
1066 Preserve execute permission on original
1069 Preserve execute permission on original
1067
1070
1068 $ echo b >> f1
1071 $ echo b >> f1
1069 $ hg commit -i -d '21 0' -maa <<EOF
1072 $ hg commit -i -d '21 0' -maa <<EOF
1070 > y
1073 > y
1071 > y
1074 > y
1072 > y
1075 > y
1073 > EOF
1076 > EOF
1074 diff --git a/subdir/f1 b/subdir/f1
1077 diff --git a/subdir/f1 b/subdir/f1
1075 1 hunks, 1 lines changed
1078 1 hunks, 1 lines changed
1076 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1079 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1077
1080
1078 @@ -1,3 +1,4 @@
1081 @@ -1,3 +1,4 @@
1079 a
1082 a
1080 a
1083 a
1081 a
1084 a
1082 +b
1085 +b
1083 record this change to 'subdir/f1'? [Ynesfdaq?] y
1086 record this change to 'subdir/f1'? [Ynesfdaq?] y
1084
1087
1085
1088
1086 $ hg tip --config diff.git=True -p
1089 $ hg tip --config diff.git=True -p
1087 changeset: 25:88903aef81c3
1090 changeset: 25:88903aef81c3
1088 tag: tip
1091 tag: tip
1089 user: test
1092 user: test
1090 date: Thu Jan 01 00:00:21 1970 +0000
1093 date: Thu Jan 01 00:00:21 1970 +0000
1091 summary: aa
1094 summary: aa
1092
1095
1093 diff --git a/subdir/f1 b/subdir/f1
1096 diff --git a/subdir/f1 b/subdir/f1
1094 --- a/subdir/f1
1097 --- a/subdir/f1
1095 +++ b/subdir/f1
1098 +++ b/subdir/f1
1096 @@ -1,3 +1,4 @@
1099 @@ -1,3 +1,4 @@
1097 a
1100 a
1098 a
1101 a
1099 a
1102 a
1100 +b
1103 +b
1101
1104
1102
1105
1103 Preserve chmod -x
1106 Preserve chmod -x
1104
1107
1105 $ chmod -x f1
1108 $ chmod -x f1
1106 $ echo c >> f1
1109 $ echo c >> f1
1107 $ hg commit -i -d '22 0' -mab <<EOF
1110 $ hg commit -i -d '22 0' -mab <<EOF
1108 > y
1111 > y
1109 > y
1112 > y
1110 > y
1113 > y
1111 > EOF
1114 > EOF
1112 diff --git a/subdir/f1 b/subdir/f1
1115 diff --git a/subdir/f1 b/subdir/f1
1113 old mode 100755
1116 old mode 100755
1114 new mode 100644
1117 new mode 100644
1115 1 hunks, 1 lines changed
1118 1 hunks, 1 lines changed
1116 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1119 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1117
1120
1118 @@ -2,3 +2,4 @@ a
1121 @@ -2,3 +2,4 @@ a
1119 a
1122 a
1120 a
1123 a
1121 b
1124 b
1122 +c
1125 +c
1123 record this change to 'subdir/f1'? [Ynesfdaq?] y
1126 record this change to 'subdir/f1'? [Ynesfdaq?] y
1124
1127
1125
1128
1126 $ hg tip --config diff.git=True -p
1129 $ hg tip --config diff.git=True -p
1127 changeset: 26:7af84b6cf560
1130 changeset: 26:7af84b6cf560
1128 tag: tip
1131 tag: tip
1129 user: test
1132 user: test
1130 date: Thu Jan 01 00:00:22 1970 +0000
1133 date: Thu Jan 01 00:00:22 1970 +0000
1131 summary: ab
1134 summary: ab
1132
1135
1133 diff --git a/subdir/f1 b/subdir/f1
1136 diff --git a/subdir/f1 b/subdir/f1
1134 old mode 100755
1137 old mode 100755
1135 new mode 100644
1138 new mode 100644
1136 --- a/subdir/f1
1139 --- a/subdir/f1
1137 +++ b/subdir/f1
1140 +++ b/subdir/f1
1138 @@ -2,3 +2,4 @@
1141 @@ -2,3 +2,4 @@
1139 a
1142 a
1140 a
1143 a
1141 b
1144 b
1142 +c
1145 +c
1143
1146
1144
1147
1145 #else
1148 #else
1146
1149
1147 Slightly bogus tests to get almost same repo structure as when x bit is used
1150 Slightly bogus tests to get almost same repo structure as when x bit is used
1148 - but with different hashes.
1151 - but with different hashes.
1149
1152
1150 Mock "Preserve chmod +x"
1153 Mock "Preserve chmod +x"
1151
1154
1152 $ echo a >> f1
1155 $ echo a >> f1
1153 $ hg commit -i -d '20 0' -mz <<EOF
1156 $ hg commit -i -d '20 0' -mz <<EOF
1154 > y
1157 > y
1155 > y
1158 > y
1156 > y
1159 > y
1157 > EOF
1160 > EOF
1158 diff --git a/subdir/f1 b/subdir/f1
1161 diff --git a/subdir/f1 b/subdir/f1
1159 1 hunks, 1 lines changed
1162 1 hunks, 1 lines changed
1160 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1163 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1161
1164
1162 @@ -1,2 +1,3 @@
1165 @@ -1,2 +1,3 @@
1163 a
1166 a
1164 a
1167 a
1165 +a
1168 +a
1166 record this change to 'subdir/f1'? [Ynesfdaq?] y
1169 record this change to 'subdir/f1'? [Ynesfdaq?] y
1167
1170
1168
1171
1169 $ hg tip --config diff.git=True -p
1172 $ hg tip --config diff.git=True -p
1170 changeset: 24:c26cfe2c4eb0
1173 changeset: 24:c26cfe2c4eb0
1171 tag: tip
1174 tag: tip
1172 user: test
1175 user: test
1173 date: Thu Jan 01 00:00:20 1970 +0000
1176 date: Thu Jan 01 00:00:20 1970 +0000
1174 summary: z
1177 summary: z
1175
1178
1176 diff --git a/subdir/f1 b/subdir/f1
1179 diff --git a/subdir/f1 b/subdir/f1
1177 --- a/subdir/f1
1180 --- a/subdir/f1
1178 +++ b/subdir/f1
1181 +++ b/subdir/f1
1179 @@ -1,2 +1,3 @@
1182 @@ -1,2 +1,3 @@
1180 a
1183 a
1181 a
1184 a
1182 +a
1185 +a
1183
1186
1184
1187
1185 Mock "Preserve execute permission on original"
1188 Mock "Preserve execute permission on original"
1186
1189
1187 $ echo b >> f1
1190 $ echo b >> f1
1188 $ hg commit -i -d '21 0' -maa <<EOF
1191 $ hg commit -i -d '21 0' -maa <<EOF
1189 > y
1192 > y
1190 > y
1193 > y
1191 > y
1194 > y
1192 > EOF
1195 > EOF
1193 diff --git a/subdir/f1 b/subdir/f1
1196 diff --git a/subdir/f1 b/subdir/f1
1194 1 hunks, 1 lines changed
1197 1 hunks, 1 lines changed
1195 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1198 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1196
1199
1197 @@ -1,3 +1,4 @@
1200 @@ -1,3 +1,4 @@
1198 a
1201 a
1199 a
1202 a
1200 a
1203 a
1201 +b
1204 +b
1202 record this change to 'subdir/f1'? [Ynesfdaq?] y
1205 record this change to 'subdir/f1'? [Ynesfdaq?] y
1203
1206
1204
1207
1205 $ hg tip --config diff.git=True -p
1208 $ hg tip --config diff.git=True -p
1206 changeset: 25:a48d2d60adde
1209 changeset: 25:a48d2d60adde
1207 tag: tip
1210 tag: tip
1208 user: test
1211 user: test
1209 date: Thu Jan 01 00:00:21 1970 +0000
1212 date: Thu Jan 01 00:00:21 1970 +0000
1210 summary: aa
1213 summary: aa
1211
1214
1212 diff --git a/subdir/f1 b/subdir/f1
1215 diff --git a/subdir/f1 b/subdir/f1
1213 --- a/subdir/f1
1216 --- a/subdir/f1
1214 +++ b/subdir/f1
1217 +++ b/subdir/f1
1215 @@ -1,3 +1,4 @@
1218 @@ -1,3 +1,4 @@
1216 a
1219 a
1217 a
1220 a
1218 a
1221 a
1219 +b
1222 +b
1220
1223
1221
1224
1222 Mock "Preserve chmod -x"
1225 Mock "Preserve chmod -x"
1223
1226
1224 $ chmod -x f1
1227 $ chmod -x f1
1225 $ echo c >> f1
1228 $ echo c >> f1
1226 $ hg commit -i -d '22 0' -mab <<EOF
1229 $ hg commit -i -d '22 0' -mab <<EOF
1227 > y
1230 > y
1228 > y
1231 > y
1229 > y
1232 > y
1230 > EOF
1233 > EOF
1231 diff --git a/subdir/f1 b/subdir/f1
1234 diff --git a/subdir/f1 b/subdir/f1
1232 1 hunks, 1 lines changed
1235 1 hunks, 1 lines changed
1233 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1236 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1234
1237
1235 @@ -2,3 +2,4 @@ a
1238 @@ -2,3 +2,4 @@ a
1236 a
1239 a
1237 a
1240 a
1238 b
1241 b
1239 +c
1242 +c
1240 record this change to 'subdir/f1'? [Ynesfdaq?] y
1243 record this change to 'subdir/f1'? [Ynesfdaq?] y
1241
1244
1242
1245
1243 $ hg tip --config diff.git=True -p
1246 $ hg tip --config diff.git=True -p
1244 changeset: 26:5cc89ae210fa
1247 changeset: 26:5cc89ae210fa
1245 tag: tip
1248 tag: tip
1246 user: test
1249 user: test
1247 date: Thu Jan 01 00:00:22 1970 +0000
1250 date: Thu Jan 01 00:00:22 1970 +0000
1248 summary: ab
1251 summary: ab
1249
1252
1250 diff --git a/subdir/f1 b/subdir/f1
1253 diff --git a/subdir/f1 b/subdir/f1
1251 --- a/subdir/f1
1254 --- a/subdir/f1
1252 +++ b/subdir/f1
1255 +++ b/subdir/f1
1253 @@ -2,3 +2,4 @@
1256 @@ -2,3 +2,4 @@
1254 a
1257 a
1255 a
1258 a
1256 b
1259 b
1257 +c
1260 +c
1258
1261
1259
1262
1260 #endif
1263 #endif
1261
1264
1262 $ cd ..
1265 $ cd ..
1263
1266
1264
1267
1265 Abort early when a merge is in progress
1268 Abort early when a merge is in progress
1266
1269
1267 $ hg up 4
1270 $ hg up 4
1268 1 files updated, 0 files merged, 7 files removed, 0 files unresolved
1271 1 files updated, 0 files merged, 7 files removed, 0 files unresolved
1269
1272
1270 $ touch iwillmergethat
1273 $ touch iwillmergethat
1271 $ hg add iwillmergethat
1274 $ hg add iwillmergethat
1272
1275
1273 $ hg branch thatbranch
1276 $ hg branch thatbranch
1274 marked working directory as branch thatbranch
1277 marked working directory as branch thatbranch
1275 (branches are permanent and global, did you want a bookmark?)
1278 (branches are permanent and global, did you want a bookmark?)
1276
1279
1277 $ hg ci -m'new head'
1280 $ hg ci -m'new head'
1278
1281
1279 $ hg up default
1282 $ hg up default
1280 7 files updated, 0 files merged, 2 files removed, 0 files unresolved
1283 7 files updated, 0 files merged, 2 files removed, 0 files unresolved
1281
1284
1282 $ hg merge thatbranch
1285 $ hg merge thatbranch
1283 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1286 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1284 (branch merge, don't forget to commit)
1287 (branch merge, don't forget to commit)
1285
1288
1286 $ hg commit -i -m'will abort'
1289 $ hg commit -i -m'will abort'
1287 abort: cannot partially commit a merge (use "hg commit" instead)
1290 abort: cannot partially commit a merge (use "hg commit" instead)
1288 [255]
1291 [255]
1289
1292
1290 $ hg up -C
1293 $ hg up -C
1291 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1294 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1292
1295
1293 Editing patch (and ignoring trailing text)
1296 Editing patch (and ignoring trailing text)
1294
1297
1295 $ cat > editor.sh << '__EOF__'
1298 $ cat > editor.sh << '__EOF__'
1296 > sed -e 7d -e '5s/^-/ /' -e '/^# ---/i\
1299 > sed -e 7d -e '5s/^-/ /' -e '/^# ---/i\
1297 > trailing\nditto' "$1" > tmp
1300 > trailing\nditto' "$1" > tmp
1298 > mv tmp "$1"
1301 > mv tmp "$1"
1299 > __EOF__
1302 > __EOF__
1300 $ cat > editedfile << '__EOF__'
1303 $ cat > editedfile << '__EOF__'
1301 > This is the first line
1304 > This is the first line
1302 > This is the second line
1305 > This is the second line
1303 > This is the third line
1306 > This is the third line
1304 > __EOF__
1307 > __EOF__
1305 $ hg add editedfile
1308 $ hg add editedfile
1306 $ hg commit -medit-patch-1
1309 $ hg commit -medit-patch-1
1307 $ cat > editedfile << '__EOF__'
1310 $ cat > editedfile << '__EOF__'
1308 > This line has changed
1311 > This line has changed
1309 > This change will be committed
1312 > This change will be committed
1310 > This is the third line
1313 > This is the third line
1311 > __EOF__
1314 > __EOF__
1312 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-2 <<EOF
1315 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-2 <<EOF
1313 > y
1316 > y
1314 > e
1317 > e
1315 > EOF
1318 > EOF
1316 diff --git a/editedfile b/editedfile
1319 diff --git a/editedfile b/editedfile
1317 1 hunks, 2 lines changed
1320 1 hunks, 2 lines changed
1318 examine changes to 'editedfile'? [Ynesfdaq?] y
1321 examine changes to 'editedfile'? [Ynesfdaq?] y
1319
1322
1320 @@ -1,3 +1,3 @@
1323 @@ -1,3 +1,3 @@
1321 -This is the first line
1324 -This is the first line
1322 -This is the second line
1325 -This is the second line
1323 +This line has changed
1326 +This line has changed
1324 +This change will be committed
1327 +This change will be committed
1325 This is the third line
1328 This is the third line
1326 record this change to 'editedfile'? [Ynesfdaq?] e
1329 record this change to 'editedfile'? [Ynesfdaq?] e
1327
1330
1328 $ cat editedfile
1331 $ cat editedfile
1329 This line has changed
1332 This line has changed
1330 This change will be committed
1333 This change will be committed
1331 This is the third line
1334 This is the third line
1332 $ hg cat -r tip editedfile
1335 $ hg cat -r tip editedfile
1333 This is the first line
1336 This is the first line
1334 This change will be committed
1337 This change will be committed
1335 This is the third line
1338 This is the third line
1336 $ hg revert editedfile
1339 $ hg revert editedfile
1337
1340
1338 Trying to edit patch for whole file
1341 Trying to edit patch for whole file
1339
1342
1340 $ echo "This is the fourth line" >> editedfile
1343 $ echo "This is the fourth line" >> editedfile
1341 $ hg commit -i <<EOF
1344 $ hg commit -i <<EOF
1342 > e
1345 > e
1343 > q
1346 > q
1344 > EOF
1347 > EOF
1345 diff --git a/editedfile b/editedfile
1348 diff --git a/editedfile b/editedfile
1346 1 hunks, 1 lines changed
1349 1 hunks, 1 lines changed
1347 examine changes to 'editedfile'? [Ynesfdaq?] e
1350 examine changes to 'editedfile'? [Ynesfdaq?] e
1348
1351
1349 cannot edit patch for whole file
1352 cannot edit patch for whole file
1350 examine changes to 'editedfile'? [Ynesfdaq?] q
1353 examine changes to 'editedfile'? [Ynesfdaq?] q
1351
1354
1352 abort: user quit
1355 abort: user quit
1353 [255]
1356 [255]
1354 $ hg revert editedfile
1357 $ hg revert editedfile
1355
1358
1356 Removing changes from patch
1359 Removing changes from patch
1357
1360
1358 $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp
1361 $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp
1359 $ mv tmp editedfile
1362 $ mv tmp editedfile
1360 $ echo "This line has been added" >> editedfile
1363 $ echo "This line has been added" >> editedfile
1361 $ cat > editor.sh << '__EOF__'
1364 $ cat > editor.sh << '__EOF__'
1362 > sed -e 's/^[-+]/ /' "$1" > tmp
1365 > sed -e 's/^[-+]/ /' "$1" > tmp
1363 > mv tmp "$1"
1366 > mv tmp "$1"
1364 > __EOF__
1367 > __EOF__
1365 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1368 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1366 > y
1369 > y
1367 > e
1370 > e
1368 > EOF
1371 > EOF
1369 diff --git a/editedfile b/editedfile
1372 diff --git a/editedfile b/editedfile
1370 1 hunks, 3 lines changed
1373 1 hunks, 3 lines changed
1371 examine changes to 'editedfile'? [Ynesfdaq?] y
1374 examine changes to 'editedfile'? [Ynesfdaq?] y
1372
1375
1373 @@ -1,3 +1,3 @@
1376 @@ -1,3 +1,3 @@
1374 -This is the first line
1377 -This is the first line
1375 -This change will be committed
1378 -This change will be committed
1376 -This is the third line
1379 -This is the third line
1377 +This change will not be committed
1380 +This change will not be committed
1378 +This is the second line
1381 +This is the second line
1379 +This line has been added
1382 +This line has been added
1380 record this change to 'editedfile'? [Ynesfdaq?] e
1383 record this change to 'editedfile'? [Ynesfdaq?] e
1381
1384
1382 no changes to record
1385 no changes to record
1383 [1]
1386 [1]
1384 $ cat editedfile
1387 $ cat editedfile
1385 This change will not be committed
1388 This change will not be committed
1386 This is the second line
1389 This is the second line
1387 This line has been added
1390 This line has been added
1388 $ hg cat -r tip editedfile
1391 $ hg cat -r tip editedfile
1389 This is the first line
1392 This is the first line
1390 This change will be committed
1393 This change will be committed
1391 This is the third line
1394 This is the third line
1392 $ hg revert editedfile
1395 $ hg revert editedfile
1393
1396
1394 Invalid patch
1397 Invalid patch
1395
1398
1396 $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp
1399 $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp
1397 $ mv tmp editedfile
1400 $ mv tmp editedfile
1398 $ echo "This line has been added" >> editedfile
1401 $ echo "This line has been added" >> editedfile
1399 $ cat > editor.sh << '__EOF__'
1402 $ cat > editor.sh << '__EOF__'
1400 > sed s/This/That/ "$1" > tmp
1403 > sed s/This/That/ "$1" > tmp
1401 > mv tmp "$1"
1404 > mv tmp "$1"
1402 > __EOF__
1405 > __EOF__
1403 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1406 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1404 > y
1407 > y
1405 > e
1408 > e
1406 > EOF
1409 > EOF
1407 diff --git a/editedfile b/editedfile
1410 diff --git a/editedfile b/editedfile
1408 1 hunks, 3 lines changed
1411 1 hunks, 3 lines changed
1409 examine changes to 'editedfile'? [Ynesfdaq?] y
1412 examine changes to 'editedfile'? [Ynesfdaq?] y
1410
1413
1411 @@ -1,3 +1,3 @@
1414 @@ -1,3 +1,3 @@
1412 -This is the first line
1415 -This is the first line
1413 -This change will be committed
1416 -This change will be committed
1414 -This is the third line
1417 -This is the third line
1415 +This change will not be committed
1418 +This change will not be committed
1416 +This is the second line
1419 +This is the second line
1417 +This line has been added
1420 +This line has been added
1418 record this change to 'editedfile'? [Ynesfdaq?] e
1421 record this change to 'editedfile'? [Ynesfdaq?] e
1419
1422
1420 patching file editedfile
1423 patching file editedfile
1421 Hunk #1 FAILED at 0
1424 Hunk #1 FAILED at 0
1422 1 out of 1 hunks FAILED -- saving rejects to file editedfile.rej
1425 1 out of 1 hunks FAILED -- saving rejects to file editedfile.rej
1423 abort: patch failed to apply
1426 abort: patch failed to apply
1424 [255]
1427 [255]
1425 $ cat editedfile
1428 $ cat editedfile
1426 This change will not be committed
1429 This change will not be committed
1427 This is the second line
1430 This is the second line
1428 This line has been added
1431 This line has been added
1429 $ hg cat -r tip editedfile
1432 $ hg cat -r tip editedfile
1430 This is the first line
1433 This is the first line
1431 This change will be committed
1434 This change will be committed
1432 This is the third line
1435 This is the third line
1433 $ cat editedfile.rej
1436 $ cat editedfile.rej
1434 --- editedfile
1437 --- editedfile
1435 +++ editedfile
1438 +++ editedfile
1436 @@ -1,3 +1,3 @@
1439 @@ -1,3 +1,3 @@
1437 -That is the first line
1440 -That is the first line
1438 -That change will be committed
1441 -That change will be committed
1439 -That is the third line
1442 -That is the third line
1440 +That change will not be committed
1443 +That change will not be committed
1441 +That is the second line
1444 +That is the second line
1442 +That line has been added
1445 +That line has been added
1443
1446
1444 Malformed patch - error handling
1447 Malformed patch - error handling
1445
1448
1446 $ cat > editor.sh << '__EOF__'
1449 $ cat > editor.sh << '__EOF__'
1447 > sed -e '/^@/p' "$1" > tmp
1450 > sed -e '/^@/p' "$1" > tmp
1448 > mv tmp "$1"
1451 > mv tmp "$1"
1449 > __EOF__
1452 > __EOF__
1450 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1453 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1451 > y
1454 > y
1452 > e
1455 > e
1453 > EOF
1456 > EOF
1454 diff --git a/editedfile b/editedfile
1457 diff --git a/editedfile b/editedfile
1455 1 hunks, 3 lines changed
1458 1 hunks, 3 lines changed
1456 examine changes to 'editedfile'? [Ynesfdaq?] y
1459 examine changes to 'editedfile'? [Ynesfdaq?] y
1457
1460
1458 @@ -1,3 +1,3 @@
1461 @@ -1,3 +1,3 @@
1459 -This is the first line
1462 -This is the first line
1460 -This change will be committed
1463 -This change will be committed
1461 -This is the third line
1464 -This is the third line
1462 +This change will not be committed
1465 +This change will not be committed
1463 +This is the second line
1466 +This is the second line
1464 +This line has been added
1467 +This line has been added
1465 record this change to 'editedfile'? [Ynesfdaq?] e
1468 record this change to 'editedfile'? [Ynesfdaq?] e
1466
1469
1467 abort: error parsing patch: unhandled transition: range -> range
1470 abort: error parsing patch: unhandled transition: range -> range
1468 [255]
1471 [255]
1469
1472
1470 Exiting editor with status 1, ignores the edit but does not stop the recording
1473 Exiting editor with status 1, ignores the edit but does not stop the recording
1471 session
1474 session
1472
1475
1473 $ HGEDITOR=false hg commit -i <<EOF
1476 $ HGEDITOR=false hg commit -i <<EOF
1474 > y
1477 > y
1475 > e
1478 > e
1476 > n
1479 > n
1477 > EOF
1480 > EOF
1478 diff --git a/editedfile b/editedfile
1481 diff --git a/editedfile b/editedfile
1479 1 hunks, 3 lines changed
1482 1 hunks, 3 lines changed
1480 examine changes to 'editedfile'? [Ynesfdaq?] y
1483 examine changes to 'editedfile'? [Ynesfdaq?] y
1481
1484
1482 @@ -1,3 +1,3 @@
1485 @@ -1,3 +1,3 @@
1483 -This is the first line
1486 -This is the first line
1484 -This change will be committed
1487 -This change will be committed
1485 -This is the third line
1488 -This is the third line
1486 +This change will not be committed
1489 +This change will not be committed
1487 +This is the second line
1490 +This is the second line
1488 +This line has been added
1491 +This line has been added
1489 record this change to 'editedfile'? [Ynesfdaq?] e
1492 record this change to 'editedfile'? [Ynesfdaq?] e
1490
1493
1491 editor exited with exit code 1
1494 editor exited with exit code 1
1492 record this change to 'editedfile'? [Ynesfdaq?] n
1495 record this change to 'editedfile'? [Ynesfdaq?] n
1493
1496
1494 no changes to record
1497 no changes to record
1495 [1]
1498 [1]
1496
1499
1497
1500
1498 random text in random positions is still an error
1501 random text in random positions is still an error
1499
1502
1500 $ cat > editor.sh << '__EOF__'
1503 $ cat > editor.sh << '__EOF__'
1501 > sed -e '/^@/i\
1504 > sed -e '/^@/i\
1502 > other' "$1" > tmp
1505 > other' "$1" > tmp
1503 > mv tmp "$1"
1506 > mv tmp "$1"
1504 > __EOF__
1507 > __EOF__
1505 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1508 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1506 > y
1509 > y
1507 > e
1510 > e
1508 > EOF
1511 > EOF
1509 diff --git a/editedfile b/editedfile
1512 diff --git a/editedfile b/editedfile
1510 1 hunks, 3 lines changed
1513 1 hunks, 3 lines changed
1511 examine changes to 'editedfile'? [Ynesfdaq?] y
1514 examine changes to 'editedfile'? [Ynesfdaq?] y
1512
1515
1513 @@ -1,3 +1,3 @@
1516 @@ -1,3 +1,3 @@
1514 -This is the first line
1517 -This is the first line
1515 -This change will be committed
1518 -This change will be committed
1516 -This is the third line
1519 -This is the third line
1517 +This change will not be committed
1520 +This change will not be committed
1518 +This is the second line
1521 +This is the second line
1519 +This line has been added
1522 +This line has been added
1520 record this change to 'editedfile'? [Ynesfdaq?] e
1523 record this change to 'editedfile'? [Ynesfdaq?] e
1521
1524
1522 abort: error parsing patch: unhandled transition: file -> other
1525 abort: error parsing patch: unhandled transition: file -> other
1523 [255]
1526 [255]
1524
1527
1525 $ hg up -C
1528 $ hg up -C
1526 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1529 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1527
1530
1528 With win32text
1531 With win32text
1529
1532
1530 $ echo '[extensions]' >> .hg/hgrc
1533 $ echo '[extensions]' >> .hg/hgrc
1531 $ echo 'win32text = ' >> .hg/hgrc
1534 $ echo 'win32text = ' >> .hg/hgrc
1532 $ echo '[decode]' >> .hg/hgrc
1535 $ echo '[decode]' >> .hg/hgrc
1533 $ echo '** = cleverdecode:' >> .hg/hgrc
1536 $ echo '** = cleverdecode:' >> .hg/hgrc
1534 $ echo '[encode]' >> .hg/hgrc
1537 $ echo '[encode]' >> .hg/hgrc
1535 $ echo '** = cleverencode:' >> .hg/hgrc
1538 $ echo '** = cleverencode:' >> .hg/hgrc
1536 $ echo '[patch]' >> .hg/hgrc
1539 $ echo '[patch]' >> .hg/hgrc
1537 $ echo 'eol = crlf' >> .hg/hgrc
1540 $ echo 'eol = crlf' >> .hg/hgrc
1538
1541
1539 Ignore win32text deprecation warning for now:
1542 Ignore win32text deprecation warning for now:
1540
1543
1541 $ echo '[win32text]' >> .hg/hgrc
1544 $ echo '[win32text]' >> .hg/hgrc
1542 $ echo 'warn = no' >> .hg/hgrc
1545 $ echo 'warn = no' >> .hg/hgrc
1543
1546
1544 $ echo d >> subdir/f1
1547 $ echo d >> subdir/f1
1545 $ hg commit -i -d '24 0' -mw1 <<EOF
1548 $ hg commit -i -d '24 0' -mw1 <<EOF
1546 > y
1549 > y
1547 > y
1550 > y
1548 > EOF
1551 > EOF
1549 diff --git a/subdir/f1 b/subdir/f1
1552 diff --git a/subdir/f1 b/subdir/f1
1550 1 hunks, 1 lines changed
1553 1 hunks, 1 lines changed
1551 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1554 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1552
1555
1553 @@ -3,3 +3,4 @@ a
1556 @@ -3,3 +3,4 @@ a
1554 a
1557 a
1555 b
1558 b
1556 c
1559 c
1557 +d
1560 +d
1558 record this change to 'subdir/f1'? [Ynesfdaq?] y
1561 record this change to 'subdir/f1'? [Ynesfdaq?] y
1559
1562
1560
1563
1561 $ hg status -A subdir/f1
1564 $ hg status -A subdir/f1
1562 C subdir/f1
1565 C subdir/f1
1563 $ hg tip -p
1566 $ hg tip -p
1564 changeset: 30:* (glob)
1567 changeset: 30:* (glob)
1565 tag: tip
1568 tag: tip
1566 user: test
1569 user: test
1567 date: Thu Jan 01 00:00:24 1970 +0000
1570 date: Thu Jan 01 00:00:24 1970 +0000
1568 summary: w1
1571 summary: w1
1569
1572
1570 diff -r ???????????? -r ???????????? subdir/f1 (glob)
1573 diff -r ???????????? -r ???????????? subdir/f1 (glob)
1571 --- a/subdir/f1 Thu Jan 01 00:00:23 1970 +0000
1574 --- a/subdir/f1 Thu Jan 01 00:00:23 1970 +0000
1572 +++ b/subdir/f1 Thu Jan 01 00:00:24 1970 +0000
1575 +++ b/subdir/f1 Thu Jan 01 00:00:24 1970 +0000
1573 @@ -3,3 +3,4 @@
1576 @@ -3,3 +3,4 @@
1574 a
1577 a
1575 b
1578 b
1576 c
1579 c
1577 +d
1580 +d
1578
1581
1579
1582
1580
1583
1581 Test --user when ui.username not set
1584 Test --user when ui.username not set
1582 $ unset HGUSER
1585 $ unset HGUSER
1583 $ echo e >> subdir/f1
1586 $ echo e >> subdir/f1
1584 $ hg commit -i --config ui.username= -d '8 0' --user xyz -m "user flag" <<EOF
1587 $ hg commit -i --config ui.username= -d '8 0' --user xyz -m "user flag" <<EOF
1585 > y
1588 > y
1586 > y
1589 > y
1587 > EOF
1590 > EOF
1588 diff --git a/subdir/f1 b/subdir/f1
1591 diff --git a/subdir/f1 b/subdir/f1
1589 1 hunks, 1 lines changed
1592 1 hunks, 1 lines changed
1590 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1593 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1591
1594
1592 @@ -4,3 +4,4 @@ a
1595 @@ -4,3 +4,4 @@ a
1593 b
1596 b
1594 c
1597 c
1595 d
1598 d
1596 +e
1599 +e
1597 record this change to 'subdir/f1'? [Ynesfdaq?] y
1600 record this change to 'subdir/f1'? [Ynesfdaq?] y
1598
1601
1599 $ hg status -A subdir/f1
1602 $ hg status -A subdir/f1
1600 C subdir/f1
1603 C subdir/f1
1601 $ hg log --template '{author}\n' -l 1
1604 $ hg log --template '{author}\n' -l 1
1602 xyz
1605 xyz
1603 $ HGUSER="test"
1606 $ HGUSER="test"
1604 $ export HGUSER
1607 $ export HGUSER
1605
1608
1606
1609
1607 Moving files
1610 Moving files
1608
1611
1609 $ hg update -C .
1612 $ hg update -C .
1610 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1613 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1611 $ hg mv plain plain3
1614 $ hg mv plain plain3
1612 $ echo somechange >> plain3
1615 $ echo somechange >> plain3
1613 $ hg commit -i -d '23 0' -mmoving_files << EOF
1616 $ hg commit -i -d '23 0' -mmoving_files << EOF
1614 > y
1617 > y
1615 > y
1618 > y
1616 > EOF
1619 > EOF
1617 diff --git a/plain b/plain3
1620 diff --git a/plain b/plain3
1618 rename from plain
1621 rename from plain
1619 rename to plain3
1622 rename to plain3
1620 1 hunks, 1 lines changed
1623 1 hunks, 1 lines changed
1621 examine changes to 'plain' and 'plain3'? [Ynesfdaq?] y
1624 examine changes to 'plain' and 'plain3'? [Ynesfdaq?] y
1622
1625
1623 @@ -11,3 +11,4 @@ 8
1626 @@ -11,3 +11,4 @@ 8
1624 9
1627 9
1625 10
1628 10
1626 11
1629 11
1627 +somechange
1630 +somechange
1628 record this change to 'plain3'? [Ynesfdaq?] y
1631 record this change to 'plain3'? [Ynesfdaq?] y
1629
1632
1630 The #if execbit block above changes the hash here on some systems
1633 The #if execbit block above changes the hash here on some systems
1631 $ hg status -A plain3
1634 $ hg status -A plain3
1632 C plain3
1635 C plain3
1633 $ hg tip
1636 $ hg tip
1634 changeset: 32:* (glob)
1637 changeset: 32:* (glob)
1635 tag: tip
1638 tag: tip
1636 user: test
1639 user: test
1637 date: Thu Jan 01 00:00:23 1970 +0000
1640 date: Thu Jan 01 00:00:23 1970 +0000
1638 summary: moving_files
1641 summary: moving_files
1639
1642
1640 Editing patch of newly added file
1643 Editing patch of newly added file
1641
1644
1642 $ hg update -C .
1645 $ hg update -C .
1643 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1646 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1644 $ cat > editor.sh << '__EOF__'
1647 $ cat > editor.sh << '__EOF__'
1645 > cat "$1" | sed "s/first/very/g" > tt
1648 > cat "$1" | sed "s/first/very/g" > tt
1646 > mv tt "$1"
1649 > mv tt "$1"
1647 > __EOF__
1650 > __EOF__
1648 $ cat > newfile << '__EOF__'
1651 $ cat > newfile << '__EOF__'
1649 > This is the first line
1652 > This is the first line
1650 > This is the second line
1653 > This is the second line
1651 > This is the third line
1654 > This is the third line
1652 > __EOF__
1655 > __EOF__
1653 $ hg add newfile
1656 $ hg add newfile
1654 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-new <<EOF
1657 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-new <<EOF
1655 > y
1658 > y
1656 > e
1659 > e
1657 > EOF
1660 > EOF
1658 diff --git a/newfile b/newfile
1661 diff --git a/newfile b/newfile
1659 new file mode 100644
1662 new file mode 100644
1660 examine changes to 'newfile'? [Ynesfdaq?] y
1663 examine changes to 'newfile'? [Ynesfdaq?] y
1661
1664
1662 @@ -0,0 +1,3 @@
1665 @@ -0,0 +1,3 @@
1663 +This is the first line
1666 +This is the first line
1664 +This is the second line
1667 +This is the second line
1665 +This is the third line
1668 +This is the third line
1666 record this change to 'newfile'? [Ynesfdaq?] e
1669 record this change to 'newfile'? [Ynesfdaq?] e
1667
1670
1668 $ hg cat -r tip newfile
1671 $ hg cat -r tip newfile
1669 This is the very line
1672 This is the very line
1670 This is the second line
1673 This is the second line
1671 This is the third line
1674 This is the third line
1672
1675
1673 $ cat newfile
1676 $ cat newfile
1674 This is the first line
1677 This is the first line
1675 This is the second line
1678 This is the second line
1676 This is the third line
1679 This is the third line
1677
1680
1678 Add new file from within a subdirectory
1681 Add new file from within a subdirectory
1679 $ hg update -C .
1682 $ hg update -C .
1680 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1683 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1681 $ mkdir folder
1684 $ mkdir folder
1682 $ cd folder
1685 $ cd folder
1683 $ echo "foo" > bar
1686 $ echo "foo" > bar
1684 $ hg add bar
1687 $ hg add bar
1685 $ hg commit -i -d '23 0' -mnewfilesubdir <<EOF
1688 $ hg commit -i -d '23 0' -mnewfilesubdir <<EOF
1686 > y
1689 > y
1687 > y
1690 > y
1688 > EOF
1691 > EOF
1689 diff --git a/folder/bar b/folder/bar
1692 diff --git a/folder/bar b/folder/bar
1690 new file mode 100644
1693 new file mode 100644
1691 examine changes to 'folder/bar'? [Ynesfdaq?] y
1694 examine changes to 'folder/bar'? [Ynesfdaq?] y
1692
1695
1693 @@ -0,0 +1,1 @@
1696 @@ -0,0 +1,1 @@
1694 +foo
1697 +foo
1695 record this change to 'folder/bar'? [Ynesfdaq?] y
1698 record this change to 'folder/bar'? [Ynesfdaq?] y
1696
1699
1697 The #if execbit block above changes the hashes here on some systems
1700 The #if execbit block above changes the hashes here on some systems
1698 $ hg tip -p
1701 $ hg tip -p
1699 changeset: 34:* (glob)
1702 changeset: 34:* (glob)
1700 tag: tip
1703 tag: tip
1701 user: test
1704 user: test
1702 date: Thu Jan 01 00:00:23 1970 +0000
1705 date: Thu Jan 01 00:00:23 1970 +0000
1703 summary: newfilesubdir
1706 summary: newfilesubdir
1704
1707
1705 diff -r * -r * folder/bar (glob)
1708 diff -r * -r * folder/bar (glob)
1706 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1709 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1707 +++ b/folder/bar Thu Jan 01 00:00:23 1970 +0000
1710 +++ b/folder/bar Thu Jan 01 00:00:23 1970 +0000
1708 @@ -0,0 +1,1 @@
1711 @@ -0,0 +1,1 @@
1709 +foo
1712 +foo
1710
1713
1711 $ cd ..
1714 $ cd ..
1712
1715
1713 $ hg status -A folder/bar
1716 $ hg status -A folder/bar
1714 C folder/bar
1717 C folder/bar
1715
1718
1716 Clear win32text configuration before size/timestamp sensitive test
1719 Clear win32text configuration before size/timestamp sensitive test
1717
1720
1718 $ cat >> .hg/hgrc <<EOF
1721 $ cat >> .hg/hgrc <<EOF
1719 > [extensions]
1722 > [extensions]
1720 > win32text = !
1723 > win32text = !
1721 > [decode]
1724 > [decode]
1722 > ** = !
1725 > ** = !
1723 > [encode]
1726 > [encode]
1724 > ** = !
1727 > ** = !
1725 > [patch]
1728 > [patch]
1726 > eol = strict
1729 > eol = strict
1727 > EOF
1730 > EOF
1728 $ hg update -q -C null
1731 $ hg update -q -C null
1729 $ hg update -q -C tip
1732 $ hg update -q -C tip
1730
1733
1731 Test that partially committed file is still treated as "modified",
1734 Test that partially committed file is still treated as "modified",
1732 even if none of mode, size and timestamp is changed on the filesystem
1735 even if none of mode, size and timestamp is changed on the filesystem
1733 (see also issue4583).
1736 (see also issue4583).
1734
1737
1735 $ cat > subdir/f1 <<EOF
1738 $ cat > subdir/f1 <<EOF
1736 > A
1739 > A
1737 > a
1740 > a
1738 > a
1741 > a
1739 > b
1742 > b
1740 > c
1743 > c
1741 > d
1744 > d
1742 > E
1745 > E
1743 > EOF
1746 > EOF
1744 $ hg diff --git subdir/f1
1747 $ hg diff --git subdir/f1
1745 diff --git a/subdir/f1 b/subdir/f1
1748 diff --git a/subdir/f1 b/subdir/f1
1746 --- a/subdir/f1
1749 --- a/subdir/f1
1747 +++ b/subdir/f1
1750 +++ b/subdir/f1
1748 @@ -1,7 +1,7 @@
1751 @@ -1,7 +1,7 @@
1749 -a
1752 -a
1750 +A
1753 +A
1751 a
1754 a
1752 a
1755 a
1753 b
1756 b
1754 c
1757 c
1755 d
1758 d
1756 -e
1759 -e
1757 +E
1760 +E
1758
1761
1759 $ touch -t 200001010000 subdir/f1
1762 $ touch -t 200001010000 subdir/f1
1760
1763
1761 $ cat >> .hg/hgrc <<EOF
1764 $ cat >> .hg/hgrc <<EOF
1762 > # emulate invoking patch.internalpatch() at 2000-01-01 00:00
1765 > # emulate invoking patch.internalpatch() at 2000-01-01 00:00
1763 > [fakepatchtime]
1766 > [fakepatchtime]
1764 > fakenow = 200001010000
1767 > fakenow = 200001010000
1765 >
1768 >
1766 > [extensions]
1769 > [extensions]
1767 > fakepatchtime = $TESTDIR/fakepatchtime.py
1770 > fakepatchtime = $TESTDIR/fakepatchtime.py
1768 > EOF
1771 > EOF
1769 $ hg commit -i -m 'commit subdir/f1 partially' <<EOF
1772 $ hg commit -i -m 'commit subdir/f1 partially' <<EOF
1770 > y
1773 > y
1771 > y
1774 > y
1772 > n
1775 > n
1773 > EOF
1776 > EOF
1774 diff --git a/subdir/f1 b/subdir/f1
1777 diff --git a/subdir/f1 b/subdir/f1
1775 2 hunks, 2 lines changed
1778 2 hunks, 2 lines changed
1776 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1779 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1777
1780
1778 @@ -1,6 +1,6 @@
1781 @@ -1,6 +1,6 @@
1779 -a
1782 -a
1780 +A
1783 +A
1781 a
1784 a
1782 a
1785 a
1783 b
1786 b
1784 c
1787 c
1785 d
1788 d
1786 record change 1/2 to 'subdir/f1'? [Ynesfdaq?] y
1789 record change 1/2 to 'subdir/f1'? [Ynesfdaq?] y
1787
1790
1788 @@ -2,6 +2,6 @@
1791 @@ -2,6 +2,6 @@
1789 a
1792 a
1790 a
1793 a
1791 b
1794 b
1792 c
1795 c
1793 d
1796 d
1794 -e
1797 -e
1795 +E
1798 +E
1796 record change 2/2 to 'subdir/f1'? [Ynesfdaq?] n
1799 record change 2/2 to 'subdir/f1'? [Ynesfdaq?] n
1797
1800
1798 $ cat >> .hg/hgrc <<EOF
1801 $ cat >> .hg/hgrc <<EOF
1799 > [extensions]
1802 > [extensions]
1800 > fakepatchtime = !
1803 > fakepatchtime = !
1801 > EOF
1804 > EOF
1802
1805
1803 $ hg debugstate | grep ' subdir/f1$'
1806 $ hg debugstate | grep ' subdir/f1$'
1804 n 0 -1 unset subdir/f1
1807 n 0 -1 unset subdir/f1
1805 $ hg status -A subdir/f1
1808 $ hg status -A subdir/f1
1806 M subdir/f1
1809 M subdir/f1
@@ -1,50 +1,54 b''
1 # Test the config layer generated by environment variables
1 # Test the config layer generated by environment variables
2
2
3 from __future__ import absolute_import, print_function
3 from __future__ import absolute_import, print_function
4
4
5 import os
5 import os
6
6
7 from mercurial import (
7 from mercurial import (
8 encoding,
8 encoding,
9 rcutil,
9 rcutil,
10 ui as uimod,
10 ui as uimod,
11 util,
11 util,
12 )
12 )
13
13
14 from mercurial.utils import (
15 procutil,
16 )
17
14 testtmp = encoding.environ[b'TESTTMP']
18 testtmp = encoding.environ[b'TESTTMP']
15
19
16 # prepare hgrc files
20 # prepare hgrc files
17 def join(name):
21 def join(name):
18 return os.path.join(testtmp, name)
22 return os.path.join(testtmp, name)
19
23
20 with open(join(b'sysrc'), 'wb') as f:
24 with open(join(b'sysrc'), 'wb') as f:
21 f.write(b'[ui]\neditor=e0\n[pager]\npager=p0\n')
25 f.write(b'[ui]\neditor=e0\n[pager]\npager=p0\n')
22
26
23 with open(join(b'userrc'), 'wb') as f:
27 with open(join(b'userrc'), 'wb') as f:
24 f.write(b'[ui]\neditor=e1')
28 f.write(b'[ui]\neditor=e1')
25
29
26 # replace rcpath functions so they point to the files above
30 # replace rcpath functions so they point to the files above
27 def systemrcpath():
31 def systemrcpath():
28 return [join(b'sysrc')]
32 return [join(b'sysrc')]
29
33
30 def userrcpath():
34 def userrcpath():
31 return [join(b'userrc')]
35 return [join(b'userrc')]
32
36
33 rcutil.systemrcpath = systemrcpath
37 rcutil.systemrcpath = systemrcpath
34 rcutil.userrcpath = userrcpath
38 rcutil.userrcpath = userrcpath
35 os.path.isdir = lambda x: False # hack: do not load default.d/*.rc
39 os.path.isdir = lambda x: False # hack: do not load default.d/*.rc
36
40
37 # utility to print configs
41 # utility to print configs
38 def printconfigs(env):
42 def printconfigs(env):
39 encoding.environ = env
43 encoding.environ = env
40 rcutil._rccomponents = None # reset cache
44 rcutil._rccomponents = None # reset cache
41 ui = uimod.ui.load()
45 ui = uimod.ui.load()
42 for section, name, value in ui.walkconfig():
46 for section, name, value in ui.walkconfig():
43 source = ui.configsource(section, name)
47 source = ui.configsource(section, name)
44 util.stdout.write(b'%s.%s=%s # %s\n'
48 procutil.stdout.write(b'%s.%s=%s # %s\n'
45 % (section, name, value, util.pconvert(source)))
49 % (section, name, value, util.pconvert(source)))
46 util.stdout.write(b'\n')
50 procutil.stdout.write(b'\n')
47
51
48 # environment variable overrides
52 # environment variable overrides
49 printconfigs({})
53 printconfigs({})
50 printconfigs({b'EDITOR': b'e2', b'PAGER': b'p2'})
54 printconfigs({b'EDITOR': b'e2', b'PAGER': b'p2'})
General Comments 0
You need to be logged in to leave comments. Login now