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