Merge lp:~jelmer/brz/hpss-archive into lp:brz
- hpss-archive
- Merge into trunk
Proposed by
Jelmer Vernooij
Status: | Merged |
---|---|
Merged at revision: | 6994 |
Proposed branch: | lp:~jelmer/brz/hpss-archive |
Merge into: | lp:brz |
Prerequisite: | lp:~jelmer/brz/tree-archive |
Diff against target: |
1333 lines (+513/-511) 15 files modified
breezy/archive/__init__.py (+103/-0) breezy/archive/tar.py (+88/-114) breezy/archive/zip.py (+49/-55) breezy/builtins.py (+12/-17) breezy/bzr/remote.py (+35/-1) breezy/bzr/smart/repository.py (+24/-0) breezy/bzr/smart/request.py (+3/-0) breezy/export.py (+120/-202) breezy/export/dir_exporter.py (+0/-93) breezy/tests/blackbox/test_export.py (+4/-3) breezy/tests/per_tree/test_archive.py (+8/-9) breezy/tests/test_export.py (+3/-13) breezy/tests/test_remote.py (+37/-0) breezy/tests/test_smart.py (+20/-0) breezy/tree.py (+7/-4) |
To merge this branch: | bzr merge lp:~jelmer/brz/hpss-archive |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Packman | Approve | ||
Jelmer Vernooij | Pending | ||
Review via email: mp+345968@code.launchpad.net |
Commit message
Add a HPSS implementation of Tree.archive.
Description of the change
Add a HPSS implementation of Tree.archive.
This very significantly speeds up 'bzr export' operations of remote trees.
This is useful for e.g. 'brz merge-upstream', especially on large trees.
To post a comment you must log in.
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote : | # |
Revision history for this message
Martin Packman (gz) wrote : | # |
One inline comment right at the end.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'breezy/archive' |
2 | === added file 'breezy/archive/__init__.py' |
3 | --- breezy/archive/__init__.py 1970-01-01 00:00:00 +0000 |
4 | +++ breezy/archive/__init__.py 2018-05-20 16:34:17 +0000 |
5 | @@ -0,0 +1,103 @@ |
6 | +# Copyright (C) 2018 Breezy Developers |
7 | +# |
8 | +# This program is free software; you can redistribute it and/or modify |
9 | +# it under the terms of the GNU General Public License as published by |
10 | +# the Free Software Foundation; either version 2 of the License, or |
11 | +# (at your option) any later version. |
12 | +# |
13 | +# This program is distributed in the hope that it will be useful, |
14 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | +# GNU General Public License for more details. |
17 | +# |
18 | +# You should have received a copy of the GNU General Public License |
19 | +# along with this program; if not, write to the Free Software |
20 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
21 | + |
22 | +"""Export trees to tarballs, zipfiles, etc. |
23 | +""" |
24 | + |
25 | +from __future__ import absolute_import |
26 | + |
27 | +import os |
28 | +import time |
29 | +import warnings |
30 | + |
31 | +from .. import ( |
32 | + errors, |
33 | + pyutils, |
34 | + registry, |
35 | + trace, |
36 | + ) |
37 | + |
38 | + |
39 | +class ArchiveFormatInfo(object): |
40 | + |
41 | + def __init__(self, extensions): |
42 | + self.extensions = extensions |
43 | + |
44 | + |
45 | +class ArchiveFormatRegistry(registry.Registry): |
46 | + """Registry of archive formats.""" |
47 | + |
48 | + def __init__(self): |
49 | + self._extension_map = {} |
50 | + super(ArchiveFormatRegistry, self).__init__() |
51 | + |
52 | + @property |
53 | + def extensions(self): |
54 | + return self._extension_map.keys() |
55 | + |
56 | + def register(self, key, factory, extensions, help=None): |
57 | + """Register an archive format. |
58 | + """ |
59 | + registry.Registry.register(self, key, factory, help, |
60 | + ArchiveFormatInfo(extensions)) |
61 | + self._register_extensions(key, extensions) |
62 | + |
63 | + def register_lazy(self, key, module_name, member_name, extensions, |
64 | + help=None): |
65 | + registry.Registry.register_lazy(self, key, module_name, member_name, |
66 | + help, ArchiveFormatInfo(extensions)) |
67 | + self._register_extensions(key, extensions) |
68 | + |
69 | + def _register_extensions(self, name, extensions): |
70 | + for ext in extensions: |
71 | + self._extension_map[ext] = name |
72 | + |
73 | + def get_format_from_filename(self, filename): |
74 | + """Determine the archive format from an extension. |
75 | + |
76 | + :param filename: Filename to guess from |
77 | + :return: A format name, or None |
78 | + """ |
79 | + for ext, format in self._extension_map.items(): |
80 | + if filename.endswith(ext): |
81 | + return format |
82 | + else: |
83 | + return None |
84 | + |
85 | + |
86 | +def create_archive(format, tree, name, root=None, subdir=None, |
87 | + force_mtime=None): |
88 | + try: |
89 | + archive_fn = format_registry.get(format) |
90 | + except KeyError: |
91 | + raise errors.NoSuchExportFormat(format) |
92 | + return archive_fn(tree, name, root=root, subdir=subdir, |
93 | + force_mtime=force_mtime) |
94 | + |
95 | + |
96 | +format_registry = ArchiveFormatRegistry() |
97 | +format_registry.register_lazy('tar', 'breezy.archive.tar', |
98 | + 'plain_tar_generator', ['.tar'], ) |
99 | +format_registry.register_lazy('tgz', 'breezy.archive.tar', |
100 | + 'tgz_generator', ['.tar.gz', '.tgz']) |
101 | +format_registry.register_lazy('tbz2', 'breezy.archive.tar', |
102 | + 'tbz_generator', ['.tar.bz2', '.tbz2']) |
103 | +format_registry.register_lazy('tlzma', 'breezy.archive.tar', |
104 | + 'tar_lzma_generator', ['.tar.lzma']) |
105 | +format_registry.register_lazy('txz', 'breezy.archive.tar', |
106 | + 'tar_xz_generator', ['.tar.xz']) |
107 | +format_registry.register_lazy('zip', 'breezy.archive.zip', |
108 | + 'zip_archive_generator', ['.zip']) |
109 | |
110 | === renamed file 'breezy/export/tar_exporter.py' => 'breezy/archive/tar.py' |
111 | --- breezy/export/tar_exporter.py 2018-05-20 16:34:17 +0000 |
112 | +++ breezy/archive/tar.py 2018-05-20 16:34:17 +0000 |
113 | @@ -18,6 +18,7 @@ |
114 | |
115 | from __future__ import absolute_import |
116 | |
117 | +from contextlib import closing |
118 | import os |
119 | import sys |
120 | import tarfile |
121 | @@ -81,141 +82,114 @@ |
122 | return (item, fileobj) |
123 | |
124 | |
125 | -def export_tarball_generator(tree, ball, root, subdir=None, force_mtime=None): |
126 | +def tarball_generator(tree, root, subdir=None, force_mtime=None, format=''): |
127 | """Export tree contents to a tarball. |
128 | |
129 | - :returns: A generator that will repeatedly produce None as each file is |
130 | - emitted. The entire generator must be consumed to complete writing |
131 | - the file. |
132 | + :returns: A generator that will produce file content chunks. |
133 | |
134 | :param tree: Tree to export |
135 | |
136 | - :param ball: Tarball to export to; it will be closed when writing is |
137 | - complete. |
138 | - |
139 | :param subdir: Sub directory to export |
140 | |
141 | :param force_mtime: Option mtime to force, instead of using tree |
142 | timestamps. |
143 | """ |
144 | - try: |
145 | + buf = BytesIO() |
146 | + with closing(tarfile.open(None, "w:%s" % format, buf)) as ball, tree.lock_read(): |
147 | for final_path, tree_path, entry in _export_iter_entries(tree, subdir): |
148 | (item, fileobj) = prepare_tarball_item( |
149 | tree, root, final_path, tree_path, entry, force_mtime) |
150 | ball.addfile(item, fileobj) |
151 | - yield |
152 | - finally: |
153 | - ball.close() |
154 | - |
155 | - |
156 | -def tgz_exporter_generator(tree, dest, root, subdir, force_mtime=None, |
157 | - fileobj=None): |
158 | - """Export this tree to a new tar file. |
159 | - |
160 | - `dest` will be created holding the contents of this tree; if it |
161 | - already exists, it will be clobbered, like with "tar -c". |
162 | - """ |
163 | - import gzip |
164 | - if force_mtime is not None: |
165 | - root_mtime = force_mtime |
166 | - elif (getattr(tree, "repository", None) and |
167 | - getattr(tree, "get_revision_id", None)): |
168 | - # If this is a revision tree, use the revisions' timestamp |
169 | - rev = tree.repository.get_revision(tree.get_revision_id()) |
170 | - root_mtime = rev.timestamp |
171 | - elif tree.get_root_id() is not None: |
172 | - root_mtime = tree.get_file_mtime('', tree.get_root_id()) |
173 | - else: |
174 | - root_mtime = None |
175 | - |
176 | - is_stdout = False |
177 | - basename = None |
178 | - if fileobj is not None: |
179 | - stream = fileobj |
180 | - elif dest == '-': |
181 | - stream = sys.stdout |
182 | - is_stdout = True |
183 | - else: |
184 | - stream = open(dest, 'wb') |
185 | - # gzip file is used with an explicit fileobj so that |
186 | - # the basename can be stored in the gzip file rather than |
187 | - # dest. (bug 102234) |
188 | - basename = os.path.basename(dest) |
189 | - zipstream = gzip.GzipFile(basename, 'w', fileobj=stream, |
190 | - mtime=root_mtime) |
191 | - ball = tarfile.open(None, 'w|', fileobj=zipstream) |
192 | - for _ in export_tarball_generator( |
193 | - tree, ball, root, subdir, force_mtime): |
194 | - yield |
195 | - # Closing zipstream may trigger writes to stream |
196 | - zipstream.close() |
197 | - if not is_stdout: |
198 | - # Now we can safely close the stream |
199 | - stream.close() |
200 | - |
201 | - |
202 | -def tbz_exporter_generator(tree, dest, root, subdir, |
203 | - force_mtime=None, fileobj=None): |
204 | - """Export this tree to a new tar file. |
205 | - |
206 | - `dest` will be created holding the contents of this tree; if it |
207 | - already exists, it will be clobbered, like with "tar -c". |
208 | - """ |
209 | - if fileobj is not None: |
210 | - ball = tarfile.open(None, 'w|bz2', fileobj) |
211 | - elif dest == '-': |
212 | - ball = tarfile.open(None, 'w|bz2', sys.stdout) |
213 | - else: |
214 | - ball = tarfile.open(dest, 'w:bz2') |
215 | - return export_tarball_generator( |
216 | - tree, ball, root, subdir, force_mtime) |
217 | - |
218 | - |
219 | -def plain_tar_exporter_generator(tree, dest, root, subdir, compression=None, |
220 | - force_mtime=None, fileobj=None): |
221 | - """Export this tree to a new tar file. |
222 | - |
223 | - `dest` will be created holding the contents of this tree; if it |
224 | - already exists, it will be clobbered, like with "tar -c". |
225 | - """ |
226 | - if fileobj is not None: |
227 | - stream = fileobj |
228 | - elif dest == '-': |
229 | - stream = sys.stdout |
230 | - else: |
231 | - stream = open(dest, 'wb') |
232 | - ball = tarfile.open(None, 'w|', stream) |
233 | - return export_tarball_generator( |
234 | - tree, ball, root, subdir, force_mtime) |
235 | - |
236 | - |
237 | -def tar_xz_exporter_generator(tree, dest, root, subdir, |
238 | - force_mtime=None, fileobj=None): |
239 | - return tar_lzma_exporter_generator(tree, dest, root, subdir, |
240 | - force_mtime, fileobj, "xz") |
241 | - |
242 | - |
243 | -def tar_lzma_exporter_generator(tree, dest, root, subdir, |
244 | - force_mtime=None, fileobj=None, |
245 | - compression_format="alone"): |
246 | + # Yield the data that was written so far, rinse, repeat. |
247 | + yield buf.getvalue() |
248 | + buf.truncate(0) |
249 | + buf.seek(0) |
250 | + yield buf.getvalue() |
251 | + |
252 | + |
253 | +def tgz_generator(tree, dest, root, subdir, force_mtime=None): |
254 | + """Export this tree to a new tar file. |
255 | + |
256 | + `dest` will be created holding the contents of this tree; if it |
257 | + already exists, it will be clobbered, like with "tar -c". |
258 | + """ |
259 | + with tree.lock_read(): |
260 | + import gzip |
261 | + if force_mtime is not None: |
262 | + root_mtime = force_mtime |
263 | + elif (getattr(tree, "repository", None) and |
264 | + getattr(tree, "get_revision_id", None)): |
265 | + # If this is a revision tree, use the revisions' timestamp |
266 | + rev = tree.repository.get_revision(tree.get_revision_id()) |
267 | + root_mtime = rev.timestamp |
268 | + elif tree.get_root_id() is not None: |
269 | + root_mtime = tree.get_file_mtime('', tree.get_root_id()) |
270 | + else: |
271 | + root_mtime = None |
272 | + |
273 | + is_stdout = False |
274 | + basename = None |
275 | + # gzip file is used with an explicit fileobj so that |
276 | + # the basename can be stored in the gzip file rather than |
277 | + # dest. (bug 102234) |
278 | + basename = os.path.basename(dest) |
279 | + buf = BytesIO() |
280 | + zipstream = gzip.GzipFile(basename, 'w', fileobj=buf, |
281 | + mtime=root_mtime) |
282 | + for chunk in tarball_generator( |
283 | + tree, root, subdir, force_mtime): |
284 | + zipstream.write(chunk) |
285 | + # Yield the data that was written so far, rinse, repeat. |
286 | + yield buf.getvalue() |
287 | + buf.truncate(0) |
288 | + buf.seek(0) |
289 | + # Closing zipstream may trigger writes to stream |
290 | + zipstream.close() |
291 | + yield buf.getvalue() |
292 | + |
293 | + |
294 | +def tbz_generator(tree, dest, root, subdir, force_mtime=None): |
295 | + """Export this tree to a new tar file. |
296 | + |
297 | + `dest` will be created holding the contents of this tree; if it |
298 | + already exists, it will be clobbered, like with "tar -c". |
299 | + """ |
300 | + return tarball_generator( |
301 | + tree, root, subdir, force_mtime, format='bz2') |
302 | + |
303 | + |
304 | +def plain_tar_generator(tree, dest, root, subdir, |
305 | + force_mtime=None): |
306 | + """Export this tree to a new tar file. |
307 | + |
308 | + `dest` will be created holding the contents of this tree; if it |
309 | + already exists, it will be clobbered, like with "tar -c". |
310 | + """ |
311 | + return tarball_generator( |
312 | + tree, root, subdir, force_mtime, format='') |
313 | + |
314 | + |
315 | +def tar_xz_generator(tree, dest, root, subdir, force_mtime=None): |
316 | + return tar_lzma_generator(tree, dest, root, subdir, force_mtime, "xz") |
317 | + |
318 | + |
319 | +def tar_lzma_generator(tree, dest, root, subdir, force_mtime=None, |
320 | + compression_format="alone"): |
321 | """Export this tree to a new .tar.lzma file. |
322 | |
323 | `dest` will be created holding the contents of this tree; if it |
324 | already exists, it will be clobbered, like with "tar -c". |
325 | """ |
326 | - if dest == '-': |
327 | - raise errors.BzrError("Writing to stdout not supported for .tar.lzma") |
328 | - |
329 | - if fileobj is not None: |
330 | - raise errors.BzrError( |
331 | - "Writing to fileobject not supported for .tar.lzma") |
332 | try: |
333 | import lzma |
334 | except ImportError as e: |
335 | raise errors.DependencyNotPresent('lzma', e) |
336 | |
337 | - stream = lzma.LZMAFile(dest.encode(osutils._fs_enc), 'w', |
338 | - options={"format": compression_format}) |
339 | - ball = tarfile.open(None, 'w:', fileobj=stream) |
340 | - return export_tarball_generator( |
341 | - tree, ball, root, subdir, force_mtime=force_mtime) |
342 | + compressor = lzma.LZMACompressor( |
343 | + options={"format": compression_format}) |
344 | + |
345 | + for chunk in tarball_generator( |
346 | + tree, root, subdir, force_mtime=force_mtime): |
347 | + yield compressor.compress(chunk) |
348 | + |
349 | + yield compressor.flush() |
350 | |
351 | === renamed file 'breezy/export/zip_exporter.py' => 'breezy/archive/zip.py' |
352 | --- breezy/export/zip_exporter.py 2018-05-14 20:49:13 +0000 |
353 | +++ breezy/archive/zip.py 2018-05-20 16:34:17 +0000 |
354 | @@ -19,9 +19,11 @@ |
355 | |
356 | from __future__ import absolute_import |
357 | |
358 | +from contextlib import closing |
359 | import os |
360 | import stat |
361 | import sys |
362 | +import tempfile |
363 | import time |
364 | import zipfile |
365 | |
366 | @@ -42,64 +44,56 @@ |
367 | _DIR_ATTR = stat.S_IFDIR | ZIP_DIRECTORY_BIT | DIR_PERMISSIONS |
368 | |
369 | |
370 | -def zip_exporter_generator(tree, dest, root, subdir=None, |
371 | - force_mtime=None, fileobj=None): |
372 | +def zip_archive_generator(tree, dest, root, subdir=None, |
373 | + force_mtime=None): |
374 | """ Export this tree to a new zip file. |
375 | |
376 | `dest` will be created holding the contents of this tree; if it |
377 | already exists, it will be overwritten". |
378 | """ |
379 | - |
380 | compression = zipfile.ZIP_DEFLATED |
381 | - if fileobj is not None: |
382 | - dest = fileobj |
383 | - elif dest == "-": |
384 | - dest = sys.stdout |
385 | - zipf = zipfile.ZipFile(dest, "w", compression) |
386 | - try: |
387 | - for dp, tp, ie in _export_iter_entries(tree, subdir): |
388 | - file_id = ie.file_id |
389 | - mutter(" export {%s} kind %s to %s", file_id, ie.kind, dest) |
390 | - |
391 | - # zipfile.ZipFile switches all paths to forward |
392 | - # slashes anyway, so just stick with that. |
393 | - if force_mtime is not None: |
394 | - mtime = force_mtime |
395 | - else: |
396 | - mtime = tree.get_file_mtime(tp, ie.file_id) |
397 | - date_time = time.localtime(mtime)[:6] |
398 | - filename = osutils.pathjoin(root, dp).encode('utf8') |
399 | - if ie.kind == "file": |
400 | - zinfo = zipfile.ZipInfo( |
401 | - filename=filename, |
402 | - date_time=date_time) |
403 | - zinfo.compress_type = compression |
404 | - zinfo.external_attr = _FILE_ATTR |
405 | - content = tree.get_file_text(tp, file_id) |
406 | - zipf.writestr(zinfo, content) |
407 | - elif ie.kind in ("directory", "tree-reference"): |
408 | - # Directories must contain a trailing slash, to indicate |
409 | - # to the zip routine that they are really directories and |
410 | - # not just empty files. |
411 | - zinfo = zipfile.ZipInfo( |
412 | - filename=filename + '/', |
413 | - date_time=date_time) |
414 | - zinfo.compress_type = compression |
415 | - zinfo.external_attr = _DIR_ATTR |
416 | - zipf.writestr(zinfo, '') |
417 | - elif ie.kind == "symlink": |
418 | - zinfo = zipfile.ZipInfo( |
419 | - filename=(filename + '.lnk'), |
420 | - date_time=date_time) |
421 | - zinfo.compress_type = compression |
422 | - zinfo.external_attr = _FILE_ATTR |
423 | - zipf.writestr(zinfo, tree.get_symlink_target(tp, file_id)) |
424 | - yield |
425 | - |
426 | - zipf.close() |
427 | - |
428 | - except UnicodeEncodeError: |
429 | - zipf.close() |
430 | - os.remove(dest) |
431 | - from breezy.errors import BzrError |
432 | - raise BzrError("Can't export non-ascii filenames to zip") |
433 | + with tempfile.SpooledTemporaryFile() as buf: |
434 | + with closing(zipfile.ZipFile(buf, "w", compression)) as zipf, \ |
435 | + tree.lock_read(): |
436 | + for dp, tp, ie in _export_iter_entries(tree, subdir): |
437 | + file_id = ie.file_id |
438 | + mutter(" export {%s} kind %s to %s", file_id, ie.kind, dest) |
439 | + |
440 | + # zipfile.ZipFile switches all paths to forward |
441 | + # slashes anyway, so just stick with that. |
442 | + if force_mtime is not None: |
443 | + mtime = force_mtime |
444 | + else: |
445 | + mtime = tree.get_file_mtime(tp, ie.file_id) |
446 | + date_time = time.localtime(mtime)[:6] |
447 | + filename = osutils.pathjoin(root, dp).encode('utf8') |
448 | + if ie.kind == "file": |
449 | + zinfo = zipfile.ZipInfo( |
450 | + filename=filename, |
451 | + date_time=date_time) |
452 | + zinfo.compress_type = compression |
453 | + zinfo.external_attr = _FILE_ATTR |
454 | + content = tree.get_file_text(tp, file_id) |
455 | + zipf.writestr(zinfo, content) |
456 | + elif ie.kind in ("directory", "tree-reference"): |
457 | + # Directories must contain a trailing slash, to indicate |
458 | + # to the zip routine that they are really directories and |
459 | + # not just empty files. |
460 | + zinfo = zipfile.ZipInfo( |
461 | + filename=filename + '/', |
462 | + date_time=date_time) |
463 | + zinfo.compress_type = compression |
464 | + zinfo.external_attr = _DIR_ATTR |
465 | + zipf.writestr(zinfo, '') |
466 | + elif ie.kind == "symlink": |
467 | + zinfo = zipfile.ZipInfo( |
468 | + filename=(filename + '.lnk'), |
469 | + date_time=date_time) |
470 | + zinfo.compress_type = compression |
471 | + zinfo.external_attr = _FILE_ATTR |
472 | + zipf.writestr(zinfo, tree.get_symlink_target(tp, file_id)) |
473 | + # Urgh, headers are written last since they include e.g. file size. |
474 | + # So we have to buffer it all :( |
475 | + buf.seek(0) |
476 | + for chunk in osutils.file_iterator(buf): |
477 | + yield chunk |
478 | |
479 | === modified file 'breezy/builtins.py' |
480 | --- breezy/builtins.py 2018-05-20 16:34:17 +0000 |
481 | +++ breezy/builtins.py 2018-05-20 16:34:17 +0000 |
482 | @@ -3311,7 +3311,7 @@ |
483 | def run(self, dest, branch_or_subdir=None, revision=None, format=None, |
484 | root=None, filters=False, per_file_timestamps=False, uncommitted=False, |
485 | directory=u'.'): |
486 | - from .export import export |
487 | + from .export import export, guess_format, get_root_name |
488 | |
489 | if branch_or_subdir is None: |
490 | branch_or_subdir = directory |
491 | @@ -3331,27 +3331,22 @@ |
492 | 'export', revision, branch=b, |
493 | tree=tree) |
494 | |
495 | + if format is None: |
496 | + format = guess_format(dest) |
497 | + |
498 | + if root is None: |
499 | + root = get_root_name(dest) |
500 | + |
501 | + if not per_file_timestamps: |
502 | + force_mtime = time.time() |
503 | + else: |
504 | + force_mtime = None |
505 | + |
506 | if filters: |
507 | from breezy.filter_tree import ContentFilterTree |
508 | export_tree = ContentFilterTree( |
509 | export_tree, export_tree._content_filter_stack) |
510 | |
511 | - # Try asking the tree first.. |
512 | - if not per_file_timestamps: |
513 | - chunks = export_tree.archive( |
514 | - dest, format, root=root, subdir=subdir) |
515 | - try: |
516 | - if dest == '-': |
517 | - self.outf.writelines(chunks) |
518 | - else: |
519 | - import tempfile |
520 | - with tempfile.NamedTemporaryFile(delete=False) as temp: |
521 | - temp.writelines(chunks) |
522 | - os.rename(temp.name, dest) |
523 | - except errors.NoSuchExportFormat: |
524 | - pass |
525 | - else: |
526 | - return |
527 | try: |
528 | export(export_tree, dest, format, root, subdir, |
529 | per_file_timestamps=per_file_timestamps) |
530 | |
531 | === modified file 'breezy/bzr/remote.py' |
532 | --- breezy/bzr/remote.py 2018-05-19 17:31:48 +0000 |
533 | +++ breezy/bzr/remote.py 2018-05-20 16:34:17 +0000 |
534 | @@ -17,6 +17,7 @@ |
535 | from __future__ import absolute_import |
536 | |
537 | import bz2 |
538 | +import os |
539 | import zlib |
540 | |
541 | from .. import ( |
542 | @@ -921,6 +922,20 @@ |
543 | return RemoteControlStore(self) |
544 | |
545 | |
546 | +class RemoteInventoryTree(InventoryRevisionTree): |
547 | + |
548 | + def __init__(self, repository, inv, revision_id): |
549 | + super(RemoteInventoryTree, self).__init__(repository, inv, revision_id) |
550 | + |
551 | + def archive(self, name, format=None, root=None, subdir=None): |
552 | + ret = self._repository._revision_archive( |
553 | + self.get_revision_id(), name, format, root, subdir) |
554 | + if ret is None: |
555 | + return super(RemoteInventoryTree, self).archive( |
556 | + name, format, root, subdir) |
557 | + return ret |
558 | + |
559 | + |
560 | class RemoteRepositoryFormat(vf_repository.VersionedFileRepositoryFormat): |
561 | """Format for repositories accessed over a _SmartClient. |
562 | |
563 | @@ -2505,7 +2520,7 @@ |
564 | with self.lock_read(): |
565 | inventories = self.iter_inventories(revision_ids) |
566 | for inv in inventories: |
567 | - yield InventoryRevisionTree(self, inv, inv.revision_id) |
568 | + yield RemoteInventoryTree(self, inv, inv.revision_id) |
569 | |
570 | def get_revision_reconcile(self, revision_id): |
571 | with self.lock_read(): |
572 | @@ -2795,6 +2810,25 @@ |
573 | if response[0] != b'ok': |
574 | raise errors.UnexpectedSmartServerResponse(response) |
575 | |
576 | + def _revision_archive(self, revision_id, name, format, root, subdir): |
577 | + path = self.controldir._path_for_remote_call(self._client) |
578 | + format = format or '' |
579 | + root = root or '' |
580 | + subdir = subdir or '' |
581 | + try: |
582 | + response, protocol = self._call_expecting_body( |
583 | + b'Repository.revision_archive', path, |
584 | + revision_id, |
585 | + os.path.basename(name).encode('utf-8'), |
586 | + format.encode('ascii'), |
587 | + root.encode('utf-8'), |
588 | + subdir.encode('utf-8')) |
589 | + except errors.UnknownSmartMethod: |
590 | + return None |
591 | + if response[0] == b'ok': |
592 | + return iter([protocol.read_body_bytes()]) |
593 | + raise errors.UnexpectedSmartServerResponse(response) |
594 | + |
595 | |
596 | class RemoteStreamSink(vf_repository.StreamSink): |
597 | |
598 | |
599 | === modified file 'breezy/bzr/smart/repository.py' |
600 | --- breezy/bzr/smart/repository.py 2018-05-13 02:18:13 +0000 |
601 | +++ breezy/bzr/smart/repository.py 2018-05-20 16:34:17 +0000 |
602 | @@ -1292,3 +1292,27 @@ |
603 | self._ordering = ordering |
604 | # Signal that we want a body |
605 | return None |
606 | + |
607 | + |
608 | +class SmartServerRepositoryRevisionArchive(SmartServerRepositoryRequest): |
609 | + |
610 | + def do_repository_request(self, repository, revision_id, name, format, |
611 | + root, subdir=None, force_mtime=None): |
612 | + """Stream an archive file for a specific revision. |
613 | + |
614 | + :param repository: The repository to stream from. |
615 | + :param revision_id: Revision for which to export the tree |
616 | + :param name: Target file name |
617 | + :param format: Format (tar, tgz, tbz2, etc) |
618 | + :param root: Name of root directory (or '') |
619 | + :param subdir: Subdirectory to export, if not the root |
620 | + """ |
621 | + tree = repository.revision_tree(revision_id) |
622 | + return SuccessfulSmartServerResponse((b'ok',), |
623 | + body_stream=self.body_stream( |
624 | + tree, os.path.basename(name), format, root, subdir, |
625 | + force_mtime)) |
626 | + |
627 | + def body_stream(self, tree, name, format, root, subdir=None, force_mtime=None): |
628 | + with tree.lock_read(): |
629 | + return tree.archive(format, name, root, subdir, force_mtime) |
630 | |
631 | === modified file 'breezy/bzr/smart/request.py' |
632 | --- breezy/bzr/smart/request.py 2018-05-19 17:31:48 +0000 |
633 | +++ breezy/bzr/smart/request.py 2018-05-20 16:34:17 +0000 |
634 | @@ -778,6 +778,9 @@ |
635 | b'Repository.reconcile', 'breezy.bzr.smart.repository', |
636 | 'SmartServerRepositoryReconcile', info='idem') |
637 | request_handlers.register_lazy( |
638 | + b'Repository.revision_archive', 'breezy.bzr.smart.repository', |
639 | + 'SmartServerRepositoryRevisionArchive', info='read') |
640 | +request_handlers.register_lazy( |
641 | b'Repository.tarball', 'breezy.bzr.smart.repository', |
642 | 'SmartServerRepositoryTarball', info='read') |
643 | request_handlers.register_lazy( |
644 | |
645 | === removed directory 'breezy/export' |
646 | === renamed file 'breezy/export/__init__.py' => 'breezy/export.py' |
647 | --- breezy/export/__init__.py 2018-05-20 16:34:17 +0000 |
648 | +++ breezy/export.py 2018-05-20 16:34:17 +0000 |
649 | @@ -19,197 +19,19 @@ |
650 | |
651 | from __future__ import absolute_import |
652 | |
653 | +import errno |
654 | import os |
655 | +import sys |
656 | import time |
657 | import warnings |
658 | |
659 | -from .. import ( |
660 | +from . import ( |
661 | + archive, |
662 | errors, |
663 | - pyutils, |
664 | + osutils, |
665 | trace, |
666 | ) |
667 | |
668 | -# Maps format name => export function |
669 | -_exporters = {} |
670 | -# Maps filename extensions => export format name |
671 | -_exporter_extensions = {} |
672 | - |
673 | - |
674 | -def register_exporter(format, extensions, func, override=False): |
675 | - """Register an exporter. |
676 | - |
677 | - :param format: This is the name of the format, such as 'tgz' or 'zip' |
678 | - :param extensions: Extensions which should be used in the case that a |
679 | - format was not explicitly specified. |
680 | - :type extensions: List |
681 | - :param func: The function. It will be called with (tree, dest, root) |
682 | - :param override: Whether to override an object which already exists. |
683 | - Frequently plugins will want to provide functionality |
684 | - until it shows up in mainline, so the default is False. |
685 | - """ |
686 | - global _exporters, _exporter_extensions |
687 | - |
688 | - if (format not in _exporters) or override: |
689 | - _exporters[format] = func |
690 | - |
691 | - for ext in extensions: |
692 | - if (ext not in _exporter_extensions) or override: |
693 | - _exporter_extensions[ext] = format |
694 | - |
695 | - |
696 | -def register_lazy_exporter(scheme, extensions, module, funcname): |
697 | - """Register lazy-loaded exporter function. |
698 | - |
699 | - When requesting a specific type of export, load the respective path. |
700 | - """ |
701 | - def _loader(tree, dest, root, subdir, force_mtime, fileobj): |
702 | - func = pyutils.get_named_object(module, funcname) |
703 | - return func(tree, dest, root, subdir, force_mtime=force_mtime, |
704 | - fileobj=fileobj) |
705 | - |
706 | - register_exporter(scheme, extensions, _loader) |
707 | - |
708 | - |
709 | -def get_stream_export_generator(tree, name=None, format=None, root=None, |
710 | - subdir=None, per_file_timestamps=False): |
711 | - """Returns a generator that exports the given tree as a stream. |
712 | - |
713 | - The generator is expected to yield None while exporting the tree while the |
714 | - actual export is written to ``fileobj``. |
715 | - |
716 | - :param tree: A Tree (such as RevisionTree) to export |
717 | - |
718 | - :param dest: The destination where the files, etc should be put |
719 | - |
720 | - :param format: The format (dir, zip, etc), if None, it will check the |
721 | - extension on dest, looking for a match |
722 | - |
723 | - :param root: The root location inside the format. It is common practise to |
724 | - have zipfiles and tarballs extract into a subdirectory, rather than |
725 | - into the current working directory. If root is None, the default root |
726 | - will be selected as the destination without its extension. |
727 | - |
728 | - :param subdir: A starting directory within the tree. None means to export |
729 | - the entire tree, and anything else should specify the relative path to |
730 | - a directory to start exporting from. |
731 | - |
732 | - :param per_file_timestamps: Whether to use the timestamp stored in the tree |
733 | - rather than now(). This will do a revision lookup for every file so |
734 | - will be significantly slower. |
735 | - """ |
736 | - global _exporters |
737 | - |
738 | - if format is None and name is not None: |
739 | - format = get_format_from_filename(name) |
740 | - |
741 | - if format is None: |
742 | - # Default to tar |
743 | - format = 'dir' |
744 | - |
745 | - if format in ('dir', 'tlzma', 'txz', 'tbz2'): |
746 | - # formats that don't support streaming |
747 | - raise errors.NoSuchExportFormat(format) |
748 | - |
749 | - if format not in _exporters: |
750 | - raise errors.NoSuchExportFormat(format) |
751 | - |
752 | - # Most of the exporters will just have to call |
753 | - # this function anyway, so why not do it for them |
754 | - if root is None: |
755 | - root = get_root_name(name) |
756 | - |
757 | - if not per_file_timestamps: |
758 | - force_mtime = time.time() |
759 | - else: |
760 | - force_mtime = None |
761 | - |
762 | - oldpos = 0 |
763 | - import tempfile |
764 | - with tempfile.NamedTemporaryFile() as temp: |
765 | - with tree.lock_read(): |
766 | - for _ in _exporters[format]( |
767 | - tree, name, root, subdir, |
768 | - force_mtime=force_mtime, fileobj=temp.file): |
769 | - pos = temp.tell() |
770 | - temp.seek(oldpos) |
771 | - data = temp.read() |
772 | - oldpos = pos |
773 | - temp.seek(pos) |
774 | - yield data |
775 | - # FIXME(JRV): urgh, some exporters close the file for us so we need to reopen |
776 | - # it here. |
777 | - with open(temp.name, 'rb') as temp: |
778 | - temp.seek(oldpos) |
779 | - yield temp.read() |
780 | - |
781 | - |
782 | -def get_format_from_filename(name): |
783 | - global _exporter_extensions |
784 | - |
785 | - for ext in _exporter_extensions: |
786 | - if name.endswith(ext): |
787 | - return _exporter_extensions[ext] |
788 | - |
789 | - |
790 | -def get_export_generator(tree, dest=None, format=None, root=None, subdir=None, |
791 | - per_file_timestamps=False, fileobj=None): |
792 | - """Returns a generator that exports the given tree. |
793 | - |
794 | - The generator is expected to yield None while exporting the tree while the |
795 | - actual export is written to ``fileobj``. |
796 | - |
797 | - :param tree: A Tree (such as RevisionTree) to export |
798 | - |
799 | - :param dest: The destination where the files, etc should be put |
800 | - |
801 | - :param format: The format (dir, zip, etc), if None, it will check the |
802 | - extension on dest, looking for a match |
803 | - |
804 | - :param root: The root location inside the format. It is common practise to |
805 | - have zipfiles and tarballs extract into a subdirectory, rather than |
806 | - into the current working directory. If root is None, the default root |
807 | - will be selected as the destination without its extension. |
808 | - |
809 | - :param subdir: A starting directory within the tree. None means to export |
810 | - the entire tree, and anything else should specify the relative path to |
811 | - a directory to start exporting from. |
812 | - |
813 | - :param per_file_timestamps: Whether to use the timestamp stored in the tree |
814 | - rather than now(). This will do a revision lookup for every file so |
815 | - will be significantly slower. |
816 | - |
817 | - :param fileobj: Optional file object to use |
818 | - """ |
819 | - global _exporters |
820 | - |
821 | - if format is None and dest is not None: |
822 | - format = get_format_from_filename(dest) |
823 | - |
824 | - if format is None: |
825 | - # Default to 'dir' |
826 | - format = 'dir' |
827 | - |
828 | - # Most of the exporters will just have to call |
829 | - # this function anyway, so why not do it for them |
830 | - if root is None: |
831 | - root = get_root_name(dest) |
832 | - |
833 | - if format not in _exporters: |
834 | - raise errors.NoSuchExportFormat(format) |
835 | - |
836 | - if not per_file_timestamps: |
837 | - force_mtime = time.time() |
838 | - else: |
839 | - force_mtime = None |
840 | - |
841 | - trace.mutter('export version %r', tree) |
842 | - |
843 | - with tree.lock_read(): |
844 | - for _ in _exporters[format]( |
845 | - tree, dest, root, subdir, |
846 | - force_mtime=force_mtime, fileobj=fileobj): |
847 | - yield |
848 | - |
849 | |
850 | def export(tree, dest, format=None, root=None, subdir=None, |
851 | per_file_timestamps=False, fileobj=None): |
852 | @@ -234,9 +56,56 @@ |
853 | for every file so will be significantly slower. |
854 | :param fileobj: Optional file object to use |
855 | """ |
856 | - for _ in get_export_generator(tree, dest, format, root, subdir, |
857 | - per_file_timestamps, fileobj): |
858 | - pass |
859 | + if format is None and dest is not None: |
860 | + format = guess_format(dest) |
861 | + |
862 | + # Most of the exporters will just have to call |
863 | + # this function anyway, so why not do it for them |
864 | + if root is None: |
865 | + root = get_root_name(dest) |
866 | + |
867 | + if not per_file_timestamps: |
868 | + force_mtime = time.time() |
869 | + else: |
870 | + force_mtime = None |
871 | + |
872 | + trace.mutter('export version %r', tree) |
873 | + |
874 | + if format == 'dir': |
875 | + # TODO(jelmer): If the tree is remote (e.g. HPSS, Git Remote), |
876 | + # then we should stream a tar file and unpack that on the fly. |
877 | + with tree.lock_read(): |
878 | + for unused in dir_exporter_generator(tree, dest, root, subdir, |
879 | + force_mtime): |
880 | + pass |
881 | + return |
882 | + |
883 | + with tree.lock_read(): |
884 | + chunks = archive.create_archive(format, tree, dest, root, subdir, |
885 | + force_mtime) |
886 | + if dest == '-': |
887 | + for chunk in chunks: |
888 | + sys.stdout.write(chunk) |
889 | + elif fileobj is not None: |
890 | + for chunk in chunks: |
891 | + fileobj.write(chunk) |
892 | + else: |
893 | + with open(dest, 'wb') as f: |
894 | + for chunk in chunks: |
895 | + f.writelines(chunk) |
896 | + |
897 | + |
898 | +def guess_format(filename, default='dir'): |
899 | + """Guess the export format based on a file name. |
900 | + |
901 | + :param filename: Filename to guess from |
902 | + :param default: Default format to fall back to |
903 | + :return: format name |
904 | + """ |
905 | + format = archive.format_registry.get_format_from_filename(filename) |
906 | + if format is None: |
907 | + format = default |
908 | + return format |
909 | |
910 | |
911 | def get_root_name(dest): |
912 | @@ -248,7 +117,7 @@ |
913 | # Exporting to -/foo doesn't make sense so use relative paths. |
914 | return '' |
915 | dest = os.path.basename(dest) |
916 | - for ext in _exporter_extensions: |
917 | + for ext in archive.format_registry.extensions: |
918 | if dest.endswith(ext): |
919 | return dest[:-len(ext)] |
920 | return dest |
921 | @@ -293,18 +162,67 @@ |
922 | yield final_path, path, entry |
923 | |
924 | |
925 | -register_lazy_exporter('dir', [], 'breezy.export.dir_exporter', |
926 | - 'dir_exporter_generator') |
927 | -register_lazy_exporter('tar', ['.tar'], 'breezy.export.tar_exporter', |
928 | - 'plain_tar_exporter_generator') |
929 | -register_lazy_exporter('tgz', ['.tar.gz', '.tgz'], |
930 | - 'breezy.export.tar_exporter', |
931 | - 'tgz_exporter_generator') |
932 | -register_lazy_exporter('tbz2', ['.tar.bz2', '.tbz2'], |
933 | - 'breezy.export.tar_exporter', 'tbz_exporter_generator') |
934 | -register_lazy_exporter('tlzma', ['.tar.lzma'], 'breezy.export.tar_exporter', |
935 | - 'tar_lzma_exporter_generator') |
936 | -register_lazy_exporter('txz', ['.tar.xz'], 'breezy.export.tar_exporter', |
937 | - 'tar_xz_exporter_generator') |
938 | -register_lazy_exporter('zip', ['.zip'], 'breezy.export.zip_exporter', |
939 | - 'zip_exporter_generator') |
940 | +def dir_exporter_generator(tree, dest, root, subdir=None, |
941 | + force_mtime=None, fileobj=None): |
942 | + """Return a generator that exports this tree to a new directory. |
943 | + |
944 | + `dest` should either not exist or should be empty. If it does not exist it |
945 | + will be created holding the contents of this tree. |
946 | + |
947 | + :note: If the export fails, the destination directory will be |
948 | + left in an incompletely exported state: export is not transactional. |
949 | + """ |
950 | + try: |
951 | + os.mkdir(dest) |
952 | + except OSError as e: |
953 | + if e.errno == errno.EEXIST: |
954 | + # check if directory empty |
955 | + if os.listdir(dest) != []: |
956 | + raise errors.BzrError( |
957 | + "Can't export tree to non-empty directory.") |
958 | + else: |
959 | + raise |
960 | + # Iterate everything, building up the files we will want to export, and |
961 | + # creating the directories and symlinks that we need. |
962 | + # This tracks (file_id, (destination_path, executable)) |
963 | + # This matches the api that tree.iter_files_bytes() wants |
964 | + # Note in the case of revision trees, this does trigger a double inventory |
965 | + # lookup, hopefully it isn't too expensive. |
966 | + to_fetch = [] |
967 | + for dp, tp, ie in _export_iter_entries(tree, subdir): |
968 | + fullpath = osutils.pathjoin(dest, dp) |
969 | + if ie.kind == "file": |
970 | + to_fetch.append((tp, (dp, tp, ie.file_id))) |
971 | + elif ie.kind in ("directory", "tree-reference"): |
972 | + os.mkdir(fullpath) |
973 | + elif ie.kind == "symlink": |
974 | + try: |
975 | + symlink_target = tree.get_symlink_target(tp, ie.file_id) |
976 | + os.symlink(symlink_target, fullpath) |
977 | + except OSError as e: |
978 | + raise errors.BzrError( |
979 | + "Failed to create symlink %r -> %r, error: %s" |
980 | + % (fullpath, symlink_target, e)) |
981 | + else: |
982 | + raise errors.BzrError("don't know how to export {%s} of kind %r" % |
983 | + (tp, ie.kind)) |
984 | + |
985 | + yield |
986 | + # The data returned here can be in any order, but we've already created all |
987 | + # the directories |
988 | + flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | getattr(os, 'O_BINARY', 0) |
989 | + for (relpath, treepath, file_id), chunks in tree.iter_files_bytes(to_fetch): |
990 | + fullpath = osutils.pathjoin(dest, relpath) |
991 | + # We set the mode and let the umask sort out the file info |
992 | + mode = 0o666 |
993 | + if tree.is_executable(treepath, file_id): |
994 | + mode = 0o777 |
995 | + with os.fdopen(os.open(fullpath, flags, mode), 'wb') as out: |
996 | + out.writelines(chunks) |
997 | + if force_mtime is not None: |
998 | + mtime = force_mtime |
999 | + else: |
1000 | + mtime = tree.get_file_mtime(treepath, file_id) |
1001 | + os.utime(fullpath, (mtime, mtime)) |
1002 | + |
1003 | + yield |
1004 | |
1005 | === removed file 'breezy/export/dir_exporter.py' |
1006 | --- breezy/export/dir_exporter.py 2018-05-14 20:49:13 +0000 |
1007 | +++ breezy/export/dir_exporter.py 1970-01-01 00:00:00 +0000 |
1008 | @@ -1,93 +0,0 @@ |
1009 | -# Copyright (C) 2005-2011 Canonical Ltd |
1010 | -# |
1011 | -# This program is free software; you can redistribute it and/or modify |
1012 | -# it under the terms of the GNU General Public License as published by |
1013 | -# the Free Software Foundation; either version 2 of the License, or |
1014 | -# (at your option) any later version. |
1015 | -# |
1016 | -# This program is distributed in the hope that it will be useful, |
1017 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1018 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1019 | -# GNU General Public License for more details. |
1020 | -# |
1021 | -# You should have received a copy of the GNU General Public License |
1022 | -# along with this program; if not, write to the Free Software |
1023 | -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
1024 | - |
1025 | -"""Export a breezy.tree.Tree to a new or empty directory.""" |
1026 | - |
1027 | -from __future__ import absolute_import |
1028 | - |
1029 | -import errno |
1030 | -import os |
1031 | - |
1032 | -from .. import errors, osutils |
1033 | -from ..export import _export_iter_entries |
1034 | - |
1035 | - |
1036 | -def dir_exporter_generator(tree, dest, root, subdir=None, |
1037 | - force_mtime=None, fileobj=None): |
1038 | - """Return a generator that exports this tree to a new directory. |
1039 | - |
1040 | - `dest` should either not exist or should be empty. If it does not exist it |
1041 | - will be created holding the contents of this tree. |
1042 | - |
1043 | - :param fileobj: Is not used in this exporter |
1044 | - |
1045 | - :note: If the export fails, the destination directory will be |
1046 | - left in an incompletely exported state: export is not transactional. |
1047 | - """ |
1048 | - try: |
1049 | - os.mkdir(dest) |
1050 | - except OSError as e: |
1051 | - if e.errno == errno.EEXIST: |
1052 | - # check if directory empty |
1053 | - if os.listdir(dest) != []: |
1054 | - raise errors.BzrError( |
1055 | - "Can't export tree to non-empty directory.") |
1056 | - else: |
1057 | - raise |
1058 | - # Iterate everything, building up the files we will want to export, and |
1059 | - # creating the directories and symlinks that we need. |
1060 | - # This tracks (file_id, (destination_path, executable)) |
1061 | - # This matches the api that tree.iter_files_bytes() wants |
1062 | - # Note in the case of revision trees, this does trigger a double inventory |
1063 | - # lookup, hopefully it isn't too expensive. |
1064 | - to_fetch = [] |
1065 | - for dp, tp, ie in _export_iter_entries(tree, subdir): |
1066 | - fullpath = osutils.pathjoin(dest, dp) |
1067 | - if ie.kind == "file": |
1068 | - to_fetch.append((tp, (dp, tp, ie.file_id))) |
1069 | - elif ie.kind in ("directory", "tree-reference"): |
1070 | - os.mkdir(fullpath) |
1071 | - elif ie.kind == "symlink": |
1072 | - try: |
1073 | - symlink_target = tree.get_symlink_target(tp, ie.file_id) |
1074 | - os.symlink(symlink_target, fullpath) |
1075 | - except OSError as e: |
1076 | - raise errors.BzrError( |
1077 | - "Failed to create symlink %r -> %r, error: %s" |
1078 | - % (fullpath, symlink_target, e)) |
1079 | - else: |
1080 | - raise errors.BzrError("don't know how to export {%s} of kind %r" % |
1081 | - (tp, ie.kind)) |
1082 | - |
1083 | - yield |
1084 | - # The data returned here can be in any order, but we've already created all |
1085 | - # the directories |
1086 | - flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | getattr(os, 'O_BINARY', 0) |
1087 | - for (relpath, treepath, file_id), chunks in tree.iter_files_bytes(to_fetch): |
1088 | - fullpath = osutils.pathjoin(dest, relpath) |
1089 | - # We set the mode and let the umask sort out the file info |
1090 | - mode = 0o666 |
1091 | - if tree.is_executable(treepath, file_id): |
1092 | - mode = 0o777 |
1093 | - with os.fdopen(os.open(fullpath, flags, mode), 'wb') as out: |
1094 | - out.writelines(chunks) |
1095 | - if force_mtime is not None: |
1096 | - mtime = force_mtime |
1097 | - else: |
1098 | - mtime = tree.get_file_mtime(treepath, file_id) |
1099 | - os.utime(fullpath, (mtime, mtime)) |
1100 | - |
1101 | - yield |
1102 | |
1103 | === modified file 'breezy/tests/blackbox/test_export.py' |
1104 | --- breezy/tests/blackbox/test_export.py 2018-02-18 15:21:06 +0000 |
1105 | +++ breezy/tests/blackbox/test_export.py 2018-05-20 16:34:17 +0000 |
1106 | @@ -25,6 +25,7 @@ |
1107 | import zipfile |
1108 | |
1109 | |
1110 | +from ...archive import zip |
1111 | from ... import ( |
1112 | export, |
1113 | ) |
1114 | @@ -224,9 +225,9 @@ |
1115 | # forward slashes |
1116 | self.assertEqual(['test/a', 'test/b/', 'test/b/c', 'test/d/'], names) |
1117 | |
1118 | - file_attr = stat.S_IFREG | export.zip_exporter.FILE_PERMISSIONS |
1119 | - dir_attr = (stat.S_IFDIR | export.zip_exporter.ZIP_DIRECTORY_BIT | |
1120 | - export.zip_exporter.DIR_PERMISSIONS) |
1121 | + file_attr = stat.S_IFREG | zip.FILE_PERMISSIONS |
1122 | + dir_attr = (stat.S_IFDIR | zip.ZIP_DIRECTORY_BIT | |
1123 | + zip.DIR_PERMISSIONS) |
1124 | |
1125 | a_info = zfile.getinfo(names[0]) |
1126 | self.assertEqual(file_attr, a_info.external_attr) |
1127 | |
1128 | === modified file 'breezy/tests/per_tree/test_archive.py' |
1129 | --- breezy/tests/per_tree/test_archive.py 2018-05-20 16:34:17 +0000 |
1130 | +++ breezy/tests/per_tree/test_archive.py 2018-05-20 16:34:17 +0000 |
1131 | @@ -41,10 +41,10 @@ |
1132 | tree_a = self.workingtree_to_test_tree(work_a) |
1133 | output_path = 'output' |
1134 | with open(output_path, 'wb') as f: |
1135 | - f.writelines(tree_a.archive(output_path, format=self.format)) |
1136 | + f.writelines(tree_a.archive(self.format, output_path)) |
1137 | names = self.get_export_names(output_path) |
1138 | - self.assertIn('output/file', names) |
1139 | - self.assertIn('output/dir', names) |
1140 | + self.assertIn('file', names) |
1141 | + self.assertIn('dir', names) |
1142 | |
1143 | def test_export_symlink(self): |
1144 | self.requireFeature(features.SymlinkFeature) |
1145 | @@ -55,9 +55,9 @@ |
1146 | tree_a = self.workingtree_to_test_tree(work_a) |
1147 | output_path = 'output' |
1148 | with open(output_path, 'wb') as f: |
1149 | - f.writelines(tree_a.archive(output_path, format=self.format)) |
1150 | + f.writelines(tree_a.archive(self.format, output_path)) |
1151 | names = self.get_export_names(output_path) |
1152 | - self.assertIn('output/link', names) |
1153 | + self.assertIn('link', names) |
1154 | |
1155 | def get_output_names(self, path): |
1156 | raise NotImplementedError(self.get_output_names) |
1157 | @@ -107,9 +107,9 @@ |
1158 | tree_a = self.workingtree_to_test_tree(work_a) |
1159 | output_path = 'output' |
1160 | with open(output_path, 'wb') as f: |
1161 | - f.writelines(tree_a.archive(output_path, format=self.format)) |
1162 | + f.writelines(tree_a.archive(self.format, output_path)) |
1163 | names = self.get_export_names(output_path) |
1164 | - self.assertIn('output/link.lnk', names) |
1165 | + self.assertIn('link.lnk', names) |
1166 | |
1167 | |
1168 | class GenericArchiveTests(TestCaseWithTree): |
1169 | @@ -125,5 +125,4 @@ |
1170 | |
1171 | self.assertRaises( |
1172 | errors.NoSuchExportFormat, |
1173 | - list, |
1174 | - tree_a.archive('foo', format='dir')) |
1175 | + tree_a.archive, 'dir', 'foo') |
1176 | |
1177 | === modified file 'breezy/tests/test_export.py' |
1178 | --- breezy/tests/test_export.py 2018-05-14 20:49:13 +0000 |
1179 | +++ breezy/tests/test_export.py 2018-05-20 16:34:17 +0000 |
1180 | @@ -27,7 +27,7 @@ |
1181 | tests, |
1182 | ) |
1183 | from ..export import get_root_name |
1184 | -from ..export.tar_exporter import export_tarball_generator |
1185 | +from ..archive.tar import tarball_generator |
1186 | from ..sixish import ( |
1187 | BytesIO, |
1188 | ) |
1189 | @@ -250,24 +250,14 @@ |
1190 | tf = tarfile.open('target.tar.bz2') |
1191 | self.assertEqual(["target/a"], tf.getnames()) |
1192 | |
1193 | - def test_xz_stdout(self): |
1194 | - wt = self.make_branch_and_tree('.') |
1195 | - self.assertRaises(errors.BzrError, export.export, wt, '-', |
1196 | - format="txz") |
1197 | - |
1198 | def test_export_tarball_generator(self): |
1199 | wt = self.make_branch_and_tree('.') |
1200 | self.build_tree(['a']) |
1201 | wt.add(["a"]) |
1202 | wt.commit("1", timestamp=42) |
1203 | target = BytesIO() |
1204 | - ball = tarfile.open(None, "w|", target) |
1205 | - wt.lock_read() |
1206 | - try: |
1207 | - for _ in export_tarball_generator(wt, ball, "bar"): |
1208 | - pass |
1209 | - finally: |
1210 | - wt.unlock() |
1211 | + with wt.lock_read(): |
1212 | + target.writelines(tarball_generator(wt, "bar")) |
1213 | # Ball should now be closed. |
1214 | target.seek(0) |
1215 | ball2 = tarfile.open(None, "r", target) |
1216 | |
1217 | === modified file 'breezy/tests/test_remote.py' |
1218 | --- breezy/tests/test_remote.py 2018-05-19 17:31:48 +0000 |
1219 | +++ breezy/tests/test_remote.py 2018-05-20 16:34:17 +0000 |
1220 | @@ -25,6 +25,7 @@ |
1221 | |
1222 | import base64 |
1223 | import bz2 |
1224 | +import tarfile |
1225 | import zlib |
1226 | |
1227 | from .. import ( |
1228 | @@ -4305,3 +4306,39 @@ |
1229 | b'success', (b'ok', ), iter([])) |
1230 | self.assertRaises(errors.NoSuchRevision, list, repo.iter_inventories( |
1231 | [b"somerevid"])) |
1232 | + |
1233 | + |
1234 | +class TestRepositoryRevisionTreeArchive(TestRemoteRepository): |
1235 | + """Test Repository.iter_inventories.""" |
1236 | + |
1237 | + def _serialize_inv_delta(self, old_name, new_name, delta): |
1238 | + serializer = inventory_delta.InventoryDeltaSerializer(True, False) |
1239 | + return b"".join(serializer.delta_to_lines(old_name, new_name, delta)) |
1240 | + |
1241 | + def test_simple(self): |
1242 | + transport_path = 'quack' |
1243 | + repo, client = self.setup_fake_client_and_repository(transport_path) |
1244 | + fmt = controldir.format_registry.get('2a')().repository_format |
1245 | + repo._format = fmt |
1246 | + stream = [('inventory-deltas', [ |
1247 | + versionedfile.FulltextContentFactory(b'somerevid', None, None, |
1248 | + self._serialize_inv_delta(b'null:', b'somerevid', []))])] |
1249 | + client.add_expected_call( |
1250 | + b'VersionedFileRepository.get_inventories', (b'quack/', b'unordered'), |
1251 | + b'success', (b'ok', ), |
1252 | + _stream_to_byte_stream(stream, fmt)) |
1253 | + f = BytesIO() |
1254 | + with tarfile.open(mode='w', fileobj=f) as tf: |
1255 | + info = tarfile.TarInfo('somefile') |
1256 | + info.mtime = 432432 |
1257 | + contents = b'some data' |
1258 | + info.type = tarfile.REGTYPE |
1259 | + info.mode = 0o644 |
1260 | + info.size = len(contents) |
1261 | + tf.addfile(info, BytesIO(contents)) |
1262 | + client.add_expected_call( |
1263 | + b'Repository.revision_archive', (b'quack/', b'somerevid', b'tar', b'', b'', b''), |
1264 | + b'success', (b'ok', ), |
1265 | + f.getvalue()) |
1266 | + tree = repo.revision_tree(b'somerevid') |
1267 | + self.assertEqual(f.getvalue(), b''.join(tree.archive('tar'))) |
1268 | |
1269 | === modified file 'breezy/tests/test_smart.py' |
1270 | --- breezy/tests/test_smart.py 2018-05-19 17:31:48 +0000 |
1271 | +++ breezy/tests/test_smart.py 2018-05-20 16:34:17 +0000 |
1272 | @@ -25,6 +25,8 @@ |
1273 | """ |
1274 | |
1275 | import bz2 |
1276 | +from io import BytesIO |
1277 | +import tarfile |
1278 | import zlib |
1279 | |
1280 | from breezy import ( |
1281 | @@ -2713,3 +2715,21 @@ |
1282 | self.assertEqual(response.args, (b"ok", )) |
1283 | self.assertEqual(b"".join(response.body_stream), |
1284 | b"Bazaar pack format 1 (introduced in 0.18)\nB54\n\nBazaar repository format 2a (needs bzr 1.16 or later)\nE") |
1285 | + |
1286 | + |
1287 | +class TestSmartServerRepositoryRevisionArchive(tests.TestCaseWithTransport): |
1288 | + |
1289 | + def test_get(self): |
1290 | + backing = self.get_transport() |
1291 | + request = smart_repo.SmartServerRepositoryRevisionArchive(backing) |
1292 | + t = self.make_branch_and_tree('.') |
1293 | + self.addCleanup(t.lock_write().unlock) |
1294 | + self.build_tree_contents([("file", b"somecontents")]) |
1295 | + t.add(["file"], [b"thefileid"]) |
1296 | + t.commit(rev_id=b'somerev', message="add file") |
1297 | + response = request.execute(b'', b"somerev", "foo.tar", "tar", "foo") |
1298 | + self.assertTrue(response.is_successful()) |
1299 | + self.assertEqual(response.args, (b"ok", )) |
1300 | + b = BytesIO(b"".join(response.body_stream)) |
1301 | + with tarfile.open(mode='r', fileobj=b) as tf: |
1302 | + self.assertEqual(['foo/file'], tf.getnames()) |
1303 | |
1304 | === modified file 'breezy/tree.py' |
1305 | --- breezy/tree.py 2018-05-20 16:34:17 +0000 |
1306 | +++ breezy/tree.py 2018-05-20 16:34:17 +0000 |
1307 | @@ -653,19 +653,22 @@ |
1308 | searcher = default_searcher |
1309 | return searcher |
1310 | |
1311 | - def archive(self, name, format=None, root=None, subdir=None): |
1312 | + def archive(self, format, name, root='', subdir=None, |
1313 | + force_mtime=None): |
1314 | """Create an archive of this tree. |
1315 | |
1316 | :param name: target file name |
1317 | :param format: Format name (e.g. 'tar') |
1318 | :param root: Root directory name (or None) |
1319 | :param subdir: Subdirectory to export (or None) |
1320 | + :param per_file_timestamps: Whether to set the timestamp |
1321 | + for each file to the last changed time. |
1322 | :return: Iterator over archive chunks |
1323 | """ |
1324 | - from .export import get_stream_export_generator |
1325 | + from .archive import create_archive |
1326 | with self.lock_read(): |
1327 | - return get_stream_export_generator(self, name, format, root, |
1328 | - subdir) |
1329 | + return create_archive(format, self, name, root, |
1330 | + subdir, force_mtime=force_mtime) |
1331 | |
1332 | @classmethod |
1333 | def versionable_kind(cls, kind): |
Voting criteria not met /ci.breezy- vcs.org/ job/land- brz/177/
https:/