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