##// END OF EJS Templates
bundlerepo: make bundle and bundlefile attributes private...
Gregory Szorc -
r35043:df2a676a default
parent child Browse files
Show More
@@ -1,584 +1,583 b''
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 self.bundlefile = self.bundle = exchange.readbundle(ui, f, bundlepath)
284 self._bundlefile = self._bundle = exchange.readbundle(ui, f, bundlepath)
285 285
286 if isinstance(self.bundle, bundle2.unbundle20):
286 if isinstance(self._bundle, bundle2.unbundle20):
287 287 hadchangegroup = False
288 for part in self.bundle.iterparts():
288 for part in self._bundle.iterparts():
289 289 if part.type == 'changegroup':
290 290 if hadchangegroup:
291 291 raise NotImplementedError("can't process "
292 292 "multiple changegroups")
293 293 hadchangegroup = True
294 294
295 295 self._handlebundle2part(part)
296 296
297 297 if not hadchangegroup:
298 298 raise error.Abort(_("No changegroups found"))
299 elif isinstance(self.bundle, changegroup.cg1unpacker):
300 if self.bundle.compressed():
301 f = self._writetempbundle(self.bundle.read, '.hg10un',
299 elif isinstance(self._bundle, changegroup.cg1unpacker):
300 if self._bundle.compressed():
301 f = self._writetempbundle(self._bundle.read, '.hg10un',
302 302 header='HG10UN')
303 self.bundlefile = self.bundle = exchange.readbundle(ui, f,
304 bundlepath,
305 self.vfs)
303 self._bundlefile = self._bundle = exchange.readbundle(
304 ui, f, bundlepath, self.vfs)
306 305 else:
307 306 raise error.Abort(_('bundle type %s cannot be read') %
308 type(self.bundle))
307 type(self._bundle))
309 308
310 309 # dict with the mapping 'filename' -> position in the bundle
311 310 self.bundlefilespos = {}
312 311
313 312 self.firstnewrev = self.changelog.repotiprev + 1
314 313 phases.retractboundary(self, None, phases.draft,
315 314 [ctx.node() for ctx in self[self.firstnewrev:]])
316 315
317 316 def _handlebundle2part(self, part):
318 317 if part.type == 'changegroup':
319 318 cgstream = part
320 319 version = part.params.get('version', '01')
321 320 legalcgvers = changegroup.supportedincomingversions(self)
322 321 if version not in legalcgvers:
323 322 msg = _('Unsupported changegroup version: %s')
324 323 raise error.Abort(msg % version)
325 if self.bundle.compressed():
324 if self._bundle.compressed():
326 325 cgstream = self._writetempbundle(part.read,
327 326 ".cg%sun" % version)
328 327
329 self.bundle = changegroup.getunbundler(version, cgstream, 'UN')
328 self._bundle = changegroup.getunbundler(version, cgstream, 'UN')
330 329
331 330 def _writetempbundle(self, readfn, suffix, header=''):
332 331 """Write a temporary file to disk
333 332 """
334 333 fdtemp, temp = self.vfs.mkstemp(prefix="hg-bundle-",
335 334 suffix=suffix)
336 335 self.tempfile = temp
337 336
338 337 with os.fdopen(fdtemp, pycompat.sysstr('wb')) as fptemp:
339 338 fptemp.write(header)
340 339 while True:
341 340 chunk = readfn(2**18)
342 341 if not chunk:
343 342 break
344 343 fptemp.write(chunk)
345 344
346 345 return self.vfs.open(self.tempfile, mode="rb")
347 346
348 347 @localrepo.unfilteredpropertycache
349 348 def _phasecache(self):
350 349 return bundlephasecache(self, self._phasedefaults)
351 350
352 351 @localrepo.unfilteredpropertycache
353 352 def changelog(self):
354 353 # consume the header if it exists
355 self.bundle.changelogheader()
356 c = bundlechangelog(self.svfs, self.bundle)
357 self.manstart = self.bundle.tell()
354 self._bundle.changelogheader()
355 c = bundlechangelog(self.svfs, self._bundle)
356 self.manstart = self._bundle.tell()
358 357 return c
359 358
360 359 def _constructmanifest(self):
361 self.bundle.seek(self.manstart)
360 self._bundle.seek(self.manstart)
362 361 # consume the header if it exists
363 self.bundle.manifestheader()
362 self._bundle.manifestheader()
364 363 linkmapper = self.unfiltered().changelog.rev
365 m = bundlemanifest(self.svfs, self.bundle, linkmapper)
366 self.filestart = self.bundle.tell()
364 m = bundlemanifest(self.svfs, self._bundle, linkmapper)
365 self.filestart = self._bundle.tell()
367 366 return m
368 367
369 368 def _consumemanifest(self):
370 369 """Consumes the manifest portion of the bundle, setting filestart so the
371 370 file portion can be read."""
372 self.bundle.seek(self.manstart)
373 self.bundle.manifestheader()
374 for delta in self.bundle.deltaiter():
371 self._bundle.seek(self.manstart)
372 self._bundle.manifestheader()
373 for delta in self._bundle.deltaiter():
375 374 pass
376 self.filestart = self.bundle.tell()
375 self.filestart = self._bundle.tell()
377 376
378 377 @localrepo.unfilteredpropertycache
379 378 def manstart(self):
380 379 self.changelog
381 380 return self.manstart
382 381
383 382 @localrepo.unfilteredpropertycache
384 383 def filestart(self):
385 384 self.manifestlog
386 385
387 386 # If filestart was not set by self.manifestlog, that means the
388 387 # manifestlog implementation did not consume the manifests from the
389 388 # changegroup (ex: it might be consuming trees from a separate bundle2
390 389 # part instead). So we need to manually consume it.
391 390 if 'filestart' not in self.__dict__:
392 391 self._consumemanifest()
393 392
394 393 return self.filestart
395 394
396 395 def url(self):
397 396 return self._url
398 397
399 398 def file(self, f):
400 399 if not self.bundlefilespos:
401 self.bundle.seek(self.filestart)
402 self.bundlefilespos = _getfilestarts(self.bundle)
400 self._bundle.seek(self.filestart)
401 self.bundlefilespos = _getfilestarts(self._bundle)
403 402
404 403 if f in self.bundlefilespos:
405 self.bundle.seek(self.bundlefilespos[f])
404 self._bundle.seek(self.bundlefilespos[f])
406 405 linkmapper = self.unfiltered().changelog.rev
407 return bundlefilelog(self.svfs, f, self.bundle, linkmapper)
406 return bundlefilelog(self.svfs, f, self._bundle, linkmapper)
408 407 else:
409 408 return filelog.filelog(self.svfs, f)
410 409
411 410 def close(self):
412 411 """Close assigned bundle file immediately."""
413 self.bundlefile.close()
412 self._bundlefile.close()
414 413 if self.tempfile is not None:
415 414 self.vfs.unlink(self.tempfile)
416 415 if self._tempparent:
417 416 shutil.rmtree(self._tempparent, True)
418 417
419 418 def cancopy(self):
420 419 return False
421 420
422 421 def peer(self):
423 422 return bundlepeer(self)
424 423
425 424 def getcwd(self):
426 425 return pycompat.getcwd() # always outside the repo
427 426
428 427 # Check if parents exist in localrepo before setting
429 428 def setparents(self, p1, p2=nullid):
430 429 p1rev = self.changelog.rev(p1)
431 430 p2rev = self.changelog.rev(p2)
432 431 msg = _("setting parent to node %s that only exists in the bundle\n")
433 432 if self.changelog.repotiprev < p1rev:
434 433 self.ui.warn(msg % nodemod.hex(p1))
435 434 if self.changelog.repotiprev < p2rev:
436 435 self.ui.warn(msg % nodemod.hex(p2))
437 436 return super(bundlerepository, self).setparents(p1, p2)
438 437
439 438 def instance(ui, path, create):
440 439 if create:
441 440 raise error.Abort(_('cannot create new bundle repository'))
442 441 # internal config: bundle.mainreporoot
443 442 parentpath = ui.config("bundle", "mainreporoot")
444 443 if not parentpath:
445 444 # try to find the correct path to the working directory repo
446 445 parentpath = cmdutil.findrepo(pycompat.getcwd())
447 446 if parentpath is None:
448 447 parentpath = ''
449 448 if parentpath:
450 449 # Try to make the full path relative so we get a nice, short URL.
451 450 # In particular, we don't want temp dir names in test outputs.
452 451 cwd = pycompat.getcwd()
453 452 if parentpath == cwd:
454 453 parentpath = ''
455 454 else:
456 455 cwd = pathutil.normasprefix(cwd)
457 456 if parentpath.startswith(cwd):
458 457 parentpath = parentpath[len(cwd):]
459 458 u = util.url(path)
460 459 path = u.localpath()
461 460 if u.scheme == 'bundle':
462 461 s = path.split("+", 1)
463 462 if len(s) == 1:
464 463 repopath, bundlename = parentpath, s[0]
465 464 else:
466 465 repopath, bundlename = s
467 466 else:
468 467 repopath, bundlename = parentpath, path
469 468 return bundlerepository(ui, repopath, bundlename)
470 469
471 470 class bundletransactionmanager(object):
472 471 def transaction(self):
473 472 return None
474 473
475 474 def close(self):
476 475 raise NotImplementedError
477 476
478 477 def release(self):
479 478 raise NotImplementedError
480 479
481 480 def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
482 481 force=False):
483 482 '''obtains a bundle of changes incoming from other
484 483
485 484 "onlyheads" restricts the returned changes to those reachable from the
486 485 specified heads.
487 486 "bundlename", if given, stores the bundle to this file path permanently;
488 487 otherwise it's stored to a temp file and gets deleted again when you call
489 488 the returned "cleanupfn".
490 489 "force" indicates whether to proceed on unrelated repos.
491 490
492 491 Returns a tuple (local, csets, cleanupfn):
493 492
494 493 "local" is a local repo from which to obtain the actual incoming
495 494 changesets; it is a bundlerepo for the obtained bundle when the
496 495 original "other" is remote.
497 496 "csets" lists the incoming changeset node ids.
498 497 "cleanupfn" must be called without arguments when you're done processing
499 498 the changes; it closes both the original "other" and the one returned
500 499 here.
501 500 '''
502 501 tmp = discovery.findcommonincoming(repo, other, heads=onlyheads,
503 502 force=force)
504 503 common, incoming, rheads = tmp
505 504 if not incoming:
506 505 try:
507 506 if bundlename:
508 507 os.unlink(bundlename)
509 508 except OSError:
510 509 pass
511 510 return repo, [], other.close
512 511
513 512 commonset = set(common)
514 513 rheads = [x for x in rheads if x not in commonset]
515 514
516 515 bundle = None
517 516 bundlerepo = None
518 517 localrepo = other.local()
519 518 if bundlename or not localrepo:
520 519 # create a bundle (uncompressed if other repo is not local)
521 520
522 521 # developer config: devel.legacy.exchange
523 522 legexc = ui.configlist('devel', 'legacy.exchange')
524 523 forcebundle1 = 'bundle2' not in legexc and 'bundle1' in legexc
525 524 canbundle2 = (not forcebundle1
526 525 and other.capable('getbundle')
527 526 and other.capable('bundle2'))
528 527 if canbundle2:
529 528 kwargs = {}
530 529 kwargs['common'] = common
531 530 kwargs['heads'] = rheads
532 531 kwargs['bundlecaps'] = exchange.caps20to10(repo)
533 532 kwargs['cg'] = True
534 533 b2 = other.getbundle('incoming', **kwargs)
535 534 fname = bundle = changegroup.writechunks(ui, b2._forwardchunks(),
536 535 bundlename)
537 536 else:
538 537 if other.capable('getbundle'):
539 538 cg = other.getbundle('incoming', common=common, heads=rheads)
540 539 elif onlyheads is None and not other.capable('changegroupsubset'):
541 540 # compat with older servers when pulling all remote heads
542 541 cg = other.changegroup(incoming, "incoming")
543 542 rheads = None
544 543 else:
545 544 cg = other.changegroupsubset(incoming, rheads, 'incoming')
546 545 if localrepo:
547 546 bundletype = "HG10BZ"
548 547 else:
549 548 bundletype = "HG10UN"
550 549 fname = bundle = bundle2.writebundle(ui, cg, bundlename,
551 550 bundletype)
552 551 # keep written bundle?
553 552 if bundlename:
554 553 bundle = None
555 554 if not localrepo:
556 555 # use the created uncompressed bundlerepo
557 556 localrepo = bundlerepo = bundlerepository(repo.baseui, repo.root,
558 557 fname)
559 558 # this repo contains local and other now, so filter out local again
560 559 common = repo.heads()
561 560 if localrepo:
562 561 # Part of common may be remotely filtered
563 562 # So use an unfiltered version
564 563 # The discovery process probably need cleanup to avoid that
565 564 localrepo = localrepo.unfiltered()
566 565
567 566 csets = localrepo.changelog.findmissing(common, rheads)
568 567
569 568 if bundlerepo:
570 569 reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev:]]
571 570 remotephases = other.listkeys('phases')
572 571
573 572 pullop = exchange.pulloperation(bundlerepo, other, heads=reponodes)
574 573 pullop.trmanager = bundletransactionmanager()
575 574 exchange._pullapplyphases(pullop, remotephases)
576 575
577 576 def cleanup():
578 577 if bundlerepo:
579 578 bundlerepo.close()
580 579 if bundle:
581 580 os.unlink(bundle)
582 581 other.close()
583 582
584 583 return (localrepo, csets, cleanup)
General Comments 0
You need to be logged in to leave comments. Login now