##// END OF EJS Templates
chgserver: respect detailed exit code in case of ConfigError...
Pulkit Goyal -
r46761:6383bb86 default
parent child Browse files
Show More
@@ -1,761 +1,763 b''
1 1 # chgserver.py - command server extension for cHg
2 2 #
3 3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """command server extension for cHg
9 9
10 10 'S' channel (read/write)
11 11 propagate ui.system() request to client
12 12
13 13 'attachio' command
14 14 attach client's stdio passed by sendmsg()
15 15
16 16 'chdir' command
17 17 change current directory
18 18
19 19 'setenv' command
20 20 replace os.environ completely
21 21
22 22 'setumask' command (DEPRECATED)
23 23 'setumask2' command
24 24 set umask
25 25
26 26 'validate' command
27 27 reload the config and check if the server is up to date
28 28
29 29 Config
30 30 ------
31 31
32 32 ::
33 33
34 34 [chgserver]
35 35 # how long (in seconds) should an idle chg server exit
36 36 idletimeout = 3600
37 37
38 38 # whether to skip config or env change checks
39 39 skiphash = False
40 40 """
41 41
42 42 from __future__ import absolute_import
43 43
44 44 import inspect
45 45 import os
46 46 import re
47 47 import socket
48 48 import stat
49 49 import struct
50 50 import time
51 51
52 52 from .i18n import _
53 53 from .pycompat import (
54 54 getattr,
55 55 setattr,
56 56 )
57 57 from .node import hex
58 58
59 59 from . import (
60 60 commandserver,
61 61 encoding,
62 62 error,
63 63 extensions,
64 64 pycompat,
65 65 util,
66 66 )
67 67
68 68 from .utils import (
69 69 hashutil,
70 70 procutil,
71 71 stringutil,
72 72 )
73 73
74 74
75 75 def _hashlist(items):
76 76 """return sha1 hexdigest for a list"""
77 77 return hex(hashutil.sha1(stringutil.pprint(items)).digest())
78 78
79 79
80 80 # sensitive config sections affecting confighash
81 81 _configsections = [
82 82 b'alias', # affects global state commands.table
83 83 b'diff-tools', # affects whether gui or not in extdiff's uisetup
84 84 b'eol', # uses setconfig('eol', ...)
85 85 b'extdiff', # uisetup will register new commands
86 86 b'extensions',
87 87 b'fastannotate', # affects annotate command and adds fastannonate cmd
88 88 b'merge-tools', # affects whether gui or not in extdiff's uisetup
89 89 b'schemes', # extsetup will update global hg.schemes
90 90 ]
91 91
92 92 _configsectionitems = [
93 93 (b'commands', b'show.aliasprefix'), # show.py reads it in extsetup
94 94 ]
95 95
96 96 # sensitive environment variables affecting confighash
97 97 _envre = re.compile(
98 98 br'''\A(?:
99 99 CHGHG
100 100 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
101 101 |HG(?:ENCODING|PLAIN).*
102 102 |LANG(?:UAGE)?
103 103 |LC_.*
104 104 |LD_.*
105 105 |PATH
106 106 |PYTHON.*
107 107 |TERM(?:INFO)?
108 108 |TZ
109 109 )\Z''',
110 110 re.X,
111 111 )
112 112
113 113
114 114 def _confighash(ui):
115 115 """return a quick hash for detecting config/env changes
116 116
117 117 confighash is the hash of sensitive config items and environment variables.
118 118
119 119 for chgserver, it is designed that once confighash changes, the server is
120 120 not qualified to serve its client and should redirect the client to a new
121 121 server. different from mtimehash, confighash change will not mark the
122 122 server outdated and exit since the user can have different configs at the
123 123 same time.
124 124 """
125 125 sectionitems = []
126 126 for section in _configsections:
127 127 sectionitems.append(ui.configitems(section))
128 128 for section, item in _configsectionitems:
129 129 sectionitems.append(ui.config(section, item))
130 130 sectionhash = _hashlist(sectionitems)
131 131 # If $CHGHG is set, the change to $HG should not trigger a new chg server
132 132 if b'CHGHG' in encoding.environ:
133 133 ignored = {b'HG'}
134 134 else:
135 135 ignored = set()
136 136 envitems = [
137 137 (k, v)
138 138 for k, v in pycompat.iteritems(encoding.environ)
139 139 if _envre.match(k) and k not in ignored
140 140 ]
141 141 envhash = _hashlist(sorted(envitems))
142 142 return sectionhash[:6] + envhash[:6]
143 143
144 144
145 145 def _getmtimepaths(ui):
146 146 """get a list of paths that should be checked to detect change
147 147
148 148 The list will include:
149 149 - extensions (will not cover all files for complex extensions)
150 150 - mercurial/__version__.py
151 151 - python binary
152 152 """
153 153 modules = [m for n, m in extensions.extensions(ui)]
154 154 try:
155 155 from . import __version__
156 156
157 157 modules.append(__version__)
158 158 except ImportError:
159 159 pass
160 160 files = []
161 161 if pycompat.sysexecutable:
162 162 files.append(pycompat.sysexecutable)
163 163 for m in modules:
164 164 try:
165 165 files.append(pycompat.fsencode(inspect.getabsfile(m)))
166 166 except TypeError:
167 167 pass
168 168 return sorted(set(files))
169 169
170 170
171 171 def _mtimehash(paths):
172 172 """return a quick hash for detecting file changes
173 173
174 174 mtimehash calls stat on given paths and calculate a hash based on size and
175 175 mtime of each file. mtimehash does not read file content because reading is
176 176 expensive. therefore it's not 100% reliable for detecting content changes.
177 177 it's possible to return different hashes for same file contents.
178 178 it's also possible to return a same hash for different file contents for
179 179 some carefully crafted situation.
180 180
181 181 for chgserver, it is designed that once mtimehash changes, the server is
182 182 considered outdated immediately and should no longer provide service.
183 183
184 184 mtimehash is not included in confighash because we only know the paths of
185 185 extensions after importing them (there is imp.find_module but that faces
186 186 race conditions). We need to calculate confighash without importing.
187 187 """
188 188
189 189 def trystat(path):
190 190 try:
191 191 st = os.stat(path)
192 192 return (st[stat.ST_MTIME], st.st_size)
193 193 except OSError:
194 194 # could be ENOENT, EPERM etc. not fatal in any case
195 195 pass
196 196
197 197 return _hashlist(pycompat.maplist(trystat, paths))[:12]
198 198
199 199
200 200 class hashstate(object):
201 201 """a structure storing confighash, mtimehash, paths used for mtimehash"""
202 202
203 203 def __init__(self, confighash, mtimehash, mtimepaths):
204 204 self.confighash = confighash
205 205 self.mtimehash = mtimehash
206 206 self.mtimepaths = mtimepaths
207 207
208 208 @staticmethod
209 209 def fromui(ui, mtimepaths=None):
210 210 if mtimepaths is None:
211 211 mtimepaths = _getmtimepaths(ui)
212 212 confighash = _confighash(ui)
213 213 mtimehash = _mtimehash(mtimepaths)
214 214 ui.log(
215 215 b'cmdserver',
216 216 b'confighash = %s mtimehash = %s\n',
217 217 confighash,
218 218 mtimehash,
219 219 )
220 220 return hashstate(confighash, mtimehash, mtimepaths)
221 221
222 222
223 223 def _newchgui(srcui, csystem, attachio):
224 224 class chgui(srcui.__class__):
225 225 def __init__(self, src=None):
226 226 super(chgui, self).__init__(src)
227 227 if src:
228 228 self._csystem = getattr(src, '_csystem', csystem)
229 229 else:
230 230 self._csystem = csystem
231 231
232 232 def _runsystem(self, cmd, environ, cwd, out):
233 233 # fallback to the original system method if
234 234 # a. the output stream is not stdout (e.g. stderr, cStringIO),
235 235 # b. or stdout is redirected by protectfinout(),
236 236 # because the chg client is not aware of these situations and
237 237 # will behave differently (i.e. write to stdout).
238 238 if (
239 239 out is not self.fout
240 240 or not util.safehasattr(self.fout, b'fileno')
241 241 or self.fout.fileno() != procutil.stdout.fileno()
242 242 or self._finoutredirected
243 243 ):
244 244 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
245 245 self.flush()
246 246 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
247 247
248 248 def _runpager(self, cmd, env=None):
249 249 self._csystem(
250 250 cmd,
251 251 procutil.shellenviron(env),
252 252 type=b'pager',
253 253 cmdtable={b'attachio': attachio},
254 254 )
255 255 return True
256 256
257 257 return chgui(srcui)
258 258
259 259
260 260 def _loadnewui(srcui, args, cdebug):
261 261 from . import dispatch # avoid cycle
262 262
263 263 newui = srcui.__class__.load()
264 264 for a in [b'fin', b'fout', b'ferr', b'environ']:
265 265 setattr(newui, a, getattr(srcui, a))
266 266 if util.safehasattr(srcui, b'_csystem'):
267 267 newui._csystem = srcui._csystem
268 268
269 269 # command line args
270 270 options = dispatch._earlyparseopts(newui, args)
271 271 dispatch._parseconfig(newui, options[b'config'])
272 272
273 273 # stolen from tortoisehg.util.copydynamicconfig()
274 274 for section, name, value in srcui.walkconfig():
275 275 source = srcui.configsource(section, name)
276 276 if b':' in source or source == b'--config' or source.startswith(b'$'):
277 277 # path:line or command line, or environ
278 278 continue
279 279 newui.setconfig(section, name, value, source)
280 280
281 281 # load wd and repo config, copied from dispatch.py
282 282 cwd = options[b'cwd']
283 283 cwd = cwd and os.path.realpath(cwd) or None
284 284 rpath = options[b'repository']
285 285 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
286 286
287 287 extensions.populateui(newui)
288 288 commandserver.setuplogging(newui, fp=cdebug)
289 289 if newui is not newlui:
290 290 extensions.populateui(newlui)
291 291 commandserver.setuplogging(newlui, fp=cdebug)
292 292
293 293 return (newui, newlui)
294 294
295 295
296 296 class channeledsystem(object):
297 297 """Propagate ui.system() request in the following format:
298 298
299 299 payload length (unsigned int),
300 300 type, '\0',
301 301 cmd, '\0',
302 302 cwd, '\0',
303 303 envkey, '=', val, '\0',
304 304 ...
305 305 envkey, '=', val
306 306
307 307 if type == 'system', waits for:
308 308
309 309 exitcode length (unsigned int),
310 310 exitcode (int)
311 311
312 312 if type == 'pager', repetitively waits for a command name ending with '\n'
313 313 and executes it defined by cmdtable, or exits the loop if the command name
314 314 is empty.
315 315 """
316 316
317 317 def __init__(self, in_, out, channel):
318 318 self.in_ = in_
319 319 self.out = out
320 320 self.channel = channel
321 321
322 322 def __call__(self, cmd, environ, cwd=None, type=b'system', cmdtable=None):
323 323 args = [type, cmd, os.path.abspath(cwd or b'.')]
324 324 args.extend(b'%s=%s' % (k, v) for k, v in pycompat.iteritems(environ))
325 325 data = b'\0'.join(args)
326 326 self.out.write(struct.pack(b'>cI', self.channel, len(data)))
327 327 self.out.write(data)
328 328 self.out.flush()
329 329
330 330 if type == b'system':
331 331 length = self.in_.read(4)
332 332 (length,) = struct.unpack(b'>I', length)
333 333 if length != 4:
334 334 raise error.Abort(_(b'invalid response'))
335 335 (rc,) = struct.unpack(b'>i', self.in_.read(4))
336 336 return rc
337 337 elif type == b'pager':
338 338 while True:
339 339 cmd = self.in_.readline()[:-1]
340 340 if not cmd:
341 341 break
342 342 if cmdtable and cmd in cmdtable:
343 343 cmdtable[cmd]()
344 344 else:
345 345 raise error.Abort(_(b'unexpected command: %s') % cmd)
346 346 else:
347 347 raise error.ProgrammingError(b'invalid S channel type: %s' % type)
348 348
349 349
350 350 _iochannels = [
351 351 # server.ch, ui.fp, mode
352 352 (b'cin', b'fin', 'rb'),
353 353 (b'cout', b'fout', 'wb'),
354 354 (b'cerr', b'ferr', 'wb'),
355 355 ]
356 356
357 357
358 358 class chgcmdserver(commandserver.server):
359 359 def __init__(
360 360 self, ui, repo, fin, fout, sock, prereposetups, hashstate, baseaddress
361 361 ):
362 362 super(chgcmdserver, self).__init__(
363 363 _newchgui(ui, channeledsystem(fin, fout, b'S'), self.attachio),
364 364 repo,
365 365 fin,
366 366 fout,
367 367 prereposetups,
368 368 )
369 369 self.clientsock = sock
370 370 self._ioattached = False
371 371 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
372 372 self.hashstate = hashstate
373 373 self.baseaddress = baseaddress
374 374 if hashstate is not None:
375 375 self.capabilities = self.capabilities.copy()
376 376 self.capabilities[b'validate'] = chgcmdserver.validate
377 377
378 378 def cleanup(self):
379 379 super(chgcmdserver, self).cleanup()
380 380 # dispatch._runcatch() does not flush outputs if exception is not
381 381 # handled by dispatch._dispatch()
382 382 self.ui.flush()
383 383 self._restoreio()
384 384 self._ioattached = False
385 385
386 386 def attachio(self):
387 387 """Attach to client's stdio passed via unix domain socket; all
388 388 channels except cresult will no longer be used
389 389 """
390 390 # tell client to sendmsg() with 1-byte payload, which makes it
391 391 # distinctive from "attachio\n" command consumed by client.read()
392 392 self.clientsock.sendall(struct.pack(b'>cI', b'I', 1))
393 393 clientfds = util.recvfds(self.clientsock.fileno())
394 394 self.ui.log(b'chgserver', b'received fds: %r\n', clientfds)
395 395
396 396 ui = self.ui
397 397 ui.flush()
398 398 self._saveio()
399 399 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
400 400 assert fd > 0
401 401 fp = getattr(ui, fn)
402 402 os.dup2(fd, fp.fileno())
403 403 os.close(fd)
404 404 if self._ioattached:
405 405 continue
406 406 # reset buffering mode when client is first attached. as we want
407 407 # to see output immediately on pager, the mode stays unchanged
408 408 # when client re-attached. ferr is unchanged because it should
409 409 # be unbuffered no matter if it is a tty or not.
410 410 if fn == b'ferr':
411 411 newfp = fp
412 412 elif pycompat.ispy3:
413 413 # On Python 3, the standard library doesn't offer line-buffered
414 414 # binary streams, so wrap/unwrap it.
415 415 if fp.isatty():
416 416 newfp = procutil.make_line_buffered(fp)
417 417 else:
418 418 newfp = procutil.unwrap_line_buffered(fp)
419 419 else:
420 420 # Python 2 uses the I/O streams provided by the C library, so
421 421 # make it line-buffered explicitly. Otherwise the default would
422 422 # be decided on first write(), where fout could be a pager.
423 423 if fp.isatty():
424 424 bufsize = 1 # line buffered
425 425 else:
426 426 bufsize = -1 # system default
427 427 newfp = os.fdopen(fp.fileno(), mode, bufsize)
428 428 if newfp is not fp:
429 429 setattr(ui, fn, newfp)
430 430 setattr(self, cn, newfp)
431 431
432 432 self._ioattached = True
433 433 self.cresult.write(struct.pack(b'>i', len(clientfds)))
434 434
435 435 def _saveio(self):
436 436 if self._oldios:
437 437 return
438 438 ui = self.ui
439 439 for cn, fn, _mode in _iochannels:
440 440 ch = getattr(self, cn)
441 441 fp = getattr(ui, fn)
442 442 fd = os.dup(fp.fileno())
443 443 self._oldios.append((ch, fp, fd))
444 444
445 445 def _restoreio(self):
446 446 if not self._oldios:
447 447 return
448 448 nullfd = os.open(os.devnull, os.O_WRONLY)
449 449 ui = self.ui
450 450 for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels):
451 451 newfp = getattr(ui, fn)
452 452 # On Python 2, newfp and fp may be separate file objects associated
453 453 # with the same fd, so we must close newfp while it's associated
454 454 # with the client. Otherwise the new associated fd would be closed
455 455 # when newfp gets deleted. On Python 3, newfp is just a wrapper
456 456 # around fp even if newfp is not fp, so deleting newfp is safe.
457 457 if not (pycompat.ispy3 or newfp is fp):
458 458 newfp.close()
459 459 # restore original fd: fp is open again
460 460 try:
461 461 if (pycompat.ispy3 or newfp is fp) and 'w' in mode:
462 462 # Discard buffered data which couldn't be flushed because
463 463 # of EPIPE. The data should belong to the current session
464 464 # and should never persist.
465 465 os.dup2(nullfd, fp.fileno())
466 466 fp.flush()
467 467 os.dup2(fd, fp.fileno())
468 468 except OSError as err:
469 469 # According to issue6330, running chg on heavy loaded systems
470 470 # can lead to EBUSY. [man dup2] indicates that, on Linux,
471 471 # EBUSY comes from a race condition between open() and dup2().
472 472 # However it's not clear why open() race occurred for
473 473 # newfd=stdin/out/err.
474 474 self.ui.log(
475 475 b'chgserver',
476 476 b'got %s while duplicating %s\n',
477 477 stringutil.forcebytestr(err),
478 478 fn,
479 479 )
480 480 os.close(fd)
481 481 setattr(self, cn, ch)
482 482 setattr(ui, fn, fp)
483 483 os.close(nullfd)
484 484 del self._oldios[:]
485 485
486 486 def validate(self):
487 487 """Reload the config and check if the server is up to date
488 488
489 489 Read a list of '\0' separated arguments.
490 490 Write a non-empty list of '\0' separated instruction strings or '\0'
491 491 if the list is empty.
492 492 An instruction string could be either:
493 493 - "unlink $path", the client should unlink the path to stop the
494 494 outdated server.
495 495 - "redirect $path", the client should attempt to connect to $path
496 496 first. If it does not work, start a new server. It implies
497 497 "reconnect".
498 498 - "exit $n", the client should exit directly with code n.
499 499 This may happen if we cannot parse the config.
500 500 - "reconnect", the client should close the connection and
501 501 reconnect.
502 502 If neither "reconnect" nor "redirect" is included in the instruction
503 503 list, the client can continue with this server after completing all
504 504 the instructions.
505 505 """
506 506 args = self._readlist()
507 507 errorraised = False
508 508 detailed_exit_code = 255
509 509 try:
510 510 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
511 511 except error.RepoError as inst:
512 512 # RepoError can be raised while trying to read shared source
513 513 # configuration
514 514 self.ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
515 515 if inst.hint:
516 516 self.ui.error(_(b"(%s)\n") % inst.hint)
517 517 errorraised = True
518 518 except error.Abort as inst:
519 519 if isinstance(inst, error.InputError):
520 520 detailed_exit_code = 10
521 elif isinstance(inst, error.ConfigError):
522 detailed_exit_code = 30
521 523 self.ui.error(inst.format())
522 524 errorraised = True
523 525
524 526 if errorraised:
525 527 self.ui.flush()
526 528 exit_code = 255
527 529 if self.ui.configbool(b'ui', b'detailed-exit-code'):
528 530 exit_code = detailed_exit_code
529 531 self.cresult.write(b'exit %d' % exit_code)
530 532 return
531 533 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
532 534 insts = []
533 535 if newhash.mtimehash != self.hashstate.mtimehash:
534 536 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
535 537 insts.append(b'unlink %s' % addr)
536 538 # mtimehash is empty if one or more extensions fail to load.
537 539 # to be compatible with hg, still serve the client this time.
538 540 if self.hashstate.mtimehash:
539 541 insts.append(b'reconnect')
540 542 if newhash.confighash != self.hashstate.confighash:
541 543 addr = _hashaddress(self.baseaddress, newhash.confighash)
542 544 insts.append(b'redirect %s' % addr)
543 545 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
544 546 self.cresult.write(b'\0'.join(insts) or b'\0')
545 547
546 548 def chdir(self):
547 549 """Change current directory
548 550
549 551 Note that the behavior of --cwd option is bit different from this.
550 552 It does not affect --config parameter.
551 553 """
552 554 path = self._readstr()
553 555 if not path:
554 556 return
555 557 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
556 558 os.chdir(path)
557 559
558 560 def setumask(self):
559 561 """Change umask (DEPRECATED)"""
560 562 # BUG: this does not follow the message frame structure, but kept for
561 563 # backward compatibility with old chg clients for some time
562 564 self._setumask(self._read(4))
563 565
564 566 def setumask2(self):
565 567 """Change umask"""
566 568 data = self._readstr()
567 569 if len(data) != 4:
568 570 raise ValueError(b'invalid mask length in setumask2 request')
569 571 self._setumask(data)
570 572
571 573 def _setumask(self, data):
572 574 mask = struct.unpack(b'>I', data)[0]
573 575 self.ui.log(b'chgserver', b'setumask %r\n', mask)
574 576 util.setumask(mask)
575 577
576 578 def runcommand(self):
577 579 # pager may be attached within the runcommand session, which should
578 580 # be detached at the end of the session. otherwise the pager wouldn't
579 581 # receive EOF.
580 582 globaloldios = self._oldios
581 583 self._oldios = []
582 584 try:
583 585 return super(chgcmdserver, self).runcommand()
584 586 finally:
585 587 self._restoreio()
586 588 self._oldios = globaloldios
587 589
588 590 def setenv(self):
589 591 """Clear and update os.environ
590 592
591 593 Note that not all variables can make an effect on the running process.
592 594 """
593 595 l = self._readlist()
594 596 try:
595 597 newenv = dict(s.split(b'=', 1) for s in l)
596 598 except ValueError:
597 599 raise ValueError(b'unexpected value in setenv request')
598 600 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
599 601
600 602 encoding.environ.clear()
601 603 encoding.environ.update(newenv)
602 604
603 605 capabilities = commandserver.server.capabilities.copy()
604 606 capabilities.update(
605 607 {
606 608 b'attachio': attachio,
607 609 b'chdir': chdir,
608 610 b'runcommand': runcommand,
609 611 b'setenv': setenv,
610 612 b'setumask': setumask,
611 613 b'setumask2': setumask2,
612 614 }
613 615 )
614 616
615 617 if util.safehasattr(procutil, b'setprocname'):
616 618
617 619 def setprocname(self):
618 620 """Change process title"""
619 621 name = self._readstr()
620 622 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
621 623 procutil.setprocname(name)
622 624
623 625 capabilities[b'setprocname'] = setprocname
624 626
625 627
626 628 def _tempaddress(address):
627 629 return b'%s.%d.tmp' % (address, os.getpid())
628 630
629 631
630 632 def _hashaddress(address, hashstr):
631 633 # if the basename of address contains '.', use only the left part. this
632 634 # makes it possible for the client to pass 'server.tmp$PID' and follow by
633 635 # an atomic rename to avoid locking when spawning new servers.
634 636 dirname, basename = os.path.split(address)
635 637 basename = basename.split(b'.', 1)[0]
636 638 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
637 639
638 640
639 641 class chgunixservicehandler(object):
640 642 """Set of operations for chg services"""
641 643
642 644 pollinterval = 1 # [sec]
643 645
644 646 def __init__(self, ui):
645 647 self.ui = ui
646 648 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
647 649 self._lastactive = time.time()
648 650
649 651 def bindsocket(self, sock, address):
650 652 self._inithashstate(address)
651 653 self._checkextensions()
652 654 self._bind(sock)
653 655 self._createsymlink()
654 656 # no "listening at" message should be printed to simulate hg behavior
655 657
656 658 def _inithashstate(self, address):
657 659 self._baseaddress = address
658 660 if self.ui.configbool(b'chgserver', b'skiphash'):
659 661 self._hashstate = None
660 662 self._realaddress = address
661 663 return
662 664 self._hashstate = hashstate.fromui(self.ui)
663 665 self._realaddress = _hashaddress(address, self._hashstate.confighash)
664 666
665 667 def _checkextensions(self):
666 668 if not self._hashstate:
667 669 return
668 670 if extensions.notloaded():
669 671 # one or more extensions failed to load. mtimehash becomes
670 672 # meaningless because we do not know the paths of those extensions.
671 673 # set mtimehash to an illegal hash value to invalidate the server.
672 674 self._hashstate.mtimehash = b''
673 675
674 676 def _bind(self, sock):
675 677 # use a unique temp address so we can stat the file and do ownership
676 678 # check later
677 679 tempaddress = _tempaddress(self._realaddress)
678 680 util.bindunixsocket(sock, tempaddress)
679 681 self._socketstat = os.stat(tempaddress)
680 682 sock.listen(socket.SOMAXCONN)
681 683 # rename will replace the old socket file if exists atomically. the
682 684 # old server will detect ownership change and exit.
683 685 util.rename(tempaddress, self._realaddress)
684 686
685 687 def _createsymlink(self):
686 688 if self._baseaddress == self._realaddress:
687 689 return
688 690 tempaddress = _tempaddress(self._baseaddress)
689 691 os.symlink(os.path.basename(self._realaddress), tempaddress)
690 692 util.rename(tempaddress, self._baseaddress)
691 693
692 694 def _issocketowner(self):
693 695 try:
694 696 st = os.stat(self._realaddress)
695 697 return (
696 698 st.st_ino == self._socketstat.st_ino
697 699 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
698 700 )
699 701 except OSError:
700 702 return False
701 703
702 704 def unlinksocket(self, address):
703 705 if not self._issocketowner():
704 706 return
705 707 # it is possible to have a race condition here that we may
706 708 # remove another server's socket file. but that's okay
707 709 # since that server will detect and exit automatically and
708 710 # the client will start a new server on demand.
709 711 util.tryunlink(self._realaddress)
710 712
711 713 def shouldexit(self):
712 714 if not self._issocketowner():
713 715 self.ui.log(
714 716 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
715 717 )
716 718 return True
717 719 if time.time() - self._lastactive > self._idletimeout:
718 720 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
719 721 return True
720 722 return False
721 723
722 724 def newconnection(self):
723 725 self._lastactive = time.time()
724 726
725 727 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
726 728 return chgcmdserver(
727 729 self.ui,
728 730 repo,
729 731 fin,
730 732 fout,
731 733 conn,
732 734 prereposetups,
733 735 self._hashstate,
734 736 self._baseaddress,
735 737 )
736 738
737 739
738 740 def chgunixservice(ui, repo, opts):
739 741 # CHGINTERNALMARK is set by chg client. It is an indication of things are
740 742 # started by chg so other code can do things accordingly, like disabling
741 743 # demandimport or detecting chg client started by chg client. When executed
742 744 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
743 745 # environ cleaner.
744 746 if b'CHGINTERNALMARK' in encoding.environ:
745 747 del encoding.environ[b'CHGINTERNALMARK']
746 748 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
747 749 # it thinks the current value is "C". This breaks the hash computation and
748 750 # causes chg to restart loop.
749 751 if b'CHGORIG_LC_CTYPE' in encoding.environ:
750 752 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
751 753 del encoding.environ[b'CHGORIG_LC_CTYPE']
752 754 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
753 755 if b'LC_CTYPE' in encoding.environ:
754 756 del encoding.environ[b'LC_CTYPE']
755 757 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
756 758
757 759 if repo:
758 760 # one chgserver can serve multiple repos. drop repo information
759 761 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
760 762 h = chgunixservicehandler(ui)
761 763 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,469 +1,469 b''
1 1 #require chg
2 2
3 3 $ mkdir log
4 4 $ cp $HGRCPATH $HGRCPATH.unconfigured
5 5 $ cat <<'EOF' >> $HGRCPATH
6 6 > [cmdserver]
7 7 > log = $TESTTMP/log/server.log
8 8 > max-log-files = 1
9 9 > max-log-size = 10 kB
10 10 > EOF
11 11 $ cp $HGRCPATH $HGRCPATH.orig
12 12
13 13 $ filterlog () {
14 14 > sed -e 's!^[0-9/]* [0-9:]* ([0-9]*)>!YYYY/MM/DD HH:MM:SS (PID)>!' \
15 15 > -e 's!\(setprocname\|received fds\|setenv\): .*!\1: ...!' \
16 16 > -e 's!\(confighash\|mtimehash\) = [0-9a-f]*!\1 = ...!g' \
17 17 > -e 's!\(in \)[0-9.]*s\b!\1 ...s!g' \
18 18 > -e 's!\(pid\)=[0-9]*!\1=...!g' \
19 19 > -e 's!\(/server-\)[0-9a-f]*!\1...!g'
20 20 > }
21 21
22 22 init repo
23 23
24 24 $ chg init foo
25 25 $ cd foo
26 26
27 27 ill-formed config
28 28
29 29 $ chg status
30 30 $ echo '=brokenconfig' >> $HGRCPATH
31 31 $ chg status
32 32 config error at * =brokenconfig (glob)
33 [255]
33 [30]
34 34
35 35 $ cp $HGRCPATH.orig $HGRCPATH
36 36
37 37 long socket path
38 38
39 39 $ sockpath=$TESTTMP/this/path/should/be/longer/than/one-hundred-and-seven/characters/where/107/is/the/typical/size/limit/of/unix-domain-socket
40 40 $ mkdir -p $sockpath
41 41 $ bakchgsockname=$CHGSOCKNAME
42 42 $ CHGSOCKNAME=$sockpath/server
43 43 $ export CHGSOCKNAME
44 44 $ chg root
45 45 $TESTTMP/foo
46 46 $ rm -rf $sockpath
47 47 $ CHGSOCKNAME=$bakchgsockname
48 48 $ export CHGSOCKNAME
49 49
50 50 $ cd ..
51 51
52 52 editor
53 53 ------
54 54
55 55 $ cat >> pushbuffer.py <<EOF
56 56 > def reposetup(ui, repo):
57 57 > repo.ui.pushbuffer(subproc=True)
58 58 > EOF
59 59
60 60 $ chg init editor
61 61 $ cd editor
62 62
63 63 by default, system() should be redirected to the client:
64 64
65 65 $ touch foo
66 66 $ CHGDEBUG= HGEDITOR=cat chg ci -Am channeled --edit 2>&1 \
67 67 > | egrep "HG:|run 'cat"
68 68 chg: debug: * run 'cat "*"' at '$TESTTMP/editor' (glob)
69 69 HG: Enter commit message. Lines beginning with 'HG:' are removed.
70 70 HG: Leave message empty to abort commit.
71 71 HG: --
72 72 HG: user: test
73 73 HG: branch 'default'
74 74 HG: added foo
75 75
76 76 but no redirection should be made if output is captured:
77 77
78 78 $ touch bar
79 79 $ CHGDEBUG= HGEDITOR=cat chg ci -Am bufferred --edit \
80 80 > --config extensions.pushbuffer="$TESTTMP/pushbuffer.py" 2>&1 \
81 81 > | egrep "HG:|run 'cat"
82 82 [1]
83 83
84 84 check that commit commands succeeded:
85 85
86 86 $ hg log -T '{rev}:{desc}\n'
87 87 1:bufferred
88 88 0:channeled
89 89
90 90 $ cd ..
91 91
92 92 pager
93 93 -----
94 94
95 95 $ cat >> fakepager.py <<EOF
96 96 > import sys
97 97 > for line in sys.stdin:
98 98 > sys.stdout.write('paged! %r\n' % line)
99 99 > EOF
100 100
101 101 enable pager extension globally, but spawns the master server with no tty:
102 102
103 103 $ chg init pager
104 104 $ cd pager
105 105 $ cat >> $HGRCPATH <<EOF
106 106 > [extensions]
107 107 > pager =
108 108 > [pager]
109 109 > pager = "$PYTHON" $TESTTMP/fakepager.py
110 110 > EOF
111 111 $ chg version > /dev/null
112 112 $ touch foo
113 113 $ chg ci -qAm foo
114 114
115 115 pager should be enabled if the attached client has a tty:
116 116
117 117 $ chg log -l1 -q --config ui.formatted=True
118 118 paged! '0:1f7b0de80e11\n'
119 119 $ chg log -l1 -q --config ui.formatted=False
120 120 0:1f7b0de80e11
121 121
122 122 chg waits for pager if runcommand raises
123 123
124 124 $ cat > $TESTTMP/crash.py <<EOF
125 125 > from mercurial import registrar
126 126 > cmdtable = {}
127 127 > command = registrar.command(cmdtable)
128 128 > @command(b'crash')
129 129 > def pagercrash(ui, repo, *pats, **opts):
130 130 > ui.write(b'going to crash\n')
131 131 > raise Exception('.')
132 132 > EOF
133 133
134 134 $ cat > $TESTTMP/fakepager.py <<EOF
135 135 > from __future__ import absolute_import
136 136 > import sys
137 137 > import time
138 138 > for line in iter(sys.stdin.readline, ''):
139 139 > if 'crash' in line: # only interested in lines containing 'crash'
140 140 > # if chg exits when pager is sleeping (incorrectly), the output
141 141 > # will be captured by the next test case
142 142 > time.sleep(1)
143 143 > sys.stdout.write('crash-pager: %s' % line)
144 144 > EOF
145 145
146 146 $ cat >> .hg/hgrc <<EOF
147 147 > [extensions]
148 148 > crash = $TESTTMP/crash.py
149 149 > EOF
150 150
151 151 $ chg crash --pager=on --config ui.formatted=True 2>/dev/null
152 152 crash-pager: going to crash
153 153 [255]
154 154
155 155 no stdout data should be printed after pager quits, and the buffered data
156 156 should never persist (issue6207)
157 157
158 158 "killed!" may be printed if terminated by SIGPIPE, which isn't important
159 159 in this test.
160 160
161 161 $ cat > $TESTTMP/bulkwrite.py <<'EOF'
162 162 > import time
163 163 > from mercurial import error, registrar
164 164 > cmdtable = {}
165 165 > command = registrar.command(cmdtable)
166 166 > @command(b'bulkwrite')
167 167 > def bulkwrite(ui, repo, *pats, **opts):
168 168 > ui.write(b'going to write massive data\n')
169 169 > ui.flush()
170 170 > t = time.time()
171 171 > while time.time() - t < 2:
172 172 > ui.write(b'x' * 1023 + b'\n') # will be interrupted by SIGPIPE
173 173 > raise error.Abort(b"write() doesn't block")
174 174 > EOF
175 175
176 176 $ cat > $TESTTMP/fakepager.py <<'EOF'
177 177 > import sys
178 178 > import time
179 179 > sys.stdout.write('paged! %r\n' % sys.stdin.readline())
180 180 > time.sleep(1) # new data will be written
181 181 > EOF
182 182
183 183 $ cat >> .hg/hgrc <<EOF
184 184 > [extensions]
185 185 > bulkwrite = $TESTTMP/bulkwrite.py
186 186 > EOF
187 187
188 188 $ chg bulkwrite --pager=on --color no --config ui.formatted=True
189 189 paged! 'going to write massive data\n'
190 190 killed! (?)
191 191 [255]
192 192
193 193 $ chg bulkwrite --pager=on --color no --config ui.formatted=True
194 194 paged! 'going to write massive data\n'
195 195 killed! (?)
196 196 [255]
197 197
198 198 $ cd ..
199 199
200 200 missing stdio
201 201 -------------
202 202
203 203 $ CHGDEBUG=1 chg version -q 0<&-
204 204 chg: debug: * stdio fds are missing (glob)
205 205 chg: debug: * execute original hg (glob)
206 206 Mercurial Distributed SCM * (glob)
207 207
208 208 server lifecycle
209 209 ----------------
210 210
211 211 chg server should be restarted on code change, and old server will shut down
212 212 automatically. In this test, we use the following time parameters:
213 213
214 214 - "sleep 1" to make mtime different
215 215 - "sleep 2" to notice mtime change (polling interval is 1 sec)
216 216
217 217 set up repository with an extension:
218 218
219 219 $ chg init extreload
220 220 $ cd extreload
221 221 $ touch dummyext.py
222 222 $ cat <<EOF >> .hg/hgrc
223 223 > [extensions]
224 224 > dummyext = dummyext.py
225 225 > EOF
226 226
227 227 isolate socket directory for stable result:
228 228
229 229 $ OLDCHGSOCKNAME=$CHGSOCKNAME
230 230 $ mkdir chgsock
231 231 $ CHGSOCKNAME=`pwd`/chgsock/server
232 232
233 233 warm up server:
234 234
235 235 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
236 236 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
237 237
238 238 new server should be started if extension modified:
239 239
240 240 $ sleep 1
241 241 $ touch dummyext.py
242 242 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
243 243 chg: debug: * instruction: unlink $TESTTMP/extreload/chgsock/server-* (glob)
244 244 chg: debug: * instruction: reconnect (glob)
245 245 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
246 246
247 247 old server will shut down, while new server should still be reachable:
248 248
249 249 $ sleep 2
250 250 $ CHGDEBUG= chg log 2>&1 | (egrep 'instruction|start' || true)
251 251
252 252 socket file should never be unlinked by old server:
253 253 (simulates unowned socket by updating mtime, which makes sure server exits
254 254 at polling cycle)
255 255
256 256 $ ls chgsock/server-*
257 257 chgsock/server-* (glob)
258 258 $ touch chgsock/server-*
259 259 $ sleep 2
260 260 $ ls chgsock/server-*
261 261 chgsock/server-* (glob)
262 262
263 263 since no server is reachable from socket file, new server should be started:
264 264 (this test makes sure that old server shut down automatically)
265 265
266 266 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
267 267 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
268 268
269 269 shut down servers and restore environment:
270 270
271 271 $ rm -R chgsock
272 272 $ sleep 2
273 273 $ CHGSOCKNAME=$OLDCHGSOCKNAME
274 274 $ cd ..
275 275
276 276 check that server events are recorded:
277 277
278 278 $ ls log
279 279 server.log
280 280 server.log.1
281 281
282 282 print only the last 10 lines, since we aren't sure how many records are
283 283 preserved (since setprocname isn't available on py3 and pure version,
284 284 the 10th-most-recent line is different when using py3):
285 285
286 286 $ cat log/server.log.1 log/server.log | tail -10 | filterlog
287 287 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ... (no-setprocname !)
288 288 YYYY/MM/DD HH:MM:SS (PID)> forked worker process (pid=...)
289 289 YYYY/MM/DD HH:MM:SS (PID)> setprocname: ... (setprocname !)
290 290 YYYY/MM/DD HH:MM:SS (PID)> received fds: ...
291 291 YYYY/MM/DD HH:MM:SS (PID)> chdir to '$TESTTMP/extreload'
292 292 YYYY/MM/DD HH:MM:SS (PID)> setumask 18
293 293 YYYY/MM/DD HH:MM:SS (PID)> setenv: ...
294 294 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
295 295 YYYY/MM/DD HH:MM:SS (PID)> validate: []
296 296 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
297 297 YYYY/MM/DD HH:MM:SS (PID)> $TESTTMP/extreload/chgsock/server-... is not owned, exiting.
298 298
299 299 global data mutated by schems
300 300 -----------------------------
301 301
302 302 $ hg init schemes
303 303 $ cd schemes
304 304
305 305 initial state
306 306
307 307 $ cat > .hg/hgrc <<'EOF'
308 308 > [extensions]
309 309 > schemes =
310 310 > [schemes]
311 311 > foo = https://foo.example.org/
312 312 > EOF
313 313 $ hg debugexpandscheme foo://expanded
314 314 https://foo.example.org/expanded
315 315 $ hg debugexpandscheme bar://unexpanded
316 316 bar://unexpanded
317 317
318 318 add bar
319 319
320 320 $ cat > .hg/hgrc <<'EOF'
321 321 > [extensions]
322 322 > schemes =
323 323 > [schemes]
324 324 > foo = https://foo.example.org/
325 325 > bar = https://bar.example.org/
326 326 > EOF
327 327 $ hg debugexpandscheme foo://expanded
328 328 https://foo.example.org/expanded
329 329 $ hg debugexpandscheme bar://expanded
330 330 https://bar.example.org/expanded
331 331
332 332 remove foo
333 333
334 334 $ cat > .hg/hgrc <<'EOF'
335 335 > [extensions]
336 336 > schemes =
337 337 > [schemes]
338 338 > bar = https://bar.example.org/
339 339 > EOF
340 340 $ hg debugexpandscheme foo://unexpanded
341 341 foo://unexpanded
342 342 $ hg debugexpandscheme bar://expanded
343 343 https://bar.example.org/expanded
344 344
345 345 $ cd ..
346 346
347 347 repository cache
348 348 ----------------
349 349
350 350 $ rm log/server.log*
351 351 $ cp $HGRCPATH.unconfigured $HGRCPATH
352 352 $ cat <<'EOF' >> $HGRCPATH
353 353 > [cmdserver]
354 354 > log = $TESTTMP/log/server.log
355 355 > max-repo-cache = 1
356 356 > track-log = command, repocache
357 357 > EOF
358 358
359 359 isolate socket directory for stable result:
360 360
361 361 $ OLDCHGSOCKNAME=$CHGSOCKNAME
362 362 $ mkdir chgsock
363 363 $ CHGSOCKNAME=`pwd`/chgsock/server
364 364
365 365 create empty repo and cache it:
366 366
367 367 $ hg init cached
368 368 $ hg id -R cached
369 369 000000000000 tip
370 370 $ sleep 1
371 371
372 372 modify repo (and cache will be invalidated):
373 373
374 374 $ touch cached/a
375 375 $ hg ci -R cached -Am 'add a'
376 376 adding a
377 377 $ sleep 1
378 378
379 379 read cached repo:
380 380
381 381 $ hg log -R cached
382 382 changeset: 0:ac82d8b1f7c4
383 383 tag: tip
384 384 user: test
385 385 date: Thu Jan 01 00:00:00 1970 +0000
386 386 summary: add a
387 387
388 388 $ sleep 1
389 389
390 390 discard cached from LRU cache:
391 391
392 392 $ hg clone cached cached2
393 393 updating to branch default
394 394 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
395 395 $ hg id -R cached2
396 396 ac82d8b1f7c4 tip
397 397 $ sleep 1
398 398
399 399 read uncached repo:
400 400
401 401 $ hg log -R cached
402 402 changeset: 0:ac82d8b1f7c4
403 403 tag: tip
404 404 user: test
405 405 date: Thu Jan 01 00:00:00 1970 +0000
406 406 summary: add a
407 407
408 408 $ sleep 1
409 409
410 410 shut down servers and restore environment:
411 411
412 412 $ rm -R chgsock
413 413 $ sleep 2
414 414 $ CHGSOCKNAME=$OLDCHGSOCKNAME
415 415
416 416 check server log:
417 417
418 418 $ cat log/server.log | filterlog
419 419 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
420 420 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...) (?)
421 421 YYYY/MM/DD HH:MM:SS (PID)> init cached
422 422 YYYY/MM/DD HH:MM:SS (PID)> id -R cached
423 423 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
424 424 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
425 425 YYYY/MM/DD HH:MM:SS (PID)> ci -R cached -Am 'add a'
426 426 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
427 427 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
428 428 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
429 429 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
430 430 YYYY/MM/DD HH:MM:SS (PID)> clone cached cached2
431 431 YYYY/MM/DD HH:MM:SS (PID)> id -R cached2
432 432 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached2 (in ...s)
433 433 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
434 434 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
435 435
436 436 Test that chg works (sets to the user's actual LC_CTYPE) even when python
437 437 "coerces" the locale (py3.7+)
438 438
439 439 $ cat > $TESTTMP/debugenv.py <<EOF
440 440 > from mercurial import encoding
441 441 > from mercurial import registrar
442 442 > cmdtable = {}
443 443 > command = registrar.command(cmdtable)
444 444 > @command(b'debugenv', [], b'', norepo=True)
445 445 > def debugenv(ui):
446 446 > for k in [b'LC_ALL', b'LC_CTYPE', b'LANG']:
447 447 > v = encoding.environ.get(k)
448 448 > if v is not None:
449 449 > ui.write(b'%s=%s\n' % (k, encoding.environ[k]))
450 450 > EOF
451 451 (hg keeps python's modified LC_CTYPE, chg doesn't)
452 452 $ (unset LC_ALL; unset LANG; LC_CTYPE= "$CHGHG" \
453 453 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
454 454 LC_CTYPE=C.UTF-8 (py37 !)
455 455 LC_CTYPE= (no-py37 !)
456 456 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
457 457 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
458 458 LC_CTYPE=
459 459 $ (unset LC_ALL; unset LANG; LC_CTYPE=unsupported_value chg \
460 460 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
461 461 LC_CTYPE=unsupported_value
462 462 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
463 463 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
464 464 LC_CTYPE=
465 465 $ LANG= LC_ALL= LC_CTYPE= chg \
466 466 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv
467 467 LC_ALL=
468 468 LC_CTYPE=
469 469 LANG=
@@ -1,414 +1,390 b''
1 1 hide outer repo
2 2 $ hg init
3 3
4 4 Invalid syntax: no value
5 5
6 6 $ cat > .hg/hgrc << EOF
7 7 > novaluekey
8 8 > EOF
9 #if chg
10 $ hg showconfig
11 config error at $TESTTMP/.hg/hgrc:1: novaluekey
12 [255]
13 #else
14 9 $ hg showconfig
15 10 config error at $TESTTMP/.hg/hgrc:1: novaluekey
16 11 [30]
17 #endif
18 12
19 13 Invalid syntax: no key
20 14
21 15 $ cat > .hg/hgrc << EOF
22 16 > =nokeyvalue
23 17 > EOF
24 #if chg
25 $ hg showconfig
26 config error at $TESTTMP/.hg/hgrc:1: =nokeyvalue
27 [255]
28 #else
29 18 $ hg showconfig
30 19 config error at $TESTTMP/.hg/hgrc:1: =nokeyvalue
31 20 [30]
32 #endif
33 21
34 22 Test hint about invalid syntax from leading white space
35 23
36 24 $ cat > .hg/hgrc << EOF
37 25 > key=value
38 26 > EOF
39 #if chg
40 $ hg showconfig
41 config error at $TESTTMP/.hg/hgrc:1: unexpected leading whitespace: key=value
42 [255]
43 #else
44 27 $ hg showconfig
45 28 config error at $TESTTMP/.hg/hgrc:1: unexpected leading whitespace: key=value
46 29 [30]
47 #endif
48 30
49 31 $ cat > .hg/hgrc << EOF
50 32 > [section]
51 33 > key=value
52 34 > EOF
53 #if chg
54 $ hg showconfig
55 config error at $TESTTMP/.hg/hgrc:1: unexpected leading whitespace: [section]
56 [255]
57 #else
58 35 $ hg showconfig
59 36 config error at $TESTTMP/.hg/hgrc:1: unexpected leading whitespace: [section]
60 37 [30]
61 #endif
62 38
63 39 Reset hgrc
64 40
65 41 $ echo > .hg/hgrc
66 42
67 43 Test case sensitive configuration
68 44
69 45 $ cat <<EOF >> $HGRCPATH
70 46 > [Section]
71 47 > KeY = Case Sensitive
72 48 > key = lower case
73 49 > EOF
74 50
75 51 $ hg showconfig Section
76 52 Section.KeY=Case Sensitive
77 53 Section.key=lower case
78 54
79 55 $ hg showconfig Section -Tjson
80 56 [
81 57 {
82 58 "defaultvalue": null,
83 59 "name": "Section.KeY",
84 60 "source": "*.hgrc:*", (glob)
85 61 "value": "Case Sensitive"
86 62 },
87 63 {
88 64 "defaultvalue": null,
89 65 "name": "Section.key",
90 66 "source": "*.hgrc:*", (glob)
91 67 "value": "lower case"
92 68 }
93 69 ]
94 70 $ hg showconfig Section.KeY -Tjson
95 71 [
96 72 {
97 73 "defaultvalue": null,
98 74 "name": "Section.KeY",
99 75 "source": "*.hgrc:*", (glob)
100 76 "value": "Case Sensitive"
101 77 }
102 78 ]
103 79 $ hg showconfig -Tjson | tail -7
104 80 {
105 81 "defaultvalue": null,
106 82 "name": "*", (glob)
107 83 "source": "*", (glob)
108 84 "value": "*" (glob)
109 85 }
110 86 ]
111 87
112 88 Test config default of various types:
113 89
114 90 {"defaultvalue": ""} for -T'json(defaultvalue)' looks weird, but that's
115 91 how the templater works. Unknown keywords are evaluated to "".
116 92
117 93 dynamicdefault
118 94
119 95 $ hg config --config alias.foo= alias -Tjson
120 96 [
121 97 {
122 98 "name": "alias.foo",
123 99 "source": "--config",
124 100 "value": ""
125 101 }
126 102 ]
127 103 $ hg config --config alias.foo= alias -T'json(defaultvalue)'
128 104 [
129 105 {"defaultvalue": ""}
130 106 ]
131 107 $ hg config --config alias.foo= alias -T'{defaultvalue}\n'
132 108
133 109
134 110 null
135 111
136 112 $ hg config --config auth.cookiefile= auth -Tjson
137 113 [
138 114 {
139 115 "defaultvalue": null,
140 116 "name": "auth.cookiefile",
141 117 "source": "--config",
142 118 "value": ""
143 119 }
144 120 ]
145 121 $ hg config --config auth.cookiefile= auth -T'json(defaultvalue)'
146 122 [
147 123 {"defaultvalue": null}
148 124 ]
149 125 $ hg config --config auth.cookiefile= auth -T'{defaultvalue}\n'
150 126
151 127
152 128 false
153 129
154 130 $ hg config --config commands.commit.post-status= commands -Tjson
155 131 [
156 132 {
157 133 "defaultvalue": false,
158 134 "name": "commands.commit.post-status",
159 135 "source": "--config",
160 136 "value": ""
161 137 }
162 138 ]
163 139 $ hg config --config commands.commit.post-status= commands -T'json(defaultvalue)'
164 140 [
165 141 {"defaultvalue": false}
166 142 ]
167 143 $ hg config --config commands.commit.post-status= commands -T'{defaultvalue}\n'
168 144 False
169 145
170 146 true
171 147
172 148 $ hg config --config format.dotencode= format -Tjson
173 149 [
174 150 {
175 151 "defaultvalue": true,
176 152 "name": "format.dotencode",
177 153 "source": "--config",
178 154 "value": ""
179 155 }
180 156 ]
181 157 $ hg config --config format.dotencode= format -T'json(defaultvalue)'
182 158 [
183 159 {"defaultvalue": true}
184 160 ]
185 161 $ hg config --config format.dotencode= format -T'{defaultvalue}\n'
186 162 True
187 163
188 164 bytes
189 165
190 166 $ hg config --config commands.resolve.mark-check= commands -Tjson
191 167 [
192 168 {
193 169 "defaultvalue": "none",
194 170 "name": "commands.resolve.mark-check",
195 171 "source": "--config",
196 172 "value": ""
197 173 }
198 174 ]
199 175 $ hg config --config commands.resolve.mark-check= commands -T'json(defaultvalue)'
200 176 [
201 177 {"defaultvalue": "none"}
202 178 ]
203 179 $ hg config --config commands.resolve.mark-check= commands -T'{defaultvalue}\n'
204 180 none
205 181
206 182 empty list
207 183
208 184 $ hg config --config commands.show.aliasprefix= commands -Tjson
209 185 [
210 186 {
211 187 "defaultvalue": [],
212 188 "name": "commands.show.aliasprefix",
213 189 "source": "--config",
214 190 "value": ""
215 191 }
216 192 ]
217 193 $ hg config --config commands.show.aliasprefix= commands -T'json(defaultvalue)'
218 194 [
219 195 {"defaultvalue": []}
220 196 ]
221 197 $ hg config --config commands.show.aliasprefix= commands -T'{defaultvalue}\n'
222 198
223 199
224 200 nonempty list
225 201
226 202 $ hg config --config progress.format= progress -Tjson
227 203 [
228 204 {
229 205 "defaultvalue": ["topic", "bar", "number", "estimate"],
230 206 "name": "progress.format",
231 207 "source": "--config",
232 208 "value": ""
233 209 }
234 210 ]
235 211 $ hg config --config progress.format= progress -T'json(defaultvalue)'
236 212 [
237 213 {"defaultvalue": ["topic", "bar", "number", "estimate"]}
238 214 ]
239 215 $ hg config --config progress.format= progress -T'{defaultvalue}\n'
240 216 topic bar number estimate
241 217
242 218 int
243 219
244 220 $ hg config --config profiling.freq= profiling -Tjson
245 221 [
246 222 {
247 223 "defaultvalue": 1000,
248 224 "name": "profiling.freq",
249 225 "source": "--config",
250 226 "value": ""
251 227 }
252 228 ]
253 229 $ hg config --config profiling.freq= profiling -T'json(defaultvalue)'
254 230 [
255 231 {"defaultvalue": 1000}
256 232 ]
257 233 $ hg config --config profiling.freq= profiling -T'{defaultvalue}\n'
258 234 1000
259 235
260 236 float
261 237
262 238 $ hg config --config profiling.showmax= profiling -Tjson
263 239 [
264 240 {
265 241 "defaultvalue": 0.999,
266 242 "name": "profiling.showmax",
267 243 "source": "--config",
268 244 "value": ""
269 245 }
270 246 ]
271 247 $ hg config --config profiling.showmax= profiling -T'json(defaultvalue)'
272 248 [
273 249 {"defaultvalue": 0.999}
274 250 ]
275 251 $ hg config --config profiling.showmax= profiling -T'{defaultvalue}\n'
276 252 0.999
277 253
278 254 Test empty config source:
279 255
280 256 $ cat <<EOF > emptysource.py
281 257 > def reposetup(ui, repo):
282 258 > ui.setconfig(b'empty', b'source', b'value')
283 259 > EOF
284 260 $ cp .hg/hgrc .hg/hgrc.orig
285 261 $ cat <<EOF >> .hg/hgrc
286 262 > [extensions]
287 263 > emptysource = `pwd`/emptysource.py
288 264 > EOF
289 265
290 266 $ hg config --debug empty.source
291 267 read config from: * (glob)
292 268 none: value
293 269 $ hg config empty.source -Tjson
294 270 [
295 271 {
296 272 "defaultvalue": null,
297 273 "name": "empty.source",
298 274 "source": "",
299 275 "value": "value"
300 276 }
301 277 ]
302 278
303 279 $ cp .hg/hgrc.orig .hg/hgrc
304 280
305 281 Test "%unset"
306 282
307 283 $ cat >> $HGRCPATH <<EOF
308 284 > [unsettest]
309 285 > local-hgrcpath = should be unset (HGRCPATH)
310 286 > %unset local-hgrcpath
311 287 >
312 288 > global = should be unset (HGRCPATH)
313 289 >
314 290 > both = should be unset (HGRCPATH)
315 291 >
316 292 > set-after-unset = should be unset (HGRCPATH)
317 293 > EOF
318 294
319 295 $ cat >> .hg/hgrc <<EOF
320 296 > [unsettest]
321 297 > local-hgrc = should be unset (.hg/hgrc)
322 298 > %unset local-hgrc
323 299 >
324 300 > %unset global
325 301 >
326 302 > both = should be unset (.hg/hgrc)
327 303 > %unset both
328 304 >
329 305 > set-after-unset = should be unset (.hg/hgrc)
330 306 > %unset set-after-unset
331 307 > set-after-unset = should be set (.hg/hgrc)
332 308 > EOF
333 309
334 310 $ hg showconfig unsettest
335 311 unsettest.set-after-unset=should be set (.hg/hgrc)
336 312
337 313 Test exit code when no config matches
338 314
339 315 $ hg config Section.idontexist
340 316 [1]
341 317
342 318 sub-options in [paths] aren't expanded
343 319
344 320 $ cat > .hg/hgrc << EOF
345 321 > [paths]
346 322 > foo = ~/foo
347 323 > foo:suboption = ~/foo
348 324 > EOF
349 325
350 326 $ hg showconfig paths
351 327 paths.foo:suboption=~/foo
352 328 paths.foo=$TESTTMP/foo
353 329
354 330 edit failure
355 331
356 332 $ HGEDITOR=false hg config --edit
357 333 abort: edit failed: false exited with status 1
358 334 [10]
359 335
360 336 config affected by environment variables
361 337
362 338 $ EDITOR=e1 VISUAL=e2 hg config --debug | grep 'ui\.editor'
363 339 $VISUAL: ui.editor=e2
364 340
365 341 $ VISUAL=e2 hg config --debug --config ui.editor=e3 | grep 'ui\.editor'
366 342 --config: ui.editor=e3
367 343
368 344 $ PAGER=p1 hg config --debug | grep 'pager\.pager'
369 345 $PAGER: pager.pager=p1
370 346
371 347 $ PAGER=p1 hg config --debug --config pager.pager=p2 | grep 'pager\.pager'
372 348 --config: pager.pager=p2
373 349
374 350 verify that aliases are evaluated as well
375 351
376 352 $ hg init aliastest
377 353 $ cd aliastest
378 354 $ cat > .hg/hgrc << EOF
379 355 > [ui]
380 356 > user = repo user
381 357 > EOF
382 358 $ touch index
383 359 $ unset HGUSER
384 360 $ hg ci -Am test
385 361 adding index
386 362 $ hg log --template '{author}\n'
387 363 repo user
388 364 $ cd ..
389 365
390 366 alias has lower priority
391 367
392 368 $ hg init aliaspriority
393 369 $ cd aliaspriority
394 370 $ cat > .hg/hgrc << EOF
395 371 > [ui]
396 372 > user = alias user
397 373 > username = repo user
398 374 > EOF
399 375 $ touch index
400 376 $ unset HGUSER
401 377 $ hg ci -Am test
402 378 adding index
403 379 $ hg log --template '{author}\n'
404 380 repo user
405 381 $ cd ..
406 382
407 383 configs should be read in lexicographical order
408 384
409 385 $ mkdir configs
410 386 $ for i in `$TESTDIR/seq.py 10 99`; do
411 387 > printf "[section]\nkey=$i" > configs/$i.rc
412 388 > done
413 389 $ HGRCPATH=configs hg config section.key
414 390 99
General Comments 0
You need to be logged in to leave comments. Login now