Merge lp:~jelmer/brz/git-archive into lp:brz
- git-archive
- Merge into trunk
Proposed by
Jelmer Vernooij
Status: | Merged |
---|---|
Approved by: | Jelmer Vernooij |
Approved revision: | no longer in the source branch. |
Merge reported by: | The Breezy Bot |
Merged at revision: | not available |
Proposed branch: | lp:~jelmer/brz/git-archive |
Merge into: | lp:brz |
Prerequisite: | lp:~jelmer/brz/tree-archive |
Diff against target: |
1243 lines (+452/-511) 11 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/export.py (+119/-202) breezy/export/dir_exporter.py (+0/-93) breezy/plugins/git/remote.py (+59/-1) 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/tree.py (+7/-4) |
To merge this branch: | bzr merge lp:~jelmer/brz/git-archive |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Packman | Approve | ||
Review via email: mp+345969@code.launchpad.net |
Commit message
Implement GitRevisionTree
Description of the change
Implement GitRevisionTree
To post a comment you must log in.
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:08:56 +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:08:56 +0000 |
112 | +++ breezy/archive/tar.py 2018-05-20 16:08:56 +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:08:56 +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:08:56 +0000 |
481 | +++ breezy/builtins.py 2018-05-20 16:08:56 +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 | === removed directory 'breezy/export' |
532 | === renamed file 'breezy/export/__init__.py' => 'breezy/export.py' |
533 | --- breezy/export/__init__.py 2018-05-20 16:08:56 +0000 |
534 | +++ breezy/export.py 2018-05-20 16:08:56 +0000 |
535 | @@ -19,197 +19,19 @@ |
536 | |
537 | from __future__ import absolute_import |
538 | |
539 | +import errno |
540 | import os |
541 | +import sys |
542 | import time |
543 | import warnings |
544 | |
545 | -from .. import ( |
546 | +from . import ( |
547 | + archive, |
548 | errors, |
549 | - pyutils, |
550 | + osutils, |
551 | trace, |
552 | ) |
553 | |
554 | -# Maps format name => export function |
555 | -_exporters = {} |
556 | -# Maps filename extensions => export format name |
557 | -_exporter_extensions = {} |
558 | - |
559 | - |
560 | -def register_exporter(format, extensions, func, override=False): |
561 | - """Register an exporter. |
562 | - |
563 | - :param format: This is the name of the format, such as 'tgz' or 'zip' |
564 | - :param extensions: Extensions which should be used in the case that a |
565 | - format was not explicitly specified. |
566 | - :type extensions: List |
567 | - :param func: The function. It will be called with (tree, dest, root) |
568 | - :param override: Whether to override an object which already exists. |
569 | - Frequently plugins will want to provide functionality |
570 | - until it shows up in mainline, so the default is False. |
571 | - """ |
572 | - global _exporters, _exporter_extensions |
573 | - |
574 | - if (format not in _exporters) or override: |
575 | - _exporters[format] = func |
576 | - |
577 | - for ext in extensions: |
578 | - if (ext not in _exporter_extensions) or override: |
579 | - _exporter_extensions[ext] = format |
580 | - |
581 | - |
582 | -def register_lazy_exporter(scheme, extensions, module, funcname): |
583 | - """Register lazy-loaded exporter function. |
584 | - |
585 | - When requesting a specific type of export, load the respective path. |
586 | - """ |
587 | - def _loader(tree, dest, root, subdir, force_mtime, fileobj): |
588 | - func = pyutils.get_named_object(module, funcname) |
589 | - return func(tree, dest, root, subdir, force_mtime=force_mtime, |
590 | - fileobj=fileobj) |
591 | - |
592 | - register_exporter(scheme, extensions, _loader) |
593 | - |
594 | - |
595 | -def get_stream_export_generator(tree, name=None, format=None, root=None, |
596 | - subdir=None, per_file_timestamps=False): |
597 | - """Returns a generator that exports the given tree as a stream. |
598 | - |
599 | - The generator is expected to yield None while exporting the tree while the |
600 | - actual export is written to ``fileobj``. |
601 | - |
602 | - :param tree: A Tree (such as RevisionTree) to export |
603 | - |
604 | - :param dest: The destination where the files, etc should be put |
605 | - |
606 | - :param format: The format (dir, zip, etc), if None, it will check the |
607 | - extension on dest, looking for a match |
608 | - |
609 | - :param root: The root location inside the format. It is common practise to |
610 | - have zipfiles and tarballs extract into a subdirectory, rather than |
611 | - into the current working directory. If root is None, the default root |
612 | - will be selected as the destination without its extension. |
613 | - |
614 | - :param subdir: A starting directory within the tree. None means to export |
615 | - the entire tree, and anything else should specify the relative path to |
616 | - a directory to start exporting from. |
617 | - |
618 | - :param per_file_timestamps: Whether to use the timestamp stored in the tree |
619 | - rather than now(). This will do a revision lookup for every file so |
620 | - will be significantly slower. |
621 | - """ |
622 | - global _exporters |
623 | - |
624 | - if format is None and name is not None: |
625 | - format = get_format_from_filename(name) |
626 | - |
627 | - if format is None: |
628 | - # Default to tar |
629 | - format = 'dir' |
630 | - |
631 | - if format in ('dir', 'tlzma', 'txz', 'tbz2'): |
632 | - # formats that don't support streaming |
633 | - raise errors.NoSuchExportFormat(format) |
634 | - |
635 | - if format not in _exporters: |
636 | - raise errors.NoSuchExportFormat(format) |
637 | - |
638 | - # Most of the exporters will just have to call |
639 | - # this function anyway, so why not do it for them |
640 | - if root is None: |
641 | - root = get_root_name(name) |
642 | - |
643 | - if not per_file_timestamps: |
644 | - force_mtime = time.time() |
645 | - else: |
646 | - force_mtime = None |
647 | - |
648 | - oldpos = 0 |
649 | - import tempfile |
650 | - with tempfile.NamedTemporaryFile() as temp: |
651 | - with tree.lock_read(): |
652 | - for _ in _exporters[format]( |
653 | - tree, name, root, subdir, |
654 | - force_mtime=force_mtime, fileobj=temp.file): |
655 | - pos = temp.tell() |
656 | - temp.seek(oldpos) |
657 | - data = temp.read() |
658 | - oldpos = pos |
659 | - temp.seek(pos) |
660 | - yield data |
661 | - # FIXME(JRV): urgh, some exporters close the file for us so we need to reopen |
662 | - # it here. |
663 | - with open(temp.name, 'rb') as temp: |
664 | - temp.seek(oldpos) |
665 | - yield temp.read() |
666 | - |
667 | - |
668 | -def get_format_from_filename(name): |
669 | - global _exporter_extensions |
670 | - |
671 | - for ext in _exporter_extensions: |
672 | - if name.endswith(ext): |
673 | - return _exporter_extensions[ext] |
674 | - |
675 | - |
676 | -def get_export_generator(tree, dest=None, format=None, root=None, subdir=None, |
677 | - per_file_timestamps=False, fileobj=None): |
678 | - """Returns a generator that exports the given tree. |
679 | - |
680 | - The generator is expected to yield None while exporting the tree while the |
681 | - actual export is written to ``fileobj``. |
682 | - |
683 | - :param tree: A Tree (such as RevisionTree) to export |
684 | - |
685 | - :param dest: The destination where the files, etc should be put |
686 | - |
687 | - :param format: The format (dir, zip, etc), if None, it will check the |
688 | - extension on dest, looking for a match |
689 | - |
690 | - :param root: The root location inside the format. It is common practise to |
691 | - have zipfiles and tarballs extract into a subdirectory, rather than |
692 | - into the current working directory. If root is None, the default root |
693 | - will be selected as the destination without its extension. |
694 | - |
695 | - :param subdir: A starting directory within the tree. None means to export |
696 | - the entire tree, and anything else should specify the relative path to |
697 | - a directory to start exporting from. |
698 | - |
699 | - :param per_file_timestamps: Whether to use the timestamp stored in the tree |
700 | - rather than now(). This will do a revision lookup for every file so |
701 | - will be significantly slower. |
702 | - |
703 | - :param fileobj: Optional file object to use |
704 | - """ |
705 | - global _exporters |
706 | - |
707 | - if format is None and dest is not None: |
708 | - format = get_format_from_filename(dest) |
709 | - |
710 | - if format is None: |
711 | - # Default to 'dir' |
712 | - format = 'dir' |
713 | - |
714 | - # Most of the exporters will just have to call |
715 | - # this function anyway, so why not do it for them |
716 | - if root is None: |
717 | - root = get_root_name(dest) |
718 | - |
719 | - if format not in _exporters: |
720 | - raise errors.NoSuchExportFormat(format) |
721 | - |
722 | - if not per_file_timestamps: |
723 | - force_mtime = time.time() |
724 | - else: |
725 | - force_mtime = None |
726 | - |
727 | - trace.mutter('export version %r', tree) |
728 | - |
729 | - with tree.lock_read(): |
730 | - for _ in _exporters[format]( |
731 | - tree, dest, root, subdir, |
732 | - force_mtime=force_mtime, fileobj=fileobj): |
733 | - yield |
734 | - |
735 | |
736 | def export(tree, dest, format=None, root=None, subdir=None, |
737 | per_file_timestamps=False, fileobj=None): |
738 | @@ -234,9 +56,55 @@ |
739 | for every file so will be significantly slower. |
740 | :param fileobj: Optional file object to use |
741 | """ |
742 | - for _ in get_export_generator(tree, dest, format, root, subdir, |
743 | - per_file_timestamps, fileobj): |
744 | - pass |
745 | + if format is None and dest is not None: |
746 | + format = guess_format(dest) |
747 | + |
748 | + # Most of the exporters will just have to call |
749 | + # this function anyway, so why not do it for them |
750 | + if root is None: |
751 | + root = get_root_name(dest) |
752 | + |
753 | + if not per_file_timestamps: |
754 | + force_mtime = time.time() |
755 | + else: |
756 | + force_mtime = None |
757 | + |
758 | + trace.mutter('export version %r', tree) |
759 | + |
760 | + if format == 'dir': |
761 | + # TODO(jelmer): If the tree is remote (e.g. HPSS, Git Remote), |
762 | + # then we should stream a tar file and unpack that on the fly. |
763 | + with tree.lock_read(): |
764 | + for unused in dir_exporter_generator(tree, dest, root, subdir, |
765 | + force_mtime): |
766 | + pass |
767 | + return |
768 | + |
769 | + with tree.lock_read(): |
770 | + chunks = tree.archive(format, dest, root=root, subdir=subdir, force_mtime=force_mtime) |
771 | + if dest == '-': |
772 | + for chunk in chunks: |
773 | + sys.stdout.write(chunk) |
774 | + elif fileobj is not None: |
775 | + for chunk in chunks: |
776 | + fileobj.write(chunk) |
777 | + else: |
778 | + with open(dest, 'wb') as f: |
779 | + for chunk in chunks: |
780 | + f.writelines(chunk) |
781 | + |
782 | + |
783 | +def guess_format(filename, default='dir'): |
784 | + """Guess the export format based on a file name. |
785 | + |
786 | + :param filename: Filename to guess from |
787 | + :param default: Default format to fall back to |
788 | + :return: format name |
789 | + """ |
790 | + format = archive.format_registry.get_format_from_filename(filename) |
791 | + if format is None: |
792 | + format = default |
793 | + return format |
794 | |
795 | |
796 | def get_root_name(dest): |
797 | @@ -248,7 +116,7 @@ |
798 | # Exporting to -/foo doesn't make sense so use relative paths. |
799 | return '' |
800 | dest = os.path.basename(dest) |
801 | - for ext in _exporter_extensions: |
802 | + for ext in archive.format_registry.extensions: |
803 | if dest.endswith(ext): |
804 | return dest[:-len(ext)] |
805 | return dest |
806 | @@ -293,18 +161,67 @@ |
807 | yield final_path, path, entry |
808 | |
809 | |
810 | -register_lazy_exporter('dir', [], 'breezy.export.dir_exporter', |
811 | - 'dir_exporter_generator') |
812 | -register_lazy_exporter('tar', ['.tar'], 'breezy.export.tar_exporter', |
813 | - 'plain_tar_exporter_generator') |
814 | -register_lazy_exporter('tgz', ['.tar.gz', '.tgz'], |
815 | - 'breezy.export.tar_exporter', |
816 | - 'tgz_exporter_generator') |
817 | -register_lazy_exporter('tbz2', ['.tar.bz2', '.tbz2'], |
818 | - 'breezy.export.tar_exporter', 'tbz_exporter_generator') |
819 | -register_lazy_exporter('tlzma', ['.tar.lzma'], 'breezy.export.tar_exporter', |
820 | - 'tar_lzma_exporter_generator') |
821 | -register_lazy_exporter('txz', ['.tar.xz'], 'breezy.export.tar_exporter', |
822 | - 'tar_xz_exporter_generator') |
823 | -register_lazy_exporter('zip', ['.zip'], 'breezy.export.zip_exporter', |
824 | - 'zip_exporter_generator') |
825 | +def dir_exporter_generator(tree, dest, root, subdir=None, |
826 | + force_mtime=None, fileobj=None): |
827 | + """Return a generator that exports this tree to a new directory. |
828 | + |
829 | + `dest` should either not exist or should be empty. If it does not exist it |
830 | + will be created holding the contents of this tree. |
831 | + |
832 | + :note: If the export fails, the destination directory will be |
833 | + left in an incompletely exported state: export is not transactional. |
834 | + """ |
835 | + try: |
836 | + os.mkdir(dest) |
837 | + except OSError as e: |
838 | + if e.errno == errno.EEXIST: |
839 | + # check if directory empty |
840 | + if os.listdir(dest) != []: |
841 | + raise errors.BzrError( |
842 | + "Can't export tree to non-empty directory.") |
843 | + else: |
844 | + raise |
845 | + # Iterate everything, building up the files we will want to export, and |
846 | + # creating the directories and symlinks that we need. |
847 | + # This tracks (file_id, (destination_path, executable)) |
848 | + # This matches the api that tree.iter_files_bytes() wants |
849 | + # Note in the case of revision trees, this does trigger a double inventory |
850 | + # lookup, hopefully it isn't too expensive. |
851 | + to_fetch = [] |
852 | + for dp, tp, ie in _export_iter_entries(tree, subdir): |
853 | + fullpath = osutils.pathjoin(dest, dp) |
854 | + if ie.kind == "file": |
855 | + to_fetch.append((tp, (dp, tp, ie.file_id))) |
856 | + elif ie.kind in ("directory", "tree-reference"): |
857 | + os.mkdir(fullpath) |
858 | + elif ie.kind == "symlink": |
859 | + try: |
860 | + symlink_target = tree.get_symlink_target(tp, ie.file_id) |
861 | + os.symlink(symlink_target, fullpath) |
862 | + except OSError as e: |
863 | + raise errors.BzrError( |
864 | + "Failed to create symlink %r -> %r, error: %s" |
865 | + % (fullpath, symlink_target, e)) |
866 | + else: |
867 | + raise errors.BzrError("don't know how to export {%s} of kind %r" % |
868 | + (tp, ie.kind)) |
869 | + |
870 | + yield |
871 | + # The data returned here can be in any order, but we've already created all |
872 | + # the directories |
873 | + flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | getattr(os, 'O_BINARY', 0) |
874 | + for (relpath, treepath, file_id), chunks in tree.iter_files_bytes(to_fetch): |
875 | + fullpath = osutils.pathjoin(dest, relpath) |
876 | + # We set the mode and let the umask sort out the file info |
877 | + mode = 0o666 |
878 | + if tree.is_executable(treepath, file_id): |
879 | + mode = 0o777 |
880 | + with os.fdopen(os.open(fullpath, flags, mode), 'wb') as out: |
881 | + out.writelines(chunks) |
882 | + if force_mtime is not None: |
883 | + mtime = force_mtime |
884 | + else: |
885 | + mtime = tree.get_file_mtime(treepath, file_id) |
886 | + os.utime(fullpath, (mtime, mtime)) |
887 | + |
888 | + yield |
889 | |
890 | === removed file 'breezy/export/dir_exporter.py' |
891 | --- breezy/export/dir_exporter.py 2018-05-14 20:49:13 +0000 |
892 | +++ breezy/export/dir_exporter.py 1970-01-01 00:00:00 +0000 |
893 | @@ -1,93 +0,0 @@ |
894 | -# Copyright (C) 2005-2011 Canonical Ltd |
895 | -# |
896 | -# This program is free software; you can redistribute it and/or modify |
897 | -# it under the terms of the GNU General Public License as published by |
898 | -# the Free Software Foundation; either version 2 of the License, or |
899 | -# (at your option) any later version. |
900 | -# |
901 | -# This program is distributed in the hope that it will be useful, |
902 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
903 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
904 | -# GNU General Public License for more details. |
905 | -# |
906 | -# You should have received a copy of the GNU General Public License |
907 | -# along with this program; if not, write to the Free Software |
908 | -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
909 | - |
910 | -"""Export a breezy.tree.Tree to a new or empty directory.""" |
911 | - |
912 | -from __future__ import absolute_import |
913 | - |
914 | -import errno |
915 | -import os |
916 | - |
917 | -from .. import errors, osutils |
918 | -from ..export import _export_iter_entries |
919 | - |
920 | - |
921 | -def dir_exporter_generator(tree, dest, root, subdir=None, |
922 | - force_mtime=None, fileobj=None): |
923 | - """Return a generator that exports this tree to a new directory. |
924 | - |
925 | - `dest` should either not exist or should be empty. If it does not exist it |
926 | - will be created holding the contents of this tree. |
927 | - |
928 | - :param fileobj: Is not used in this exporter |
929 | - |
930 | - :note: If the export fails, the destination directory will be |
931 | - left in an incompletely exported state: export is not transactional. |
932 | - """ |
933 | - try: |
934 | - os.mkdir(dest) |
935 | - except OSError as e: |
936 | - if e.errno == errno.EEXIST: |
937 | - # check if directory empty |
938 | - if os.listdir(dest) != []: |
939 | - raise errors.BzrError( |
940 | - "Can't export tree to non-empty directory.") |
941 | - else: |
942 | - raise |
943 | - # Iterate everything, building up the files we will want to export, and |
944 | - # creating the directories and symlinks that we need. |
945 | - # This tracks (file_id, (destination_path, executable)) |
946 | - # This matches the api that tree.iter_files_bytes() wants |
947 | - # Note in the case of revision trees, this does trigger a double inventory |
948 | - # lookup, hopefully it isn't too expensive. |
949 | - to_fetch = [] |
950 | - for dp, tp, ie in _export_iter_entries(tree, subdir): |
951 | - fullpath = osutils.pathjoin(dest, dp) |
952 | - if ie.kind == "file": |
953 | - to_fetch.append((tp, (dp, tp, ie.file_id))) |
954 | - elif ie.kind in ("directory", "tree-reference"): |
955 | - os.mkdir(fullpath) |
956 | - elif ie.kind == "symlink": |
957 | - try: |
958 | - symlink_target = tree.get_symlink_target(tp, ie.file_id) |
959 | - os.symlink(symlink_target, fullpath) |
960 | - except OSError as e: |
961 | - raise errors.BzrError( |
962 | - "Failed to create symlink %r -> %r, error: %s" |
963 | - % (fullpath, symlink_target, e)) |
964 | - else: |
965 | - raise errors.BzrError("don't know how to export {%s} of kind %r" % |
966 | - (tp, ie.kind)) |
967 | - |
968 | - yield |
969 | - # The data returned here can be in any order, but we've already created all |
970 | - # the directories |
971 | - flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | getattr(os, 'O_BINARY', 0) |
972 | - for (relpath, treepath, file_id), chunks in tree.iter_files_bytes(to_fetch): |
973 | - fullpath = osutils.pathjoin(dest, relpath) |
974 | - # We set the mode and let the umask sort out the file info |
975 | - mode = 0o666 |
976 | - if tree.is_executable(treepath, file_id): |
977 | - mode = 0o777 |
978 | - with os.fdopen(os.open(fullpath, flags, mode), 'wb') as out: |
979 | - out.writelines(chunks) |
980 | - if force_mtime is not None: |
981 | - mtime = force_mtime |
982 | - else: |
983 | - mtime = tree.get_file_mtime(treepath, file_id) |
984 | - os.utime(fullpath, (mtime, mtime)) |
985 | - |
986 | - yield |
987 | |
988 | === modified file 'breezy/plugins/git/remote.py' |
989 | --- breezy/plugins/git/remote.py 2018-05-13 22:54:28 +0000 |
990 | +++ breezy/plugins/git/remote.py 2018-05-20 16:08:56 +0000 |
991 | @@ -18,12 +18,14 @@ |
992 | |
993 | from __future__ import absolute_import |
994 | |
995 | +from io import BytesIO |
996 | import re |
997 | |
998 | from ... import ( |
999 | config, |
1000 | debug, |
1001 | errors, |
1002 | + osutils, |
1003 | trace, |
1004 | ui, |
1005 | urlutils, |
1006 | @@ -45,6 +47,7 @@ |
1007 | NoWorkingTree, |
1008 | UninitializableFormat, |
1009 | ) |
1010 | +from ...revisiontree import RevisionTree |
1011 | from ...transport import ( |
1012 | Transport, |
1013 | register_urlparse_netloc_protocol, |
1014 | @@ -364,6 +367,25 @@ |
1015 | def _gitrepository_class(self): |
1016 | return RemoteGitRepository |
1017 | |
1018 | + def archive(self, format, committish, write_data, progress=None, write_error=None, |
1019 | + subdirs=None, prefix=None): |
1020 | + if format not in ('tar', 'zip'): |
1021 | + raise errors.NoSuchExportFormat(format) |
1022 | + if progress is None: |
1023 | + pb = ui.ui_factory.nested_progress_bar() |
1024 | + progress = DefaultProgressReporter(pb).progress |
1025 | + else: |
1026 | + pb = None |
1027 | + try: |
1028 | + self._client.archive(self._client_path, committish, |
1029 | + write_data, progress, write_error, format=format, |
1030 | + subdirs=subdirs, prefix=prefix) |
1031 | + except GitProtocolError as e: |
1032 | + raise parse_git_error(self.transport.external_url(), e) |
1033 | + finally: |
1034 | + if pb is not None: |
1035 | + pb.finished() |
1036 | + |
1037 | def fetch_pack(self, determine_wants, graph_walker, pack_data, progress=None): |
1038 | if progress is None: |
1039 | pb = ui.ui_factory.nested_progress_bar() |
1040 | @@ -699,6 +721,39 @@ |
1041 | external_url.startswith("git:")) |
1042 | |
1043 | |
1044 | +class GitRemoteRevisionTree(RevisionTree): |
1045 | + |
1046 | + def archive(self, format, name, root=None, subdir=None, force_mtime=None): |
1047 | + """Create an archive of this tree. |
1048 | + |
1049 | + :param format: Format name (e.g. 'tar') |
1050 | + :param name: target file name |
1051 | + :param root: Root directory name (or None) |
1052 | + :param subdir: Subdirectory to export (or None) |
1053 | + :return: Iterator over archive chunks |
1054 | + """ |
1055 | + commit = self._repository.lookup_bzr_revision_id( |
1056 | + self.get_revision_id())[0] |
1057 | + f = tempfile.SpooledTemporaryFile() |
1058 | + # git-upload-archive(1) generaly only supports refs. So let's see if we |
1059 | + # can find one. |
1060 | + reverse_refs = { |
1061 | + v: k for (k, v) in |
1062 | + self._repository.controldir.get_refs_container().as_dict().items()} |
1063 | + try: |
1064 | + committish = reverse_refs[commit] |
1065 | + except KeyError: |
1066 | + # No? Maybe the user has uploadArchive.allowUnreachable enabled. |
1067 | + # Let's hope for the best. |
1068 | + committish = commit |
1069 | + self._repository.archive( |
1070 | + format, committish, f.write, |
1071 | + subdirs=([subdir] if subdir else None), |
1072 | + prefix=(root+'/') if root else '') |
1073 | + f.seek(0) |
1074 | + return osutils.file_iterator(f) |
1075 | + |
1076 | + |
1077 | class RemoteGitRepository(GitRepository): |
1078 | |
1079 | @property |
1080 | @@ -708,6 +763,9 @@ |
1081 | def get_parent_map(self, revids): |
1082 | raise GitSmartRemoteNotSupported(self.get_parent_map, self) |
1083 | |
1084 | + def archive(self, *args, **kwargs): |
1085 | + return self.controldir.archive(*args, **kwargs) |
1086 | + |
1087 | def fetch_pack(self, determine_wants, graph_walker, pack_data, |
1088 | progress=None): |
1089 | return self.controldir.fetch_pack(determine_wants, graph_walker, |
1090 | @@ -745,7 +803,7 @@ |
1091 | return mapping.revision_id_foreign_to_bzr(foreign_revid) |
1092 | |
1093 | def revision_tree(self, revid): |
1094 | - raise GitSmartRemoteNotSupported(self.revision_tree, self) |
1095 | + return GitRemoteRevisionTree(self, revid) |
1096 | |
1097 | def get_revisions(self, revids): |
1098 | raise GitSmartRemoteNotSupported(self.get_revisions, self) |
1099 | |
1100 | === modified file 'breezy/tests/blackbox/test_export.py' |
1101 | --- breezy/tests/blackbox/test_export.py 2018-02-18 15:21:06 +0000 |
1102 | +++ breezy/tests/blackbox/test_export.py 2018-05-20 16:08:56 +0000 |
1103 | @@ -25,6 +25,7 @@ |
1104 | import zipfile |
1105 | |
1106 | |
1107 | +from ...archive import zip |
1108 | from ... import ( |
1109 | export, |
1110 | ) |
1111 | @@ -224,9 +225,9 @@ |
1112 | # forward slashes |
1113 | self.assertEqual(['test/a', 'test/b/', 'test/b/c', 'test/d/'], names) |
1114 | |
1115 | - file_attr = stat.S_IFREG | export.zip_exporter.FILE_PERMISSIONS |
1116 | - dir_attr = (stat.S_IFDIR | export.zip_exporter.ZIP_DIRECTORY_BIT | |
1117 | - export.zip_exporter.DIR_PERMISSIONS) |
1118 | + file_attr = stat.S_IFREG | zip.FILE_PERMISSIONS |
1119 | + dir_attr = (stat.S_IFDIR | zip.ZIP_DIRECTORY_BIT | |
1120 | + zip.DIR_PERMISSIONS) |
1121 | |
1122 | a_info = zfile.getinfo(names[0]) |
1123 | self.assertEqual(file_attr, a_info.external_attr) |
1124 | |
1125 | === modified file 'breezy/tests/per_tree/test_archive.py' |
1126 | --- breezy/tests/per_tree/test_archive.py 2018-05-20 16:08:56 +0000 |
1127 | +++ breezy/tests/per_tree/test_archive.py 2018-05-20 16:08:56 +0000 |
1128 | @@ -41,10 +41,10 @@ |
1129 | tree_a = self.workingtree_to_test_tree(work_a) |
1130 | output_path = 'output' |
1131 | with open(output_path, 'wb') as f: |
1132 | - f.writelines(tree_a.archive(output_path, format=self.format)) |
1133 | + f.writelines(tree_a.archive(self.format, output_path)) |
1134 | names = self.get_export_names(output_path) |
1135 | - self.assertIn('output/file', names) |
1136 | - self.assertIn('output/dir', names) |
1137 | + self.assertIn('file', names) |
1138 | + self.assertIn('dir', names) |
1139 | |
1140 | def test_export_symlink(self): |
1141 | self.requireFeature(features.SymlinkFeature) |
1142 | @@ -55,9 +55,9 @@ |
1143 | tree_a = self.workingtree_to_test_tree(work_a) |
1144 | output_path = 'output' |
1145 | with open(output_path, 'wb') as f: |
1146 | - f.writelines(tree_a.archive(output_path, format=self.format)) |
1147 | + f.writelines(tree_a.archive(self.format, output_path)) |
1148 | names = self.get_export_names(output_path) |
1149 | - self.assertIn('output/link', names) |
1150 | + self.assertIn('link', names) |
1151 | |
1152 | def get_output_names(self, path): |
1153 | raise NotImplementedError(self.get_output_names) |
1154 | @@ -107,9 +107,9 @@ |
1155 | tree_a = self.workingtree_to_test_tree(work_a) |
1156 | output_path = 'output' |
1157 | with open(output_path, 'wb') as f: |
1158 | - f.writelines(tree_a.archive(output_path, format=self.format)) |
1159 | + f.writelines(tree_a.archive(self.format, output_path)) |
1160 | names = self.get_export_names(output_path) |
1161 | - self.assertIn('output/link.lnk', names) |
1162 | + self.assertIn('link.lnk', names) |
1163 | |
1164 | |
1165 | class GenericArchiveTests(TestCaseWithTree): |
1166 | @@ -125,5 +125,4 @@ |
1167 | |
1168 | self.assertRaises( |
1169 | errors.NoSuchExportFormat, |
1170 | - list, |
1171 | - tree_a.archive('foo', format='dir')) |
1172 | + tree_a.archive, 'dir', 'foo') |
1173 | |
1174 | === modified file 'breezy/tests/test_export.py' |
1175 | --- breezy/tests/test_export.py 2018-05-14 20:49:13 +0000 |
1176 | +++ breezy/tests/test_export.py 2018-05-20 16:08:56 +0000 |
1177 | @@ -27,7 +27,7 @@ |
1178 | tests, |
1179 | ) |
1180 | from ..export import get_root_name |
1181 | -from ..export.tar_exporter import export_tarball_generator |
1182 | +from ..archive.tar import tarball_generator |
1183 | from ..sixish import ( |
1184 | BytesIO, |
1185 | ) |
1186 | @@ -250,24 +250,14 @@ |
1187 | tf = tarfile.open('target.tar.bz2') |
1188 | self.assertEqual(["target/a"], tf.getnames()) |
1189 | |
1190 | - def test_xz_stdout(self): |
1191 | - wt = self.make_branch_and_tree('.') |
1192 | - self.assertRaises(errors.BzrError, export.export, wt, '-', |
1193 | - format="txz") |
1194 | - |
1195 | def test_export_tarball_generator(self): |
1196 | wt = self.make_branch_and_tree('.') |
1197 | self.build_tree(['a']) |
1198 | wt.add(["a"]) |
1199 | wt.commit("1", timestamp=42) |
1200 | target = BytesIO() |
1201 | - ball = tarfile.open(None, "w|", target) |
1202 | - wt.lock_read() |
1203 | - try: |
1204 | - for _ in export_tarball_generator(wt, ball, "bar"): |
1205 | - pass |
1206 | - finally: |
1207 | - wt.unlock() |
1208 | + with wt.lock_read(): |
1209 | + target.writelines(tarball_generator(wt, "bar")) |
1210 | # Ball should now be closed. |
1211 | target.seek(0) |
1212 | ball2 = tarfile.open(None, "r", target) |
1213 | |
1214 | === modified file 'breezy/tree.py' |
1215 | --- breezy/tree.py 2018-05-20 16:08:56 +0000 |
1216 | +++ breezy/tree.py 2018-05-20 16:08:56 +0000 |
1217 | @@ -653,19 +653,22 @@ |
1218 | searcher = default_searcher |
1219 | return searcher |
1220 | |
1221 | - def archive(self, name, format=None, root=None, subdir=None): |
1222 | + def archive(self, format, name, root='', subdir=None, |
1223 | + force_mtime=None): |
1224 | """Create an archive of this tree. |
1225 | |
1226 | :param name: target file name |
1227 | :param format: Format name (e.g. 'tar') |
1228 | :param root: Root directory name (or None) |
1229 | :param subdir: Subdirectory to export (or None) |
1230 | + :param per_file_timestamps: Whether to set the timestamp |
1231 | + for each file to the last changed time. |
1232 | :return: Iterator over archive chunks |
1233 | """ |
1234 | + from .archive import create_archive |
1235 | with self.lock_read(): |
1236 | - from .export import get_stream_export_generator |
1237 | - return get_stream_export_generator(self, name, format, root, |
1238 | - subdir) |
1239 | + return create_archive(format, self, name, root, |
1240 | + subdir, force_mtime=force_mtime) |
1241 | |
1242 | @classmethod |
1243 | def versionable_kind(cls, kind): |
Looks good, see one inline comment.