##// END OF EJS Templates
chgserver: backout changeset dfb19aed409e (per discussion)...
Yuya Nishihara -
r30645:a3f335d1 default
parent child Browse files
Show More
@@ -1,645 +1,644 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 'getpager' command
20 20 checks if pager is enabled and which pager should be executed
21 21
22 22 'setenv' command
23 23 replace os.environ completely
24 24
25 25 'setumask' command
26 26 set umask
27 27
28 28 'validate' command
29 29 reload the config and check if the server is up to date
30 30
31 31 Config
32 32 ------
33 33
34 34 ::
35 35
36 36 [chgserver]
37 37 idletimeout = 3600 # seconds, after which an idle server will exit
38 38 skiphash = False # whether to skip config or env change checks
39 39 """
40 40
41 41 from __future__ import absolute_import
42 42
43 43 import errno
44 44 import hashlib
45 45 import inspect
46 46 import os
47 47 import re
48 48 import signal
49 49 import struct
50 50 import sys
51 51 import time
52 52
53 53 from .i18n import _
54 54
55 55 from . import (
56 56 cmdutil,
57 57 commandserver,
58 58 encoding,
59 59 error,
60 60 extensions,
61 61 osutil,
62 62 util,
63 63 )
64 64
65 65 _log = commandserver.log
66 66
67 67 def _hashlist(items):
68 68 """return sha1 hexdigest for a list"""
69 69 return hashlib.sha1(str(items)).hexdigest()
70 70
71 71 # sensitive config sections affecting confighash
72 72 _configsections = [
73 73 'alias', # affects global state commands.table
74 74 'extdiff', # uisetup will register new commands
75 75 'extensions',
76 76 ]
77 77
78 78 # sensitive environment variables affecting confighash
79 79 _envre = re.compile(r'''\A(?:
80 80 CHGHG
81 81 |HG(?:[A-Z].*)?
82 82 |LANG(?:UAGE)?
83 83 |LC_.*
84 84 |LD_.*
85 85 |PATH
86 86 |PYTHON.*
87 87 |TERM(?:INFO)?
88 88 |TZ
89 89 )\Z''', re.X)
90 90
91 91 def _confighash(ui):
92 92 """return a quick hash for detecting config/env changes
93 93
94 94 confighash is the hash of sensitive config items and environment variables.
95 95
96 96 for chgserver, it is designed that once confighash changes, the server is
97 97 not qualified to serve its client and should redirect the client to a new
98 98 server. different from mtimehash, confighash change will not mark the
99 99 server outdated and exit since the user can have different configs at the
100 100 same time.
101 101 """
102 102 sectionitems = []
103 103 for section in _configsections:
104 104 sectionitems.append(ui.configitems(section))
105 105 sectionhash = _hashlist(sectionitems)
106 106 envitems = [(k, v) for k, v in encoding.environ.iteritems()
107 107 if _envre.match(k)]
108 108 envhash = _hashlist(sorted(envitems))
109 109 return sectionhash[:6] + envhash[:6]
110 110
111 111 def _getmtimepaths(ui):
112 112 """get a list of paths that should be checked to detect change
113 113
114 114 The list will include:
115 115 - extensions (will not cover all files for complex extensions)
116 116 - mercurial/__version__.py
117 117 - python binary
118 118 """
119 119 modules = [m for n, m in extensions.extensions(ui)]
120 120 try:
121 121 from . import __version__
122 122 modules.append(__version__)
123 123 except ImportError:
124 124 pass
125 125 files = [sys.executable]
126 126 for m in modules:
127 127 try:
128 128 files.append(inspect.getabsfile(m))
129 129 except TypeError:
130 130 pass
131 131 return sorted(set(files))
132 132
133 133 def _mtimehash(paths):
134 134 """return a quick hash for detecting file changes
135 135
136 136 mtimehash calls stat on given paths and calculate a hash based on size and
137 137 mtime of each file. mtimehash does not read file content because reading is
138 138 expensive. therefore it's not 100% reliable for detecting content changes.
139 139 it's possible to return different hashes for same file contents.
140 140 it's also possible to return a same hash for different file contents for
141 141 some carefully crafted situation.
142 142
143 143 for chgserver, it is designed that once mtimehash changes, the server is
144 144 considered outdated immediately and should no longer provide service.
145 145
146 146 mtimehash is not included in confighash because we only know the paths of
147 147 extensions after importing them (there is imp.find_module but that faces
148 148 race conditions). We need to calculate confighash without importing.
149 149 """
150 150 def trystat(path):
151 151 try:
152 152 st = os.stat(path)
153 153 return (st.st_mtime, st.st_size)
154 154 except OSError:
155 155 # could be ENOENT, EPERM etc. not fatal in any case
156 156 pass
157 157 return _hashlist(map(trystat, paths))[:12]
158 158
159 159 class hashstate(object):
160 160 """a structure storing confighash, mtimehash, paths used for mtimehash"""
161 161 def __init__(self, confighash, mtimehash, mtimepaths):
162 162 self.confighash = confighash
163 163 self.mtimehash = mtimehash
164 164 self.mtimepaths = mtimepaths
165 165
166 166 @staticmethod
167 167 def fromui(ui, mtimepaths=None):
168 168 if mtimepaths is None:
169 169 mtimepaths = _getmtimepaths(ui)
170 170 confighash = _confighash(ui)
171 171 mtimehash = _mtimehash(mtimepaths)
172 172 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
173 173 return hashstate(confighash, mtimehash, mtimepaths)
174 174
175 175 # copied from hgext/pager.py:uisetup()
176 176 def _setuppagercmd(ui, options, cmd):
177 177 from . import commands # avoid cycle
178 178
179 179 if not ui.formatted():
180 180 return
181 181
182 182 p = ui.config("pager", "pager", encoding.environ.get("PAGER"))
183 183 usepager = False
184 184 always = util.parsebool(options['pager'])
185 185 auto = options['pager'] == 'auto'
186 186
187 187 if not p:
188 188 pass
189 189 elif always:
190 190 usepager = True
191 191 elif not auto:
192 192 usepager = False
193 193 else:
194 194 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
195 195 attend = ui.configlist('pager', 'attend', attended)
196 196 ignore = ui.configlist('pager', 'ignore')
197 197 cmds, _ = cmdutil.findcmd(cmd, commands.table)
198 198
199 199 for cmd in cmds:
200 200 var = 'attend-%s' % cmd
201 201 if ui.config('pager', var):
202 202 usepager = ui.configbool('pager', var)
203 203 break
204 204 if (cmd in attend or
205 205 (cmd not in ignore and not attend)):
206 206 usepager = True
207 207 break
208 208
209 209 if usepager:
210 210 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
211 211 ui.setconfig('ui', 'interactive', False, 'pager')
212 212 return p
213 213
214 214 def _newchgui(srcui, csystem):
215 215 class chgui(srcui.__class__):
216 216 def __init__(self, src=None):
217 217 super(chgui, self).__init__(src)
218 218 if src:
219 219 self._csystem = getattr(src, '_csystem', csystem)
220 220 else:
221 221 self._csystem = csystem
222 222
223 223 def system(self, cmd, environ=None, cwd=None, onerr=None,
224 224 errprefix=None):
225 225 # fallback to the original system method if the output needs to be
226 226 # captured (to self._buffers), or the output stream is not stdout
227 227 # (e.g. stderr, cStringIO), because the chg client is not aware of
228 228 # these situations and will behave differently (write to stdout).
229 229 if (any(s[1] for s in self._bufferstates)
230 230 or not util.safehasattr(self.fout, 'fileno')
231 231 or self.fout.fileno() != util.stdout.fileno()):
232 232 return super(chgui, self).system(cmd, environ, cwd, onerr,
233 233 errprefix)
234 234 # copied from mercurial/util.py:system()
235 235 self.flush()
236 236 def py2shell(val):
237 237 if val is None or val is False:
238 238 return '0'
239 239 if val is True:
240 240 return '1'
241 241 return str(val)
242 242 env = encoding.environ.copy()
243 243 if environ:
244 244 env.update((k, py2shell(v)) for k, v in environ.iteritems())
245 245 env['HG'] = util.hgexecutable()
246 246 rc = self._csystem(cmd, env, cwd)
247 247 if rc and onerr:
248 248 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
249 249 util.explainexit(rc)[0])
250 250 if errprefix:
251 251 errmsg = '%s: %s' % (errprefix, errmsg)
252 252 raise onerr(errmsg)
253 253 return rc
254 254
255 255 return chgui(srcui)
256 256
257 257 def _loadnewui(srcui, args):
258 258 from . import dispatch # avoid cycle
259 259
260 260 newui = srcui.__class__.load()
261 261 for a in ['fin', 'fout', 'ferr', 'environ']:
262 262 setattr(newui, a, getattr(srcui, a))
263 263 if util.safehasattr(srcui, '_csystem'):
264 264 newui._csystem = srcui._csystem
265 265
266 266 # command line args
267 267 args = args[:]
268 268 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
269 269
270 270 # stolen from tortoisehg.util.copydynamicconfig()
271 271 for section, name, value in srcui.walkconfig():
272 272 source = srcui.configsource(section, name)
273 273 if ':' in source or source == '--config':
274 274 # path:line or command line
275 275 continue
276 276 newui.setconfig(section, name, value, source)
277 277
278 278 # load wd and repo config, copied from dispatch.py
279 279 cwds = dispatch._earlygetopt(['--cwd'], args)
280 280 cwd = cwds and os.path.realpath(cwds[-1]) or None
281 281 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
282 282 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
283 283
284 284 return (newui, newlui)
285 285
286 286 class channeledsystem(object):
287 287 """Propagate ui.system() request in the following format:
288 288
289 289 payload length (unsigned int),
290 290 cmd, '\0',
291 291 cwd, '\0',
292 292 envkey, '=', val, '\0',
293 293 ...
294 294 envkey, '=', val
295 295
296 296 and waits:
297 297
298 298 exitcode length (unsigned int),
299 299 exitcode (int)
300 300 """
301 301 def __init__(self, in_, out, channel):
302 302 self.in_ = in_
303 303 self.out = out
304 304 self.channel = channel
305 305
306 306 def __call__(self, cmd, environ, cwd):
307 307 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
308 308 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
309 309 data = '\0'.join(args)
310 310 self.out.write(struct.pack('>cI', self.channel, len(data)))
311 311 self.out.write(data)
312 312 self.out.flush()
313 313
314 314 length = self.in_.read(4)
315 315 length, = struct.unpack('>I', length)
316 316 if length != 4:
317 317 raise error.Abort(_('invalid response'))
318 318 rc, = struct.unpack('>i', self.in_.read(4))
319 319 return rc
320 320
321 321 _iochannels = [
322 322 # server.ch, ui.fp, mode
323 323 ('cin', 'fin', 'rb'),
324 324 ('cout', 'fout', 'wb'),
325 325 ('cerr', 'ferr', 'wb'),
326 326 ]
327 327
328 328 class chgcmdserver(commandserver.server):
329 329 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
330 self._csystem = channeledsystem(fin, fout, 'S')
331 330 super(chgcmdserver, self).__init__(
332 _newchgui(ui, self._csystem), repo, fin, fout)
331 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
333 332 self.clientsock = sock
334 333 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
335 334 self.hashstate = hashstate
336 335 self.baseaddress = baseaddress
337 336 if hashstate is not None:
338 337 self.capabilities = self.capabilities.copy()
339 338 self.capabilities['validate'] = chgcmdserver.validate
340 339
341 340 def cleanup(self):
342 341 super(chgcmdserver, self).cleanup()
343 342 # dispatch._runcatch() does not flush outputs if exception is not
344 343 # handled by dispatch._dispatch()
345 344 self.ui.flush()
346 345 self._restoreio()
347 346
348 347 def attachio(self):
349 348 """Attach to client's stdio passed via unix domain socket; all
350 349 channels except cresult will no longer be used
351 350 """
352 351 # tell client to sendmsg() with 1-byte payload, which makes it
353 352 # distinctive from "attachio\n" command consumed by client.read()
354 353 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
355 354 clientfds = osutil.recvfds(self.clientsock.fileno())
356 355 _log('received fds: %r\n' % clientfds)
357 356
358 357 ui = self.ui
359 358 ui.flush()
360 359 first = self._saveio()
361 360 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
362 361 assert fd > 0
363 362 fp = getattr(ui, fn)
364 363 os.dup2(fd, fp.fileno())
365 364 os.close(fd)
366 365 if not first:
367 366 continue
368 367 # reset buffering mode when client is first attached. as we want
369 368 # to see output immediately on pager, the mode stays unchanged
370 369 # when client re-attached. ferr is unchanged because it should
371 370 # be unbuffered no matter if it is a tty or not.
372 371 if fn == 'ferr':
373 372 newfp = fp
374 373 else:
375 374 # make it line buffered explicitly because the default is
376 375 # decided on first write(), where fout could be a pager.
377 376 if fp.isatty():
378 377 bufsize = 1 # line buffered
379 378 else:
380 379 bufsize = -1 # system default
381 380 newfp = os.fdopen(fp.fileno(), mode, bufsize)
382 381 setattr(ui, fn, newfp)
383 382 setattr(self, cn, newfp)
384 383
385 384 self.cresult.write(struct.pack('>i', len(clientfds)))
386 385
387 386 def _saveio(self):
388 387 if self._oldios:
389 388 return False
390 389 ui = self.ui
391 390 for cn, fn, _mode in _iochannels:
392 391 ch = getattr(self, cn)
393 392 fp = getattr(ui, fn)
394 393 fd = os.dup(fp.fileno())
395 394 self._oldios.append((ch, fp, fd))
396 395 return True
397 396
398 397 def _restoreio(self):
399 398 ui = self.ui
400 399 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
401 400 newfp = getattr(ui, fn)
402 401 # close newfp while it's associated with client; otherwise it
403 402 # would be closed when newfp is deleted
404 403 if newfp is not fp:
405 404 newfp.close()
406 405 # restore original fd: fp is open again
407 406 os.dup2(fd, fp.fileno())
408 407 os.close(fd)
409 408 setattr(self, cn, ch)
410 409 setattr(ui, fn, fp)
411 410 del self._oldios[:]
412 411
413 412 def validate(self):
414 413 """Reload the config and check if the server is up to date
415 414
416 415 Read a list of '\0' separated arguments.
417 416 Write a non-empty list of '\0' separated instruction strings or '\0'
418 417 if the list is empty.
419 418 An instruction string could be either:
420 419 - "unlink $path", the client should unlink the path to stop the
421 420 outdated server.
422 421 - "redirect $path", the client should attempt to connect to $path
423 422 first. If it does not work, start a new server. It implies
424 423 "reconnect".
425 424 - "exit $n", the client should exit directly with code n.
426 425 This may happen if we cannot parse the config.
427 426 - "reconnect", the client should close the connection and
428 427 reconnect.
429 428 If neither "reconnect" nor "redirect" is included in the instruction
430 429 list, the client can continue with this server after completing all
431 430 the instructions.
432 431 """
433 432 from . import dispatch # avoid cycle
434 433
435 434 args = self._readlist()
436 435 try:
437 436 self.ui, lui = _loadnewui(self.ui, args)
438 437 except error.ParseError as inst:
439 438 dispatch._formatparse(self.ui.warn, inst)
440 439 self.ui.flush()
441 440 self.cresult.write('exit 255')
442 441 return
443 442 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
444 443 insts = []
445 444 if newhash.mtimehash != self.hashstate.mtimehash:
446 445 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
447 446 insts.append('unlink %s' % addr)
448 447 # mtimehash is empty if one or more extensions fail to load.
449 448 # to be compatible with hg, still serve the client this time.
450 449 if self.hashstate.mtimehash:
451 450 insts.append('reconnect')
452 451 if newhash.confighash != self.hashstate.confighash:
453 452 addr = _hashaddress(self.baseaddress, newhash.confighash)
454 453 insts.append('redirect %s' % addr)
455 454 _log('validate: %s\n' % insts)
456 455 self.cresult.write('\0'.join(insts) or '\0')
457 456
458 457 def chdir(self):
459 458 """Change current directory
460 459
461 460 Note that the behavior of --cwd option is bit different from this.
462 461 It does not affect --config parameter.
463 462 """
464 463 path = self._readstr()
465 464 if not path:
466 465 return
467 466 _log('chdir to %r\n' % path)
468 467 os.chdir(path)
469 468
470 469 def setumask(self):
471 470 """Change umask"""
472 471 mask = struct.unpack('>I', self._read(4))[0]
473 472 _log('setumask %r\n' % mask)
474 473 os.umask(mask)
475 474
476 475 def getpager(self):
477 476 """Read cmdargs and write pager command to r-channel if enabled
478 477
479 478 If pager isn't enabled, this writes '\0' because channeledoutput
480 479 does not allow to write empty data.
481 480 """
482 481 from . import dispatch # avoid cycle
483 482
484 483 args = self._readlist()
485 484 try:
486 485 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
487 486 args)
488 487 except (error.Abort, error.AmbiguousCommand, error.CommandError,
489 488 error.UnknownCommand):
490 489 cmd = None
491 490 options = {}
492 491 if not cmd or 'pager' not in options:
493 492 self.cresult.write('\0')
494 493 return
495 494
496 495 pagercmd = _setuppagercmd(self.ui, options, cmd)
497 496 if pagercmd:
498 497 # Python's SIGPIPE is SIG_IGN by default. change to SIG_DFL so
499 498 # we can exit if the pipe to the pager is closed
500 499 if util.safehasattr(signal, 'SIGPIPE') and \
501 500 signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
502 501 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
503 502 self.cresult.write(pagercmd)
504 503 else:
505 504 self.cresult.write('\0')
506 505
507 506 def runcommand(self):
508 507 return super(chgcmdserver, self).runcommand()
509 508
510 509 def setenv(self):
511 510 """Clear and update os.environ
512 511
513 512 Note that not all variables can make an effect on the running process.
514 513 """
515 514 l = self._readlist()
516 515 try:
517 516 newenv = dict(s.split('=', 1) for s in l)
518 517 except ValueError:
519 518 raise ValueError('unexpected value in setenv request')
520 519 _log('setenv: %r\n' % sorted(newenv.keys()))
521 520 encoding.environ.clear()
522 521 encoding.environ.update(newenv)
523 522
524 523 capabilities = commandserver.server.capabilities.copy()
525 524 capabilities.update({'attachio': attachio,
526 525 'chdir': chdir,
527 526 'getpager': getpager,
528 527 'runcommand': runcommand,
529 528 'setenv': setenv,
530 529 'setumask': setumask})
531 530
532 531 def _tempaddress(address):
533 532 return '%s.%d.tmp' % (address, os.getpid())
534 533
535 534 def _hashaddress(address, hashstr):
536 535 # if the basename of address contains '.', use only the left part. this
537 536 # makes it possible for the client to pass 'server.tmp$PID' and follow by
538 537 # an atomic rename to avoid locking when spawning new servers.
539 538 dirname, basename = os.path.split(address)
540 539 basename = basename.split('.', 1)[0]
541 540 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
542 541
543 542 class chgunixservicehandler(object):
544 543 """Set of operations for chg services"""
545 544
546 545 pollinterval = 1 # [sec]
547 546
548 547 def __init__(self, ui):
549 548 self.ui = ui
550 549 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
551 550 self._lastactive = time.time()
552 551
553 552 def bindsocket(self, sock, address):
554 553 self._inithashstate(address)
555 554 self._checkextensions()
556 555 self._bind(sock)
557 556 self._createsymlink()
558 557
559 558 def _inithashstate(self, address):
560 559 self._baseaddress = address
561 560 if self.ui.configbool('chgserver', 'skiphash', False):
562 561 self._hashstate = None
563 562 self._realaddress = address
564 563 return
565 564 self._hashstate = hashstate.fromui(self.ui)
566 565 self._realaddress = _hashaddress(address, self._hashstate.confighash)
567 566
568 567 def _checkextensions(self):
569 568 if not self._hashstate:
570 569 return
571 570 if extensions.notloaded():
572 571 # one or more extensions failed to load. mtimehash becomes
573 572 # meaningless because we do not know the paths of those extensions.
574 573 # set mtimehash to an illegal hash value to invalidate the server.
575 574 self._hashstate.mtimehash = ''
576 575
577 576 def _bind(self, sock):
578 577 # use a unique temp address so we can stat the file and do ownership
579 578 # check later
580 579 tempaddress = _tempaddress(self._realaddress)
581 580 util.bindunixsocket(sock, tempaddress)
582 581 self._socketstat = os.stat(tempaddress)
583 582 # rename will replace the old socket file if exists atomically. the
584 583 # old server will detect ownership change and exit.
585 584 util.rename(tempaddress, self._realaddress)
586 585
587 586 def _createsymlink(self):
588 587 if self._baseaddress == self._realaddress:
589 588 return
590 589 tempaddress = _tempaddress(self._baseaddress)
591 590 os.symlink(os.path.basename(self._realaddress), tempaddress)
592 591 util.rename(tempaddress, self._baseaddress)
593 592
594 593 def _issocketowner(self):
595 594 try:
596 595 stat = os.stat(self._realaddress)
597 596 return (stat.st_ino == self._socketstat.st_ino and
598 597 stat.st_mtime == self._socketstat.st_mtime)
599 598 except OSError:
600 599 return False
601 600
602 601 def unlinksocket(self, address):
603 602 if not self._issocketowner():
604 603 return
605 604 # it is possible to have a race condition here that we may
606 605 # remove another server's socket file. but that's okay
607 606 # since that server will detect and exit automatically and
608 607 # the client will start a new server on demand.
609 608 try:
610 609 os.unlink(self._realaddress)
611 610 except OSError as exc:
612 611 if exc.errno != errno.ENOENT:
613 612 raise
614 613
615 614 def printbanner(self, address):
616 615 # no "listening at" message should be printed to simulate hg behavior
617 616 pass
618 617
619 618 def shouldexit(self):
620 619 if not self._issocketowner():
621 620 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
622 621 return True
623 622 if time.time() - self._lastactive > self._idletimeout:
624 623 self.ui.debug('being idle too long. exiting.\n')
625 624 return True
626 625 return False
627 626
628 627 def newconnection(self):
629 628 self._lastactive = time.time()
630 629
631 630 def createcmdserver(self, repo, conn, fin, fout):
632 631 return chgcmdserver(self.ui, repo, fin, fout, conn,
633 632 self._hashstate, self._baseaddress)
634 633
635 634 def chgunixservice(ui, repo, opts):
636 635 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
637 636 # start another chg. drop it to avoid possible side effects.
638 637 if 'CHGINTERNALMARK' in encoding.environ:
639 638 del encoding.environ['CHGINTERNALMARK']
640 639
641 640 if repo:
642 641 # one chgserver can serve multiple repos. drop repo information
643 642 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
644 643 h = chgunixservicehandler(ui)
645 644 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
General Comments 0
You need to be logged in to leave comments. Login now