##// END OF EJS Templates
bundlerepo: store validated deltabase rev in basemap instead of node...
Mads Kiilerich -
r18416:87f370c5 default
parent child Browse files
Show More
@@ -1,380 +1,386
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 node import nullid
15 15 from i18n import _
16 16 import os, tempfile, shutil
17 17 import changegroup, util, mdiff, discovery, cmdutil, scmutil
18 18 import localrepo, changelog, manifest, filelog, revlog, error
19 19
20 20 class bundlerevlog(revlog.revlog):
21 21 def __init__(self, opener, indexfile, bundle, linkmapper):
22 22 # How it works:
23 23 # To retrieve a revision, we need to know the offset of the revision in
24 24 # the bundle (an unbundle object). We store this offset in the index
25 25 # (start).
26 26 #
27 27 # basemap is indexed with revisions coming from the bundle, and it
28 # maps to the node that is the base of the corresponding delta.
28 # maps to the revision that is the base of the corresponding delta.
29 29 #
30 30 # To differentiate a rev in the bundle from a rev in the revlog, we
31 31 # check revision against basemap.
32 32 opener = scmutil.readonlyvfs(opener)
33 33 revlog.revlog.__init__(self, opener, indexfile)
34 34 self.bundle = bundle
35 self.basemap = {}
35 self.basemap = {} # mapping rev to delta base rev
36 36 n = len(self)
37 37 chain = None
38 38 self.bundlerevs = set() # used by 'bundle()' revset expression
39 39 while True:
40 40 chunkdata = bundle.deltachunk(chain)
41 41 if not chunkdata:
42 42 break
43 43 node = chunkdata['node']
44 44 p1 = chunkdata['p1']
45 45 p2 = chunkdata['p2']
46 46 cs = chunkdata['cs']
47 47 deltabase = chunkdata['deltabase']
48 48 delta = chunkdata['delta']
49 49
50 50 size = len(delta)
51 51 start = bundle.tell() - size
52 52
53 53 link = linkmapper(cs)
54 54 if node in self.nodemap:
55 55 # this can happen if two branches make the same change
56 56 chain = node
57 57 self.bundlerevs.add(self.nodemap[node])
58 58 continue
59 59
60 60 for p in (p1, p2):
61 61 if p not in self.nodemap:
62 62 raise error.LookupError(p, self.indexfile,
63 63 _("unknown parent"))
64
65 if deltabase not in self.nodemap:
66 raise LookupError(deltabase, self.indexfile,
67 _('unknown delta base'))
68
69 baserev = self.rev(deltabase)
64 70 # start, size, full unc. size, base (unused), link, p1, p2, node
65 71 e = (revlog.offset_type(start, 0), size, -1, -1, link,
66 72 self.rev(p1), self.rev(p2), node)
67 self.basemap[n] = deltabase
73 self.basemap[n] = baserev
68 74 self.index.insert(-1, e)
69 75 self.nodemap[node] = n
70 76 self.bundlerevs.add(n)
71 77 chain = node
72 78 n += 1
73 79
74 80 def _chunk(self, rev):
75 81 # Warning: in case of bundle, the diff is against self.basemap,
76 82 # not against rev - 1
77 83 # XXX: could use some caching
78 84 if rev not in self.basemap:
79 85 return revlog.revlog._chunk(self, rev)
80 86 self.bundle.seek(self.start(rev))
81 87 return self.bundle.read(self.length(rev))
82 88
83 89 def revdiff(self, rev1, rev2):
84 90 """return or calculate a delta between two revisions"""
85 91 if rev1 in self.basemap and rev2 in self.basemap:
86 92 # hot path for bundle
87 revb = self.rev(self.basemap[rev2])
93 revb = self.basemap[rev2]
88 94 if revb == rev1:
89 95 return self._chunk(rev2)
90 96 elif rev1 not in self.basemap and rev2 not in self.basemap:
91 97 return revlog.revlog.revdiff(self, rev1, rev2)
92 98
93 99 return mdiff.textdiff(self.revision(self.node(rev1)),
94 100 self.revision(self.node(rev2)))
95 101
96 102 def revision(self, nodeorrev):
97 103 """return an uncompressed revision of a given node or revision
98 104 number.
99 105 """
100 106 if isinstance(nodeorrev, int):
101 107 rev = nodeorrev
102 108 node = self.node(rev)
103 109 else:
104 110 node = nodeorrev
105 111 rev = self.rev(node)
106 112
107 113 if node == nullid:
108 114 return ""
109 115
110 116 text = None
111 117 chain = []
112 118 iterrev = rev
113 119 # reconstruct the revision if it is from a changegroup
114 120 while iterrev in self.basemap:
115 121 if self._cache and self._cache[1] == iterrev:
116 122 text = self._cache[2]
117 123 break
118 124 chain.append(iterrev)
119 iterrev = self.rev(self.basemap[iterrev])
125 iterrev = self.basemap[iterrev]
120 126 if text is None:
121 127 text = revlog.revlog.revision(self, iterrev)
122 128
123 129 while chain:
124 130 delta = self._chunk(chain.pop())
125 131 text = mdiff.patches(text, [delta])
126 132
127 133 p1, p2 = self.parents(node)
128 134 if node != revlog.hash(text, p1, p2):
129 135 raise error.RevlogError(_("integrity check failed on %s:%d")
130 136 % (self.datafile, rev))
131 137
132 138 self._cache = (node, rev, text)
133 139 return text
134 140
135 141 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
136 142 raise NotImplementedError
137 143 def addgroup(self, revs, linkmapper, transaction):
138 144 raise NotImplementedError
139 145 def strip(self, rev, minlink):
140 146 raise NotImplementedError
141 147 def checksize(self):
142 148 raise NotImplementedError
143 149
144 150 class bundlechangelog(bundlerevlog, changelog.changelog):
145 151 def __init__(self, opener, bundle):
146 152 changelog.changelog.__init__(self, opener)
147 153 linkmapper = lambda x: x
148 154 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
149 155 linkmapper)
150 156
151 157 class bundlemanifest(bundlerevlog, manifest.manifest):
152 158 def __init__(self, opener, bundle, linkmapper):
153 159 manifest.manifest.__init__(self, opener)
154 160 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
155 161 linkmapper)
156 162
157 163 class bundlefilelog(bundlerevlog, filelog.filelog):
158 164 def __init__(self, opener, path, bundle, linkmapper, repo):
159 165 filelog.filelog.__init__(self, opener, path)
160 166 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
161 167 linkmapper)
162 168 self._repo = repo
163 169
164 170 def _file(self, f):
165 171 self._repo.file(f)
166 172
167 173 class bundlepeer(localrepo.localpeer):
168 174 def canpush(self):
169 175 return False
170 176
171 177 class bundlerepository(localrepo.localrepository):
172 178 def __init__(self, ui, path, bundlename):
173 179 self._tempparent = None
174 180 try:
175 181 localrepo.localrepository.__init__(self, ui, path)
176 182 except error.RepoError:
177 183 self._tempparent = tempfile.mkdtemp()
178 184 localrepo.instance(ui, self._tempparent, 1)
179 185 localrepo.localrepository.__init__(self, ui, self._tempparent)
180 186 self.ui.setconfig('phases', 'publish', False)
181 187
182 188 if path:
183 189 self._url = 'bundle:' + util.expandpath(path) + '+' + bundlename
184 190 else:
185 191 self._url = 'bundle:' + bundlename
186 192
187 193 self.tempfile = None
188 194 f = util.posixfile(bundlename, "rb")
189 195 self.bundle = changegroup.readbundle(f, bundlename)
190 196 if self.bundle.compressed():
191 197 fdtemp, temp = tempfile.mkstemp(prefix="hg-bundle-",
192 198 suffix=".hg10un", dir=self.path)
193 199 self.tempfile = temp
194 200 fptemp = os.fdopen(fdtemp, 'wb')
195 201
196 202 try:
197 203 fptemp.write("HG10UN")
198 204 while True:
199 205 chunk = self.bundle.read(2**18)
200 206 if not chunk:
201 207 break
202 208 fptemp.write(chunk)
203 209 finally:
204 210 fptemp.close()
205 211
206 212 f = util.posixfile(self.tempfile, "rb")
207 213 self.bundle = changegroup.readbundle(f, bundlename)
208 214
209 215 # dict with the mapping 'filename' -> position in the bundle
210 216 self.bundlefilespos = {}
211 217
212 218 @localrepo.unfilteredpropertycache
213 219 def changelog(self):
214 220 # consume the header if it exists
215 221 self.bundle.changelogheader()
216 222 c = bundlechangelog(self.sopener, self.bundle)
217 223 self.manstart = self.bundle.tell()
218 224 return c
219 225
220 226 @localrepo.unfilteredpropertycache
221 227 def manifest(self):
222 228 self.bundle.seek(self.manstart)
223 229 # consume the header if it exists
224 230 self.bundle.manifestheader()
225 231 m = bundlemanifest(self.sopener, self.bundle, self.changelog.rev)
226 232 self.filestart = self.bundle.tell()
227 233 return m
228 234
229 235 @localrepo.unfilteredpropertycache
230 236 def manstart(self):
231 237 self.changelog
232 238 return self.manstart
233 239
234 240 @localrepo.unfilteredpropertycache
235 241 def filestart(self):
236 242 self.manifest
237 243 return self.filestart
238 244
239 245 def url(self):
240 246 return self._url
241 247
242 248 def file(self, f):
243 249 if not self.bundlefilespos:
244 250 self.bundle.seek(self.filestart)
245 251 while True:
246 252 chunkdata = self.bundle.filelogheader()
247 253 if not chunkdata:
248 254 break
249 255 fname = chunkdata['filename']
250 256 self.bundlefilespos[fname] = self.bundle.tell()
251 257 while True:
252 258 c = self.bundle.deltachunk(None)
253 259 if not c:
254 260 break
255 261
256 262 if f[0] == '/':
257 263 f = f[1:]
258 264 if f in self.bundlefilespos:
259 265 self.bundle.seek(self.bundlefilespos[f])
260 266 return bundlefilelog(self.sopener, f, self.bundle,
261 267 self.changelog.rev, self)
262 268 else:
263 269 return filelog.filelog(self.sopener, f)
264 270
265 271 def close(self):
266 272 """Close assigned bundle file immediately."""
267 273 self.bundle.close()
268 274 if self.tempfile is not None:
269 275 os.unlink(self.tempfile)
270 276 if self._tempparent:
271 277 shutil.rmtree(self._tempparent, True)
272 278
273 279 def cancopy(self):
274 280 return False
275 281
276 282 def peer(self):
277 283 return bundlepeer(self)
278 284
279 285 def getcwd(self):
280 286 return os.getcwd() # always outside the repo
281 287
282 288
283 289 def instance(ui, path, create):
284 290 if create:
285 291 raise util.Abort(_('cannot create new bundle repository'))
286 292 parentpath = ui.config("bundle", "mainreporoot", "")
287 293 if not parentpath:
288 294 # try to find the correct path to the working directory repo
289 295 parentpath = cmdutil.findrepo(os.getcwd())
290 296 if parentpath is None:
291 297 parentpath = ''
292 298 if parentpath:
293 299 # Try to make the full path relative so we get a nice, short URL.
294 300 # In particular, we don't want temp dir names in test outputs.
295 301 cwd = os.getcwd()
296 302 if parentpath == cwd:
297 303 parentpath = ''
298 304 else:
299 305 cwd = os.path.join(cwd,'')
300 306 if parentpath.startswith(cwd):
301 307 parentpath = parentpath[len(cwd):]
302 308 u = util.url(path)
303 309 path = u.localpath()
304 310 if u.scheme == 'bundle':
305 311 s = path.split("+", 1)
306 312 if len(s) == 1:
307 313 repopath, bundlename = parentpath, s[0]
308 314 else:
309 315 repopath, bundlename = s
310 316 else:
311 317 repopath, bundlename = parentpath, path
312 318 return bundlerepository(ui, repopath, bundlename)
313 319
314 320 def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
315 321 force=False):
316 322 '''obtains a bundle of changes incoming from other
317 323
318 324 "onlyheads" restricts the returned changes to those reachable from the
319 325 specified heads.
320 326 "bundlename", if given, stores the bundle to this file path permanently;
321 327 otherwise it's stored to a temp file and gets deleted again when you call
322 328 the returned "cleanupfn".
323 329 "force" indicates whether to proceed on unrelated repos.
324 330
325 331 Returns a tuple (local, csets, cleanupfn):
326 332
327 333 "local" is a local repo from which to obtain the actual incoming
328 334 changesets; it is a bundlerepo for the obtained bundle when the
329 335 original "other" is remote.
330 336 "csets" lists the incoming changeset node ids.
331 337 "cleanupfn" must be called without arguments when you're done processing
332 338 the changes; it closes both the original "other" and the one returned
333 339 here.
334 340 '''
335 341 tmp = discovery.findcommonincoming(repo, other, heads=onlyheads,
336 342 force=force)
337 343 common, incoming, rheads = tmp
338 344 if not incoming:
339 345 try:
340 346 if bundlename:
341 347 os.unlink(bundlename)
342 348 except OSError:
343 349 pass
344 350 return repo, [], other.close
345 351
346 352 bundle = None
347 353 bundlerepo = None
348 354 localrepo = other.local()
349 355 if bundlename or not localrepo:
350 356 # create a bundle (uncompressed if other repo is not local)
351 357
352 358 if other.capable('getbundle'):
353 359 cg = other.getbundle('incoming', common=common, heads=rheads)
354 360 elif onlyheads is None and not other.capable('changegroupsubset'):
355 361 # compat with older servers when pulling all remote heads
356 362 cg = other.changegroup(incoming, "incoming")
357 363 rheads = None
358 364 else:
359 365 cg = other.changegroupsubset(incoming, rheads, 'incoming')
360 366 bundletype = localrepo and "HG10BZ" or "HG10UN"
361 367 fname = bundle = changegroup.writebundle(cg, bundlename, bundletype)
362 368 # keep written bundle?
363 369 if bundlename:
364 370 bundle = None
365 371 if not localrepo:
366 372 # use the created uncompressed bundlerepo
367 373 localrepo = bundlerepo = bundlerepository(ui, repo.root, fname)
368 374 # this repo contains local and other now, so filter out local again
369 375 common = repo.heads()
370 376
371 377 csets = localrepo.changelog.findmissing(common, rheads)
372 378
373 379 def cleanup():
374 380 if bundlerepo:
375 381 bundlerepo.close()
376 382 if bundle:
377 383 os.unlink(bundle)
378 384 other.close()
379 385
380 386 return (localrepo, csets, cleanup)
General Comments 0
You need to be logged in to leave comments. Login now