##// END OF EJS Templates
bundlerepo: use early return...
Gregory Szorc -
r35074:3eeb0a3e default
parent child Browse files
Show More
@@ -1,589 +1,589
1 1 # bundlerepo.py - repository class for viewing uncompressed bundles
2 2 #
3 3 # Copyright 2006, 2007 Benoit Boissinot <bboissin@gmail.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 """Repository class for viewing uncompressed bundles.
9 9
10 10 This provides a read-only repository interface to bundles as if they
11 11 were part of the actual repository.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import os
17 17 import shutil
18 18 import tempfile
19 19
20 20 from .i18n import _
21 21 from .node import nullid
22 22
23 23 from . import (
24 24 bundle2,
25 25 changegroup,
26 26 changelog,
27 27 cmdutil,
28 28 discovery,
29 29 error,
30 30 exchange,
31 31 filelog,
32 32 localrepo,
33 33 manifest,
34 34 mdiff,
35 35 node as nodemod,
36 36 pathutil,
37 37 phases,
38 38 pycompat,
39 39 revlog,
40 40 util,
41 41 vfs as vfsmod,
42 42 )
43 43
44 44 class bundlerevlog(revlog.revlog):
45 45 def __init__(self, opener, indexfile, bundle, linkmapper):
46 46 # How it works:
47 47 # To retrieve a revision, we need to know the offset of the revision in
48 48 # the bundle (an unbundle object). We store this offset in the index
49 49 # (start). The base of the delta is stored in the base field.
50 50 #
51 51 # To differentiate a rev in the bundle from a rev in the revlog, we
52 52 # check revision against repotiprev.
53 53 opener = vfsmod.readonlyvfs(opener)
54 54 revlog.revlog.__init__(self, opener, indexfile)
55 55 self.bundle = bundle
56 56 n = len(self)
57 57 self.repotiprev = n - 1
58 58 self.bundlerevs = set() # used by 'bundle()' revset expression
59 59 for deltadata in bundle.deltaiter():
60 60 node, p1, p2, cs, deltabase, delta, flags = deltadata
61 61
62 62 size = len(delta)
63 63 start = bundle.tell() - size
64 64
65 65 link = linkmapper(cs)
66 66 if node in self.nodemap:
67 67 # this can happen if two branches make the same change
68 68 self.bundlerevs.add(self.nodemap[node])
69 69 continue
70 70
71 71 for p in (p1, p2):
72 72 if p not in self.nodemap:
73 73 raise error.LookupError(p, self.indexfile,
74 74 _("unknown parent"))
75 75
76 76 if deltabase not in self.nodemap:
77 77 raise LookupError(deltabase, self.indexfile,
78 78 _('unknown delta base'))
79 79
80 80 baserev = self.rev(deltabase)
81 81 # start, size, full unc. size, base (unused), link, p1, p2, node
82 82 e = (revlog.offset_type(start, flags), size, -1, baserev, link,
83 83 self.rev(p1), self.rev(p2), node)
84 84 self.index.insert(-1, e)
85 85 self.nodemap[node] = n
86 86 self.bundlerevs.add(n)
87 87 n += 1
88 88
89 89 def _chunk(self, rev, df=None):
90 90 # Warning: in case of bundle, the diff is against what we stored as
91 91 # delta base, not against rev - 1
92 92 # XXX: could use some caching
93 93 if rev <= self.repotiprev:
94 94 return revlog.revlog._chunk(self, rev)
95 95 self.bundle.seek(self.start(rev))
96 96 return self.bundle.read(self.length(rev))
97 97
98 98 def revdiff(self, rev1, rev2):
99 99 """return or calculate a delta between two revisions"""
100 100 if rev1 > self.repotiprev and rev2 > self.repotiprev:
101 101 # hot path for bundle
102 102 revb = self.index[rev2][3]
103 103 if revb == rev1:
104 104 return self._chunk(rev2)
105 105 elif rev1 <= self.repotiprev and rev2 <= self.repotiprev:
106 106 return revlog.revlog.revdiff(self, rev1, rev2)
107 107
108 108 return mdiff.textdiff(self.revision(rev1, raw=True),
109 109 self.revision(rev2, raw=True))
110 110
111 111 def revision(self, nodeorrev, _df=None, raw=False):
112 112 """return an uncompressed revision of a given node or revision
113 113 number.
114 114 """
115 115 if isinstance(nodeorrev, int):
116 116 rev = nodeorrev
117 117 node = self.node(rev)
118 118 else:
119 119 node = nodeorrev
120 120 rev = self.rev(node)
121 121
122 122 if node == nullid:
123 123 return ""
124 124
125 125 rawtext = None
126 126 chain = []
127 127 iterrev = rev
128 128 # reconstruct the revision if it is from a changegroup
129 129 while iterrev > self.repotiprev:
130 130 if self._cache and self._cache[1] == iterrev:
131 131 rawtext = self._cache[2]
132 132 break
133 133 chain.append(iterrev)
134 134 iterrev = self.index[iterrev][3]
135 135 if rawtext is None:
136 136 rawtext = self.baserevision(iterrev)
137 137
138 138 while chain:
139 139 delta = self._chunk(chain.pop())
140 140 rawtext = mdiff.patches(rawtext, [delta])
141 141
142 142 text, validatehash = self._processflags(rawtext, self.flags(rev),
143 143 'read', raw=raw)
144 144 if validatehash:
145 145 self.checkhash(text, node, rev=rev)
146 146 self._cache = (node, rev, rawtext)
147 147 return text
148 148
149 149 def baserevision(self, nodeorrev):
150 150 # Revlog subclasses may override 'revision' method to modify format of
151 151 # content retrieved from revlog. To use bundlerevlog with such class one
152 152 # needs to override 'baserevision' and make more specific call here.
153 153 return revlog.revlog.revision(self, nodeorrev, raw=True)
154 154
155 155 def addrevision(self, *args, **kwargs):
156 156 raise NotImplementedError
157 157
158 158 def addgroup(self, *args, **kwargs):
159 159 raise NotImplementedError
160 160
161 161 def strip(self, *args, **kwargs):
162 162 raise NotImplementedError
163 163
164 164 def checksize(self):
165 165 raise NotImplementedError
166 166
167 167 class bundlechangelog(bundlerevlog, changelog.changelog):
168 168 def __init__(self, opener, bundle):
169 169 changelog.changelog.__init__(self, opener)
170 170 linkmapper = lambda x: x
171 171 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
172 172 linkmapper)
173 173
174 174 def baserevision(self, nodeorrev):
175 175 # Although changelog doesn't override 'revision' method, some extensions
176 176 # may replace this class with another that does. Same story with
177 177 # manifest and filelog classes.
178 178
179 179 # This bypasses filtering on changelog.node() and rev() because we need
180 180 # revision text of the bundle base even if it is hidden.
181 181 oldfilter = self.filteredrevs
182 182 try:
183 183 self.filteredrevs = ()
184 184 return changelog.changelog.revision(self, nodeorrev, raw=True)
185 185 finally:
186 186 self.filteredrevs = oldfilter
187 187
188 188 class bundlemanifest(bundlerevlog, manifest.manifestrevlog):
189 189 def __init__(self, opener, bundle, linkmapper, dirlogstarts=None, dir=''):
190 190 manifest.manifestrevlog.__init__(self, opener, dir=dir)
191 191 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
192 192 linkmapper)
193 193 if dirlogstarts is None:
194 194 dirlogstarts = {}
195 195 if self.bundle.version == "03":
196 196 dirlogstarts = _getfilestarts(self.bundle)
197 197 self._dirlogstarts = dirlogstarts
198 198 self._linkmapper = linkmapper
199 199
200 200 def baserevision(self, nodeorrev):
201 201 node = nodeorrev
202 202 if isinstance(node, int):
203 203 node = self.node(node)
204 204
205 205 if node in self.fulltextcache:
206 206 result = '%s' % self.fulltextcache[node]
207 207 else:
208 208 result = manifest.manifestrevlog.revision(self, nodeorrev, raw=True)
209 209 return result
210 210
211 211 def dirlog(self, d):
212 212 if d in self._dirlogstarts:
213 213 self.bundle.seek(self._dirlogstarts[d])
214 214 return bundlemanifest(
215 215 self.opener, self.bundle, self._linkmapper,
216 216 self._dirlogstarts, dir=d)
217 217 return super(bundlemanifest, self).dirlog(d)
218 218
219 219 class bundlefilelog(bundlerevlog, filelog.filelog):
220 220 def __init__(self, opener, path, bundle, linkmapper):
221 221 filelog.filelog.__init__(self, opener, path)
222 222 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
223 223 linkmapper)
224 224
225 225 def baserevision(self, nodeorrev):
226 226 return filelog.filelog.revision(self, nodeorrev, raw=True)
227 227
228 228 class bundlepeer(localrepo.localpeer):
229 229 def canpush(self):
230 230 return False
231 231
232 232 class bundlephasecache(phases.phasecache):
233 233 def __init__(self, *args, **kwargs):
234 234 super(bundlephasecache, self).__init__(*args, **kwargs)
235 235 if util.safehasattr(self, 'opener'):
236 236 self.opener = vfsmod.readonlyvfs(self.opener)
237 237
238 238 def write(self):
239 239 raise NotImplementedError
240 240
241 241 def _write(self, fp):
242 242 raise NotImplementedError
243 243
244 244 def _updateroots(self, phase, newroots, tr):
245 245 self.phaseroots[phase] = newroots
246 246 self.invalidate()
247 247 self.dirty = True
248 248
249 249 def _getfilestarts(bundle):
250 250 bundlefilespos = {}
251 251 for chunkdata in iter(bundle.filelogheader, {}):
252 252 fname = chunkdata['filename']
253 253 bundlefilespos[fname] = bundle.tell()
254 254 for chunk in iter(lambda: bundle.deltachunk(None), {}):
255 255 pass
256 256 return bundlefilespos
257 257
258 258 class bundlerepository(localrepo.localrepository):
259 259 """A repository instance that is a union of a local repo and a bundle.
260 260
261 261 Instances represent a read-only repository composed of a local repository
262 262 with the contents of a bundle file applied. The repository instance is
263 263 conceptually similar to the state of a repository after an
264 264 ``hg unbundle`` operation. However, the contents of the bundle are never
265 265 applied to the actual base repository.
266 266 """
267 267 def __init__(self, ui, repopath, bundlepath):
268 268 self._tempparent = None
269 269 try:
270 270 localrepo.localrepository.__init__(self, ui, repopath)
271 271 except error.RepoError:
272 272 self._tempparent = tempfile.mkdtemp()
273 273 localrepo.instance(ui, self._tempparent, 1)
274 274 localrepo.localrepository.__init__(self, ui, self._tempparent)
275 275 self.ui.setconfig('phases', 'publish', False, 'bundlerepo')
276 276
277 277 if repopath:
278 278 self._url = 'bundle:' + util.expandpath(repopath) + '+' + bundlepath
279 279 else:
280 280 self._url = 'bundle:' + bundlepath
281 281
282 282 self.tempfile = None
283 283 f = util.posixfile(bundlepath, "rb")
284 284 bundle = exchange.readbundle(ui, f, bundlepath)
285 285
286 286 if isinstance(bundle, bundle2.unbundle20):
287 287 self._bundlefile = bundle
288 288 self._cgunpacker = None
289 289
290 290 hadchangegroup = False
291 291 for part in bundle.iterparts():
292 292 if part.type == 'changegroup':
293 293 if hadchangegroup:
294 294 raise NotImplementedError("can't process "
295 295 "multiple changegroups")
296 296 hadchangegroup = True
297 297
298 298 self._handlebundle2part(bundle, part)
299 299
300 300 if not hadchangegroup:
301 301 raise error.Abort(_("No changegroups found"))
302 302 elif isinstance(bundle, changegroup.cg1unpacker):
303 303 if bundle.compressed():
304 304 f = self._writetempbundle(bundle.read, '.hg10un',
305 305 header='HG10UN')
306 306 bundle = exchange.readbundle(ui, f, bundlepath, self.vfs)
307 307
308 308 self._bundlefile = bundle
309 309 self._cgunpacker = bundle
310 310 else:
311 311 raise error.Abort(_('bundle type %s cannot be read') %
312 312 type(bundle))
313 313
314 314 # dict with the mapping 'filename' -> position in the bundle
315 315 self.bundlefilespos = {}
316 316
317 317 self.firstnewrev = self.changelog.repotiprev + 1
318 318 phases.retractboundary(self, None, phases.draft,
319 319 [ctx.node() for ctx in self[self.firstnewrev:]])
320 320
321 321 def _handlebundle2part(self, bundle, part):
322 if part.type == 'changegroup':
322 if part.type != 'changegroup':
323 return
324
323 325 cgstream = part
324 326 version = part.params.get('version', '01')
325 327 legalcgvers = changegroup.supportedincomingversions(self)
326 328 if version not in legalcgvers:
327 329 msg = _('Unsupported changegroup version: %s')
328 330 raise error.Abort(msg % version)
329 331 if bundle.compressed():
330 cgstream = self._writetempbundle(part.read,
331 ".cg%sun" % version)
332 cgstream = self._writetempbundle(part.read, '.cg%sun' % version)
332 333
333 self._cgunpacker = changegroup.getunbundler(version, cgstream,
334 'UN')
334 self._cgunpacker = changegroup.getunbundler(version, cgstream, 'UN')
335 335
336 336 def _writetempbundle(self, readfn, suffix, header=''):
337 337 """Write a temporary file to disk
338 338 """
339 339 fdtemp, temp = self.vfs.mkstemp(prefix="hg-bundle-",
340 340 suffix=suffix)
341 341 self.tempfile = temp
342 342
343 343 with os.fdopen(fdtemp, pycompat.sysstr('wb')) as fptemp:
344 344 fptemp.write(header)
345 345 while True:
346 346 chunk = readfn(2**18)
347 347 if not chunk:
348 348 break
349 349 fptemp.write(chunk)
350 350
351 351 return self.vfs.open(self.tempfile, mode="rb")
352 352
353 353 @localrepo.unfilteredpropertycache
354 354 def _phasecache(self):
355 355 return bundlephasecache(self, self._phasedefaults)
356 356
357 357 @localrepo.unfilteredpropertycache
358 358 def changelog(self):
359 359 # consume the header if it exists
360 360 self._cgunpacker.changelogheader()
361 361 c = bundlechangelog(self.svfs, self._cgunpacker)
362 362 self.manstart = self._cgunpacker.tell()
363 363 return c
364 364
365 365 def _constructmanifest(self):
366 366 self._cgunpacker.seek(self.manstart)
367 367 # consume the header if it exists
368 368 self._cgunpacker.manifestheader()
369 369 linkmapper = self.unfiltered().changelog.rev
370 370 m = bundlemanifest(self.svfs, self._cgunpacker, linkmapper)
371 371 self.filestart = self._cgunpacker.tell()
372 372 return m
373 373
374 374 def _consumemanifest(self):
375 375 """Consumes the manifest portion of the bundle, setting filestart so the
376 376 file portion can be read."""
377 377 self._cgunpacker.seek(self.manstart)
378 378 self._cgunpacker.manifestheader()
379 379 for delta in self._cgunpacker.deltaiter():
380 380 pass
381 381 self.filestart = self._cgunpacker.tell()
382 382
383 383 @localrepo.unfilteredpropertycache
384 384 def manstart(self):
385 385 self.changelog
386 386 return self.manstart
387 387
388 388 @localrepo.unfilteredpropertycache
389 389 def filestart(self):
390 390 self.manifestlog
391 391
392 392 # If filestart was not set by self.manifestlog, that means the
393 393 # manifestlog implementation did not consume the manifests from the
394 394 # changegroup (ex: it might be consuming trees from a separate bundle2
395 395 # part instead). So we need to manually consume it.
396 396 if 'filestart' not in self.__dict__:
397 397 self._consumemanifest()
398 398
399 399 return self.filestart
400 400
401 401 def url(self):
402 402 return self._url
403 403
404 404 def file(self, f):
405 405 if not self.bundlefilespos:
406 406 self._cgunpacker.seek(self.filestart)
407 407 self.bundlefilespos = _getfilestarts(self._cgunpacker)
408 408
409 409 if f in self.bundlefilespos:
410 410 self._cgunpacker.seek(self.bundlefilespos[f])
411 411 linkmapper = self.unfiltered().changelog.rev
412 412 return bundlefilelog(self.svfs, f, self._cgunpacker, linkmapper)
413 413 else:
414 414 return filelog.filelog(self.svfs, f)
415 415
416 416 def close(self):
417 417 """Close assigned bundle file immediately."""
418 418 self._bundlefile.close()
419 419 if self.tempfile is not None:
420 420 self.vfs.unlink(self.tempfile)
421 421 if self._tempparent:
422 422 shutil.rmtree(self._tempparent, True)
423 423
424 424 def cancopy(self):
425 425 return False
426 426
427 427 def peer(self):
428 428 return bundlepeer(self)
429 429
430 430 def getcwd(self):
431 431 return pycompat.getcwd() # always outside the repo
432 432
433 433 # Check if parents exist in localrepo before setting
434 434 def setparents(self, p1, p2=nullid):
435 435 p1rev = self.changelog.rev(p1)
436 436 p2rev = self.changelog.rev(p2)
437 437 msg = _("setting parent to node %s that only exists in the bundle\n")
438 438 if self.changelog.repotiprev < p1rev:
439 439 self.ui.warn(msg % nodemod.hex(p1))
440 440 if self.changelog.repotiprev < p2rev:
441 441 self.ui.warn(msg % nodemod.hex(p2))
442 442 return super(bundlerepository, self).setparents(p1, p2)
443 443
444 444 def instance(ui, path, create):
445 445 if create:
446 446 raise error.Abort(_('cannot create new bundle repository'))
447 447 # internal config: bundle.mainreporoot
448 448 parentpath = ui.config("bundle", "mainreporoot")
449 449 if not parentpath:
450 450 # try to find the correct path to the working directory repo
451 451 parentpath = cmdutil.findrepo(pycompat.getcwd())
452 452 if parentpath is None:
453 453 parentpath = ''
454 454 if parentpath:
455 455 # Try to make the full path relative so we get a nice, short URL.
456 456 # In particular, we don't want temp dir names in test outputs.
457 457 cwd = pycompat.getcwd()
458 458 if parentpath == cwd:
459 459 parentpath = ''
460 460 else:
461 461 cwd = pathutil.normasprefix(cwd)
462 462 if parentpath.startswith(cwd):
463 463 parentpath = parentpath[len(cwd):]
464 464 u = util.url(path)
465 465 path = u.localpath()
466 466 if u.scheme == 'bundle':
467 467 s = path.split("+", 1)
468 468 if len(s) == 1:
469 469 repopath, bundlename = parentpath, s[0]
470 470 else:
471 471 repopath, bundlename = s
472 472 else:
473 473 repopath, bundlename = parentpath, path
474 474 return bundlerepository(ui, repopath, bundlename)
475 475
476 476 class bundletransactionmanager(object):
477 477 def transaction(self):
478 478 return None
479 479
480 480 def close(self):
481 481 raise NotImplementedError
482 482
483 483 def release(self):
484 484 raise NotImplementedError
485 485
486 486 def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
487 487 force=False):
488 488 '''obtains a bundle of changes incoming from other
489 489
490 490 "onlyheads" restricts the returned changes to those reachable from the
491 491 specified heads.
492 492 "bundlename", if given, stores the bundle to this file path permanently;
493 493 otherwise it's stored to a temp file and gets deleted again when you call
494 494 the returned "cleanupfn".
495 495 "force" indicates whether to proceed on unrelated repos.
496 496
497 497 Returns a tuple (local, csets, cleanupfn):
498 498
499 499 "local" is a local repo from which to obtain the actual incoming
500 500 changesets; it is a bundlerepo for the obtained bundle when the
501 501 original "other" is remote.
502 502 "csets" lists the incoming changeset node ids.
503 503 "cleanupfn" must be called without arguments when you're done processing
504 504 the changes; it closes both the original "other" and the one returned
505 505 here.
506 506 '''
507 507 tmp = discovery.findcommonincoming(repo, other, heads=onlyheads,
508 508 force=force)
509 509 common, incoming, rheads = tmp
510 510 if not incoming:
511 511 try:
512 512 if bundlename:
513 513 os.unlink(bundlename)
514 514 except OSError:
515 515 pass
516 516 return repo, [], other.close
517 517
518 518 commonset = set(common)
519 519 rheads = [x for x in rheads if x not in commonset]
520 520
521 521 bundle = None
522 522 bundlerepo = None
523 523 localrepo = other.local()
524 524 if bundlename or not localrepo:
525 525 # create a bundle (uncompressed if other repo is not local)
526 526
527 527 # developer config: devel.legacy.exchange
528 528 legexc = ui.configlist('devel', 'legacy.exchange')
529 529 forcebundle1 = 'bundle2' not in legexc and 'bundle1' in legexc
530 530 canbundle2 = (not forcebundle1
531 531 and other.capable('getbundle')
532 532 and other.capable('bundle2'))
533 533 if canbundle2:
534 534 kwargs = {}
535 535 kwargs['common'] = common
536 536 kwargs['heads'] = rheads
537 537 kwargs['bundlecaps'] = exchange.caps20to10(repo)
538 538 kwargs['cg'] = True
539 539 b2 = other.getbundle('incoming', **kwargs)
540 540 fname = bundle = changegroup.writechunks(ui, b2._forwardchunks(),
541 541 bundlename)
542 542 else:
543 543 if other.capable('getbundle'):
544 544 cg = other.getbundle('incoming', common=common, heads=rheads)
545 545 elif onlyheads is None and not other.capable('changegroupsubset'):
546 546 # compat with older servers when pulling all remote heads
547 547 cg = other.changegroup(incoming, "incoming")
548 548 rheads = None
549 549 else:
550 550 cg = other.changegroupsubset(incoming, rheads, 'incoming')
551 551 if localrepo:
552 552 bundletype = "HG10BZ"
553 553 else:
554 554 bundletype = "HG10UN"
555 555 fname = bundle = bundle2.writebundle(ui, cg, bundlename,
556 556 bundletype)
557 557 # keep written bundle?
558 558 if bundlename:
559 559 bundle = None
560 560 if not localrepo:
561 561 # use the created uncompressed bundlerepo
562 562 localrepo = bundlerepo = bundlerepository(repo.baseui, repo.root,
563 563 fname)
564 564 # this repo contains local and other now, so filter out local again
565 565 common = repo.heads()
566 566 if localrepo:
567 567 # Part of common may be remotely filtered
568 568 # So use an unfiltered version
569 569 # The discovery process probably need cleanup to avoid that
570 570 localrepo = localrepo.unfiltered()
571 571
572 572 csets = localrepo.changelog.findmissing(common, rheads)
573 573
574 574 if bundlerepo:
575 575 reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev:]]
576 576 remotephases = other.listkeys('phases')
577 577
578 578 pullop = exchange.pulloperation(bundlerepo, other, heads=reponodes)
579 579 pullop.trmanager = bundletransactionmanager()
580 580 exchange._pullapplyphases(pullop, remotephases)
581 581
582 582 def cleanup():
583 583 if bundlerepo:
584 584 bundlerepo.close()
585 585 if bundle:
586 586 os.unlink(bundle)
587 587 other.close()
588 588
589 589 return (localrepo, csets, cleanup)
General Comments 0
You need to be logged in to leave comments. Login now