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