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