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