Merge lp:~jelmer/brz/hpss-archive into lp:brz

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
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):

Subscribers

People subscribed via source and target branches