Merge lp:~jelmer/bzr/export-tgz-711226 into lp:bzr

Proposed by Jelmer Vernooij
Status: Superseded
Proposed branch: lp:~jelmer/bzr/export-tgz-711226
Merge into: lp:bzr
Diff against target: 788 lines (+371/-107)
8 files modified
bzrlib/export/__init__.py (+22/-24)
bzrlib/export/dir_exporter.py (+6/-11)
bzrlib/export/tar_exporter.py (+119/-46)
bzrlib/export/zip_exporter.py (+11/-12)
bzrlib/tests/__init__.py (+0/-1)
bzrlib/tests/blackbox/test_export.py (+41/-0)
bzrlib/tests/test_export.py (+155/-12)
doc/en/release-notes/bzr-2.4.txt (+17/-1)
To merge this branch: bzr merge lp:~jelmer/bzr/export-tgz-711226
Reviewer Review Type Date Requested Status
John A Meinel Needs Fixing
Review via email: mp+53183@code.launchpad.net

This proposal has been superseded by a proposal from 2011-03-14.

Description of the change

Several export-related fixes:

 * add optional support for .tar.xz files. This uses the external lzma Python module. (bug #551714)
 * deterministic output for .tar.gz files by always using the same mtime for the contained file in a gzip file. (bug #711226)
 * add tests to verify that exporting an empty tree works (bug #236153)
 * support exporting zip files to stdout. (bug #513752)
 * add some more tests for the exporters in general
 * split out export_tarball() - this should make it possible to stream .tar.gz files to the user without overriding sys.stdout in loggerhead.
 * fix --per-file-timestamps for zip files.
 * embed just tar file basename rather than full tarfile path in .tgz files (bug #102234)

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

test_xz should be guarded with a Feature (ModuleAvailableFeature('lzma') most likely). That way when the suite finishes they get a message that the test could have been run if they had the dependency.

ZipExporter test sets a timestamp, but no timezone. You may want to force the timezone to UTC just to be safe.

A few more tweaks as discussed on IRC.

review: Needs Fixing
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Both fixed, resubmitting...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/export/__init__.py'
--- bzrlib/export/__init__.py 2010-09-21 03:20:09 +0000
+++ bzrlib/export/__init__.py 2011-03-14 12:24:22 +0000
@@ -20,9 +20,11 @@
20"""20"""
2121
22import os22import os
23import time
23from bzrlib import (24from bzrlib import (
24 errors,25 errors,
25 pyutils,26 pyutils,
27 trace,
26 )28 )
2729
28# Maps format name => export function30# Maps format name => export function
@@ -57,10 +59,10 @@
5759
58 When requesting a specific type of export, load the respective path.60 When requesting a specific type of export, load the respective path.
59 """61 """
60 def _loader(tree, dest, root, subdir, filtered, per_file_timestamps):62 def _loader(tree, dest, root, subdir, filtered, force_mtime):
61 func = pyutils.get_named_object(module, funcname)63 func = pyutils.get_named_object(module, funcname)
62 return func(tree, dest, root, subdir, filtered=filtered,64 return func(tree, dest, root, subdir, filtered=filtered,
63 per_file_timestamps=per_file_timestamps)65 force_mtime=force_mtime)
64 register_exporter(scheme, extensions, _loader)66 register_exporter(scheme, extensions, _loader)
6567
6668
@@ -103,10 +105,18 @@
103105
104 if format not in _exporters:106 if format not in _exporters:
105 raise errors.NoSuchExportFormat(format)107 raise errors.NoSuchExportFormat(format)
108
109 if not per_file_timestamps:
110 force_mtime = time.time()
111 else:
112 force_mtime = None
113
114 trace.mutter('export version %r', tree)
115
106 tree.lock_read()116 tree.lock_read()
107 try:117 try:
108 return _exporters[format](tree, dest, root, subdir, filtered=filtered,118 return _exporters[format](tree, dest, root, subdir, filtered=filtered,
109 per_file_timestamps=per_file_timestamps)119 force_mtime=force_mtime)
110 finally:120 finally:
111 tree.unlock()121 tree.unlock()
112122
@@ -114,26 +124,11 @@
114def get_root_name(dest):124def get_root_name(dest):
115 """Get just the root name for an export.125 """Get just the root name for an export.
116126
117 >>> get_root_name('../mytest.tar')
118 'mytest'
119 >>> get_root_name('mytar.tar')
120 'mytar'
121 >>> get_root_name('mytar.tar.bz2')
122 'mytar'
123 >>> get_root_name('tar.tar.tar.tgz')
124 'tar.tar.tar'
125 >>> get_root_name('bzr-0.0.5.tar.gz')
126 'bzr-0.0.5'
127 >>> get_root_name('bzr-0.0.5.zip')
128 'bzr-0.0.5'
129 >>> get_root_name('bzr-0.0.5')
130 'bzr-0.0.5'
131 >>> get_root_name('a/long/path/mytar.tgz')
132 'mytar'
133 >>> get_root_name('../parent/../dir/other.tbz2')
134 'other'
135 """127 """
136 global _exporter_extensions128 global _exporter_extensions
129 if dest == '-':
130 # Exporting to -/foo doesn't make sense so use relative paths.
131 return ''
137 dest = os.path.basename(dest)132 dest = os.path.basename(dest)
138 for ext in _exporter_extensions:133 for ext in _exporter_extensions:
139 if dest.endswith(ext):134 if dest.endswith(ext):
@@ -141,11 +136,12 @@
141 return dest136 return dest
142137
143138
144def _export_iter_entries(tree, subdir):139def _export_iter_entries(tree, subdir, skip_special=True):
145 """Iter the entries for tree suitable for exporting.140 """Iter the entries for tree suitable for exporting.
146141
147 :param tree: A tree object.142 :param tree: A tree object.
148 :param subdir: None or the path of an entry to start exporting from.143 :param subdir: None or the path of an entry to start exporting from.
144 :param skip_special: Whether to skip .bzr files.
149 """145 """
150 inv = tree.inventory146 inv = tree.inventory
151 if subdir is None:147 if subdir is None:
@@ -167,7 +163,7 @@
167 for entry in entries:163 for entry in entries:
168 # The .bzr* namespace is reserved for "magic" files like164 # The .bzr* namespace is reserved for "magic" files like
169 # .bzrignore and .bzrrules - do not export these165 # .bzrignore and .bzrrules - do not export these
170 if entry[0].startswith(".bzr"):166 if skip_special and entry[0].startswith(".bzr"):
171 continue167 continue
172 if subdir is None:168 if subdir is None:
173 if not tree.has_filename(entry[0]):169 if not tree.has_filename(entry[0]):
@@ -180,8 +176,10 @@
180176
181register_lazy_exporter(None, [], 'bzrlib.export.dir_exporter', 'dir_exporter')177register_lazy_exporter(None, [], 'bzrlib.export.dir_exporter', 'dir_exporter')
182register_lazy_exporter('dir', [], 'bzrlib.export.dir_exporter', 'dir_exporter')178register_lazy_exporter('dir', [], 'bzrlib.export.dir_exporter', 'dir_exporter')
183register_lazy_exporter('tar', ['.tar'], 'bzrlib.export.tar_exporter', 'tar_exporter')179register_lazy_exporter('tar', ['.tar'], 'bzrlib.export.tar_exporter', 'plain_tar_exporter')
184register_lazy_exporter('tgz', ['.tar.gz', '.tgz'], 'bzrlib.export.tar_exporter', 'tgz_exporter')180register_lazy_exporter('tgz', ['.tar.gz', '.tgz'], 'bzrlib.export.tar_exporter', 'tgz_exporter')
185register_lazy_exporter('tbz2', ['.tar.bz2', '.tbz2'], 'bzrlib.export.tar_exporter', 'tbz_exporter')181register_lazy_exporter('tbz2', ['.tar.bz2', '.tbz2'], 'bzrlib.export.tar_exporter', 'tbz_exporter')
182register_lazy_exporter('tlzma', ['.tar.lzma'], 'bzrlib.export.tar_exporter', 'tar_lzma_exporter')
183register_lazy_exporter('txz', ['.tar.xz'], 'bzrlib.export.tar_exporter', 'tar_xz_exporter')
186register_lazy_exporter('zip', ['.zip'], 'bzrlib.export.zip_exporter', 'zip_exporter')184register_lazy_exporter('zip', ['.zip'], 'bzrlib.export.zip_exporter', 'zip_exporter')
187185
188186
=== modified file 'bzrlib/export/dir_exporter.py'
--- bzrlib/export/dir_exporter.py 2010-05-25 17:27:52 +0000
+++ bzrlib/export/dir_exporter.py 2011-03-14 12:24:22 +0000
@@ -18,7 +18,6 @@
1818
19import errno19import errno
20import os20import os
21import time
2221
23from bzrlib import errors, osutils22from bzrlib import errors, osutils
24from bzrlib.export import _export_iter_entries23from bzrlib.export import _export_iter_entries
@@ -26,11 +25,9 @@
26 ContentFilterContext,25 ContentFilterContext,
27 filtered_output_bytes,26 filtered_output_bytes,
28 )27 )
29from bzrlib.trace import mutter28
3029
3130def dir_exporter(tree, dest, root, subdir=None, filtered=False, force_mtime=None):
32def dir_exporter(tree, dest, root, subdir, filtered=False,
33 per_file_timestamps=False):
34 """Export this tree to a new directory.31 """Export this tree to a new directory.
3532
36 `dest` should either not exist or should be empty. If it does not exist it33 `dest` should either not exist or should be empty. If it does not exist it
@@ -39,7 +36,6 @@
39 :note: If the export fails, the destination directory will be36 :note: If the export fails, the destination directory will be
40 left in an incompletely exported state: export is not transactional.37 left in an incompletely exported state: export is not transactional.
41 """38 """
42 mutter('export version %r', tree)
43 try:39 try:
44 os.mkdir(dest)40 os.mkdir(dest)
45 except OSError, e:41 except OSError, e:
@@ -76,7 +72,6 @@
76 # The data returned here can be in any order, but we've already created all72 # The data returned here can be in any order, but we've already created all
77 # the directories73 # the directories
78 flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | getattr(os, 'O_BINARY', 0)74 flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | getattr(os, 'O_BINARY', 0)
79 now = time.time()
80 for (relpath, executable), chunks in tree.iter_files_bytes(to_fetch):75 for (relpath, executable), chunks in tree.iter_files_bytes(to_fetch):
81 if filtered:76 if filtered:
82 filters = tree._content_filter_stack(relpath)77 filters = tree._content_filter_stack(relpath)
@@ -92,8 +87,8 @@
92 out.writelines(chunks)87 out.writelines(chunks)
93 finally:88 finally:
94 out.close()89 out.close()
95 if per_file_timestamps:90 if force_mtime is not None:
91 mtime = force_mtime
92 else:
96 mtime = tree.get_file_mtime(tree.path2id(relpath), relpath)93 mtime = tree.get_file_mtime(tree.path2id(relpath), relpath)
97 else:
98 mtime = now
99 os.utime(fullpath, (mtime, mtime))94 os.utime(fullpath, (mtime, mtime))
10095
=== modified file 'bzrlib/export/tar_exporter.py'
--- bzrlib/export/tar_exporter.py 2011-03-13 18:18:10 +0000
+++ bzrlib/export/tar_exporter.py 2011-03-14 12:24:22 +0000
@@ -17,6 +17,7 @@
17"""Export a Tree to a non-versioned directory.17"""Export a Tree to a non-versioned directory.
18"""18"""
1919
20import os
20import StringIO21import StringIO
21import sys22import sys
22import tarfile23import tarfile
@@ -24,7 +25,6 @@
2425
25from bzrlib import (26from bzrlib import (
26 errors,27 errors,
27 export,
28 osutils,28 osutils,
29 )29 )
30from bzrlib.export import _export_iter_entries30from bzrlib.export import _export_iter_entries
@@ -32,41 +32,26 @@
32 ContentFilterContext,32 ContentFilterContext,
33 filtered_output_bytes,33 filtered_output_bytes,
34 )34 )
35from bzrlib.trace import mutter35
3636
3737def export_tarball(tree, ball, root, subdir=None, filtered=False,
38def tar_exporter(tree, dest, root, subdir, compression=None, filtered=False,38 force_mtime=None):
39 per_file_timestamps=False):39 """Export tree contents to a tarball.
40 """Export this tree to a new tar file.40
4141 :param tree: Tree to export
42 `dest` will be created holding the contents of this tree; if it42 :param ball: Tarball to export to
43 already exists, it will be clobbered, like with "tar -c".43 :param filtered: Whether to apply filters
44 :param subdir: Sub directory to export
45 :param force_mtime: Option mtime to force, instead of using
46 tree timestamps.
44 """47 """
45 mutter('export version %r', tree)
46 now = time.time()
47 compression = str(compression or '')
48 if dest == '-':
49 # XXX: If no root is given, the output tarball will contain files
50 # named '-/foo'; perhaps this is the most reasonable thing.
51 ball = tarfile.open(None, 'w|' + compression, sys.stdout)
52 else:
53 if root is None:
54 root = export.get_root_name(dest)
55
56 # tarfile.open goes on to do 'os.getcwd() + dest' for opening
57 # the tar file. With dest being unicode, this throws UnicodeDecodeError
58 # unless we encode dest before passing it on. This works around
59 # upstream python bug http://bugs.python.org/issue8396
60 # (fixed in Python 2.6.5 and 2.7b1)
61 ball = tarfile.open(dest.encode(osutils._fs_enc), 'w:' + compression)
62
63 for dp, ie in _export_iter_entries(tree, subdir):48 for dp, ie in _export_iter_entries(tree, subdir):
64 filename = osutils.pathjoin(root, dp).encode('utf8')49 filename = osutils.pathjoin(root, dp).encode('utf8')
65 item = tarfile.TarInfo(filename)50 item = tarfile.TarInfo(filename)
66 if per_file_timestamps:51 if force_mtime is not None:
52 item.mtime = force_mtime
53 else:
67 item.mtime = tree.get_file_mtime(ie.file_id, dp)54 item.mtime = tree.get_file_mtime(ie.file_id, dp)
68 else:
69 item.mtime = now
70 if ie.kind == "file":55 if ie.kind == "file":
71 item.type = tarfile.REGTYPE56 item.type = tarfile.REGTYPE
72 if tree.is_executable(ie.file_id):57 if tree.is_executable(ie.file_id):
@@ -82,7 +67,7 @@
82 item.size = len(content)67 item.size = len(content)
83 fileobj = StringIO.StringIO(content)68 fileobj = StringIO.StringIO(content)
84 else:69 else:
85 item.size = ie.text_size70 item.size = tree.get_file_size(ie.file_id)
86 fileobj = tree.get_file(ie.file_id)71 fileobj = tree.get_file(ie.file_id)
87 elif ie.kind == "directory":72 elif ie.kind == "directory":
88 item.type = tarfile.DIRTYPE73 item.type = tarfile.DIRTYPE
@@ -94,22 +79,110 @@
94 item.type = tarfile.SYMTYPE79 item.type = tarfile.SYMTYPE
95 item.size = 080 item.size = 0
96 item.mode = 075581 item.mode = 0755
97 item.linkname = ie.symlink_target82 item.linkname = tree.get_symlink_target(ie.file_id)
98 fileobj = None83 fileobj = None
99 else:84 else:
100 raise errors.BzrError("don't know how to export {%s} of kind %r" %85 raise errors.BzrError("don't know how to export {%s} of kind %r" %
101 (ie.file_id, ie.kind))86 (ie.file_id, ie.kind))
102 ball.addfile(item, fileobj)87 ball.addfile(item, fileobj)
103 ball.close()88
10489
10590def tgz_exporter(tree, dest, root, subdir, filtered=False, force_mtime=None):
106def tgz_exporter(tree, dest, root, subdir, filtered=False,91 """Export this tree to a new tar file.
107 per_file_timestamps=False):92
108 tar_exporter(tree, dest, root, subdir, compression='gz',93 `dest` will be created holding the contents of this tree; if it
109 filtered=filtered, per_file_timestamps=per_file_timestamps)94 already exists, it will be clobbered, like with "tar -c".
11095 """
11196 import gzip
112def tbz_exporter(tree, dest, root, subdir, filtered=False,97 if force_mtime is not None:
113 per_file_timestamps=False):98 root_mtime = force_mtime
114 tar_exporter(tree, dest, root, subdir, compression='bz2',99 elif (getattr(tree, "repository", None) and
115 filtered=filtered, per_file_timestamps=per_file_timestamps)100 getattr(tree, "get_revision_id", None)):
101 # If this is a revision tree, use the revisions' timestamp
102 rev = tree.repository.get_revision(tree.get_revision_id())
103 root_mtime = rev.timestamp
104 elif tree.get_root_id() is not None:
105 root_mtime = tree.get_file_mtime(tree.get_root_id())
106 else:
107 root_mtime = time.time()
108 if dest == '-':
109 stream = gzip.GzipFile(None, mode='w', mtime=root_mtime,
110 fileobj=sys.stdout)
111 else:
112 stream = open(dest.encode(osutils._fs_enc), 'w')
113 # gzip file is used with an explicit fileobj so that
114 # the basename can be stored in the gzip file rather than
115 # dest. (bug 102234)
116 stream = gzip.GzipFile(os.path.basename(dest), 'w',
117 mtime=root_mtime, fileobj=stream)
118 ball = tarfile.open(None, 'w|', fileobj=stream)
119 export_tarball(tree, ball, root, subdir, filtered=filtered,
120 force_mtime=force_mtime)
121 ball.close()
122
123
124def tbz_exporter(tree, dest, root, subdir, filtered=False, force_mtime=None):
125 """Export this tree to a new tar file.
126
127 `dest` will be created holding the contents of this tree; if it
128 already exists, it will be clobbered, like with "tar -c".
129 """
130 if dest == '-':
131 ball = tarfile.open(None, 'w|bz2', sys.stdout)
132 else:
133 # tarfile.open goes on to do 'os.getcwd() + dest' for opening
134 # the tar file. With dest being unicode, this throws UnicodeDecodeError
135 # unless we encode dest before passing it on. This works around
136 # upstream python bug http://bugs.python.org/issue8396
137 # (fixed in Python 2.6.5 and 2.7b1)
138 ball = tarfile.open(dest.encode(osutils._fs_enc), 'w:bz2')
139 export_tarball(tree, ball, root, subdir, filtered=filtered,
140 force_mtime=force_mtime)
141 ball.close()
142
143
144def plain_tar_exporter(tree, dest, root, subdir, compression=None,
145 filtered=False, force_mtime=None):
146 """Export this tree to a new tar file.
147
148 `dest` will be created holding the contents of this tree; if it
149 already exists, it will be clobbered, like with "tar -c".
150 """
151 if dest == '-':
152 stream = sys.stdout
153 else:
154 stream = open(dest.encode(osutils._fs_enc), 'w')
155 ball = tarfile.open(None, 'w|', stream)
156 export_tarball(tree, ball, root, subdir, filtered=filtered,
157 force_mtime=force_mtime)
158 ball.close()
159
160
161def tar_xz_exporter(tree, dest, root, subdir, filtered=False,
162 force_mtime=None):
163 return tar_lzma_exporter(tree, dest, root, subdir, filtered=filtered,
164 force_mtime=force_mtime, compression_format="xz")
165
166
167def tar_lzma_exporter(tree, dest, root, subdir, filtered=False, force_mtime=None, compression_format="lzma"):
168 """Export this tree to a new .tar.lzma file.
169
170 `dest` will be created holding the contents of this tree; if it
171 already exists, it will be clobbered, like with "tar -c".
172 """
173 if dest == '-':
174 raise errors.BzrError("Writing to stdout not supported for .tar.lzma")
175
176 try:
177 import lzma
178 except ImportError, e:
179 raise errors.DependencyNotPresent('lzma', e)
180
181 assert compression_format in ("lzma", "xz")
182 stream = lzma.LZMAFile(dest.encode(osutils._fs_enc), 'w',
183 options={"format": compression_format})
184 ball = tarfile.open(None, 'w:', fileobj=stream)
185 export_tarball(tree, ball, root, subdir, filtered=filtered,
186 force_mtime=force_mtime)
187 ball.close()
188
116189
=== modified file 'bzrlib/export/zip_exporter.py'
--- bzrlib/export/zip_exporter.py 2011-02-16 17:20:10 +0000
+++ bzrlib/export/zip_exporter.py 2011-03-14 12:24:22 +0000
@@ -19,6 +19,7 @@
1919
20import os20import os
21import stat21import stat
22import sys
22import time23import time
23import zipfile24import zipfile
2425
@@ -43,20 +44,17 @@
43_DIR_ATTR = stat.S_IFDIR | ZIP_DIRECTORY_BIT | DIR_PERMISSIONS44_DIR_ATTR = stat.S_IFDIR | ZIP_DIRECTORY_BIT | DIR_PERMISSIONS
4445
4546
46def zip_exporter(tree, dest, root, subdir, filtered=False,47def zip_exporter(tree, dest, root, subdir=None, filtered=False, force_mtime=None):
47 per_file_timestamps=False):
48 """ Export this tree to a new zip file.48 """ Export this tree to a new zip file.
4949
50 `dest` will be created holding the contents of this tree; if it50 `dest` will be created holding the contents of this tree; if it
51 already exists, it will be overwritten".51 already exists, it will be overwritten".
52 """52 """
53 mutter('export version %r', tree)
54
55 now = time.localtime()[:6]
5653
57 compression = zipfile.ZIP_DEFLATED54 compression = zipfile.ZIP_DEFLATED
55 if dest == "-":
56 dest = sys.stdout
58 zipf = zipfile.ZipFile(dest, "w", compression)57 zipf = zipfile.ZipFile(dest, "w", compression)
59
60 try:58 try:
61 for dp, ie in _export_iter_entries(tree, subdir):59 for dp, ie in _export_iter_entries(tree, subdir):
62 file_id = ie.file_id60 file_id = ie.file_id
@@ -64,15 +62,16 @@
6462
65 # zipfile.ZipFile switches all paths to forward63 # zipfile.ZipFile switches all paths to forward
66 # slashes anyway, so just stick with that.64 # slashes anyway, so just stick with that.
67 if per_file_timestamps:65 if force_mtime is not None:
66 mtime = force_mtime
67 else:
68 mtime = tree.get_file_mtime(ie.file_id, dp)68 mtime = tree.get_file_mtime(ie.file_id, dp)
69 else:69 date_time = time.localtime(mtime)[:6]
70 mtime = now
71 filename = osutils.pathjoin(root, dp).encode('utf8')70 filename = osutils.pathjoin(root, dp).encode('utf8')
72 if ie.kind == "file":71 if ie.kind == "file":
73 zinfo = zipfile.ZipInfo(72 zinfo = zipfile.ZipInfo(
74 filename=filename,73 filename=filename,
75 date_time=mtime)74 date_time=date_time)
76 zinfo.compress_type = compression75 zinfo.compress_type = compression
77 zinfo.external_attr = _FILE_ATTR76 zinfo.external_attr = _FILE_ATTR
78 if filtered:77 if filtered:
@@ -90,14 +89,14 @@
90 # not just empty files.89 # not just empty files.
91 zinfo = zipfile.ZipInfo(90 zinfo = zipfile.ZipInfo(
92 filename=filename + '/',91 filename=filename + '/',
93 date_time=mtime)92 date_time=date_time)
94 zinfo.compress_type = compression93 zinfo.compress_type = compression
95 zinfo.external_attr = _DIR_ATTR94 zinfo.external_attr = _DIR_ATTR
96 zipf.writestr(zinfo,'')95 zipf.writestr(zinfo,'')
97 elif ie.kind == "symlink":96 elif ie.kind == "symlink":
98 zinfo = zipfile.ZipInfo(97 zinfo = zipfile.ZipInfo(
99 filename=(filename + '.lnk'),98 filename=(filename + '.lnk'),
100 date_time=mtime)99 date_time=date_time)
101 zinfo.compress_type = compression100 zinfo.compress_type = compression
102 zinfo.external_attr = _FILE_ATTR101 zinfo.external_attr = _FILE_ATTR
103 zipf.writestr(zinfo, ie.symlink_target)102 zipf.writestr(zinfo, ie.symlink_target)
104103
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2011-03-10 13:29:54 +0000
+++ bzrlib/tests/__init__.py 2011-03-14 12:24:22 +0000
@@ -3903,7 +3903,6 @@
3903 'bzrlib',3903 'bzrlib',
3904 'bzrlib.branchbuilder',3904 'bzrlib.branchbuilder',
3905 'bzrlib.decorators',3905 'bzrlib.decorators',
3906 'bzrlib.export',
3907 'bzrlib.inventory',3906 'bzrlib.inventory',
3908 'bzrlib.iterablefile',3907 'bzrlib.iterablefile',
3909 'bzrlib.lockdir',3908 'bzrlib.lockdir',
39103909
=== modified file 'bzrlib/tests/blackbox/test_export.py'
--- bzrlib/tests/blackbox/test_export.py 2011-02-16 17:20:10 +0000
+++ bzrlib/tests/blackbox/test_export.py 2011-03-14 12:24:22 +0000
@@ -117,6 +117,36 @@
117 # '.bzrignore'.117 # '.bzrignore'.
118 self.assertEqual(['test/a'], sorted(zfile.namelist()))118 self.assertEqual(['test/a'], sorted(zfile.namelist()))
119119
120 def test_zip_export_stdout(self):
121 tree = self.make_branch_and_tree('zip')
122 self.build_tree(['zip/a'])
123 tree.add('a')
124 tree.commit('1')
125 os.chdir('zip')
126 contents = self.run_bzr('export --format=zip -')[0]
127 zfile = zipfile.ZipFile(StringIO(contents))
128 self.assertEqual(['a'], sorted(zfile.namelist()))
129
130 def test_tgz_export_stdout(self):
131 tree = self.make_branch_and_tree('z')
132 self.build_tree(['z/a'])
133 tree.add('a')
134 tree.commit('1')
135 os.chdir('z')
136 contents = self.run_bzr('export --format=tgz -')[0]
137 ball = tarfile.open(mode='r|gz', fileobj=StringIO(contents))
138 self.assertEqual(['a'], ball.getnames())
139
140 def test_tbz2_export_stdout(self):
141 tree = self.make_branch_and_tree('z')
142 self.build_tree(['z/a'])
143 tree.add('a')
144 tree.commit('1')
145 os.chdir('z')
146 contents = self.run_bzr('export --format=tbz2 -')[0]
147 ball = tarfile.open(mode='r|bz2', fileobj=StringIO(contents))
148 self.assertEqual(['a'], ball.getnames())
149
120 def test_zip_export_unicode(self):150 def test_zip_export_unicode(self):
121 self.requireFeature(tests.UnicodeFilenameFeature)151 self.requireFeature(tests.UnicodeFilenameFeature)
122 tree = self.make_branch_and_tree('zip')152 tree = self.make_branch_and_tree('zip')
@@ -317,3 +347,14 @@
317 self.run_bzr(['export', '--directory=branch', 'latest'])347 self.run_bzr(['export', '--directory=branch', 'latest'])
318 self.assertEqual(['goodbye', 'hello'], sorted(os.listdir('latest')))348 self.assertEqual(['goodbye', 'hello'], sorted(os.listdir('latest')))
319 self.check_file_contents('latest/goodbye', 'baz')349 self.check_file_contents('latest/goodbye', 'baz')
350
351 def test_zip_export_per_file_timestamps(self):
352 tree = self.example_branch()
353 self.build_tree_contents([('branch/har', 'foo')])
354 tree.add('har')
355 # Earliest allowable date on FAT32 filesystems is 1980-01-01
356 tree.commit('setup', timestamp=315532800)
357 self.run_bzr('export --per-file-timestamps test.zip branch')
358 zfile = zipfile.ZipFile('test.zip')
359 info = zfile.getinfo("test/har")
360 self.assertEquals((1980, 1, 1, 1, 0, 0), info.date_time)
320361
=== modified file 'bzrlib/tests/test_export.py'
--- bzrlib/tests/test_export.py 2010-04-14 00:11:32 +0000
+++ bzrlib/tests/test_export.py 2011-03-14 12:24:22 +0000
@@ -14,19 +14,26 @@
14# along with this program; if not, write to the Free Software14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1616
17"""Tests for bzrlib.export."""
18
19from cStringIO import StringIO
17import os20import os
21import tarfile
18import time22import time
23import zipfile
1924
20from bzrlib import (25from bzrlib import (
21 errors,26 errors,
22 export,27 export,
23 tests,28 tests,
24 )29 )
2530from bzrlib.export import get_root_name
2631from bzrlib.export.tar_exporter import export_tarball
27class TestExport(tests.TestCaseWithTransport):32
2833
29 def test_dir_export_missing_file(self):34class TestDirExport(tests.TestCaseWithTransport):
35
36 def test_missing_file(self):
30 self.build_tree(['a/', 'a/b', 'a/c'])37 self.build_tree(['a/', 'a/b', 'a/c'])
31 wt = self.make_branch_and_tree('.')38 wt = self.make_branch_and_tree('.')
32 wt.add(['a', 'a/b', 'a/c'])39 wt.add(['a', 'a/b', 'a/c'])
@@ -35,7 +42,12 @@
35 self.failUnlessExists('target/a/b')42 self.failUnlessExists('target/a/b')
36 self.failIfExists('target/a/c')43 self.failIfExists('target/a/c')
3744
38 def test_dir_export_symlink(self):45 def test_empty(self):
46 wt = self.make_branch_and_tree('.')
47 export.export(wt, 'target', format="dir")
48 self.assertEquals([], os.listdir("target"))
49
50 def test_symlink(self):
39 self.requireFeature(tests.SymlinkFeature)51 self.requireFeature(tests.SymlinkFeature)
40 wt = self.make_branch_and_tree('.')52 wt = self.make_branch_and_tree('.')
41 os.symlink('source', 'link')53 os.symlink('source', 'link')
@@ -43,7 +55,7 @@
43 export.export(wt, 'target', format="dir")55 export.export(wt, 'target', format="dir")
44 self.failUnlessExists('target/link')56 self.failUnlessExists('target/link')
4557
46 def test_dir_export_to_existing_empty_dir_success(self):58 def test_to_existing_empty_dir_success(self):
47 self.build_tree(['source/', 'source/a', 'source/b/', 'source/b/c'])59 self.build_tree(['source/', 'source/a', 'source/b/', 'source/b/c'])
48 wt = self.make_branch_and_tree('source')60 wt = self.make_branch_and_tree('source')
49 wt.add(['a', 'b', 'b/c'])61 wt.add(['a', 'b', 'b/c'])
@@ -54,7 +66,7 @@
54 self.failUnlessExists('target/b')66 self.failUnlessExists('target/b')
55 self.failUnlessExists('target/b/c')67 self.failUnlessExists('target/b/c')
5668
57 def test_dir_export_to_existing_nonempty_dir_fail(self):69 def test_to_existing_nonempty_dir_fail(self):
58 self.build_tree(['source/', 'source/a', 'source/b/', 'source/b/c'])70 self.build_tree(['source/', 'source/a', 'source/b/', 'source/b/c'])
59 wt = self.make_branch_and_tree('source')71 wt = self.make_branch_and_tree('source')
60 wt.add(['a', 'b', 'b/c'])72 wt.add(['a', 'b', 'b/c'])
@@ -62,7 +74,7 @@
62 self.build_tree(['target/', 'target/foo'])74 self.build_tree(['target/', 'target/foo'])
63 self.assertRaises(errors.BzrError, export.export, wt, 'target', format="dir")75 self.assertRaises(errors.BzrError, export.export, wt, 'target', format="dir")
6476
65 def test_dir_export_existing_single_file(self):77 def test_existing_single_file(self):
66 self.build_tree(['dir1/', 'dir1/dir2/', 'dir1/first', 'dir1/dir2/second'])78 self.build_tree(['dir1/', 'dir1/dir2/', 'dir1/first', 'dir1/dir2/second'])
67 wtree = self.make_branch_and_tree('dir1')79 wtree = self.make_branch_and_tree('dir1')
68 wtree.add(['dir2', 'first', 'dir2/second'])80 wtree.add(['dir2', 'first', 'dir2/second'])
@@ -71,8 +83,8 @@
71 self.failUnlessExists('target1/first')83 self.failUnlessExists('target1/first')
72 export.export(wtree, 'target2', format='dir', subdir='dir2/second')84 export.export(wtree, 'target2', format='dir', subdir='dir2/second')
73 self.failUnlessExists('target2/second')85 self.failUnlessExists('target2/second')
74 86
75 def test_dir_export_files_same_timestamp(self):87 def test_files_same_timestamp(self):
76 builder = self.make_branch_builder('source')88 builder = self.make_branch_builder('source')
77 builder.start_series()89 builder.start_series()
78 builder.build_snapshot(None, None, [90 builder.build_snapshot(None, None, [
@@ -99,7 +111,7 @@
99 # All files must be given the same mtime.111 # All files must be given the same mtime.
100 self.assertEqual(st_a.st_mtime, st_b.st_mtime)112 self.assertEqual(st_a.st_mtime, st_b.st_mtime)
101113
102 def test_dir_export_files_per_file_timestamps(self):114 def test_files_per_file_timestamps(self):
103 builder = self.make_branch_builder('source')115 builder = self.make_branch_builder('source')
104 builder.start_series()116 builder.start_series()
105 # Earliest allowable date on FAT32 filesystems is 1980-01-01117 # Earliest allowable date on FAT32 filesystems is 1980-01-01
@@ -121,3 +133,134 @@
121 t = self.get_transport('target')133 t = self.get_transport('target')
122 self.assertEqual(a_time, t.stat('a').st_mtime)134 self.assertEqual(a_time, t.stat('a').st_mtime)
123 self.assertEqual(b_time, t.stat('b').st_mtime)135 self.assertEqual(b_time, t.stat('b').st_mtime)
136
137
138class TarExporterTests(tests.TestCaseWithTransport):
139
140 def test_empty(self):
141 wt = self.make_branch_and_tree('.')
142 export.export(wt, 'target.tar', format="tar")
143 tf = tarfile.open('target.tar')
144 self.assertEquals([], tf.getnames())
145
146 def test_xz(self):
147 wt = self.make_branch_and_tree('.')
148 self.build_tree(['a'])
149 wt.add(["a"])
150 wt.commit("1")
151 try:
152 export.export(wt, 'target.tar.xz', format="txz")
153 except errors.DependencyNotPresent:
154 raise tests.TestSkipped("lzma module not available")
155 import lzma
156 tf = tarfile.open(fileobj=lzma.LZMAFile('target.tar.xz'))
157 self.assertEquals(["target/a"], tf.getnames())
158
159 def test_lzma(self):
160 wt = self.make_branch_and_tree('.')
161 self.build_tree(['a'])
162 wt.add(["a"])
163 wt.commit("1")
164 try:
165 export.export(wt, 'target.tar.lzma', format="tlzma")
166 except errors.DependencyNotPresent:
167 raise tests.TestSkipped("lzma module not available")
168 import lzma
169 tf = tarfile.open(fileobj=lzma.LZMAFile('target.tar.lzma'))
170 self.assertEquals(["target/a"], tf.getnames())
171
172 def test_tgz(self):
173 wt = self.make_branch_and_tree('.')
174 self.build_tree(['a'])
175 wt.add(["a"])
176 wt.commit("1")
177 export.export(wt, 'target.tar.gz', format="tgz")
178 tf = tarfile.open('target.tar.gz')
179 self.assertEquals(["target/a"], tf.getnames())
180
181 def test_tgz_ignores_dest_path(self):
182 # The target path should not be a part of the target file.
183 # (bug #102234)
184 wt = self.make_branch_and_tree('.')
185 self.build_tree(['a'])
186 wt.add(["a"])
187 wt.commit("1")
188 os.mkdir("testdir1")
189 os.mkdir("testdir2")
190 export.export(wt, 'testdir1/target.tar.gz', format="tgz",
191 per_file_timestamps=True)
192 export.export(wt, 'testdir2/target.tar.gz', format="tgz",
193 per_file_timestamps=True)
194 file1 = open('testdir1/target.tar.gz', 'r')
195 self.addCleanup(file1.close)
196 file2 = open('testdir1/target.tar.gz', 'r')
197 self.addCleanup(file2.close)
198 content1 = file1.read()
199 content2 = file2.read()
200 self.assertEqualDiff(content1, content2)
201 # the gzip module doesn't have a way to read back to the original
202 # filename, but it's stored as-is in the tarfile.
203 self.assertFalse("testdir1" in content1)
204 self.assertFalse("target.tar.gz" in content1)
205 self.assertTrue("target.tar" in content1)
206
207 def test_tbz2(self):
208 wt = self.make_branch_and_tree('.')
209 self.build_tree(['a'])
210 wt.add(["a"])
211 wt.commit("1")
212 export.export(wt, 'target.tar.bz2', format="tbz2")
213 tf = tarfile.open('target.tar.bz2')
214 self.assertEquals(["target/a"], tf.getnames())
215
216 def test_xz_stdout(self):
217 wt = self.make_branch_and_tree('.')
218 self.assertRaises(errors.BzrError, export.export, wt, '-',
219 format="txz")
220
221 def test_export_tarball(self):
222 wt = self.make_branch_and_tree('.')
223 self.build_tree(['a'])
224 wt.add(["a"])
225 wt.commit("1", timestamp=42)
226 target = StringIO()
227 ball = tarfile.open(None, "w|", target)
228 wt.lock_read()
229 try:
230 export_tarball(wt, ball, "bar", subdir=None)
231 finally:
232 wt.unlock()
233 self.assertEquals(["bar/a"], ball.getnames())
234 ball.close()
235
236
237class ZipExporterTests(tests.TestCaseWithTransport):
238
239 def test_per_file_timestamps(self):
240 tree = self.make_branch_and_tree('.')
241 self.build_tree_contents([('har', 'foo')])
242 tree.add('har')
243 # Earliest allowable date on FAT32 filesystems is 1980-01-01
244 tree.commit('setup', timestamp=315532800)
245 export.export(tree.basis_tree(), 'test.zip', format='zip',
246 per_file_timestamps=True)
247 zfile = zipfile.ZipFile('test.zip')
248 info = zfile.getinfo("test/har")
249 self.assertEquals((1980, 1, 1, 1, 0, 0), info.date_time)
250
251
252
253class RootNameTests(tests.TestCase):
254
255 def test_root_name(self):
256 self.assertEquals('mytest', get_root_name('../mytest.tar'))
257 self.assertEquals('mytar', get_root_name('mytar.tar'))
258 self.assertEquals('mytar', get_root_name('mytar.tar.bz2'))
259 self.assertEquals('tar.tar.tar', get_root_name('tar.tar.tar.tgz'))
260 self.assertEquals('bzr-0.0.5', get_root_name('bzr-0.0.5.tar.gz'))
261 self.assertEquals('bzr-0.0.5', get_root_name('bzr-0.0.5.zip'))
262 self.assertEquals('bzr-0.0.5', get_root_name('bzr-0.0.5'))
263 self.assertEquals('mytar', get_root_name('a/long/path/mytar.tgz'))
264 self.assertEquals('other',
265 get_root_name('../parent/../dir/other.tbz2'))
266 self.assertEquals('', get_root_name('-'))
124267
=== modified file 'doc/en/release-notes/bzr-2.4.txt'
--- doc/en/release-notes/bzr-2.4.txt 2011-03-11 15:36:12 +0000
+++ doc/en/release-notes/bzr-2.4.txt 2011-03-14 12:24:22 +0000
@@ -51,7 +51,20 @@
51* Branching, merging and pulling a branch now copies revisions named in51* Branching, merging and pulling a branch now copies revisions named in
52 tags, not just the tag metadata. (Andrew Bennetts, #309682)52 tags, not just the tag metadata. (Andrew Bennetts, #309682)
53 53
54* ``bzr cat-revision`` no longer requires a working tree. (Jelmer Vernooij, #704405)54* ``bzr cat-revision`` no longer requires a working tree.
55 (Jelmer Vernooij, #704405)
56
57* ``bzr export --per-file-timestamps`` for .tar.gz files will now use the
58 root entry's last change time as the tar file mtime. This makes the
59 output of ``bzr export --per-file-timestamps`` for a particular tree
60 deterministic. (Jelmer Vernooij, #711226)
61
62* ``bzr export --format=zip`` can now export to standard output,
63 like the other exporters can. (Jelmer Vernooij, #513752)
64
65* ``bzr export`` can now create ``.tar.xz`` and ``.tar.lzma`` files.
66 (Jelmer Vernooij, #551714)
67
5568
56Bug Fixes69Bug Fixes
57*********70*********
@@ -69,6 +82,9 @@
69* ``bzr export`` to zip files will now set a mode on directories.82* ``bzr export`` to zip files will now set a mode on directories.
70 (Jelmer Vernooij, #207253)83 (Jelmer Vernooij, #207253)
7184
85* ``bzr export`` to tgz files will only write out the basename of the
86 tarfile to the gzip file. (Jelmer Vernooij, #102234)
87
72* ``bzr push --overwrite`` with an older revision specified will now correctly88* ``bzr push --overwrite`` with an older revision specified will now correctly
73 roll back the target branch. (Jelmer Vernooij, #386576)89 roll back the target branch. (Jelmer Vernooij, #386576)
7490