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

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
Reviewer Review Type Date Requested Status
Martin Packman Approve
Review via email: mp+345969@code.launchpad.net

Commit message

Implement GitRevisionTree.archive for remote git trees.

Description of the change

Implement GitRevisionTree.archive for remote git trees.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Looks good, see one inline comment.

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

Subscribers

People subscribed via source and target branches