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