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