##// END OF EJS Templates
dispatch: protect against malicious 'hg serve --stdio' invocations (sec)...
Augie Fackler -
r32050:77eaf953 4.1.3 stable
parent child Browse files
Show More
@@ -1,86 +1,87 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de>
4 4 #
5 5 # Author(s):
6 6 # Thomas Arendsen Hein <thomas@intevation.de>
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 9 # GNU General Public License version 2 or any later version.
10 10
11 11 """
12 12 hg-ssh - a wrapper for ssh access to a limited set of mercurial repos
13 13
14 14 To be used in ~/.ssh/authorized_keys with the "command" option, see sshd(8):
15 15 command="hg-ssh path/to/repo1 /path/to/repo2 ~/repo3 ~user/repo4" ssh-dss ...
16 16 (probably together with these other useful options:
17 17 no-port-forwarding,no-X11-forwarding,no-agent-forwarding)
18 18
19 19 This allows pull/push over ssh from/to the repositories given as arguments.
20 20
21 21 If all your repositories are subdirectories of a common directory, you can
22 22 allow shorter paths with:
23 23 command="cd path/to/my/repositories && hg-ssh repo1 subdir/repo2"
24 24
25 25 You can use pattern matching of your normal shell, e.g.:
26 26 command="cd repos && hg-ssh user/thomas/* projects/{mercurial,foo}"
27 27
28 28 You can also add a --read-only flag to allow read-only access to a key, e.g.:
29 29 command="hg-ssh --read-only repos/*"
30 30 """
31 31
32 32 # enable importing on demand to reduce startup time
33 33 from mercurial import demandimport; demandimport.enable()
34 34
35 from mercurial import dispatch
35 from mercurial import dispatch, ui as uimod
36 36
37 37 import sys, os, shlex
38 38
39 39 def main():
40 40 cwd = os.getcwd()
41 41 readonly = False
42 42 args = sys.argv[1:]
43 43 while len(args):
44 44 if args[0] == '--read-only':
45 45 readonly = True
46 46 args.pop(0)
47 47 else:
48 48 break
49 49 allowed_paths = [os.path.normpath(os.path.join(cwd,
50 50 os.path.expanduser(path)))
51 51 for path in args]
52 52 orig_cmd = os.getenv('SSH_ORIGINAL_COMMAND', '?')
53 53 try:
54 54 cmdargv = shlex.split(orig_cmd)
55 55 except ValueError as e:
56 56 sys.stderr.write('Illegal command "%s": %s\n' % (orig_cmd, e))
57 57 sys.exit(255)
58 58
59 59 if cmdargv[:2] == ['hg', '-R'] and cmdargv[3:] == ['serve', '--stdio']:
60 60 path = cmdargv[2]
61 61 repo = os.path.normpath(os.path.join(cwd, os.path.expanduser(path)))
62 62 if repo in allowed_paths:
63 63 cmd = ['-R', repo, 'serve', '--stdio']
64 req = dispatch.request(cmd)
64 65 if readonly:
65 cmd += [
66 '--config',
67 'hooks.pretxnopen.hg-ssh=python:__main__.rejectpush',
68 '--config',
69 'hooks.prepushkey.hg-ssh=python:__main__.rejectpush'
70 ]
71 dispatch.dispatch(dispatch.request(cmd))
66 if not req.ui:
67 req.ui = uimod.ui.load()
68 req.ui.setconfig('hooks', 'pretxnopen.hg-ssh',
69 'python:__main__.rejectpush', 'hg-ssh')
70 req.ui.setconfig('hooks', 'prepushkey.hg-ssh',
71 'python:__main__.rejectpush', 'hg-ssh')
72 dispatch.dispatch(req)
72 73 else:
73 74 sys.stderr.write('Illegal repository "%s"\n' % repo)
74 75 sys.exit(255)
75 76 else:
76 77 sys.stderr.write('Illegal command "%s"\n' % orig_cmd)
77 78 sys.exit(255)
78 79
79 80 def rejectpush(ui, **kwargs):
80 81 ui.warn(("Permission denied\n"))
81 82 # mercurial hooks use unix process conventions for hook return values
82 83 # so a truthy return means failure
83 84 return True
84 85
85 86 if __name__ == '__main__':
86 87 main()
@@ -1,888 +1,919 b''
1 1 # dispatch.py - command dispatching for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import, print_function
9 9
10 10 import atexit
11 11 import difflib
12 12 import errno
13 13 import getopt
14 14 import os
15 15 import pdb
16 16 import re
17 17 import signal
18 18 import sys
19 19 import time
20 20 import traceback
21 21
22 22
23 23 from .i18n import _
24 24
25 25 from . import (
26 26 cmdutil,
27 27 color,
28 28 commands,
29 29 debugcommands,
30 30 demandimport,
31 31 encoding,
32 32 error,
33 33 extensions,
34 34 fancyopts,
35 35 fileset,
36 36 hg,
37 37 hook,
38 38 profiling,
39 39 pycompat,
40 40 revset,
41 41 scmutil,
42 42 templatefilters,
43 43 templatekw,
44 44 templater,
45 45 ui as uimod,
46 46 util,
47 47 )
48 48
49 49 class request(object):
50 50 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
51 51 ferr=None):
52 52 self.args = args
53 53 self.ui = ui
54 54 self.repo = repo
55 55
56 56 # input/output/error streams
57 57 self.fin = fin
58 58 self.fout = fout
59 59 self.ferr = ferr
60 60
61 61 def run():
62 62 "run the command in sys.argv"
63 63 sys.exit((dispatch(request(pycompat.sysargv[1:])) or 0) & 255)
64 64
65 65 def _getsimilar(symbols, value):
66 66 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
67 67 # The cutoff for similarity here is pretty arbitrary. It should
68 68 # probably be investigated and tweaked.
69 69 return [s for s in symbols if sim(s) > 0.6]
70 70
71 71 def _reportsimilar(write, similar):
72 72 if len(similar) == 1:
73 73 write(_("(did you mean %s?)\n") % similar[0])
74 74 elif similar:
75 75 ss = ", ".join(sorted(similar))
76 76 write(_("(did you mean one of %s?)\n") % ss)
77 77
78 78 def _formatparse(write, inst):
79 79 similar = []
80 80 if isinstance(inst, error.UnknownIdentifier):
81 81 # make sure to check fileset first, as revset can invoke fileset
82 82 similar = _getsimilar(inst.symbols, inst.function)
83 83 if len(inst.args) > 1:
84 84 write(_("hg: parse error at %s: %s\n") %
85 85 (inst.args[1], inst.args[0]))
86 86 if (inst.args[0][0] == ' '):
87 87 write(_("unexpected leading whitespace\n"))
88 88 else:
89 89 write(_("hg: parse error: %s\n") % inst.args[0])
90 90 _reportsimilar(write, similar)
91 91 if inst.hint:
92 92 write(_("(%s)\n") % inst.hint)
93 93
94 94 def dispatch(req):
95 95 "run the command specified in req.args"
96 96 if req.ferr:
97 97 ferr = req.ferr
98 98 elif req.ui:
99 99 ferr = req.ui.ferr
100 100 else:
101 101 ferr = util.stderr
102 102
103 103 try:
104 104 if not req.ui:
105 105 req.ui = uimod.ui.load()
106 106 if '--traceback' in req.args:
107 107 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
108 108
109 109 # set ui streams from the request
110 110 if req.fin:
111 111 req.ui.fin = req.fin
112 112 if req.fout:
113 113 req.ui.fout = req.fout
114 114 if req.ferr:
115 115 req.ui.ferr = req.ferr
116 116 except error.Abort as inst:
117 117 ferr.write(_("abort: %s\n") % inst)
118 118 if inst.hint:
119 119 ferr.write(_("(%s)\n") % inst.hint)
120 120 return -1
121 121 except error.ParseError as inst:
122 122 _formatparse(ferr.write, inst)
123 123 return -1
124 124
125 125 msg = ' '.join(' ' in a and repr(a) or a for a in req.args)
126 126 starttime = time.time()
127 127 ret = None
128 128 try:
129 129 ret = _runcatch(req)
130 130 except KeyboardInterrupt:
131 131 try:
132 132 req.ui.warn(_("interrupted!\n"))
133 133 except IOError as inst:
134 134 if inst.errno != errno.EPIPE:
135 135 raise
136 136 ret = -1
137 137 finally:
138 138 duration = time.time() - starttime
139 139 req.ui.flush()
140 140 req.ui.log("commandfinish", "%s exited %s after %0.2f seconds\n",
141 141 msg, ret or 0, duration)
142 142 return ret
143 143
144 144 def _runcatch(req):
145 145 def catchterm(*args):
146 146 raise error.SignalInterrupt
147 147
148 148 ui = req.ui
149 149 try:
150 150 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
151 151 num = getattr(signal, name, None)
152 152 if num:
153 153 signal.signal(num, catchterm)
154 154 except ValueError:
155 155 pass # happens if called in a thread
156 156
157 157 def _runcatchfunc():
158 realcmd = None
159 try:
160 cmdargs = fancyopts.fancyopts(req.args[:], commands.globalopts, {})
161 cmd = cmdargs[0]
162 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
163 realcmd = aliases[0]
164 except (error.UnknownCommand, error.AmbiguousCommand,
165 IndexError, getopt.GetoptError):
166 # Don't handle this here. We know the command is
167 # invalid, but all we're worried about for now is that
168 # it's not a command that server operators expect to
169 # be safe to offer to users in a sandbox.
170 pass
171 if realcmd == 'serve' and '--stdio' in cmdargs:
172 # We want to constrain 'hg serve --stdio' instances pretty
173 # closely, as many shared-ssh access tools want to grant
174 # access to run *only* 'hg -R $repo serve --stdio'. We
175 # restrict to exactly that set of arguments, and prohibit
176 # any repo name that starts with '--' to prevent
177 # shenanigans wherein a user does something like pass
178 # --debugger or --config=ui.debugger=1 as a repo
179 # name. This used to actually run the debugger.
180 if (len(req.args) != 4 or
181 req.args[0] != '-R' or
182 req.args[1].startswith('--') or
183 req.args[2] != 'serve' or
184 req.args[3] != '--stdio'):
185 raise error.Abort(
186 _('potentially unsafe serve --stdio invocation: %r') %
187 (req.args,))
188
158 189 try:
159 190 debugger = 'pdb'
160 191 debugtrace = {
161 192 'pdb' : pdb.set_trace
162 193 }
163 194 debugmortem = {
164 195 'pdb' : pdb.post_mortem
165 196 }
166 197
167 198 # read --config before doing anything else
168 199 # (e.g. to change trust settings for reading .hg/hgrc)
169 200 cfgs = _parseconfig(req.ui, _earlygetopt(['--config'], req.args))
170 201
171 202 if req.repo:
172 203 # copy configs that were passed on the cmdline (--config) to
173 204 # the repo ui
174 205 for sec, name, val in cfgs:
175 206 req.repo.ui.setconfig(sec, name, val, source='--config')
176 207
177 208 # developer config: ui.debugger
178 209 debugger = ui.config("ui", "debugger")
179 210 debugmod = pdb
180 211 if not debugger or ui.plain():
181 212 # if we are in HGPLAIN mode, then disable custom debugging
182 213 debugger = 'pdb'
183 214 elif '--debugger' in req.args:
184 215 # This import can be slow for fancy debuggers, so only
185 216 # do it when absolutely necessary, i.e. when actual
186 217 # debugging has been requested
187 218 with demandimport.deactivated():
188 219 try:
189 220 debugmod = __import__(debugger)
190 221 except ImportError:
191 222 pass # Leave debugmod = pdb
192 223
193 224 debugtrace[debugger] = debugmod.set_trace
194 225 debugmortem[debugger] = debugmod.post_mortem
195 226
196 227 # enter the debugger before command execution
197 228 if '--debugger' in req.args:
198 229 ui.warn(_("entering debugger - "
199 230 "type c to continue starting hg or h for help\n"))
200 231
201 232 if (debugger != 'pdb' and
202 233 debugtrace[debugger] == debugtrace['pdb']):
203 234 ui.warn(_("%s debugger specified "
204 235 "but its module was not found\n") % debugger)
205 236 with demandimport.deactivated():
206 237 debugtrace[debugger]()
207 238 try:
208 239 return _dispatch(req)
209 240 finally:
210 241 ui.flush()
211 242 except: # re-raises
212 243 # enter the debugger when we hit an exception
213 244 if '--debugger' in req.args:
214 245 traceback.print_exc()
215 246 debugmortem[debugger](sys.exc_info()[2])
216 247 ui.traceback()
217 248 raise
218 249
219 250 return callcatch(ui, _runcatchfunc)
220 251
221 252 def callcatch(ui, func):
222 253 """like scmutil.callcatch but handles more high-level exceptions about
223 254 config parsing and commands. besides, use handlecommandexception to handle
224 255 uncaught exceptions.
225 256 """
226 257 try:
227 258 return scmutil.callcatch(ui, func)
228 259 except error.AmbiguousCommand as inst:
229 260 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
230 261 (inst.args[0], " ".join(inst.args[1])))
231 262 except error.CommandError as inst:
232 263 if inst.args[0]:
233 264 ui.warn(_("hg %s: %s\n") % (inst.args[0], inst.args[1]))
234 265 commands.help_(ui, inst.args[0], full=False, command=True)
235 266 else:
236 267 ui.warn(_("hg: %s\n") % inst.args[1])
237 268 commands.help_(ui, 'shortlist')
238 269 except error.ParseError as inst:
239 270 _formatparse(ui.warn, inst)
240 271 return -1
241 272 except error.UnknownCommand as inst:
242 273 ui.warn(_("hg: unknown command '%s'\n") % inst.args[0])
243 274 try:
244 275 # check if the command is in a disabled extension
245 276 # (but don't check for extensions themselves)
246 277 commands.help_(ui, inst.args[0], unknowncmd=True)
247 278 except (error.UnknownCommand, error.Abort):
248 279 suggested = False
249 280 if len(inst.args) == 2:
250 281 sim = _getsimilar(inst.args[1], inst.args[0])
251 282 if sim:
252 283 _reportsimilar(ui.warn, sim)
253 284 suggested = True
254 285 if not suggested:
255 286 commands.help_(ui, 'shortlist')
256 287 except IOError:
257 288 raise
258 289 except KeyboardInterrupt:
259 290 raise
260 291 except: # probably re-raises
261 292 if not handlecommandexception(ui):
262 293 raise
263 294
264 295 return -1
265 296
266 297 def aliasargs(fn, givenargs):
267 298 args = getattr(fn, 'args', [])
268 299 if args:
269 300 cmd = ' '.join(map(util.shellquote, args))
270 301
271 302 nums = []
272 303 def replacer(m):
273 304 num = int(m.group(1)) - 1
274 305 nums.append(num)
275 306 if num < len(givenargs):
276 307 return givenargs[num]
277 308 raise error.Abort(_('too few arguments for command alias'))
278 309 cmd = re.sub(r'\$(\d+|\$)', replacer, cmd)
279 310 givenargs = [x for i, x in enumerate(givenargs)
280 311 if i not in nums]
281 312 args = pycompat.shlexsplit(cmd)
282 313 return args + givenargs
283 314
284 315 def aliasinterpolate(name, args, cmd):
285 316 '''interpolate args into cmd for shell aliases
286 317
287 318 This also handles $0, $@ and "$@".
288 319 '''
289 320 # util.interpolate can't deal with "$@" (with quotes) because it's only
290 321 # built to match prefix + patterns.
291 322 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
292 323 replacemap['$0'] = name
293 324 replacemap['$$'] = '$'
294 325 replacemap['$@'] = ' '.join(args)
295 326 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
296 327 # parameters, separated out into words. Emulate the same behavior here by
297 328 # quoting the arguments individually. POSIX shells will then typically
298 329 # tokenize each argument into exactly one word.
299 330 replacemap['"$@"'] = ' '.join(util.shellquote(arg) for arg in args)
300 331 # escape '\$' for regex
301 332 regex = '|'.join(replacemap.keys()).replace('$', r'\$')
302 333 r = re.compile(regex)
303 334 return r.sub(lambda x: replacemap[x.group()], cmd)
304 335
305 336 class cmdalias(object):
306 337 def __init__(self, name, definition, cmdtable, source):
307 338 self.name = self.cmd = name
308 339 self.cmdname = ''
309 340 self.definition = definition
310 341 self.fn = None
311 342 self.givenargs = []
312 343 self.opts = []
313 344 self.help = ''
314 345 self.badalias = None
315 346 self.unknowncmd = False
316 347 self.source = source
317 348
318 349 try:
319 350 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
320 351 for alias, e in cmdtable.iteritems():
321 352 if e is entry:
322 353 self.cmd = alias
323 354 break
324 355 self.shadows = True
325 356 except error.UnknownCommand:
326 357 self.shadows = False
327 358
328 359 if not self.definition:
329 360 self.badalias = _("no definition for alias '%s'") % self.name
330 361 return
331 362
332 363 if self.definition.startswith('!'):
333 364 self.shell = True
334 365 def fn(ui, *args):
335 366 env = {'HG_ARGS': ' '.join((self.name,) + args)}
336 367 def _checkvar(m):
337 368 if m.groups()[0] == '$':
338 369 return m.group()
339 370 elif int(m.groups()[0]) <= len(args):
340 371 return m.group()
341 372 else:
342 373 ui.debug("No argument found for substitution "
343 374 "of %i variable in alias '%s' definition."
344 375 % (int(m.groups()[0]), self.name))
345 376 return ''
346 377 cmd = re.sub(r'\$(\d+|\$)', _checkvar, self.definition[1:])
347 378 cmd = aliasinterpolate(self.name, args, cmd)
348 379 return ui.system(cmd, environ=env)
349 380 self.fn = fn
350 381 return
351 382
352 383 try:
353 384 args = pycompat.shlexsplit(self.definition)
354 385 except ValueError as inst:
355 386 self.badalias = (_("error in definition for alias '%s': %s")
356 387 % (self.name, inst))
357 388 return
358 389 self.cmdname = cmd = args.pop(0)
359 390 self.givenargs = args
360 391
361 392 for invalidarg in ("--cwd", "-R", "--repository", "--repo", "--config"):
362 393 if _earlygetopt([invalidarg], args):
363 394 self.badalias = (_("error in definition for alias '%s': %s may "
364 395 "only be given on the command line")
365 396 % (self.name, invalidarg))
366 397 return
367 398
368 399 try:
369 400 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
370 401 if len(tableentry) > 2:
371 402 self.fn, self.opts, self.help = tableentry
372 403 else:
373 404 self.fn, self.opts = tableentry
374 405
375 406 if self.help.startswith("hg " + cmd):
376 407 # drop prefix in old-style help lines so hg shows the alias
377 408 self.help = self.help[4 + len(cmd):]
378 409 self.__doc__ = self.fn.__doc__
379 410
380 411 except error.UnknownCommand:
381 412 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
382 413 % (self.name, cmd))
383 414 self.unknowncmd = True
384 415 except error.AmbiguousCommand:
385 416 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
386 417 % (self.name, cmd))
387 418
388 419 @property
389 420 def args(self):
390 421 args = map(util.expandpath, self.givenargs)
391 422 return aliasargs(self.fn, args)
392 423
393 424 def __getattr__(self, name):
394 425 adefaults = {'norepo': True, 'optionalrepo': False, 'inferrepo': False}
395 426 if name not in adefaults:
396 427 raise AttributeError(name)
397 428 if self.badalias or util.safehasattr(self, 'shell'):
398 429 return adefaults[name]
399 430 return getattr(self.fn, name)
400 431
401 432 def __call__(self, ui, *args, **opts):
402 433 if self.badalias:
403 434 hint = None
404 435 if self.unknowncmd:
405 436 try:
406 437 # check if the command is in a disabled extension
407 438 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
408 439 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
409 440 except error.UnknownCommand:
410 441 pass
411 442 raise error.Abort(self.badalias, hint=hint)
412 443 if self.shadows:
413 444 ui.debug("alias '%s' shadows command '%s'\n" %
414 445 (self.name, self.cmdname))
415 446
416 447 ui.log('commandalias', "alias '%s' expands to '%s'\n",
417 448 self.name, self.definition)
418 449 if util.safehasattr(self, 'shell'):
419 450 return self.fn(ui, *args, **opts)
420 451 else:
421 452 try:
422 453 return util.checksignature(self.fn)(ui, *args, **opts)
423 454 except error.SignatureError:
424 455 args = ' '.join([self.cmdname] + self.args)
425 456 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
426 457 raise
427 458
428 459 def addaliases(ui, cmdtable):
429 460 # aliases are processed after extensions have been loaded, so they
430 461 # may use extension commands. Aliases can also use other alias definitions,
431 462 # but only if they have been defined prior to the current definition.
432 463 for alias, definition in ui.configitems('alias'):
433 464 source = ui.configsource('alias', alias)
434 465 aliasdef = cmdalias(alias, definition, cmdtable, source)
435 466
436 467 try:
437 468 olddef = cmdtable[aliasdef.cmd][0]
438 469 if olddef.definition == aliasdef.definition:
439 470 continue
440 471 except (KeyError, AttributeError):
441 472 # definition might not exist or it might not be a cmdalias
442 473 pass
443 474
444 475 cmdtable[aliasdef.name] = (aliasdef, aliasdef.opts, aliasdef.help)
445 476
446 477 def _parse(ui, args):
447 478 options = {}
448 479 cmdoptions = {}
449 480
450 481 try:
451 482 args = fancyopts.fancyopts(args, commands.globalopts, options)
452 483 except getopt.GetoptError as inst:
453 484 raise error.CommandError(None, inst)
454 485
455 486 if args:
456 487 cmd, args = args[0], args[1:]
457 488 aliases, entry = cmdutil.findcmd(cmd, commands.table,
458 489 ui.configbool("ui", "strict"))
459 490 cmd = aliases[0]
460 491 args = aliasargs(entry[0], args)
461 492 defaults = ui.config("defaults", cmd)
462 493 if defaults:
463 494 args = map(util.expandpath, pycompat.shlexsplit(defaults)) + args
464 495 c = list(entry[1])
465 496 else:
466 497 cmd = None
467 498 c = []
468 499
469 500 # combine global options into local
470 501 for o in commands.globalopts:
471 502 c.append((o[0], o[1], options[o[1]], o[3]))
472 503
473 504 try:
474 505 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
475 506 except getopt.GetoptError as inst:
476 507 raise error.CommandError(cmd, inst)
477 508
478 509 # separate global options back out
479 510 for o in commands.globalopts:
480 511 n = o[1]
481 512 options[n] = cmdoptions[n]
482 513 del cmdoptions[n]
483 514
484 515 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
485 516
486 517 def _parseconfig(ui, config):
487 518 """parse the --config options from the command line"""
488 519 configs = []
489 520
490 521 for cfg in config:
491 522 try:
492 523 name, value = [cfgelem.strip()
493 524 for cfgelem in cfg.split('=', 1)]
494 525 section, name = name.split('.', 1)
495 526 if not section or not name:
496 527 raise IndexError
497 528 ui.setconfig(section, name, value, '--config')
498 529 configs.append((section, name, value))
499 530 except (IndexError, ValueError):
500 531 raise error.Abort(_('malformed --config option: %r '
501 532 '(use --config section.name=value)') % cfg)
502 533
503 534 return configs
504 535
505 536 def _earlygetopt(aliases, args):
506 537 """Return list of values for an option (or aliases).
507 538
508 539 The values are listed in the order they appear in args.
509 540 The options and values are removed from args.
510 541
511 542 >>> args = ['x', '--cwd', 'foo', 'y']
512 543 >>> _earlygetopt(['--cwd'], args), args
513 544 (['foo'], ['x', 'y'])
514 545
515 546 >>> args = ['x', '--cwd=bar', 'y']
516 547 >>> _earlygetopt(['--cwd'], args), args
517 548 (['bar'], ['x', 'y'])
518 549
519 550 >>> args = ['x', '-R', 'foo', 'y']
520 551 >>> _earlygetopt(['-R'], args), args
521 552 (['foo'], ['x', 'y'])
522 553
523 554 >>> args = ['x', '-Rbar', 'y']
524 555 >>> _earlygetopt(['-R'], args), args
525 556 (['bar'], ['x', 'y'])
526 557 """
527 558 try:
528 559 argcount = args.index("--")
529 560 except ValueError:
530 561 argcount = len(args)
531 562 shortopts = [opt for opt in aliases if len(opt) == 2]
532 563 values = []
533 564 pos = 0
534 565 while pos < argcount:
535 566 fullarg = arg = args[pos]
536 567 equals = arg.find('=')
537 568 if equals > -1:
538 569 arg = arg[:equals]
539 570 if arg in aliases:
540 571 del args[pos]
541 572 if equals > -1:
542 573 values.append(fullarg[equals + 1:])
543 574 argcount -= 1
544 575 else:
545 576 if pos + 1 >= argcount:
546 577 # ignore and let getopt report an error if there is no value
547 578 break
548 579 values.append(args.pop(pos))
549 580 argcount -= 2
550 581 elif arg[:2] in shortopts:
551 582 # short option can have no following space, e.g. hg log -Rfoo
552 583 values.append(args.pop(pos)[2:])
553 584 argcount -= 1
554 585 else:
555 586 pos += 1
556 587 return values
557 588
558 589 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
559 590 # run pre-hook, and abort if it fails
560 591 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
561 592 pats=cmdpats, opts=cmdoptions)
562 593 try:
563 594 ret = _runcommand(ui, options, cmd, d)
564 595 # run post-hook, passing command result
565 596 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
566 597 result=ret, pats=cmdpats, opts=cmdoptions)
567 598 except Exception:
568 599 # run failure hook and re-raise
569 600 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
570 601 pats=cmdpats, opts=cmdoptions)
571 602 raise
572 603 return ret
573 604
574 605 def _getlocal(ui, rpath, wd=None):
575 606 """Return (path, local ui object) for the given target path.
576 607
577 608 Takes paths in [cwd]/.hg/hgrc into account."
578 609 """
579 610 if wd is None:
580 611 try:
581 612 wd = pycompat.getcwd()
582 613 except OSError as e:
583 614 raise error.Abort(_("error getting current working directory: %s") %
584 615 e.strerror)
585 616 path = cmdutil.findrepo(wd) or ""
586 617 if not path:
587 618 lui = ui
588 619 else:
589 620 lui = ui.copy()
590 621 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
591 622
592 623 if rpath and rpath[-1]:
593 624 path = lui.expandpath(rpath[-1])
594 625 lui = ui.copy()
595 626 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
596 627
597 628 return path, lui
598 629
599 630 def _checkshellalias(lui, ui, args):
600 631 """Return the function to run the shell alias, if it is required"""
601 632 options = {}
602 633
603 634 try:
604 635 args = fancyopts.fancyopts(args, commands.globalopts, options)
605 636 except getopt.GetoptError:
606 637 return
607 638
608 639 if not args:
609 640 return
610 641
611 642 cmdtable = commands.table
612 643
613 644 cmd = args[0]
614 645 try:
615 646 strict = ui.configbool("ui", "strict")
616 647 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
617 648 except (error.AmbiguousCommand, error.UnknownCommand):
618 649 return
619 650
620 651 cmd = aliases[0]
621 652 fn = entry[0]
622 653
623 654 if cmd and util.safehasattr(fn, 'shell'):
624 655 d = lambda: fn(ui, *args[1:])
625 656 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
626 657 [], {})
627 658
628 659 _loaded = set()
629 660
630 661 # list of (objname, loadermod, loadername) tuple:
631 662 # - objname is the name of an object in extension module, from which
632 663 # extra information is loaded
633 664 # - loadermod is the module where loader is placed
634 665 # - loadername is the name of the function, which takes (ui, extensionname,
635 666 # extraobj) arguments
636 667 extraloaders = [
637 668 ('cmdtable', commands, 'loadcmdtable'),
638 669 ('colortable', color, 'loadcolortable'),
639 670 ('filesetpredicate', fileset, 'loadpredicate'),
640 671 ('revsetpredicate', revset, 'loadpredicate'),
641 672 ('templatefilter', templatefilters, 'loadfilter'),
642 673 ('templatefunc', templater, 'loadfunction'),
643 674 ('templatekeyword', templatekw, 'loadkeyword'),
644 675 ]
645 676
646 677 def _dispatch(req):
647 678 args = req.args
648 679 ui = req.ui
649 680
650 681 # check for cwd
651 682 cwd = _earlygetopt(['--cwd'], args)
652 683 if cwd:
653 684 os.chdir(cwd[-1])
654 685
655 686 rpath = _earlygetopt(["-R", "--repository", "--repo"], args)
656 687 path, lui = _getlocal(ui, rpath)
657 688
658 689 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
659 690 # reposetup. Programs like TortoiseHg will call _dispatch several
660 691 # times so we keep track of configured extensions in _loaded.
661 692 extensions.loadall(lui)
662 693 exts = [ext for ext in extensions.extensions() if ext[0] not in _loaded]
663 694 # Propagate any changes to lui.__class__ by extensions
664 695 ui.__class__ = lui.__class__
665 696
666 697 # (uisetup and extsetup are handled in extensions.loadall)
667 698
668 699 for name, module in exts:
669 700 for objname, loadermod, loadername in extraloaders:
670 701 extraobj = getattr(module, objname, None)
671 702 if extraobj is not None:
672 703 getattr(loadermod, loadername)(ui, name, extraobj)
673 704 _loaded.add(name)
674 705
675 706 # (reposetup is handled in hg.repository)
676 707
677 708 # Side-effect of accessing is debugcommands module is guaranteed to be
678 709 # imported and commands.table is populated.
679 710 debugcommands.command
680 711
681 712 addaliases(lui, commands.table)
682 713
683 714 # All aliases and commands are completely defined, now.
684 715 # Check abbreviation/ambiguity of shell alias.
685 716 shellaliasfn = _checkshellalias(lui, ui, args)
686 717 if shellaliasfn:
687 718 with profiling.maybeprofile(lui):
688 719 return shellaliasfn()
689 720
690 721 # check for fallback encoding
691 722 fallback = lui.config('ui', 'fallbackencoding')
692 723 if fallback:
693 724 encoding.fallbackencoding = fallback
694 725
695 726 fullargs = args
696 727 cmd, func, args, options, cmdoptions = _parse(lui, args)
697 728
698 729 if options["config"]:
699 730 raise error.Abort(_("option --config may not be abbreviated!"))
700 731 if options["cwd"]:
701 732 raise error.Abort(_("option --cwd may not be abbreviated!"))
702 733 if options["repository"]:
703 734 raise error.Abort(_(
704 735 "option -R has to be separated from other options (e.g. not -qR) "
705 736 "and --repository may only be abbreviated as --repo!"))
706 737
707 738 if options["encoding"]:
708 739 encoding.encoding = options["encoding"]
709 740 if options["encodingmode"]:
710 741 encoding.encodingmode = options["encodingmode"]
711 742 if options["time"]:
712 743 def get_times():
713 744 t = os.times()
714 745 if t[4] == 0.0: # Windows leaves this as zero, so use time.clock()
715 746 t = (t[0], t[1], t[2], t[3], time.clock())
716 747 return t
717 748 s = get_times()
718 749 def print_time():
719 750 t = get_times()
720 751 ui.warn(_("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
721 752 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
722 753 atexit.register(print_time)
723 754
724 755 uis = set([ui, lui])
725 756
726 757 if req.repo:
727 758 uis.add(req.repo.ui)
728 759
729 760 if options['verbose'] or options['debug'] or options['quiet']:
730 761 for opt in ('verbose', 'debug', 'quiet'):
731 762 val = str(bool(options[opt]))
732 763 for ui_ in uis:
733 764 ui_.setconfig('ui', opt, val, '--' + opt)
734 765
735 766 if options['profile']:
736 767 for ui_ in uis:
737 768 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
738 769
739 770 if options['traceback']:
740 771 for ui_ in uis:
741 772 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
742 773
743 774 if options['noninteractive']:
744 775 for ui_ in uis:
745 776 ui_.setconfig('ui', 'interactive', 'off', '-y')
746 777
747 778 if cmdoptions.get('insecure', False):
748 779 for ui_ in uis:
749 780 ui_.insecureconnections = True
750 781
751 782 if options['version']:
752 783 return commands.version_(ui)
753 784 if options['help']:
754 785 return commands.help_(ui, cmd, command=cmd is not None)
755 786 elif not cmd:
756 787 return commands.help_(ui, 'shortlist')
757 788
758 789 with profiling.maybeprofile(lui):
759 790 repo = None
760 791 cmdpats = args[:]
761 792 if not func.norepo:
762 793 # use the repo from the request only if we don't have -R
763 794 if not rpath and not cwd:
764 795 repo = req.repo
765 796
766 797 if repo:
767 798 # set the descriptors of the repo ui to those of ui
768 799 repo.ui.fin = ui.fin
769 800 repo.ui.fout = ui.fout
770 801 repo.ui.ferr = ui.ferr
771 802 else:
772 803 try:
773 804 repo = hg.repository(ui, path=path)
774 805 if not repo.local():
775 806 raise error.Abort(_("repository '%s' is not local")
776 807 % path)
777 808 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
778 809 'repo')
779 810 except error.RequirementError:
780 811 raise
781 812 except error.RepoError:
782 813 if rpath and rpath[-1]: # invalid -R path
783 814 raise
784 815 if not func.optionalrepo:
785 816 if func.inferrepo and args and not path:
786 817 # try to infer -R from command args
787 818 repos = map(cmdutil.findrepo, args)
788 819 guess = repos[0]
789 820 if guess and repos.count(guess) == len(repos):
790 821 req.args = ['--repository', guess] + fullargs
791 822 return _dispatch(req)
792 823 if not path:
793 824 raise error.RepoError(_("no repository found in"
794 825 " '%s' (.hg not found)")
795 826 % pycompat.getcwd())
796 827 raise
797 828 if repo:
798 829 ui = repo.ui
799 830 if options['hidden']:
800 831 repo = repo.unfiltered()
801 832 args.insert(0, repo)
802 833 elif rpath:
803 834 ui.warn(_("warning: --repository ignored\n"))
804 835
805 836 msg = ' '.join(' ' in a and repr(a) or a for a in fullargs)
806 837 ui.log("command", '%s\n', msg)
807 838 strcmdopt = pycompat.strkwargs(cmdoptions)
808 839 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
809 840 try:
810 841 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
811 842 cmdpats, cmdoptions)
812 843 finally:
813 844 if repo and repo != req.repo:
814 845 repo.close()
815 846
816 847 def _runcommand(ui, options, cmd, cmdfunc):
817 848 """Run a command function, possibly with profiling enabled."""
818 849 try:
819 850 return cmdfunc()
820 851 except error.SignatureError:
821 852 raise error.CommandError(cmd, _('invalid arguments'))
822 853
823 854 def _exceptionwarning(ui):
824 855 """Produce a warning message for the current active exception"""
825 856
826 857 # For compatibility checking, we discard the portion of the hg
827 858 # version after the + on the assumption that if a "normal
828 859 # user" is running a build with a + in it the packager
829 860 # probably built from fairly close to a tag and anyone with a
830 861 # 'make local' copy of hg (where the version number can be out
831 862 # of date) will be clueful enough to notice the implausible
832 863 # version number and try updating.
833 864 ct = util.versiontuple(n=2)
834 865 worst = None, ct, ''
835 866 if ui.config('ui', 'supportcontact', None) is None:
836 867 for name, mod in extensions.extensions():
837 868 testedwith = getattr(mod, 'testedwith', '')
838 869 report = getattr(mod, 'buglink', _('the extension author.'))
839 870 if not testedwith.strip():
840 871 # We found an untested extension. It's likely the culprit.
841 872 worst = name, 'unknown', report
842 873 break
843 874
844 875 # Never blame on extensions bundled with Mercurial.
845 876 if extensions.ismoduleinternal(mod):
846 877 continue
847 878
848 879 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
849 880 if ct in tested:
850 881 continue
851 882
852 883 lower = [t for t in tested if t < ct]
853 884 nearest = max(lower or tested)
854 885 if worst[0] is None or nearest < worst[1]:
855 886 worst = name, nearest, report
856 887 if worst[0] is not None:
857 888 name, testedwith, report = worst
858 889 if not isinstance(testedwith, str):
859 890 testedwith = '.'.join([str(c) for c in testedwith])
860 891 warning = (_('** Unknown exception encountered with '
861 892 'possibly-broken third-party extension %s\n'
862 893 '** which supports versions %s of Mercurial.\n'
863 894 '** Please disable %s and try your action again.\n'
864 895 '** If that fixes the bug please report it to %s\n')
865 896 % (name, testedwith, name, report))
866 897 else:
867 898 bugtracker = ui.config('ui', 'supportcontact', None)
868 899 if bugtracker is None:
869 900 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
870 901 warning = (_("** unknown exception encountered, "
871 902 "please report by visiting\n** ") + bugtracker + '\n')
872 903 warning += ((_("** Python %s\n") % sys.version.replace('\n', '')) +
873 904 (_("** Mercurial Distributed SCM (version %s)\n") %
874 905 util.version()) +
875 906 (_("** Extensions loaded: %s\n") %
876 907 ", ".join([x[0] for x in extensions.extensions()])))
877 908 return warning
878 909
879 910 def handlecommandexception(ui):
880 911 """Produce a warning message for broken commands
881 912
882 913 Called when handling an exception; the exception is reraised if
883 914 this function returns False, ignored otherwise.
884 915 """
885 916 warning = _exceptionwarning(ui)
886 917 ui.log("commandexception", "%s\n%s\n", warning, traceback.format_exc())
887 918 ui.warn(warning)
888 919 return False # re-raise the exception
@@ -1,564 +1,577 b''
1 1
2 2 This test tries to exercise the ssh functionality with a dummy script
3 3
4 4 $ cat <<EOF >> $HGRCPATH
5 5 > [format]
6 6 > usegeneraldelta=yes
7 7 > EOF
8 8
9 9 creating 'remote' repo
10 10
11 11 $ hg init remote
12 12 $ cd remote
13 13 $ echo this > foo
14 14 $ echo this > fooO
15 15 $ hg ci -A -m "init" foo fooO
16 16
17 17 insert a closed branch (issue4428)
18 18
19 19 $ hg up null
20 20 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
21 21 $ hg branch closed
22 22 marked working directory as branch closed
23 23 (branches are permanent and global, did you want a bookmark?)
24 24 $ hg ci -mc0
25 25 $ hg ci --close-branch -mc1
26 26 $ hg up -q default
27 27
28 28 configure for serving
29 29
30 30 $ cat <<EOF > .hg/hgrc
31 31 > [server]
32 32 > uncompressed = True
33 33 >
34 34 > [hooks]
35 35 > changegroup = sh -c "printenv.py changegroup-in-remote 0 ../dummylog"
36 36 > EOF
37 37 $ cd ..
38 38
39 39 repo not found error
40 40
41 41 $ hg clone -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/nonexistent local
42 42 remote: abort: repository nonexistent not found!
43 43 abort: no suitable response from remote hg!
44 44 [255]
45 45
46 46 non-existent absolute path
47 47
48 48 $ hg clone -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/`pwd`/nonexistent local
49 49 remote: abort: repository $TESTTMP/nonexistent not found!
50 50 abort: no suitable response from remote hg!
51 51 [255]
52 52
53 53 clone remote via stream
54 54
55 55 $ hg clone -e "python \"$TESTDIR/dummyssh\"" --uncompressed ssh://user@dummy/remote local-stream
56 56 streaming all changes
57 57 4 files to transfer, 602 bytes of data
58 58 transferred 602 bytes in * seconds (*) (glob)
59 59 searching for changes
60 60 no changes found
61 61 updating to branch default
62 62 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
63 63 $ cd local-stream
64 64 $ hg verify
65 65 checking changesets
66 66 checking manifests
67 67 crosschecking files in changesets and manifests
68 68 checking files
69 69 2 files, 3 changesets, 2 total revisions
70 70 $ hg branches
71 71 default 0:1160648e36ce
72 72 $ cd ..
73 73
74 74 clone bookmarks via stream
75 75
76 76 $ hg -R local-stream book mybook
77 77 $ hg clone -e "python \"$TESTDIR/dummyssh\"" --uncompressed ssh://user@dummy/local-stream stream2
78 78 streaming all changes
79 79 4 files to transfer, 602 bytes of data
80 80 transferred 602 bytes in * seconds (*) (glob)
81 81 searching for changes
82 82 no changes found
83 83 updating to branch default
84 84 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
85 85 $ cd stream2
86 86 $ hg book
87 87 mybook 0:1160648e36ce
88 88 $ cd ..
89 89 $ rm -rf local-stream stream2
90 90
91 91 clone remote via pull
92 92
93 93 $ hg clone -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local
94 94 requesting all changes
95 95 adding changesets
96 96 adding manifests
97 97 adding file changes
98 98 added 3 changesets with 2 changes to 2 files
99 99 updating to branch default
100 100 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
101 101
102 102 verify
103 103
104 104 $ cd local
105 105 $ hg verify
106 106 checking changesets
107 107 checking manifests
108 108 crosschecking files in changesets and manifests
109 109 checking files
110 110 2 files, 3 changesets, 2 total revisions
111 111 $ cat >> .hg/hgrc <<EOF
112 112 > [hooks]
113 113 > changegroup = sh -c "printenv.py changegroup-in-local 0 ../dummylog"
114 114 > EOF
115 115
116 116 empty default pull
117 117
118 118 $ hg paths
119 119 default = ssh://user@dummy/remote
120 120 $ hg pull -e "python \"$TESTDIR/dummyssh\""
121 121 pulling from ssh://user@dummy/remote
122 122 searching for changes
123 123 no changes found
124 124
125 125 pull from wrong ssh URL
126 126
127 127 $ hg pull -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/doesnotexist
128 128 pulling from ssh://user@dummy/doesnotexist
129 129 remote: abort: repository doesnotexist not found!
130 130 abort: no suitable response from remote hg!
131 131 [255]
132 132
133 133 local change
134 134
135 135 $ echo bleah > foo
136 136 $ hg ci -m "add"
137 137
138 138 updating rc
139 139
140 140 $ echo "default-push = ssh://user@dummy/remote" >> .hg/hgrc
141 141 $ echo "[ui]" >> .hg/hgrc
142 142 $ echo "ssh = python \"$TESTDIR/dummyssh\"" >> .hg/hgrc
143 143
144 144 find outgoing
145 145
146 146 $ hg out ssh://user@dummy/remote
147 147 comparing with ssh://user@dummy/remote
148 148 searching for changes
149 149 changeset: 3:a28a9d1a809c
150 150 tag: tip
151 151 parent: 0:1160648e36ce
152 152 user: test
153 153 date: Thu Jan 01 00:00:00 1970 +0000
154 154 summary: add
155 155
156 156
157 157 find incoming on the remote side
158 158
159 159 $ hg incoming -R ../remote -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/local
160 160 comparing with ssh://user@dummy/local
161 161 searching for changes
162 162 changeset: 3:a28a9d1a809c
163 163 tag: tip
164 164 parent: 0:1160648e36ce
165 165 user: test
166 166 date: Thu Jan 01 00:00:00 1970 +0000
167 167 summary: add
168 168
169 169
170 170 find incoming on the remote side (using absolute path)
171 171
172 172 $ hg incoming -R ../remote -e "python \"$TESTDIR/dummyssh\"" "ssh://user@dummy/`pwd`"
173 173 comparing with ssh://user@dummy/$TESTTMP/local
174 174 searching for changes
175 175 changeset: 3:a28a9d1a809c
176 176 tag: tip
177 177 parent: 0:1160648e36ce
178 178 user: test
179 179 date: Thu Jan 01 00:00:00 1970 +0000
180 180 summary: add
181 181
182 182
183 183 push
184 184
185 185 $ hg push
186 186 pushing to ssh://user@dummy/remote
187 187 searching for changes
188 188 remote: adding changesets
189 189 remote: adding manifests
190 190 remote: adding file changes
191 191 remote: added 1 changesets with 1 changes to 1 files
192 192 $ cd ../remote
193 193
194 194 check remote tip
195 195
196 196 $ hg tip
197 197 changeset: 3:a28a9d1a809c
198 198 tag: tip
199 199 parent: 0:1160648e36ce
200 200 user: test
201 201 date: Thu Jan 01 00:00:00 1970 +0000
202 202 summary: add
203 203
204 204 $ hg verify
205 205 checking changesets
206 206 checking manifests
207 207 crosschecking files in changesets and manifests
208 208 checking files
209 209 2 files, 4 changesets, 3 total revisions
210 210 $ hg cat -r tip foo
211 211 bleah
212 212 $ echo z > z
213 213 $ hg ci -A -m z z
214 214 created new head
215 215
216 216 test pushkeys and bookmarks
217 217
218 218 $ cd ../local
219 219 $ hg debugpushkey --config ui.ssh="python \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote namespaces
220 220 bookmarks
221 221 namespaces
222 222 phases
223 223 $ hg book foo -r 0
224 224 $ hg out -B
225 225 comparing with ssh://user@dummy/remote
226 226 searching for changed bookmarks
227 227 foo 1160648e36ce
228 228 $ hg push -B foo
229 229 pushing to ssh://user@dummy/remote
230 230 searching for changes
231 231 no changes found
232 232 exporting bookmark foo
233 233 [1]
234 234 $ hg debugpushkey --config ui.ssh="python \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote bookmarks
235 235 foo 1160648e36cec0054048a7edc4110c6f84fde594
236 236 $ hg book -f foo
237 237 $ hg push --traceback
238 238 pushing to ssh://user@dummy/remote
239 239 searching for changes
240 240 no changes found
241 241 updating bookmark foo
242 242 [1]
243 243 $ hg book -d foo
244 244 $ hg in -B
245 245 comparing with ssh://user@dummy/remote
246 246 searching for changed bookmarks
247 247 foo a28a9d1a809c
248 248 $ hg book -f -r 0 foo
249 249 $ hg pull -B foo
250 250 pulling from ssh://user@dummy/remote
251 251 no changes found
252 252 updating bookmark foo
253 253 $ hg book -d foo
254 254 $ hg push -B foo
255 255 pushing to ssh://user@dummy/remote
256 256 searching for changes
257 257 no changes found
258 258 deleting remote bookmark foo
259 259 [1]
260 260
261 261 a bad, evil hook that prints to stdout
262 262
263 263 $ cat <<EOF > $TESTTMP/badhook
264 264 > import sys
265 265 > sys.stdout.write("KABOOM\n")
266 266 > EOF
267 267
268 268 $ cat <<EOF > $TESTTMP/badpyhook.py
269 269 > import sys
270 270 > def hook(ui, repo, hooktype, **kwargs):
271 271 > sys.stdout.write("KABOOM IN PROCESS\n")
272 272 > EOF
273 273
274 274 $ cat <<EOF >> ../remote/.hg/hgrc
275 275 > [hooks]
276 276 > changegroup.stdout = python $TESTTMP/badhook
277 277 > changegroup.pystdout = python:$TESTTMP/badpyhook.py:hook
278 278 > EOF
279 279 $ echo r > r
280 280 $ hg ci -A -m z r
281 281
282 282 push should succeed even though it has an unexpected response
283 283
284 284 $ hg push
285 285 pushing to ssh://user@dummy/remote
286 286 searching for changes
287 287 remote has heads on branch 'default' that are not known locally: 6c0482d977a3
288 288 remote: adding changesets
289 289 remote: adding manifests
290 290 remote: adding file changes
291 291 remote: added 1 changesets with 1 changes to 1 files
292 292 remote: KABOOM
293 293 remote: KABOOM IN PROCESS
294 294 $ hg -R ../remote heads
295 295 changeset: 5:1383141674ec
296 296 tag: tip
297 297 parent: 3:a28a9d1a809c
298 298 user: test
299 299 date: Thu Jan 01 00:00:00 1970 +0000
300 300 summary: z
301 301
302 302 changeset: 4:6c0482d977a3
303 303 parent: 0:1160648e36ce
304 304 user: test
305 305 date: Thu Jan 01 00:00:00 1970 +0000
306 306 summary: z
307 307
308 308
309 309 clone bookmarks
310 310
311 311 $ hg -R ../remote bookmark test
312 312 $ hg -R ../remote bookmarks
313 313 * test 4:6c0482d977a3
314 314 $ hg clone -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local-bookmarks
315 315 requesting all changes
316 316 adding changesets
317 317 adding manifests
318 318 adding file changes
319 319 added 6 changesets with 5 changes to 4 files (+1 heads)
320 320 updating to branch default
321 321 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
322 322 $ hg -R local-bookmarks bookmarks
323 323 test 4:6c0482d977a3
324 324
325 325 passwords in ssh urls are not supported
326 326 (we use a glob here because different Python versions give different
327 327 results here)
328 328
329 329 $ hg push ssh://user:erroneouspwd@dummy/remote
330 330 pushing to ssh://user:*@dummy/remote (glob)
331 331 abort: password in URL not supported!
332 332 [255]
333 333
334 334 $ cd ..
335 335
336 336 hide outer repo
337 337 $ hg init
338 338
339 339 Test remote paths with spaces (issue2983):
340 340
341 341 $ hg init --ssh "python \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
342 342 $ touch "$TESTTMP/a repo/test"
343 343 $ hg -R 'a repo' commit -A -m "test"
344 344 adding test
345 345 $ hg -R 'a repo' tag tag
346 346 $ hg id --ssh "python \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
347 347 73649e48688a
348 348
349 349 $ hg id --ssh "python \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo#noNoNO"
350 350 abort: unknown revision 'noNoNO'!
351 351 [255]
352 352
353 353 Test (non-)escaping of remote paths with spaces when cloning (issue3145):
354 354
355 355 $ hg clone --ssh "python \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
356 356 destination directory: a repo
357 357 abort: destination 'a repo' is not empty
358 358 [255]
359 359
360 Make sure hg is really paranoid in serve --stdio mode. It used to be
361 possible to get a debugger REPL by specifying a repo named --debugger.
362 $ hg -R --debugger serve --stdio
363 abort: potentially unsafe serve --stdio invocation: ['-R', '--debugger', 'serve', '--stdio']
364 [255]
365 $ hg -R --config=ui.debugger=yes serve --stdio
366 abort: potentially unsafe serve --stdio invocation: ['-R', '--config=ui.debugger=yes', 'serve', '--stdio']
367 [255]
368 Abbreviations of 'serve' also don't work, to avoid shenanigans.
369 $ hg -R narf serv --stdio
370 abort: potentially unsafe serve --stdio invocation: ['-R', 'narf', 'serv', '--stdio']
371 [255]
372
360 373 Test hg-ssh using a helper script that will restore PYTHONPATH (which might
361 374 have been cleared by a hg.exe wrapper) and invoke hg-ssh with the right
362 375 parameters:
363 376
364 377 $ cat > ssh.sh << EOF
365 378 > userhost="\$1"
366 379 > SSH_ORIGINAL_COMMAND="\$2"
367 380 > export SSH_ORIGINAL_COMMAND
368 381 > PYTHONPATH="$PYTHONPATH"
369 382 > export PYTHONPATH
370 383 > python "$TESTDIR/../contrib/hg-ssh" "$TESTTMP/a repo"
371 384 > EOF
372 385
373 386 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a repo"
374 387 73649e48688a
375 388
376 389 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a'repo"
377 390 remote: Illegal repository "$TESTTMP/a'repo" (glob)
378 391 abort: no suitable response from remote hg!
379 392 [255]
380 393
381 394 $ hg id --ssh "sh ssh.sh" --remotecmd hacking "ssh://user@dummy/a'repo"
382 395 remote: Illegal command "hacking -R 'a'\''repo' serve --stdio"
383 396 abort: no suitable response from remote hg!
384 397 [255]
385 398
386 399 $ SSH_ORIGINAL_COMMAND="'hg' -R 'a'repo' serve --stdio" python "$TESTDIR/../contrib/hg-ssh"
387 400 Illegal command "'hg' -R 'a'repo' serve --stdio": No closing quotation
388 401 [255]
389 402
390 403 Test hg-ssh in read-only mode:
391 404
392 405 $ cat > ssh.sh << EOF
393 406 > userhost="\$1"
394 407 > SSH_ORIGINAL_COMMAND="\$2"
395 408 > export SSH_ORIGINAL_COMMAND
396 409 > PYTHONPATH="$PYTHONPATH"
397 410 > export PYTHONPATH
398 411 > python "$TESTDIR/../contrib/hg-ssh" --read-only "$TESTTMP/remote"
399 412 > EOF
400 413
401 414 $ hg clone --ssh "sh ssh.sh" "ssh://user@dummy/$TESTTMP/remote" read-only-local
402 415 requesting all changes
403 416 adding changesets
404 417 adding manifests
405 418 adding file changes
406 419 added 6 changesets with 5 changes to 4 files (+1 heads)
407 420 updating to branch default
408 421 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
409 422
410 423 $ cd read-only-local
411 424 $ echo "baz" > bar
412 425 $ hg ci -A -m "unpushable commit" bar
413 426 $ hg push --ssh "sh ../ssh.sh"
414 427 pushing to ssh://user@dummy/*/remote (glob)
415 428 searching for changes
416 429 remote: Permission denied
417 430 remote: pretxnopen.hg-ssh hook failed
418 431 abort: push failed on remote
419 432 [255]
420 433
421 434 $ cd ..
422 435
423 436 stderr from remote commands should be printed before stdout from local code (issue4336)
424 437
425 438 $ hg clone remote stderr-ordering
426 439 updating to branch default
427 440 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
428 441 $ cd stderr-ordering
429 442 $ cat >> localwrite.py << EOF
430 443 > from mercurial import exchange, extensions
431 444 >
432 445 > def wrappedpush(orig, repo, *args, **kwargs):
433 446 > res = orig(repo, *args, **kwargs)
434 447 > repo.ui.write('local stdout\n')
435 448 > return res
436 449 >
437 450 > def extsetup(ui):
438 451 > extensions.wrapfunction(exchange, 'push', wrappedpush)
439 452 > EOF
440 453
441 454 $ cat >> .hg/hgrc << EOF
442 455 > [paths]
443 456 > default-push = ssh://user@dummy/remote
444 457 > [ui]
445 458 > ssh = python "$TESTDIR/dummyssh"
446 459 > [extensions]
447 460 > localwrite = localwrite.py
448 461 > EOF
449 462
450 463 $ echo localwrite > foo
451 464 $ hg commit -m 'testing localwrite'
452 465 $ hg push
453 466 pushing to ssh://user@dummy/remote
454 467 searching for changes
455 468 remote: adding changesets
456 469 remote: adding manifests
457 470 remote: adding file changes
458 471 remote: added 1 changesets with 1 changes to 1 files
459 472 remote: KABOOM
460 473 remote: KABOOM IN PROCESS
461 474 local stdout
462 475
463 476 debug output
464 477
465 478 $ hg pull --debug ssh://user@dummy/remote
466 479 pulling from ssh://user@dummy/remote
467 480 running python ".*/dummyssh" user@dummy ('|")hg -R remote serve --stdio('|") (re)
468 481 sending hello command
469 482 sending between command
470 483 remote: 355
471 484 remote: capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512%0Aerror%3Dabort%2Cunsupportedcontent%2Cpushraced%2Cpushkey%0Ahgtagsfnodes%0Alistkeys%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN
472 485 remote: 1
473 486 query 1; heads
474 487 sending batch command
475 488 searching for changes
476 489 all remote heads known locally
477 490 no changes found
478 491 sending getbundle command
479 492 bundle2-input-bundle: with-transaction
480 493 bundle2-input-part: "listkeys" (params: 1 mandatory) supported
481 494 bundle2-input-part: total payload size 15
482 495 bundle2-input-part: "listkeys" (params: 1 mandatory) supported
483 496 bundle2-input-part: total payload size 45
484 497 bundle2-input-bundle: 1 parts total
485 498 checking for updated bookmarks
486 499
487 500 $ cd ..
488 501
489 502 $ cat dummylog
490 503 Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio
491 504 Got arguments 1:user@dummy 2:hg -R $TESTTMP/nonexistent serve --stdio
492 505 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
493 506 Got arguments 1:user@dummy 2:hg -R local-stream serve --stdio
494 507 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
495 508 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
496 509 Got arguments 1:user@dummy 2:hg -R doesnotexist serve --stdio
497 510 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
498 511 Got arguments 1:user@dummy 2:hg -R local serve --stdio
499 512 Got arguments 1:user@dummy 2:hg -R $TESTTMP/local serve --stdio
500 513 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
501 514 changegroup-in-remote hook: HG_BUNDLE2=1 HG_NODE=a28a9d1a809cab7d4e2fde4bee738a9ede948b60 HG_NODE_LAST=a28a9d1a809cab7d4e2fde4bee738a9ede948b60 HG_SOURCE=serve HG_TXNID=TXN:* HG_URL=remote:ssh:127.0.0.1 (glob)
502 515 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
503 516 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
504 517 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
505 518 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
506 519 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
507 520 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
508 521 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
509 522 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
510 523 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
511 524 changegroup-in-remote hook: HG_BUNDLE2=1 HG_NODE=1383141674ec756a6056f6a9097618482fe0f4a6 HG_NODE_LAST=1383141674ec756a6056f6a9097618482fe0f4a6 HG_SOURCE=serve HG_TXNID=TXN:* HG_URL=remote:ssh:127.0.0.1 (glob)
512 525 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
513 526 Got arguments 1:user@dummy 2:hg init 'a repo'
514 527 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
515 528 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
516 529 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
517 530 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
518 531 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
519 532 changegroup-in-remote hook: HG_BUNDLE2=1 HG_NODE=65c38f4125f9602c8db4af56530cc221d93b8ef8 HG_NODE_LAST=65c38f4125f9602c8db4af56530cc221d93b8ef8 HG_SOURCE=serve HG_TXNID=TXN:* HG_URL=remote:ssh:127.0.0.1 (glob)
520 533 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
521 534
522 535 remote hook failure is attributed to remote
523 536
524 537 $ cat > $TESTTMP/failhook << EOF
525 538 > def hook(ui, repo, **kwargs):
526 539 > ui.write('hook failure!\n')
527 540 > ui.flush()
528 541 > return 1
529 542 > EOF
530 543
531 544 $ echo "pretxnchangegroup.fail = python:$TESTTMP/failhook:hook" >> remote/.hg/hgrc
532 545
533 546 $ hg -q --config ui.ssh="python $TESTDIR/dummyssh" clone ssh://user@dummy/remote hookout
534 547 $ cd hookout
535 548 $ touch hookfailure
536 549 $ hg -q commit -A -m 'remote hook failure'
537 550 $ hg --config ui.ssh="python $TESTDIR/dummyssh" push
538 551 pushing to ssh://user@dummy/remote
539 552 searching for changes
540 553 remote: adding changesets
541 554 remote: adding manifests
542 555 remote: adding file changes
543 556 remote: added 1 changesets with 1 changes to 1 files
544 557 remote: hook failure!
545 558 remote: transaction abort!
546 559 remote: rollback completed
547 560 remote: pretxnchangegroup.fail hook failed
548 561 abort: push failed on remote
549 562 [255]
550 563
551 564 abort during pull is properly reported as such
552 565
553 566 $ echo morefoo >> ../remote/foo
554 567 $ hg -R ../remote commit --message "more foo to be pulled"
555 568 $ cat >> ../remote/.hg/hgrc << EOF
556 569 > [extensions]
557 570 > crash = ${TESTDIR}/crashgetbundler.py
558 571 > EOF
559 572 $ hg --config ui.ssh="python $TESTDIR/dummyssh" pull
560 573 pulling from ssh://user@dummy/remote
561 574 searching for changes
562 575 remote: abort: this is an exercise
563 576 abort: pull failed on remote
564 577 [255]
General Comments 0
You need to be logged in to leave comments. Login now