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