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