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