##// END OF EJS Templates
streamclone: use read()...
Gregory Szorc -
r27632:9fea6b38 default
parent child Browse files
Show More
@@ -1,392 +1,387 b''
1 # streamclone.py - producing and consuming streaming repository data
1 # streamclone.py - producing and consuming streaming repository data
2 #
2 #
3 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import struct
10 import struct
11 import time
11 import time
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 branchmap,
15 branchmap,
16 error,
16 error,
17 store,
17 store,
18 util,
18 util,
19 )
19 )
20
20
21 def canperformstreamclone(pullop, bailifbundle2supported=False):
21 def canperformstreamclone(pullop, bailifbundle2supported=False):
22 """Whether it is possible to perform a streaming clone as part of pull.
22 """Whether it is possible to perform a streaming clone as part of pull.
23
23
24 ``bailifbundle2supported`` will cause the function to return False if
24 ``bailifbundle2supported`` will cause the function to return False if
25 bundle2 stream clones are supported. It should only be called by the
25 bundle2 stream clones are supported. It should only be called by the
26 legacy stream clone code path.
26 legacy stream clone code path.
27
27
28 Returns a tuple of (supported, requirements). ``supported`` is True if
28 Returns a tuple of (supported, requirements). ``supported`` is True if
29 streaming clone is supported and False otherwise. ``requirements`` is
29 streaming clone is supported and False otherwise. ``requirements`` is
30 a set of repo requirements from the remote, or ``None`` if stream clone
30 a set of repo requirements from the remote, or ``None`` if stream clone
31 isn't supported.
31 isn't supported.
32 """
32 """
33 repo = pullop.repo
33 repo = pullop.repo
34 remote = pullop.remote
34 remote = pullop.remote
35
35
36 bundle2supported = False
36 bundle2supported = False
37 if pullop.canusebundle2:
37 if pullop.canusebundle2:
38 if 'v1' in pullop.remotebundle2caps.get('stream', []):
38 if 'v1' in pullop.remotebundle2caps.get('stream', []):
39 bundle2supported = True
39 bundle2supported = True
40 # else
40 # else
41 # Server doesn't support bundle2 stream clone or doesn't support
41 # Server doesn't support bundle2 stream clone or doesn't support
42 # the versions we support. Fall back and possibly allow legacy.
42 # the versions we support. Fall back and possibly allow legacy.
43
43
44 # Ensures legacy code path uses available bundle2.
44 # Ensures legacy code path uses available bundle2.
45 if bailifbundle2supported and bundle2supported:
45 if bailifbundle2supported and bundle2supported:
46 return False, None
46 return False, None
47 # Ensures bundle2 doesn't try to do a stream clone if it isn't supported.
47 # Ensures bundle2 doesn't try to do a stream clone if it isn't supported.
48 #elif not bailifbundle2supported and not bundle2supported:
48 #elif not bailifbundle2supported and not bundle2supported:
49 # return False, None
49 # return False, None
50
50
51 # Streaming clone only works on empty repositories.
51 # Streaming clone only works on empty repositories.
52 if len(repo):
52 if len(repo):
53 return False, None
53 return False, None
54
54
55 # Streaming clone only works if all data is being requested.
55 # Streaming clone only works if all data is being requested.
56 if pullop.heads:
56 if pullop.heads:
57 return False, None
57 return False, None
58
58
59 streamrequested = pullop.streamclonerequested
59 streamrequested = pullop.streamclonerequested
60
60
61 # If we don't have a preference, let the server decide for us. This
61 # If we don't have a preference, let the server decide for us. This
62 # likely only comes into play in LANs.
62 # likely only comes into play in LANs.
63 if streamrequested is None:
63 if streamrequested is None:
64 # The server can advertise whether to prefer streaming clone.
64 # The server can advertise whether to prefer streaming clone.
65 streamrequested = remote.capable('stream-preferred')
65 streamrequested = remote.capable('stream-preferred')
66
66
67 if not streamrequested:
67 if not streamrequested:
68 return False, None
68 return False, None
69
69
70 # In order for stream clone to work, the client has to support all the
70 # In order for stream clone to work, the client has to support all the
71 # requirements advertised by the server.
71 # requirements advertised by the server.
72 #
72 #
73 # The server advertises its requirements via the "stream" and "streamreqs"
73 # The server advertises its requirements via the "stream" and "streamreqs"
74 # capability. "stream" (a value-less capability) is advertised if and only
74 # capability. "stream" (a value-less capability) is advertised if and only
75 # if the only requirement is "revlogv1." Else, the "streamreqs" capability
75 # if the only requirement is "revlogv1." Else, the "streamreqs" capability
76 # is advertised and contains a comma-delimited list of requirements.
76 # is advertised and contains a comma-delimited list of requirements.
77 requirements = set()
77 requirements = set()
78 if remote.capable('stream'):
78 if remote.capable('stream'):
79 requirements.add('revlogv1')
79 requirements.add('revlogv1')
80 else:
80 else:
81 streamreqs = remote.capable('streamreqs')
81 streamreqs = remote.capable('streamreqs')
82 # This is weird and shouldn't happen with modern servers.
82 # This is weird and shouldn't happen with modern servers.
83 if not streamreqs:
83 if not streamreqs:
84 return False, None
84 return False, None
85
85
86 streamreqs = set(streamreqs.split(','))
86 streamreqs = set(streamreqs.split(','))
87 # Server requires something we don't support. Bail.
87 # Server requires something we don't support. Bail.
88 if streamreqs - repo.supportedformats:
88 if streamreqs - repo.supportedformats:
89 return False, None
89 return False, None
90 requirements = streamreqs
90 requirements = streamreqs
91
91
92 return True, requirements
92 return True, requirements
93
93
94 def maybeperformlegacystreamclone(pullop):
94 def maybeperformlegacystreamclone(pullop):
95 """Possibly perform a legacy stream clone operation.
95 """Possibly perform a legacy stream clone operation.
96
96
97 Legacy stream clones are performed as part of pull but before all other
97 Legacy stream clones are performed as part of pull but before all other
98 operations.
98 operations.
99
99
100 A legacy stream clone will not be performed if a bundle2 stream clone is
100 A legacy stream clone will not be performed if a bundle2 stream clone is
101 supported.
101 supported.
102 """
102 """
103 supported, requirements = canperformstreamclone(pullop)
103 supported, requirements = canperformstreamclone(pullop)
104
104
105 if not supported:
105 if not supported:
106 return
106 return
107
107
108 repo = pullop.repo
108 repo = pullop.repo
109 remote = pullop.remote
109 remote = pullop.remote
110
110
111 # Save remote branchmap. We will use it later to speed up branchcache
111 # Save remote branchmap. We will use it later to speed up branchcache
112 # creation.
112 # creation.
113 rbranchmap = None
113 rbranchmap = None
114 if remote.capable('branchmap'):
114 if remote.capable('branchmap'):
115 rbranchmap = remote.branchmap()
115 rbranchmap = remote.branchmap()
116
116
117 repo.ui.status(_('streaming all changes\n'))
117 repo.ui.status(_('streaming all changes\n'))
118
118
119 fp = remote.stream_out()
119 fp = remote.stream_out()
120 l = fp.readline()
120 l = fp.readline()
121 try:
121 try:
122 resp = int(l)
122 resp = int(l)
123 except ValueError:
123 except ValueError:
124 raise error.ResponseError(
124 raise error.ResponseError(
125 _('unexpected response from remote server:'), l)
125 _('unexpected response from remote server:'), l)
126 if resp == 1:
126 if resp == 1:
127 raise error.Abort(_('operation forbidden by server'))
127 raise error.Abort(_('operation forbidden by server'))
128 elif resp == 2:
128 elif resp == 2:
129 raise error.Abort(_('locking the remote repository failed'))
129 raise error.Abort(_('locking the remote repository failed'))
130 elif resp != 0:
130 elif resp != 0:
131 raise error.Abort(_('the server sent an unknown error code'))
131 raise error.Abort(_('the server sent an unknown error code'))
132
132
133 l = fp.readline()
133 l = fp.readline()
134 try:
134 try:
135 filecount, bytecount = map(int, l.split(' ', 1))
135 filecount, bytecount = map(int, l.split(' ', 1))
136 except (ValueError, TypeError):
136 except (ValueError, TypeError):
137 raise error.ResponseError(
137 raise error.ResponseError(
138 _('unexpected response from remote server:'), l)
138 _('unexpected response from remote server:'), l)
139
139
140 lock = repo.lock()
140 lock = repo.lock()
141 try:
141 try:
142 consumev1(repo, fp, filecount, bytecount)
142 consumev1(repo, fp, filecount, bytecount)
143
143
144 # new requirements = old non-format requirements +
144 # new requirements = old non-format requirements +
145 # new format-related remote requirements
145 # new format-related remote requirements
146 # requirements from the streamed-in repository
146 # requirements from the streamed-in repository
147 repo.requirements = requirements | (
147 repo.requirements = requirements | (
148 repo.requirements - repo.supportedformats)
148 repo.requirements - repo.supportedformats)
149 repo._applyopenerreqs()
149 repo._applyopenerreqs()
150 repo._writerequirements()
150 repo._writerequirements()
151
151
152 if rbranchmap:
152 if rbranchmap:
153 branchmap.replacecache(repo, rbranchmap)
153 branchmap.replacecache(repo, rbranchmap)
154
154
155 repo.invalidate()
155 repo.invalidate()
156 finally:
156 finally:
157 lock.release()
157 lock.release()
158
158
159 def allowservergeneration(ui):
159 def allowservergeneration(ui):
160 """Whether streaming clones are allowed from the server."""
160 """Whether streaming clones are allowed from the server."""
161 return ui.configbool('server', 'uncompressed', True, untrusted=True)
161 return ui.configbool('server', 'uncompressed', True, untrusted=True)
162
162
163 # This is it's own function so extensions can override it.
163 # This is it's own function so extensions can override it.
164 def _walkstreamfiles(repo):
164 def _walkstreamfiles(repo):
165 return repo.store.walk()
165 return repo.store.walk()
166
166
167 def generatev1(repo):
167 def generatev1(repo):
168 """Emit content for version 1 of a streaming clone.
168 """Emit content for version 1 of a streaming clone.
169
169
170 This returns a 3-tuple of (file count, byte size, data iterator).
170 This returns a 3-tuple of (file count, byte size, data iterator).
171
171
172 The data iterator consists of N entries for each file being transferred.
172 The data iterator consists of N entries for each file being transferred.
173 Each file entry starts as a line with the file name and integer size
173 Each file entry starts as a line with the file name and integer size
174 delimited by a null byte.
174 delimited by a null byte.
175
175
176 The raw file data follows. Following the raw file data is the next file
176 The raw file data follows. Following the raw file data is the next file
177 entry, or EOF.
177 entry, or EOF.
178
178
179 When used on the wire protocol, an additional line indicating protocol
179 When used on the wire protocol, an additional line indicating protocol
180 success will be prepended to the stream. This function is not responsible
180 success will be prepended to the stream. This function is not responsible
181 for adding it.
181 for adding it.
182
182
183 This function will obtain a repository lock to ensure a consistent view of
183 This function will obtain a repository lock to ensure a consistent view of
184 the store is captured. It therefore may raise LockError.
184 the store is captured. It therefore may raise LockError.
185 """
185 """
186 entries = []
186 entries = []
187 total_bytes = 0
187 total_bytes = 0
188 # Get consistent snapshot of repo, lock during scan.
188 # Get consistent snapshot of repo, lock during scan.
189 lock = repo.lock()
189 lock = repo.lock()
190 try:
190 try:
191 repo.ui.debug('scanning\n')
191 repo.ui.debug('scanning\n')
192 for name, ename, size in _walkstreamfiles(repo):
192 for name, ename, size in _walkstreamfiles(repo):
193 if size:
193 if size:
194 entries.append((name, size))
194 entries.append((name, size))
195 total_bytes += size
195 total_bytes += size
196 finally:
196 finally:
197 lock.release()
197 lock.release()
198
198
199 repo.ui.debug('%d files, %d bytes to transfer\n' %
199 repo.ui.debug('%d files, %d bytes to transfer\n' %
200 (len(entries), total_bytes))
200 (len(entries), total_bytes))
201
201
202 svfs = repo.svfs
202 svfs = repo.svfs
203 oldaudit = svfs.mustaudit
203 oldaudit = svfs.mustaudit
204 debugflag = repo.ui.debugflag
204 debugflag = repo.ui.debugflag
205 svfs.mustaudit = False
205 svfs.mustaudit = False
206
206
207 def emitrevlogdata():
207 def emitrevlogdata():
208 try:
208 try:
209 for name, size in entries:
209 for name, size in entries:
210 if debugflag:
210 if debugflag:
211 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
211 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
212 # partially encode name over the wire for backwards compat
212 # partially encode name over the wire for backwards compat
213 yield '%s\0%d\n' % (store.encodedir(name), size)
213 yield '%s\0%d\n' % (store.encodedir(name), size)
214 if size <= 65536:
214 if size <= 65536:
215 fp = svfs(name)
215 yield svfs.read(name)
216 try:
217 data = fp.read(size)
218 finally:
219 fp.close()
220 yield data
221 else:
216 else:
222 for chunk in util.filechunkiter(svfs(name), limit=size):
217 for chunk in util.filechunkiter(svfs(name), limit=size):
223 yield chunk
218 yield chunk
224 finally:
219 finally:
225 svfs.mustaudit = oldaudit
220 svfs.mustaudit = oldaudit
226
221
227 return len(entries), total_bytes, emitrevlogdata()
222 return len(entries), total_bytes, emitrevlogdata()
228
223
229 def generatev1wireproto(repo):
224 def generatev1wireproto(repo):
230 """Emit content for version 1 of streaming clone suitable for the wire.
225 """Emit content for version 1 of streaming clone suitable for the wire.
231
226
232 This is the data output from ``generatev1()`` with a header line
227 This is the data output from ``generatev1()`` with a header line
233 indicating file count and byte size.
228 indicating file count and byte size.
234 """
229 """
235 filecount, bytecount, it = generatev1(repo)
230 filecount, bytecount, it = generatev1(repo)
236 yield '%d %d\n' % (filecount, bytecount)
231 yield '%d %d\n' % (filecount, bytecount)
237 for chunk in it:
232 for chunk in it:
238 yield chunk
233 yield chunk
239
234
240 def generatebundlev1(repo, compression='UN'):
235 def generatebundlev1(repo, compression='UN'):
241 """Emit content for version 1 of a stream clone bundle.
236 """Emit content for version 1 of a stream clone bundle.
242
237
243 The first 4 bytes of the output ("HGS1") denote this as stream clone
238 The first 4 bytes of the output ("HGS1") denote this as stream clone
244 bundle version 1.
239 bundle version 1.
245
240
246 The next 2 bytes indicate the compression type. Only "UN" is currently
241 The next 2 bytes indicate the compression type. Only "UN" is currently
247 supported.
242 supported.
248
243
249 The next 16 bytes are two 64-bit big endian unsigned integers indicating
244 The next 16 bytes are two 64-bit big endian unsigned integers indicating
250 file count and byte count, respectively.
245 file count and byte count, respectively.
251
246
252 The next 2 bytes is a 16-bit big endian unsigned short declaring the length
247 The next 2 bytes is a 16-bit big endian unsigned short declaring the length
253 of the requirements string, including a trailing \0. The following N bytes
248 of the requirements string, including a trailing \0. The following N bytes
254 are the requirements string, which is ASCII containing a comma-delimited
249 are the requirements string, which is ASCII containing a comma-delimited
255 list of repo requirements that are needed to support the data.
250 list of repo requirements that are needed to support the data.
256
251
257 The remaining content is the output of ``generatev1()`` (which may be
252 The remaining content is the output of ``generatev1()`` (which may be
258 compressed in the future).
253 compressed in the future).
259
254
260 Returns a tuple of (requirements, data generator).
255 Returns a tuple of (requirements, data generator).
261 """
256 """
262 if compression != 'UN':
257 if compression != 'UN':
263 raise ValueError('we do not support the compression argument yet')
258 raise ValueError('we do not support the compression argument yet')
264
259
265 requirements = repo.requirements & repo.supportedformats
260 requirements = repo.requirements & repo.supportedformats
266 requires = ','.join(sorted(requirements))
261 requires = ','.join(sorted(requirements))
267
262
268 def gen():
263 def gen():
269 yield 'HGS1'
264 yield 'HGS1'
270 yield compression
265 yield compression
271
266
272 filecount, bytecount, it = generatev1(repo)
267 filecount, bytecount, it = generatev1(repo)
273 repo.ui.status(_('writing %d bytes for %d files\n') %
268 repo.ui.status(_('writing %d bytes for %d files\n') %
274 (bytecount, filecount))
269 (bytecount, filecount))
275
270
276 yield struct.pack('>QQ', filecount, bytecount)
271 yield struct.pack('>QQ', filecount, bytecount)
277 yield struct.pack('>H', len(requires) + 1)
272 yield struct.pack('>H', len(requires) + 1)
278 yield requires + '\0'
273 yield requires + '\0'
279
274
280 # This is where we'll add compression in the future.
275 # This is where we'll add compression in the future.
281 assert compression == 'UN'
276 assert compression == 'UN'
282
277
283 seen = 0
278 seen = 0
284 repo.ui.progress(_('bundle'), 0, total=bytecount)
279 repo.ui.progress(_('bundle'), 0, total=bytecount)
285
280
286 for chunk in it:
281 for chunk in it:
287 seen += len(chunk)
282 seen += len(chunk)
288 repo.ui.progress(_('bundle'), seen, total=bytecount)
283 repo.ui.progress(_('bundle'), seen, total=bytecount)
289 yield chunk
284 yield chunk
290
285
291 repo.ui.progress(_('bundle'), None)
286 repo.ui.progress(_('bundle'), None)
292
287
293 return requirements, gen()
288 return requirements, gen()
294
289
295 def consumev1(repo, fp, filecount, bytecount):
290 def consumev1(repo, fp, filecount, bytecount):
296 """Apply the contents from version 1 of a streaming clone file handle.
291 """Apply the contents from version 1 of a streaming clone file handle.
297
292
298 This takes the output from "streamout" and applies it to the specified
293 This takes the output from "streamout" and applies it to the specified
299 repository.
294 repository.
300
295
301 Like "streamout," the status line added by the wire protocol is not handled
296 Like "streamout," the status line added by the wire protocol is not handled
302 by this function.
297 by this function.
303 """
298 """
304 lock = repo.lock()
299 lock = repo.lock()
305 try:
300 try:
306 repo.ui.status(_('%d files to transfer, %s of data\n') %
301 repo.ui.status(_('%d files to transfer, %s of data\n') %
307 (filecount, util.bytecount(bytecount)))
302 (filecount, util.bytecount(bytecount)))
308 handled_bytes = 0
303 handled_bytes = 0
309 repo.ui.progress(_('clone'), 0, total=bytecount)
304 repo.ui.progress(_('clone'), 0, total=bytecount)
310 start = time.time()
305 start = time.time()
311
306
312 tr = repo.transaction(_('clone'))
307 tr = repo.transaction(_('clone'))
313 try:
308 try:
314 for i in xrange(filecount):
309 for i in xrange(filecount):
315 # XXX doesn't support '\n' or '\r' in filenames
310 # XXX doesn't support '\n' or '\r' in filenames
316 l = fp.readline()
311 l = fp.readline()
317 try:
312 try:
318 name, size = l.split('\0', 1)
313 name, size = l.split('\0', 1)
319 size = int(size)
314 size = int(size)
320 except (ValueError, TypeError):
315 except (ValueError, TypeError):
321 raise error.ResponseError(
316 raise error.ResponseError(
322 _('unexpected response from remote server:'), l)
317 _('unexpected response from remote server:'), l)
323 if repo.ui.debugflag:
318 if repo.ui.debugflag:
324 repo.ui.debug('adding %s (%s)\n' %
319 repo.ui.debug('adding %s (%s)\n' %
325 (name, util.bytecount(size)))
320 (name, util.bytecount(size)))
326 # for backwards compat, name was partially encoded
321 # for backwards compat, name was partially encoded
327 ofp = repo.svfs(store.decodedir(name), 'w')
322 ofp = repo.svfs(store.decodedir(name), 'w')
328 for chunk in util.filechunkiter(fp, limit=size):
323 for chunk in util.filechunkiter(fp, limit=size):
329 handled_bytes += len(chunk)
324 handled_bytes += len(chunk)
330 repo.ui.progress(_('clone'), handled_bytes, total=bytecount)
325 repo.ui.progress(_('clone'), handled_bytes, total=bytecount)
331 ofp.write(chunk)
326 ofp.write(chunk)
332 ofp.close()
327 ofp.close()
333 tr.close()
328 tr.close()
334 finally:
329 finally:
335 tr.release()
330 tr.release()
336
331
337 # Writing straight to files circumvented the inmemory caches
332 # Writing straight to files circumvented the inmemory caches
338 repo.invalidate()
333 repo.invalidate()
339
334
340 elapsed = time.time() - start
335 elapsed = time.time() - start
341 if elapsed <= 0:
336 if elapsed <= 0:
342 elapsed = 0.001
337 elapsed = 0.001
343 repo.ui.progress(_('clone'), None)
338 repo.ui.progress(_('clone'), None)
344 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
339 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
345 (util.bytecount(bytecount), elapsed,
340 (util.bytecount(bytecount), elapsed,
346 util.bytecount(bytecount / elapsed)))
341 util.bytecount(bytecount / elapsed)))
347 finally:
342 finally:
348 lock.release()
343 lock.release()
349
344
350 def applybundlev1(repo, fp):
345 def applybundlev1(repo, fp):
351 """Apply the content from a stream clone bundle version 1.
346 """Apply the content from a stream clone bundle version 1.
352
347
353 We assume the 4 byte header has been read and validated and the file handle
348 We assume the 4 byte header has been read and validated and the file handle
354 is at the 2 byte compression identifier.
349 is at the 2 byte compression identifier.
355 """
350 """
356 if len(repo):
351 if len(repo):
357 raise error.Abort(_('cannot apply stream clone bundle on non-empty '
352 raise error.Abort(_('cannot apply stream clone bundle on non-empty '
358 'repo'))
353 'repo'))
359
354
360 compression = fp.read(2)
355 compression = fp.read(2)
361 if compression != 'UN':
356 if compression != 'UN':
362 raise error.Abort(_('only uncompressed stream clone bundles are '
357 raise error.Abort(_('only uncompressed stream clone bundles are '
363 'supported; got %s') % compression)
358 'supported; got %s') % compression)
364
359
365 filecount, bytecount = struct.unpack('>QQ', fp.read(16))
360 filecount, bytecount = struct.unpack('>QQ', fp.read(16))
366 requireslen = struct.unpack('>H', fp.read(2))[0]
361 requireslen = struct.unpack('>H', fp.read(2))[0]
367 requires = fp.read(requireslen)
362 requires = fp.read(requireslen)
368
363
369 if not requires.endswith('\0'):
364 if not requires.endswith('\0'):
370 raise error.Abort(_('malformed stream clone bundle: '
365 raise error.Abort(_('malformed stream clone bundle: '
371 'requirements not properly encoded'))
366 'requirements not properly encoded'))
372
367
373 requirements = set(requires.rstrip('\0').split(','))
368 requirements = set(requires.rstrip('\0').split(','))
374 missingreqs = requirements - repo.supportedformats
369 missingreqs = requirements - repo.supportedformats
375 if missingreqs:
370 if missingreqs:
376 raise error.Abort(_('unable to apply stream clone: '
371 raise error.Abort(_('unable to apply stream clone: '
377 'unsupported format: %s') %
372 'unsupported format: %s') %
378 ', '.join(sorted(missingreqs)))
373 ', '.join(sorted(missingreqs)))
379
374
380 consumev1(repo, fp, filecount, bytecount)
375 consumev1(repo, fp, filecount, bytecount)
381
376
382 class streamcloneapplier(object):
377 class streamcloneapplier(object):
383 """Class to manage applying streaming clone bundles.
378 """Class to manage applying streaming clone bundles.
384
379
385 We need to wrap ``applybundlev1()`` in a dedicated type to enable bundle
380 We need to wrap ``applybundlev1()`` in a dedicated type to enable bundle
386 readers to perform bundle type-specific functionality.
381 readers to perform bundle type-specific functionality.
387 """
382 """
388 def __init__(self, fh):
383 def __init__(self, fh):
389 self._fh = fh
384 self._fh = fh
390
385
391 def apply(self, repo):
386 def apply(self, repo):
392 return applybundlev1(repo, self._fh)
387 return applybundlev1(repo, self._fh)
General Comments 0
You need to be logged in to leave comments. Login now