##// END OF EJS Templates
subrepo: only do clean update when overwrite is set (issue3276)...
Simon Heimberg -
r17895:17c03001 stable
parent child Browse files
Show More
@@ -1,634 +1,642
1 1 # hg.py - repository classes for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from i18n import _
10 10 from lock import release
11 11 from node import hex, nullid
12 12 import localrepo, bundlerepo, httppeer, sshpeer, statichttprepo, bookmarks
13 13 import lock, util, extensions, error, node, scmutil, phases, url
14 14 import cmdutil, discovery
15 15 import merge as mergemod
16 16 import verify as verifymod
17 17 import errno, os, shutil
18 18
19 19 def _local(path):
20 20 path = util.expandpath(util.urllocalpath(path))
21 21 return (os.path.isfile(path) and bundlerepo or localrepo)
22 22
23 23 def addbranchrevs(lrepo, other, branches, revs):
24 24 peer = other.peer() # a courtesy to callers using a localrepo for other
25 25 hashbranch, branches = branches
26 26 if not hashbranch and not branches:
27 27 return revs or None, revs and revs[0] or None
28 28 revs = revs and list(revs) or []
29 29 if not peer.capable('branchmap'):
30 30 if branches:
31 31 raise util.Abort(_("remote branch lookup not supported"))
32 32 revs.append(hashbranch)
33 33 return revs, revs[0]
34 34 branchmap = peer.branchmap()
35 35
36 36 def primary(branch):
37 37 if branch == '.':
38 38 if not lrepo:
39 39 raise util.Abort(_("dirstate branch not accessible"))
40 40 branch = lrepo.dirstate.branch()
41 41 if branch in branchmap:
42 42 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
43 43 return True
44 44 else:
45 45 return False
46 46
47 47 for branch in branches:
48 48 if not primary(branch):
49 49 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
50 50 if hashbranch:
51 51 if not primary(hashbranch):
52 52 revs.append(hashbranch)
53 53 return revs, revs[0]
54 54
55 55 def parseurl(path, branches=None):
56 56 '''parse url#branch, returning (url, (branch, branches))'''
57 57
58 58 u = util.url(path)
59 59 branch = None
60 60 if u.fragment:
61 61 branch = u.fragment
62 62 u.fragment = None
63 63 return str(u), (branch, branches or [])
64 64
65 65 schemes = {
66 66 'bundle': bundlerepo,
67 67 'file': _local,
68 68 'http': httppeer,
69 69 'https': httppeer,
70 70 'ssh': sshpeer,
71 71 'static-http': statichttprepo,
72 72 }
73 73
74 74 def _peerlookup(path):
75 75 u = util.url(path)
76 76 scheme = u.scheme or 'file'
77 77 thing = schemes.get(scheme) or schemes['file']
78 78 try:
79 79 return thing(path)
80 80 except TypeError:
81 81 return thing
82 82
83 83 def islocal(repo):
84 84 '''return true if repo or path is local'''
85 85 if isinstance(repo, str):
86 86 try:
87 87 return _peerlookup(repo).islocal(repo)
88 88 except AttributeError:
89 89 return False
90 90 return repo.local()
91 91
92 92 def openpath(ui, path):
93 93 '''open path with open if local, url.open if remote'''
94 94 if islocal(path):
95 95 return open(util.urllocalpath(path))
96 96 else:
97 97 return url.open(ui, path)
98 98
99 99 def _peerorrepo(ui, path, create=False):
100 100 """return a repository object for the specified path"""
101 101 obj = _peerlookup(path).instance(ui, path, create)
102 102 ui = getattr(obj, "ui", ui)
103 103 for name, module in extensions.extensions():
104 104 hook = getattr(module, 'reposetup', None)
105 105 if hook:
106 106 hook(ui, obj)
107 107 return obj
108 108
109 109 def repository(ui, path='', create=False):
110 110 """return a repository object for the specified path"""
111 111 peer = _peerorrepo(ui, path, create)
112 112 repo = peer.local()
113 113 if not repo:
114 114 raise util.Abort(_("repository '%s' is not local") %
115 115 (path or peer.url()))
116 116 return repo
117 117
118 118 def peer(uiorrepo, opts, path, create=False):
119 119 '''return a repository peer for the specified path'''
120 120 rui = remoteui(uiorrepo, opts)
121 121 return _peerorrepo(rui, path, create).peer()
122 122
123 123 def defaultdest(source):
124 124 '''return default destination of clone if none is given'''
125 125 return os.path.basename(os.path.normpath(util.url(source).path))
126 126
127 127 def share(ui, source, dest=None, update=True):
128 128 '''create a shared repository'''
129 129
130 130 if not islocal(source):
131 131 raise util.Abort(_('can only share local repositories'))
132 132
133 133 if not dest:
134 134 dest = defaultdest(source)
135 135 else:
136 136 dest = ui.expandpath(dest)
137 137
138 138 if isinstance(source, str):
139 139 origsource = ui.expandpath(source)
140 140 source, branches = parseurl(origsource)
141 141 srcrepo = repository(ui, source)
142 142 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
143 143 else:
144 144 srcrepo = source.local()
145 145 origsource = source = srcrepo.url()
146 146 checkout = None
147 147
148 148 sharedpath = srcrepo.sharedpath # if our source is already sharing
149 149
150 150 root = os.path.realpath(dest)
151 151 roothg = os.path.join(root, '.hg')
152 152
153 153 if os.path.exists(roothg):
154 154 raise util.Abort(_('destination already exists'))
155 155
156 156 if not os.path.isdir(root):
157 157 os.mkdir(root)
158 158 util.makedir(roothg, notindexed=True)
159 159
160 160 requirements = ''
161 161 try:
162 162 requirements = srcrepo.opener.read('requires')
163 163 except IOError, inst:
164 164 if inst.errno != errno.ENOENT:
165 165 raise
166 166
167 167 requirements += 'shared\n'
168 168 util.writefile(os.path.join(roothg, 'requires'), requirements)
169 169 util.writefile(os.path.join(roothg, 'sharedpath'), sharedpath)
170 170
171 171 r = repository(ui, root)
172 172
173 173 default = srcrepo.ui.config('paths', 'default')
174 174 if default:
175 175 fp = r.opener("hgrc", "w", text=True)
176 176 fp.write("[paths]\n")
177 177 fp.write("default = %s\n" % default)
178 178 fp.close()
179 179
180 180 if update:
181 181 r.ui.status(_("updating working directory\n"))
182 182 if update is not True:
183 183 checkout = update
184 184 for test in (checkout, 'default', 'tip'):
185 185 if test is None:
186 186 continue
187 187 try:
188 188 uprev = r.lookup(test)
189 189 break
190 190 except error.RepoLookupError:
191 191 continue
192 192 _update(r, uprev)
193 193
194 194 def copystore(ui, srcrepo, destpath):
195 195 '''copy files from store of srcrepo in destpath
196 196
197 197 returns destlock
198 198 '''
199 199 destlock = None
200 200 try:
201 201 hardlink = None
202 202 num = 0
203 203 srcpublishing = srcrepo.ui.configbool('phases', 'publish', True)
204 204 for f in srcrepo.store.copylist():
205 205 if srcpublishing and f.endswith('phaseroots'):
206 206 continue
207 207 src = os.path.join(srcrepo.sharedpath, f)
208 208 dst = os.path.join(destpath, f)
209 209 dstbase = os.path.dirname(dst)
210 210 if dstbase and not os.path.exists(dstbase):
211 211 os.mkdir(dstbase)
212 212 if os.path.exists(src):
213 213 if dst.endswith('data'):
214 214 # lock to avoid premature writing to the target
215 215 destlock = lock.lock(os.path.join(dstbase, "lock"))
216 216 hardlink, n = util.copyfiles(src, dst, hardlink)
217 217 num += n
218 218 if hardlink:
219 219 ui.debug("linked %d files\n" % num)
220 220 else:
221 221 ui.debug("copied %d files\n" % num)
222 222 return destlock
223 223 except: # re-raises
224 224 release(destlock)
225 225 raise
226 226
227 227 def clone(ui, peeropts, source, dest=None, pull=False, rev=None,
228 228 update=True, stream=False, branch=None):
229 229 """Make a copy of an existing repository.
230 230
231 231 Create a copy of an existing repository in a new directory. The
232 232 source and destination are URLs, as passed to the repository
233 233 function. Returns a pair of repository peers, the source and
234 234 newly created destination.
235 235
236 236 The location of the source is added to the new repository's
237 237 .hg/hgrc file, as the default to be used for future pulls and
238 238 pushes.
239 239
240 240 If an exception is raised, the partly cloned/updated destination
241 241 repository will be deleted.
242 242
243 243 Arguments:
244 244
245 245 source: repository object or URL
246 246
247 247 dest: URL of destination repository to create (defaults to base
248 248 name of source repository)
249 249
250 250 pull: always pull from source repository, even in local case
251 251
252 252 stream: stream raw data uncompressed from repository (fast over
253 253 LAN, slow over WAN)
254 254
255 255 rev: revision to clone up to (implies pull=True)
256 256
257 257 update: update working directory after clone completes, if
258 258 destination is local repository (True means update to default rev,
259 259 anything else is treated as a revision)
260 260
261 261 branch: branches to clone
262 262 """
263 263
264 264 if isinstance(source, str):
265 265 origsource = ui.expandpath(source)
266 266 source, branch = parseurl(origsource, branch)
267 267 srcpeer = peer(ui, peeropts, source)
268 268 else:
269 269 srcpeer = source.peer() # in case we were called with a localrepo
270 270 branch = (None, branch or [])
271 271 origsource = source = srcpeer.url()
272 272 rev, checkout = addbranchrevs(srcpeer, srcpeer, branch, rev)
273 273
274 274 if dest is None:
275 275 dest = defaultdest(source)
276 276 ui.status(_("destination directory: %s\n") % dest)
277 277 else:
278 278 dest = ui.expandpath(dest)
279 279
280 280 dest = util.urllocalpath(dest)
281 281 source = util.urllocalpath(source)
282 282
283 283 if not dest:
284 284 raise util.Abort(_("empty destination path is not valid"))
285 285 if os.path.exists(dest):
286 286 if not os.path.isdir(dest):
287 287 raise util.Abort(_("destination '%s' already exists") % dest)
288 288 elif os.listdir(dest):
289 289 raise util.Abort(_("destination '%s' is not empty") % dest)
290 290
291 291 class DirCleanup(object):
292 292 def __init__(self, dir_):
293 293 self.rmtree = shutil.rmtree
294 294 self.dir_ = dir_
295 295 def close(self):
296 296 self.dir_ = None
297 297 def cleanup(self):
298 298 if self.dir_:
299 299 self.rmtree(self.dir_, True)
300 300
301 301 srclock = destlock = dircleanup = None
302 302 srcrepo = srcpeer.local()
303 303 try:
304 304 abspath = origsource
305 305 if islocal(origsource):
306 306 abspath = os.path.abspath(util.urllocalpath(origsource))
307 307
308 308 if islocal(dest):
309 309 dircleanup = DirCleanup(dest)
310 310
311 311 copy = False
312 312 if (srcrepo and srcrepo.cancopy() and islocal(dest)
313 313 and not phases.hassecret(srcrepo)):
314 314 copy = not pull and not rev
315 315
316 316 if copy:
317 317 try:
318 318 # we use a lock here because if we race with commit, we
319 319 # can end up with extra data in the cloned revlogs that's
320 320 # not pointed to by changesets, thus causing verify to
321 321 # fail
322 322 srclock = srcrepo.lock(wait=False)
323 323 except error.LockError:
324 324 copy = False
325 325
326 326 if copy:
327 327 srcrepo.hook('preoutgoing', throw=True, source='clone')
328 328 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
329 329 if not os.path.exists(dest):
330 330 os.mkdir(dest)
331 331 else:
332 332 # only clean up directories we create ourselves
333 333 dircleanup.dir_ = hgdir
334 334 try:
335 335 destpath = hgdir
336 336 util.makedir(destpath, notindexed=True)
337 337 except OSError, inst:
338 338 if inst.errno == errno.EEXIST:
339 339 dircleanup.close()
340 340 raise util.Abort(_("destination '%s' already exists")
341 341 % dest)
342 342 raise
343 343
344 344 destlock = copystore(ui, srcrepo, destpath)
345 345
346 346 # Recomputing branch cache might be slow on big repos,
347 347 # so just copy it
348 348 dstcachedir = os.path.join(destpath, 'cache')
349 349 srcbranchcache = srcrepo.sjoin('cache/branchheads')
350 350 dstbranchcache = os.path.join(dstcachedir, 'branchheads')
351 351 if os.path.exists(srcbranchcache):
352 352 if not os.path.exists(dstcachedir):
353 353 os.mkdir(dstcachedir)
354 354 util.copyfile(srcbranchcache, dstbranchcache)
355 355
356 356 # we need to re-init the repo after manually copying the data
357 357 # into it
358 358 destpeer = peer(srcrepo, peeropts, dest)
359 359 srcrepo.hook('outgoing', source='clone',
360 360 node=node.hex(node.nullid))
361 361 else:
362 362 try:
363 363 destpeer = peer(srcrepo or ui, peeropts, dest, create=True)
364 364 # only pass ui when no srcrepo
365 365 except OSError, inst:
366 366 if inst.errno == errno.EEXIST:
367 367 dircleanup.close()
368 368 raise util.Abort(_("destination '%s' already exists")
369 369 % dest)
370 370 raise
371 371
372 372 revs = None
373 373 if rev:
374 374 if not srcpeer.capable('lookup'):
375 375 raise util.Abort(_("src repository does not support "
376 376 "revision lookup and so doesn't "
377 377 "support clone by revision"))
378 378 revs = [srcpeer.lookup(r) for r in rev]
379 379 checkout = revs[0]
380 380 if destpeer.local():
381 381 destpeer.local().clone(srcpeer, heads=revs, stream=stream)
382 382 elif srcrepo:
383 383 srcrepo.push(destpeer, revs=revs)
384 384 else:
385 385 raise util.Abort(_("clone from remote to remote not supported"))
386 386
387 387 if dircleanup:
388 388 dircleanup.close()
389 389
390 390 # clone all bookmarks except divergent ones
391 391 destrepo = destpeer.local()
392 392 if destrepo and srcpeer.capable("pushkey"):
393 393 rb = srcpeer.listkeys('bookmarks')
394 394 for k, n in rb.iteritems():
395 395 try:
396 396 m = destrepo.lookup(n)
397 397 destrepo._bookmarks[k] = m
398 398 except error.RepoLookupError:
399 399 pass
400 400 if rb:
401 401 bookmarks.write(destrepo)
402 402 elif srcrepo and destpeer.capable("pushkey"):
403 403 for k, n in srcrepo._bookmarks.iteritems():
404 404 destpeer.pushkey('bookmarks', k, '', hex(n))
405 405
406 406 if destrepo:
407 407 fp = destrepo.opener("hgrc", "w", text=True)
408 408 fp.write("[paths]\n")
409 409 u = util.url(abspath)
410 410 u.passwd = None
411 411 defaulturl = str(u)
412 412 fp.write("default = %s\n" % defaulturl)
413 413 fp.close()
414 414
415 415 destrepo.ui.setconfig('paths', 'default', defaulturl)
416 416
417 417 if update:
418 418 if update is not True:
419 419 checkout = srcpeer.lookup(update)
420 420 uprev = None
421 421 status = None
422 422 if checkout is not None:
423 423 try:
424 424 uprev = destrepo.lookup(checkout)
425 425 except error.RepoLookupError:
426 426 pass
427 427 if uprev is None:
428 428 try:
429 429 uprev = destrepo._bookmarks['@']
430 430 update = '@'
431 431 bn = destrepo[uprev].branch()
432 432 if bn == 'default':
433 433 status = _("updating to bookmark @\n")
434 434 else:
435 435 status = _("updating to bookmark @ on branch %s\n"
436 436 % bn)
437 437 except KeyError:
438 438 try:
439 439 uprev = destrepo.branchtip('default')
440 440 except error.RepoLookupError:
441 441 uprev = destrepo.lookup('tip')
442 442 if not status:
443 443 bn = destrepo[uprev].branch()
444 444 status = _("updating to branch %s\n") % bn
445 445 destrepo.ui.status(status)
446 446 _update(destrepo, uprev)
447 447 if update in destrepo._bookmarks:
448 448 bookmarks.setcurrent(destrepo, update)
449 449
450 450 return srcpeer, destpeer
451 451 finally:
452 452 release(srclock, destlock)
453 453 if dircleanup is not None:
454 454 dircleanup.cleanup()
455 455 if srcpeer is not None:
456 456 srcpeer.close()
457 457
458 458 def _showstats(repo, stats):
459 459 repo.ui.status(_("%d files updated, %d files merged, "
460 460 "%d files removed, %d files unresolved\n") % stats)
461 461
462 def updaterepo(repo, node, overwrite):
463 """Update the working directory to node.
464
465 When overwrite is set, changes are clobbered, merged else
466
467 returns stats (see pydoc mercurial.merge.applyupdates)"""
468 return mergemod.update(repo, node, False, overwrite, None)
469
462 470 def update(repo, node):
463 471 """update the working directory to node, merging linear changes"""
464 stats = mergemod.update(repo, node, False, False, None)
472 stats = updaterepo(repo, node, False)
465 473 _showstats(repo, stats)
466 474 if stats[3]:
467 475 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
468 476 return stats[3] > 0
469 477
470 478 # naming conflict in clone()
471 479 _update = update
472 480
473 481 def clean(repo, node, show_stats=True):
474 482 """forcibly switch the working directory to node, clobbering changes"""
475 stats = mergemod.update(repo, node, False, True, None)
483 stats = updaterepo(repo, node, True)
476 484 if show_stats:
477 485 _showstats(repo, stats)
478 486 return stats[3] > 0
479 487
480 488 def merge(repo, node, force=None, remind=True):
481 489 """Branch merge with node, resolving changes. Return true if any
482 490 unresolved conflicts."""
483 491 stats = mergemod.update(repo, node, True, force, False)
484 492 _showstats(repo, stats)
485 493 if stats[3]:
486 494 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
487 495 "or 'hg update -C .' to abandon\n"))
488 496 elif remind:
489 497 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
490 498 return stats[3] > 0
491 499
492 500 def _incoming(displaychlist, subreporecurse, ui, repo, source,
493 501 opts, buffered=False):
494 502 """
495 503 Helper for incoming / gincoming.
496 504 displaychlist gets called with
497 505 (remoterepo, incomingchangesetlist, displayer) parameters,
498 506 and is supposed to contain only code that can't be unified.
499 507 """
500 508 source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
501 509 other = peer(repo, opts, source)
502 510 ui.status(_('comparing with %s\n') % util.hidepassword(source))
503 511 revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
504 512
505 513 if revs:
506 514 revs = [other.lookup(rev) for rev in revs]
507 515 other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
508 516 revs, opts["bundle"], opts["force"])
509 517 try:
510 518 if not chlist:
511 519 ui.status(_("no changes found\n"))
512 520 return subreporecurse()
513 521
514 522 displayer = cmdutil.show_changeset(ui, other, opts, buffered)
515 523
516 524 # XXX once graphlog extension makes it into core,
517 525 # should be replaced by a if graph/else
518 526 displaychlist(other, chlist, displayer)
519 527
520 528 displayer.close()
521 529 finally:
522 530 cleanupfn()
523 531 subreporecurse()
524 532 return 0 # exit code is zero since we found incoming changes
525 533
526 534 def incoming(ui, repo, source, opts):
527 535 def subreporecurse():
528 536 ret = 1
529 537 if opts.get('subrepos'):
530 538 ctx = repo[None]
531 539 for subpath in sorted(ctx.substate):
532 540 sub = ctx.sub(subpath)
533 541 ret = min(ret, sub.incoming(ui, source, opts))
534 542 return ret
535 543
536 544 def display(other, chlist, displayer):
537 545 limit = cmdutil.loglimit(opts)
538 546 if opts.get('newest_first'):
539 547 chlist.reverse()
540 548 count = 0
541 549 for n in chlist:
542 550 if limit is not None and count >= limit:
543 551 break
544 552 parents = [p for p in other.changelog.parents(n) if p != nullid]
545 553 if opts.get('no_merges') and len(parents) == 2:
546 554 continue
547 555 count += 1
548 556 displayer.show(other[n])
549 557 return _incoming(display, subreporecurse, ui, repo, source, opts)
550 558
551 559 def _outgoing(ui, repo, dest, opts):
552 560 dest = ui.expandpath(dest or 'default-push', dest or 'default')
553 561 dest, branches = parseurl(dest, opts.get('branch'))
554 562 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
555 563 revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
556 564 if revs:
557 565 revs = [repo.lookup(rev) for rev in scmutil.revrange(repo, revs)]
558 566
559 567 other = peer(repo, opts, dest)
560 568 outgoing = discovery.findcommonoutgoing(repo, other, revs,
561 569 force=opts.get('force'))
562 570 o = outgoing.missing
563 571 if not o:
564 572 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
565 573 return None
566 574 return o
567 575
568 576 def outgoing(ui, repo, dest, opts):
569 577 def recurse():
570 578 ret = 1
571 579 if opts.get('subrepos'):
572 580 ctx = repo[None]
573 581 for subpath in sorted(ctx.substate):
574 582 sub = ctx.sub(subpath)
575 583 ret = min(ret, sub.outgoing(ui, dest, opts))
576 584 return ret
577 585
578 586 limit = cmdutil.loglimit(opts)
579 587 o = _outgoing(ui, repo, dest, opts)
580 588 if o is None:
581 589 return recurse()
582 590
583 591 if opts.get('newest_first'):
584 592 o.reverse()
585 593 displayer = cmdutil.show_changeset(ui, repo, opts)
586 594 count = 0
587 595 for n in o:
588 596 if limit is not None and count >= limit:
589 597 break
590 598 parents = [p for p in repo.changelog.parents(n) if p != nullid]
591 599 if opts.get('no_merges') and len(parents) == 2:
592 600 continue
593 601 count += 1
594 602 displayer.show(repo[n])
595 603 displayer.close()
596 604 recurse()
597 605 return 0 # exit code is zero since we found outgoing changes
598 606
599 607 def revert(repo, node, choose):
600 608 """revert changes to revision in node without updating dirstate"""
601 609 return mergemod.update(repo, node, False, True, choose)[3] > 0
602 610
603 611 def verify(repo):
604 612 """verify the consistency of a repository"""
605 613 return verifymod.verify(repo)
606 614
607 615 def remoteui(src, opts):
608 616 'build a remote ui from ui or repo and opts'
609 617 if util.safehasattr(src, 'baseui'): # looks like a repository
610 618 dst = src.baseui.copy() # drop repo-specific config
611 619 src = src.ui # copy target options from repo
612 620 else: # assume it's a global ui object
613 621 dst = src.copy() # keep all global options
614 622
615 623 # copy ssh-specific options
616 624 for o in 'ssh', 'remotecmd':
617 625 v = opts.get(o) or src.config('ui', o)
618 626 if v:
619 627 dst.setconfig("ui", o, v)
620 628
621 629 # copy bundle-specific options
622 630 r = src.config('bundle', 'mainreporoot')
623 631 if r:
624 632 dst.setconfig('bundle', 'mainreporoot', r)
625 633
626 634 # copy selected local settings to the remote ui
627 635 for sect in ('auth', 'hostfingerprints', 'http_proxy'):
628 636 for key, val in src.configitems(sect):
629 637 dst.setconfig(sect, key, val)
630 638 v = src.config('web', 'cacerts')
631 639 if v:
632 640 dst.setconfig('web', 'cacerts', util.expandpath(v))
633 641
634 642 return dst
@@ -1,1281 +1,1281
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 errno, os, re, xml.dom.minidom, shutil, posixpath
9 9 import stat, subprocess, tarfile
10 10 from i18n import _
11 11 import config, scmutil, util, node, error, cmdutil, bookmarks, match as matchmod
12 12 hg = None
13 13 propertycache = util.propertycache
14 14
15 15 nullstate = ('', '', 'empty')
16 16
17 17 def state(ctx, ui):
18 18 """return a state dict, mapping subrepo paths configured in .hgsub
19 19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 20 (key in types dict))
21 21 """
22 22 p = config.config()
23 23 def read(f, sections=None, remap=None):
24 24 if f in ctx:
25 25 try:
26 26 data = ctx[f].data()
27 27 except IOError, err:
28 28 if err.errno != errno.ENOENT:
29 29 raise
30 30 # handle missing subrepo spec files as removed
31 31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 32 return
33 33 p.parse(f, data, sections, remap, read)
34 34 else:
35 35 raise util.Abort(_("subrepo spec file %s not found") % f)
36 36
37 37 if '.hgsub' in ctx:
38 38 read('.hgsub')
39 39
40 40 for path, src in ui.configitems('subpaths'):
41 41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42 42
43 43 rev = {}
44 44 if '.hgsubstate' in ctx:
45 45 try:
46 46 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
47 47 l = l.lstrip()
48 48 if not l:
49 49 continue
50 50 try:
51 51 revision, path = l.split(" ", 1)
52 52 except ValueError:
53 53 raise util.Abort(_("invalid subrepository revision "
54 54 "specifier in .hgsubstate line %d")
55 55 % (i + 1))
56 56 rev[path] = revision
57 57 except IOError, err:
58 58 if err.errno != errno.ENOENT:
59 59 raise
60 60
61 61 def remap(src):
62 62 for pattern, repl in p.items('subpaths'):
63 63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
64 64 # does a string decode.
65 65 repl = repl.encode('string-escape')
66 66 # However, we still want to allow back references to go
67 67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
68 68 # extra escapes are needed because re.sub string decodes.
69 69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
70 70 try:
71 71 src = re.sub(pattern, repl, src, 1)
72 72 except re.error, e:
73 73 raise util.Abort(_("bad subrepository pattern in %s: %s")
74 74 % (p.source('subpaths', pattern), e))
75 75 return src
76 76
77 77 state = {}
78 78 for path, src in p[''].items():
79 79 kind = 'hg'
80 80 if src.startswith('['):
81 81 if ']' not in src:
82 82 raise util.Abort(_('missing ] in subrepo source'))
83 83 kind, src = src.split(']', 1)
84 84 kind = kind[1:]
85 85 src = src.lstrip() # strip any extra whitespace after ']'
86 86
87 87 if not util.url(src).isabs():
88 88 parent = _abssource(ctx._repo, abort=False)
89 89 if parent:
90 90 parent = util.url(parent)
91 91 parent.path = posixpath.join(parent.path or '', src)
92 92 parent.path = posixpath.normpath(parent.path)
93 93 joined = str(parent)
94 94 # Remap the full joined path and use it if it changes,
95 95 # else remap the original source.
96 96 remapped = remap(joined)
97 97 if remapped == joined:
98 98 src = remap(src)
99 99 else:
100 100 src = remapped
101 101
102 102 src = remap(src)
103 103 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
104 104
105 105 return state
106 106
107 107 def writestate(repo, state):
108 108 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
109 109 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
110 110 repo.wwrite('.hgsubstate', ''.join(lines), '')
111 111
112 112 def submerge(repo, wctx, mctx, actx, overwrite):
113 113 """delegated from merge.applyupdates: merging of .hgsubstate file
114 114 in working context, merging context and ancestor context"""
115 115 if mctx == actx: # backwards?
116 116 actx = wctx.p1()
117 117 s1 = wctx.substate
118 118 s2 = mctx.substate
119 119 sa = actx.substate
120 120 sm = {}
121 121
122 122 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
123 123
124 124 def debug(s, msg, r=""):
125 125 if r:
126 126 r = "%s:%s:%s" % r
127 127 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
128 128
129 129 for s, l in s1.items():
130 130 a = sa.get(s, nullstate)
131 131 ld = l # local state with possible dirty flag for compares
132 132 if wctx.sub(s).dirty():
133 133 ld = (l[0], l[1] + "+")
134 134 if wctx == actx: # overwrite
135 135 a = ld
136 136
137 137 if s in s2:
138 138 r = s2[s]
139 139 if ld == r or r == a: # no change or local is newer
140 140 sm[s] = l
141 141 continue
142 142 elif ld == a: # other side changed
143 143 debug(s, "other changed, get", r)
144 144 wctx.sub(s).get(r, overwrite)
145 145 sm[s] = r
146 146 elif ld[0] != r[0]: # sources differ
147 147 if repo.ui.promptchoice(
148 148 _(' subrepository sources for %s differ\n'
149 149 'use (l)ocal source (%s) or (r)emote source (%s)?')
150 150 % (s, l[0], r[0]),
151 151 (_('&Local'), _('&Remote')), 0):
152 152 debug(s, "prompt changed, get", r)
153 153 wctx.sub(s).get(r, overwrite)
154 154 sm[s] = r
155 155 elif ld[1] == a[1]: # local side is unchanged
156 156 debug(s, "other side changed, get", r)
157 157 wctx.sub(s).get(r, overwrite)
158 158 sm[s] = r
159 159 else:
160 160 debug(s, "both sides changed, merge with", r)
161 161 wctx.sub(s).merge(r)
162 162 sm[s] = l
163 163 elif ld == a: # remote removed, local unchanged
164 164 debug(s, "remote removed, remove")
165 165 wctx.sub(s).remove()
166 166 elif a == nullstate: # not present in remote or ancestor
167 167 debug(s, "local added, keep")
168 168 sm[s] = l
169 169 continue
170 170 else:
171 171 if repo.ui.promptchoice(
172 172 _(' local changed subrepository %s which remote removed\n'
173 173 'use (c)hanged version or (d)elete?') % s,
174 174 (_('&Changed'), _('&Delete')), 0):
175 175 debug(s, "prompt remove")
176 176 wctx.sub(s).remove()
177 177
178 178 for s, r in sorted(s2.items()):
179 179 if s in s1:
180 180 continue
181 181 elif s not in sa:
182 182 debug(s, "remote added, get", r)
183 183 mctx.sub(s).get(r)
184 184 sm[s] = r
185 185 elif r != sa[s]:
186 186 if repo.ui.promptchoice(
187 187 _(' remote changed subrepository %s which local removed\n'
188 188 'use (c)hanged version or (d)elete?') % s,
189 189 (_('&Changed'), _('&Delete')), 0) == 0:
190 190 debug(s, "prompt recreate", r)
191 191 wctx.sub(s).get(r)
192 192 sm[s] = r
193 193
194 194 # record merged .hgsubstate
195 195 writestate(repo, sm)
196 196
197 197 def _updateprompt(ui, sub, dirty, local, remote):
198 198 if dirty:
199 199 msg = (_(' subrepository sources for %s differ\n'
200 200 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
201 201 % (subrelpath(sub), local, remote))
202 202 else:
203 203 msg = (_(' subrepository sources for %s differ (in checked out '
204 204 'version)\n'
205 205 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
206 206 % (subrelpath(sub), local, remote))
207 207 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
208 208
209 209 def reporelpath(repo):
210 210 """return path to this (sub)repo as seen from outermost repo"""
211 211 parent = repo
212 212 while util.safehasattr(parent, '_subparent'):
213 213 parent = parent._subparent
214 214 p = parent.root.rstrip(os.sep)
215 215 return repo.root[len(p) + 1:]
216 216
217 217 def subrelpath(sub):
218 218 """return path to this subrepo as seen from outermost repo"""
219 219 if util.safehasattr(sub, '_relpath'):
220 220 return sub._relpath
221 221 if not util.safehasattr(sub, '_repo'):
222 222 return sub._path
223 223 return reporelpath(sub._repo)
224 224
225 225 def _abssource(repo, push=False, abort=True):
226 226 """return pull/push path of repo - either based on parent repo .hgsub info
227 227 or on the top repo config. Abort or return None if no source found."""
228 228 if util.safehasattr(repo, '_subparent'):
229 229 source = util.url(repo._subsource)
230 230 if source.isabs():
231 231 return str(source)
232 232 source.path = posixpath.normpath(source.path)
233 233 parent = _abssource(repo._subparent, push, abort=False)
234 234 if parent:
235 235 parent = util.url(util.pconvert(parent))
236 236 parent.path = posixpath.join(parent.path or '', source.path)
237 237 parent.path = posixpath.normpath(parent.path)
238 238 return str(parent)
239 239 else: # recursion reached top repo
240 240 if util.safehasattr(repo, '_subtoppath'):
241 241 return repo._subtoppath
242 242 if push and repo.ui.config('paths', 'default-push'):
243 243 return repo.ui.config('paths', 'default-push')
244 244 if repo.ui.config('paths', 'default'):
245 245 return repo.ui.config('paths', 'default')
246 246 if abort:
247 247 raise util.Abort(_("default path for subrepository %s not found") %
248 248 reporelpath(repo))
249 249
250 250 def itersubrepos(ctx1, ctx2):
251 251 """find subrepos in ctx1 or ctx2"""
252 252 # Create a (subpath, ctx) mapping where we prefer subpaths from
253 253 # ctx1. The subpaths from ctx2 are important when the .hgsub file
254 254 # has been modified (in ctx2) but not yet committed (in ctx1).
255 255 subpaths = dict.fromkeys(ctx2.substate, ctx2)
256 256 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
257 257 for subpath, ctx in sorted(subpaths.iteritems()):
258 258 yield subpath, ctx.sub(subpath)
259 259
260 260 def subrepo(ctx, path):
261 261 """return instance of the right subrepo class for subrepo in path"""
262 262 # subrepo inherently violates our import layering rules
263 263 # because it wants to make repo objects from deep inside the stack
264 264 # so we manually delay the circular imports to not break
265 265 # scripts that don't use our demand-loading
266 266 global hg
267 267 import hg as h
268 268 hg = h
269 269
270 270 scmutil.pathauditor(ctx._repo.root)(path)
271 271 state = ctx.substate[path]
272 272 if state[2] not in types:
273 273 raise util.Abort(_('unknown subrepo type %s') % state[2])
274 274 return types[state[2]](ctx, path, state[:2])
275 275
276 276 # subrepo classes need to implement the following abstract class:
277 277
278 278 class abstractsubrepo(object):
279 279
280 280 def dirty(self, ignoreupdate=False):
281 281 """returns true if the dirstate of the subrepo is dirty or does not
282 282 match current stored state. If ignoreupdate is true, only check
283 283 whether the subrepo has uncommitted changes in its dirstate.
284 284 """
285 285 raise NotImplementedError
286 286
287 287 def basestate(self):
288 288 """current working directory base state, disregarding .hgsubstate
289 289 state and working directory modifications"""
290 290 raise NotImplementedError
291 291
292 292 def checknested(self, path):
293 293 """check if path is a subrepository within this repository"""
294 294 return False
295 295
296 296 def commit(self, text, user, date):
297 297 """commit the current changes to the subrepo with the given
298 298 log message. Use given user and date if possible. Return the
299 299 new state of the subrepo.
300 300 """
301 301 raise NotImplementedError
302 302
303 303 def remove(self):
304 304 """remove the subrepo
305 305
306 306 (should verify the dirstate is not dirty first)
307 307 """
308 308 raise NotImplementedError
309 309
310 310 def get(self, state, overwrite=False):
311 311 """run whatever commands are needed to put the subrepo into
312 312 this state
313 313 """
314 314 raise NotImplementedError
315 315
316 316 def merge(self, state):
317 317 """merge currently-saved state with the new state."""
318 318 raise NotImplementedError
319 319
320 320 def push(self, opts):
321 321 """perform whatever action is analogous to 'hg push'
322 322
323 323 This may be a no-op on some systems.
324 324 """
325 325 raise NotImplementedError
326 326
327 327 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
328 328 return []
329 329
330 330 def status(self, rev2, **opts):
331 331 return [], [], [], [], [], [], []
332 332
333 333 def diff(self, diffopts, node2, match, prefix, **opts):
334 334 pass
335 335
336 336 def outgoing(self, ui, dest, opts):
337 337 return 1
338 338
339 339 def incoming(self, ui, source, opts):
340 340 return 1
341 341
342 342 def files(self):
343 343 """return filename iterator"""
344 344 raise NotImplementedError
345 345
346 346 def filedata(self, name):
347 347 """return file data"""
348 348 raise NotImplementedError
349 349
350 350 def fileflags(self, name):
351 351 """return file flags"""
352 352 return ''
353 353
354 354 def archive(self, ui, archiver, prefix, match=None):
355 355 if match is not None:
356 356 files = [f for f in self.files() if match(f)]
357 357 else:
358 358 files = self.files()
359 359 total = len(files)
360 360 relpath = subrelpath(self)
361 361 ui.progress(_('archiving (%s)') % relpath, 0,
362 362 unit=_('files'), total=total)
363 363 for i, name in enumerate(files):
364 364 flags = self.fileflags(name)
365 365 mode = 'x' in flags and 0755 or 0644
366 366 symlink = 'l' in flags
367 367 archiver.addfile(os.path.join(prefix, self._path, name),
368 368 mode, symlink, self.filedata(name))
369 369 ui.progress(_('archiving (%s)') % relpath, i + 1,
370 370 unit=_('files'), total=total)
371 371 ui.progress(_('archiving (%s)') % relpath, None)
372 372
373 373 def walk(self, match):
374 374 '''
375 375 walk recursively through the directory tree, finding all files
376 376 matched by the match function
377 377 '''
378 378 pass
379 379
380 380 def forget(self, ui, match, prefix):
381 381 return ([], [])
382 382
383 383 def revert(self, ui, substate, *pats, **opts):
384 384 ui.warn('%s: reverting %s subrepos is unsupported\n' \
385 385 % (substate[0], substate[2]))
386 386 return []
387 387
388 388 class hgsubrepo(abstractsubrepo):
389 389 def __init__(self, ctx, path, state):
390 390 self._path = path
391 391 self._state = state
392 392 r = ctx._repo
393 393 root = r.wjoin(path)
394 394 create = False
395 395 if not os.path.exists(os.path.join(root, '.hg')):
396 396 create = True
397 397 util.makedirs(root)
398 398 self._repo = hg.repository(r.baseui, root, create=create)
399 399 for s, k in [('ui', 'commitsubrepos')]:
400 400 v = r.ui.config(s, k)
401 401 if v:
402 402 self._repo.ui.setconfig(s, k, v)
403 403 self._initrepo(r, state[0], create)
404 404
405 405 def _initrepo(self, parentrepo, source, create):
406 406 self._repo._subparent = parentrepo
407 407 self._repo._subsource = source
408 408
409 409 if create:
410 410 fp = self._repo.opener("hgrc", "w", text=True)
411 411 fp.write('[paths]\n')
412 412
413 413 def addpathconfig(key, value):
414 414 if value:
415 415 fp.write('%s = %s\n' % (key, value))
416 416 self._repo.ui.setconfig('paths', key, value)
417 417
418 418 defpath = _abssource(self._repo, abort=False)
419 419 defpushpath = _abssource(self._repo, True, abort=False)
420 420 addpathconfig('default', defpath)
421 421 if defpath != defpushpath:
422 422 addpathconfig('default-push', defpushpath)
423 423 fp.close()
424 424
425 425 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
426 426 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
427 427 os.path.join(prefix, self._path), explicitonly)
428 428
429 429 def status(self, rev2, **opts):
430 430 try:
431 431 rev1 = self._state[1]
432 432 ctx1 = self._repo[rev1]
433 433 ctx2 = self._repo[rev2]
434 434 return self._repo.status(ctx1, ctx2, **opts)
435 435 except error.RepoLookupError, inst:
436 436 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
437 437 % (inst, subrelpath(self)))
438 438 return [], [], [], [], [], [], []
439 439
440 440 def diff(self, diffopts, node2, match, prefix, **opts):
441 441 try:
442 442 node1 = node.bin(self._state[1])
443 443 # We currently expect node2 to come from substate and be
444 444 # in hex format
445 445 if node2 is not None:
446 446 node2 = node.bin(node2)
447 447 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
448 448 node1, node2, match,
449 449 prefix=os.path.join(prefix, self._path),
450 450 listsubrepos=True, **opts)
451 451 except error.RepoLookupError, inst:
452 452 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
453 453 % (inst, subrelpath(self)))
454 454
455 455 def archive(self, ui, archiver, prefix, match=None):
456 456 self._get(self._state + ('hg',))
457 457 abstractsubrepo.archive(self, ui, archiver, prefix, match)
458 458
459 459 rev = self._state[1]
460 460 ctx = self._repo[rev]
461 461 for subpath in ctx.substate:
462 462 s = subrepo(ctx, subpath)
463 463 submatch = matchmod.narrowmatcher(subpath, match)
464 464 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
465 465
466 466 def dirty(self, ignoreupdate=False):
467 467 r = self._state[1]
468 468 if r == '' and not ignoreupdate: # no state recorded
469 469 return True
470 470 w = self._repo[None]
471 471 if r != w.p1().hex() and not ignoreupdate:
472 472 # different version checked out
473 473 return True
474 474 return w.dirty() # working directory changed
475 475
476 476 def basestate(self):
477 477 return self._repo['.'].hex()
478 478
479 479 def checknested(self, path):
480 480 return self._repo._checknested(self._repo.wjoin(path))
481 481
482 482 def commit(self, text, user, date):
483 483 # don't bother committing in the subrepo if it's only been
484 484 # updated
485 485 if not self.dirty(True):
486 486 return self._repo['.'].hex()
487 487 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
488 488 n = self._repo.commit(text, user, date)
489 489 if not n:
490 490 return self._repo['.'].hex() # different version checked out
491 491 return node.hex(n)
492 492
493 493 def remove(self):
494 494 # we can't fully delete the repository as it may contain
495 495 # local-only history
496 496 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
497 497 hg.clean(self._repo, node.nullid, False)
498 498
499 499 def _get(self, state):
500 500 source, revision, kind = state
501 501 if revision not in self._repo:
502 502 self._repo._subsource = source
503 503 srcurl = _abssource(self._repo)
504 504 other = hg.peer(self._repo, {}, srcurl)
505 505 if len(self._repo) == 0:
506 506 self._repo.ui.status(_('cloning subrepo %s from %s\n')
507 507 % (subrelpath(self), srcurl))
508 508 parentrepo = self._repo._subparent
509 509 shutil.rmtree(self._repo.path)
510 510 other, cloned = hg.clone(self._repo._subparent.baseui, {},
511 511 other, self._repo.root,
512 512 update=False)
513 513 self._repo = cloned.local()
514 514 self._initrepo(parentrepo, source, create=True)
515 515 else:
516 516 self._repo.ui.status(_('pulling subrepo %s from %s\n')
517 517 % (subrelpath(self), srcurl))
518 518 self._repo.pull(other)
519 519 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
520 520 srcurl)
521 521
522 522 def get(self, state, overwrite=False):
523 523 self._get(state)
524 524 source, revision, kind = state
525 525 self._repo.ui.debug("getting subrepo %s\n" % self._path)
526 hg.clean(self._repo, revision, False)
526 hg.updaterepo(self._repo, revision, overwrite)
527 527
528 528 def merge(self, state):
529 529 self._get(state)
530 530 cur = self._repo['.']
531 531 dst = self._repo[state[1]]
532 532 anc = dst.ancestor(cur)
533 533
534 534 def mergefunc():
535 535 if anc == cur and dst.branch() == cur.branch():
536 536 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
537 537 hg.update(self._repo, state[1])
538 538 elif anc == dst:
539 539 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
540 540 else:
541 541 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
542 542 hg.merge(self._repo, state[1], remind=False)
543 543
544 544 wctx = self._repo[None]
545 545 if self.dirty():
546 546 if anc != dst:
547 547 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
548 548 mergefunc()
549 549 else:
550 550 mergefunc()
551 551 else:
552 552 mergefunc()
553 553
554 554 def push(self, opts):
555 555 force = opts.get('force')
556 556 newbranch = opts.get('new_branch')
557 557 ssh = opts.get('ssh')
558 558
559 559 # push subrepos depth-first for coherent ordering
560 560 c = self._repo['']
561 561 subs = c.substate # only repos that are committed
562 562 for s in sorted(subs):
563 563 if c.sub(s).push(opts) == 0:
564 564 return False
565 565
566 566 dsturl = _abssource(self._repo, True)
567 567 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
568 568 (subrelpath(self), dsturl))
569 569 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
570 570 return self._repo.push(other, force, newbranch=newbranch)
571 571
572 572 def outgoing(self, ui, dest, opts):
573 573 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
574 574
575 575 def incoming(self, ui, source, opts):
576 576 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
577 577
578 578 def files(self):
579 579 rev = self._state[1]
580 580 ctx = self._repo[rev]
581 581 return ctx.manifest()
582 582
583 583 def filedata(self, name):
584 584 rev = self._state[1]
585 585 return self._repo[rev][name].data()
586 586
587 587 def fileflags(self, name):
588 588 rev = self._state[1]
589 589 ctx = self._repo[rev]
590 590 return ctx.flags(name)
591 591
592 592 def walk(self, match):
593 593 ctx = self._repo[None]
594 594 return ctx.walk(match)
595 595
596 596 def forget(self, ui, match, prefix):
597 597 return cmdutil.forget(ui, self._repo, match,
598 598 os.path.join(prefix, self._path), True)
599 599
600 600 def revert(self, ui, substate, *pats, **opts):
601 601 # reverting a subrepo is a 2 step process:
602 602 # 1. if the no_backup is not set, revert all modified
603 603 # files inside the subrepo
604 604 # 2. update the subrepo to the revision specified in
605 605 # the corresponding substate dictionary
606 606 ui.status(_('reverting subrepo %s\n') % substate[0])
607 607 if not opts.get('no_backup'):
608 608 # Revert all files on the subrepo, creating backups
609 609 # Note that this will not recursively revert subrepos
610 610 # We could do it if there was a set:subrepos() predicate
611 611 opts = opts.copy()
612 612 opts['date'] = None
613 613 opts['rev'] = substate[1]
614 614
615 615 pats = []
616 616 if not opts['all']:
617 617 pats = ['set:modified()']
618 618 self.filerevert(ui, *pats, **opts)
619 619
620 620 # Update the repo to the revision specified in the given substate
621 621 self.get(substate, overwrite=True)
622 622
623 623 def filerevert(self, ui, *pats, **opts):
624 624 ctx = self._repo[opts['rev']]
625 625 parents = self._repo.dirstate.parents()
626 626 if opts['all']:
627 627 pats = ['set:modified()']
628 628 else:
629 629 pats = []
630 630 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
631 631
632 632 class svnsubrepo(abstractsubrepo):
633 633 def __init__(self, ctx, path, state):
634 634 self._path = path
635 635 self._state = state
636 636 self._ctx = ctx
637 637 self._ui = ctx._repo.ui
638 638 self._exe = util.findexe('svn')
639 639 if not self._exe:
640 640 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
641 641 % self._path)
642 642
643 643 def _svncommand(self, commands, filename='', failok=False):
644 644 cmd = [self._exe]
645 645 extrakw = {}
646 646 if not self._ui.interactive():
647 647 # Making stdin be a pipe should prevent svn from behaving
648 648 # interactively even if we can't pass --non-interactive.
649 649 extrakw['stdin'] = subprocess.PIPE
650 650 # Starting in svn 1.5 --non-interactive is a global flag
651 651 # instead of being per-command, but we need to support 1.4 so
652 652 # we have to be intelligent about what commands take
653 653 # --non-interactive.
654 654 if commands[0] in ('update', 'checkout', 'commit'):
655 655 cmd.append('--non-interactive')
656 656 cmd.extend(commands)
657 657 if filename is not None:
658 658 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
659 659 cmd.append(path)
660 660 env = dict(os.environ)
661 661 # Avoid localized output, preserve current locale for everything else.
662 662 lc_all = env.get('LC_ALL')
663 663 if lc_all:
664 664 env['LANG'] = lc_all
665 665 del env['LC_ALL']
666 666 env['LC_MESSAGES'] = 'C'
667 667 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
668 668 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
669 669 universal_newlines=True, env=env, **extrakw)
670 670 stdout, stderr = p.communicate()
671 671 stderr = stderr.strip()
672 672 if not failok:
673 673 if p.returncode:
674 674 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
675 675 if stderr:
676 676 self._ui.warn(stderr + '\n')
677 677 return stdout, stderr
678 678
679 679 @propertycache
680 680 def _svnversion(self):
681 681 output, err = self._svncommand(['--version', '--quiet'], filename=None)
682 682 m = re.search(r'^(\d+)\.(\d+)', output)
683 683 if not m:
684 684 raise util.Abort(_('cannot retrieve svn tool version'))
685 685 return (int(m.group(1)), int(m.group(2)))
686 686
687 687 def _wcrevs(self):
688 688 # Get the working directory revision as well as the last
689 689 # commit revision so we can compare the subrepo state with
690 690 # both. We used to store the working directory one.
691 691 output, err = self._svncommand(['info', '--xml'])
692 692 doc = xml.dom.minidom.parseString(output)
693 693 entries = doc.getElementsByTagName('entry')
694 694 lastrev, rev = '0', '0'
695 695 if entries:
696 696 rev = str(entries[0].getAttribute('revision')) or '0'
697 697 commits = entries[0].getElementsByTagName('commit')
698 698 if commits:
699 699 lastrev = str(commits[0].getAttribute('revision')) or '0'
700 700 return (lastrev, rev)
701 701
702 702 def _wcrev(self):
703 703 return self._wcrevs()[0]
704 704
705 705 def _wcchanged(self):
706 706 """Return (changes, extchanges, missing) where changes is True
707 707 if the working directory was changed, extchanges is
708 708 True if any of these changes concern an external entry and missing
709 709 is True if any change is a missing entry.
710 710 """
711 711 output, err = self._svncommand(['status', '--xml'])
712 712 externals, changes, missing = [], [], []
713 713 doc = xml.dom.minidom.parseString(output)
714 714 for e in doc.getElementsByTagName('entry'):
715 715 s = e.getElementsByTagName('wc-status')
716 716 if not s:
717 717 continue
718 718 item = s[0].getAttribute('item')
719 719 props = s[0].getAttribute('props')
720 720 path = e.getAttribute('path')
721 721 if item == 'external':
722 722 externals.append(path)
723 723 elif item == 'missing':
724 724 missing.append(path)
725 725 if (item not in ('', 'normal', 'unversioned', 'external')
726 726 or props not in ('', 'none', 'normal')):
727 727 changes.append(path)
728 728 for path in changes:
729 729 for ext in externals:
730 730 if path == ext or path.startswith(ext + os.sep):
731 731 return True, True, bool(missing)
732 732 return bool(changes), False, bool(missing)
733 733
734 734 def dirty(self, ignoreupdate=False):
735 735 if not self._wcchanged()[0]:
736 736 if self._state[1] in self._wcrevs() or ignoreupdate:
737 737 return False
738 738 return True
739 739
740 740 def basestate(self):
741 741 lastrev, rev = self._wcrevs()
742 742 if lastrev != rev:
743 743 # Last committed rev is not the same than rev. We would
744 744 # like to take lastrev but we do not know if the subrepo
745 745 # URL exists at lastrev. Test it and fallback to rev it
746 746 # is not there.
747 747 try:
748 748 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
749 749 return lastrev
750 750 except error.Abort:
751 751 pass
752 752 return rev
753 753
754 754 def commit(self, text, user, date):
755 755 # user and date are out of our hands since svn is centralized
756 756 changed, extchanged, missing = self._wcchanged()
757 757 if not changed:
758 758 return self.basestate()
759 759 if extchanged:
760 760 # Do not try to commit externals
761 761 raise util.Abort(_('cannot commit svn externals'))
762 762 if missing:
763 763 # svn can commit with missing entries but aborting like hg
764 764 # seems a better approach.
765 765 raise util.Abort(_('cannot commit missing svn entries'))
766 766 commitinfo, err = self._svncommand(['commit', '-m', text])
767 767 self._ui.status(commitinfo)
768 768 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
769 769 if not newrev:
770 770 if not commitinfo.strip():
771 771 # Sometimes, our definition of "changed" differs from
772 772 # svn one. For instance, svn ignores missing files
773 773 # when committing. If there are only missing files, no
774 774 # commit is made, no output and no error code.
775 775 raise util.Abort(_('failed to commit svn changes'))
776 776 raise util.Abort(commitinfo.splitlines()[-1])
777 777 newrev = newrev.groups()[0]
778 778 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
779 779 return newrev
780 780
781 781 def remove(self):
782 782 if self.dirty():
783 783 self._ui.warn(_('not removing repo %s because '
784 784 'it has changes.\n' % self._path))
785 785 return
786 786 self._ui.note(_('removing subrepo %s\n') % self._path)
787 787
788 788 def onerror(function, path, excinfo):
789 789 if function is not os.remove:
790 790 raise
791 791 # read-only files cannot be unlinked under Windows
792 792 s = os.stat(path)
793 793 if (s.st_mode & stat.S_IWRITE) != 0:
794 794 raise
795 795 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
796 796 os.remove(path)
797 797
798 798 path = self._ctx._repo.wjoin(self._path)
799 799 shutil.rmtree(path, onerror=onerror)
800 800 try:
801 801 os.removedirs(os.path.dirname(path))
802 802 except OSError:
803 803 pass
804 804
805 805 def get(self, state, overwrite=False):
806 806 if overwrite:
807 807 self._svncommand(['revert', '--recursive'])
808 808 args = ['checkout']
809 809 if self._svnversion >= (1, 5):
810 810 args.append('--force')
811 811 # The revision must be specified at the end of the URL to properly
812 812 # update to a directory which has since been deleted and recreated.
813 813 args.append('%s@%s' % (state[0], state[1]))
814 814 status, err = self._svncommand(args, failok=True)
815 815 if not re.search('Checked out revision [0-9]+.', status):
816 816 if ('is already a working copy for a different URL' in err
817 817 and (self._wcchanged()[:2] == (False, False))):
818 818 # obstructed but clean working copy, so just blow it away.
819 819 self.remove()
820 820 self.get(state, overwrite=False)
821 821 return
822 822 raise util.Abort((status or err).splitlines()[-1])
823 823 self._ui.status(status)
824 824
825 825 def merge(self, state):
826 826 old = self._state[1]
827 827 new = state[1]
828 828 wcrev = self._wcrev()
829 829 if new != wcrev:
830 830 dirty = old == wcrev or self._wcchanged()[0]
831 831 if _updateprompt(self._ui, self, dirty, wcrev, new):
832 832 self.get(state, False)
833 833
834 834 def push(self, opts):
835 835 # push is a no-op for SVN
836 836 return True
837 837
838 838 def files(self):
839 839 output = self._svncommand(['list', '--recursive', '--xml'])[0]
840 840 doc = xml.dom.minidom.parseString(output)
841 841 paths = []
842 842 for e in doc.getElementsByTagName('entry'):
843 843 kind = str(e.getAttribute('kind'))
844 844 if kind != 'file':
845 845 continue
846 846 name = ''.join(c.data for c
847 847 in e.getElementsByTagName('name')[0].childNodes
848 848 if c.nodeType == c.TEXT_NODE)
849 849 paths.append(name.encode('utf-8'))
850 850 return paths
851 851
852 852 def filedata(self, name):
853 853 return self._svncommand(['cat'], name)[0]
854 854
855 855
856 856 class gitsubrepo(abstractsubrepo):
857 857 def __init__(self, ctx, path, state):
858 858 self._state = state
859 859 self._ctx = ctx
860 860 self._path = path
861 861 self._relpath = os.path.join(reporelpath(ctx._repo), path)
862 862 self._abspath = ctx._repo.wjoin(path)
863 863 self._subparent = ctx._repo
864 864 self._ui = ctx._repo.ui
865 865 self._ensuregit()
866 866
867 867 def _ensuregit(self):
868 868 try:
869 869 self._gitexecutable = 'git'
870 870 out, err = self._gitnodir(['--version'])
871 871 except OSError, e:
872 872 if e.errno != 2 or os.name != 'nt':
873 873 raise
874 874 self._gitexecutable = 'git.cmd'
875 875 out, err = self._gitnodir(['--version'])
876 876 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
877 877 if not m:
878 878 self._ui.warn(_('cannot retrieve git version'))
879 879 return
880 880 version = (int(m.group(1)), m.group(2), m.group(3))
881 881 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
882 882 # despite the docstring comment. For now, error on 1.4.0, warn on
883 883 # 1.5.0 but attempt to continue.
884 884 if version < (1, 5, 0):
885 885 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
886 886 elif version < (1, 6, 0):
887 887 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
888 888
889 889 def _gitcommand(self, commands, env=None, stream=False):
890 890 return self._gitdir(commands, env=env, stream=stream)[0]
891 891
892 892 def _gitdir(self, commands, env=None, stream=False):
893 893 return self._gitnodir(commands, env=env, stream=stream,
894 894 cwd=self._abspath)
895 895
896 896 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
897 897 """Calls the git command
898 898
899 899 The methods tries to call the git command. versions prior to 1.6.0
900 900 are not supported and very probably fail.
901 901 """
902 902 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
903 903 # unless ui.quiet is set, print git's stderr,
904 904 # which is mostly progress and useful info
905 905 errpipe = None
906 906 if self._ui.quiet:
907 907 errpipe = open(os.devnull, 'w')
908 908 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
909 909 cwd=cwd, env=env, close_fds=util.closefds,
910 910 stdout=subprocess.PIPE, stderr=errpipe)
911 911 if stream:
912 912 return p.stdout, None
913 913
914 914 retdata = p.stdout.read().strip()
915 915 # wait for the child to exit to avoid race condition.
916 916 p.wait()
917 917
918 918 if p.returncode != 0 and p.returncode != 1:
919 919 # there are certain error codes that are ok
920 920 command = commands[0]
921 921 if command in ('cat-file', 'symbolic-ref'):
922 922 return retdata, p.returncode
923 923 # for all others, abort
924 924 raise util.Abort('git %s error %d in %s' %
925 925 (command, p.returncode, self._relpath))
926 926
927 927 return retdata, p.returncode
928 928
929 929 def _gitmissing(self):
930 930 return not os.path.exists(os.path.join(self._abspath, '.git'))
931 931
932 932 def _gitstate(self):
933 933 return self._gitcommand(['rev-parse', 'HEAD'])
934 934
935 935 def _gitcurrentbranch(self):
936 936 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
937 937 if err:
938 938 current = None
939 939 return current
940 940
941 941 def _gitremote(self, remote):
942 942 out = self._gitcommand(['remote', 'show', '-n', remote])
943 943 line = out.split('\n')[1]
944 944 i = line.index('URL: ') + len('URL: ')
945 945 return line[i:]
946 946
947 947 def _githavelocally(self, revision):
948 948 out, code = self._gitdir(['cat-file', '-e', revision])
949 949 return code == 0
950 950
951 951 def _gitisancestor(self, r1, r2):
952 952 base = self._gitcommand(['merge-base', r1, r2])
953 953 return base == r1
954 954
955 955 def _gitisbare(self):
956 956 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
957 957
958 958 def _gitupdatestat(self):
959 959 """This must be run before git diff-index.
960 960 diff-index only looks at changes to file stat;
961 961 this command looks at file contents and updates the stat."""
962 962 self._gitcommand(['update-index', '-q', '--refresh'])
963 963
964 964 def _gitbranchmap(self):
965 965 '''returns 2 things:
966 966 a map from git branch to revision
967 967 a map from revision to branches'''
968 968 branch2rev = {}
969 969 rev2branch = {}
970 970
971 971 out = self._gitcommand(['for-each-ref', '--format',
972 972 '%(objectname) %(refname)'])
973 973 for line in out.split('\n'):
974 974 revision, ref = line.split(' ')
975 975 if (not ref.startswith('refs/heads/') and
976 976 not ref.startswith('refs/remotes/')):
977 977 continue
978 978 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
979 979 continue # ignore remote/HEAD redirects
980 980 branch2rev[ref] = revision
981 981 rev2branch.setdefault(revision, []).append(ref)
982 982 return branch2rev, rev2branch
983 983
984 984 def _gittracking(self, branches):
985 985 'return map of remote branch to local tracking branch'
986 986 # assumes no more than one local tracking branch for each remote
987 987 tracking = {}
988 988 for b in branches:
989 989 if b.startswith('refs/remotes/'):
990 990 continue
991 991 bname = b.split('/', 2)[2]
992 992 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
993 993 if remote:
994 994 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
995 995 tracking['refs/remotes/%s/%s' %
996 996 (remote, ref.split('/', 2)[2])] = b
997 997 return tracking
998 998
999 999 def _abssource(self, source):
1000 1000 if '://' not in source:
1001 1001 # recognize the scp syntax as an absolute source
1002 1002 colon = source.find(':')
1003 1003 if colon != -1 and '/' not in source[:colon]:
1004 1004 return source
1005 1005 self._subsource = source
1006 1006 return _abssource(self)
1007 1007
1008 1008 def _fetch(self, source, revision):
1009 1009 if self._gitmissing():
1010 1010 source = self._abssource(source)
1011 1011 self._ui.status(_('cloning subrepo %s from %s\n') %
1012 1012 (self._relpath, source))
1013 1013 self._gitnodir(['clone', source, self._abspath])
1014 1014 if self._githavelocally(revision):
1015 1015 return
1016 1016 self._ui.status(_('pulling subrepo %s from %s\n') %
1017 1017 (self._relpath, self._gitremote('origin')))
1018 1018 # try only origin: the originally cloned repo
1019 1019 self._gitcommand(['fetch'])
1020 1020 if not self._githavelocally(revision):
1021 1021 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1022 1022 (revision, self._relpath))
1023 1023
1024 1024 def dirty(self, ignoreupdate=False):
1025 1025 if self._gitmissing():
1026 1026 return self._state[1] != ''
1027 1027 if self._gitisbare():
1028 1028 return True
1029 1029 if not ignoreupdate and self._state[1] != self._gitstate():
1030 1030 # different version checked out
1031 1031 return True
1032 1032 # check for staged changes or modified files; ignore untracked files
1033 1033 self._gitupdatestat()
1034 1034 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1035 1035 return code == 1
1036 1036
1037 1037 def basestate(self):
1038 1038 return self._gitstate()
1039 1039
1040 1040 def get(self, state, overwrite=False):
1041 1041 source, revision, kind = state
1042 1042 if not revision:
1043 1043 self.remove()
1044 1044 return
1045 1045 self._fetch(source, revision)
1046 1046 # if the repo was set to be bare, unbare it
1047 1047 if self._gitisbare():
1048 1048 self._gitcommand(['config', 'core.bare', 'false'])
1049 1049 if self._gitstate() == revision:
1050 1050 self._gitcommand(['reset', '--hard', 'HEAD'])
1051 1051 return
1052 1052 elif self._gitstate() == revision:
1053 1053 if overwrite:
1054 1054 # first reset the index to unmark new files for commit, because
1055 1055 # reset --hard will otherwise throw away files added for commit,
1056 1056 # not just unmark them.
1057 1057 self._gitcommand(['reset', 'HEAD'])
1058 1058 self._gitcommand(['reset', '--hard', 'HEAD'])
1059 1059 return
1060 1060 branch2rev, rev2branch = self._gitbranchmap()
1061 1061
1062 1062 def checkout(args):
1063 1063 cmd = ['checkout']
1064 1064 if overwrite:
1065 1065 # first reset the index to unmark new files for commit, because
1066 1066 # the -f option will otherwise throw away files added for
1067 1067 # commit, not just unmark them.
1068 1068 self._gitcommand(['reset', 'HEAD'])
1069 1069 cmd.append('-f')
1070 1070 self._gitcommand(cmd + args)
1071 1071
1072 1072 def rawcheckout():
1073 1073 # no branch to checkout, check it out with no branch
1074 1074 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1075 1075 self._relpath)
1076 1076 self._ui.warn(_('check out a git branch if you intend '
1077 1077 'to make changes\n'))
1078 1078 checkout(['-q', revision])
1079 1079
1080 1080 if revision not in rev2branch:
1081 1081 rawcheckout()
1082 1082 return
1083 1083 branches = rev2branch[revision]
1084 1084 firstlocalbranch = None
1085 1085 for b in branches:
1086 1086 if b == 'refs/heads/master':
1087 1087 # master trumps all other branches
1088 1088 checkout(['refs/heads/master'])
1089 1089 return
1090 1090 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1091 1091 firstlocalbranch = b
1092 1092 if firstlocalbranch:
1093 1093 checkout([firstlocalbranch])
1094 1094 return
1095 1095
1096 1096 tracking = self._gittracking(branch2rev.keys())
1097 1097 # choose a remote branch already tracked if possible
1098 1098 remote = branches[0]
1099 1099 if remote not in tracking:
1100 1100 for b in branches:
1101 1101 if b in tracking:
1102 1102 remote = b
1103 1103 break
1104 1104
1105 1105 if remote not in tracking:
1106 1106 # create a new local tracking branch
1107 1107 local = remote.split('/', 2)[2]
1108 1108 checkout(['-b', local, remote])
1109 1109 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1110 1110 # When updating to a tracked remote branch,
1111 1111 # if the local tracking branch is downstream of it,
1112 1112 # a normal `git pull` would have performed a "fast-forward merge"
1113 1113 # which is equivalent to updating the local branch to the remote.
1114 1114 # Since we are only looking at branching at update, we need to
1115 1115 # detect this situation and perform this action lazily.
1116 1116 if tracking[remote] != self._gitcurrentbranch():
1117 1117 checkout([tracking[remote]])
1118 1118 self._gitcommand(['merge', '--ff', remote])
1119 1119 else:
1120 1120 # a real merge would be required, just checkout the revision
1121 1121 rawcheckout()
1122 1122
1123 1123 def commit(self, text, user, date):
1124 1124 if self._gitmissing():
1125 1125 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1126 1126 cmd = ['commit', '-a', '-m', text]
1127 1127 env = os.environ.copy()
1128 1128 if user:
1129 1129 cmd += ['--author', user]
1130 1130 if date:
1131 1131 # git's date parser silently ignores when seconds < 1e9
1132 1132 # convert to ISO8601
1133 1133 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1134 1134 '%Y-%m-%dT%H:%M:%S %1%2')
1135 1135 self._gitcommand(cmd, env=env)
1136 1136 # make sure commit works otherwise HEAD might not exist under certain
1137 1137 # circumstances
1138 1138 return self._gitstate()
1139 1139
1140 1140 def merge(self, state):
1141 1141 source, revision, kind = state
1142 1142 self._fetch(source, revision)
1143 1143 base = self._gitcommand(['merge-base', revision, self._state[1]])
1144 1144 self._gitupdatestat()
1145 1145 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1146 1146
1147 1147 def mergefunc():
1148 1148 if base == revision:
1149 1149 self.get(state) # fast forward merge
1150 1150 elif base != self._state[1]:
1151 1151 self._gitcommand(['merge', '--no-commit', revision])
1152 1152
1153 1153 if self.dirty():
1154 1154 if self._gitstate() != revision:
1155 1155 dirty = self._gitstate() == self._state[1] or code != 0
1156 1156 if _updateprompt(self._ui, self, dirty,
1157 1157 self._state[1][:7], revision[:7]):
1158 1158 mergefunc()
1159 1159 else:
1160 1160 mergefunc()
1161 1161
1162 1162 def push(self, opts):
1163 1163 force = opts.get('force')
1164 1164
1165 1165 if not self._state[1]:
1166 1166 return True
1167 1167 if self._gitmissing():
1168 1168 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1169 1169 # if a branch in origin contains the revision, nothing to do
1170 1170 branch2rev, rev2branch = self._gitbranchmap()
1171 1171 if self._state[1] in rev2branch:
1172 1172 for b in rev2branch[self._state[1]]:
1173 1173 if b.startswith('refs/remotes/origin/'):
1174 1174 return True
1175 1175 for b, revision in branch2rev.iteritems():
1176 1176 if b.startswith('refs/remotes/origin/'):
1177 1177 if self._gitisancestor(self._state[1], revision):
1178 1178 return True
1179 1179 # otherwise, try to push the currently checked out branch
1180 1180 cmd = ['push']
1181 1181 if force:
1182 1182 cmd.append('--force')
1183 1183
1184 1184 current = self._gitcurrentbranch()
1185 1185 if current:
1186 1186 # determine if the current branch is even useful
1187 1187 if not self._gitisancestor(self._state[1], current):
1188 1188 self._ui.warn(_('unrelated git branch checked out '
1189 1189 'in subrepo %s\n') % self._relpath)
1190 1190 return False
1191 1191 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1192 1192 (current.split('/', 2)[2], self._relpath))
1193 1193 self._gitcommand(cmd + ['origin', current])
1194 1194 return True
1195 1195 else:
1196 1196 self._ui.warn(_('no branch checked out in subrepo %s\n'
1197 1197 'cannot push revision %s\n') %
1198 1198 (self._relpath, self._state[1]))
1199 1199 return False
1200 1200
1201 1201 def remove(self):
1202 1202 if self._gitmissing():
1203 1203 return
1204 1204 if self.dirty():
1205 1205 self._ui.warn(_('not removing repo %s because '
1206 1206 'it has changes.\n') % self._relpath)
1207 1207 return
1208 1208 # we can't fully delete the repository as it may contain
1209 1209 # local-only history
1210 1210 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1211 1211 self._gitcommand(['config', 'core.bare', 'true'])
1212 1212 for f in os.listdir(self._abspath):
1213 1213 if f == '.git':
1214 1214 continue
1215 1215 path = os.path.join(self._abspath, f)
1216 1216 if os.path.isdir(path) and not os.path.islink(path):
1217 1217 shutil.rmtree(path)
1218 1218 else:
1219 1219 os.remove(path)
1220 1220
1221 1221 def archive(self, ui, archiver, prefix, match=None):
1222 1222 source, revision = self._state
1223 1223 if not revision:
1224 1224 return
1225 1225 self._fetch(source, revision)
1226 1226
1227 1227 # Parse git's native archive command.
1228 1228 # This should be much faster than manually traversing the trees
1229 1229 # and objects with many subprocess calls.
1230 1230 tarstream = self._gitcommand(['archive', revision], stream=True)
1231 1231 tar = tarfile.open(fileobj=tarstream, mode='r|')
1232 1232 relpath = subrelpath(self)
1233 1233 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1234 1234 for i, info in enumerate(tar):
1235 1235 if info.isdir():
1236 1236 continue
1237 1237 if match and not match(info.name):
1238 1238 continue
1239 1239 if info.issym():
1240 1240 data = info.linkname
1241 1241 else:
1242 1242 data = tar.extractfile(info).read()
1243 1243 archiver.addfile(os.path.join(prefix, self._path, info.name),
1244 1244 info.mode, info.issym(), data)
1245 1245 ui.progress(_('archiving (%s)') % relpath, i + 1,
1246 1246 unit=_('files'))
1247 1247 ui.progress(_('archiving (%s)') % relpath, None)
1248 1248
1249 1249
1250 1250 def status(self, rev2, **opts):
1251 1251 rev1 = self._state[1]
1252 1252 if self._gitmissing() or not rev1:
1253 1253 # if the repo is missing, return no results
1254 1254 return [], [], [], [], [], [], []
1255 1255 modified, added, removed = [], [], []
1256 1256 self._gitupdatestat()
1257 1257 if rev2:
1258 1258 command = ['diff-tree', rev1, rev2]
1259 1259 else:
1260 1260 command = ['diff-index', rev1]
1261 1261 out = self._gitcommand(command)
1262 1262 for line in out.split('\n'):
1263 1263 tab = line.find('\t')
1264 1264 if tab == -1:
1265 1265 continue
1266 1266 status, f = line[tab - 1], line[tab + 1:]
1267 1267 if status == 'M':
1268 1268 modified.append(f)
1269 1269 elif status == 'A':
1270 1270 added.append(f)
1271 1271 elif status == 'D':
1272 1272 removed.append(f)
1273 1273
1274 1274 deleted = unknown = ignored = clean = []
1275 1275 return modified, added, removed, deleted, unknown, ignored, clean
1276 1276
1277 1277 types = {
1278 1278 'hg': hgsubrepo,
1279 1279 'svn': svnsubrepo,
1280 1280 'git': gitsubrepo,
1281 1281 }
@@ -1,1023 +1,1045
1 1 Let commit recurse into subrepos by default to match pre-2.0 behavior:
2 2
3 3 $ echo "[ui]" >> $HGRCPATH
4 4 $ echo "commitsubrepos = Yes" >> $HGRCPATH
5 5
6 6 $ hg init t
7 7 $ cd t
8 8
9 9 first revision, no sub
10 10
11 11 $ echo a > a
12 12 $ hg ci -Am0
13 13 adding a
14 14
15 15 add first sub
16 16
17 17 $ echo s = s > .hgsub
18 18 $ hg add .hgsub
19 19 $ hg init s
20 20 $ echo a > s/a
21 21
22 22 Issue2232: committing a subrepo without .hgsub
23 23
24 24 $ hg ci -mbad s
25 25 abort: can't commit subrepos without .hgsub
26 26 [255]
27 27
28 28 $ hg -R s ci -Ams0
29 29 adding a
30 30 $ hg sum
31 31 parent: 0:f7b1eb17ad24 tip
32 32 0
33 33 branch: default
34 34 commit: 1 added, 1 subrepos
35 35 update: (current)
36 36 $ hg ci -m1
37 37
38 38 Revert subrepo and test subrepo fileset keyword:
39 39
40 40 $ echo b > s/a
41 41 $ hg revert "set:subrepo('glob:s*')"
42 42 reverting subrepo s
43 43 reverting s/a (glob)
44 44 $ rm s/a.orig
45 45
46 46 Revert subrepo with no backup. The "reverting s/a" line is gone since
47 47 we're really running 'hg update' in the subrepo:
48 48
49 49 $ echo b > s/a
50 50 $ hg revert --no-backup s
51 51 reverting subrepo s
52 52
53 53 Issue2022: update -C
54 54
55 55 $ echo b > s/a
56 56 $ hg sum
57 57 parent: 1:7cf8cfea66e4 tip
58 58 1
59 59 branch: default
60 60 commit: 1 subrepos
61 61 update: (current)
62 62 $ hg co -C 1
63 63 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
64 64 $ hg sum
65 65 parent: 1:7cf8cfea66e4 tip
66 66 1
67 67 branch: default
68 68 commit: (clean)
69 69 update: (current)
70 70
71 71 commands that require a clean repo should respect subrepos
72 72
73 73 $ echo b >> s/a
74 74 $ hg backout tip
75 75 abort: uncommitted changes in subrepo s
76 76 [255]
77 77 $ hg revert -C -R s s/a
78 78
79 79 add sub sub
80 80
81 81 $ echo ss = ss > s/.hgsub
82 82 $ hg init s/ss
83 83 $ echo a > s/ss/a
84 84 $ hg -R s add s/.hgsub
85 85 $ hg -R s/ss add s/ss/a
86 86 $ hg sum
87 87 parent: 1:7cf8cfea66e4 tip
88 88 1
89 89 branch: default
90 90 commit: 1 subrepos
91 91 update: (current)
92 92 $ hg ci -m2
93 93 committing subrepository s
94 94 committing subrepository s/ss (glob)
95 95 $ hg sum
96 96 parent: 2:df30734270ae tip
97 97 2
98 98 branch: default
99 99 commit: (clean)
100 100 update: (current)
101 101
102 102 bump sub rev (and check it is ignored by ui.commitsubrepos)
103 103
104 104 $ echo b > s/a
105 105 $ hg -R s ci -ms1
106 106 $ hg --config ui.commitsubrepos=no ci -m3
107 107
108 108 leave sub dirty (and check ui.commitsubrepos=no aborts the commit)
109 109
110 110 $ echo c > s/a
111 111 $ hg --config ui.commitsubrepos=no ci -m4
112 112 abort: uncommitted changes in subrepo s
113 113 (use --subrepos for recursive commit)
114 114 [255]
115 115 $ hg id
116 116 f6affe3fbfaa+ tip
117 117 $ hg -R s ci -mc
118 118 $ hg id
119 119 f6affe3fbfaa+ tip
120 120 $ echo d > s/a
121 121 $ hg ci -m4
122 122 committing subrepository s
123 123 $ hg tip -R s
124 124 changeset: 4:02dcf1d70411
125 125 tag: tip
126 126 user: test
127 127 date: Thu Jan 01 00:00:00 1970 +0000
128 128 summary: 4
129 129
130 130
131 131 check caching
132 132
133 133 $ hg co 0
134 134 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
135 135 $ hg debugsub
136 136
137 137 restore
138 138
139 139 $ hg co
140 140 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
141 141 $ hg debugsub
142 142 path s
143 143 source s
144 144 revision 02dcf1d704118aee3ee306ccfa1910850d5b05ef
145 145
146 146 new branch for merge tests
147 147
148 148 $ hg co 1
149 149 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
150 150 $ echo t = t >> .hgsub
151 151 $ hg init t
152 152 $ echo t > t/t
153 153 $ hg -R t add t
154 154 adding t/t (glob)
155 155
156 156 5
157 157
158 158 $ hg ci -m5 # add sub
159 159 committing subrepository t
160 160 created new head
161 161 $ echo t2 > t/t
162 162
163 163 6
164 164
165 165 $ hg st -R s
166 166 $ hg ci -m6 # change sub
167 167 committing subrepository t
168 168 $ hg debugsub
169 169 path s
170 170 source s
171 171 revision e4ece1bf43360ddc8f6a96432201a37b7cd27ae4
172 172 path t
173 173 source t
174 174 revision 6747d179aa9a688023c4b0cad32e4c92bb7f34ad
175 175 $ echo t3 > t/t
176 176
177 177 7
178 178
179 179 $ hg ci -m7 # change sub again for conflict test
180 180 committing subrepository t
181 181 $ hg rm .hgsub
182 182
183 183 8
184 184
185 185 $ hg ci -m8 # remove sub
186 186
187 187 merge tests
188 188
189 189 $ hg co -C 3
190 190 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
191 191 $ hg merge 5 # test adding
192 192 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
193 193 (branch merge, don't forget to commit)
194 194 $ hg debugsub
195 195 path s
196 196 source s
197 197 revision fc627a69481fcbe5f1135069e8a3881c023e4cf5
198 198 path t
199 199 source t
200 200 revision 60ca1237c19474e7a3978b0dc1ca4e6f36d51382
201 201 $ hg ci -m9
202 202 created new head
203 203 $ hg merge 6 --debug # test change
204 204 searching for copies back to rev 2
205 205 resolving manifests
206 206 overwrite: False, partial: False
207 207 ancestor: 1f14a2e2d3ec, local: f0d2028bf86d+, remote: 1831e14459c4
208 208 .hgsubstate: versions differ -> m
209 209 updating: .hgsubstate 1/1 files (100.00%)
210 210 subrepo merge f0d2028bf86d+ 1831e14459c4 1f14a2e2d3ec
211 211 subrepo t: other changed, get t:6747d179aa9a688023c4b0cad32e4c92bb7f34ad:hg
212 212 getting subrepo t
213 searching for copies back to rev 1
213 214 resolving manifests
214 overwrite: True, partial: False
215 ancestor: 60ca1237c194+, local: 60ca1237c194+, remote: 6747d179aa9a
215 overwrite: False, partial: False
216 ancestor: 60ca1237c194, local: 60ca1237c194+, remote: 6747d179aa9a
216 217 t: remote is newer -> g
217 218 updating: t 1/1 files (100.00%)
218 219 getting t
219 220 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
220 221 (branch merge, don't forget to commit)
221 222 $ hg debugsub
222 223 path s
223 224 source s
224 225 revision fc627a69481fcbe5f1135069e8a3881c023e4cf5
225 226 path t
226 227 source t
227 228 revision 6747d179aa9a688023c4b0cad32e4c92bb7f34ad
228 229 $ echo conflict > t/t
229 230 $ hg ci -m10
230 231 committing subrepository t
231 232 $ HGMERGE=internal:merge hg merge --debug 7 # test conflict
232 233 searching for copies back to rev 2
233 234 resolving manifests
234 235 overwrite: False, partial: False
235 236 ancestor: 1831e14459c4, local: e45c8b14af55+, remote: f94576341bcf
236 237 .hgsubstate: versions differ -> m
237 238 updating: .hgsubstate 1/1 files (100.00%)
238 239 subrepo merge e45c8b14af55+ f94576341bcf 1831e14459c4
239 240 subrepo t: both sides changed, merge with t:7af322bc1198a32402fe903e0b7ebcfc5c9bf8f4:hg
240 241 merging subrepo t
241 242 searching for copies back to rev 2
242 243 resolving manifests
243 244 overwrite: False, partial: False
244 245 ancestor: 6747d179aa9a, local: 20a0db6fbf6c+, remote: 7af322bc1198
245 246 t: versions differ -> m
246 247 preserving t for resolve of t
247 248 updating: t 1/1 files (100.00%)
248 249 picked tool 'internal:merge' for t (binary False symlink False)
249 250 merging t
250 251 my t@20a0db6fbf6c+ other t@7af322bc1198 ancestor t@6747d179aa9a
251 252 warning: conflicts during merge.
252 253 merging t incomplete! (edit conflicts, then use 'hg resolve --mark')
253 254 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
254 255 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
255 256 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
256 257 (branch merge, don't forget to commit)
257 258
258 259 should conflict
259 260
260 261 $ cat t/t
261 262 <<<<<<< local
262 263 conflict
263 264 =======
264 265 t3
265 266 >>>>>>> other
266 267
267 268 clone
268 269
269 270 $ cd ..
270 271 $ hg clone t tc
271 272 updating to branch default
272 273 cloning subrepo s from $TESTTMP/t/s (glob)
273 274 cloning subrepo s/ss from $TESTTMP/t/s/ss (glob)
274 275 cloning subrepo t from $TESTTMP/t/t (glob)
275 276 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
276 277 $ cd tc
277 278 $ hg debugsub
278 279 path s
279 280 source s
280 281 revision fc627a69481fcbe5f1135069e8a3881c023e4cf5
281 282 path t
282 283 source t
283 284 revision 20a0db6fbf6c3d2836e6519a642ae929bfc67c0e
284 285
285 286 push
286 287
287 288 $ echo bah > t/t
288 289 $ hg ci -m11
289 290 committing subrepository t
290 291 $ hg push
291 292 pushing to $TESTTMP/t (glob)
292 293 pushing subrepo s/ss to $TESTTMP/t/s/ss (glob)
293 294 searching for changes
294 295 no changes found
295 296 pushing subrepo s to $TESTTMP/t/s (glob)
296 297 searching for changes
297 298 no changes found
298 299 pushing subrepo t to $TESTTMP/t/t (glob)
299 300 searching for changes
300 301 adding changesets
301 302 adding manifests
302 303 adding file changes
303 304 added 1 changesets with 1 changes to 1 files
304 305 searching for changes
305 306 adding changesets
306 307 adding manifests
307 308 adding file changes
308 309 added 1 changesets with 1 changes to 1 files
309 310
310 311 push -f
311 312
312 313 $ echo bah > s/a
313 314 $ hg ci -m12
314 315 committing subrepository s
315 316 $ hg push
316 317 pushing to $TESTTMP/t (glob)
317 318 pushing subrepo s/ss to $TESTTMP/t/s/ss (glob)
318 319 searching for changes
319 320 no changes found
320 321 pushing subrepo s to $TESTTMP/t/s (glob)
321 322 searching for changes
322 323 abort: push creates new remote head 12a213df6fa9!
323 324 (did you forget to merge? use push -f to force)
324 325 [255]
325 326 $ hg push -f
326 327 pushing to $TESTTMP/t (glob)
327 328 pushing subrepo s/ss to $TESTTMP/t/s/ss (glob)
328 329 searching for changes
329 330 no changes found
330 331 pushing subrepo s to $TESTTMP/t/s (glob)
331 332 searching for changes
332 333 adding changesets
333 334 adding manifests
334 335 adding file changes
335 336 added 1 changesets with 1 changes to 1 files (+1 heads)
336 337 pushing subrepo t to $TESTTMP/t/t (glob)
337 338 searching for changes
338 339 no changes found
339 340 searching for changes
340 341 adding changesets
341 342 adding manifests
342 343 adding file changes
343 344 added 1 changesets with 1 changes to 1 files
344 345
345 346 update
346 347
347 348 $ cd ../t
348 349 $ hg up -C # discard our earlier merge
349 350 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
350 351 $ echo blah > t/t
351 352 $ hg ci -m13
352 353 committing subrepository t
353 354
354 355 pull
355 356
356 357 $ cd ../tc
357 358 $ hg pull
358 359 pulling from $TESTTMP/t (glob)
359 360 searching for changes
360 361 adding changesets
361 362 adding manifests
362 363 adding file changes
363 364 added 1 changesets with 1 changes to 1 files
364 365 (run 'hg update' to get a working copy)
365 366
366 367 should pull t
367 368
368 369 $ hg up
369 370 pulling subrepo t from $TESTTMP/t/t (glob)
370 371 searching for changes
371 372 adding changesets
372 373 adding manifests
373 374 adding file changes
374 375 added 1 changesets with 1 changes to 1 files
375 376 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
376 377 $ cat t/t
377 378 blah
378 379
379 380 bogus subrepo path aborts
380 381
381 382 $ echo 'bogus=[boguspath' >> .hgsub
382 383 $ hg ci -m 'bogus subrepo path'
383 384 abort: missing ] in subrepo source
384 385 [255]
385 386
386 387 Issue1986: merge aborts when trying to merge a subrepo that
387 388 shouldn't need merging
388 389
389 390 # subrepo layout
390 391 #
391 392 # o 5 br
392 393 # /|
393 394 # o | 4 default
394 395 # | |
395 396 # | o 3 br
396 397 # |/|
397 398 # o | 2 default
398 399 # | |
399 400 # | o 1 br
400 401 # |/
401 402 # o 0 default
402 403
403 404 $ cd ..
404 405 $ rm -rf sub
405 406 $ hg init main
406 407 $ cd main
407 408 $ hg init s
408 409 $ cd s
409 410 $ echo a > a
410 411 $ hg ci -Am1
411 412 adding a
412 413 $ hg branch br
413 414 marked working directory as branch br
414 415 (branches are permanent and global, did you want a bookmark?)
415 416 $ echo a >> a
416 417 $ hg ci -m1
417 418 $ hg up default
418 419 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
419 420 $ echo b > b
420 421 $ hg ci -Am1
421 422 adding b
422 423 $ hg up br
423 424 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
424 425 $ hg merge tip
425 426 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
426 427 (branch merge, don't forget to commit)
427 428 $ hg ci -m1
428 429 $ hg up 2
429 430 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
430 431 $ echo c > c
431 432 $ hg ci -Am1
432 433 adding c
433 434 $ hg up 3
434 435 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
435 436 $ hg merge 4
436 437 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
437 438 (branch merge, don't forget to commit)
438 439 $ hg ci -m1
439 440
440 441 # main repo layout:
441 442 #
442 443 # * <-- try to merge default into br again
443 444 # .`|
444 445 # . o 5 br --> substate = 5
445 446 # . |
446 447 # o | 4 default --> substate = 4
447 448 # | |
448 449 # | o 3 br --> substate = 2
449 450 # |/|
450 451 # o | 2 default --> substate = 2
451 452 # | |
452 453 # | o 1 br --> substate = 3
453 454 # |/
454 455 # o 0 default --> substate = 2
455 456
456 457 $ cd ..
457 458 $ echo 's = s' > .hgsub
458 459 $ hg -R s up 2
459 460 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
460 461 $ hg ci -Am1
461 462 adding .hgsub
462 463 $ hg branch br
463 464 marked working directory as branch br
464 465 (branches are permanent and global, did you want a bookmark?)
465 466 $ echo b > b
466 467 $ hg -R s up 3
467 468 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
468 469 $ hg ci -Am1
469 470 adding b
470 471 $ hg up default
471 472 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
472 473 $ echo c > c
473 474 $ hg ci -Am1
474 475 adding c
475 476 $ hg up 1
476 477 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
477 478 $ hg merge 2
478 479 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
479 480 (branch merge, don't forget to commit)
480 481 $ hg ci -m1
481 482 $ hg up 2
482 483 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
483 484 $ hg -R s up 4
484 485 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
485 486 $ echo d > d
486 487 $ hg ci -Am1
487 488 adding d
488 489 $ hg up 3
489 490 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
490 491 $ hg -R s up 5
491 492 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
492 493 $ echo e > e
493 494 $ hg ci -Am1
494 495 adding e
495 496
496 497 $ hg up 5
497 498 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
498 499 $ hg merge 4 # try to merge default into br again
499 500 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
500 501 (branch merge, don't forget to commit)
501 502 $ cd ..
502 503
503 504 test subrepo delete from .hgsubstate
504 505
505 506 $ hg init testdelete
506 507 $ mkdir testdelete/nested testdelete/nested2
507 508 $ hg init testdelete/nested
508 509 $ hg init testdelete/nested2
509 510 $ echo test > testdelete/nested/foo
510 511 $ echo test > testdelete/nested2/foo
511 512 $ hg -R testdelete/nested add
512 513 adding testdelete/nested/foo (glob)
513 514 $ hg -R testdelete/nested2 add
514 515 adding testdelete/nested2/foo (glob)
515 516 $ hg -R testdelete/nested ci -m test
516 517 $ hg -R testdelete/nested2 ci -m test
517 518 $ echo nested = nested > testdelete/.hgsub
518 519 $ echo nested2 = nested2 >> testdelete/.hgsub
519 520 $ hg -R testdelete add
520 521 adding testdelete/.hgsub (glob)
521 522 $ hg -R testdelete ci -m "nested 1 & 2 added"
522 523 $ echo nested = nested > testdelete/.hgsub
523 524 $ hg -R testdelete ci -m "nested 2 deleted"
524 525 $ cat testdelete/.hgsubstate
525 526 bdf5c9a3103743d900b12ae0db3ffdcfd7b0d878 nested
526 527 $ hg -R testdelete remove testdelete/.hgsub
527 528 $ hg -R testdelete ci -m ".hgsub deleted"
528 529 $ cat testdelete/.hgsubstate
529 530 bdf5c9a3103743d900b12ae0db3ffdcfd7b0d878 nested
530 531
531 532 test repository cloning
532 533
533 534 $ mkdir mercurial mercurial2
534 535 $ hg init nested_absolute
535 536 $ echo test > nested_absolute/foo
536 537 $ hg -R nested_absolute add
537 538 adding nested_absolute/foo (glob)
538 539 $ hg -R nested_absolute ci -mtest
539 540 $ cd mercurial
540 541 $ hg init nested_relative
541 542 $ echo test2 > nested_relative/foo2
542 543 $ hg -R nested_relative add
543 544 adding nested_relative/foo2 (glob)
544 545 $ hg -R nested_relative ci -mtest2
545 546 $ hg init main
546 547 $ echo "nested_relative = ../nested_relative" > main/.hgsub
547 548 $ echo "nested_absolute = `pwd`/nested_absolute" >> main/.hgsub
548 549 $ hg -R main add
549 550 adding main/.hgsub (glob)
550 551 $ hg -R main ci -m "add subrepos"
551 552 $ cd ..
552 553 $ hg clone mercurial/main mercurial2/main
553 554 updating to branch default
554 555 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
555 556 $ cat mercurial2/main/nested_absolute/.hg/hgrc \
556 557 > mercurial2/main/nested_relative/.hg/hgrc
557 558 [paths]
558 559 default = $TESTTMP/mercurial/nested_absolute
559 560 [paths]
560 561 default = $TESTTMP/mercurial/nested_relative
561 562 $ rm -rf mercurial mercurial2
562 563
563 564 Issue1977: multirepo push should fail if subrepo push fails
564 565
565 566 $ hg init repo
566 567 $ hg init repo/s
567 568 $ echo a > repo/s/a
568 569 $ hg -R repo/s ci -Am0
569 570 adding a
570 571 $ echo s = s > repo/.hgsub
571 572 $ hg -R repo ci -Am1
572 573 adding .hgsub
573 574 $ hg clone repo repo2
574 575 updating to branch default
575 576 cloning subrepo s from $TESTTMP/repo/s (glob)
576 577 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
577 578 $ hg -q -R repo2 pull -u
578 579 $ echo 1 > repo2/s/a
579 580 $ hg -R repo2/s ci -m2
580 581 $ hg -q -R repo2/s push
581 582 $ hg -R repo2/s up -C 0
582 583 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
583 $ echo 2 > repo2/s/a
584 $ hg -R repo2/s ci -m3
584 $ echo 2 > repo2/s/b
585 $ hg -R repo2/s ci -m3 -A
586 adding b
585 587 created new head
586 588 $ hg -R repo2 ci -m3
587 589 $ hg -q -R repo2 push
588 abort: push creates new remote head 9d66565e64e1!
590 abort: push creates new remote head cc505f09a8b2!
589 591 (did you forget to merge? use push -f to force)
590 592 [255]
591 593 $ hg -R repo update
592 594 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
595
596 test if untracked file is not overwritten
597
598 $ echo issue3276_ok > repo/s/b
599 $ hg -R repo2 push -f -q
600 $ hg -R repo update
601 b: untracked file differs
602 abort: untracked files in working directory differ from files in requested revision
603 [255]
604
605 $ cat repo/s/b
606 issue3276_ok
607 $ rm repo/s/b
608 $ hg -R repo revert --all
609 reverting repo/.hgsubstate
610 reverting subrepo s
611 $ hg -R repo update
612 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
613 $ cat repo/s/b
614 2
593 615 $ rm -rf repo2 repo
594 616
595 617
596 618 Issue1852 subrepos with relative paths always push/pull relative to default
597 619
598 620 Prepare a repo with subrepo
599 621
600 622 $ hg init issue1852a
601 623 $ cd issue1852a
602 624 $ hg init sub/repo
603 625 $ echo test > sub/repo/foo
604 626 $ hg -R sub/repo add sub/repo/foo
605 627 $ echo sub/repo = sub/repo > .hgsub
606 628 $ hg add .hgsub
607 629 $ hg ci -mtest
608 630 committing subrepository sub/repo (glob)
609 631 $ echo test >> sub/repo/foo
610 632 $ hg ci -mtest
611 633 committing subrepository sub/repo (glob)
612 634 $ cd ..
613 635
614 636 Create repo without default path, pull top repo, and see what happens on update
615 637
616 638 $ hg init issue1852b
617 639 $ hg -R issue1852b pull issue1852a
618 640 pulling from issue1852a
619 641 requesting all changes
620 642 adding changesets
621 643 adding manifests
622 644 adding file changes
623 645 added 2 changesets with 3 changes to 2 files
624 646 (run 'hg update' to get a working copy)
625 647 $ hg -R issue1852b update
626 648 abort: default path for subrepository sub/repo not found (glob)
627 649 [255]
628 650
629 651 Pull -u now doesn't help
630 652
631 653 $ hg -R issue1852b pull -u issue1852a
632 654 pulling from issue1852a
633 655 searching for changes
634 656 no changes found
635 657
636 658 Try the same, but with pull -u
637 659
638 660 $ hg init issue1852c
639 661 $ hg -R issue1852c pull -r0 -u issue1852a
640 662 pulling from issue1852a
641 663 adding changesets
642 664 adding manifests
643 665 adding file changes
644 666 added 1 changesets with 2 changes to 2 files
645 667 cloning subrepo sub/repo from issue1852a/sub/repo (glob)
646 668 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
647 669
648 670 Try to push from the other side
649 671
650 672 $ hg -R issue1852a push `pwd`/issue1852c
651 673 pushing to $TESTTMP/issue1852c
652 674 pushing subrepo sub/repo to $TESTTMP/issue1852c/sub/repo (glob)
653 675 searching for changes
654 676 no changes found
655 677 searching for changes
656 678 adding changesets
657 679 adding manifests
658 680 adding file changes
659 681 added 1 changesets with 1 changes to 1 files
660 682
661 683 Incoming and outgoing should not use the default path:
662 684
663 685 $ hg clone -q issue1852a issue1852d
664 686 $ hg -R issue1852d outgoing --subrepos issue1852c
665 687 comparing with issue1852c
666 688 searching for changes
667 689 no changes found
668 690 comparing with issue1852c/sub/repo
669 691 searching for changes
670 692 no changes found
671 693 [1]
672 694 $ hg -R issue1852d incoming --subrepos issue1852c
673 695 comparing with issue1852c
674 696 searching for changes
675 697 no changes found
676 698 comparing with issue1852c/sub/repo
677 699 searching for changes
678 700 no changes found
679 701 [1]
680 702
681 703 Check status of files when none of them belong to the first
682 704 subrepository:
683 705
684 706 $ hg init subrepo-status
685 707 $ cd subrepo-status
686 708 $ hg init subrepo-1
687 709 $ hg init subrepo-2
688 710 $ cd subrepo-2
689 711 $ touch file
690 712 $ hg add file
691 713 $ cd ..
692 714 $ echo subrepo-1 = subrepo-1 > .hgsub
693 715 $ echo subrepo-2 = subrepo-2 >> .hgsub
694 716 $ hg add .hgsub
695 717 $ hg ci -m 'Added subrepos'
696 718 committing subrepository subrepo-2
697 719 $ hg st subrepo-2/file
698 720
699 721 Check hg update --clean
700 722 $ cd $TESTTMP/t
701 723 $ rm -r t/t.orig
702 724 $ hg status -S --all
703 725 C .hgsub
704 726 C .hgsubstate
705 727 C a
706 728 C s/.hgsub
707 729 C s/.hgsubstate
708 730 C s/a
709 731 C s/ss/a
710 732 C t/t
711 733 $ echo c1 > s/a
712 734 $ cd s
713 735 $ echo c1 > b
714 736 $ echo c1 > c
715 737 $ hg add b
716 738 $ cd ..
717 739 $ hg status -S
718 740 M s/a
719 741 A s/b
720 742 ? s/c
721 743 $ hg update -C
722 744 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
723 745 $ hg status -S
724 746 ? s/b
725 747 ? s/c
726 748
727 749 Sticky subrepositories, no changes
728 750 $ cd $TESTTMP/t
729 751 $ hg id
730 752 925c17564ef8 tip
731 753 $ hg -R s id
732 754 12a213df6fa9 tip
733 755 $ hg -R t id
734 756 52c0adc0515a tip
735 757 $ hg update 11
736 758 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
737 759 $ hg id
738 760 365661e5936a
739 761 $ hg -R s id
740 762 fc627a69481f
741 763 $ hg -R t id
742 764 e95bcfa18a35
743 765
744 766 Sticky subrepositorys, file changes
745 767 $ touch s/f1
746 768 $ touch t/f1
747 769 $ hg add -S s/f1
748 770 $ hg add -S t/f1
749 771 $ hg id
750 772 365661e5936a+
751 773 $ hg -R s id
752 774 fc627a69481f+
753 775 $ hg -R t id
754 776 e95bcfa18a35+
755 777 $ hg update tip
756 778 subrepository sources for s differ
757 779 use (l)ocal source (fc627a69481f) or (r)emote source (12a213df6fa9)?
758 780 l
759 781 subrepository sources for t differ
760 782 use (l)ocal source (e95bcfa18a35) or (r)emote source (52c0adc0515a)?
761 783 l
762 784 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
763 785 $ hg id
764 786 925c17564ef8+ tip
765 787 $ hg -R s id
766 788 fc627a69481f+
767 789 $ hg -R t id
768 790 e95bcfa18a35+
769 791 $ hg update --clean tip
770 792 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
771 793
772 794 Sticky subrepository, revision updates
773 795 $ hg id
774 796 925c17564ef8 tip
775 797 $ hg -R s id
776 798 12a213df6fa9 tip
777 799 $ hg -R t id
778 800 52c0adc0515a tip
779 801 $ cd s
780 802 $ hg update -r -2
781 803 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
782 804 $ cd ../t
783 805 $ hg update -r 2
784 806 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
785 807 $ cd ..
786 808 $ hg update 10
787 809 subrepository sources for t differ (in checked out version)
788 810 use (l)ocal source (7af322bc1198) or (r)emote source (20a0db6fbf6c)?
789 811 l
790 812 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
791 813 $ hg id
792 814 e45c8b14af55+
793 815 $ hg -R s id
794 816 02dcf1d70411
795 817 $ hg -R t id
796 818 7af322bc1198
797 819
798 820 Sticky subrepository, file changes and revision updates
799 821 $ touch s/f1
800 822 $ touch t/f1
801 823 $ hg add -S s/f1
802 824 $ hg add -S t/f1
803 825 $ hg id
804 826 e45c8b14af55+
805 827 $ hg -R s id
806 828 02dcf1d70411+
807 829 $ hg -R t id
808 830 7af322bc1198+
809 831 $ hg update tip
810 832 subrepository sources for s differ
811 833 use (l)ocal source (02dcf1d70411) or (r)emote source (12a213df6fa9)?
812 834 l
813 835 subrepository sources for t differ
814 836 use (l)ocal source (7af322bc1198) or (r)emote source (52c0adc0515a)?
815 837 l
816 838 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
817 839 $ hg id
818 840 925c17564ef8+ tip
819 841 $ hg -R s id
820 842 02dcf1d70411+
821 843 $ hg -R t id
822 844 7af322bc1198+
823 845
824 846 Sticky repository, update --clean
825 847 $ hg update --clean tip
826 848 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
827 849 $ hg id
828 850 925c17564ef8 tip
829 851 $ hg -R s id
830 852 12a213df6fa9 tip
831 853 $ hg -R t id
832 854 52c0adc0515a tip
833 855
834 856 Test subrepo already at intended revision:
835 857 $ cd s
836 858 $ hg update fc627a69481f
837 859 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
838 860 $ cd ..
839 861 $ hg update 11
840 862 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
841 863 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
842 864 $ hg id -n
843 865 11+
844 866 $ hg -R s id
845 867 fc627a69481f
846 868 $ hg -R t id
847 869 e95bcfa18a35
848 870
849 871 Test that removing .hgsubstate doesn't break anything:
850 872
851 873 $ hg rm -f .hgsubstate
852 874 $ hg ci -mrm
853 875 nothing changed
854 876 [1]
855 877 $ hg log -vr tip
856 878 changeset: 13:925c17564ef8
857 879 tag: tip
858 880 user: test
859 881 date: Thu Jan 01 00:00:00 1970 +0000
860 882 files: .hgsubstate
861 883 description:
862 884 13
863 885
864 886
865 887
866 888 Test that removing .hgsub removes .hgsubstate:
867 889
868 890 $ hg rm .hgsub
869 891 $ hg ci -mrm2
870 892 created new head
871 893 $ hg log -vr tip
872 894 changeset: 14:2400bccd50af
873 895 tag: tip
874 896 parent: 11:365661e5936a
875 897 user: test
876 898 date: Thu Jan 01 00:00:00 1970 +0000
877 899 files: .hgsub .hgsubstate
878 900 description:
879 901 rm2
880 902
881 903
882 904 Test issue3153: diff -S with deleted subrepos
883 905
884 906 $ hg diff --nodates -S -c .
885 907 diff -r 365661e5936a -r 2400bccd50af .hgsub
886 908 --- a/.hgsub
887 909 +++ /dev/null
888 910 @@ -1,2 +0,0 @@
889 911 -s = s
890 912 -t = t
891 913 diff -r 365661e5936a -r 2400bccd50af .hgsubstate
892 914 --- a/.hgsubstate
893 915 +++ /dev/null
894 916 @@ -1,2 +0,0 @@
895 917 -fc627a69481fcbe5f1135069e8a3881c023e4cf5 s
896 918 -e95bcfa18a358dc4936da981ebf4147b4cad1362 t
897 919
898 920 Test behavior of add for explicit path in subrepo:
899 921 $ cd ..
900 922 $ hg init explicit
901 923 $ cd explicit
902 924 $ echo s = s > .hgsub
903 925 $ hg add .hgsub
904 926 $ hg init s
905 927 $ hg ci -m0
906 928 Adding with an explicit path in a subrepo adds the file
907 929 $ echo c1 > f1
908 930 $ echo c2 > s/f2
909 931 $ hg st -S
910 932 ? f1
911 933 ? s/f2
912 934 $ hg add s/f2
913 935 $ hg st -S
914 936 A s/f2
915 937 ? f1
916 938 $ hg ci -R s -m0
917 939 $ hg ci -Am1
918 940 adding f1
919 941 Adding with an explicit path in a subrepo with -S has the same behavior
920 942 $ echo c3 > f3
921 943 $ echo c4 > s/f4
922 944 $ hg st -S
923 945 ? f3
924 946 ? s/f4
925 947 $ hg add -S s/f4
926 948 $ hg st -S
927 949 A s/f4
928 950 ? f3
929 951 $ hg ci -R s -m1
930 952 $ hg ci -Ama2
931 953 adding f3
932 954 Adding without a path or pattern silently ignores subrepos
933 955 $ echo c5 > f5
934 956 $ echo c6 > s/f6
935 957 $ echo c7 > s/f7
936 958 $ hg st -S
937 959 ? f5
938 960 ? s/f6
939 961 ? s/f7
940 962 $ hg add
941 963 adding f5
942 964 $ hg st -S
943 965 A f5
944 966 ? s/f6
945 967 ? s/f7
946 968 $ hg ci -R s -Am2
947 969 adding f6
948 970 adding f7
949 971 $ hg ci -m3
950 972 Adding without a path or pattern with -S also adds files in subrepos
951 973 $ echo c8 > f8
952 974 $ echo c9 > s/f9
953 975 $ echo c10 > s/f10
954 976 $ hg st -S
955 977 ? f8
956 978 ? s/f10
957 979 ? s/f9
958 980 $ hg add -S
959 981 adding f8
960 982 adding s/f10 (glob)
961 983 adding s/f9 (glob)
962 984 $ hg st -S
963 985 A f8
964 986 A s/f10
965 987 A s/f9
966 988 $ hg ci -R s -m3
967 989 $ hg ci -m4
968 990 Adding with a pattern silently ignores subrepos
969 991 $ echo c11 > fm11
970 992 $ echo c12 > fn12
971 993 $ echo c13 > s/fm13
972 994 $ echo c14 > s/fn14
973 995 $ hg st -S
974 996 ? fm11
975 997 ? fn12
976 998 ? s/fm13
977 999 ? s/fn14
978 1000 $ hg add 'glob:**fm*'
979 1001 adding fm11
980 1002 $ hg st -S
981 1003 A fm11
982 1004 ? fn12
983 1005 ? s/fm13
984 1006 ? s/fn14
985 1007 $ hg ci -R s -Am4
986 1008 adding fm13
987 1009 adding fn14
988 1010 $ hg ci -Am5
989 1011 adding fn12
990 1012 Adding with a pattern with -S also adds matches in subrepos
991 1013 $ echo c15 > fm15
992 1014 $ echo c16 > fn16
993 1015 $ echo c17 > s/fm17
994 1016 $ echo c18 > s/fn18
995 1017 $ hg st -S
996 1018 ? fm15
997 1019 ? fn16
998 1020 ? s/fm17
999 1021 ? s/fn18
1000 1022 $ hg add -S 'glob:**fm*'
1001 1023 adding fm15
1002 1024 adding s/fm17 (glob)
1003 1025 $ hg st -S
1004 1026 A fm15
1005 1027 A s/fm17
1006 1028 ? fn16
1007 1029 ? s/fn18
1008 1030 $ hg ci -R s -Am5
1009 1031 adding fn18
1010 1032 $ hg ci -Am6
1011 1033 adding fn16
1012 1034
1013 1035 Test behavior of forget for explicit path in subrepo:
1014 1036 Forgetting an explicit path in a subrepo untracks the file
1015 1037 $ echo c19 > s/f19
1016 1038 $ hg add s/f19
1017 1039 $ hg st -S
1018 1040 A s/f19
1019 1041 $ hg forget s/f19
1020 1042 $ hg st -S
1021 1043 ? s/f19
1022 1044 $ rm s/f19
1023 1045 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now