##// END OF EJS Templates
bundlerepo: use native str when peeking in __dict__...
Augie Fackler -
r35851:87b085a4 default
parent child Browse files
Show More
@@ -1,598 +1,598 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, cgunpacker, 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 = cgunpacker
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 cgunpacker.deltaiter():
60 60 node, p1, p2, cs, deltabase, delta, flags = deltadata
61 61
62 62 size = len(delta)
63 63 start = cgunpacker.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, cgunpacker):
169 169 changelog.changelog.__init__(self, opener)
170 170 linkmapper = lambda x: x
171 171 bundlerevlog.__init__(self, opener, self.indexfile, cgunpacker,
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, cgunpacker, linkmapper, dirlogstarts=None,
190 190 dir=''):
191 191 manifest.manifestrevlog.__init__(self, opener, dir=dir)
192 192 bundlerevlog.__init__(self, opener, self.indexfile, cgunpacker,
193 193 linkmapper)
194 194 if dirlogstarts is None:
195 195 dirlogstarts = {}
196 196 if self.bundle.version == "03":
197 197 dirlogstarts = _getfilestarts(self.bundle)
198 198 self._dirlogstarts = dirlogstarts
199 199 self._linkmapper = linkmapper
200 200
201 201 def baserevision(self, nodeorrev):
202 202 node = nodeorrev
203 203 if isinstance(node, int):
204 204 node = self.node(node)
205 205
206 206 if node in self.fulltextcache:
207 207 result = '%s' % self.fulltextcache[node]
208 208 else:
209 209 result = manifest.manifestrevlog.revision(self, nodeorrev, raw=True)
210 210 return result
211 211
212 212 def dirlog(self, d):
213 213 if d in self._dirlogstarts:
214 214 self.bundle.seek(self._dirlogstarts[d])
215 215 return bundlemanifest(
216 216 self.opener, self.bundle, self._linkmapper,
217 217 self._dirlogstarts, dir=d)
218 218 return super(bundlemanifest, self).dirlog(d)
219 219
220 220 class bundlefilelog(bundlerevlog, filelog.filelog):
221 221 def __init__(self, opener, path, cgunpacker, linkmapper):
222 222 filelog.filelog.__init__(self, opener, path)
223 223 bundlerevlog.__init__(self, opener, self.indexfile, cgunpacker,
224 224 linkmapper)
225 225
226 226 def baserevision(self, nodeorrev):
227 227 return filelog.filelog.revision(self, nodeorrev, raw=True)
228 228
229 229 class bundlepeer(localrepo.localpeer):
230 230 def canpush(self):
231 231 return False
232 232
233 233 class bundlephasecache(phases.phasecache):
234 234 def __init__(self, *args, **kwargs):
235 235 super(bundlephasecache, self).__init__(*args, **kwargs)
236 236 if util.safehasattr(self, 'opener'):
237 237 self.opener = vfsmod.readonlyvfs(self.opener)
238 238
239 239 def write(self):
240 240 raise NotImplementedError
241 241
242 242 def _write(self, fp):
243 243 raise NotImplementedError
244 244
245 245 def _updateroots(self, phase, newroots, tr):
246 246 self.phaseroots[phase] = newroots
247 247 self.invalidate()
248 248 self.dirty = True
249 249
250 250 def _getfilestarts(cgunpacker):
251 251 filespos = {}
252 252 for chunkdata in iter(cgunpacker.filelogheader, {}):
253 253 fname = chunkdata['filename']
254 254 filespos[fname] = cgunpacker.tell()
255 255 for chunk in iter(lambda: cgunpacker.deltachunk(None), {}):
256 256 pass
257 257 return filespos
258 258
259 259 class bundlerepository(localrepo.localrepository):
260 260 """A repository instance that is a union of a local repo and a bundle.
261 261
262 262 Instances represent a read-only repository composed of a local repository
263 263 with the contents of a bundle file applied. The repository instance is
264 264 conceptually similar to the state of a repository after an
265 265 ``hg unbundle`` operation. However, the contents of the bundle are never
266 266 applied to the actual base repository.
267 267 """
268 268 def __init__(self, ui, repopath, bundlepath):
269 269 self._tempparent = None
270 270 try:
271 271 localrepo.localrepository.__init__(self, ui, repopath)
272 272 except error.RepoError:
273 273 self._tempparent = tempfile.mkdtemp()
274 274 localrepo.instance(ui, self._tempparent, 1)
275 275 localrepo.localrepository.__init__(self, ui, self._tempparent)
276 276 self.ui.setconfig('phases', 'publish', False, 'bundlerepo')
277 277
278 278 if repopath:
279 279 self._url = 'bundle:' + util.expandpath(repopath) + '+' + bundlepath
280 280 else:
281 281 self._url = 'bundle:' + bundlepath
282 282
283 283 self.tempfile = None
284 284 f = util.posixfile(bundlepath, "rb")
285 285 bundle = exchange.readbundle(ui, f, bundlepath)
286 286
287 287 if isinstance(bundle, bundle2.unbundle20):
288 288 self._bundlefile = bundle
289 289 self._cgunpacker = None
290 290
291 291 cgpart = None
292 292 for part in bundle.iterparts(seekable=True):
293 293 if part.type == 'changegroup':
294 294 if cgpart:
295 295 raise NotImplementedError("can't process "
296 296 "multiple changegroups")
297 297 cgpart = part
298 298
299 299 self._handlebundle2part(bundle, part)
300 300
301 301 if not cgpart:
302 302 raise error.Abort(_("No changegroups found"))
303 303
304 304 # This is required to placate a later consumer, which expects
305 305 # the payload offset to be at the beginning of the changegroup.
306 306 # We need to do this after the iterparts() generator advances
307 307 # because iterparts() will seek to end of payload after the
308 308 # generator returns control to iterparts().
309 309 cgpart.seek(0, os.SEEK_SET)
310 310
311 311 elif isinstance(bundle, changegroup.cg1unpacker):
312 312 if bundle.compressed():
313 313 f = self._writetempbundle(bundle.read, '.hg10un',
314 314 header='HG10UN')
315 315 bundle = exchange.readbundle(ui, f, bundlepath, self.vfs)
316 316
317 317 self._bundlefile = bundle
318 318 self._cgunpacker = bundle
319 319 else:
320 320 raise error.Abort(_('bundle type %s cannot be read') %
321 321 type(bundle))
322 322
323 323 # dict with the mapping 'filename' -> position in the changegroup.
324 324 self._cgfilespos = {}
325 325
326 326 self.firstnewrev = self.changelog.repotiprev + 1
327 327 phases.retractboundary(self, None, phases.draft,
328 328 [ctx.node() for ctx in self[self.firstnewrev:]])
329 329
330 330 def _handlebundle2part(self, bundle, part):
331 331 if part.type != 'changegroup':
332 332 return
333 333
334 334 cgstream = part
335 335 version = part.params.get('version', '01')
336 336 legalcgvers = changegroup.supportedincomingversions(self)
337 337 if version not in legalcgvers:
338 338 msg = _('Unsupported changegroup version: %s')
339 339 raise error.Abort(msg % version)
340 340 if bundle.compressed():
341 341 cgstream = self._writetempbundle(part.read, '.cg%sun' % version)
342 342
343 343 self._cgunpacker = changegroup.getunbundler(version, cgstream, 'UN')
344 344
345 345 def _writetempbundle(self, readfn, suffix, header=''):
346 346 """Write a temporary file to disk
347 347 """
348 348 fdtemp, temp = self.vfs.mkstemp(prefix="hg-bundle-",
349 349 suffix=suffix)
350 350 self.tempfile = temp
351 351
352 352 with os.fdopen(fdtemp, pycompat.sysstr('wb')) as fptemp:
353 353 fptemp.write(header)
354 354 while True:
355 355 chunk = readfn(2**18)
356 356 if not chunk:
357 357 break
358 358 fptemp.write(chunk)
359 359
360 360 return self.vfs.open(self.tempfile, mode="rb")
361 361
362 362 @localrepo.unfilteredpropertycache
363 363 def _phasecache(self):
364 364 return bundlephasecache(self, self._phasedefaults)
365 365
366 366 @localrepo.unfilteredpropertycache
367 367 def changelog(self):
368 368 # consume the header if it exists
369 369 self._cgunpacker.changelogheader()
370 370 c = bundlechangelog(self.svfs, self._cgunpacker)
371 371 self.manstart = self._cgunpacker.tell()
372 372 return c
373 373
374 374 def _constructmanifest(self):
375 375 self._cgunpacker.seek(self.manstart)
376 376 # consume the header if it exists
377 377 self._cgunpacker.manifestheader()
378 378 linkmapper = self.unfiltered().changelog.rev
379 379 m = bundlemanifest(self.svfs, self._cgunpacker, linkmapper)
380 380 self.filestart = self._cgunpacker.tell()
381 381 return m
382 382
383 383 def _consumemanifest(self):
384 384 """Consumes the manifest portion of the bundle, setting filestart so the
385 385 file portion can be read."""
386 386 self._cgunpacker.seek(self.manstart)
387 387 self._cgunpacker.manifestheader()
388 388 for delta in self._cgunpacker.deltaiter():
389 389 pass
390 390 self.filestart = self._cgunpacker.tell()
391 391
392 392 @localrepo.unfilteredpropertycache
393 393 def manstart(self):
394 394 self.changelog
395 395 return self.manstart
396 396
397 397 @localrepo.unfilteredpropertycache
398 398 def filestart(self):
399 399 self.manifestlog
400 400
401 401 # If filestart was not set by self.manifestlog, that means the
402 402 # manifestlog implementation did not consume the manifests from the
403 403 # changegroup (ex: it might be consuming trees from a separate bundle2
404 404 # part instead). So we need to manually consume it.
405 if 'filestart' not in self.__dict__:
405 if r'filestart' not in self.__dict__:
406 406 self._consumemanifest()
407 407
408 408 return self.filestart
409 409
410 410 def url(self):
411 411 return self._url
412 412
413 413 def file(self, f):
414 414 if not self._cgfilespos:
415 415 self._cgunpacker.seek(self.filestart)
416 416 self._cgfilespos = _getfilestarts(self._cgunpacker)
417 417
418 418 if f in self._cgfilespos:
419 419 self._cgunpacker.seek(self._cgfilespos[f])
420 420 linkmapper = self.unfiltered().changelog.rev
421 421 return bundlefilelog(self.svfs, f, self._cgunpacker, linkmapper)
422 422 else:
423 423 return filelog.filelog(self.svfs, f)
424 424
425 425 def close(self):
426 426 """Close assigned bundle file immediately."""
427 427 self._bundlefile.close()
428 428 if self.tempfile is not None:
429 429 self.vfs.unlink(self.tempfile)
430 430 if self._tempparent:
431 431 shutil.rmtree(self._tempparent, True)
432 432
433 433 def cancopy(self):
434 434 return False
435 435
436 436 def peer(self):
437 437 return bundlepeer(self)
438 438
439 439 def getcwd(self):
440 440 return pycompat.getcwd() # always outside the repo
441 441
442 442 # Check if parents exist in localrepo before setting
443 443 def setparents(self, p1, p2=nullid):
444 444 p1rev = self.changelog.rev(p1)
445 445 p2rev = self.changelog.rev(p2)
446 446 msg = _("setting parent to node %s that only exists in the bundle\n")
447 447 if self.changelog.repotiprev < p1rev:
448 448 self.ui.warn(msg % nodemod.hex(p1))
449 449 if self.changelog.repotiprev < p2rev:
450 450 self.ui.warn(msg % nodemod.hex(p2))
451 451 return super(bundlerepository, self).setparents(p1, p2)
452 452
453 453 def instance(ui, path, create):
454 454 if create:
455 455 raise error.Abort(_('cannot create new bundle repository'))
456 456 # internal config: bundle.mainreporoot
457 457 parentpath = ui.config("bundle", "mainreporoot")
458 458 if not parentpath:
459 459 # try to find the correct path to the working directory repo
460 460 parentpath = cmdutil.findrepo(pycompat.getcwd())
461 461 if parentpath is None:
462 462 parentpath = ''
463 463 if parentpath:
464 464 # Try to make the full path relative so we get a nice, short URL.
465 465 # In particular, we don't want temp dir names in test outputs.
466 466 cwd = pycompat.getcwd()
467 467 if parentpath == cwd:
468 468 parentpath = ''
469 469 else:
470 470 cwd = pathutil.normasprefix(cwd)
471 471 if parentpath.startswith(cwd):
472 472 parentpath = parentpath[len(cwd):]
473 473 u = util.url(path)
474 474 path = u.localpath()
475 475 if u.scheme == 'bundle':
476 476 s = path.split("+", 1)
477 477 if len(s) == 1:
478 478 repopath, bundlename = parentpath, s[0]
479 479 else:
480 480 repopath, bundlename = s
481 481 else:
482 482 repopath, bundlename = parentpath, path
483 483 return bundlerepository(ui, repopath, bundlename)
484 484
485 485 class bundletransactionmanager(object):
486 486 def transaction(self):
487 487 return None
488 488
489 489 def close(self):
490 490 raise NotImplementedError
491 491
492 492 def release(self):
493 493 raise NotImplementedError
494 494
495 495 def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
496 496 force=False):
497 497 '''obtains a bundle of changes incoming from other
498 498
499 499 "onlyheads" restricts the returned changes to those reachable from the
500 500 specified heads.
501 501 "bundlename", if given, stores the bundle to this file path permanently;
502 502 otherwise it's stored to a temp file and gets deleted again when you call
503 503 the returned "cleanupfn".
504 504 "force" indicates whether to proceed on unrelated repos.
505 505
506 506 Returns a tuple (local, csets, cleanupfn):
507 507
508 508 "local" is a local repo from which to obtain the actual incoming
509 509 changesets; it is a bundlerepo for the obtained bundle when the
510 510 original "other" is remote.
511 511 "csets" lists the incoming changeset node ids.
512 512 "cleanupfn" must be called without arguments when you're done processing
513 513 the changes; it closes both the original "other" and the one returned
514 514 here.
515 515 '''
516 516 tmp = discovery.findcommonincoming(repo, other, heads=onlyheads,
517 517 force=force)
518 518 common, incoming, rheads = tmp
519 519 if not incoming:
520 520 try:
521 521 if bundlename:
522 522 os.unlink(bundlename)
523 523 except OSError:
524 524 pass
525 525 return repo, [], other.close
526 526
527 527 commonset = set(common)
528 528 rheads = [x for x in rheads if x not in commonset]
529 529
530 530 bundle = None
531 531 bundlerepo = None
532 532 localrepo = other.local()
533 533 if bundlename or not localrepo:
534 534 # create a bundle (uncompressed if other repo is not local)
535 535
536 536 # developer config: devel.legacy.exchange
537 537 legexc = ui.configlist('devel', 'legacy.exchange')
538 538 forcebundle1 = 'bundle2' not in legexc and 'bundle1' in legexc
539 539 canbundle2 = (not forcebundle1
540 540 and other.capable('getbundle')
541 541 and other.capable('bundle2'))
542 542 if canbundle2:
543 543 kwargs = {}
544 544 kwargs[r'common'] = common
545 545 kwargs[r'heads'] = rheads
546 546 kwargs[r'bundlecaps'] = exchange.caps20to10(repo, role='client')
547 547 kwargs[r'cg'] = True
548 548 b2 = other.getbundle('incoming', **kwargs)
549 549 fname = bundle = changegroup.writechunks(ui, b2._forwardchunks(),
550 550 bundlename)
551 551 else:
552 552 if other.capable('getbundle'):
553 553 cg = other.getbundle('incoming', common=common, heads=rheads)
554 554 elif onlyheads is None and not other.capable('changegroupsubset'):
555 555 # compat with older servers when pulling all remote heads
556 556 cg = other.changegroup(incoming, "incoming")
557 557 rheads = None
558 558 else:
559 559 cg = other.changegroupsubset(incoming, rheads, 'incoming')
560 560 if localrepo:
561 561 bundletype = "HG10BZ"
562 562 else:
563 563 bundletype = "HG10UN"
564 564 fname = bundle = bundle2.writebundle(ui, cg, bundlename,
565 565 bundletype)
566 566 # keep written bundle?
567 567 if bundlename:
568 568 bundle = None
569 569 if not localrepo:
570 570 # use the created uncompressed bundlerepo
571 571 localrepo = bundlerepo = bundlerepository(repo.baseui, repo.root,
572 572 fname)
573 573 # this repo contains local and other now, so filter out local again
574 574 common = repo.heads()
575 575 if localrepo:
576 576 # Part of common may be remotely filtered
577 577 # So use an unfiltered version
578 578 # The discovery process probably need cleanup to avoid that
579 579 localrepo = localrepo.unfiltered()
580 580
581 581 csets = localrepo.changelog.findmissing(common, rheads)
582 582
583 583 if bundlerepo:
584 584 reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev:]]
585 585 remotephases = other.listkeys('phases')
586 586
587 587 pullop = exchange.pulloperation(bundlerepo, other, heads=reponodes)
588 588 pullop.trmanager = bundletransactionmanager()
589 589 exchange._pullapplyphases(pullop, remotephases)
590 590
591 591 def cleanup():
592 592 if bundlerepo:
593 593 bundlerepo.close()
594 594 if bundle:
595 595 os.unlink(bundle)
596 596 other.close()
597 597
598 598 return (localrepo, csets, cleanup)
General Comments 0
You need to be logged in to leave comments. Login now