##// END OF EJS Templates
subrepo: prohibit variable expansion on creation of hg subrepo (SEC)...
Yuya Nishihara -
r41550:6c10eba6 stable
parent child Browse files
Show More
@@ -1,1834 +1,1843
1 1 # subrepo.py - sub-repository classes and factory
2 2 #
3 3 # Copyright 2009-2010 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 copy
11 11 import errno
12 12 import hashlib
13 13 import os
14 14 import posixpath
15 15 import re
16 16 import stat
17 17 import subprocess
18 18 import sys
19 19 import tarfile
20 20 import xml.dom.minidom
21 21
22 22 from .i18n import _
23 23 from . import (
24 24 cmdutil,
25 25 encoding,
26 26 error,
27 27 exchange,
28 28 logcmdutil,
29 29 match as matchmod,
30 30 node,
31 31 pathutil,
32 32 phases,
33 33 pycompat,
34 34 scmutil,
35 35 subrepoutil,
36 36 util,
37 37 vfs as vfsmod,
38 38 )
39 39 from .utils import (
40 40 dateutil,
41 41 procutil,
42 42 stringutil,
43 43 )
44 44
45 45 hg = None
46 46 reporelpath = subrepoutil.reporelpath
47 47 subrelpath = subrepoutil.subrelpath
48 48 _abssource = subrepoutil._abssource
49 49 propertycache = util.propertycache
50 50
51 51 def _expandedabspath(path):
52 52 '''
53 53 get a path or url and if it is a path expand it and return an absolute path
54 54 '''
55 55 expandedpath = util.urllocalpath(util.expandpath(path))
56 56 u = util.url(expandedpath)
57 57 if not u.scheme:
58 58 path = util.normpath(os.path.abspath(u.path))
59 59 return path
60 60
61 61 def _getstorehashcachename(remotepath):
62 62 '''get a unique filename for the store hash cache of a remote repository'''
63 63 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
64 64
65 65 class SubrepoAbort(error.Abort):
66 66 """Exception class used to avoid handling a subrepo error more than once"""
67 67 def __init__(self, *args, **kw):
68 68 self.subrepo = kw.pop(r'subrepo', None)
69 69 self.cause = kw.pop(r'cause', None)
70 70 error.Abort.__init__(self, *args, **kw)
71 71
72 72 def annotatesubrepoerror(func):
73 73 def decoratedmethod(self, *args, **kargs):
74 74 try:
75 75 res = func(self, *args, **kargs)
76 76 except SubrepoAbort as ex:
77 77 # This exception has already been handled
78 78 raise ex
79 79 except error.Abort as ex:
80 80 subrepo = subrelpath(self)
81 81 errormsg = (stringutil.forcebytestr(ex) + ' '
82 82 + _('(in subrepository "%s")') % subrepo)
83 83 # avoid handling this exception by raising a SubrepoAbort exception
84 84 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
85 85 cause=sys.exc_info())
86 86 return res
87 87 return decoratedmethod
88 88
89 89 def _updateprompt(ui, sub, dirty, local, remote):
90 90 if dirty:
91 91 msg = (_(' subrepository sources for %s differ\n'
92 92 'use (l)ocal source (%s) or (r)emote source (%s)?'
93 93 '$$ &Local $$ &Remote')
94 94 % (subrelpath(sub), local, remote))
95 95 else:
96 96 msg = (_(' subrepository sources for %s differ (in checked out '
97 97 'version)\n'
98 98 'use (l)ocal source (%s) or (r)emote source (%s)?'
99 99 '$$ &Local $$ &Remote')
100 100 % (subrelpath(sub), local, remote))
101 101 return ui.promptchoice(msg, 0)
102 102
103 103 def _sanitize(ui, vfs, ignore):
104 104 for dirname, dirs, names in vfs.walk():
105 105 for i, d in enumerate(dirs):
106 106 if d.lower() == ignore:
107 107 del dirs[i]
108 108 break
109 109 if vfs.basename(dirname).lower() != '.hg':
110 110 continue
111 111 for f in names:
112 112 if f.lower() == 'hgrc':
113 113 ui.warn(_("warning: removing potentially hostile 'hgrc' "
114 114 "in '%s'\n") % vfs.join(dirname))
115 115 vfs.unlink(vfs.reljoin(dirname, f))
116 116
117 117 def _auditsubrepopath(repo, path):
118 118 # auditor doesn't check if the path itself is a symlink
119 119 pathutil.pathauditor(repo.root)(path)
120 120 if repo.wvfs.islink(path):
121 121 raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
122 122
123 123 SUBREPO_ALLOWED_DEFAULTS = {
124 124 'hg': True,
125 125 'git': False,
126 126 'svn': False,
127 127 }
128 128
129 129 def _checktype(ui, kind):
130 130 # subrepos.allowed is a master kill switch. If disabled, subrepos are
131 131 # disabled period.
132 132 if not ui.configbool('subrepos', 'allowed', True):
133 133 raise error.Abort(_('subrepos not enabled'),
134 134 hint=_("see 'hg help config.subrepos' for details"))
135 135
136 136 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
137 137 if not ui.configbool('subrepos', '%s:allowed' % kind, default):
138 138 raise error.Abort(_('%s subrepos not allowed') % kind,
139 139 hint=_("see 'hg help config.subrepos' for details"))
140 140
141 141 if kind not in types:
142 142 raise error.Abort(_('unknown subrepo type %s') % kind)
143 143
144 144 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
145 145 """return instance of the right subrepo class for subrepo in path"""
146 146 # subrepo inherently violates our import layering rules
147 147 # because it wants to make repo objects from deep inside the stack
148 148 # so we manually delay the circular imports to not break
149 149 # scripts that don't use our demand-loading
150 150 global hg
151 151 from . import hg as h
152 152 hg = h
153 153
154 154 repo = ctx.repo()
155 155 _auditsubrepopath(repo, path)
156 156 state = ctx.substate[path]
157 157 _checktype(repo.ui, state[2])
158 158 if allowwdir:
159 159 state = (state[0], ctx.subrev(path), state[2])
160 160 return types[state[2]](ctx, path, state[:2], allowcreate)
161 161
162 162 def nullsubrepo(ctx, path, pctx):
163 163 """return an empty subrepo in pctx for the extant subrepo in ctx"""
164 164 # subrepo inherently violates our import layering rules
165 165 # because it wants to make repo objects from deep inside the stack
166 166 # so we manually delay the circular imports to not break
167 167 # scripts that don't use our demand-loading
168 168 global hg
169 169 from . import hg as h
170 170 hg = h
171 171
172 172 repo = ctx.repo()
173 173 _auditsubrepopath(repo, path)
174 174 state = ctx.substate[path]
175 175 _checktype(repo.ui, state[2])
176 176 subrev = ''
177 177 if state[2] == 'hg':
178 178 subrev = "0" * 40
179 179 return types[state[2]](pctx, path, (state[0], subrev), True)
180 180
181 181 # subrepo classes need to implement the following abstract class:
182 182
183 183 class abstractsubrepo(object):
184 184
185 185 def __init__(self, ctx, path):
186 186 """Initialize abstractsubrepo part
187 187
188 188 ``ctx`` is the context referring this subrepository in the
189 189 parent repository.
190 190
191 191 ``path`` is the path to this subrepository as seen from
192 192 innermost repository.
193 193 """
194 194 self.ui = ctx.repo().ui
195 195 self._ctx = ctx
196 196 self._path = path
197 197
198 198 def addwebdirpath(self, serverpath, webconf):
199 199 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
200 200
201 201 ``serverpath`` is the path component of the URL for this repo.
202 202
203 203 ``webconf`` is the dictionary of hgwebdir entries.
204 204 """
205 205 pass
206 206
207 207 def storeclean(self, path):
208 208 """
209 209 returns true if the repository has not changed since it was last
210 210 cloned from or pushed to a given repository.
211 211 """
212 212 return False
213 213
214 214 def dirty(self, ignoreupdate=False, missing=False):
215 215 """returns true if the dirstate of the subrepo is dirty or does not
216 216 match current stored state. If ignoreupdate is true, only check
217 217 whether the subrepo has uncommitted changes in its dirstate. If missing
218 218 is true, check for deleted files.
219 219 """
220 220 raise NotImplementedError
221 221
222 222 def dirtyreason(self, ignoreupdate=False, missing=False):
223 223 """return reason string if it is ``dirty()``
224 224
225 225 Returned string should have enough information for the message
226 226 of exception.
227 227
228 228 This returns None, otherwise.
229 229 """
230 230 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
231 231 return _('uncommitted changes in subrepository "%s"'
232 232 ) % subrelpath(self)
233 233
234 234 def bailifchanged(self, ignoreupdate=False, hint=None):
235 235 """raise Abort if subrepository is ``dirty()``
236 236 """
237 237 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
238 238 missing=True)
239 239 if dirtyreason:
240 240 raise error.Abort(dirtyreason, hint=hint)
241 241
242 242 def basestate(self):
243 243 """current working directory base state, disregarding .hgsubstate
244 244 state and working directory modifications"""
245 245 raise NotImplementedError
246 246
247 247 def checknested(self, path):
248 248 """check if path is a subrepository within this repository"""
249 249 return False
250 250
251 251 def commit(self, text, user, date):
252 252 """commit the current changes to the subrepo with the given
253 253 log message. Use given user and date if possible. Return the
254 254 new state of the subrepo.
255 255 """
256 256 raise NotImplementedError
257 257
258 258 def phase(self, state):
259 259 """returns phase of specified state in the subrepository.
260 260 """
261 261 return phases.public
262 262
263 263 def remove(self):
264 264 """remove the subrepo
265 265
266 266 (should verify the dirstate is not dirty first)
267 267 """
268 268 raise NotImplementedError
269 269
270 270 def get(self, state, overwrite=False):
271 271 """run whatever commands are needed to put the subrepo into
272 272 this state
273 273 """
274 274 raise NotImplementedError
275 275
276 276 def merge(self, state):
277 277 """merge currently-saved state with the new state."""
278 278 raise NotImplementedError
279 279
280 280 def push(self, opts):
281 281 """perform whatever action is analogous to 'hg push'
282 282
283 283 This may be a no-op on some systems.
284 284 """
285 285 raise NotImplementedError
286 286
287 287 def add(self, ui, match, prefix, explicitonly, **opts):
288 288 return []
289 289
290 290 def addremove(self, matcher, prefix, opts):
291 291 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
292 292 return 1
293 293
294 294 def cat(self, match, fm, fntemplate, prefix, **opts):
295 295 return 1
296 296
297 297 def status(self, rev2, **opts):
298 298 return scmutil.status([], [], [], [], [], [], [])
299 299
300 300 def diff(self, ui, diffopts, node2, match, prefix, **opts):
301 301 pass
302 302
303 303 def outgoing(self, ui, dest, opts):
304 304 return 1
305 305
306 306 def incoming(self, ui, source, opts):
307 307 return 1
308 308
309 309 def files(self):
310 310 """return filename iterator"""
311 311 raise NotImplementedError
312 312
313 313 def filedata(self, name, decode):
314 314 """return file data, optionally passed through repo decoders"""
315 315 raise NotImplementedError
316 316
317 317 def fileflags(self, name):
318 318 """return file flags"""
319 319 return ''
320 320
321 321 def matchfileset(self, expr, badfn=None):
322 322 """Resolve the fileset expression for this repo"""
323 323 return matchmod.nevermatcher(self.wvfs.base, '', badfn=badfn)
324 324
325 325 def printfiles(self, ui, m, fm, fmt, subrepos):
326 326 """handle the files command for this subrepo"""
327 327 return 1
328 328
329 329 def archive(self, archiver, prefix, match=None, decode=True):
330 330 if match is not None:
331 331 files = [f for f in self.files() if match(f)]
332 332 else:
333 333 files = self.files()
334 334 total = len(files)
335 335 relpath = subrelpath(self)
336 336 progress = self.ui.makeprogress(_('archiving (%s)') % relpath,
337 337 unit=_('files'), total=total)
338 338 progress.update(0)
339 339 for name in files:
340 340 flags = self.fileflags(name)
341 341 mode = 'x' in flags and 0o755 or 0o644
342 342 symlink = 'l' in flags
343 343 archiver.addfile(prefix + self._path + '/' + name,
344 344 mode, symlink, self.filedata(name, decode))
345 345 progress.increment()
346 346 progress.complete()
347 347 return total
348 348
349 349 def walk(self, match):
350 350 '''
351 351 walk recursively through the directory tree, finding all files
352 352 matched by the match function
353 353 '''
354 354
355 355 def forget(self, match, prefix, dryrun, interactive):
356 356 return ([], [])
357 357
358 358 def removefiles(self, matcher, prefix, after, force, subrepos,
359 359 dryrun, warnings):
360 360 """remove the matched files from the subrepository and the filesystem,
361 361 possibly by force and/or after the file has been removed from the
362 362 filesystem. Return 0 on success, 1 on any warning.
363 363 """
364 364 warnings.append(_("warning: removefiles not implemented (%s)")
365 365 % self._path)
366 366 return 1
367 367
368 368 def revert(self, substate, *pats, **opts):
369 369 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
370 370 % (substate[0], substate[2]))
371 371 return []
372 372
373 373 def shortid(self, revid):
374 374 return revid
375 375
376 376 def unshare(self):
377 377 '''
378 378 convert this repository from shared to normal storage.
379 379 '''
380 380
381 381 def verify(self):
382 382 '''verify the integrity of the repository. Return 0 on success or
383 383 warning, 1 on any error.
384 384 '''
385 385 return 0
386 386
387 387 @propertycache
388 388 def wvfs(self):
389 389 """return vfs to access the working directory of this subrepository
390 390 """
391 391 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
392 392
393 393 @propertycache
394 394 def _relpath(self):
395 395 """return path to this subrepository as seen from outermost repository
396 396 """
397 397 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
398 398
399 399 class hgsubrepo(abstractsubrepo):
400 400 def __init__(self, ctx, path, state, allowcreate):
401 401 super(hgsubrepo, self).__init__(ctx, path)
402 402 self._state = state
403 403 r = ctx.repo()
404 404 root = r.wjoin(path)
405 405 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
406 # repository constructor does expand variables in path, which is
407 # unsafe since subrepo path might come from untrusted source.
408 if os.path.realpath(util.expandpath(root)) != root:
409 raise error.Abort(_('subrepo path contains illegal component: %s')
410 % path)
406 411 self._repo = hg.repository(r.baseui, root, create=create)
412 if self._repo.root != root:
413 raise error.ProgrammingError('failed to reject unsafe subrepo '
414 'path: %s (expanded to %s)'
415 % (root, self._repo.root))
407 416
408 417 # Propagate the parent's --hidden option
409 418 if r is r.unfiltered():
410 419 self._repo = self._repo.unfiltered()
411 420
412 421 self.ui = self._repo.ui
413 422 for s, k in [('ui', 'commitsubrepos')]:
414 423 v = r.ui.config(s, k)
415 424 if v:
416 425 self.ui.setconfig(s, k, v, 'subrepo')
417 426 # internal config: ui._usedassubrepo
418 427 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
419 428 self._initrepo(r, state[0], create)
420 429
421 430 @annotatesubrepoerror
422 431 def addwebdirpath(self, serverpath, webconf):
423 432 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
424 433
425 434 def storeclean(self, path):
426 435 with self._repo.lock():
427 436 return self._storeclean(path)
428 437
429 438 def _storeclean(self, path):
430 439 clean = True
431 440 itercache = self._calcstorehash(path)
432 441 for filehash in self._readstorehashcache(path):
433 442 if filehash != next(itercache, None):
434 443 clean = False
435 444 break
436 445 if clean:
437 446 # if not empty:
438 447 # the cached and current pull states have a different size
439 448 clean = next(itercache, None) is None
440 449 return clean
441 450
442 451 def _calcstorehash(self, remotepath):
443 452 '''calculate a unique "store hash"
444 453
445 454 This method is used to to detect when there are changes that may
446 455 require a push to a given remote path.'''
447 456 # sort the files that will be hashed in increasing (likely) file size
448 457 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
449 458 yield '# %s\n' % _expandedabspath(remotepath)
450 459 vfs = self._repo.vfs
451 460 for relname in filelist:
452 461 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
453 462 yield '%s = %s\n' % (relname, filehash)
454 463
455 464 @propertycache
456 465 def _cachestorehashvfs(self):
457 466 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
458 467
459 468 def _readstorehashcache(self, remotepath):
460 469 '''read the store hash cache for a given remote repository'''
461 470 cachefile = _getstorehashcachename(remotepath)
462 471 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
463 472
464 473 def _cachestorehash(self, remotepath):
465 474 '''cache the current store hash
466 475
467 476 Each remote repo requires its own store hash cache, because a subrepo
468 477 store may be "clean" versus a given remote repo, but not versus another
469 478 '''
470 479 cachefile = _getstorehashcachename(remotepath)
471 480 with self._repo.lock():
472 481 storehash = list(self._calcstorehash(remotepath))
473 482 vfs = self._cachestorehashvfs
474 483 vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
475 484
476 485 def _getctx(self):
477 486 '''fetch the context for this subrepo revision, possibly a workingctx
478 487 '''
479 488 if self._ctx.rev() is None:
480 489 return self._repo[None] # workingctx if parent is workingctx
481 490 else:
482 491 rev = self._state[1]
483 492 return self._repo[rev]
484 493
485 494 @annotatesubrepoerror
486 495 def _initrepo(self, parentrepo, source, create):
487 496 self._repo._subparent = parentrepo
488 497 self._repo._subsource = source
489 498
490 499 if create:
491 500 lines = ['[paths]\n']
492 501
493 502 def addpathconfig(key, value):
494 503 if value:
495 504 lines.append('%s = %s\n' % (key, value))
496 505 self.ui.setconfig('paths', key, value, 'subrepo')
497 506
498 507 defpath = _abssource(self._repo, abort=False)
499 508 defpushpath = _abssource(self._repo, True, abort=False)
500 509 addpathconfig('default', defpath)
501 510 if defpath != defpushpath:
502 511 addpathconfig('default-push', defpushpath)
503 512
504 513 self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
505 514
506 515 @annotatesubrepoerror
507 516 def add(self, ui, match, prefix, explicitonly, **opts):
508 517 return cmdutil.add(ui, self._repo, match,
509 518 self.wvfs.reljoin(prefix, self._path),
510 519 explicitonly, **opts)
511 520
512 521 @annotatesubrepoerror
513 522 def addremove(self, m, prefix, opts):
514 523 # In the same way as sub directories are processed, once in a subrepo,
515 524 # always entry any of its subrepos. Don't corrupt the options that will
516 525 # be used to process sibling subrepos however.
517 526 opts = copy.copy(opts)
518 527 opts['subrepos'] = True
519 528 return scmutil.addremove(self._repo, m,
520 529 self.wvfs.reljoin(prefix, self._path), opts)
521 530
522 531 @annotatesubrepoerror
523 532 def cat(self, match, fm, fntemplate, prefix, **opts):
524 533 rev = self._state[1]
525 534 ctx = self._repo[rev]
526 535 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
527 536 prefix, **opts)
528 537
529 538 @annotatesubrepoerror
530 539 def status(self, rev2, **opts):
531 540 try:
532 541 rev1 = self._state[1]
533 542 ctx1 = self._repo[rev1]
534 543 ctx2 = self._repo[rev2]
535 544 return self._repo.status(ctx1, ctx2, **opts)
536 545 except error.RepoLookupError as inst:
537 546 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
538 547 % (inst, subrelpath(self)))
539 548 return scmutil.status([], [], [], [], [], [], [])
540 549
541 550 @annotatesubrepoerror
542 551 def diff(self, ui, diffopts, node2, match, prefix, **opts):
543 552 try:
544 553 node1 = node.bin(self._state[1])
545 554 # We currently expect node2 to come from substate and be
546 555 # in hex format
547 556 if node2 is not None:
548 557 node2 = node.bin(node2)
549 558 logcmdutil.diffordiffstat(ui, self._repo, diffopts,
550 559 node1, node2, match,
551 560 prefix=posixpath.join(prefix, self._path),
552 561 listsubrepos=True, **opts)
553 562 except error.RepoLookupError as inst:
554 563 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
555 564 % (inst, subrelpath(self)))
556 565
557 566 @annotatesubrepoerror
558 567 def archive(self, archiver, prefix, match=None, decode=True):
559 568 self._get(self._state + ('hg',))
560 569 files = self.files()
561 570 if match:
562 571 files = [f for f in files if match(f)]
563 572 rev = self._state[1]
564 573 ctx = self._repo[rev]
565 574 scmutil.prefetchfiles(self._repo, [ctx.rev()],
566 575 scmutil.matchfiles(self._repo, files))
567 576 total = abstractsubrepo.archive(self, archiver, prefix, match)
568 577 for subpath in ctx.substate:
569 578 s = subrepo(ctx, subpath, True)
570 579 submatch = matchmod.subdirmatcher(subpath, match)
571 580 total += s.archive(archiver, prefix + self._path + '/', submatch,
572 581 decode)
573 582 return total
574 583
575 584 @annotatesubrepoerror
576 585 def dirty(self, ignoreupdate=False, missing=False):
577 586 r = self._state[1]
578 587 if r == '' and not ignoreupdate: # no state recorded
579 588 return True
580 589 w = self._repo[None]
581 590 if r != w.p1().hex() and not ignoreupdate:
582 591 # different version checked out
583 592 return True
584 593 return w.dirty(missing=missing) # working directory changed
585 594
586 595 def basestate(self):
587 596 return self._repo['.'].hex()
588 597
589 598 def checknested(self, path):
590 599 return self._repo._checknested(self._repo.wjoin(path))
591 600
592 601 @annotatesubrepoerror
593 602 def commit(self, text, user, date):
594 603 # don't bother committing in the subrepo if it's only been
595 604 # updated
596 605 if not self.dirty(True):
597 606 return self._repo['.'].hex()
598 607 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
599 608 n = self._repo.commit(text, user, date)
600 609 if not n:
601 610 return self._repo['.'].hex() # different version checked out
602 611 return node.hex(n)
603 612
604 613 @annotatesubrepoerror
605 614 def phase(self, state):
606 615 return self._repo[state or '.'].phase()
607 616
608 617 @annotatesubrepoerror
609 618 def remove(self):
610 619 # we can't fully delete the repository as it may contain
611 620 # local-only history
612 621 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
613 622 hg.clean(self._repo, node.nullid, False)
614 623
615 624 def _get(self, state):
616 625 source, revision, kind = state
617 626 parentrepo = self._repo._subparent
618 627
619 628 if revision in self._repo.unfiltered():
620 629 # Allow shared subrepos tracked at null to setup the sharedpath
621 630 if len(self._repo) != 0 or not parentrepo.shared():
622 631 return True
623 632 self._repo._subsource = source
624 633 srcurl = _abssource(self._repo)
625 634
626 635 # Defer creating the peer until after the status message is logged, in
627 636 # case there are network problems.
628 637 getpeer = lambda: hg.peer(self._repo, {}, srcurl)
629 638
630 639 if len(self._repo) == 0:
631 640 # use self._repo.vfs instead of self.wvfs to remove .hg only
632 641 self._repo.vfs.rmtree()
633 642
634 643 # A remote subrepo could be shared if there is a local copy
635 644 # relative to the parent's share source. But clone pooling doesn't
636 645 # assemble the repos in a tree, so that can't be consistently done.
637 646 # A simpler option is for the user to configure clone pooling, and
638 647 # work with that.
639 648 if parentrepo.shared() and hg.islocal(srcurl):
640 649 self.ui.status(_('sharing subrepo %s from %s\n')
641 650 % (subrelpath(self), srcurl))
642 651 shared = hg.share(self._repo._subparent.baseui,
643 652 getpeer(), self._repo.root,
644 653 update=False, bookmarks=False)
645 654 self._repo = shared.local()
646 655 else:
647 656 # TODO: find a common place for this and this code in the
648 657 # share.py wrap of the clone command.
649 658 if parentrepo.shared():
650 659 pool = self.ui.config('share', 'pool')
651 660 if pool:
652 661 pool = util.expandpath(pool)
653 662
654 663 shareopts = {
655 664 'pool': pool,
656 665 'mode': self.ui.config('share', 'poolnaming'),
657 666 }
658 667 else:
659 668 shareopts = {}
660 669
661 670 self.ui.status(_('cloning subrepo %s from %s\n')
662 671 % (subrelpath(self), util.hidepassword(srcurl)))
663 672 other, cloned = hg.clone(self._repo._subparent.baseui, {},
664 673 getpeer(), self._repo.root,
665 674 update=False, shareopts=shareopts)
666 675 self._repo = cloned.local()
667 676 self._initrepo(parentrepo, source, create=True)
668 677 self._cachestorehash(srcurl)
669 678 else:
670 679 self.ui.status(_('pulling subrepo %s from %s\n')
671 680 % (subrelpath(self), util.hidepassword(srcurl)))
672 681 cleansub = self.storeclean(srcurl)
673 682 exchange.pull(self._repo, getpeer())
674 683 if cleansub:
675 684 # keep the repo clean after pull
676 685 self._cachestorehash(srcurl)
677 686 return False
678 687
679 688 @annotatesubrepoerror
680 689 def get(self, state, overwrite=False):
681 690 inrepo = self._get(state)
682 691 source, revision, kind = state
683 692 repo = self._repo
684 693 repo.ui.debug("getting subrepo %s\n" % self._path)
685 694 if inrepo:
686 695 urepo = repo.unfiltered()
687 696 ctx = urepo[revision]
688 697 if ctx.hidden():
689 698 urepo.ui.warn(
690 699 _('revision %s in subrepository "%s" is hidden\n') \
691 700 % (revision[0:12], self._path))
692 701 repo = urepo
693 702 hg.updaterepo(repo, revision, overwrite)
694 703
695 704 @annotatesubrepoerror
696 705 def merge(self, state):
697 706 self._get(state)
698 707 cur = self._repo['.']
699 708 dst = self._repo[state[1]]
700 709 anc = dst.ancestor(cur)
701 710
702 711 def mergefunc():
703 712 if anc == cur and dst.branch() == cur.branch():
704 713 self.ui.debug('updating subrepository "%s"\n'
705 714 % subrelpath(self))
706 715 hg.update(self._repo, state[1])
707 716 elif anc == dst:
708 717 self.ui.debug('skipping subrepository "%s"\n'
709 718 % subrelpath(self))
710 719 else:
711 720 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
712 721 hg.merge(self._repo, state[1], remind=False)
713 722
714 723 wctx = self._repo[None]
715 724 if self.dirty():
716 725 if anc != dst:
717 726 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
718 727 mergefunc()
719 728 else:
720 729 mergefunc()
721 730 else:
722 731 mergefunc()
723 732
724 733 @annotatesubrepoerror
725 734 def push(self, opts):
726 735 force = opts.get('force')
727 736 newbranch = opts.get('new_branch')
728 737 ssh = opts.get('ssh')
729 738
730 739 # push subrepos depth-first for coherent ordering
731 740 c = self._repo['.']
732 741 subs = c.substate # only repos that are committed
733 742 for s in sorted(subs):
734 743 if c.sub(s).push(opts) == 0:
735 744 return False
736 745
737 746 dsturl = _abssource(self._repo, True)
738 747 if not force:
739 748 if self.storeclean(dsturl):
740 749 self.ui.status(
741 750 _('no changes made to subrepo %s since last push to %s\n')
742 751 % (subrelpath(self), util.hidepassword(dsturl)))
743 752 return None
744 753 self.ui.status(_('pushing subrepo %s to %s\n') %
745 754 (subrelpath(self), util.hidepassword(dsturl)))
746 755 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
747 756 res = exchange.push(self._repo, other, force, newbranch=newbranch)
748 757
749 758 # the repo is now clean
750 759 self._cachestorehash(dsturl)
751 760 return res.cgresult
752 761
753 762 @annotatesubrepoerror
754 763 def outgoing(self, ui, dest, opts):
755 764 if 'rev' in opts or 'branch' in opts:
756 765 opts = copy.copy(opts)
757 766 opts.pop('rev', None)
758 767 opts.pop('branch', None)
759 768 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
760 769
761 770 @annotatesubrepoerror
762 771 def incoming(self, ui, source, opts):
763 772 if 'rev' in opts or 'branch' in opts:
764 773 opts = copy.copy(opts)
765 774 opts.pop('rev', None)
766 775 opts.pop('branch', None)
767 776 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
768 777
769 778 @annotatesubrepoerror
770 779 def files(self):
771 780 rev = self._state[1]
772 781 ctx = self._repo[rev]
773 782 return ctx.manifest().keys()
774 783
775 784 def filedata(self, name, decode):
776 785 rev = self._state[1]
777 786 data = self._repo[rev][name].data()
778 787 if decode:
779 788 data = self._repo.wwritedata(name, data)
780 789 return data
781 790
782 791 def fileflags(self, name):
783 792 rev = self._state[1]
784 793 ctx = self._repo[rev]
785 794 return ctx.flags(name)
786 795
787 796 @annotatesubrepoerror
788 797 def printfiles(self, ui, m, fm, fmt, subrepos):
789 798 # If the parent context is a workingctx, use the workingctx here for
790 799 # consistency.
791 800 if self._ctx.rev() is None:
792 801 ctx = self._repo[None]
793 802 else:
794 803 rev = self._state[1]
795 804 ctx = self._repo[rev]
796 805 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
797 806
798 807 @annotatesubrepoerror
799 808 def matchfileset(self, expr, badfn=None):
800 809 repo = self._repo
801 810 if self._ctx.rev() is None:
802 811 ctx = repo[None]
803 812 else:
804 813 rev = self._state[1]
805 814 ctx = repo[rev]
806 815
807 816 matchers = [ctx.matchfileset(expr, badfn=badfn)]
808 817
809 818 for subpath in ctx.substate:
810 819 sub = ctx.sub(subpath)
811 820
812 821 try:
813 822 sm = sub.matchfileset(expr, badfn=badfn)
814 823 pm = matchmod.prefixdirmatcher(repo.root, repo.getcwd(),
815 824 subpath, sm, badfn=badfn)
816 825 matchers.append(pm)
817 826 except error.LookupError:
818 827 self.ui.status(_("skipping missing subrepository: %s\n")
819 828 % self.wvfs.reljoin(reporelpath(self), subpath))
820 829 if len(matchers) == 1:
821 830 return matchers[0]
822 831 return matchmod.unionmatcher(matchers)
823 832
824 833 def walk(self, match):
825 834 ctx = self._repo[None]
826 835 return ctx.walk(match)
827 836
828 837 @annotatesubrepoerror
829 838 def forget(self, match, prefix, dryrun, interactive):
830 839 return cmdutil.forget(self.ui, self._repo, match,
831 840 self.wvfs.reljoin(prefix, self._path),
832 841 True, dryrun=dryrun, interactive=interactive)
833 842
834 843 @annotatesubrepoerror
835 844 def removefiles(self, matcher, prefix, after, force, subrepos,
836 845 dryrun, warnings):
837 846 return cmdutil.remove(self.ui, self._repo, matcher,
838 847 self.wvfs.reljoin(prefix, self._path),
839 848 after, force, subrepos, dryrun)
840 849
841 850 @annotatesubrepoerror
842 851 def revert(self, substate, *pats, **opts):
843 852 # reverting a subrepo is a 2 step process:
844 853 # 1. if the no_backup is not set, revert all modified
845 854 # files inside the subrepo
846 855 # 2. update the subrepo to the revision specified in
847 856 # the corresponding substate dictionary
848 857 self.ui.status(_('reverting subrepo %s\n') % substate[0])
849 858 if not opts.get(r'no_backup'):
850 859 # Revert all files on the subrepo, creating backups
851 860 # Note that this will not recursively revert subrepos
852 861 # We could do it if there was a set:subrepos() predicate
853 862 opts = opts.copy()
854 863 opts[r'date'] = None
855 864 opts[r'rev'] = substate[1]
856 865
857 866 self.filerevert(*pats, **opts)
858 867
859 868 # Update the repo to the revision specified in the given substate
860 869 if not opts.get(r'dry_run'):
861 870 self.get(substate, overwrite=True)
862 871
863 872 def filerevert(self, *pats, **opts):
864 873 ctx = self._repo[opts[r'rev']]
865 874 parents = self._repo.dirstate.parents()
866 875 if opts.get(r'all'):
867 876 pats = ['set:modified()']
868 877 else:
869 878 pats = []
870 879 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
871 880
872 881 def shortid(self, revid):
873 882 return revid[:12]
874 883
875 884 @annotatesubrepoerror
876 885 def unshare(self):
877 886 # subrepo inherently violates our import layering rules
878 887 # because it wants to make repo objects from deep inside the stack
879 888 # so we manually delay the circular imports to not break
880 889 # scripts that don't use our demand-loading
881 890 global hg
882 891 from . import hg as h
883 892 hg = h
884 893
885 894 # Nothing prevents a user from sharing in a repo, and then making that a
886 895 # subrepo. Alternately, the previous unshare attempt may have failed
887 896 # part way through. So recurse whether or not this layer is shared.
888 897 if self._repo.shared():
889 898 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
890 899
891 900 hg.unshare(self.ui, self._repo)
892 901
893 902 def verify(self):
894 903 try:
895 904 rev = self._state[1]
896 905 ctx = self._repo.unfiltered()[rev]
897 906 if ctx.hidden():
898 907 # Since hidden revisions aren't pushed/pulled, it seems worth an
899 908 # explicit warning.
900 909 ui = self._repo.ui
901 910 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
902 911 (self._relpath, node.short(self._ctx.node())))
903 912 return 0
904 913 except error.RepoLookupError:
905 914 # A missing subrepo revision may be a case of needing to pull it, so
906 915 # don't treat this as an error.
907 916 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
908 917 (self._relpath, node.short(self._ctx.node())))
909 918 return 0
910 919
911 920 @propertycache
912 921 def wvfs(self):
913 922 """return own wvfs for efficiency and consistency
914 923 """
915 924 return self._repo.wvfs
916 925
917 926 @propertycache
918 927 def _relpath(self):
919 928 """return path to this subrepository as seen from outermost repository
920 929 """
921 930 # Keep consistent dir separators by avoiding vfs.join(self._path)
922 931 return reporelpath(self._repo)
923 932
924 933 class svnsubrepo(abstractsubrepo):
925 934 def __init__(self, ctx, path, state, allowcreate):
926 935 super(svnsubrepo, self).__init__(ctx, path)
927 936 self._state = state
928 937 self._exe = procutil.findexe('svn')
929 938 if not self._exe:
930 939 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
931 940 % self._path)
932 941
933 942 def _svncommand(self, commands, filename='', failok=False):
934 943 cmd = [self._exe]
935 944 extrakw = {}
936 945 if not self.ui.interactive():
937 946 # Making stdin be a pipe should prevent svn from behaving
938 947 # interactively even if we can't pass --non-interactive.
939 948 extrakw[r'stdin'] = subprocess.PIPE
940 949 # Starting in svn 1.5 --non-interactive is a global flag
941 950 # instead of being per-command, but we need to support 1.4 so
942 951 # we have to be intelligent about what commands take
943 952 # --non-interactive.
944 953 if commands[0] in ('update', 'checkout', 'commit'):
945 954 cmd.append('--non-interactive')
946 955 cmd.extend(commands)
947 956 if filename is not None:
948 957 path = self.wvfs.reljoin(self._ctx.repo().origroot,
949 958 self._path, filename)
950 959 cmd.append(path)
951 960 env = dict(encoding.environ)
952 961 # Avoid localized output, preserve current locale for everything else.
953 962 lc_all = env.get('LC_ALL')
954 963 if lc_all:
955 964 env['LANG'] = lc_all
956 965 del env['LC_ALL']
957 966 env['LC_MESSAGES'] = 'C'
958 967 p = subprocess.Popen(pycompat.rapply(procutil.tonativestr, cmd),
959 968 bufsize=-1, close_fds=procutil.closefds,
960 969 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
961 970 universal_newlines=True,
962 971 env=procutil.tonativeenv(env), **extrakw)
963 972 stdout, stderr = p.communicate()
964 973 stderr = stderr.strip()
965 974 if not failok:
966 975 if p.returncode:
967 976 raise error.Abort(stderr or 'exited with code %d'
968 977 % p.returncode)
969 978 if stderr:
970 979 self.ui.warn(stderr + '\n')
971 980 return stdout, stderr
972 981
973 982 @propertycache
974 983 def _svnversion(self):
975 984 output, err = self._svncommand(['--version', '--quiet'], filename=None)
976 985 m = re.search(br'^(\d+)\.(\d+)', output)
977 986 if not m:
978 987 raise error.Abort(_('cannot retrieve svn tool version'))
979 988 return (int(m.group(1)), int(m.group(2)))
980 989
981 990 def _svnmissing(self):
982 991 return not self.wvfs.exists('.svn')
983 992
984 993 def _wcrevs(self):
985 994 # Get the working directory revision as well as the last
986 995 # commit revision so we can compare the subrepo state with
987 996 # both. We used to store the working directory one.
988 997 output, err = self._svncommand(['info', '--xml'])
989 998 doc = xml.dom.minidom.parseString(output)
990 999 entries = doc.getElementsByTagName('entry')
991 1000 lastrev, rev = '0', '0'
992 1001 if entries:
993 1002 rev = str(entries[0].getAttribute('revision')) or '0'
994 1003 commits = entries[0].getElementsByTagName('commit')
995 1004 if commits:
996 1005 lastrev = str(commits[0].getAttribute('revision')) or '0'
997 1006 return (lastrev, rev)
998 1007
999 1008 def _wcrev(self):
1000 1009 return self._wcrevs()[0]
1001 1010
1002 1011 def _wcchanged(self):
1003 1012 """Return (changes, extchanges, missing) where changes is True
1004 1013 if the working directory was changed, extchanges is
1005 1014 True if any of these changes concern an external entry and missing
1006 1015 is True if any change is a missing entry.
1007 1016 """
1008 1017 output, err = self._svncommand(['status', '--xml'])
1009 1018 externals, changes, missing = [], [], []
1010 1019 doc = xml.dom.minidom.parseString(output)
1011 1020 for e in doc.getElementsByTagName('entry'):
1012 1021 s = e.getElementsByTagName('wc-status')
1013 1022 if not s:
1014 1023 continue
1015 1024 item = s[0].getAttribute('item')
1016 1025 props = s[0].getAttribute('props')
1017 1026 path = e.getAttribute('path')
1018 1027 if item == 'external':
1019 1028 externals.append(path)
1020 1029 elif item == 'missing':
1021 1030 missing.append(path)
1022 1031 if (item not in ('', 'normal', 'unversioned', 'external')
1023 1032 or props not in ('', 'none', 'normal')):
1024 1033 changes.append(path)
1025 1034 for path in changes:
1026 1035 for ext in externals:
1027 1036 if path == ext or path.startswith(ext + pycompat.ossep):
1028 1037 return True, True, bool(missing)
1029 1038 return bool(changes), False, bool(missing)
1030 1039
1031 1040 @annotatesubrepoerror
1032 1041 def dirty(self, ignoreupdate=False, missing=False):
1033 1042 if self._svnmissing():
1034 1043 return self._state[1] != ''
1035 1044 wcchanged = self._wcchanged()
1036 1045 changed = wcchanged[0] or (missing and wcchanged[2])
1037 1046 if not changed:
1038 1047 if self._state[1] in self._wcrevs() or ignoreupdate:
1039 1048 return False
1040 1049 return True
1041 1050
1042 1051 def basestate(self):
1043 1052 lastrev, rev = self._wcrevs()
1044 1053 if lastrev != rev:
1045 1054 # Last committed rev is not the same than rev. We would
1046 1055 # like to take lastrev but we do not know if the subrepo
1047 1056 # URL exists at lastrev. Test it and fallback to rev it
1048 1057 # is not there.
1049 1058 try:
1050 1059 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1051 1060 return lastrev
1052 1061 except error.Abort:
1053 1062 pass
1054 1063 return rev
1055 1064
1056 1065 @annotatesubrepoerror
1057 1066 def commit(self, text, user, date):
1058 1067 # user and date are out of our hands since svn is centralized
1059 1068 changed, extchanged, missing = self._wcchanged()
1060 1069 if not changed:
1061 1070 return self.basestate()
1062 1071 if extchanged:
1063 1072 # Do not try to commit externals
1064 1073 raise error.Abort(_('cannot commit svn externals'))
1065 1074 if missing:
1066 1075 # svn can commit with missing entries but aborting like hg
1067 1076 # seems a better approach.
1068 1077 raise error.Abort(_('cannot commit missing svn entries'))
1069 1078 commitinfo, err = self._svncommand(['commit', '-m', text])
1070 1079 self.ui.status(commitinfo)
1071 1080 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1072 1081 if not newrev:
1073 1082 if not commitinfo.strip():
1074 1083 # Sometimes, our definition of "changed" differs from
1075 1084 # svn one. For instance, svn ignores missing files
1076 1085 # when committing. If there are only missing files, no
1077 1086 # commit is made, no output and no error code.
1078 1087 raise error.Abort(_('failed to commit svn changes'))
1079 1088 raise error.Abort(commitinfo.splitlines()[-1])
1080 1089 newrev = newrev.groups()[0]
1081 1090 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1082 1091 return newrev
1083 1092
1084 1093 @annotatesubrepoerror
1085 1094 def remove(self):
1086 1095 if self.dirty():
1087 1096 self.ui.warn(_('not removing repo %s because '
1088 1097 'it has changes.\n') % self._path)
1089 1098 return
1090 1099 self.ui.note(_('removing subrepo %s\n') % self._path)
1091 1100
1092 1101 self.wvfs.rmtree(forcibly=True)
1093 1102 try:
1094 1103 pwvfs = self._ctx.repo().wvfs
1095 1104 pwvfs.removedirs(pwvfs.dirname(self._path))
1096 1105 except OSError:
1097 1106 pass
1098 1107
1099 1108 @annotatesubrepoerror
1100 1109 def get(self, state, overwrite=False):
1101 1110 if overwrite:
1102 1111 self._svncommand(['revert', '--recursive'])
1103 1112 args = ['checkout']
1104 1113 if self._svnversion >= (1, 5):
1105 1114 args.append('--force')
1106 1115 # The revision must be specified at the end of the URL to properly
1107 1116 # update to a directory which has since been deleted and recreated.
1108 1117 args.append('%s@%s' % (state[0], state[1]))
1109 1118
1110 1119 # SEC: check that the ssh url is safe
1111 1120 util.checksafessh(state[0])
1112 1121
1113 1122 status, err = self._svncommand(args, failok=True)
1114 1123 _sanitize(self.ui, self.wvfs, '.svn')
1115 1124 if not re.search('Checked out revision [0-9]+.', status):
1116 1125 if ('is already a working copy for a different URL' in err
1117 1126 and (self._wcchanged()[:2] == (False, False))):
1118 1127 # obstructed but clean working copy, so just blow it away.
1119 1128 self.remove()
1120 1129 self.get(state, overwrite=False)
1121 1130 return
1122 1131 raise error.Abort((status or err).splitlines()[-1])
1123 1132 self.ui.status(status)
1124 1133
1125 1134 @annotatesubrepoerror
1126 1135 def merge(self, state):
1127 1136 old = self._state[1]
1128 1137 new = state[1]
1129 1138 wcrev = self._wcrev()
1130 1139 if new != wcrev:
1131 1140 dirty = old == wcrev or self._wcchanged()[0]
1132 1141 if _updateprompt(self.ui, self, dirty, wcrev, new):
1133 1142 self.get(state, False)
1134 1143
1135 1144 def push(self, opts):
1136 1145 # push is a no-op for SVN
1137 1146 return True
1138 1147
1139 1148 @annotatesubrepoerror
1140 1149 def files(self):
1141 1150 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1142 1151 doc = xml.dom.minidom.parseString(output)
1143 1152 paths = []
1144 1153 for e in doc.getElementsByTagName('entry'):
1145 1154 kind = pycompat.bytestr(e.getAttribute('kind'))
1146 1155 if kind != 'file':
1147 1156 continue
1148 1157 name = ''.join(c.data for c
1149 1158 in e.getElementsByTagName('name')[0].childNodes
1150 1159 if c.nodeType == c.TEXT_NODE)
1151 1160 paths.append(name.encode('utf-8'))
1152 1161 return paths
1153 1162
1154 1163 def filedata(self, name, decode):
1155 1164 return self._svncommand(['cat'], name)[0]
1156 1165
1157 1166
1158 1167 class gitsubrepo(abstractsubrepo):
1159 1168 def __init__(self, ctx, path, state, allowcreate):
1160 1169 super(gitsubrepo, self).__init__(ctx, path)
1161 1170 self._state = state
1162 1171 self._abspath = ctx.repo().wjoin(path)
1163 1172 self._subparent = ctx.repo()
1164 1173 self._ensuregit()
1165 1174
1166 1175 def _ensuregit(self):
1167 1176 try:
1168 1177 self._gitexecutable = 'git'
1169 1178 out, err = self._gitnodir(['--version'])
1170 1179 except OSError as e:
1171 1180 genericerror = _("error executing git for subrepo '%s': %s")
1172 1181 notfoundhint = _("check git is installed and in your PATH")
1173 1182 if e.errno != errno.ENOENT:
1174 1183 raise error.Abort(genericerror % (
1175 1184 self._path, encoding.strtolocal(e.strerror)))
1176 1185 elif pycompat.iswindows:
1177 1186 try:
1178 1187 self._gitexecutable = 'git.cmd'
1179 1188 out, err = self._gitnodir(['--version'])
1180 1189 except OSError as e2:
1181 1190 if e2.errno == errno.ENOENT:
1182 1191 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1183 1192 " for subrepo '%s'") % self._path,
1184 1193 hint=notfoundhint)
1185 1194 else:
1186 1195 raise error.Abort(genericerror % (self._path,
1187 1196 encoding.strtolocal(e2.strerror)))
1188 1197 else:
1189 1198 raise error.Abort(_("couldn't find git for subrepo '%s'")
1190 1199 % self._path, hint=notfoundhint)
1191 1200 versionstatus = self._checkversion(out)
1192 1201 if versionstatus == 'unknown':
1193 1202 self.ui.warn(_('cannot retrieve git version\n'))
1194 1203 elif versionstatus == 'abort':
1195 1204 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1196 1205 elif versionstatus == 'warning':
1197 1206 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1198 1207
1199 1208 @staticmethod
1200 1209 def _gitversion(out):
1201 1210 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1202 1211 if m:
1203 1212 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1204 1213
1205 1214 m = re.search(br'^git version (\d+)\.(\d+)', out)
1206 1215 if m:
1207 1216 return (int(m.group(1)), int(m.group(2)), 0)
1208 1217
1209 1218 return -1
1210 1219
1211 1220 @staticmethod
1212 1221 def _checkversion(out):
1213 1222 '''ensure git version is new enough
1214 1223
1215 1224 >>> _checkversion = gitsubrepo._checkversion
1216 1225 >>> _checkversion(b'git version 1.6.0')
1217 1226 'ok'
1218 1227 >>> _checkversion(b'git version 1.8.5')
1219 1228 'ok'
1220 1229 >>> _checkversion(b'git version 1.4.0')
1221 1230 'abort'
1222 1231 >>> _checkversion(b'git version 1.5.0')
1223 1232 'warning'
1224 1233 >>> _checkversion(b'git version 1.9-rc0')
1225 1234 'ok'
1226 1235 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1227 1236 'ok'
1228 1237 >>> _checkversion(b'git version 1.9.0.GIT')
1229 1238 'ok'
1230 1239 >>> _checkversion(b'git version 12345')
1231 1240 'unknown'
1232 1241 >>> _checkversion(b'no')
1233 1242 'unknown'
1234 1243 '''
1235 1244 version = gitsubrepo._gitversion(out)
1236 1245 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1237 1246 # despite the docstring comment. For now, error on 1.4.0, warn on
1238 1247 # 1.5.0 but attempt to continue.
1239 1248 if version == -1:
1240 1249 return 'unknown'
1241 1250 if version < (1, 5, 0):
1242 1251 return 'abort'
1243 1252 elif version < (1, 6, 0):
1244 1253 return 'warning'
1245 1254 return 'ok'
1246 1255
1247 1256 def _gitcommand(self, commands, env=None, stream=False):
1248 1257 return self._gitdir(commands, env=env, stream=stream)[0]
1249 1258
1250 1259 def _gitdir(self, commands, env=None, stream=False):
1251 1260 return self._gitnodir(commands, env=env, stream=stream,
1252 1261 cwd=self._abspath)
1253 1262
1254 1263 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1255 1264 """Calls the git command
1256 1265
1257 1266 The methods tries to call the git command. versions prior to 1.6.0
1258 1267 are not supported and very probably fail.
1259 1268 """
1260 1269 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1261 1270 if env is None:
1262 1271 env = encoding.environ.copy()
1263 1272 # disable localization for Git output (issue5176)
1264 1273 env['LC_ALL'] = 'C'
1265 1274 # fix for Git CVE-2015-7545
1266 1275 if 'GIT_ALLOW_PROTOCOL' not in env:
1267 1276 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1268 1277 # unless ui.quiet is set, print git's stderr,
1269 1278 # which is mostly progress and useful info
1270 1279 errpipe = None
1271 1280 if self.ui.quiet:
1272 1281 errpipe = open(os.devnull, 'w')
1273 1282 if self.ui._colormode and len(commands) and commands[0] == "diff":
1274 1283 # insert the argument in the front,
1275 1284 # the end of git diff arguments is used for paths
1276 1285 commands.insert(1, '--color')
1277 1286 p = subprocess.Popen(pycompat.rapply(procutil.tonativestr,
1278 1287 [self._gitexecutable] + commands),
1279 1288 bufsize=-1,
1280 1289 cwd=pycompat.rapply(procutil.tonativestr, cwd),
1281 1290 env=procutil.tonativeenv(env),
1282 1291 close_fds=procutil.closefds,
1283 1292 stdout=subprocess.PIPE, stderr=errpipe)
1284 1293 if stream:
1285 1294 return p.stdout, None
1286 1295
1287 1296 retdata = p.stdout.read().strip()
1288 1297 # wait for the child to exit to avoid race condition.
1289 1298 p.wait()
1290 1299
1291 1300 if p.returncode != 0 and p.returncode != 1:
1292 1301 # there are certain error codes that are ok
1293 1302 command = commands[0]
1294 1303 if command in ('cat-file', 'symbolic-ref'):
1295 1304 return retdata, p.returncode
1296 1305 # for all others, abort
1297 1306 raise error.Abort(_('git %s error %d in %s') %
1298 1307 (command, p.returncode, self._relpath))
1299 1308
1300 1309 return retdata, p.returncode
1301 1310
1302 1311 def _gitmissing(self):
1303 1312 return not self.wvfs.exists('.git')
1304 1313
1305 1314 def _gitstate(self):
1306 1315 return self._gitcommand(['rev-parse', 'HEAD'])
1307 1316
1308 1317 def _gitcurrentbranch(self):
1309 1318 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1310 1319 if err:
1311 1320 current = None
1312 1321 return current
1313 1322
1314 1323 def _gitremote(self, remote):
1315 1324 out = self._gitcommand(['remote', 'show', '-n', remote])
1316 1325 line = out.split('\n')[1]
1317 1326 i = line.index('URL: ') + len('URL: ')
1318 1327 return line[i:]
1319 1328
1320 1329 def _githavelocally(self, revision):
1321 1330 out, code = self._gitdir(['cat-file', '-e', revision])
1322 1331 return code == 0
1323 1332
1324 1333 def _gitisancestor(self, r1, r2):
1325 1334 base = self._gitcommand(['merge-base', r1, r2])
1326 1335 return base == r1
1327 1336
1328 1337 def _gitisbare(self):
1329 1338 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1330 1339
1331 1340 def _gitupdatestat(self):
1332 1341 """This must be run before git diff-index.
1333 1342 diff-index only looks at changes to file stat;
1334 1343 this command looks at file contents and updates the stat."""
1335 1344 self._gitcommand(['update-index', '-q', '--refresh'])
1336 1345
1337 1346 def _gitbranchmap(self):
1338 1347 '''returns 2 things:
1339 1348 a map from git branch to revision
1340 1349 a map from revision to branches'''
1341 1350 branch2rev = {}
1342 1351 rev2branch = {}
1343 1352
1344 1353 out = self._gitcommand(['for-each-ref', '--format',
1345 1354 '%(objectname) %(refname)'])
1346 1355 for line in out.split('\n'):
1347 1356 revision, ref = line.split(' ')
1348 1357 if (not ref.startswith('refs/heads/') and
1349 1358 not ref.startswith('refs/remotes/')):
1350 1359 continue
1351 1360 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1352 1361 continue # ignore remote/HEAD redirects
1353 1362 branch2rev[ref] = revision
1354 1363 rev2branch.setdefault(revision, []).append(ref)
1355 1364 return branch2rev, rev2branch
1356 1365
1357 1366 def _gittracking(self, branches):
1358 1367 'return map of remote branch to local tracking branch'
1359 1368 # assumes no more than one local tracking branch for each remote
1360 1369 tracking = {}
1361 1370 for b in branches:
1362 1371 if b.startswith('refs/remotes/'):
1363 1372 continue
1364 1373 bname = b.split('/', 2)[2]
1365 1374 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1366 1375 if remote:
1367 1376 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1368 1377 tracking['refs/remotes/%s/%s' %
1369 1378 (remote, ref.split('/', 2)[2])] = b
1370 1379 return tracking
1371 1380
1372 1381 def _abssource(self, source):
1373 1382 if '://' not in source:
1374 1383 # recognize the scp syntax as an absolute source
1375 1384 colon = source.find(':')
1376 1385 if colon != -1 and '/' not in source[:colon]:
1377 1386 return source
1378 1387 self._subsource = source
1379 1388 return _abssource(self)
1380 1389
1381 1390 def _fetch(self, source, revision):
1382 1391 if self._gitmissing():
1383 1392 # SEC: check for safe ssh url
1384 1393 util.checksafessh(source)
1385 1394
1386 1395 source = self._abssource(source)
1387 1396 self.ui.status(_('cloning subrepo %s from %s\n') %
1388 1397 (self._relpath, source))
1389 1398 self._gitnodir(['clone', source, self._abspath])
1390 1399 if self._githavelocally(revision):
1391 1400 return
1392 1401 self.ui.status(_('pulling subrepo %s from %s\n') %
1393 1402 (self._relpath, self._gitremote('origin')))
1394 1403 # try only origin: the originally cloned repo
1395 1404 self._gitcommand(['fetch'])
1396 1405 if not self._githavelocally(revision):
1397 1406 raise error.Abort(_('revision %s does not exist in subrepository '
1398 1407 '"%s"\n') % (revision, self._relpath))
1399 1408
1400 1409 @annotatesubrepoerror
1401 1410 def dirty(self, ignoreupdate=False, missing=False):
1402 1411 if self._gitmissing():
1403 1412 return self._state[1] != ''
1404 1413 if self._gitisbare():
1405 1414 return True
1406 1415 if not ignoreupdate and self._state[1] != self._gitstate():
1407 1416 # different version checked out
1408 1417 return True
1409 1418 # check for staged changes or modified files; ignore untracked files
1410 1419 self._gitupdatestat()
1411 1420 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1412 1421 return code == 1
1413 1422
1414 1423 def basestate(self):
1415 1424 return self._gitstate()
1416 1425
1417 1426 @annotatesubrepoerror
1418 1427 def get(self, state, overwrite=False):
1419 1428 source, revision, kind = state
1420 1429 if not revision:
1421 1430 self.remove()
1422 1431 return
1423 1432 self._fetch(source, revision)
1424 1433 # if the repo was set to be bare, unbare it
1425 1434 if self._gitisbare():
1426 1435 self._gitcommand(['config', 'core.bare', 'false'])
1427 1436 if self._gitstate() == revision:
1428 1437 self._gitcommand(['reset', '--hard', 'HEAD'])
1429 1438 return
1430 1439 elif self._gitstate() == revision:
1431 1440 if overwrite:
1432 1441 # first reset the index to unmark new files for commit, because
1433 1442 # reset --hard will otherwise throw away files added for commit,
1434 1443 # not just unmark them.
1435 1444 self._gitcommand(['reset', 'HEAD'])
1436 1445 self._gitcommand(['reset', '--hard', 'HEAD'])
1437 1446 return
1438 1447 branch2rev, rev2branch = self._gitbranchmap()
1439 1448
1440 1449 def checkout(args):
1441 1450 cmd = ['checkout']
1442 1451 if overwrite:
1443 1452 # first reset the index to unmark new files for commit, because
1444 1453 # the -f option will otherwise throw away files added for
1445 1454 # commit, not just unmark them.
1446 1455 self._gitcommand(['reset', 'HEAD'])
1447 1456 cmd.append('-f')
1448 1457 self._gitcommand(cmd + args)
1449 1458 _sanitize(self.ui, self.wvfs, '.git')
1450 1459
1451 1460 def rawcheckout():
1452 1461 # no branch to checkout, check it out with no branch
1453 1462 self.ui.warn(_('checking out detached HEAD in '
1454 1463 'subrepository "%s"\n') % self._relpath)
1455 1464 self.ui.warn(_('check out a git branch if you intend '
1456 1465 'to make changes\n'))
1457 1466 checkout(['-q', revision])
1458 1467
1459 1468 if revision not in rev2branch:
1460 1469 rawcheckout()
1461 1470 return
1462 1471 branches = rev2branch[revision]
1463 1472 firstlocalbranch = None
1464 1473 for b in branches:
1465 1474 if b == 'refs/heads/master':
1466 1475 # master trumps all other branches
1467 1476 checkout(['refs/heads/master'])
1468 1477 return
1469 1478 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1470 1479 firstlocalbranch = b
1471 1480 if firstlocalbranch:
1472 1481 checkout([firstlocalbranch])
1473 1482 return
1474 1483
1475 1484 tracking = self._gittracking(branch2rev.keys())
1476 1485 # choose a remote branch already tracked if possible
1477 1486 remote = branches[0]
1478 1487 if remote not in tracking:
1479 1488 for b in branches:
1480 1489 if b in tracking:
1481 1490 remote = b
1482 1491 break
1483 1492
1484 1493 if remote not in tracking:
1485 1494 # create a new local tracking branch
1486 1495 local = remote.split('/', 3)[3]
1487 1496 checkout(['-b', local, remote])
1488 1497 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1489 1498 # When updating to a tracked remote branch,
1490 1499 # if the local tracking branch is downstream of it,
1491 1500 # a normal `git pull` would have performed a "fast-forward merge"
1492 1501 # which is equivalent to updating the local branch to the remote.
1493 1502 # Since we are only looking at branching at update, we need to
1494 1503 # detect this situation and perform this action lazily.
1495 1504 if tracking[remote] != self._gitcurrentbranch():
1496 1505 checkout([tracking[remote]])
1497 1506 self._gitcommand(['merge', '--ff', remote])
1498 1507 _sanitize(self.ui, self.wvfs, '.git')
1499 1508 else:
1500 1509 # a real merge would be required, just checkout the revision
1501 1510 rawcheckout()
1502 1511
1503 1512 @annotatesubrepoerror
1504 1513 def commit(self, text, user, date):
1505 1514 if self._gitmissing():
1506 1515 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1507 1516 cmd = ['commit', '-a', '-m', text]
1508 1517 env = encoding.environ.copy()
1509 1518 if user:
1510 1519 cmd += ['--author', user]
1511 1520 if date:
1512 1521 # git's date parser silently ignores when seconds < 1e9
1513 1522 # convert to ISO8601
1514 1523 env['GIT_AUTHOR_DATE'] = dateutil.datestr(date,
1515 1524 '%Y-%m-%dT%H:%M:%S %1%2')
1516 1525 self._gitcommand(cmd, env=env)
1517 1526 # make sure commit works otherwise HEAD might not exist under certain
1518 1527 # circumstances
1519 1528 return self._gitstate()
1520 1529
1521 1530 @annotatesubrepoerror
1522 1531 def merge(self, state):
1523 1532 source, revision, kind = state
1524 1533 self._fetch(source, revision)
1525 1534 base = self._gitcommand(['merge-base', revision, self._state[1]])
1526 1535 self._gitupdatestat()
1527 1536 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1528 1537
1529 1538 def mergefunc():
1530 1539 if base == revision:
1531 1540 self.get(state) # fast forward merge
1532 1541 elif base != self._state[1]:
1533 1542 self._gitcommand(['merge', '--no-commit', revision])
1534 1543 _sanitize(self.ui, self.wvfs, '.git')
1535 1544
1536 1545 if self.dirty():
1537 1546 if self._gitstate() != revision:
1538 1547 dirty = self._gitstate() == self._state[1] or code != 0
1539 1548 if _updateprompt(self.ui, self, dirty,
1540 1549 self._state[1][:7], revision[:7]):
1541 1550 mergefunc()
1542 1551 else:
1543 1552 mergefunc()
1544 1553
1545 1554 @annotatesubrepoerror
1546 1555 def push(self, opts):
1547 1556 force = opts.get('force')
1548 1557
1549 1558 if not self._state[1]:
1550 1559 return True
1551 1560 if self._gitmissing():
1552 1561 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1553 1562 # if a branch in origin contains the revision, nothing to do
1554 1563 branch2rev, rev2branch = self._gitbranchmap()
1555 1564 if self._state[1] in rev2branch:
1556 1565 for b in rev2branch[self._state[1]]:
1557 1566 if b.startswith('refs/remotes/origin/'):
1558 1567 return True
1559 1568 for b, revision in branch2rev.iteritems():
1560 1569 if b.startswith('refs/remotes/origin/'):
1561 1570 if self._gitisancestor(self._state[1], revision):
1562 1571 return True
1563 1572 # otherwise, try to push the currently checked out branch
1564 1573 cmd = ['push']
1565 1574 if force:
1566 1575 cmd.append('--force')
1567 1576
1568 1577 current = self._gitcurrentbranch()
1569 1578 if current:
1570 1579 # determine if the current branch is even useful
1571 1580 if not self._gitisancestor(self._state[1], current):
1572 1581 self.ui.warn(_('unrelated git branch checked out '
1573 1582 'in subrepository "%s"\n') % self._relpath)
1574 1583 return False
1575 1584 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1576 1585 (current.split('/', 2)[2], self._relpath))
1577 1586 ret = self._gitdir(cmd + ['origin', current])
1578 1587 return ret[1] == 0
1579 1588 else:
1580 1589 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1581 1590 'cannot push revision %s\n') %
1582 1591 (self._relpath, self._state[1]))
1583 1592 return False
1584 1593
1585 1594 @annotatesubrepoerror
1586 1595 def add(self, ui, match, prefix, explicitonly, **opts):
1587 1596 if self._gitmissing():
1588 1597 return []
1589 1598
1590 1599 s = self.status(None, unknown=True, clean=True)
1591 1600
1592 1601 tracked = set()
1593 1602 # dirstates 'amn' warn, 'r' is added again
1594 1603 for l in (s.modified, s.added, s.deleted, s.clean):
1595 1604 tracked.update(l)
1596 1605
1597 1606 # Unknown files not of interest will be rejected by the matcher
1598 1607 files = s.unknown
1599 1608 files.extend(match.files())
1600 1609
1601 1610 rejected = []
1602 1611
1603 1612 files = [f for f in sorted(set(files)) if match(f)]
1604 1613 for f in files:
1605 1614 exact = match.exact(f)
1606 1615 command = ["add"]
1607 1616 if exact:
1608 1617 command.append("-f") #should be added, even if ignored
1609 1618 if ui.verbose or not exact:
1610 1619 ui.status(_('adding %s\n') % match.rel(f))
1611 1620
1612 1621 if f in tracked: # hg prints 'adding' even if already tracked
1613 1622 if exact:
1614 1623 rejected.append(f)
1615 1624 continue
1616 1625 if not opts.get(r'dry_run'):
1617 1626 self._gitcommand(command + [f])
1618 1627
1619 1628 for f in rejected:
1620 1629 ui.warn(_("%s already tracked!\n") % match.abs(f))
1621 1630
1622 1631 return rejected
1623 1632
1624 1633 @annotatesubrepoerror
1625 1634 def remove(self):
1626 1635 if self._gitmissing():
1627 1636 return
1628 1637 if self.dirty():
1629 1638 self.ui.warn(_('not removing repo %s because '
1630 1639 'it has changes.\n') % self._relpath)
1631 1640 return
1632 1641 # we can't fully delete the repository as it may contain
1633 1642 # local-only history
1634 1643 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1635 1644 self._gitcommand(['config', 'core.bare', 'true'])
1636 1645 for f, kind in self.wvfs.readdir():
1637 1646 if f == '.git':
1638 1647 continue
1639 1648 if kind == stat.S_IFDIR:
1640 1649 self.wvfs.rmtree(f)
1641 1650 else:
1642 1651 self.wvfs.unlink(f)
1643 1652
1644 1653 def archive(self, archiver, prefix, match=None, decode=True):
1645 1654 total = 0
1646 1655 source, revision = self._state
1647 1656 if not revision:
1648 1657 return total
1649 1658 self._fetch(source, revision)
1650 1659
1651 1660 # Parse git's native archive command.
1652 1661 # This should be much faster than manually traversing the trees
1653 1662 # and objects with many subprocess calls.
1654 1663 tarstream = self._gitcommand(['archive', revision], stream=True)
1655 1664 tar = tarfile.open(fileobj=tarstream, mode=r'r|')
1656 1665 relpath = subrelpath(self)
1657 1666 progress = self.ui.makeprogress(_('archiving (%s)') % relpath,
1658 1667 unit=_('files'))
1659 1668 progress.update(0)
1660 1669 for info in tar:
1661 1670 if info.isdir():
1662 1671 continue
1663 1672 if match and not match(info.name):
1664 1673 continue
1665 1674 if info.issym():
1666 1675 data = info.linkname
1667 1676 else:
1668 1677 data = tar.extractfile(info).read()
1669 1678 archiver.addfile(prefix + self._path + '/' + info.name,
1670 1679 info.mode, info.issym(), data)
1671 1680 total += 1
1672 1681 progress.increment()
1673 1682 progress.complete()
1674 1683 return total
1675 1684
1676 1685
1677 1686 @annotatesubrepoerror
1678 1687 def cat(self, match, fm, fntemplate, prefix, **opts):
1679 1688 rev = self._state[1]
1680 1689 if match.anypats():
1681 1690 return 1 #No support for include/exclude yet
1682 1691
1683 1692 if not match.files():
1684 1693 return 1
1685 1694
1686 1695 # TODO: add support for non-plain formatter (see cmdutil.cat())
1687 1696 for f in match.files():
1688 1697 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1689 1698 fp = cmdutil.makefileobj(self._ctx, fntemplate,
1690 1699 pathname=self.wvfs.reljoin(prefix, f))
1691 1700 fp.write(output)
1692 1701 fp.close()
1693 1702 return 0
1694 1703
1695 1704
1696 1705 @annotatesubrepoerror
1697 1706 def status(self, rev2, **opts):
1698 1707 rev1 = self._state[1]
1699 1708 if self._gitmissing() or not rev1:
1700 1709 # if the repo is missing, return no results
1701 1710 return scmutil.status([], [], [], [], [], [], [])
1702 1711 modified, added, removed = [], [], []
1703 1712 self._gitupdatestat()
1704 1713 if rev2:
1705 1714 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1706 1715 else:
1707 1716 command = ['diff-index', '--no-renames', rev1]
1708 1717 out = self._gitcommand(command)
1709 1718 for line in out.split('\n'):
1710 1719 tab = line.find('\t')
1711 1720 if tab == -1:
1712 1721 continue
1713 1722 status, f = line[tab - 1:tab], line[tab + 1:]
1714 1723 if status == 'M':
1715 1724 modified.append(f)
1716 1725 elif status == 'A':
1717 1726 added.append(f)
1718 1727 elif status == 'D':
1719 1728 removed.append(f)
1720 1729
1721 1730 deleted, unknown, ignored, clean = [], [], [], []
1722 1731
1723 1732 command = ['status', '--porcelain', '-z']
1724 1733 if opts.get(r'unknown'):
1725 1734 command += ['--untracked-files=all']
1726 1735 if opts.get(r'ignored'):
1727 1736 command += ['--ignored']
1728 1737 out = self._gitcommand(command)
1729 1738
1730 1739 changedfiles = set()
1731 1740 changedfiles.update(modified)
1732 1741 changedfiles.update(added)
1733 1742 changedfiles.update(removed)
1734 1743 for line in out.split('\0'):
1735 1744 if not line:
1736 1745 continue
1737 1746 st = line[0:2]
1738 1747 #moves and copies show 2 files on one line
1739 1748 if line.find('\0') >= 0:
1740 1749 filename1, filename2 = line[3:].split('\0')
1741 1750 else:
1742 1751 filename1 = line[3:]
1743 1752 filename2 = None
1744 1753
1745 1754 changedfiles.add(filename1)
1746 1755 if filename2:
1747 1756 changedfiles.add(filename2)
1748 1757
1749 1758 if st == '??':
1750 1759 unknown.append(filename1)
1751 1760 elif st == '!!':
1752 1761 ignored.append(filename1)
1753 1762
1754 1763 if opts.get(r'clean'):
1755 1764 out = self._gitcommand(['ls-files'])
1756 1765 for f in out.split('\n'):
1757 1766 if not f in changedfiles:
1758 1767 clean.append(f)
1759 1768
1760 1769 return scmutil.status(modified, added, removed, deleted,
1761 1770 unknown, ignored, clean)
1762 1771
1763 1772 @annotatesubrepoerror
1764 1773 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1765 1774 node1 = self._state[1]
1766 1775 cmd = ['diff', '--no-renames']
1767 1776 if opts[r'stat']:
1768 1777 cmd.append('--stat')
1769 1778 else:
1770 1779 # for Git, this also implies '-p'
1771 1780 cmd.append('-U%d' % diffopts.context)
1772 1781
1773 1782 gitprefix = self.wvfs.reljoin(prefix, self._path)
1774 1783
1775 1784 if diffopts.noprefix:
1776 1785 cmd.extend(['--src-prefix=%s/' % gitprefix,
1777 1786 '--dst-prefix=%s/' % gitprefix])
1778 1787 else:
1779 1788 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1780 1789 '--dst-prefix=b/%s/' % gitprefix])
1781 1790
1782 1791 if diffopts.ignorews:
1783 1792 cmd.append('--ignore-all-space')
1784 1793 if diffopts.ignorewsamount:
1785 1794 cmd.append('--ignore-space-change')
1786 1795 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1787 1796 and diffopts.ignoreblanklines:
1788 1797 cmd.append('--ignore-blank-lines')
1789 1798
1790 1799 cmd.append(node1)
1791 1800 if node2:
1792 1801 cmd.append(node2)
1793 1802
1794 1803 output = ""
1795 1804 if match.always():
1796 1805 output += self._gitcommand(cmd) + '\n'
1797 1806 else:
1798 1807 st = self.status(node2)[:3]
1799 1808 files = [f for sublist in st for f in sublist]
1800 1809 for f in files:
1801 1810 if match(f):
1802 1811 output += self._gitcommand(cmd + ['--', f]) + '\n'
1803 1812
1804 1813 if output.strip():
1805 1814 ui.write(output)
1806 1815
1807 1816 @annotatesubrepoerror
1808 1817 def revert(self, substate, *pats, **opts):
1809 1818 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1810 1819 if not opts.get(r'no_backup'):
1811 1820 status = self.status(None)
1812 1821 names = status.modified
1813 1822 origvfs = scmutil.getorigvfs(self.ui, self._subparent)
1814 1823 if origvfs is None:
1815 1824 origvfs = self.wvfs
1816 1825 for name in names:
1817 1826 bakname = scmutil.origpath(self.ui, self._subparent, name)
1818 1827 self.ui.note(_('saving current version of %s as %s\n') %
1819 1828 (name, bakname))
1820 1829 name = self.wvfs.join(name)
1821 1830 origvfs.rename(name, bakname)
1822 1831
1823 1832 if not opts.get(r'dry_run'):
1824 1833 self.get(substate, overwrite=True)
1825 1834 return []
1826 1835
1827 1836 def shortid(self, revid):
1828 1837 return revid[:7]
1829 1838
1830 1839 types = {
1831 1840 'hg': hgsubrepo,
1832 1841 'svn': svnsubrepo,
1833 1842 'git': gitsubrepo,
1834 1843 }
@@ -1,619 +1,643
1 1 Test illegal name
2 2 -----------------
3 3
4 4 on commit:
5 5
6 6 $ hg init hgname
7 7 $ cd hgname
8 8 $ mkdir sub
9 9 $ hg init sub/.hg
10 10 $ echo 'sub/.hg = sub/.hg' >> .hgsub
11 11 $ hg ci -qAm 'add subrepo "sub/.hg"'
12 12 abort: path 'sub/.hg' is inside nested repo 'sub'
13 13 [255]
14 14
15 15 prepare tampered repo (including the commit above):
16 16
17 17 $ hg import --bypass -qm 'add subrepo "sub/.hg"' - <<'EOF'
18 18 > diff --git a/.hgsub b/.hgsub
19 19 > new file mode 100644
20 20 > --- /dev/null
21 21 > +++ b/.hgsub
22 22 > @@ -0,0 +1,1 @@
23 23 > +sub/.hg = sub/.hg
24 24 > diff --git a/.hgsubstate b/.hgsubstate
25 25 > new file mode 100644
26 26 > --- /dev/null
27 27 > +++ b/.hgsubstate
28 28 > @@ -0,0 +1,1 @@
29 29 > +0000000000000000000000000000000000000000 sub/.hg
30 30 > EOF
31 31 $ cd ..
32 32
33 33 on clone (and update):
34 34
35 35 $ hg clone -q hgname hgname2
36 36 abort: path 'sub/.hg' is inside nested repo 'sub'
37 37 [255]
38 38
39 39 Test absolute path
40 40 ------------------
41 41
42 42 on commit:
43 43
44 44 $ hg init absolutepath
45 45 $ cd absolutepath
46 46 $ hg init sub
47 47 $ echo '/sub = sub' >> .hgsub
48 48 $ hg ci -qAm 'add subrepo "/sub"'
49 49 abort: path contains illegal component: /sub
50 50 [255]
51 51
52 52 prepare tampered repo (including the commit above):
53 53
54 54 $ hg import --bypass -qm 'add subrepo "/sub"' - <<'EOF'
55 55 > diff --git a/.hgsub b/.hgsub
56 56 > new file mode 100644
57 57 > --- /dev/null
58 58 > +++ b/.hgsub
59 59 > @@ -0,0 +1,1 @@
60 60 > +/sub = sub
61 61 > diff --git a/.hgsubstate b/.hgsubstate
62 62 > new file mode 100644
63 63 > --- /dev/null
64 64 > +++ b/.hgsubstate
65 65 > @@ -0,0 +1,1 @@
66 66 > +0000000000000000000000000000000000000000 /sub
67 67 > EOF
68 68 $ cd ..
69 69
70 70 on clone (and update):
71 71
72 72 $ hg clone -q absolutepath absolutepath2
73 73 abort: path contains illegal component: /sub
74 74 [255]
75 75
76 76 Test root path
77 77 --------------
78 78
79 79 on commit:
80 80
81 81 $ hg init rootpath
82 82 $ cd rootpath
83 83 $ hg init sub
84 84 $ echo '/ = sub' >> .hgsub
85 85 $ hg ci -qAm 'add subrepo "/"'
86 86 abort: path ends in directory separator: /
87 87 [255]
88 88
89 89 prepare tampered repo (including the commit above):
90 90
91 91 $ hg import --bypass -qm 'add subrepo "/"' - <<'EOF'
92 92 > diff --git a/.hgsub b/.hgsub
93 93 > new file mode 100644
94 94 > --- /dev/null
95 95 > +++ b/.hgsub
96 96 > @@ -0,0 +1,1 @@
97 97 > +/ = sub
98 98 > diff --git a/.hgsubstate b/.hgsubstate
99 99 > new file mode 100644
100 100 > --- /dev/null
101 101 > +++ b/.hgsubstate
102 102 > @@ -0,0 +1,1 @@
103 103 > +0000000000000000000000000000000000000000 /
104 104 > EOF
105 105 $ cd ..
106 106
107 107 on clone (and update):
108 108
109 109 $ hg clone -q rootpath rootpath2
110 110 abort: path ends in directory separator: /
111 111 [255]
112 112
113 113 Test empty path
114 114 ---------------
115 115
116 116 on commit:
117 117
118 118 $ hg init emptypath
119 119 $ cd emptypath
120 120 $ hg init sub
121 121 $ echo '= sub' >> .hgsub
122 122 $ hg ci -qAm 'add subrepo ""'
123 123 hg: parse error at .hgsub:1: = sub
124 124 [255]
125 125
126 126 prepare tampered repo (including the commit above):
127 127
128 128 $ hg import --bypass -qm 'add subrepo ""' - <<'EOF'
129 129 > diff --git a/.hgsub b/.hgsub
130 130 > new file mode 100644
131 131 > --- /dev/null
132 132 > +++ b/.hgsub
133 133 > @@ -0,0 +1,1 @@
134 134 > += sub
135 135 > diff --git a/.hgsubstate b/.hgsubstate
136 136 > new file mode 100644
137 137 > --- /dev/null
138 138 > +++ b/.hgsubstate
139 139 > @@ -0,0 +1,1 @@
140 140 > +0000000000000000000000000000000000000000
141 141 > EOF
142 142 $ cd ..
143 143
144 144 on clone (and update):
145 145
146 146 $ hg clone -q emptypath emptypath2
147 147 hg: parse error at .hgsub:1: = sub
148 148 [255]
149 149
150 150 Test current path
151 151 -----------------
152 152
153 153 on commit:
154 BROKEN: should fail
155 154
156 155 $ hg init currentpath
157 156 $ cd currentpath
158 157 $ hg init sub
159 158 $ echo '. = sub' >> .hgsub
160 159 $ hg ci -qAm 'add subrepo "."'
160 abort: subrepo path contains illegal component: .
161 [255]
162
163 prepare tampered repo (including the commit above):
164
165 $ hg import --bypass -qm 'add subrepo "."' - <<'EOF'
166 > diff --git a/.hgsub b/.hgsub
167 > new file mode 100644
168 > --- /dev/null
169 > +++ b/.hgsub
170 > @@ -0,0 +1,1 @@
171 > +.= sub
172 > diff --git a/.hgsubstate b/.hgsubstate
173 > new file mode 100644
174 > --- /dev/null
175 > +++ b/.hgsubstate
176 > @@ -0,0 +1,1 @@
177 > +0000000000000000000000000000000000000000 .
178 > EOF
161 179 $ cd ..
162 180
163 181 on clone (and update):
164 182
165 $ hg clone -q currentpath currentpath2 --config ui.timeout=1
166 waiting for lock on working directory of $TESTTMP/currentpath2/. * (glob)
167 abort: working directory of $TESTTMP/currentpath2/.: timed out waiting for lock held by '*' (glob)
183 $ hg clone -q currentpath currentpath2
184 abort: subrepo path contains illegal component: .
168 185 [255]
169 186
170 187 Test outer path
171 188 ---------------
172 189
173 190 on commit:
174 191
175 192 $ mkdir outerpath
176 193 $ cd outerpath
177 194 $ hg init main
178 195 $ cd main
179 196 $ hg init ../sub
180 197 $ echo '../sub = ../sub' >> .hgsub
181 198 $ hg ci -qAm 'add subrepo "../sub"'
182 199 abort: path contains illegal component: ../sub
183 200 [255]
184 201
185 202 prepare tampered repo (including the commit above):
186 203
187 204 $ hg import --bypass -qm 'add subrepo "../sub"' - <<'EOF'
188 205 > diff --git a/.hgsub b/.hgsub
189 206 > new file mode 100644
190 207 > --- /dev/null
191 208 > +++ b/.hgsub
192 209 > @@ -0,0 +1,1 @@
193 210 > +../sub = ../sub
194 211 > diff --git a/.hgsubstate b/.hgsubstate
195 212 > new file mode 100644
196 213 > --- /dev/null
197 214 > +++ b/.hgsubstate
198 215 > @@ -0,0 +1,1 @@
199 216 > +0000000000000000000000000000000000000000 ../sub
200 217 > EOF
201 218 $ cd ..
202 219
203 220 on clone (and update):
204 221
205 222 $ hg clone -q main main2
206 223 abort: path contains illegal component: ../sub
207 224 [255]
208 225 $ cd ..
209 226
210 227 Test variable expansion
211 228 -----------------------
212 229
213 230 Subrepository paths shouldn't be expanded, but we fail to handle them
214 231 properly. Any local repository paths are expanded.
215 232
216 233 on commit:
217 BROKEN: wrong error message
218 234
219 235 $ mkdir envvar
220 236 $ cd envvar
221 237 $ hg init main
222 238 $ cd main
223 239 $ hg init sub1
224 240 $ cat <<'EOF' > sub1/hgrc
225 241 > [hooks]
226 242 > log = echo pwned
227 243 > EOF
228 244 $ hg -R sub1 ci -qAm 'add sub1 files'
229 245 $ hg -R sub1 log -r. -T '{node}\n'
230 246 39eb4b4d3e096527668784893a9280578a8f38b8
231 247 $ echo '$SUB = sub1' >> .hgsub
232 248 $ SUB=sub1 hg ci -qAm 'add subrepo "$SUB"'
233 abort: repository $TESTTMP/envvar/main/$SUB already exists!
249 abort: subrepo path contains illegal component: $SUB
234 250 [255]
235 251
236 252 prepare tampered repo (including the changes above as two commits):
237 253
238 254 $ hg import --bypass -qm 'add subrepo "$SUB"' - <<'EOF'
239 255 > diff --git a/.hgsub b/.hgsub
240 256 > new file mode 100644
241 257 > --- /dev/null
242 258 > +++ b/.hgsub
243 259 > @@ -0,0 +1,1 @@
244 260 > +$SUB = sub1
245 261 > diff --git a/.hgsubstate b/.hgsubstate
246 262 > new file mode 100644
247 263 > --- /dev/null
248 264 > +++ b/.hgsubstate
249 265 > @@ -0,0 +1,1 @@
250 266 > +0000000000000000000000000000000000000000 $SUB
251 267 > EOF
252 268 $ hg debugsetparents 0
253 269 $ hg import --bypass -qm 'update subrepo "$SUB"' - <<'EOF'
254 270 > diff --git a/.hgsubstate b/.hgsubstate
255 271 > --- a/.hgsubstate
256 272 > +++ b/.hgsubstate
257 273 > @@ -1,1 +1,1 @@
258 274 > -0000000000000000000000000000000000000000 $SUB
259 275 > +39eb4b4d3e096527668784893a9280578a8f38b8 $SUB
260 276 > EOF
261 277 $ cd ..
262 278
263 279 on clone (and update) with various substitutions:
264 280
265 281 $ hg clone -q main main2
266 282 $ ls main2
267 283 $SUB
268 284
269 285 $ SUB=sub1 hg clone -q main main3
286 abort: subrepo path contains illegal component: $SUB
287 [255]
270 288 $ ls main3
271 sub1
272 289
273 290 $ SUB=sub2 hg clone -q main main4
291 abort: subrepo path contains illegal component: $SUB
292 [255]
274 293 $ ls main4
275 sub2
276 294
277 295 on clone empty subrepo into .hg, then pull (and update), which at least fails:
278 BROKEN: the first clone should fail
279 296
280 297 $ SUB=.hg hg clone -qr0 main main5
298 abort: subrepo path contains illegal component: $SUB
299 [255]
281 300 $ ls main5
282 $ ls -d main5/.hg/.hg
283 main5/.hg/.hg
301 $ test -d main5/.hg/.hg
302 [1]
284 303 $ SUB=.hg hg -R main5 pull -u
285 304 pulling from $TESTTMP/envvar/main
286 305 searching for changes
287 306 adding changesets
288 307 adding manifests
289 308 adding file changes
290 309 added 1 changesets with 1 changes to 1 files
291 310 new changesets 7a2f0e59146f
292 abort: repository $TESTTMP/envvar/main5/$SUB already exists!
311 .hgsubstate: untracked file differs
312 abort: untracked files in working directory differ from files in requested revision
293 313 [255]
294 314 $ cat main5/.hg/hgrc | grep pwned
295 315 [1]
296 316
297 317 on clone (and update) into .hg, which at least fails:
298 318
299 319 $ SUB=.hg hg clone -q main main6
300 abort: destination '$TESTTMP/envvar/main6/.hg' is not empty (in subrepository ".hg")
320 abort: subrepo path contains illegal component: $SUB
301 321 [255]
302 322 $ ls main6
303 323 $ cat main6/.hg/hgrc | grep pwned
304 324 [1]
305 325
306 326 on clone (and update) into .hg/* subdir:
307 BROKEN: should fail
308 327
309 328 $ SUB=.hg/foo hg clone -q main main7
329 abort: subrepo path contains illegal component: $SUB
330 [255]
310 331 $ ls main7
311 $ ls main7/.hg/foo
312 hgrc
332 $ test -d main7/.hg/.hg
333 [1]
313 334
314 335 on clone (and update) into outer tree:
315 BROKEN: should fail
316 336
317 337 $ SUB=../out-of-tree-write hg clone -q main main8
338 abort: subrepo path contains illegal component: $SUB
339 [255]
318 340 $ ls main8
319 341
320 342 on clone (and update) into e.g. $HOME, which doesn't work since subrepo paths
321 343 are concatenated prior to variable expansion:
322 344
323 345 $ SUB="$TESTTMP/envvar/fakehome" hg clone -q main main9
346 abort: subrepo path contains illegal component: $SUB
347 [255]
324 348 $ ls main9 | wc -l
325 \s*1 (re)
349 \s*0 (re)
326 350
327 351 $ ls
328 352 main
329 353 main2
330 354 main3
331 355 main4
332 356 main5
333 357 main6
334 358 main7
335 359 main8
336 360 main9
337 out-of-tree-write
338 361 $ cd ..
339 362
340 363 Test tilde
341 364 ----------
342 365
343 366 The leading tilde may be expanded to $HOME, but it's a valid subrepo path.
344 367 However, we might want to prohibit it as it seems potentially unsafe.
345 368
346 369 on commit:
347 370
348 371 $ hg init tilde
349 372 $ cd tilde
350 373 $ hg init './~'
351 374 $ echo '~ = ~' >> .hgsub
352 375 $ hg ci -qAm 'add subrepo "~"'
353 376 $ ls
354 377 ~
355 378 $ cd ..
356 379
357 380 on clone (and update):
358 381
359 382 $ hg clone -q tilde tilde2
360 383 $ ls tilde2
361 384 ~
362 385
363 386 Test direct symlink traversal
364 387 -----------------------------
365 388
366 389 #if symlink
367 390
368 391 on commit:
369 392
370 393 $ mkdir hgsymdir
371 394 $ hg init hgsymdir/root
372 395 $ cd hgsymdir/root
373 396 $ ln -s ../out
374 397 $ hg ci -qAm 'add symlink "out"'
375 398 $ hg init ../out
376 399 $ echo 'out = out' >> .hgsub
377 400 $ hg ci -qAm 'add subrepo "out"'
378 401 abort: subrepo 'out' traverses symbolic link
379 402 [255]
380 403
381 404 prepare tampered repo (including the commit above):
382 405
383 406 $ hg import --bypass -qm 'add subrepo "out"' - <<'EOF'
384 407 > diff --git a/.hgsub b/.hgsub
385 408 > new file mode 100644
386 409 > --- /dev/null
387 410 > +++ b/.hgsub
388 411 > @@ -0,0 +1,1 @@
389 412 > +out = out
390 413 > diff --git a/.hgsubstate b/.hgsubstate
391 414 > new file mode 100644
392 415 > --- /dev/null
393 416 > +++ b/.hgsubstate
394 417 > @@ -0,0 +1,1 @@
395 418 > +0000000000000000000000000000000000000000 out
396 419 > EOF
397 420 $ cd ../..
398 421
399 422 on clone (and update):
400 423
401 424 $ mkdir hgsymdir2
402 425 $ hg clone -q hgsymdir/root hgsymdir2/root
403 426 abort: subrepo 'out' traverses symbolic link
404 427 [255]
405 428 $ ls hgsymdir2
406 429 root
407 430
408 431 #endif
409 432
410 433 Test indirect symlink traversal
411 434 -------------------------------
412 435
413 436 #if symlink
414 437
415 438 on commit:
416 439
417 440 $ mkdir hgsymin
418 441 $ hg init hgsymin/root
419 442 $ cd hgsymin/root
420 443 $ ln -s ../out
421 444 $ hg ci -qAm 'add symlink "out"'
422 445 $ mkdir ../out
423 446 $ hg init ../out/sub
424 447 $ echo 'out/sub = out/sub' >> .hgsub
425 448 $ hg ci -qAm 'add subrepo "out/sub"'
426 449 abort: path 'out/sub' traverses symbolic link 'out'
427 450 [255]
428 451
429 452 prepare tampered repo (including the commit above):
430 453
431 454 $ hg import --bypass -qm 'add subrepo "out/sub"' - <<'EOF'
432 455 > diff --git a/.hgsub b/.hgsub
433 456 > new file mode 100644
434 457 > --- /dev/null
435 458 > +++ b/.hgsub
436 459 > @@ -0,0 +1,1 @@
437 460 > +out/sub = out/sub
438 461 > diff --git a/.hgsubstate b/.hgsubstate
439 462 > new file mode 100644
440 463 > --- /dev/null
441 464 > +++ b/.hgsubstate
442 465 > @@ -0,0 +1,1 @@
443 466 > +0000000000000000000000000000000000000000 out/sub
444 467 > EOF
445 468 $ cd ../..
446 469
447 470 on clone (and update):
448 471
449 472 $ mkdir hgsymin2
450 473 $ hg clone -q hgsymin/root hgsymin2/root
451 474 abort: path 'out/sub' traverses symbolic link 'out'
452 475 [255]
453 476 $ ls hgsymin2
454 477 root
455 478
456 479 #endif
457 480
458 481 Test symlink traversal by variable expansion
459 482 --------------------------------------------
460 483
461 484 #if symlink
462 485
463 486 $ FAKEHOME="$TESTTMP/envvarsym/fakehome"
464 487
465 488 on commit:
466 BROKEN: wrong error message
467 489
468 490 $ mkdir envvarsym
469 491 $ cd envvarsym
470 492 $ hg init main
471 493 $ cd main
472 494 $ ln -s "`echo "$FAKEHOME" | sed 's|\(.\)/.*|\1|'`"
473 495 $ hg ci -qAm 'add symlink to top-level system directory'
474 496
475 497 $ hg init sub1
476 498 $ echo pwned > sub1/pwned
477 499 $ hg -R sub1 ci -qAm 'add sub1 files'
478 500 $ hg -R sub1 log -r. -T '{node}\n'
479 501 f40c9134ba1b6961e12f250868823f0092fb68a8
480 502 $ echo '$SUB = sub1' >> .hgsub
481 503 $ SUB="$FAKEHOME" hg ci -qAm 'add subrepo "$SUB"'
482 abort: repository $TESTTMP/envvarsym/main/$SUB already exists!
504 abort: subrepo path contains illegal component: $SUB
483 505 [255]
484 506
485 507 prepare tampered repo (including the changes above as two commits):
486 508
487 509 $ hg import --bypass -qm 'add subrepo "$SUB"' - <<'EOF'
488 510 > diff --git a/.hgsub b/.hgsub
489 511 > new file mode 100644
490 512 > --- /dev/null
491 513 > +++ b/.hgsub
492 514 > @@ -0,0 +1,1 @@
493 515 > +$SUB = sub1
494 516 > diff --git a/.hgsubstate b/.hgsubstate
495 517 > new file mode 100644
496 518 > --- /dev/null
497 519 > +++ b/.hgsubstate
498 520 > @@ -0,0 +1,1 @@
499 521 > +0000000000000000000000000000000000000000 $SUB
500 522 > EOF
501 523 $ hg debugsetparents 1
502 524 $ hg import --bypass -qm 'update subrepo "$SUB"' - <<'EOF'
503 525 > diff --git a/.hgsubstate b/.hgsubstate
504 526 > --- a/.hgsubstate
505 527 > +++ b/.hgsubstate
506 528 > @@ -1,1 +1,1 @@
507 529 > -0000000000000000000000000000000000000000 $SUB
508 530 > +f40c9134ba1b6961e12f250868823f0092fb68a8 $SUB
509 531 > EOF
510 532 $ cd ..
511 533
512 534 on clone (and update) without fakehome directory:
513 BROKEN: should fail
514 535
515 536 $ rm -fR "$FAKEHOME"
516 537 $ SUB="$FAKEHOME" hg clone -q main main2
517 $ ls "$FAKEHOME"
518 pwned
538 abort: subrepo path contains illegal component: $SUB
539 [255]
540 $ test -d "$FAKEHOME"
541 [1]
519 542
520 543 on clone (and update) with empty fakehome directory:
521 BROKEN: should fail
522 544
523 545 $ rm -fR "$FAKEHOME"
524 546 $ mkdir "$FAKEHOME"
525 547 $ SUB="$FAKEHOME" hg clone -q main main3
548 abort: subrepo path contains illegal component: $SUB
549 [255]
526 550 $ ls "$FAKEHOME"
527 pwned
528 551
529 552 on clone (and update) with non-empty fakehome directory:
530 BROKEN: wrong error message
531 553
532 554 $ rm -fR "$FAKEHOME"
533 555 $ mkdir "$FAKEHOME"
534 556 $ touch "$FAKEHOME/a"
535 557 $ SUB="$FAKEHOME" hg clone -q main main4
536 abort: destination '$TESTTMP/envvarsym/fakehome' is not empty (in subrepository "*") (glob)
558 abort: subrepo path contains illegal component: $SUB
537 559 [255]
538 560 $ ls "$FAKEHOME"
539 561 a
540 562
541 563 on clone empty subrepo with non-empty fakehome directory,
542 564 then pull (and update):
543 BROKEN: the first clone should fail
544 565
545 566 $ rm -fR "$FAKEHOME"
546 567 $ mkdir "$FAKEHOME"
547 568 $ touch "$FAKEHOME/a"
548 569 $ SUB="$FAKEHOME" hg clone -qr1 main main5
570 abort: subrepo path contains illegal component: $SUB
571 [255]
549 572 $ ls "$FAKEHOME"
550 573 a
551 $ ls -d "$FAKEHOME/.hg"
552 $TESTTMP/envvarsym/fakehome/.hg
574 $ test -d "$FAKEHOME/.hg"
575 [1]
553 576 $ SUB="$FAKEHOME" hg -R main5 pull -u
554 577 pulling from $TESTTMP/envvarsym/main
555 578 searching for changes
556 579 adding changesets
557 580 adding manifests
558 581 adding file changes
559 582 added 1 changesets with 1 changes to 1 files
560 583 new changesets * (glob)
561 abort: repository $TESTTMP/envvarsym/main5/$SUB already exists!
584 .hgsubstate: untracked file differs
585 abort: untracked files in working directory differ from files in requested revision
562 586 [255]
563 587 $ ls "$FAKEHOME"
564 588 a
589 $ test -d "$FAKEHOME/.hg"
590 [1]
565 591
566 592 on clone empty subrepo with hg-managed fakehome directory,
567 593 then pull (and update):
568 BROKEN: wrong error message
569 594
570 595 $ rm -fR "$FAKEHOME"
571 596 $ hg init "$FAKEHOME"
572 597 $ touch "$FAKEHOME/a"
573 598 $ hg -R "$FAKEHOME" ci -qAm 'add fakehome file'
574 599 $ SUB="$FAKEHOME" hg clone -qr1 main main6
575 abort: repository $TESTTMP/envvarsym/main6/$SUB already exists!
600 abort: subrepo path contains illegal component: $SUB
576 601 [255]
577 602 $ ls "$FAKEHOME"
578 603 a
579 604 $ SUB="$FAKEHOME" hg -R main6 pull -u
580 605 pulling from $TESTTMP/envvarsym/main
581 606 searching for changes
582 607 adding changesets
583 608 adding manifests
584 609 adding file changes
585 610 added 1 changesets with 1 changes to 1 files
586 611 new changesets * (glob)
587 612 .hgsubstate: untracked file differs
588 613 abort: untracked files in working directory differ from files in requested revision
589 614 [255]
590 615 $ ls "$FAKEHOME"
591 616 a
592 617
593 618 on clone only symlink with hg-managed fakehome directory,
594 619 then pull (and update):
595 BROKEN: wrong error message
596 620
597 621 $ rm -fR "$FAKEHOME"
598 622 $ hg init "$FAKEHOME"
599 623 $ touch "$FAKEHOME/a"
600 624 $ hg -R "$FAKEHOME" ci -qAm 'add fakehome file'
601 625 $ SUB="$FAKEHOME" hg clone -qr0 main main7
602 626 $ ls "$FAKEHOME"
603 627 a
604 628 $ SUB="$FAKEHOME" hg -R main7 pull -uf
605 629 pulling from $TESTTMP/envvarsym/main
606 630 searching for changes
607 631 adding changesets
608 632 adding manifests
609 633 adding file changes
610 634 added 2 changesets with 3 changes to 2 files
611 635 new changesets * (glob)
612 abort: repository $TESTTMP/envvarsym/main7/$SUB already exists!
636 abort: subrepo path contains illegal component: $SUB
613 637 [255]
614 638 $ ls "$FAKEHOME"
615 639 a
616 640
617 641 $ cd ..
618 642
619 643 #endif
General Comments 0
You need to be logged in to leave comments. Login now