##// END OF EJS Templates
ssh: quote parameters using shellquote (SEC)...
Jun Wu -
r33732:8cb9e921 stable
parent child Browse files
Show More
@@ -1,672 +1,675
1 1 # posix.py - Posix utility function implementations for Mercurial
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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
9 9
10 10 import errno
11 11 import fcntl
12 12 import getpass
13 13 import grp
14 14 import os
15 15 import pwd
16 16 import re
17 17 import select
18 18 import stat
19 19 import sys
20 20 import tempfile
21 21 import unicodedata
22 22
23 23 from .i18n import _
24 24 from . import (
25 25 encoding,
26 26 error,
27 27 pycompat,
28 28 )
29 29
30 30 posixfile = open
31 31 normpath = os.path.normpath
32 32 samestat = os.path.samestat
33 33 try:
34 34 oslink = os.link
35 35 except AttributeError:
36 36 # Some platforms build Python without os.link on systems that are
37 37 # vaguely unix-like but don't have hardlink support. For those
38 38 # poor souls, just say we tried and that it failed so we fall back
39 39 # to copies.
40 40 def oslink(src, dst):
41 41 raise OSError(errno.EINVAL,
42 42 'hardlinks not supported: %s to %s' % (src, dst))
43 43 unlink = os.unlink
44 44 rename = os.rename
45 45 removedirs = os.removedirs
46 46 expandglobs = False
47 47
48 48 umask = os.umask(0)
49 49 os.umask(umask)
50 50
51 51 def split(p):
52 52 '''Same as posixpath.split, but faster
53 53
54 54 >>> import posixpath
55 55 >>> for f in ['/absolute/path/to/file',
56 56 ... 'relative/path/to/file',
57 57 ... 'file_alone',
58 58 ... 'path/to/directory/',
59 59 ... '/multiple/path//separators',
60 60 ... '/file_at_root',
61 61 ... '///multiple_leading_separators_at_root',
62 62 ... '']:
63 63 ... assert split(f) == posixpath.split(f), f
64 64 '''
65 65 ht = p.rsplit('/', 1)
66 66 if len(ht) == 1:
67 67 return '', p
68 68 nh = ht[0].rstrip('/')
69 69 if nh:
70 70 return nh, ht[1]
71 71 return ht[0] + '/', ht[1]
72 72
73 73 def openhardlinks():
74 74 '''return true if it is safe to hold open file handles to hardlinks'''
75 75 return True
76 76
77 77 def nlinks(name):
78 78 '''return number of hardlinks for the given file'''
79 79 return os.lstat(name).st_nlink
80 80
81 81 def parsepatchoutput(output_line):
82 82 """parses the output produced by patch and returns the filename"""
83 83 pf = output_line[14:]
84 84 if pycompat.sysplatform == 'OpenVMS':
85 85 if pf[0] == '`':
86 86 pf = pf[1:-1] # Remove the quotes
87 87 else:
88 88 if pf.startswith("'") and pf.endswith("'") and " " in pf:
89 89 pf = pf[1:-1] # Remove the quotes
90 90 return pf
91 91
92 92 def sshargs(sshcmd, host, user, port):
93 93 '''Build argument list for ssh'''
94 94 args = user and ("%s@%s" % (user, host)) or host
95 if '-' in args[:2]:
95 if '-' in args[:1]:
96 96 raise error.Abort(
97 97 _('illegal ssh hostname or username starting with -: %s') % args)
98 return port and ("%s -p %s" % (args, port)) or args
98 args = shellquote(args)
99 if port:
100 args = '-p %s %s' % (shellquote(port), args)
101 return args
99 102
100 103 def isexec(f):
101 104 """check whether a file is executable"""
102 105 return (os.lstat(f).st_mode & 0o100 != 0)
103 106
104 107 def setflags(f, l, x):
105 108 st = os.lstat(f)
106 109 s = st.st_mode
107 110 if l:
108 111 if not stat.S_ISLNK(s):
109 112 # switch file to link
110 113 fp = open(f)
111 114 data = fp.read()
112 115 fp.close()
113 116 unlink(f)
114 117 try:
115 118 os.symlink(data, f)
116 119 except OSError:
117 120 # failed to make a link, rewrite file
118 121 fp = open(f, "w")
119 122 fp.write(data)
120 123 fp.close()
121 124 # no chmod needed at this point
122 125 return
123 126 if stat.S_ISLNK(s):
124 127 # switch link to file
125 128 data = os.readlink(f)
126 129 unlink(f)
127 130 fp = open(f, "w")
128 131 fp.write(data)
129 132 fp.close()
130 133 s = 0o666 & ~umask # avoid restatting for chmod
131 134
132 135 sx = s & 0o100
133 136 if st.st_nlink > 1 and bool(x) != bool(sx):
134 137 # the file is a hardlink, break it
135 138 with open(f, "rb") as fp:
136 139 data = fp.read()
137 140 unlink(f)
138 141 with open(f, "wb") as fp:
139 142 fp.write(data)
140 143
141 144 if x and not sx:
142 145 # Turn on +x for every +r bit when making a file executable
143 146 # and obey umask.
144 147 os.chmod(f, s | (s & 0o444) >> 2 & ~umask)
145 148 elif not x and sx:
146 149 # Turn off all +x bits
147 150 os.chmod(f, s & 0o666)
148 151
149 152 def copymode(src, dst, mode=None):
150 153 '''Copy the file mode from the file at path src to dst.
151 154 If src doesn't exist, we're using mode instead. If mode is None, we're
152 155 using umask.'''
153 156 try:
154 157 st_mode = os.lstat(src).st_mode & 0o777
155 158 except OSError as inst:
156 159 if inst.errno != errno.ENOENT:
157 160 raise
158 161 st_mode = mode
159 162 if st_mode is None:
160 163 st_mode = ~umask
161 164 st_mode &= 0o666
162 165 os.chmod(dst, st_mode)
163 166
164 167 def checkexec(path):
165 168 """
166 169 Check whether the given path is on a filesystem with UNIX-like exec flags
167 170
168 171 Requires a directory (like /foo/.hg)
169 172 """
170 173
171 174 # VFAT on some Linux versions can flip mode but it doesn't persist
172 175 # a FS remount. Frequently we can detect it if files are created
173 176 # with exec bit on.
174 177
175 178 try:
176 179 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
177 180 cachedir = os.path.join(path, '.hg', 'cache')
178 181 if os.path.isdir(cachedir):
179 182 checkisexec = os.path.join(cachedir, 'checkisexec')
180 183 checknoexec = os.path.join(cachedir, 'checknoexec')
181 184
182 185 try:
183 186 m = os.stat(checkisexec).st_mode
184 187 except OSError as e:
185 188 if e.errno != errno.ENOENT:
186 189 raise
187 190 # checkisexec does not exist - fall through ...
188 191 else:
189 192 # checkisexec exists, check if it actually is exec
190 193 if m & EXECFLAGS != 0:
191 194 # ensure checkisexec exists, check it isn't exec
192 195 try:
193 196 m = os.stat(checknoexec).st_mode
194 197 except OSError as e:
195 198 if e.errno != errno.ENOENT:
196 199 raise
197 200 open(checknoexec, 'w').close() # might fail
198 201 m = os.stat(checknoexec).st_mode
199 202 if m & EXECFLAGS == 0:
200 203 # check-exec is exec and check-no-exec is not exec
201 204 return True
202 205 # checknoexec exists but is exec - delete it
203 206 unlink(checknoexec)
204 207 # checkisexec exists but is not exec - delete it
205 208 unlink(checkisexec)
206 209
207 210 # check using one file, leave it as checkisexec
208 211 checkdir = cachedir
209 212 else:
210 213 # check directly in path and don't leave checkisexec behind
211 214 checkdir = path
212 215 checkisexec = None
213 216 fh, fn = tempfile.mkstemp(dir=checkdir, prefix='hg-checkexec-')
214 217 try:
215 218 os.close(fh)
216 219 m = os.stat(fn).st_mode
217 220 if m & EXECFLAGS == 0:
218 221 os.chmod(fn, m & 0o777 | EXECFLAGS)
219 222 if os.stat(fn).st_mode & EXECFLAGS != 0:
220 223 if checkisexec is not None:
221 224 os.rename(fn, checkisexec)
222 225 fn = None
223 226 return True
224 227 finally:
225 228 if fn is not None:
226 229 unlink(fn)
227 230 except (IOError, OSError):
228 231 # we don't care, the user probably won't be able to commit anyway
229 232 return False
230 233
231 234 def checklink(path):
232 235 """check whether the given path is on a symlink-capable filesystem"""
233 236 # mktemp is not racy because symlink creation will fail if the
234 237 # file already exists
235 238 while True:
236 239 cachedir = os.path.join(path, '.hg', 'cache')
237 240 checklink = os.path.join(cachedir, 'checklink')
238 241 # try fast path, read only
239 242 if os.path.islink(checklink):
240 243 return True
241 244 if os.path.isdir(cachedir):
242 245 checkdir = cachedir
243 246 else:
244 247 checkdir = path
245 248 cachedir = None
246 249 fscheckdir = pycompat.fsdecode(checkdir)
247 250 name = tempfile.mktemp(dir=fscheckdir,
248 251 prefix=r'checklink-')
249 252 name = pycompat.fsencode(name)
250 253 try:
251 254 fd = None
252 255 if cachedir is None:
253 256 fd = tempfile.NamedTemporaryFile(dir=fscheckdir,
254 257 prefix=r'hg-checklink-')
255 258 target = pycompat.fsencode(os.path.basename(fd.name))
256 259 else:
257 260 # create a fixed file to link to; doesn't matter if it
258 261 # already exists.
259 262 target = 'checklink-target'
260 263 try:
261 264 open(os.path.join(cachedir, target), 'w').close()
262 265 except IOError as inst:
263 266 if inst[0] == errno.EACCES:
264 267 # If we can't write to cachedir, just pretend
265 268 # that the fs is readonly and by association
266 269 # that the fs won't support symlinks. This
267 270 # seems like the least dangerous way to avoid
268 271 # data loss.
269 272 return False
270 273 raise
271 274 try:
272 275 os.symlink(target, name)
273 276 if cachedir is None:
274 277 unlink(name)
275 278 else:
276 279 try:
277 280 os.rename(name, checklink)
278 281 except OSError:
279 282 unlink(name)
280 283 return True
281 284 except OSError as inst:
282 285 # link creation might race, try again
283 286 if inst[0] == errno.EEXIST:
284 287 continue
285 288 raise
286 289 finally:
287 290 if fd is not None:
288 291 fd.close()
289 292 except AttributeError:
290 293 return False
291 294 except OSError as inst:
292 295 # sshfs might report failure while successfully creating the link
293 296 if inst[0] == errno.EIO and os.path.exists(name):
294 297 unlink(name)
295 298 return False
296 299
297 300 def checkosfilename(path):
298 301 '''Check that the base-relative path is a valid filename on this platform.
299 302 Returns None if the path is ok, or a UI string describing the problem.'''
300 303 pass # on posix platforms, every path is ok
301 304
302 305 def setbinary(fd):
303 306 pass
304 307
305 308 def pconvert(path):
306 309 return path
307 310
308 311 def localpath(path):
309 312 return path
310 313
311 314 def samefile(fpath1, fpath2):
312 315 """Returns whether path1 and path2 refer to the same file. This is only
313 316 guaranteed to work for files, not directories."""
314 317 return os.path.samefile(fpath1, fpath2)
315 318
316 319 def samedevice(fpath1, fpath2):
317 320 """Returns whether fpath1 and fpath2 are on the same device. This is only
318 321 guaranteed to work for files, not directories."""
319 322 st1 = os.lstat(fpath1)
320 323 st2 = os.lstat(fpath2)
321 324 return st1.st_dev == st2.st_dev
322 325
323 326 # os.path.normcase is a no-op, which doesn't help us on non-native filesystems
324 327 def normcase(path):
325 328 return path.lower()
326 329
327 330 # what normcase does to ASCII strings
328 331 normcasespec = encoding.normcasespecs.lower
329 332 # fallback normcase function for non-ASCII strings
330 333 normcasefallback = normcase
331 334
332 335 if pycompat.sysplatform == 'darwin':
333 336
334 337 def normcase(path):
335 338 '''
336 339 Normalize a filename for OS X-compatible comparison:
337 340 - escape-encode invalid characters
338 341 - decompose to NFD
339 342 - lowercase
340 343 - omit ignored characters [200c-200f, 202a-202e, 206a-206f,feff]
341 344
342 345 >>> normcase('UPPER')
343 346 'upper'
344 347 >>> normcase('Caf\xc3\xa9')
345 348 'cafe\\xcc\\x81'
346 349 >>> normcase('\xc3\x89')
347 350 'e\\xcc\\x81'
348 351 >>> normcase('\xb8\xca\xc3\xca\xbe\xc8.JPG') # issue3918
349 352 '%b8%ca%c3\\xca\\xbe%c8.jpg'
350 353 '''
351 354
352 355 try:
353 356 return encoding.asciilower(path) # exception for non-ASCII
354 357 except UnicodeDecodeError:
355 358 return normcasefallback(path)
356 359
357 360 normcasespec = encoding.normcasespecs.lower
358 361
359 362 def normcasefallback(path):
360 363 try:
361 364 u = path.decode('utf-8')
362 365 except UnicodeDecodeError:
363 366 # OS X percent-encodes any bytes that aren't valid utf-8
364 367 s = ''
365 368 pos = 0
366 369 l = len(path)
367 370 while pos < l:
368 371 try:
369 372 c = encoding.getutf8char(path, pos)
370 373 pos += len(c)
371 374 except ValueError:
372 375 c = '%%%02X' % ord(path[pos])
373 376 pos += 1
374 377 s += c
375 378
376 379 u = s.decode('utf-8')
377 380
378 381 # Decompose then lowercase (HFS+ technote specifies lower)
379 382 enc = unicodedata.normalize('NFD', u).lower().encode('utf-8')
380 383 # drop HFS+ ignored characters
381 384 return encoding.hfsignoreclean(enc)
382 385
383 386 if pycompat.sysplatform == 'cygwin':
384 387 # workaround for cygwin, in which mount point part of path is
385 388 # treated as case sensitive, even though underlying NTFS is case
386 389 # insensitive.
387 390
388 391 # default mount points
389 392 cygwinmountpoints = sorted([
390 393 "/usr/bin",
391 394 "/usr/lib",
392 395 "/cygdrive",
393 396 ], reverse=True)
394 397
395 398 # use upper-ing as normcase as same as NTFS workaround
396 399 def normcase(path):
397 400 pathlen = len(path)
398 401 if (pathlen == 0) or (path[0] != pycompat.ossep):
399 402 # treat as relative
400 403 return encoding.upper(path)
401 404
402 405 # to preserve case of mountpoint part
403 406 for mp in cygwinmountpoints:
404 407 if not path.startswith(mp):
405 408 continue
406 409
407 410 mplen = len(mp)
408 411 if mplen == pathlen: # mount point itself
409 412 return mp
410 413 if path[mplen] == pycompat.ossep:
411 414 return mp + encoding.upper(path[mplen:])
412 415
413 416 return encoding.upper(path)
414 417
415 418 normcasespec = encoding.normcasespecs.other
416 419 normcasefallback = normcase
417 420
418 421 # Cygwin translates native ACLs to POSIX permissions,
419 422 # but these translations are not supported by native
420 423 # tools, so the exec bit tends to be set erroneously.
421 424 # Therefore, disable executable bit access on Cygwin.
422 425 def checkexec(path):
423 426 return False
424 427
425 428 # Similarly, Cygwin's symlink emulation is likely to create
426 429 # problems when Mercurial is used from both Cygwin and native
427 430 # Windows, with other native tools, or on shared volumes
428 431 def checklink(path):
429 432 return False
430 433
431 434 _needsshellquote = None
432 435 def shellquote(s):
433 436 if pycompat.sysplatform == 'OpenVMS':
434 437 return '"%s"' % s
435 438 global _needsshellquote
436 439 if _needsshellquote is None:
437 440 _needsshellquote = re.compile(br'[^a-zA-Z0-9._/+-]').search
438 441 if s and not _needsshellquote(s):
439 442 # "s" shouldn't have to be quoted
440 443 return s
441 444 else:
442 445 return "'%s'" % s.replace("'", "'\\''")
443 446
444 447 def quotecommand(cmd):
445 448 return cmd
446 449
447 450 def popen(command, mode='r'):
448 451 return os.popen(command, mode)
449 452
450 453 def testpid(pid):
451 454 '''return False if pid dead, True if running or not sure'''
452 455 if pycompat.sysplatform == 'OpenVMS':
453 456 return True
454 457 try:
455 458 os.kill(pid, 0)
456 459 return True
457 460 except OSError as inst:
458 461 return inst.errno != errno.ESRCH
459 462
460 463 def explainexit(code):
461 464 """return a 2-tuple (desc, code) describing a subprocess status
462 465 (codes from kill are negative - not os.system/wait encoding)"""
463 466 if code >= 0:
464 467 return _("exited with status %d") % code, code
465 468 return _("killed by signal %d") % -code, -code
466 469
467 470 def isowner(st):
468 471 """Return True if the stat object st is from the current user."""
469 472 return st.st_uid == os.getuid()
470 473
471 474 def findexe(command):
472 475 '''Find executable for command searching like which does.
473 476 If command is a basename then PATH is searched for command.
474 477 PATH isn't searched if command is an absolute or relative path.
475 478 If command isn't found None is returned.'''
476 479 if pycompat.sysplatform == 'OpenVMS':
477 480 return command
478 481
479 482 def findexisting(executable):
480 483 'Will return executable if existing file'
481 484 if os.path.isfile(executable) and os.access(executable, os.X_OK):
482 485 return executable
483 486 return None
484 487
485 488 if pycompat.ossep in command:
486 489 return findexisting(command)
487 490
488 491 if pycompat.sysplatform == 'plan9':
489 492 return findexisting(os.path.join('/bin', command))
490 493
491 494 for path in encoding.environ.get('PATH', '').split(pycompat.ospathsep):
492 495 executable = findexisting(os.path.join(path, command))
493 496 if executable is not None:
494 497 return executable
495 498 return None
496 499
497 500 def setsignalhandler():
498 501 pass
499 502
500 503 _wantedkinds = {stat.S_IFREG, stat.S_IFLNK}
501 504
502 505 def statfiles(files):
503 506 '''Stat each file in files. Yield each stat, or None if a file does not
504 507 exist or has a type we don't care about.'''
505 508 lstat = os.lstat
506 509 getkind = stat.S_IFMT
507 510 for nf in files:
508 511 try:
509 512 st = lstat(nf)
510 513 if getkind(st.st_mode) not in _wantedkinds:
511 514 st = None
512 515 except OSError as err:
513 516 if err.errno not in (errno.ENOENT, errno.ENOTDIR):
514 517 raise
515 518 st = None
516 519 yield st
517 520
518 521 def getuser():
519 522 '''return name of current user'''
520 523 return pycompat.fsencode(getpass.getuser())
521 524
522 525 def username(uid=None):
523 526 """Return the name of the user with the given uid.
524 527
525 528 If uid is None, return the name of the current user."""
526 529
527 530 if uid is None:
528 531 uid = os.getuid()
529 532 try:
530 533 return pwd.getpwuid(uid)[0]
531 534 except KeyError:
532 535 return str(uid)
533 536
534 537 def groupname(gid=None):
535 538 """Return the name of the group with the given gid.
536 539
537 540 If gid is None, return the name of the current group."""
538 541
539 542 if gid is None:
540 543 gid = os.getgid()
541 544 try:
542 545 return grp.getgrgid(gid)[0]
543 546 except KeyError:
544 547 return str(gid)
545 548
546 549 def groupmembers(name):
547 550 """Return the list of members of the group with the given
548 551 name, KeyError if the group does not exist.
549 552 """
550 553 return list(grp.getgrnam(name).gr_mem)
551 554
552 555 def spawndetached(args):
553 556 return os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
554 557 args[0], args)
555 558
556 559 def gethgcmd():
557 560 return sys.argv[:1]
558 561
559 562 def makedir(path, notindexed):
560 563 os.mkdir(path)
561 564
562 565 def lookupreg(key, name=None, scope=None):
563 566 return None
564 567
565 568 def hidewindow():
566 569 """Hide current shell window.
567 570
568 571 Used to hide the window opened when starting asynchronous
569 572 child process under Windows, unneeded on other systems.
570 573 """
571 574 pass
572 575
573 576 class cachestat(object):
574 577 def __init__(self, path):
575 578 self.stat = os.stat(path)
576 579
577 580 def cacheable(self):
578 581 return bool(self.stat.st_ino)
579 582
580 583 __hash__ = object.__hash__
581 584
582 585 def __eq__(self, other):
583 586 try:
584 587 # Only dev, ino, size, mtime and atime are likely to change. Out
585 588 # of these, we shouldn't compare atime but should compare the
586 589 # rest. However, one of the other fields changing indicates
587 590 # something fishy going on, so return False if anything but atime
588 591 # changes.
589 592 return (self.stat.st_mode == other.stat.st_mode and
590 593 self.stat.st_ino == other.stat.st_ino and
591 594 self.stat.st_dev == other.stat.st_dev and
592 595 self.stat.st_nlink == other.stat.st_nlink and
593 596 self.stat.st_uid == other.stat.st_uid and
594 597 self.stat.st_gid == other.stat.st_gid and
595 598 self.stat.st_size == other.stat.st_size and
596 599 self.stat.st_mtime == other.stat.st_mtime and
597 600 self.stat.st_ctime == other.stat.st_ctime)
598 601 except AttributeError:
599 602 return False
600 603
601 604 def __ne__(self, other):
602 605 return not self == other
603 606
604 607 def executablepath():
605 608 return None # available on Windows only
606 609
607 610 def statislink(st):
608 611 '''check whether a stat result is a symlink'''
609 612 return st and stat.S_ISLNK(st.st_mode)
610 613
611 614 def statisexec(st):
612 615 '''check whether a stat result is an executable file'''
613 616 return st and (st.st_mode & 0o100 != 0)
614 617
615 618 def poll(fds):
616 619 """block until something happens on any file descriptor
617 620
618 621 This is a generic helper that will check for any activity
619 622 (read, write. exception) and return the list of touched files.
620 623
621 624 In unsupported cases, it will raise a NotImplementedError"""
622 625 try:
623 626 while True:
624 627 try:
625 628 res = select.select(fds, fds, fds)
626 629 break
627 630 except select.error as inst:
628 631 if inst.args[0] == errno.EINTR:
629 632 continue
630 633 raise
631 634 except ValueError: # out of range file descriptor
632 635 raise NotImplementedError()
633 636 return sorted(list(set(sum(res, []))))
634 637
635 638 def readpipe(pipe):
636 639 """Read all available data from a pipe."""
637 640 # We can't fstat() a pipe because Linux will always report 0.
638 641 # So, we set the pipe to non-blocking mode and read everything
639 642 # that's available.
640 643 flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
641 644 flags |= os.O_NONBLOCK
642 645 oldflags = fcntl.fcntl(pipe, fcntl.F_SETFL, flags)
643 646
644 647 try:
645 648 chunks = []
646 649 while True:
647 650 try:
648 651 s = pipe.read()
649 652 if not s:
650 653 break
651 654 chunks.append(s)
652 655 except IOError:
653 656 break
654 657
655 658 return ''.join(chunks)
656 659 finally:
657 660 fcntl.fcntl(pipe, fcntl.F_SETFL, oldflags)
658 661
659 662 def bindunixsocket(sock, path):
660 663 """Bind the UNIX domain socket to the specified path"""
661 664 # use relative path instead of full path at bind() if possible, since
662 665 # AF_UNIX path has very small length limit (107 chars) on common
663 666 # platforms (see sys/un.h)
664 667 dirname, basename = os.path.split(path)
665 668 bakwdfd = None
666 669 if dirname:
667 670 bakwdfd = os.open('.', os.O_DIRECTORY)
668 671 os.chdir(dirname)
669 672 sock.bind(basename)
670 673 if bakwdfd:
671 674 os.fchdir(bakwdfd)
672 675 os.close(bakwdfd)
@@ -1,371 +1,368
1 1 # sshpeer.py - ssh repository proxy class for mercurial
2 2 #
3 3 # Copyright 2005, 2006 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
9 9
10 10 import re
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 pycompat,
16 16 util,
17 17 wireproto,
18 18 )
19 19
20 20 class remotelock(object):
21 21 def __init__(self, repo):
22 22 self.repo = repo
23 23 def release(self):
24 24 self.repo.unlock()
25 25 self.repo = None
26 26 def __enter__(self):
27 27 return self
28 28 def __exit__(self, exc_type, exc_val, exc_tb):
29 29 if self.repo:
30 30 self.release()
31 31 def __del__(self):
32 32 if self.repo:
33 33 self.release()
34 34
35 35 def _serverquote(s):
36 36 if not s:
37 37 return s
38 38 '''quote a string for the remote shell ... which we assume is sh'''
39 39 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
40 40 return s
41 41 return "'%s'" % s.replace("'", "'\\''")
42 42
43 43 def _forwardoutput(ui, pipe):
44 44 """display all data currently available on pipe as remote output.
45 45
46 46 This is non blocking."""
47 47 s = util.readpipe(pipe)
48 48 if s:
49 49 for l in s.splitlines():
50 50 ui.status(_("remote: "), l, '\n')
51 51
52 52 class doublepipe(object):
53 53 """Operate a side-channel pipe in addition of a main one
54 54
55 55 The side-channel pipe contains server output to be forwarded to the user
56 56 input. The double pipe will behave as the "main" pipe, but will ensure the
57 57 content of the "side" pipe is properly processed while we wait for blocking
58 58 call on the "main" pipe.
59 59
60 60 If large amounts of data are read from "main", the forward will cease after
61 61 the first bytes start to appear. This simplifies the implementation
62 62 without affecting actual output of sshpeer too much as we rarely issue
63 63 large read for data not yet emitted by the server.
64 64
65 65 The main pipe is expected to be a 'bufferedinputpipe' from the util module
66 66 that handle all the os specific bits. This class lives in this module
67 67 because it focus on behavior specific to the ssh protocol."""
68 68
69 69 def __init__(self, ui, main, side):
70 70 self._ui = ui
71 71 self._main = main
72 72 self._side = side
73 73
74 74 def _wait(self):
75 75 """wait until some data are available on main or side
76 76
77 77 return a pair of boolean (ismainready, issideready)
78 78
79 79 (This will only wait for data if the setup is supported by `util.poll`)
80 80 """
81 81 if getattr(self._main, 'hasbuffer', False): # getattr for classic pipe
82 82 return (True, True) # main has data, assume side is worth poking at.
83 83 fds = [self._main.fileno(), self._side.fileno()]
84 84 try:
85 85 act = util.poll(fds)
86 86 except NotImplementedError:
87 87 # non supported yet case, assume all have data.
88 88 act = fds
89 89 return (self._main.fileno() in act, self._side.fileno() in act)
90 90
91 91 def write(self, data):
92 92 return self._call('write', data)
93 93
94 94 def read(self, size):
95 95 r = self._call('read', size)
96 96 if size != 0 and not r:
97 97 # We've observed a condition that indicates the
98 98 # stdout closed unexpectedly. Check stderr one
99 99 # more time and snag anything that's there before
100 100 # letting anyone know the main part of the pipe
101 101 # closed prematurely.
102 102 _forwardoutput(self._ui, self._side)
103 103 return r
104 104
105 105 def readline(self):
106 106 return self._call('readline')
107 107
108 108 def _call(self, methname, data=None):
109 109 """call <methname> on "main", forward output of "side" while blocking
110 110 """
111 111 # data can be '' or 0
112 112 if (data is not None and not data) or self._main.closed:
113 113 _forwardoutput(self._ui, self._side)
114 114 return ''
115 115 while True:
116 116 mainready, sideready = self._wait()
117 117 if sideready:
118 118 _forwardoutput(self._ui, self._side)
119 119 if mainready:
120 120 meth = getattr(self._main, methname)
121 121 if data is None:
122 122 return meth()
123 123 else:
124 124 return meth(data)
125 125
126 126 def close(self):
127 127 return self._main.close()
128 128
129 129 def flush(self):
130 130 return self._main.flush()
131 131
132 132 class sshpeer(wireproto.wirepeer):
133 133 def __init__(self, ui, path, create=False):
134 134 self._url = path
135 135 self.ui = ui
136 136 self.pipeo = self.pipei = self.pipee = None
137 137
138 138 u = util.url(path, parsequery=False, parsefragment=False)
139 139 if u.scheme != 'ssh' or not u.host or u.path is None:
140 140 self._abort(error.RepoError(_("couldn't parse location %s") % path))
141 141
142 142 util.checksafessh(path)
143 143
144 144 self.user = u.user
145 145 if u.passwd is not None:
146 146 self._abort(error.RepoError(_("password in URL not supported")))
147 147 self.host = u.host
148 148 self.port = u.port
149 149 self.path = u.path or "."
150 150
151 151 sshcmd = self.ui.config("ui", "ssh")
152 152 remotecmd = self.ui.config("ui", "remotecmd")
153 153
154 args = util.sshargs(sshcmd,
155 _serverquote(self.host),
156 _serverquote(self.user),
157 _serverquote(self.port))
154 args = util.sshargs(sshcmd, self.host, self.user, self.port)
158 155
159 156 if create:
160 157 cmd = '%s %s %s' % (sshcmd, args,
161 158 util.shellquote("%s init %s" %
162 159 (_serverquote(remotecmd), _serverquote(self.path))))
163 160 ui.debug('running %s\n' % cmd)
164 161 res = ui.system(cmd, blockedtag='sshpeer')
165 162 if res != 0:
166 163 self._abort(error.RepoError(_("could not create remote repo")))
167 164
168 165 self._validaterepo(sshcmd, args, remotecmd)
169 166
170 167 def url(self):
171 168 return self._url
172 169
173 170 def _validaterepo(self, sshcmd, args, remotecmd):
174 171 # cleanup up previous run
175 172 self.cleanup()
176 173
177 174 cmd = '%s %s %s' % (sshcmd, args,
178 175 util.shellquote("%s -R %s serve --stdio" %
179 176 (_serverquote(remotecmd), _serverquote(self.path))))
180 177 self.ui.debug('running %s\n' % cmd)
181 178 cmd = util.quotecommand(cmd)
182 179
183 180 # while self.subprocess isn't used, having it allows the subprocess to
184 181 # to clean up correctly later
185 182 #
186 183 # no buffer allow the use of 'select'
187 184 # feel free to remove buffering and select usage when we ultimately
188 185 # move to threading.
189 186 sub = util.popen4(cmd, bufsize=0)
190 187 self.pipeo, self.pipei, self.pipee, self.subprocess = sub
191 188
192 189 self.pipei = util.bufferedinputpipe(self.pipei)
193 190 self.pipei = doublepipe(self.ui, self.pipei, self.pipee)
194 191 self.pipeo = doublepipe(self.ui, self.pipeo, self.pipee)
195 192
196 193 # skip any noise generated by remote shell
197 194 self._callstream("hello")
198 195 r = self._callstream("between", pairs=("%s-%s" % ("0"*40, "0"*40)))
199 196 lines = ["", "dummy"]
200 197 max_noise = 500
201 198 while lines[-1] and max_noise:
202 199 l = r.readline()
203 200 self.readerr()
204 201 if lines[-1] == "1\n" and l == "\n":
205 202 break
206 203 if l:
207 204 self.ui.debug("remote: ", l)
208 205 lines.append(l)
209 206 max_noise -= 1
210 207 else:
211 208 self._abort(error.RepoError(_('no suitable response from '
212 209 'remote hg')))
213 210
214 211 self._caps = set()
215 212 for l in reversed(lines):
216 213 if l.startswith("capabilities:"):
217 214 self._caps.update(l[:-1].split(":")[1].split())
218 215 break
219 216
220 217 def _capabilities(self):
221 218 return self._caps
222 219
223 220 def readerr(self):
224 221 _forwardoutput(self.ui, self.pipee)
225 222
226 223 def _abort(self, exception):
227 224 self.cleanup()
228 225 raise exception
229 226
230 227 def cleanup(self):
231 228 if self.pipeo is None:
232 229 return
233 230 self.pipeo.close()
234 231 self.pipei.close()
235 232 try:
236 233 # read the error descriptor until EOF
237 234 for l in self.pipee:
238 235 self.ui.status(_("remote: "), l)
239 236 except (IOError, ValueError):
240 237 pass
241 238 self.pipee.close()
242 239
243 240 __del__ = cleanup
244 241
245 242 def _submitbatch(self, req):
246 243 rsp = self._callstream("batch", cmds=wireproto.encodebatchcmds(req))
247 244 available = self._getamount()
248 245 # TODO this response parsing is probably suboptimal for large
249 246 # batches with large responses.
250 247 toread = min(available, 1024)
251 248 work = rsp.read(toread)
252 249 available -= toread
253 250 chunk = work
254 251 while chunk:
255 252 while ';' in work:
256 253 one, work = work.split(';', 1)
257 254 yield wireproto.unescapearg(one)
258 255 toread = min(available, 1024)
259 256 chunk = rsp.read(toread)
260 257 available -= toread
261 258 work += chunk
262 259 yield wireproto.unescapearg(work)
263 260
264 261 def _callstream(self, cmd, **args):
265 262 args = pycompat.byteskwargs(args)
266 263 self.ui.debug("sending %s command\n" % cmd)
267 264 self.pipeo.write("%s\n" % cmd)
268 265 _func, names = wireproto.commands[cmd]
269 266 keys = names.split()
270 267 wireargs = {}
271 268 for k in keys:
272 269 if k == '*':
273 270 wireargs['*'] = args
274 271 break
275 272 else:
276 273 wireargs[k] = args[k]
277 274 del args[k]
278 275 for k, v in sorted(wireargs.iteritems()):
279 276 self.pipeo.write("%s %d\n" % (k, len(v)))
280 277 if isinstance(v, dict):
281 278 for dk, dv in v.iteritems():
282 279 self.pipeo.write("%s %d\n" % (dk, len(dv)))
283 280 self.pipeo.write(dv)
284 281 else:
285 282 self.pipeo.write(v)
286 283 self.pipeo.flush()
287 284
288 285 return self.pipei
289 286
290 287 def _callcompressable(self, cmd, **args):
291 288 return self._callstream(cmd, **args)
292 289
293 290 def _call(self, cmd, **args):
294 291 self._callstream(cmd, **args)
295 292 return self._recv()
296 293
297 294 def _callpush(self, cmd, fp, **args):
298 295 r = self._call(cmd, **args)
299 296 if r:
300 297 return '', r
301 298 for d in iter(lambda: fp.read(4096), ''):
302 299 self._send(d)
303 300 self._send("", flush=True)
304 301 r = self._recv()
305 302 if r:
306 303 return '', r
307 304 return self._recv(), ''
308 305
309 306 def _calltwowaystream(self, cmd, fp, **args):
310 307 r = self._call(cmd, **args)
311 308 if r:
312 309 # XXX needs to be made better
313 310 raise error.Abort(_('unexpected remote reply: %s') % r)
314 311 for d in iter(lambda: fp.read(4096), ''):
315 312 self._send(d)
316 313 self._send("", flush=True)
317 314 return self.pipei
318 315
319 316 def _getamount(self):
320 317 l = self.pipei.readline()
321 318 if l == '\n':
322 319 self.readerr()
323 320 msg = _('check previous remote output')
324 321 self._abort(error.OutOfBandError(hint=msg))
325 322 self.readerr()
326 323 try:
327 324 return int(l)
328 325 except ValueError:
329 326 self._abort(error.ResponseError(_("unexpected response:"), l))
330 327
331 328 def _recv(self):
332 329 return self.pipei.read(self._getamount())
333 330
334 331 def _send(self, data, flush=False):
335 332 self.pipeo.write("%d\n" % len(data))
336 333 if data:
337 334 self.pipeo.write(data)
338 335 if flush:
339 336 self.pipeo.flush()
340 337 self.readerr()
341 338
342 339 def lock(self):
343 340 self._call("lock")
344 341 return remotelock(self)
345 342
346 343 def unlock(self):
347 344 self._call("unlock")
348 345
349 346 def addchangegroup(self, cg, source, url, lock=None):
350 347 '''Send a changegroup to the remote server. Return an integer
351 348 similar to unbundle(). DEPRECATED, since it requires locking the
352 349 remote.'''
353 350 d = self._call("addchangegroup")
354 351 if d:
355 352 self._abort(error.RepoError(_("push refused: %s") % d))
356 353 for d in iter(lambda: cg.read(4096), ''):
357 354 self.pipeo.write(d)
358 355 self.readerr()
359 356
360 357 self.pipeo.flush()
361 358
362 359 self.readerr()
363 360 r = self._recv()
364 361 if not r:
365 362 return 1
366 363 try:
367 364 return int(r)
368 365 except ValueError:
369 366 self._abort(error.ResponseError(_("unexpected response:"), r))
370 367
371 368 instance = sshpeer
@@ -1,481 +1,484
1 1 # windows.py - Windows utility function implementations for Mercurial
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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
9 9
10 10 import errno
11 11 import msvcrt
12 12 import os
13 13 import re
14 14 import stat
15 15 import sys
16 16
17 17 from .i18n import _
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 policy,
22 22 pycompat,
23 23 win32,
24 24 )
25 25
26 26 try:
27 27 import _winreg as winreg
28 28 winreg.CloseKey
29 29 except ImportError:
30 30 import winreg
31 31
32 32 osutil = policy.importmod(r'osutil')
33 33
34 34 executablepath = win32.executablepath
35 35 getuser = win32.getuser
36 36 hidewindow = win32.hidewindow
37 37 makedir = win32.makedir
38 38 nlinks = win32.nlinks
39 39 oslink = win32.oslink
40 40 samedevice = win32.samedevice
41 41 samefile = win32.samefile
42 42 setsignalhandler = win32.setsignalhandler
43 43 spawndetached = win32.spawndetached
44 44 split = os.path.split
45 45 testpid = win32.testpid
46 46 unlink = win32.unlink
47 47
48 48 umask = 0o022
49 49
50 50 class mixedfilemodewrapper(object):
51 51 """Wraps a file handle when it is opened in read/write mode.
52 52
53 53 fopen() and fdopen() on Windows have a specific-to-Windows requirement
54 54 that files opened with mode r+, w+, or a+ make a call to a file positioning
55 55 function when switching between reads and writes. Without this extra call,
56 56 Python will raise a not very intuitive "IOError: [Errno 0] Error."
57 57
58 58 This class wraps posixfile instances when the file is opened in read/write
59 59 mode and automatically adds checks or inserts appropriate file positioning
60 60 calls when necessary.
61 61 """
62 62 OPNONE = 0
63 63 OPREAD = 1
64 64 OPWRITE = 2
65 65
66 66 def __init__(self, fp):
67 67 object.__setattr__(self, r'_fp', fp)
68 68 object.__setattr__(self, r'_lastop', 0)
69 69
70 70 def __enter__(self):
71 71 return self._fp.__enter__()
72 72
73 73 def __exit__(self, exc_type, exc_val, exc_tb):
74 74 self._fp.__exit__(exc_type, exc_val, exc_tb)
75 75
76 76 def __getattr__(self, name):
77 77 return getattr(self._fp, name)
78 78
79 79 def __setattr__(self, name, value):
80 80 return self._fp.__setattr__(name, value)
81 81
82 82 def _noopseek(self):
83 83 self._fp.seek(0, os.SEEK_CUR)
84 84
85 85 def seek(self, *args, **kwargs):
86 86 object.__setattr__(self, r'_lastop', self.OPNONE)
87 87 return self._fp.seek(*args, **kwargs)
88 88
89 89 def write(self, d):
90 90 if self._lastop == self.OPREAD:
91 91 self._noopseek()
92 92
93 93 object.__setattr__(self, r'_lastop', self.OPWRITE)
94 94 return self._fp.write(d)
95 95
96 96 def writelines(self, *args, **kwargs):
97 97 if self._lastop == self.OPREAD:
98 98 self._noopeseek()
99 99
100 100 object.__setattr__(self, r'_lastop', self.OPWRITE)
101 101 return self._fp.writelines(*args, **kwargs)
102 102
103 103 def read(self, *args, **kwargs):
104 104 if self._lastop == self.OPWRITE:
105 105 self._noopseek()
106 106
107 107 object.__setattr__(self, r'_lastop', self.OPREAD)
108 108 return self._fp.read(*args, **kwargs)
109 109
110 110 def readline(self, *args, **kwargs):
111 111 if self._lastop == self.OPWRITE:
112 112 self._noopseek()
113 113
114 114 object.__setattr__(self, r'_lastop', self.OPREAD)
115 115 return self._fp.readline(*args, **kwargs)
116 116
117 117 def readlines(self, *args, **kwargs):
118 118 if self._lastop == self.OPWRITE:
119 119 self._noopseek()
120 120
121 121 object.__setattr__(self, r'_lastop', self.OPREAD)
122 122 return self._fp.readlines(*args, **kwargs)
123 123
124 124 def posixfile(name, mode='r', buffering=-1):
125 125 '''Open a file with even more POSIX-like semantics'''
126 126 try:
127 127 fp = osutil.posixfile(name, mode, buffering) # may raise WindowsError
128 128
129 129 # The position when opening in append mode is implementation defined, so
130 130 # make it consistent with other platforms, which position at EOF.
131 131 if 'a' in mode:
132 132 fp.seek(0, os.SEEK_END)
133 133
134 134 if '+' in mode:
135 135 return mixedfilemodewrapper(fp)
136 136
137 137 return fp
138 138 except WindowsError as err:
139 139 # convert to a friendlier exception
140 140 raise IOError(err.errno, '%s: %s' % (name, err.strerror))
141 141
142 142 # may be wrapped by win32mbcs extension
143 143 listdir = osutil.listdir
144 144
145 145 class winstdout(object):
146 146 '''stdout on windows misbehaves if sent through a pipe'''
147 147
148 148 def __init__(self, fp):
149 149 self.fp = fp
150 150
151 151 def __getattr__(self, key):
152 152 return getattr(self.fp, key)
153 153
154 154 def close(self):
155 155 try:
156 156 self.fp.close()
157 157 except IOError:
158 158 pass
159 159
160 160 def write(self, s):
161 161 try:
162 162 # This is workaround for "Not enough space" error on
163 163 # writing large size of data to console.
164 164 limit = 16000
165 165 l = len(s)
166 166 start = 0
167 167 self.softspace = 0
168 168 while start < l:
169 169 end = start + limit
170 170 self.fp.write(s[start:end])
171 171 start = end
172 172 except IOError as inst:
173 173 if inst.errno != 0:
174 174 raise
175 175 self.close()
176 176 raise IOError(errno.EPIPE, 'Broken pipe')
177 177
178 178 def flush(self):
179 179 try:
180 180 return self.fp.flush()
181 181 except IOError as inst:
182 182 if inst.errno != errno.EINVAL:
183 183 raise
184 184 raise IOError(errno.EPIPE, 'Broken pipe')
185 185
186 186 def _is_win_9x():
187 187 '''return true if run on windows 95, 98 or me.'''
188 188 try:
189 189 return sys.getwindowsversion()[3] == 1
190 190 except AttributeError:
191 191 return 'command' in encoding.environ.get('comspec', '')
192 192
193 193 def openhardlinks():
194 194 return not _is_win_9x()
195 195
196 196 def parsepatchoutput(output_line):
197 197 """parses the output produced by patch and returns the filename"""
198 198 pf = output_line[14:]
199 199 if pf[0] == '`':
200 200 pf = pf[1:-1] # Remove the quotes
201 201 return pf
202 202
203 203 def sshargs(sshcmd, host, user, port):
204 204 '''Build argument list for ssh or Plink'''
205 205 pflag = 'plink' in sshcmd.lower() and '-P' or '-p'
206 206 args = user and ("%s@%s" % (user, host)) or host
207 207 if args.startswith('-') or args.startswith('/'):
208 208 raise error.Abort(
209 209 _('illegal ssh hostname or username starting with - or /: %s') %
210 210 args)
211 return port and ("%s %s %s" % (args, pflag, port)) or args
211 args = shellquote(args)
212 if port:
213 args = '%s %s %s' % (pflag, shellquote(port), args)
214 return args
212 215
213 216 def setflags(f, l, x):
214 217 pass
215 218
216 219 def copymode(src, dst, mode=None):
217 220 pass
218 221
219 222 def checkexec(path):
220 223 return False
221 224
222 225 def checklink(path):
223 226 return False
224 227
225 228 def setbinary(fd):
226 229 # When run without console, pipes may expose invalid
227 230 # fileno(), usually set to -1.
228 231 fno = getattr(fd, 'fileno', None)
229 232 if fno is not None and fno() >= 0:
230 233 msvcrt.setmode(fno(), os.O_BINARY)
231 234
232 235 def pconvert(path):
233 236 return path.replace(pycompat.ossep, '/')
234 237
235 238 def localpath(path):
236 239 return path.replace('/', '\\')
237 240
238 241 def normpath(path):
239 242 return pconvert(os.path.normpath(path))
240 243
241 244 def normcase(path):
242 245 return encoding.upper(path) # NTFS compares via upper()
243 246
244 247 # see posix.py for definitions
245 248 normcasespec = encoding.normcasespecs.upper
246 249 normcasefallback = encoding.upperfallback
247 250
248 251 def samestat(s1, s2):
249 252 return False
250 253
251 254 # A sequence of backslashes is special iff it precedes a double quote:
252 255 # - if there's an even number of backslashes, the double quote is not
253 256 # quoted (i.e. it ends the quoted region)
254 257 # - if there's an odd number of backslashes, the double quote is quoted
255 258 # - in both cases, every pair of backslashes is unquoted into a single
256 259 # backslash
257 260 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
258 261 # So, to quote a string, we must surround it in double quotes, double
259 262 # the number of backslashes that precede double quotes and add another
260 263 # backslash before every double quote (being careful with the double
261 264 # quote we've appended to the end)
262 265 _quotere = None
263 266 _needsshellquote = None
264 267 def shellquote(s):
265 268 r"""
266 269 >>> shellquote(r'C:\Users\xyz')
267 270 '"C:\\Users\\xyz"'
268 271 >>> shellquote(r'C:\Users\xyz/mixed')
269 272 '"C:\\Users\\xyz/mixed"'
270 273 >>> # Would be safe not to quote too, since it is all double backslashes
271 274 >>> shellquote(r'C:\\Users\\xyz')
272 275 '"C:\\\\Users\\\\xyz"'
273 276 >>> # But this must be quoted
274 277 >>> shellquote(r'C:\\Users\\xyz/abc')
275 278 '"C:\\\\Users\\\\xyz/abc"'
276 279 """
277 280 global _quotere
278 281 if _quotere is None:
279 282 _quotere = re.compile(r'(\\*)("|\\$)')
280 283 global _needsshellquote
281 284 if _needsshellquote is None:
282 285 # ":" is also treated as "safe character", because it is used as a part
283 286 # of path name on Windows. "\" is also part of a path name, but isn't
284 287 # safe because shlex.split() (kind of) treats it as an escape char and
285 288 # drops it. It will leave the next character, even if it is another
286 289 # "\".
287 290 _needsshellquote = re.compile(r'[^a-zA-Z0-9._:/-]').search
288 291 if s and not _needsshellquote(s) and not _quotere.search(s):
289 292 # "s" shouldn't have to be quoted
290 293 return s
291 294 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
292 295
293 296 def quotecommand(cmd):
294 297 """Build a command string suitable for os.popen* calls."""
295 298 if sys.version_info < (2, 7, 1):
296 299 # Python versions since 2.7.1 do this extra quoting themselves
297 300 return '"' + cmd + '"'
298 301 return cmd
299 302
300 303 def popen(command, mode='r'):
301 304 # Work around "popen spawned process may not write to stdout
302 305 # under windows"
303 306 # http://bugs.python.org/issue1366
304 307 command += " 2> %s" % os.devnull
305 308 return os.popen(quotecommand(command), mode)
306 309
307 310 def explainexit(code):
308 311 return _("exited with status %d") % code, code
309 312
310 313 # if you change this stub into a real check, please try to implement the
311 314 # username and groupname functions above, too.
312 315 def isowner(st):
313 316 return True
314 317
315 318 def findexe(command):
316 319 '''Find executable for command searching like cmd.exe does.
317 320 If command is a basename then PATH is searched for command.
318 321 PATH isn't searched if command is an absolute or relative path.
319 322 An extension from PATHEXT is found and added if not present.
320 323 If command isn't found None is returned.'''
321 324 pathext = encoding.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD')
322 325 pathexts = [ext for ext in pathext.lower().split(pycompat.ospathsep)]
323 326 if os.path.splitext(command)[1].lower() in pathexts:
324 327 pathexts = ['']
325 328
326 329 def findexisting(pathcommand):
327 330 'Will append extension (if needed) and return existing file'
328 331 for ext in pathexts:
329 332 executable = pathcommand + ext
330 333 if os.path.exists(executable):
331 334 return executable
332 335 return None
333 336
334 337 if pycompat.ossep in command:
335 338 return findexisting(command)
336 339
337 340 for path in encoding.environ.get('PATH', '').split(pycompat.ospathsep):
338 341 executable = findexisting(os.path.join(path, command))
339 342 if executable is not None:
340 343 return executable
341 344 return findexisting(os.path.expanduser(os.path.expandvars(command)))
342 345
343 346 _wantedkinds = {stat.S_IFREG, stat.S_IFLNK}
344 347
345 348 def statfiles(files):
346 349 '''Stat each file in files. Yield each stat, or None if a file
347 350 does not exist or has a type we don't care about.
348 351
349 352 Cluster and cache stat per directory to minimize number of OS stat calls.'''
350 353 dircache = {} # dirname -> filename -> status | None if file does not exist
351 354 getkind = stat.S_IFMT
352 355 for nf in files:
353 356 nf = normcase(nf)
354 357 dir, base = os.path.split(nf)
355 358 if not dir:
356 359 dir = '.'
357 360 cache = dircache.get(dir, None)
358 361 if cache is None:
359 362 try:
360 363 dmap = dict([(normcase(n), s)
361 364 for n, k, s in listdir(dir, True)
362 365 if getkind(s.st_mode) in _wantedkinds])
363 366 except OSError as err:
364 367 # Python >= 2.5 returns ENOENT and adds winerror field
365 368 # EINVAL is raised if dir is not a directory.
366 369 if err.errno not in (errno.ENOENT, errno.EINVAL,
367 370 errno.ENOTDIR):
368 371 raise
369 372 dmap = {}
370 373 cache = dircache.setdefault(dir, dmap)
371 374 yield cache.get(base, None)
372 375
373 376 def username(uid=None):
374 377 """Return the name of the user with the given uid.
375 378
376 379 If uid is None, return the name of the current user."""
377 380 return None
378 381
379 382 def groupname(gid=None):
380 383 """Return the name of the group with the given gid.
381 384
382 385 If gid is None, return the name of the current group."""
383 386 return None
384 387
385 388 def removedirs(name):
386 389 """special version of os.removedirs that does not remove symlinked
387 390 directories or junction points if they actually contain files"""
388 391 if listdir(name):
389 392 return
390 393 os.rmdir(name)
391 394 head, tail = os.path.split(name)
392 395 if not tail:
393 396 head, tail = os.path.split(head)
394 397 while head and tail:
395 398 try:
396 399 if listdir(head):
397 400 return
398 401 os.rmdir(head)
399 402 except (ValueError, OSError):
400 403 break
401 404 head, tail = os.path.split(head)
402 405
403 406 def rename(src, dst):
404 407 '''atomically rename file src to dst, replacing dst if it exists'''
405 408 try:
406 409 os.rename(src, dst)
407 410 except OSError as e:
408 411 if e.errno != errno.EEXIST:
409 412 raise
410 413 unlink(dst)
411 414 os.rename(src, dst)
412 415
413 416 def gethgcmd():
414 417 return [sys.executable] + sys.argv[:1]
415 418
416 419 def groupmembers(name):
417 420 # Don't support groups on Windows for now
418 421 raise KeyError
419 422
420 423 def isexec(f):
421 424 return False
422 425
423 426 class cachestat(object):
424 427 def __init__(self, path):
425 428 pass
426 429
427 430 def cacheable(self):
428 431 return False
429 432
430 433 def lookupreg(key, valname=None, scope=None):
431 434 ''' Look up a key/value name in the Windows registry.
432 435
433 436 valname: value name. If unspecified, the default value for the key
434 437 is used.
435 438 scope: optionally specify scope for registry lookup, this can be
436 439 a sequence of scopes to look up in order. Default (CURRENT_USER,
437 440 LOCAL_MACHINE).
438 441 '''
439 442 if scope is None:
440 443 scope = (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE)
441 444 elif not isinstance(scope, (list, tuple)):
442 445 scope = (scope,)
443 446 for s in scope:
444 447 try:
445 448 val = winreg.QueryValueEx(winreg.OpenKey(s, key), valname)[0]
446 449 # never let a Unicode string escape into the wild
447 450 return encoding.unitolocal(val)
448 451 except EnvironmentError:
449 452 pass
450 453
451 454 expandglobs = True
452 455
453 456 def statislink(st):
454 457 '''check whether a stat result is a symlink'''
455 458 return False
456 459
457 460 def statisexec(st):
458 461 '''check whether a stat result is an executable file'''
459 462 return False
460 463
461 464 def poll(fds):
462 465 # see posix.py for description
463 466 raise NotImplementedError()
464 467
465 468 def readpipe(pipe):
466 469 """Read all available data from a pipe."""
467 470 chunks = []
468 471 while True:
469 472 size = win32.peekpipe(pipe)
470 473 if not size:
471 474 break
472 475
473 476 s = pipe.read(size)
474 477 if not s:
475 478 break
476 479 chunks.append(s)
477 480
478 481 return ''.join(chunks)
479 482
480 483 def bindunixsocket(sock, path):
481 484 raise NotImplementedError('unsupported platform')
@@ -1,1121 +1,1162
1 1 Prepare repo a:
2 2
3 3 $ hg init a
4 4 $ cd a
5 5 $ echo a > a
6 6 $ hg add a
7 7 $ hg commit -m test
8 8 $ echo first line > b
9 9 $ hg add b
10 10
11 11 Create a non-inlined filelog:
12 12
13 13 $ $PYTHON -c 'file("data1", "wb").write("".join("%s\n" % x for x in range(10000)))'
14 14 $ for j in 0 1 2 3 4 5 6 7 8 9; do
15 15 > cat data1 >> b
16 16 > hg commit -m test
17 17 > done
18 18
19 19 List files in store/data (should show a 'b.d'):
20 20
21 21 $ for i in .hg/store/data/*; do
22 22 > echo $i
23 23 > done
24 24 .hg/store/data/a.i
25 25 .hg/store/data/b.d
26 26 .hg/store/data/b.i
27 27
28 28 Trigger branchcache creation:
29 29
30 30 $ hg branches
31 31 default 10:a7949464abda
32 32 $ ls .hg/cache
33 33 branch2-served
34 34 checkisexec (execbit !)
35 35 checklink (symlink !)
36 36 checklink-target (symlink !)
37 37 checknoexec (execbit !)
38 38 rbc-names-v1
39 39 rbc-revs-v1
40 40
41 41 Default operation:
42 42
43 43 $ hg clone . ../b
44 44 updating to branch default
45 45 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
46 46 $ cd ../b
47 47
48 48 Ensure branchcache got copied over:
49 49
50 50 $ ls .hg/cache
51 51 branch2-served
52 52 checkisexec (execbit !)
53 53 checklink (symlink !)
54 54 checklink-target (symlink !)
55 55 rbc-names-v1
56 56 rbc-revs-v1
57 57
58 58 $ cat a
59 59 a
60 60 $ hg verify
61 61 checking changesets
62 62 checking manifests
63 63 crosschecking files in changesets and manifests
64 64 checking files
65 65 2 files, 11 changesets, 11 total revisions
66 66
67 67 Invalid dest '' must abort:
68 68
69 69 $ hg clone . ''
70 70 abort: empty destination path is not valid
71 71 [255]
72 72
73 73 No update, with debug option:
74 74
75 75 #if hardlink
76 76 $ hg --debug clone -U . ../c --config progress.debug=true
77 77 linking: 1
78 78 linking: 2
79 79 linking: 3
80 80 linking: 4
81 81 linking: 5
82 82 linking: 6
83 83 linking: 7
84 84 linking: 8
85 85 linked 8 files
86 86 #else
87 87 $ hg --debug clone -U . ../c --config progress.debug=true
88 88 linking: 1
89 89 copying: 2
90 90 copying: 3
91 91 copying: 4
92 92 copying: 5
93 93 copying: 6
94 94 copying: 7
95 95 copying: 8
96 96 copied 8 files
97 97 #endif
98 98 $ cd ../c
99 99
100 100 Ensure branchcache got copied over:
101 101
102 102 $ ls .hg/cache
103 103 branch2-served
104 104 rbc-names-v1
105 105 rbc-revs-v1
106 106
107 107 $ cat a 2>/dev/null || echo "a not present"
108 108 a not present
109 109 $ hg verify
110 110 checking changesets
111 111 checking manifests
112 112 crosschecking files in changesets and manifests
113 113 checking files
114 114 2 files, 11 changesets, 11 total revisions
115 115
116 116 Default destination:
117 117
118 118 $ mkdir ../d
119 119 $ cd ../d
120 120 $ hg clone ../a
121 121 destination directory: a
122 122 updating to branch default
123 123 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
124 124 $ cd a
125 125 $ hg cat a
126 126 a
127 127 $ cd ../..
128 128
129 129 Check that we drop the 'file:' from the path before writing the .hgrc:
130 130
131 131 $ hg clone file:a e
132 132 updating to branch default
133 133 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
134 134 $ grep 'file:' e/.hg/hgrc
135 135 [1]
136 136
137 137 Check that path aliases are expanded:
138 138
139 139 $ hg clone -q -U --config 'paths.foobar=a#0' foobar f
140 140 $ hg -R f showconfig paths.default
141 141 $TESTTMP/a#0 (glob)
142 142
143 143 Use --pull:
144 144
145 145 $ hg clone --pull a g
146 146 requesting all changes
147 147 adding changesets
148 148 adding manifests
149 149 adding file changes
150 150 added 11 changesets with 11 changes to 2 files
151 151 updating to branch default
152 152 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
153 153 $ hg -R g verify
154 154 checking changesets
155 155 checking manifests
156 156 crosschecking files in changesets and manifests
157 157 checking files
158 158 2 files, 11 changesets, 11 total revisions
159 159
160 160 Invalid dest '' with --pull must abort (issue2528):
161 161
162 162 $ hg clone --pull a ''
163 163 abort: empty destination path is not valid
164 164 [255]
165 165
166 166 Clone to '.':
167 167
168 168 $ mkdir h
169 169 $ cd h
170 170 $ hg clone ../a .
171 171 updating to branch default
172 172 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
173 173 $ cd ..
174 174
175 175
176 176 *** Tests for option -u ***
177 177
178 178 Adding some more history to repo a:
179 179
180 180 $ cd a
181 181 $ hg tag ref1
182 182 $ echo the quick brown fox >a
183 183 $ hg ci -m "hacked default"
184 184 $ hg up ref1
185 185 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
186 186 $ hg branch stable
187 187 marked working directory as branch stable
188 188 (branches are permanent and global, did you want a bookmark?)
189 189 $ echo some text >a
190 190 $ hg ci -m "starting branch stable"
191 191 $ hg tag ref2
192 192 $ echo some more text >a
193 193 $ hg ci -m "another change for branch stable"
194 194 $ hg up ref2
195 195 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
196 196 $ hg parents
197 197 changeset: 13:e8ece76546a6
198 198 branch: stable
199 199 tag: ref2
200 200 parent: 10:a7949464abda
201 201 user: test
202 202 date: Thu Jan 01 00:00:00 1970 +0000
203 203 summary: starting branch stable
204 204
205 205
206 206 Repo a has two heads:
207 207
208 208 $ hg heads
209 209 changeset: 15:0aae7cf88f0d
210 210 branch: stable
211 211 tag: tip
212 212 user: test
213 213 date: Thu Jan 01 00:00:00 1970 +0000
214 214 summary: another change for branch stable
215 215
216 216 changeset: 12:f21241060d6a
217 217 user: test
218 218 date: Thu Jan 01 00:00:00 1970 +0000
219 219 summary: hacked default
220 220
221 221
222 222 $ cd ..
223 223
224 224
225 225 Testing --noupdate with --updaterev (must abort):
226 226
227 227 $ hg clone --noupdate --updaterev 1 a ua
228 228 abort: cannot specify both --noupdate and --updaterev
229 229 [255]
230 230
231 231
232 232 Testing clone -u:
233 233
234 234 $ hg clone -u . a ua
235 235 updating to branch stable
236 236 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
237 237
238 238 Repo ua has both heads:
239 239
240 240 $ hg -R ua heads
241 241 changeset: 15:0aae7cf88f0d
242 242 branch: stable
243 243 tag: tip
244 244 user: test
245 245 date: Thu Jan 01 00:00:00 1970 +0000
246 246 summary: another change for branch stable
247 247
248 248 changeset: 12:f21241060d6a
249 249 user: test
250 250 date: Thu Jan 01 00:00:00 1970 +0000
251 251 summary: hacked default
252 252
253 253
254 254 Same revision checked out in repo a and ua:
255 255
256 256 $ hg -R a parents --template "{node|short}\n"
257 257 e8ece76546a6
258 258 $ hg -R ua parents --template "{node|short}\n"
259 259 e8ece76546a6
260 260
261 261 $ rm -r ua
262 262
263 263
264 264 Testing clone --pull -u:
265 265
266 266 $ hg clone --pull -u . a ua
267 267 requesting all changes
268 268 adding changesets
269 269 adding manifests
270 270 adding file changes
271 271 added 16 changesets with 16 changes to 3 files (+1 heads)
272 272 updating to branch stable
273 273 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
274 274
275 275 Repo ua has both heads:
276 276
277 277 $ hg -R ua heads
278 278 changeset: 15:0aae7cf88f0d
279 279 branch: stable
280 280 tag: tip
281 281 user: test
282 282 date: Thu Jan 01 00:00:00 1970 +0000
283 283 summary: another change for branch stable
284 284
285 285 changeset: 12:f21241060d6a
286 286 user: test
287 287 date: Thu Jan 01 00:00:00 1970 +0000
288 288 summary: hacked default
289 289
290 290
291 291 Same revision checked out in repo a and ua:
292 292
293 293 $ hg -R a parents --template "{node|short}\n"
294 294 e8ece76546a6
295 295 $ hg -R ua parents --template "{node|short}\n"
296 296 e8ece76546a6
297 297
298 298 $ rm -r ua
299 299
300 300
301 301 Testing clone -u <branch>:
302 302
303 303 $ hg clone -u stable a ua
304 304 updating to branch stable
305 305 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
306 306
307 307 Repo ua has both heads:
308 308
309 309 $ hg -R ua heads
310 310 changeset: 15:0aae7cf88f0d
311 311 branch: stable
312 312 tag: tip
313 313 user: test
314 314 date: Thu Jan 01 00:00:00 1970 +0000
315 315 summary: another change for branch stable
316 316
317 317 changeset: 12:f21241060d6a
318 318 user: test
319 319 date: Thu Jan 01 00:00:00 1970 +0000
320 320 summary: hacked default
321 321
322 322
323 323 Branch 'stable' is checked out:
324 324
325 325 $ hg -R ua parents
326 326 changeset: 15:0aae7cf88f0d
327 327 branch: stable
328 328 tag: tip
329 329 user: test
330 330 date: Thu Jan 01 00:00:00 1970 +0000
331 331 summary: another change for branch stable
332 332
333 333
334 334 $ rm -r ua
335 335
336 336
337 337 Testing default checkout:
338 338
339 339 $ hg clone a ua
340 340 updating to branch default
341 341 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
342 342
343 343 Repo ua has both heads:
344 344
345 345 $ hg -R ua heads
346 346 changeset: 15:0aae7cf88f0d
347 347 branch: stable
348 348 tag: tip
349 349 user: test
350 350 date: Thu Jan 01 00:00:00 1970 +0000
351 351 summary: another change for branch stable
352 352
353 353 changeset: 12:f21241060d6a
354 354 user: test
355 355 date: Thu Jan 01 00:00:00 1970 +0000
356 356 summary: hacked default
357 357
358 358
359 359 Branch 'default' is checked out:
360 360
361 361 $ hg -R ua parents
362 362 changeset: 12:f21241060d6a
363 363 user: test
364 364 date: Thu Jan 01 00:00:00 1970 +0000
365 365 summary: hacked default
366 366
367 367 Test clone with a branch named "@" (issue3677)
368 368
369 369 $ hg -R ua branch @
370 370 marked working directory as branch @
371 371 $ hg -R ua commit -m 'created branch @'
372 372 $ hg clone ua atbranch
373 373 updating to branch default
374 374 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
375 375 $ hg -R atbranch heads
376 376 changeset: 16:798b6d97153e
377 377 branch: @
378 378 tag: tip
379 379 parent: 12:f21241060d6a
380 380 user: test
381 381 date: Thu Jan 01 00:00:00 1970 +0000
382 382 summary: created branch @
383 383
384 384 changeset: 15:0aae7cf88f0d
385 385 branch: stable
386 386 user: test
387 387 date: Thu Jan 01 00:00:00 1970 +0000
388 388 summary: another change for branch stable
389 389
390 390 changeset: 12:f21241060d6a
391 391 user: test
392 392 date: Thu Jan 01 00:00:00 1970 +0000
393 393 summary: hacked default
394 394
395 395 $ hg -R atbranch parents
396 396 changeset: 12:f21241060d6a
397 397 user: test
398 398 date: Thu Jan 01 00:00:00 1970 +0000
399 399 summary: hacked default
400 400
401 401
402 402 $ rm -r ua atbranch
403 403
404 404
405 405 Testing #<branch>:
406 406
407 407 $ hg clone -u . a#stable ua
408 408 adding changesets
409 409 adding manifests
410 410 adding file changes
411 411 added 14 changesets with 14 changes to 3 files
412 412 updating to branch stable
413 413 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
414 414
415 415 Repo ua has branch 'stable' and 'default' (was changed in fd511e9eeea6):
416 416
417 417 $ hg -R ua heads
418 418 changeset: 13:0aae7cf88f0d
419 419 branch: stable
420 420 tag: tip
421 421 user: test
422 422 date: Thu Jan 01 00:00:00 1970 +0000
423 423 summary: another change for branch stable
424 424
425 425 changeset: 10:a7949464abda
426 426 user: test
427 427 date: Thu Jan 01 00:00:00 1970 +0000
428 428 summary: test
429 429
430 430
431 431 Same revision checked out in repo a and ua:
432 432
433 433 $ hg -R a parents --template "{node|short}\n"
434 434 e8ece76546a6
435 435 $ hg -R ua parents --template "{node|short}\n"
436 436 e8ece76546a6
437 437
438 438 $ rm -r ua
439 439
440 440
441 441 Testing -u -r <branch>:
442 442
443 443 $ hg clone -u . -r stable a ua
444 444 adding changesets
445 445 adding manifests
446 446 adding file changes
447 447 added 14 changesets with 14 changes to 3 files
448 448 updating to branch stable
449 449 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
450 450
451 451 Repo ua has branch 'stable' and 'default' (was changed in fd511e9eeea6):
452 452
453 453 $ hg -R ua heads
454 454 changeset: 13:0aae7cf88f0d
455 455 branch: stable
456 456 tag: tip
457 457 user: test
458 458 date: Thu Jan 01 00:00:00 1970 +0000
459 459 summary: another change for branch stable
460 460
461 461 changeset: 10:a7949464abda
462 462 user: test
463 463 date: Thu Jan 01 00:00:00 1970 +0000
464 464 summary: test
465 465
466 466
467 467 Same revision checked out in repo a and ua:
468 468
469 469 $ hg -R a parents --template "{node|short}\n"
470 470 e8ece76546a6
471 471 $ hg -R ua parents --template "{node|short}\n"
472 472 e8ece76546a6
473 473
474 474 $ rm -r ua
475 475
476 476
477 477 Testing -r <branch>:
478 478
479 479 $ hg clone -r stable a ua
480 480 adding changesets
481 481 adding manifests
482 482 adding file changes
483 483 added 14 changesets with 14 changes to 3 files
484 484 updating to branch stable
485 485 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
486 486
487 487 Repo ua has branch 'stable' and 'default' (was changed in fd511e9eeea6):
488 488
489 489 $ hg -R ua heads
490 490 changeset: 13:0aae7cf88f0d
491 491 branch: stable
492 492 tag: tip
493 493 user: test
494 494 date: Thu Jan 01 00:00:00 1970 +0000
495 495 summary: another change for branch stable
496 496
497 497 changeset: 10:a7949464abda
498 498 user: test
499 499 date: Thu Jan 01 00:00:00 1970 +0000
500 500 summary: test
501 501
502 502
503 503 Branch 'stable' is checked out:
504 504
505 505 $ hg -R ua parents
506 506 changeset: 13:0aae7cf88f0d
507 507 branch: stable
508 508 tag: tip
509 509 user: test
510 510 date: Thu Jan 01 00:00:00 1970 +0000
511 511 summary: another change for branch stable
512 512
513 513
514 514 $ rm -r ua
515 515
516 516
517 517 Issue2267: Error in 1.6 hg.py: TypeError: 'NoneType' object is not
518 518 iterable in addbranchrevs()
519 519
520 520 $ cat <<EOF > simpleclone.py
521 521 > from mercurial import ui, hg
522 522 > myui = ui.ui.load()
523 523 > repo = hg.repository(myui, 'a')
524 524 > hg.clone(myui, {}, repo, dest="ua")
525 525 > EOF
526 526
527 527 $ $PYTHON simpleclone.py
528 528 updating to branch default
529 529 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
530 530
531 531 $ rm -r ua
532 532
533 533 $ cat <<EOF > branchclone.py
534 534 > from mercurial import ui, hg, extensions
535 535 > myui = ui.ui.load()
536 536 > extensions.loadall(myui)
537 537 > repo = hg.repository(myui, 'a')
538 538 > hg.clone(myui, {}, repo, dest="ua", branch=["stable",])
539 539 > EOF
540 540
541 541 $ $PYTHON branchclone.py
542 542 adding changesets
543 543 adding manifests
544 544 adding file changes
545 545 added 14 changesets with 14 changes to 3 files
546 546 updating to branch stable
547 547 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
548 548 $ rm -r ua
549 549
550 550
551 551 Test clone with special '@' bookmark:
552 552 $ cd a
553 553 $ hg bookmark -r a7949464abda @ # branch point of stable from default
554 554 $ hg clone . ../i
555 555 updating to bookmark @
556 556 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
557 557 $ hg id -i ../i
558 558 a7949464abda
559 559 $ rm -r ../i
560 560
561 561 $ hg bookmark -f -r stable @
562 562 $ hg bookmarks
563 563 @ 15:0aae7cf88f0d
564 564 $ hg clone . ../i
565 565 updating to bookmark @ on branch stable
566 566 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
567 567 $ hg id -i ../i
568 568 0aae7cf88f0d
569 569 $ cd "$TESTTMP"
570 570
571 571
572 572 Testing failures:
573 573
574 574 $ mkdir fail
575 575 $ cd fail
576 576
577 577 No local source
578 578
579 579 $ hg clone a b
580 580 abort: repository a not found!
581 581 [255]
582 582
583 583 No remote source
584 584
585 585 #if windows
586 586 $ hg clone http://$LOCALIP:3121/a b
587 587 abort: error: * (glob)
588 588 [255]
589 589 #else
590 590 $ hg clone http://$LOCALIP:3121/a b
591 591 abort: error: *refused* (glob)
592 592 [255]
593 593 #endif
594 594 $ rm -rf b # work around bug with http clone
595 595
596 596
597 597 #if unix-permissions no-root
598 598
599 599 Inaccessible source
600 600
601 601 $ mkdir a
602 602 $ chmod 000 a
603 603 $ hg clone a b
604 604 abort: repository a not found!
605 605 [255]
606 606
607 607 Inaccessible destination
608 608
609 609 $ hg init b
610 610 $ cd b
611 611 $ hg clone . ../a
612 612 abort: Permission denied: '../a'
613 613 [255]
614 614 $ cd ..
615 615 $ chmod 700 a
616 616 $ rm -r a b
617 617
618 618 #endif
619 619
620 620
621 621 #if fifo
622 622
623 623 Source of wrong type
624 624
625 625 $ mkfifo a
626 626 $ hg clone a b
627 627 abort: repository a not found!
628 628 [255]
629 629 $ rm a
630 630
631 631 #endif
632 632
633 633 Default destination, same directory
634 634
635 635 $ hg init q
636 636 $ hg clone q
637 637 destination directory: q
638 638 abort: destination 'q' is not empty
639 639 [255]
640 640
641 641 destination directory not empty
642 642
643 643 $ mkdir a
644 644 $ echo stuff > a/a
645 645 $ hg clone q a
646 646 abort: destination 'a' is not empty
647 647 [255]
648 648
649 649
650 650 #if unix-permissions no-root
651 651
652 652 leave existing directory in place after clone failure
653 653
654 654 $ hg init c
655 655 $ cd c
656 656 $ echo c > c
657 657 $ hg commit -A -m test
658 658 adding c
659 659 $ chmod -rx .hg/store/data
660 660 $ cd ..
661 661 $ mkdir d
662 662 $ hg clone c d 2> err
663 663 [255]
664 664 $ test -d d
665 665 $ test -d d/.hg
666 666 [1]
667 667
668 668 re-enable perm to allow deletion
669 669
670 670 $ chmod +rx c/.hg/store/data
671 671
672 672 #endif
673 673
674 674 $ cd ..
675 675
676 676 Test clone from the repository in (emulated) revlog format 0 (issue4203):
677 677
678 678 $ mkdir issue4203
679 679 $ mkdir -p src/.hg
680 680 $ echo foo > src/foo
681 681 $ hg -R src add src/foo
682 682 $ hg -R src commit -m '#0'
683 683 $ hg -R src log -q
684 684 0:e1bab28bca43
685 685 $ hg clone -U -q src dst
686 686 $ hg -R dst log -q
687 687 0:e1bab28bca43
688 688
689 689 Create repositories to test auto sharing functionality
690 690
691 691 $ cat >> $HGRCPATH << EOF
692 692 > [extensions]
693 693 > share=
694 694 > EOF
695 695
696 696 $ hg init empty
697 697 $ hg init source1a
698 698 $ cd source1a
699 699 $ echo initial1 > foo
700 700 $ hg -q commit -A -m initial
701 701 $ echo second > foo
702 702 $ hg commit -m second
703 703 $ cd ..
704 704
705 705 $ hg init filteredrev0
706 706 $ cd filteredrev0
707 707 $ cat >> .hg/hgrc << EOF
708 708 > [experimental]
709 709 > evolution=createmarkers
710 710 > EOF
711 711 $ echo initial1 > foo
712 712 $ hg -q commit -A -m initial0
713 713 $ hg -q up -r null
714 714 $ echo initial2 > foo
715 715 $ hg -q commit -A -m initial1
716 716 $ hg debugobsolete c05d5c47a5cf81401869999f3d05f7d699d2b29a e082c1832e09a7d1e78b7fd49a592d372de854c8
717 717 obsoleted 1 changesets
718 718 $ cd ..
719 719
720 720 $ hg -q clone --pull source1a source1b
721 721 $ cd source1a
722 722 $ hg bookmark bookA
723 723 $ echo 1a > foo
724 724 $ hg commit -m 1a
725 725 $ cd ../source1b
726 726 $ hg -q up -r 0
727 727 $ echo head1 > foo
728 728 $ hg commit -m head1
729 729 created new head
730 730 $ hg bookmark head1
731 731 $ hg -q up -r 0
732 732 $ echo head2 > foo
733 733 $ hg commit -m head2
734 734 created new head
735 735 $ hg bookmark head2
736 736 $ hg -q up -r 0
737 737 $ hg branch branch1
738 738 marked working directory as branch branch1
739 739 (branches are permanent and global, did you want a bookmark?)
740 740 $ echo branch1 > foo
741 741 $ hg commit -m branch1
742 742 $ hg -q up -r 0
743 743 $ hg branch branch2
744 744 marked working directory as branch branch2
745 745 $ echo branch2 > foo
746 746 $ hg commit -m branch2
747 747 $ cd ..
748 748 $ hg init source2
749 749 $ cd source2
750 750 $ echo initial2 > foo
751 751 $ hg -q commit -A -m initial2
752 752 $ echo second > foo
753 753 $ hg commit -m second
754 754 $ cd ..
755 755
756 756 Clone with auto share from an empty repo should not result in share
757 757
758 758 $ mkdir share
759 759 $ hg --config share.pool=share clone empty share-empty
760 760 (not using pooled storage: remote appears to be empty)
761 761 updating to branch default
762 762 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
763 763 $ ls share
764 764 $ test -d share-empty/.hg/store
765 765 $ test -f share-empty/.hg/sharedpath
766 766 [1]
767 767
768 768 Clone with auto share from a repo with filtered revision 0 should not result in share
769 769
770 770 $ hg --config share.pool=share clone filteredrev0 share-filtered
771 771 (not using pooled storage: unable to resolve identity of remote)
772 772 requesting all changes
773 773 adding changesets
774 774 adding manifests
775 775 adding file changes
776 776 added 1 changesets with 1 changes to 1 files
777 777 updating to branch default
778 778 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
779 779
780 780 Clone from repo with content should result in shared store being created
781 781
782 782 $ hg --config share.pool=share clone source1a share-dest1a
783 783 (sharing from new pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
784 784 requesting all changes
785 785 adding changesets
786 786 adding manifests
787 787 adding file changes
788 788 added 3 changesets with 3 changes to 1 files
789 789 searching for changes
790 790 no changes found
791 791 adding remote bookmark bookA
792 792 updating working directory
793 793 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
794 794
795 795 The shared repo should have been created
796 796
797 797 $ ls share
798 798 b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1
799 799
800 800 The destination should point to it
801 801
802 802 $ cat share-dest1a/.hg/sharedpath; echo
803 803 $TESTTMP/share/b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1/.hg (glob)
804 804
805 805 The destination should have bookmarks
806 806
807 807 $ hg -R share-dest1a bookmarks
808 808 bookA 2:e5bfe23c0b47
809 809
810 810 The default path should be the remote, not the share
811 811
812 812 $ hg -R share-dest1a config paths.default
813 813 $TESTTMP/source1a (glob)
814 814
815 815 Clone with existing share dir should result in pull + share
816 816
817 817 $ hg --config share.pool=share clone source1b share-dest1b
818 818 (sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
819 819 searching for changes
820 820 adding changesets
821 821 adding manifests
822 822 adding file changes
823 823 added 4 changesets with 4 changes to 1 files (+4 heads)
824 824 adding remote bookmark head1
825 825 adding remote bookmark head2
826 826 updating working directory
827 827 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
828 828
829 829 $ ls share
830 830 b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1
831 831
832 832 $ cat share-dest1b/.hg/sharedpath; echo
833 833 $TESTTMP/share/b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1/.hg (glob)
834 834
835 835 We only get bookmarks from the remote, not everything in the share
836 836
837 837 $ hg -R share-dest1b bookmarks
838 838 head1 3:4a8dc1ab4c13
839 839 head2 4:99f71071f117
840 840
841 841 Default path should be source, not share.
842 842
843 843 $ hg -R share-dest1b config paths.default
844 844 $TESTTMP/source1b (glob)
845 845
846 846 Checked out revision should be head of default branch
847 847
848 848 $ hg -R share-dest1b log -r .
849 849 changeset: 4:99f71071f117
850 850 bookmark: head2
851 851 parent: 0:b5f04eac9d8f
852 852 user: test
853 853 date: Thu Jan 01 00:00:00 1970 +0000
854 854 summary: head2
855 855
856 856
857 857 Clone from unrelated repo should result in new share
858 858
859 859 $ hg --config share.pool=share clone source2 share-dest2
860 860 (sharing from new pooled repository 22aeff664783fd44c6d9b435618173c118c3448e)
861 861 requesting all changes
862 862 adding changesets
863 863 adding manifests
864 864 adding file changes
865 865 added 2 changesets with 2 changes to 1 files
866 866 searching for changes
867 867 no changes found
868 868 updating working directory
869 869 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
870 870
871 871 $ ls share
872 872 22aeff664783fd44c6d9b435618173c118c3448e
873 873 b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1
874 874
875 875 remote naming mode works as advertised
876 876
877 877 $ hg --config share.pool=shareremote --config share.poolnaming=remote clone source1a share-remote1a
878 878 (sharing from new pooled repository 195bb1fcdb595c14a6c13e0269129ed78f6debde)
879 879 requesting all changes
880 880 adding changesets
881 881 adding manifests
882 882 adding file changes
883 883 added 3 changesets with 3 changes to 1 files
884 884 searching for changes
885 885 no changes found
886 886 adding remote bookmark bookA
887 887 updating working directory
888 888 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
889 889
890 890 $ ls shareremote
891 891 195bb1fcdb595c14a6c13e0269129ed78f6debde
892 892
893 893 $ hg --config share.pool=shareremote --config share.poolnaming=remote clone source1b share-remote1b
894 894 (sharing from new pooled repository c0d4f83847ca2a873741feb7048a45085fd47c46)
895 895 requesting all changes
896 896 adding changesets
897 897 adding manifests
898 898 adding file changes
899 899 added 6 changesets with 6 changes to 1 files (+4 heads)
900 900 searching for changes
901 901 no changes found
902 902 adding remote bookmark head1
903 903 adding remote bookmark head2
904 904 updating working directory
905 905 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
906 906
907 907 $ ls shareremote
908 908 195bb1fcdb595c14a6c13e0269129ed78f6debde
909 909 c0d4f83847ca2a873741feb7048a45085fd47c46
910 910
911 911 request to clone a single revision is respected in sharing mode
912 912
913 913 $ hg --config share.pool=sharerevs clone -r 4a8dc1ab4c13 source1b share-1arev
914 914 (sharing from new pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
915 915 adding changesets
916 916 adding manifests
917 917 adding file changes
918 918 added 2 changesets with 2 changes to 1 files
919 919 no changes found
920 920 adding remote bookmark head1
921 921 updating working directory
922 922 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
923 923
924 924 $ hg -R share-1arev log -G
925 925 @ changeset: 1:4a8dc1ab4c13
926 926 | bookmark: head1
927 927 | tag: tip
928 928 | user: test
929 929 | date: Thu Jan 01 00:00:00 1970 +0000
930 930 | summary: head1
931 931 |
932 932 o changeset: 0:b5f04eac9d8f
933 933 user: test
934 934 date: Thu Jan 01 00:00:00 1970 +0000
935 935 summary: initial
936 936
937 937
938 938 making another clone should only pull down requested rev
939 939
940 940 $ hg --config share.pool=sharerevs clone -r 99f71071f117 source1b share-1brev
941 941 (sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
942 942 searching for changes
943 943 adding changesets
944 944 adding manifests
945 945 adding file changes
946 946 added 1 changesets with 1 changes to 1 files (+1 heads)
947 947 adding remote bookmark head1
948 948 adding remote bookmark head2
949 949 updating working directory
950 950 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
951 951
952 952 $ hg -R share-1brev log -G
953 953 @ changeset: 2:99f71071f117
954 954 | bookmark: head2
955 955 | tag: tip
956 956 | parent: 0:b5f04eac9d8f
957 957 | user: test
958 958 | date: Thu Jan 01 00:00:00 1970 +0000
959 959 | summary: head2
960 960 |
961 961 | o changeset: 1:4a8dc1ab4c13
962 962 |/ bookmark: head1
963 963 | user: test
964 964 | date: Thu Jan 01 00:00:00 1970 +0000
965 965 | summary: head1
966 966 |
967 967 o changeset: 0:b5f04eac9d8f
968 968 user: test
969 969 date: Thu Jan 01 00:00:00 1970 +0000
970 970 summary: initial
971 971
972 972
973 973 Request to clone a single branch is respected in sharing mode
974 974
975 975 $ hg --config share.pool=sharebranch clone -b branch1 source1b share-1bbranch1
976 976 (sharing from new pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
977 977 adding changesets
978 978 adding manifests
979 979 adding file changes
980 980 added 2 changesets with 2 changes to 1 files
981 981 no changes found
982 982 updating working directory
983 983 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
984 984
985 985 $ hg -R share-1bbranch1 log -G
986 986 o changeset: 1:5f92a6c1a1b1
987 987 | branch: branch1
988 988 | tag: tip
989 989 | user: test
990 990 | date: Thu Jan 01 00:00:00 1970 +0000
991 991 | summary: branch1
992 992 |
993 993 @ changeset: 0:b5f04eac9d8f
994 994 user: test
995 995 date: Thu Jan 01 00:00:00 1970 +0000
996 996 summary: initial
997 997
998 998
999 999 $ hg --config share.pool=sharebranch clone -b branch2 source1b share-1bbranch2
1000 1000 (sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
1001 1001 searching for changes
1002 1002 adding changesets
1003 1003 adding manifests
1004 1004 adding file changes
1005 1005 added 1 changesets with 1 changes to 1 files (+1 heads)
1006 1006 updating working directory
1007 1007 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1008 1008
1009 1009 $ hg -R share-1bbranch2 log -G
1010 1010 o changeset: 2:6bacf4683960
1011 1011 | branch: branch2
1012 1012 | tag: tip
1013 1013 | parent: 0:b5f04eac9d8f
1014 1014 | user: test
1015 1015 | date: Thu Jan 01 00:00:00 1970 +0000
1016 1016 | summary: branch2
1017 1017 |
1018 1018 | o changeset: 1:5f92a6c1a1b1
1019 1019 |/ branch: branch1
1020 1020 | user: test
1021 1021 | date: Thu Jan 01 00:00:00 1970 +0000
1022 1022 | summary: branch1
1023 1023 |
1024 1024 @ changeset: 0:b5f04eac9d8f
1025 1025 user: test
1026 1026 date: Thu Jan 01 00:00:00 1970 +0000
1027 1027 summary: initial
1028 1028
1029 1029
1030 1030 -U is respected in share clone mode
1031 1031
1032 1032 $ hg --config share.pool=share clone -U source1a share-1anowc
1033 1033 (sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
1034 1034 searching for changes
1035 1035 no changes found
1036 1036 adding remote bookmark bookA
1037 1037
1038 1038 $ ls share-1anowc
1039 1039
1040 1040 Test that auto sharing doesn't cause failure of "hg clone local remote"
1041 1041
1042 1042 $ cd $TESTTMP
1043 1043 $ hg -R a id -r 0
1044 1044 acb14030fe0a
1045 1045 $ hg id -R remote -r 0
1046 1046 abort: repository remote not found!
1047 1047 [255]
1048 1048 $ hg --config share.pool=share -q clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" a ssh://user@dummy/remote
1049 1049 $ hg -R remote id -r 0
1050 1050 acb14030fe0a
1051 1051
1052 1052 Cloning into pooled storage doesn't race (issue5104)
1053 1053
1054 1054 $ HGPOSTLOCKDELAY=2.0 hg --config share.pool=racepool --config extensions.lockdelay=$TESTDIR/lockdelay.py clone source1a share-destrace1 > race1.log 2>&1 &
1055 1055 $ HGPRELOCKDELAY=1.0 hg --config share.pool=racepool --config extensions.lockdelay=$TESTDIR/lockdelay.py clone source1a share-destrace2 > race2.log 2>&1
1056 1056 $ wait
1057 1057
1058 1058 $ hg -R share-destrace1 log -r tip
1059 1059 changeset: 2:e5bfe23c0b47
1060 1060 bookmark: bookA
1061 1061 tag: tip
1062 1062 user: test
1063 1063 date: Thu Jan 01 00:00:00 1970 +0000
1064 1064 summary: 1a
1065 1065
1066 1066
1067 1067 $ hg -R share-destrace2 log -r tip
1068 1068 changeset: 2:e5bfe23c0b47
1069 1069 bookmark: bookA
1070 1070 tag: tip
1071 1071 user: test
1072 1072 date: Thu Jan 01 00:00:00 1970 +0000
1073 1073 summary: 1a
1074 1074
1075 1075 One repo should be new, the other should be shared from the pool. We
1076 1076 don't care which is which, so we just make sure we always print the
1077 1077 one containing "new pooled" first, then one one containing "existing
1078 1078 pooled".
1079 1079
1080 1080 $ (grep 'new pooled' race1.log > /dev/null && cat race1.log || cat race2.log) | grep -v lock
1081 1081 (sharing from new pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
1082 1082 requesting all changes
1083 1083 adding changesets
1084 1084 adding manifests
1085 1085 adding file changes
1086 1086 added 3 changesets with 3 changes to 1 files
1087 1087 searching for changes
1088 1088 no changes found
1089 1089 adding remote bookmark bookA
1090 1090 updating working directory
1091 1091 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1092 1092
1093 1093 $ (grep 'existing pooled' race1.log > /dev/null && cat race1.log || cat race2.log) | grep -v lock
1094 1094 (sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
1095 1095 searching for changes
1096 1096 no changes found
1097 1097 adding remote bookmark bookA
1098 1098 updating working directory
1099 1099 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1100 1100
1101 1101 SEC: check for unsafe ssh url
1102 1102
1103 $ cat >> $HGRCPATH << EOF
1104 > [ui]
1105 > ssh = sh -c "read l; read l; read l"
1106 > EOF
1107
1103 1108 $ hg clone 'ssh://-oProxyCommand=touch${IFS}owned/path'
1104 1109 abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path'
1105 1110 [255]
1106 1111 $ hg clone 'ssh://%2DoProxyCommand=touch${IFS}owned/path'
1107 1112 abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path'
1108 1113 [255]
1109 1114 $ hg clone 'ssh://fakehost|shellcommand/path'
1110 1115 abort: potentially unsafe url: 'ssh://fakehost|shellcommand/path'
1111 1116 [255]
1112 1117 $ hg clone 'ssh://fakehost%7Cshellcommand/path'
1113 1118 abort: potentially unsafe url: 'ssh://fakehost|shellcommand/path'
1114 1119 [255]
1115 1120
1116 1121 $ hg clone 'ssh://-oProxyCommand=touch owned%20foo@example.com/nonexistent/path'
1117 1122 abort: potentially unsafe url: 'ssh://-oProxyCommand=touch owned foo@example.com/nonexistent/path'
1118 1123 [255]
1124
1125 #if windows
1126 $ hg clone "ssh://%26touch%20owned%20/" --debug
1127 running sh -c "read l; read l; read l" "&touch owned " "hg -R . serve --stdio"
1128 sending hello command
1129 sending between command
1130 abort: no suitable response from remote hg!
1131 [255]
1132 $ hg clone "ssh://example.com:%26touch%20owned%20/" --debug
1133 running sh -c "read l; read l; read l" -p "&touch owned " example.com "hg -R . serve --stdio"
1134 sending hello command
1135 sending between command
1136 abort: no suitable response from remote hg!
1137 [255]
1138 #else
1139 $ hg clone "ssh://%3btouch%20owned%20/" --debug
1140 running sh -c "read l; read l; read l" ';touch owned ' 'hg -R . serve --stdio'
1141 sending hello command
1142 sending between command
1143 abort: no suitable response from remote hg!
1144 [255]
1145 $ hg clone "ssh://example.com:%3btouch%20owned%20/" --debug
1146 running sh -c "read l; read l; read l" -p ';touch owned ' example.com 'hg -R . serve --stdio'
1147 sending hello command
1148 sending between command
1149 abort: no suitable response from remote hg!
1150 [255]
1151 #endif
1152
1153 $ hg clone "ssh://v-alid.example.com/" --debug
1154 running sh -c "read l; read l; read l" v-alid\.example\.com ['"]hg -R \. serve --stdio['"] (re)
1155 sending hello command
1156 sending between command
1157 abort: no suitable response from remote hg!
1158 [255]
1159
1119 1160 We should not have created a file named owned - if it exists, the
1120 1161 attack succeeded.
1121 1162 $ if test -f owned; then echo 'you got owned'; fi
@@ -1,562 +1,562
1 1 This test is a duplicate of 'test-http.t' feel free to factor out
2 2 parts that are not bundle1/bundle2 specific.
3 3
4 4 $ cat << EOF >> $HGRCPATH
5 5 > [devel]
6 6 > # This test is dedicated to interaction through old bundle
7 7 > legacy.exchange = bundle1
8 8 > [format] # temporary settings
9 9 > usegeneraldelta=yes
10 10 > EOF
11 11
12 12
13 13 This test tries to exercise the ssh functionality with a dummy script
14 14
15 15 creating 'remote' repo
16 16
17 17 $ hg init remote
18 18 $ cd remote
19 19 $ echo this > foo
20 20 $ echo this > fooO
21 21 $ hg ci -A -m "init" foo fooO
22 22
23 23 insert a closed branch (issue4428)
24 24
25 25 $ hg up null
26 26 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
27 27 $ hg branch closed
28 28 marked working directory as branch closed
29 29 (branches are permanent and global, did you want a bookmark?)
30 30 $ hg ci -mc0
31 31 $ hg ci --close-branch -mc1
32 32 $ hg up -q default
33 33
34 34 configure for serving
35 35
36 36 $ cat <<EOF > .hg/hgrc
37 37 > [server]
38 38 > uncompressed = True
39 39 >
40 40 > [hooks]
41 41 > changegroup = sh -c "printenv.py changegroup-in-remote 0 ../dummylog"
42 42 > EOF
43 43 $ cd ..
44 44
45 45 repo not found error
46 46
47 47 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/nonexistent local
48 48 remote: abort: repository nonexistent not found!
49 49 abort: no suitable response from remote hg!
50 50 [255]
51 51
52 52 non-existent absolute path
53 53
54 54 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy//`pwd`/nonexistent local
55 55 remote: abort: repository /$TESTTMP/nonexistent not found!
56 56 abort: no suitable response from remote hg!
57 57 [255]
58 58
59 59 clone remote via stream
60 60
61 61 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --uncompressed ssh://user@dummy/remote local-stream
62 62 streaming all changes
63 63 4 files to transfer, 602 bytes of data
64 64 transferred 602 bytes in * seconds (*) (glob)
65 65 searching for changes
66 66 no changes found
67 67 updating to branch default
68 68 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
69 69 $ cd local-stream
70 70 $ hg verify
71 71 checking changesets
72 72 checking manifests
73 73 crosschecking files in changesets and manifests
74 74 checking files
75 75 2 files, 3 changesets, 2 total revisions
76 76 $ hg branches
77 77 default 0:1160648e36ce
78 78 $ cd ..
79 79
80 80 clone bookmarks via stream
81 81
82 82 $ hg -R local-stream book mybook
83 83 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --uncompressed ssh://user@dummy/local-stream stream2
84 84 streaming all changes
85 85 4 files to transfer, 602 bytes of data
86 86 transferred 602 bytes in * seconds (*) (glob)
87 87 searching for changes
88 88 no changes found
89 89 updating to branch default
90 90 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
91 91 $ cd stream2
92 92 $ hg book
93 93 mybook 0:1160648e36ce
94 94 $ cd ..
95 95 $ rm -rf local-stream stream2
96 96
97 97 clone remote via pull
98 98
99 99 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local
100 100 requesting all changes
101 101 adding changesets
102 102 adding manifests
103 103 adding file changes
104 104 added 3 changesets with 2 changes to 2 files
105 105 updating to branch default
106 106 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
107 107
108 108 verify
109 109
110 110 $ cd local
111 111 $ hg verify
112 112 checking changesets
113 113 checking manifests
114 114 crosschecking files in changesets and manifests
115 115 checking files
116 116 2 files, 3 changesets, 2 total revisions
117 117 $ cat >> .hg/hgrc <<EOF
118 118 > [hooks]
119 119 > changegroup = sh -c "printenv.py changegroup-in-local 0 ../dummylog"
120 120 > EOF
121 121
122 122 empty default pull
123 123
124 124 $ hg paths
125 125 default = ssh://user@dummy/remote
126 126 $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\""
127 127 pulling from ssh://user@dummy/remote
128 128 searching for changes
129 129 no changes found
130 130
131 131 pull from wrong ssh URL
132 132
133 133 $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/doesnotexist
134 134 pulling from ssh://user@dummy/doesnotexist
135 135 remote: abort: repository doesnotexist not found!
136 136 abort: no suitable response from remote hg!
137 137 [255]
138 138
139 139 local change
140 140
141 141 $ echo bleah > foo
142 142 $ hg ci -m "add"
143 143
144 144 updating rc
145 145
146 146 $ echo "default-push = ssh://user@dummy/remote" >> .hg/hgrc
147 147 $ echo "[ui]" >> .hg/hgrc
148 148 $ echo "ssh = \"$PYTHON\" \"$TESTDIR/dummyssh\"" >> .hg/hgrc
149 149
150 150 find outgoing
151 151
152 152 $ hg out ssh://user@dummy/remote
153 153 comparing with ssh://user@dummy/remote
154 154 searching for changes
155 155 changeset: 3:a28a9d1a809c
156 156 tag: tip
157 157 parent: 0:1160648e36ce
158 158 user: test
159 159 date: Thu Jan 01 00:00:00 1970 +0000
160 160 summary: add
161 161
162 162
163 163 find incoming on the remote side
164 164
165 165 $ hg incoming -R ../remote -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/local
166 166 comparing with ssh://user@dummy/local
167 167 searching for changes
168 168 changeset: 3:a28a9d1a809c
169 169 tag: tip
170 170 parent: 0:1160648e36ce
171 171 user: test
172 172 date: Thu Jan 01 00:00:00 1970 +0000
173 173 summary: add
174 174
175 175
176 176 find incoming on the remote side (using absolute path)
177 177
178 178 $ hg incoming -R ../remote -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/`pwd`"
179 179 comparing with ssh://user@dummy/$TESTTMP/local
180 180 searching for changes
181 181 changeset: 3:a28a9d1a809c
182 182 tag: tip
183 183 parent: 0:1160648e36ce
184 184 user: test
185 185 date: Thu Jan 01 00:00:00 1970 +0000
186 186 summary: add
187 187
188 188
189 189 push
190 190
191 191 $ hg push
192 192 pushing to ssh://user@dummy/remote
193 193 searching for changes
194 194 remote: adding changesets
195 195 remote: adding manifests
196 196 remote: adding file changes
197 197 remote: added 1 changesets with 1 changes to 1 files
198 198 $ cd ../remote
199 199
200 200 check remote tip
201 201
202 202 $ hg tip
203 203 changeset: 3:a28a9d1a809c
204 204 tag: tip
205 205 parent: 0:1160648e36ce
206 206 user: test
207 207 date: Thu Jan 01 00:00:00 1970 +0000
208 208 summary: add
209 209
210 210 $ hg verify
211 211 checking changesets
212 212 checking manifests
213 213 crosschecking files in changesets and manifests
214 214 checking files
215 215 2 files, 4 changesets, 3 total revisions
216 216 $ hg cat -r tip foo
217 217 bleah
218 218 $ echo z > z
219 219 $ hg ci -A -m z z
220 220 created new head
221 221
222 222 test pushkeys and bookmarks
223 223
224 224 $ cd ../local
225 225 $ hg debugpushkey --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote namespaces
226 226 bookmarks
227 227 namespaces
228 228 phases
229 229 $ hg book foo -r 0
230 230 $ hg out -B
231 231 comparing with ssh://user@dummy/remote
232 232 searching for changed bookmarks
233 233 foo 1160648e36ce
234 234 $ hg push -B foo
235 235 pushing to ssh://user@dummy/remote
236 236 searching for changes
237 237 no changes found
238 238 exporting bookmark foo
239 239 [1]
240 240 $ hg debugpushkey --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote bookmarks
241 241 foo 1160648e36cec0054048a7edc4110c6f84fde594
242 242 $ hg book -f foo
243 243 $ hg push --traceback
244 244 pushing to ssh://user@dummy/remote
245 245 searching for changes
246 246 no changes found
247 247 updating bookmark foo
248 248 [1]
249 249 $ hg book -d foo
250 250 $ hg in -B
251 251 comparing with ssh://user@dummy/remote
252 252 searching for changed bookmarks
253 253 foo a28a9d1a809c
254 254 $ hg book -f -r 0 foo
255 255 $ hg pull -B foo
256 256 pulling from ssh://user@dummy/remote
257 257 no changes found
258 258 updating bookmark foo
259 259 $ hg book -d foo
260 260 $ hg push -B foo
261 261 pushing to ssh://user@dummy/remote
262 262 searching for changes
263 263 no changes found
264 264 deleting remote bookmark foo
265 265 [1]
266 266
267 267 a bad, evil hook that prints to stdout
268 268
269 269 $ cat <<EOF > $TESTTMP/badhook
270 270 > import sys
271 271 > sys.stdout.write("KABOOM\n")
272 272 > EOF
273 273
274 274 $ echo '[hooks]' >> ../remote/.hg/hgrc
275 275 $ echo "changegroup.stdout = \"$PYTHON\" $TESTTMP/badhook" >> ../remote/.hg/hgrc
276 276 $ echo r > r
277 277 $ hg ci -A -m z r
278 278
279 279 push should succeed even though it has an unexpected response
280 280
281 281 $ hg push
282 282 pushing to ssh://user@dummy/remote
283 283 searching for changes
284 284 remote has heads on branch 'default' that are not known locally: 6c0482d977a3
285 285 remote: adding changesets
286 286 remote: adding manifests
287 287 remote: adding file changes
288 288 remote: added 1 changesets with 1 changes to 1 files
289 289 remote: KABOOM
290 290 $ hg -R ../remote heads
291 291 changeset: 5:1383141674ec
292 292 tag: tip
293 293 parent: 3:a28a9d1a809c
294 294 user: test
295 295 date: Thu Jan 01 00:00:00 1970 +0000
296 296 summary: z
297 297
298 298 changeset: 4:6c0482d977a3
299 299 parent: 0:1160648e36ce
300 300 user: test
301 301 date: Thu Jan 01 00:00:00 1970 +0000
302 302 summary: z
303 303
304 304
305 305 clone bookmarks
306 306
307 307 $ hg -R ../remote bookmark test
308 308 $ hg -R ../remote bookmarks
309 309 * test 4:6c0482d977a3
310 310 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local-bookmarks
311 311 requesting all changes
312 312 adding changesets
313 313 adding manifests
314 314 adding file changes
315 315 added 6 changesets with 5 changes to 4 files (+1 heads)
316 316 updating to branch default
317 317 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
318 318 $ hg -R local-bookmarks bookmarks
319 319 test 4:6c0482d977a3
320 320
321 321 passwords in ssh urls are not supported
322 322 (we use a glob here because different Python versions give different
323 323 results here)
324 324
325 325 $ hg push ssh://user:erroneouspwd@dummy/remote
326 326 pushing to ssh://user:*@dummy/remote (glob)
327 327 abort: password in URL not supported!
328 328 [255]
329 329
330 330 $ cd ..
331 331
332 332 hide outer repo
333 333 $ hg init
334 334
335 335 Test remote paths with spaces (issue2983):
336 336
337 337 $ hg init --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
338 338 $ touch "$TESTTMP/a repo/test"
339 339 $ hg -R 'a repo' commit -A -m "test"
340 340 adding test
341 341 $ hg -R 'a repo' tag tag
342 342 $ hg id --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
343 343 73649e48688a
344 344
345 345 $ hg id --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo#noNoNO"
346 346 abort: unknown revision 'noNoNO'!
347 347 [255]
348 348
349 349 Test (non-)escaping of remote paths with spaces when cloning (issue3145):
350 350
351 351 $ hg clone --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
352 352 destination directory: a repo
353 353 abort: destination 'a repo' is not empty
354 354 [255]
355 355
356 356 Test hg-ssh using a helper script that will restore PYTHONPATH (which might
357 357 have been cleared by a hg.exe wrapper) and invoke hg-ssh with the right
358 358 parameters:
359 359
360 360 $ cat > ssh.sh << EOF
361 361 > userhost="\$1"
362 362 > SSH_ORIGINAL_COMMAND="\$2"
363 363 > export SSH_ORIGINAL_COMMAND
364 364 > PYTHONPATH="$PYTHONPATH"
365 365 > export PYTHONPATH
366 366 > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" "$TESTTMP/a repo"
367 367 > EOF
368 368
369 369 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a repo"
370 370 73649e48688a
371 371
372 372 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a'repo"
373 373 remote: Illegal repository "$TESTTMP/a'repo" (glob)
374 374 abort: no suitable response from remote hg!
375 375 [255]
376 376
377 377 $ hg id --ssh "sh ssh.sh" --remotecmd hacking "ssh://user@dummy/a'repo"
378 378 remote: Illegal command "hacking -R 'a'\''repo' serve --stdio"
379 379 abort: no suitable response from remote hg!
380 380 [255]
381 381
382 382 $ SSH_ORIGINAL_COMMAND="'hg' serve -R 'a'repo' --stdio" $PYTHON "$TESTDIR/../contrib/hg-ssh"
383 383 Illegal command "'hg' serve -R 'a'repo' --stdio": No closing quotation
384 384 [255]
385 385
386 386 Test hg-ssh in read-only mode:
387 387
388 388 $ cat > ssh.sh << EOF
389 389 > userhost="\$1"
390 390 > SSH_ORIGINAL_COMMAND="\$2"
391 391 > export SSH_ORIGINAL_COMMAND
392 392 > PYTHONPATH="$PYTHONPATH"
393 393 > export PYTHONPATH
394 394 > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" --read-only "$TESTTMP/remote"
395 395 > EOF
396 396
397 397 $ hg clone --ssh "sh ssh.sh" "ssh://user@dummy/$TESTTMP/remote" read-only-local
398 398 requesting all changes
399 399 adding changesets
400 400 adding manifests
401 401 adding file changes
402 402 added 6 changesets with 5 changes to 4 files (+1 heads)
403 403 updating to branch default
404 404 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
405 405
406 406 $ cd read-only-local
407 407 $ echo "baz" > bar
408 408 $ hg ci -A -m "unpushable commit" bar
409 409 $ hg push --ssh "sh ../ssh.sh"
410 410 pushing to ssh://user@dummy/*/remote (glob)
411 411 searching for changes
412 412 remote: Permission denied
413 413 remote: abort: pretxnopen.hg-ssh hook failed
414 414 remote: Permission denied
415 415 remote: pushkey-abort: prepushkey.hg-ssh hook failed
416 416 updating 6c0482d977a3 to public failed!
417 417 [1]
418 418
419 419 $ cd ..
420 420
421 421 stderr from remote commands should be printed before stdout from local code (issue4336)
422 422
423 423 $ hg clone remote stderr-ordering
424 424 updating to branch default
425 425 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
426 426 $ cd stderr-ordering
427 427 $ cat >> localwrite.py << EOF
428 428 > from mercurial import exchange, extensions
429 429 >
430 430 > def wrappedpush(orig, repo, *args, **kwargs):
431 431 > res = orig(repo, *args, **kwargs)
432 432 > repo.ui.write('local stdout\n')
433 433 > return res
434 434 >
435 435 > def extsetup(ui):
436 436 > extensions.wrapfunction(exchange, 'push', wrappedpush)
437 437 > EOF
438 438
439 439 $ cat >> .hg/hgrc << EOF
440 440 > [paths]
441 441 > default-push = ssh://user@dummy/remote
442 442 > [ui]
443 443 > ssh = "$PYTHON" "$TESTDIR/dummyssh"
444 444 > [extensions]
445 445 > localwrite = localwrite.py
446 446 > EOF
447 447
448 448 $ echo localwrite > foo
449 449 $ hg commit -m 'testing localwrite'
450 450 $ hg push
451 451 pushing to ssh://user@dummy/remote
452 452 searching for changes
453 453 remote: adding changesets
454 454 remote: adding manifests
455 455 remote: adding file changes
456 456 remote: added 1 changesets with 1 changes to 1 files
457 457 remote: KABOOM
458 458 local stdout
459 459
460 460 debug output
461 461
462 462 $ hg pull --debug ssh://user@dummy/remote
463 463 pulling from ssh://user@dummy/remote
464 running .* ".*/dummyssh" user@dummy ('|")hg -R remote serve --stdio('|") (re)
464 running .* ".*/dummyssh" ['"]user@dummy['"] ('|")hg -R remote serve --stdio('|") (re)
465 465 sending hello command
466 466 sending between command
467 467 remote: 355
468 468 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
469 469 remote: 1
470 470 preparing listkeys for "bookmarks"
471 471 sending listkeys command
472 472 received listkey for "bookmarks": 45 bytes
473 473 query 1; heads
474 474 sending batch command
475 475 searching for changes
476 476 all remote heads known locally
477 477 no changes found
478 478 preparing listkeys for "phases"
479 479 sending listkeys command
480 480 received listkey for "phases": 15 bytes
481 481 checking for updated bookmarks
482 482
483 483 $ cd ..
484 484
485 485 $ cat dummylog
486 486 Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio
487 487 Got arguments 1:user@dummy 2:hg -R /$TESTTMP/nonexistent serve --stdio
488 488 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
489 489 Got arguments 1:user@dummy 2:hg -R local-stream serve --stdio
490 490 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
491 491 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
492 492 Got arguments 1:user@dummy 2:hg -R doesnotexist serve --stdio
493 493 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
494 494 Got arguments 1:user@dummy 2:hg -R local serve --stdio
495 495 Got arguments 1:user@dummy 2:hg -R $TESTTMP/local serve --stdio
496 496 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
497 497 changegroup-in-remote hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=a28a9d1a809cab7d4e2fde4bee738a9ede948b60 HG_NODE_LAST=a28a9d1a809cab7d4e2fde4bee738a9ede948b60 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:ssh:$LOCALIP
498 498 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
499 499 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
500 500 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
501 501 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
502 502 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
503 503 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
504 504 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
505 505 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
506 506 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
507 507 changegroup-in-remote hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=1383141674ec756a6056f6a9097618482fe0f4a6 HG_NODE_LAST=1383141674ec756a6056f6a9097618482fe0f4a6 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:ssh:$LOCALIP
508 508 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
509 509 Got arguments 1:user@dummy 2:hg init 'a repo'
510 510 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
511 511 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
512 512 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
513 513 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
514 514 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
515 515 changegroup-in-remote hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=65c38f4125f9602c8db4af56530cc221d93b8ef8 HG_NODE_LAST=65c38f4125f9602c8db4af56530cc221d93b8ef8 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:ssh:$LOCALIP
516 516 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
517 517
518 518 remote hook failure is attributed to remote
519 519
520 520 $ cat > $TESTTMP/failhook << EOF
521 521 > def hook(ui, repo, **kwargs):
522 522 > ui.write('hook failure!\n')
523 523 > ui.flush()
524 524 > return 1
525 525 > EOF
526 526
527 527 $ echo "pretxnchangegroup.fail = python:$TESTTMP/failhook:hook" >> remote/.hg/hgrc
528 528
529 529 $ hg -q --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" clone ssh://user@dummy/remote hookout
530 530 $ cd hookout
531 531 $ touch hookfailure
532 532 $ hg -q commit -A -m 'remote hook failure'
533 533 $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" push
534 534 pushing to ssh://user@dummy/remote
535 535 searching for changes
536 536 remote: adding changesets
537 537 remote: adding manifests
538 538 remote: adding file changes
539 539 remote: added 1 changesets with 1 changes to 1 files
540 540 remote: hook failure!
541 541 remote: transaction abort!
542 542 remote: rollback completed
543 543 remote: abort: pretxnchangegroup.fail hook failed
544 544 [1]
545 545
546 546 abort during pull is properly reported as such
547 547
548 548 $ echo morefoo >> ../remote/foo
549 549 $ hg -R ../remote commit --message "more foo to be pulled"
550 550 $ cat >> ../remote/.hg/hgrc << EOF
551 551 > [extensions]
552 552 > crash = ${TESTDIR}/crashgetbundler.py
553 553 > EOF
554 554 $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" pull
555 555 pulling from ssh://user@dummy/remote
556 556 searching for changes
557 557 adding changesets
558 558 remote: abort: this is an exercise
559 559 transaction abort!
560 560 rollback completed
561 561 abort: stream ended unexpectedly (got 0 bytes, expected 4)
562 562 [255]
@@ -1,577 +1,577
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 360 Make sure hg is really paranoid in serve --stdio mode. It used to be
361 361 possible to get a debugger REPL by specifying a repo named --debugger.
362 362 $ hg -R --debugger serve --stdio
363 363 abort: potentially unsafe serve --stdio invocation: ['-R', '--debugger', 'serve', '--stdio']
364 364 [255]
365 365 $ hg -R --config=ui.debugger=yes serve --stdio
366 366 abort: potentially unsafe serve --stdio invocation: ['-R', '--config=ui.debugger=yes', 'serve', '--stdio']
367 367 [255]
368 368 Abbreviations of 'serve' also don't work, to avoid shenanigans.
369 369 $ hg -R narf serv --stdio
370 370 abort: potentially unsafe serve --stdio invocation: ['-R', 'narf', 'serv', '--stdio']
371 371 [255]
372 372
373 373 Test hg-ssh using a helper script that will restore PYTHONPATH (which might
374 374 have been cleared by a hg.exe wrapper) and invoke hg-ssh with the right
375 375 parameters:
376 376
377 377 $ cat > ssh.sh << EOF
378 378 > userhost="\$1"
379 379 > SSH_ORIGINAL_COMMAND="\$2"
380 380 > export SSH_ORIGINAL_COMMAND
381 381 > PYTHONPATH="$PYTHONPATH"
382 382 > export PYTHONPATH
383 383 > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" "$TESTTMP/a repo"
384 384 > EOF
385 385
386 386 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a repo"
387 387 73649e48688a
388 388
389 389 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a'repo"
390 390 remote: Illegal repository "$TESTTMP/a'repo" (glob)
391 391 abort: no suitable response from remote hg!
392 392 [255]
393 393
394 394 $ hg id --ssh "sh ssh.sh" --remotecmd hacking "ssh://user@dummy/a'repo"
395 395 remote: Illegal command "hacking -R 'a'\''repo' serve --stdio"
396 396 abort: no suitable response from remote hg!
397 397 [255]
398 398
399 399 $ SSH_ORIGINAL_COMMAND="'hg' -R 'a'repo' serve --stdio" $PYTHON "$TESTDIR/../contrib/hg-ssh"
400 400 Illegal command "'hg' -R 'a'repo' serve --stdio": No closing quotation
401 401 [255]
402 402
403 403 Test hg-ssh in read-only mode:
404 404
405 405 $ cat > ssh.sh << EOF
406 406 > userhost="\$1"
407 407 > SSH_ORIGINAL_COMMAND="\$2"
408 408 > export SSH_ORIGINAL_COMMAND
409 409 > PYTHONPATH="$PYTHONPATH"
410 410 > export PYTHONPATH
411 411 > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" --read-only "$TESTTMP/remote"
412 412 > EOF
413 413
414 414 $ hg clone --ssh "sh ssh.sh" "ssh://user@dummy/$TESTTMP/remote" read-only-local
415 415 requesting all changes
416 416 adding changesets
417 417 adding manifests
418 418 adding file changes
419 419 added 6 changesets with 5 changes to 4 files (+1 heads)
420 420 updating to branch default
421 421 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
422 422
423 423 $ cd read-only-local
424 424 $ echo "baz" > bar
425 425 $ hg ci -A -m "unpushable commit" bar
426 426 $ hg push --ssh "sh ../ssh.sh"
427 427 pushing to ssh://user@dummy/*/remote (glob)
428 428 searching for changes
429 429 remote: Permission denied
430 430 remote: pretxnopen.hg-ssh hook failed
431 431 abort: push failed on remote
432 432 [255]
433 433
434 434 $ cd ..
435 435
436 436 stderr from remote commands should be printed before stdout from local code (issue4336)
437 437
438 438 $ hg clone remote stderr-ordering
439 439 updating to branch default
440 440 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
441 441 $ cd stderr-ordering
442 442 $ cat >> localwrite.py << EOF
443 443 > from mercurial import exchange, extensions
444 444 >
445 445 > def wrappedpush(orig, repo, *args, **kwargs):
446 446 > res = orig(repo, *args, **kwargs)
447 447 > repo.ui.write('local stdout\n')
448 448 > return res
449 449 >
450 450 > def extsetup(ui):
451 451 > extensions.wrapfunction(exchange, 'push', wrappedpush)
452 452 > EOF
453 453
454 454 $ cat >> .hg/hgrc << EOF
455 455 > [paths]
456 456 > default-push = ssh://user@dummy/remote
457 457 > [ui]
458 458 > ssh = "$PYTHON" "$TESTDIR/dummyssh"
459 459 > [extensions]
460 460 > localwrite = localwrite.py
461 461 > EOF
462 462
463 463 $ echo localwrite > foo
464 464 $ hg commit -m 'testing localwrite'
465 465 $ hg push
466 466 pushing to ssh://user@dummy/remote
467 467 searching for changes
468 468 remote: adding changesets
469 469 remote: adding manifests
470 470 remote: adding file changes
471 471 remote: added 1 changesets with 1 changes to 1 files
472 472 remote: KABOOM
473 473 remote: KABOOM IN PROCESS
474 474 local stdout
475 475
476 476 debug output
477 477
478 478 $ hg pull --debug ssh://user@dummy/remote
479 479 pulling from ssh://user@dummy/remote
480 running .* ".*/dummyssh" user@dummy ('|")hg -R remote serve --stdio('|") (re)
480 running .* ".*/dummyssh" ['"]user@dummy['"] ('|")hg -R remote serve --stdio('|") (re)
481 481 sending hello command
482 482 sending between command
483 483 remote: 355
484 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
485 485 remote: 1
486 486 query 1; heads
487 487 sending batch command
488 488 searching for changes
489 489 all remote heads known locally
490 490 no changes found
491 491 sending getbundle command
492 492 bundle2-input-bundle: with-transaction
493 493 bundle2-input-part: "listkeys" (params: 1 mandatory) supported
494 494 bundle2-input-part: total payload size 15
495 495 bundle2-input-part: "listkeys" (params: 1 mandatory) supported
496 496 bundle2-input-part: total payload size 45
497 497 bundle2-input-bundle: 1 parts total
498 498 checking for updated bookmarks
499 499
500 500 $ cd ..
501 501
502 502 $ cat dummylog
503 503 Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio
504 504 Got arguments 1:user@dummy 2:hg -R $TESTTMP/nonexistent serve --stdio
505 505 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
506 506 Got arguments 1:user@dummy 2:hg -R local-stream serve --stdio
507 507 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
508 508 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
509 509 Got arguments 1:user@dummy 2:hg -R doesnotexist serve --stdio
510 510 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
511 511 Got arguments 1:user@dummy 2:hg -R local serve --stdio
512 512 Got arguments 1:user@dummy 2:hg -R $TESTTMP/local serve --stdio
513 513 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
514 514 changegroup-in-remote hook: HG_BUNDLE2=1 HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=a28a9d1a809cab7d4e2fde4bee738a9ede948b60 HG_NODE_LAST=a28a9d1a809cab7d4e2fde4bee738a9ede948b60 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:ssh:$LOCALIP
515 515 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
516 516 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
517 517 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
518 518 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
519 519 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
520 520 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
521 521 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
522 522 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
523 523 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
524 524 changegroup-in-remote hook: HG_BUNDLE2=1 HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=1383141674ec756a6056f6a9097618482fe0f4a6 HG_NODE_LAST=1383141674ec756a6056f6a9097618482fe0f4a6 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:ssh:$LOCALIP
525 525 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
526 526 Got arguments 1:user@dummy 2:hg init 'a repo'
527 527 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
528 528 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
529 529 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
530 530 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
531 531 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
532 532 changegroup-in-remote hook: HG_BUNDLE2=1 HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=65c38f4125f9602c8db4af56530cc221d93b8ef8 HG_NODE_LAST=65c38f4125f9602c8db4af56530cc221d93b8ef8 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:ssh:$LOCALIP
533 533 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
534 534
535 535 remote hook failure is attributed to remote
536 536
537 537 $ cat > $TESTTMP/failhook << EOF
538 538 > def hook(ui, repo, **kwargs):
539 539 > ui.write('hook failure!\n')
540 540 > ui.flush()
541 541 > return 1
542 542 > EOF
543 543
544 544 $ echo "pretxnchangegroup.fail = python:$TESTTMP/failhook:hook" >> remote/.hg/hgrc
545 545
546 546 $ hg -q --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" clone ssh://user@dummy/remote hookout
547 547 $ cd hookout
548 548 $ touch hookfailure
549 549 $ hg -q commit -A -m 'remote hook failure'
550 550 $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" push
551 551 pushing to ssh://user@dummy/remote
552 552 searching for changes
553 553 remote: adding changesets
554 554 remote: adding manifests
555 555 remote: adding file changes
556 556 remote: added 1 changesets with 1 changes to 1 files
557 557 remote: hook failure!
558 558 remote: transaction abort!
559 559 remote: rollback completed
560 560 remote: pretxnchangegroup.fail hook failed
561 561 abort: push failed on remote
562 562 [255]
563 563
564 564 abort during pull is properly reported as such
565 565
566 566 $ echo morefoo >> ../remote/foo
567 567 $ hg -R ../remote commit --message "more foo to be pulled"
568 568 $ cat >> ../remote/.hg/hgrc << EOF
569 569 > [extensions]
570 570 > crash = ${TESTDIR}/crashgetbundler.py
571 571 > EOF
572 572 $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" pull
573 573 pulling from ssh://user@dummy/remote
574 574 searching for changes
575 575 remote: abort: this is an exercise
576 576 abort: pull failed on remote
577 577 [255]
General Comments 0
You need to be logged in to leave comments. Login now