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