Merge lp:~jelmer/bzr/export-tgz-711226 into lp:bzr
- export-tgz-711226
- Merge into bzr.dev
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 | ||||||||||||||||
Related bugs: |
|
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.
Commit message
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-
* embed just tar file basename rather than full tarfile path in .tgz files (bug #102234)
Jelmer Vernooij (jelmer) wrote : | # |
Both fixed, resubmitting...
Preview Diff
1 | === modified file 'bzrlib/export/__init__.py' | |||
2 | --- bzrlib/export/__init__.py 2010-09-21 03:20:09 +0000 | |||
3 | +++ bzrlib/export/__init__.py 2011-03-14 12:24:22 +0000 | |||
4 | @@ -20,9 +20,11 @@ | |||
5 | 20 | """ | 20 | """ |
6 | 21 | 21 | ||
7 | 22 | import os | 22 | import os |
8 | 23 | import time | ||
9 | 23 | from bzrlib import ( | 24 | from bzrlib import ( |
10 | 24 | errors, | 25 | errors, |
11 | 25 | pyutils, | 26 | pyutils, |
12 | 27 | trace, | ||
13 | 26 | ) | 28 | ) |
14 | 27 | 29 | ||
15 | 28 | # Maps format name => export function | 30 | # Maps format name => export function |
16 | @@ -57,10 +59,10 @@ | |||
17 | 57 | 59 | ||
18 | 58 | When requesting a specific type of export, load the respective path. | 60 | When requesting a specific type of export, load the respective path. |
19 | 59 | """ | 61 | """ |
21 | 60 | def _loader(tree, dest, root, subdir, filtered, per_file_timestamps): | 62 | def _loader(tree, dest, root, subdir, filtered, force_mtime): |
22 | 61 | func = pyutils.get_named_object(module, funcname) | 63 | func = pyutils.get_named_object(module, funcname) |
23 | 62 | return func(tree, dest, root, subdir, filtered=filtered, | 64 | return func(tree, dest, root, subdir, filtered=filtered, |
25 | 63 | per_file_timestamps=per_file_timestamps) | 65 | force_mtime=force_mtime) |
26 | 64 | register_exporter(scheme, extensions, _loader) | 66 | register_exporter(scheme, extensions, _loader) |
27 | 65 | 67 | ||
28 | 66 | 68 | ||
29 | @@ -103,10 +105,18 @@ | |||
30 | 103 | 105 | ||
31 | 104 | if format not in _exporters: | 106 | if format not in _exporters: |
32 | 105 | raise errors.NoSuchExportFormat(format) | 107 | raise errors.NoSuchExportFormat(format) |
33 | 108 | |||
34 | 109 | if not per_file_timestamps: | ||
35 | 110 | force_mtime = time.time() | ||
36 | 111 | else: | ||
37 | 112 | force_mtime = None | ||
38 | 113 | |||
39 | 114 | trace.mutter('export version %r', tree) | ||
40 | 115 | |||
41 | 106 | tree.lock_read() | 116 | tree.lock_read() |
42 | 107 | try: | 117 | try: |
43 | 108 | return _exporters[format](tree, dest, root, subdir, filtered=filtered, | 118 | return _exporters[format](tree, dest, root, subdir, filtered=filtered, |
45 | 109 | per_file_timestamps=per_file_timestamps) | 119 | force_mtime=force_mtime) |
46 | 110 | finally: | 120 | finally: |
47 | 111 | tree.unlock() | 121 | tree.unlock() |
48 | 112 | 122 | ||
49 | @@ -114,26 +124,11 @@ | |||
50 | 114 | def get_root_name(dest): | 124 | def get_root_name(dest): |
51 | 115 | """Get just the root name for an export. | 125 | """Get just the root name for an export. |
52 | 116 | 126 | ||
53 | 117 | >>> get_root_name('../mytest.tar') | ||
54 | 118 | 'mytest' | ||
55 | 119 | >>> get_root_name('mytar.tar') | ||
56 | 120 | 'mytar' | ||
57 | 121 | >>> get_root_name('mytar.tar.bz2') | ||
58 | 122 | 'mytar' | ||
59 | 123 | >>> get_root_name('tar.tar.tar.tgz') | ||
60 | 124 | 'tar.tar.tar' | ||
61 | 125 | >>> get_root_name('bzr-0.0.5.tar.gz') | ||
62 | 126 | 'bzr-0.0.5' | ||
63 | 127 | >>> get_root_name('bzr-0.0.5.zip') | ||
64 | 128 | 'bzr-0.0.5' | ||
65 | 129 | >>> get_root_name('bzr-0.0.5') | ||
66 | 130 | 'bzr-0.0.5' | ||
67 | 131 | >>> get_root_name('a/long/path/mytar.tgz') | ||
68 | 132 | 'mytar' | ||
69 | 133 | >>> get_root_name('../parent/../dir/other.tbz2') | ||
70 | 134 | 'other' | ||
71 | 135 | """ | 127 | """ |
72 | 136 | global _exporter_extensions | 128 | global _exporter_extensions |
73 | 129 | if dest == '-': | ||
74 | 130 | # Exporting to -/foo doesn't make sense so use relative paths. | ||
75 | 131 | return '' | ||
76 | 137 | dest = os.path.basename(dest) | 132 | dest = os.path.basename(dest) |
77 | 138 | for ext in _exporter_extensions: | 133 | for ext in _exporter_extensions: |
78 | 139 | if dest.endswith(ext): | 134 | if dest.endswith(ext): |
79 | @@ -141,11 +136,12 @@ | |||
80 | 141 | return dest | 136 | return dest |
81 | 142 | 137 | ||
82 | 143 | 138 | ||
84 | 144 | def _export_iter_entries(tree, subdir): | 139 | def _export_iter_entries(tree, subdir, skip_special=True): |
85 | 145 | """Iter the entries for tree suitable for exporting. | 140 | """Iter the entries for tree suitable for exporting. |
86 | 146 | 141 | ||
87 | 147 | :param tree: A tree object. | 142 | :param tree: A tree object. |
88 | 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. |
89 | 144 | :param skip_special: Whether to skip .bzr files. | ||
90 | 149 | """ | 145 | """ |
91 | 150 | inv = tree.inventory | 146 | inv = tree.inventory |
92 | 151 | if subdir is None: | 147 | if subdir is None: |
93 | @@ -167,7 +163,7 @@ | |||
94 | 167 | for entry in entries: | 163 | for entry in entries: |
95 | 168 | # The .bzr* namespace is reserved for "magic" files like | 164 | # The .bzr* namespace is reserved for "magic" files like |
96 | 169 | # .bzrignore and .bzrrules - do not export these | 165 | # .bzrignore and .bzrrules - do not export these |
98 | 170 | if entry[0].startswith(".bzr"): | 166 | if skip_special and entry[0].startswith(".bzr"): |
99 | 171 | continue | 167 | continue |
100 | 172 | if subdir is None: | 168 | if subdir is None: |
101 | 173 | if not tree.has_filename(entry[0]): | 169 | if not tree.has_filename(entry[0]): |
102 | @@ -180,8 +176,10 @@ | |||
103 | 180 | 176 | ||
104 | 181 | register_lazy_exporter(None, [], 'bzrlib.export.dir_exporter', 'dir_exporter') | 177 | register_lazy_exporter(None, [], 'bzrlib.export.dir_exporter', 'dir_exporter') |
105 | 182 | register_lazy_exporter('dir', [], 'bzrlib.export.dir_exporter', 'dir_exporter') | 178 | register_lazy_exporter('dir', [], 'bzrlib.export.dir_exporter', 'dir_exporter') |
107 | 183 | register_lazy_exporter('tar', ['.tar'], 'bzrlib.export.tar_exporter', 'tar_exporter') | 179 | register_lazy_exporter('tar', ['.tar'], 'bzrlib.export.tar_exporter', 'plain_tar_exporter') |
108 | 184 | register_lazy_exporter('tgz', ['.tar.gz', '.tgz'], 'bzrlib.export.tar_exporter', 'tgz_exporter') | 180 | register_lazy_exporter('tgz', ['.tar.gz', '.tgz'], 'bzrlib.export.tar_exporter', 'tgz_exporter') |
109 | 185 | register_lazy_exporter('tbz2', ['.tar.bz2', '.tbz2'], 'bzrlib.export.tar_exporter', 'tbz_exporter') | 181 | register_lazy_exporter('tbz2', ['.tar.bz2', '.tbz2'], 'bzrlib.export.tar_exporter', 'tbz_exporter') |
110 | 182 | register_lazy_exporter('tlzma', ['.tar.lzma'], 'bzrlib.export.tar_exporter', 'tar_lzma_exporter') | ||
111 | 183 | register_lazy_exporter('txz', ['.tar.xz'], 'bzrlib.export.tar_exporter', 'tar_xz_exporter') | ||
112 | 186 | register_lazy_exporter('zip', ['.zip'], 'bzrlib.export.zip_exporter', 'zip_exporter') | 184 | register_lazy_exporter('zip', ['.zip'], 'bzrlib.export.zip_exporter', 'zip_exporter') |
113 | 187 | 185 | ||
114 | 188 | 186 | ||
115 | === modified file 'bzrlib/export/dir_exporter.py' | |||
116 | --- bzrlib/export/dir_exporter.py 2010-05-25 17:27:52 +0000 | |||
117 | +++ bzrlib/export/dir_exporter.py 2011-03-14 12:24:22 +0000 | |||
118 | @@ -18,7 +18,6 @@ | |||
119 | 18 | 18 | ||
120 | 19 | import errno | 19 | import errno |
121 | 20 | import os | 20 | import os |
122 | 21 | import time | ||
123 | 22 | 21 | ||
124 | 23 | from bzrlib import errors, osutils | 22 | from bzrlib import errors, osutils |
125 | 24 | from bzrlib.export import _export_iter_entries | 23 | from bzrlib.export import _export_iter_entries |
126 | @@ -26,11 +25,9 @@ | |||
127 | 26 | ContentFilterContext, | 25 | ContentFilterContext, |
128 | 27 | filtered_output_bytes, | 26 | filtered_output_bytes, |
129 | 28 | ) | 27 | ) |
135 | 29 | from bzrlib.trace import mutter | 28 | |
136 | 30 | 29 | ||
137 | 31 | 30 | def dir_exporter(tree, dest, root, subdir=None, filtered=False, force_mtime=None): | |
133 | 32 | def dir_exporter(tree, dest, root, subdir, filtered=False, | ||
134 | 33 | per_file_timestamps=False): | ||
138 | 34 | """Export this tree to a new directory. | 31 | """Export this tree to a new directory. |
139 | 35 | 32 | ||
140 | 36 | `dest` should either not exist or should be empty. If it does not exist it | 33 | `dest` should either not exist or should be empty. If it does not exist it |
141 | @@ -39,7 +36,6 @@ | |||
142 | 39 | :note: If the export fails, the destination directory will be | 36 | :note: If the export fails, the destination directory will be |
143 | 40 | left in an incompletely exported state: export is not transactional. | 37 | left in an incompletely exported state: export is not transactional. |
144 | 41 | """ | 38 | """ |
145 | 42 | mutter('export version %r', tree) | ||
146 | 43 | try: | 39 | try: |
147 | 44 | os.mkdir(dest) | 40 | os.mkdir(dest) |
148 | 45 | except OSError, e: | 41 | except OSError, e: |
149 | @@ -76,7 +72,6 @@ | |||
150 | 76 | # The data returned here can be in any order, but we've already created all | 72 | # The data returned here can be in any order, but we've already created all |
151 | 77 | # the directories | 73 | # the directories |
152 | 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) |
153 | 79 | now = time.time() | ||
154 | 80 | for (relpath, executable), chunks in tree.iter_files_bytes(to_fetch): | 75 | for (relpath, executable), chunks in tree.iter_files_bytes(to_fetch): |
155 | 81 | if filtered: | 76 | if filtered: |
156 | 82 | filters = tree._content_filter_stack(relpath) | 77 | filters = tree._content_filter_stack(relpath) |
157 | @@ -92,8 +87,8 @@ | |||
158 | 92 | out.writelines(chunks) | 87 | out.writelines(chunks) |
159 | 93 | finally: | 88 | finally: |
160 | 94 | out.close() | 89 | out.close() |
162 | 95 | if per_file_timestamps: | 90 | if force_mtime is not None: |
163 | 91 | mtime = force_mtime | ||
164 | 92 | else: | ||
165 | 96 | mtime = tree.get_file_mtime(tree.path2id(relpath), relpath) | 93 | mtime = tree.get_file_mtime(tree.path2id(relpath), relpath) |
166 | 97 | else: | ||
167 | 98 | mtime = now | ||
168 | 99 | os.utime(fullpath, (mtime, mtime)) | 94 | os.utime(fullpath, (mtime, mtime)) |
169 | 100 | 95 | ||
170 | === modified file 'bzrlib/export/tar_exporter.py' | |||
171 | --- bzrlib/export/tar_exporter.py 2011-03-13 18:18:10 +0000 | |||
172 | +++ bzrlib/export/tar_exporter.py 2011-03-14 12:24:22 +0000 | |||
173 | @@ -17,6 +17,7 @@ | |||
174 | 17 | """Export a Tree to a non-versioned directory. | 17 | """Export a Tree to a non-versioned directory. |
175 | 18 | """ | 18 | """ |
176 | 19 | 19 | ||
177 | 20 | import os | ||
178 | 20 | import StringIO | 21 | import StringIO |
179 | 21 | import sys | 22 | import sys |
180 | 22 | import tarfile | 23 | import tarfile |
181 | @@ -24,7 +25,6 @@ | |||
182 | 24 | 25 | ||
183 | 25 | from bzrlib import ( | 26 | from bzrlib import ( |
184 | 26 | errors, | 27 | errors, |
185 | 27 | export, | ||
186 | 28 | osutils, | 28 | osutils, |
187 | 29 | ) | 29 | ) |
188 | 30 | from bzrlib.export import _export_iter_entries | 30 | from bzrlib.export import _export_iter_entries |
189 | @@ -32,41 +32,26 @@ | |||
190 | 32 | ContentFilterContext, | 32 | ContentFilterContext, |
191 | 33 | filtered_output_bytes, | 33 | filtered_output_bytes, |
192 | 34 | ) | 34 | ) |
202 | 35 | from bzrlib.trace import mutter | 35 | |
203 | 36 | 36 | ||
204 | 37 | 37 | def export_tarball(tree, ball, root, subdir=None, filtered=False, | |
205 | 38 | def tar_exporter(tree, dest, root, subdir, compression=None, filtered=False, | 38 | force_mtime=None): |
206 | 39 | per_file_timestamps=False): | 39 | """Export tree contents to a tarball. |
207 | 40 | """Export this tree to a new tar file. | 40 | |
208 | 41 | 41 | :param tree: Tree to export | |
209 | 42 | `dest` will be created holding the contents of this tree; if it | 42 | :param ball: Tarball to export to |
210 | 43 | already exists, it will be clobbered, like with "tar -c". | 43 | :param filtered: Whether to apply filters |
211 | 44 | :param subdir: Sub directory to export | ||
212 | 45 | :param force_mtime: Option mtime to force, instead of using | ||
213 | 46 | tree timestamps. | ||
214 | 44 | """ | 47 | """ |
215 | 45 | mutter('export version %r', tree) | ||
216 | 46 | now = time.time() | ||
217 | 47 | compression = str(compression or '') | ||
218 | 48 | if dest == '-': | ||
219 | 49 | # XXX: If no root is given, the output tarball will contain files | ||
220 | 50 | # named '-/foo'; perhaps this is the most reasonable thing. | ||
221 | 51 | ball = tarfile.open(None, 'w|' + compression, sys.stdout) | ||
222 | 52 | else: | ||
223 | 53 | if root is None: | ||
224 | 54 | root = export.get_root_name(dest) | ||
225 | 55 | |||
226 | 56 | # tarfile.open goes on to do 'os.getcwd() + dest' for opening | ||
227 | 57 | # the tar file. With dest being unicode, this throws UnicodeDecodeError | ||
228 | 58 | # unless we encode dest before passing it on. This works around | ||
229 | 59 | # upstream python bug http://bugs.python.org/issue8396 | ||
230 | 60 | # (fixed in Python 2.6.5 and 2.7b1) | ||
231 | 61 | ball = tarfile.open(dest.encode(osutils._fs_enc), 'w:' + compression) | ||
232 | 62 | |||
233 | 63 | for dp, ie in _export_iter_entries(tree, subdir): | 48 | for dp, ie in _export_iter_entries(tree, subdir): |
234 | 64 | filename = osutils.pathjoin(root, dp).encode('utf8') | 49 | filename = osutils.pathjoin(root, dp).encode('utf8') |
235 | 65 | item = tarfile.TarInfo(filename) | 50 | item = tarfile.TarInfo(filename) |
237 | 66 | if per_file_timestamps: | 51 | if force_mtime is not None: |
238 | 52 | item.mtime = force_mtime | ||
239 | 53 | else: | ||
240 | 67 | item.mtime = tree.get_file_mtime(ie.file_id, dp) | 54 | item.mtime = tree.get_file_mtime(ie.file_id, dp) |
241 | 68 | else: | ||
242 | 69 | item.mtime = now | ||
243 | 70 | if ie.kind == "file": | 55 | if ie.kind == "file": |
244 | 71 | item.type = tarfile.REGTYPE | 56 | item.type = tarfile.REGTYPE |
245 | 72 | if tree.is_executable(ie.file_id): | 57 | if tree.is_executable(ie.file_id): |
246 | @@ -82,7 +67,7 @@ | |||
247 | 82 | item.size = len(content) | 67 | item.size = len(content) |
248 | 83 | fileobj = StringIO.StringIO(content) | 68 | fileobj = StringIO.StringIO(content) |
249 | 84 | else: | 69 | else: |
251 | 85 | item.size = ie.text_size | 70 | item.size = tree.get_file_size(ie.file_id) |
252 | 86 | fileobj = tree.get_file(ie.file_id) | 71 | fileobj = tree.get_file(ie.file_id) |
253 | 87 | elif ie.kind == "directory": | 72 | elif ie.kind == "directory": |
254 | 88 | item.type = tarfile.DIRTYPE | 73 | item.type = tarfile.DIRTYPE |
255 | @@ -94,22 +79,110 @@ | |||
256 | 94 | item.type = tarfile.SYMTYPE | 79 | item.type = tarfile.SYMTYPE |
257 | 95 | item.size = 0 | 80 | item.size = 0 |
258 | 96 | item.mode = 0755 | 81 | item.mode = 0755 |
260 | 97 | item.linkname = ie.symlink_target | 82 | item.linkname = tree.get_symlink_target(ie.file_id) |
261 | 98 | fileobj = None | 83 | fileobj = None |
262 | 99 | else: | 84 | else: |
263 | 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" % |
264 | 101 | (ie.file_id, ie.kind)) | 86 | (ie.file_id, ie.kind)) |
265 | 102 | ball.addfile(item, fileobj) | 87 | ball.addfile(item, fileobj) |
279 | 103 | ball.close() | 88 | |
280 | 104 | 89 | ||
281 | 105 | 90 | def tgz_exporter(tree, dest, root, subdir, filtered=False, force_mtime=None): | |
282 | 106 | def tgz_exporter(tree, dest, root, subdir, filtered=False, | 91 | """Export this tree to a new tar file. |
283 | 107 | per_file_timestamps=False): | 92 | |
284 | 108 | tar_exporter(tree, dest, root, subdir, compression='gz', | 93 | `dest` will be created holding the contents of this tree; if it |
285 | 109 | filtered=filtered, per_file_timestamps=per_file_timestamps) | 94 | already exists, it will be clobbered, like with "tar -c". |
286 | 110 | 95 | """ | |
287 | 111 | 96 | import gzip | |
288 | 112 | def tbz_exporter(tree, dest, root, subdir, filtered=False, | 97 | if force_mtime is not None: |
289 | 113 | per_file_timestamps=False): | 98 | root_mtime = force_mtime |
290 | 114 | tar_exporter(tree, dest, root, subdir, compression='bz2', | 99 | elif (getattr(tree, "repository", None) and |
291 | 115 | filtered=filtered, per_file_timestamps=per_file_timestamps) | 100 | getattr(tree, "get_revision_id", None)): |
292 | 101 | # If this is a revision tree, use the revisions' timestamp | ||
293 | 102 | rev = tree.repository.get_revision(tree.get_revision_id()) | ||
294 | 103 | root_mtime = rev.timestamp | ||
295 | 104 | elif tree.get_root_id() is not None: | ||
296 | 105 | root_mtime = tree.get_file_mtime(tree.get_root_id()) | ||
297 | 106 | else: | ||
298 | 107 | root_mtime = time.time() | ||
299 | 108 | if dest == '-': | ||
300 | 109 | stream = gzip.GzipFile(None, mode='w', mtime=root_mtime, | ||
301 | 110 | fileobj=sys.stdout) | ||
302 | 111 | else: | ||
303 | 112 | stream = open(dest.encode(osutils._fs_enc), 'w') | ||
304 | 113 | # gzip file is used with an explicit fileobj so that | ||
305 | 114 | # the basename can be stored in the gzip file rather than | ||
306 | 115 | # dest. (bug 102234) | ||
307 | 116 | stream = gzip.GzipFile(os.path.basename(dest), 'w', | ||
308 | 117 | mtime=root_mtime, fileobj=stream) | ||
309 | 118 | ball = tarfile.open(None, 'w|', fileobj=stream) | ||
310 | 119 | export_tarball(tree, ball, root, subdir, filtered=filtered, | ||
311 | 120 | force_mtime=force_mtime) | ||
312 | 121 | ball.close() | ||
313 | 122 | |||
314 | 123 | |||
315 | 124 | def tbz_exporter(tree, dest, root, subdir, filtered=False, force_mtime=None): | ||
316 | 125 | """Export this tree to a new tar file. | ||
317 | 126 | |||
318 | 127 | `dest` will be created holding the contents of this tree; if it | ||
319 | 128 | already exists, it will be clobbered, like with "tar -c". | ||
320 | 129 | """ | ||
321 | 130 | if dest == '-': | ||
322 | 131 | ball = tarfile.open(None, 'w|bz2', sys.stdout) | ||
323 | 132 | else: | ||
324 | 133 | # tarfile.open goes on to do 'os.getcwd() + dest' for opening | ||
325 | 134 | # the tar file. With dest being unicode, this throws UnicodeDecodeError | ||
326 | 135 | # unless we encode dest before passing it on. This works around | ||
327 | 136 | # upstream python bug http://bugs.python.org/issue8396 | ||
328 | 137 | # (fixed in Python 2.6.5 and 2.7b1) | ||
329 | 138 | ball = tarfile.open(dest.encode(osutils._fs_enc), 'w:bz2') | ||
330 | 139 | export_tarball(tree, ball, root, subdir, filtered=filtered, | ||
331 | 140 | force_mtime=force_mtime) | ||
332 | 141 | ball.close() | ||
333 | 142 | |||
334 | 143 | |||
335 | 144 | def plain_tar_exporter(tree, dest, root, subdir, compression=None, | ||
336 | 145 | filtered=False, force_mtime=None): | ||
337 | 146 | """Export this tree to a new tar file. | ||
338 | 147 | |||
339 | 148 | `dest` will be created holding the contents of this tree; if it | ||
340 | 149 | already exists, it will be clobbered, like with "tar -c". | ||
341 | 150 | """ | ||
342 | 151 | if dest == '-': | ||
343 | 152 | stream = sys.stdout | ||
344 | 153 | else: | ||
345 | 154 | stream = open(dest.encode(osutils._fs_enc), 'w') | ||
346 | 155 | ball = tarfile.open(None, 'w|', stream) | ||
347 | 156 | export_tarball(tree, ball, root, subdir, filtered=filtered, | ||
348 | 157 | force_mtime=force_mtime) | ||
349 | 158 | ball.close() | ||
350 | 159 | |||
351 | 160 | |||
352 | 161 | def tar_xz_exporter(tree, dest, root, subdir, filtered=False, | ||
353 | 162 | force_mtime=None): | ||
354 | 163 | return tar_lzma_exporter(tree, dest, root, subdir, filtered=filtered, | ||
355 | 164 | force_mtime=force_mtime, compression_format="xz") | ||
356 | 165 | |||
357 | 166 | |||
358 | 167 | def tar_lzma_exporter(tree, dest, root, subdir, filtered=False, force_mtime=None, compression_format="lzma"): | ||
359 | 168 | """Export this tree to a new .tar.lzma file. | ||
360 | 169 | |||
361 | 170 | `dest` will be created holding the contents of this tree; if it | ||
362 | 171 | already exists, it will be clobbered, like with "tar -c". | ||
363 | 172 | """ | ||
364 | 173 | if dest == '-': | ||
365 | 174 | raise errors.BzrError("Writing to stdout not supported for .tar.lzma") | ||
366 | 175 | |||
367 | 176 | try: | ||
368 | 177 | import lzma | ||
369 | 178 | except ImportError, e: | ||
370 | 179 | raise errors.DependencyNotPresent('lzma', e) | ||
371 | 180 | |||
372 | 181 | assert compression_format in ("lzma", "xz") | ||
373 | 182 | stream = lzma.LZMAFile(dest.encode(osutils._fs_enc), 'w', | ||
374 | 183 | options={"format": compression_format}) | ||
375 | 184 | ball = tarfile.open(None, 'w:', fileobj=stream) | ||
376 | 185 | export_tarball(tree, ball, root, subdir, filtered=filtered, | ||
377 | 186 | force_mtime=force_mtime) | ||
378 | 187 | ball.close() | ||
379 | 188 | |||
380 | 116 | 189 | ||
381 | === modified file 'bzrlib/export/zip_exporter.py' | |||
382 | --- bzrlib/export/zip_exporter.py 2011-02-16 17:20:10 +0000 | |||
383 | +++ bzrlib/export/zip_exporter.py 2011-03-14 12:24:22 +0000 | |||
384 | @@ -19,6 +19,7 @@ | |||
385 | 19 | 19 | ||
386 | 20 | import os | 20 | import os |
387 | 21 | import stat | 21 | import stat |
388 | 22 | import sys | ||
389 | 22 | import time | 23 | import time |
390 | 23 | import zipfile | 24 | import zipfile |
391 | 24 | 25 | ||
392 | @@ -43,20 +44,17 @@ | |||
393 | 43 | _DIR_ATTR = stat.S_IFDIR | ZIP_DIRECTORY_BIT | DIR_PERMISSIONS | 44 | _DIR_ATTR = stat.S_IFDIR | ZIP_DIRECTORY_BIT | DIR_PERMISSIONS |
394 | 44 | 45 | ||
395 | 45 | 46 | ||
398 | 46 | def zip_exporter(tree, dest, root, subdir, filtered=False, | 47 | def zip_exporter(tree, dest, root, subdir=None, filtered=False, force_mtime=None): |
397 | 47 | per_file_timestamps=False): | ||
399 | 48 | """ Export this tree to a new zip file. | 48 | """ Export this tree to a new zip file. |
400 | 49 | 49 | ||
401 | 50 | `dest` will be created holding the contents of this tree; if it | 50 | `dest` will be created holding the contents of this tree; if it |
402 | 51 | already exists, it will be overwritten". | 51 | already exists, it will be overwritten". |
403 | 52 | """ | 52 | """ |
404 | 53 | mutter('export version %r', tree) | ||
405 | 54 | |||
406 | 55 | now = time.localtime()[:6] | ||
407 | 56 | 53 | ||
408 | 57 | compression = zipfile.ZIP_DEFLATED | 54 | compression = zipfile.ZIP_DEFLATED |
409 | 55 | if dest == "-": | ||
410 | 56 | dest = sys.stdout | ||
411 | 58 | zipf = zipfile.ZipFile(dest, "w", compression) | 57 | zipf = zipfile.ZipFile(dest, "w", compression) |
412 | 59 | |||
413 | 60 | try: | 58 | try: |
414 | 61 | for dp, ie in _export_iter_entries(tree, subdir): | 59 | for dp, ie in _export_iter_entries(tree, subdir): |
415 | 62 | file_id = ie.file_id | 60 | file_id = ie.file_id |
416 | @@ -64,15 +62,16 @@ | |||
417 | 64 | 62 | ||
418 | 65 | # zipfile.ZipFile switches all paths to forward | 63 | # zipfile.ZipFile switches all paths to forward |
419 | 66 | # slashes anyway, so just stick with that. | 64 | # slashes anyway, so just stick with that. |
421 | 67 | if per_file_timestamps: | 65 | if force_mtime is not None: |
422 | 66 | mtime = force_mtime | ||
423 | 67 | else: | ||
424 | 68 | mtime = tree.get_file_mtime(ie.file_id, dp) | 68 | mtime = tree.get_file_mtime(ie.file_id, dp) |
427 | 69 | else: | 69 | date_time = time.localtime(mtime)[:6] |
426 | 70 | mtime = now | ||
428 | 71 | filename = osutils.pathjoin(root, dp).encode('utf8') | 70 | filename = osutils.pathjoin(root, dp).encode('utf8') |
429 | 72 | if ie.kind == "file": | 71 | if ie.kind == "file": |
430 | 73 | zinfo = zipfile.ZipInfo( | 72 | zinfo = zipfile.ZipInfo( |
431 | 74 | filename=filename, | 73 | filename=filename, |
433 | 75 | date_time=mtime) | 74 | date_time=date_time) |
434 | 76 | zinfo.compress_type = compression | 75 | zinfo.compress_type = compression |
435 | 77 | zinfo.external_attr = _FILE_ATTR | 76 | zinfo.external_attr = _FILE_ATTR |
436 | 78 | if filtered: | 77 | if filtered: |
437 | @@ -90,14 +89,14 @@ | |||
438 | 90 | # not just empty files. | 89 | # not just empty files. |
439 | 91 | zinfo = zipfile.ZipInfo( | 90 | zinfo = zipfile.ZipInfo( |
440 | 92 | filename=filename + '/', | 91 | filename=filename + '/', |
442 | 93 | date_time=mtime) | 92 | date_time=date_time) |
443 | 94 | zinfo.compress_type = compression | 93 | zinfo.compress_type = compression |
444 | 95 | zinfo.external_attr = _DIR_ATTR | 94 | zinfo.external_attr = _DIR_ATTR |
445 | 96 | zipf.writestr(zinfo,'') | 95 | zipf.writestr(zinfo,'') |
446 | 97 | elif ie.kind == "symlink": | 96 | elif ie.kind == "symlink": |
447 | 98 | zinfo = zipfile.ZipInfo( | 97 | zinfo = zipfile.ZipInfo( |
448 | 99 | filename=(filename + '.lnk'), | 98 | filename=(filename + '.lnk'), |
450 | 100 | date_time=mtime) | 99 | date_time=date_time) |
451 | 101 | zinfo.compress_type = compression | 100 | zinfo.compress_type = compression |
452 | 102 | zinfo.external_attr = _FILE_ATTR | 101 | zinfo.external_attr = _FILE_ATTR |
453 | 103 | zipf.writestr(zinfo, ie.symlink_target) | 102 | zipf.writestr(zinfo, ie.symlink_target) |
454 | 104 | 103 | ||
455 | === modified file 'bzrlib/tests/__init__.py' | |||
456 | --- bzrlib/tests/__init__.py 2011-03-10 13:29:54 +0000 | |||
457 | +++ bzrlib/tests/__init__.py 2011-03-14 12:24:22 +0000 | |||
458 | @@ -3903,7 +3903,6 @@ | |||
459 | 3903 | 'bzrlib', | 3903 | 'bzrlib', |
460 | 3904 | 'bzrlib.branchbuilder', | 3904 | 'bzrlib.branchbuilder', |
461 | 3905 | 'bzrlib.decorators', | 3905 | 'bzrlib.decorators', |
462 | 3906 | 'bzrlib.export', | ||
463 | 3907 | 'bzrlib.inventory', | 3906 | 'bzrlib.inventory', |
464 | 3908 | 'bzrlib.iterablefile', | 3907 | 'bzrlib.iterablefile', |
465 | 3909 | 'bzrlib.lockdir', | 3908 | 'bzrlib.lockdir', |
466 | 3910 | 3909 | ||
467 | === modified file 'bzrlib/tests/blackbox/test_export.py' | |||
468 | --- bzrlib/tests/blackbox/test_export.py 2011-02-16 17:20:10 +0000 | |||
469 | +++ bzrlib/tests/blackbox/test_export.py 2011-03-14 12:24:22 +0000 | |||
470 | @@ -117,6 +117,36 @@ | |||
471 | 117 | # '.bzrignore'. | 117 | # '.bzrignore'. |
472 | 118 | self.assertEqual(['test/a'], sorted(zfile.namelist())) | 118 | self.assertEqual(['test/a'], sorted(zfile.namelist())) |
473 | 119 | 119 | ||
474 | 120 | def test_zip_export_stdout(self): | ||
475 | 121 | tree = self.make_branch_and_tree('zip') | ||
476 | 122 | self.build_tree(['zip/a']) | ||
477 | 123 | tree.add('a') | ||
478 | 124 | tree.commit('1') | ||
479 | 125 | os.chdir('zip') | ||
480 | 126 | contents = self.run_bzr('export --format=zip -')[0] | ||
481 | 127 | zfile = zipfile.ZipFile(StringIO(contents)) | ||
482 | 128 | self.assertEqual(['a'], sorted(zfile.namelist())) | ||
483 | 129 | |||
484 | 130 | def test_tgz_export_stdout(self): | ||
485 | 131 | tree = self.make_branch_and_tree('z') | ||
486 | 132 | self.build_tree(['z/a']) | ||
487 | 133 | tree.add('a') | ||
488 | 134 | tree.commit('1') | ||
489 | 135 | os.chdir('z') | ||
490 | 136 | contents = self.run_bzr('export --format=tgz -')[0] | ||
491 | 137 | ball = tarfile.open(mode='r|gz', fileobj=StringIO(contents)) | ||
492 | 138 | self.assertEqual(['a'], ball.getnames()) | ||
493 | 139 | |||
494 | 140 | def test_tbz2_export_stdout(self): | ||
495 | 141 | tree = self.make_branch_and_tree('z') | ||
496 | 142 | self.build_tree(['z/a']) | ||
497 | 143 | tree.add('a') | ||
498 | 144 | tree.commit('1') | ||
499 | 145 | os.chdir('z') | ||
500 | 146 | contents = self.run_bzr('export --format=tbz2 -')[0] | ||
501 | 147 | ball = tarfile.open(mode='r|bz2', fileobj=StringIO(contents)) | ||
502 | 148 | self.assertEqual(['a'], ball.getnames()) | ||
503 | 149 | |||
504 | 120 | def test_zip_export_unicode(self): | 150 | def test_zip_export_unicode(self): |
505 | 121 | self.requireFeature(tests.UnicodeFilenameFeature) | 151 | self.requireFeature(tests.UnicodeFilenameFeature) |
506 | 122 | tree = self.make_branch_and_tree('zip') | 152 | tree = self.make_branch_and_tree('zip') |
507 | @@ -317,3 +347,14 @@ | |||
508 | 317 | self.run_bzr(['export', '--directory=branch', 'latest']) | 347 | self.run_bzr(['export', '--directory=branch', 'latest']) |
509 | 318 | self.assertEqual(['goodbye', 'hello'], sorted(os.listdir('latest'))) | 348 | self.assertEqual(['goodbye', 'hello'], sorted(os.listdir('latest'))) |
510 | 319 | self.check_file_contents('latest/goodbye', 'baz') | 349 | self.check_file_contents('latest/goodbye', 'baz') |
511 | 350 | |||
512 | 351 | def test_zip_export_per_file_timestamps(self): | ||
513 | 352 | tree = self.example_branch() | ||
514 | 353 | self.build_tree_contents([('branch/har', 'foo')]) | ||
515 | 354 | tree.add('har') | ||
516 | 355 | # Earliest allowable date on FAT32 filesystems is 1980-01-01 | ||
517 | 356 | tree.commit('setup', timestamp=315532800) | ||
518 | 357 | self.run_bzr('export --per-file-timestamps test.zip branch') | ||
519 | 358 | zfile = zipfile.ZipFile('test.zip') | ||
520 | 359 | info = zfile.getinfo("test/har") | ||
521 | 360 | self.assertEquals((1980, 1, 1, 1, 0, 0), info.date_time) | ||
522 | 320 | 361 | ||
523 | === modified file 'bzrlib/tests/test_export.py' | |||
524 | --- bzrlib/tests/test_export.py 2010-04-14 00:11:32 +0000 | |||
525 | +++ bzrlib/tests/test_export.py 2011-03-14 12:24:22 +0000 | |||
526 | @@ -14,19 +14,26 @@ | |||
527 | 14 | # along with this program; if not, write to the Free Software | 14 | # along with this program; if not, write to the Free Software |
528 | 15 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | 15 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
529 | 16 | 16 | ||
530 | 17 | """Tests for bzrlib.export.""" | ||
531 | 18 | |||
532 | 19 | from cStringIO import StringIO | ||
533 | 17 | import os | 20 | import os |
534 | 21 | import tarfile | ||
535 | 18 | import time | 22 | import time |
536 | 23 | import zipfile | ||
537 | 19 | 24 | ||
538 | 20 | from bzrlib import ( | 25 | from bzrlib import ( |
539 | 21 | errors, | 26 | errors, |
540 | 22 | export, | 27 | export, |
541 | 23 | tests, | 28 | tests, |
542 | 24 | ) | 29 | ) |
548 | 25 | 30 | from bzrlib.export import get_root_name | |
549 | 26 | 31 | from bzrlib.export.tar_exporter import export_tarball | |
550 | 27 | class TestExport(tests.TestCaseWithTransport): | 32 | |
551 | 28 | 33 | ||
552 | 29 | def test_dir_export_missing_file(self): | 34 | class TestDirExport(tests.TestCaseWithTransport): |
553 | 35 | |||
554 | 36 | def test_missing_file(self): | ||
555 | 30 | self.build_tree(['a/', 'a/b', 'a/c']) | 37 | self.build_tree(['a/', 'a/b', 'a/c']) |
556 | 31 | wt = self.make_branch_and_tree('.') | 38 | wt = self.make_branch_and_tree('.') |
557 | 32 | wt.add(['a', 'a/b', 'a/c']) | 39 | wt.add(['a', 'a/b', 'a/c']) |
558 | @@ -35,7 +42,12 @@ | |||
559 | 35 | self.failUnlessExists('target/a/b') | 42 | self.failUnlessExists('target/a/b') |
560 | 36 | self.failIfExists('target/a/c') | 43 | self.failIfExists('target/a/c') |
561 | 37 | 44 | ||
563 | 38 | def test_dir_export_symlink(self): | 45 | def test_empty(self): |
564 | 46 | wt = self.make_branch_and_tree('.') | ||
565 | 47 | export.export(wt, 'target', format="dir") | ||
566 | 48 | self.assertEquals([], os.listdir("target")) | ||
567 | 49 | |||
568 | 50 | def test_symlink(self): | ||
569 | 39 | self.requireFeature(tests.SymlinkFeature) | 51 | self.requireFeature(tests.SymlinkFeature) |
570 | 40 | wt = self.make_branch_and_tree('.') | 52 | wt = self.make_branch_and_tree('.') |
571 | 41 | os.symlink('source', 'link') | 53 | os.symlink('source', 'link') |
572 | @@ -43,7 +55,7 @@ | |||
573 | 43 | export.export(wt, 'target', format="dir") | 55 | export.export(wt, 'target', format="dir") |
574 | 44 | self.failUnlessExists('target/link') | 56 | self.failUnlessExists('target/link') |
575 | 45 | 57 | ||
577 | 46 | def test_dir_export_to_existing_empty_dir_success(self): | 58 | def test_to_existing_empty_dir_success(self): |
578 | 47 | self.build_tree(['source/', 'source/a', 'source/b/', 'source/b/c']) | 59 | self.build_tree(['source/', 'source/a', 'source/b/', 'source/b/c']) |
579 | 48 | wt = self.make_branch_and_tree('source') | 60 | wt = self.make_branch_and_tree('source') |
580 | 49 | wt.add(['a', 'b', 'b/c']) | 61 | wt.add(['a', 'b', 'b/c']) |
581 | @@ -54,7 +66,7 @@ | |||
582 | 54 | self.failUnlessExists('target/b') | 66 | self.failUnlessExists('target/b') |
583 | 55 | self.failUnlessExists('target/b/c') | 67 | self.failUnlessExists('target/b/c') |
584 | 56 | 68 | ||
586 | 57 | def test_dir_export_to_existing_nonempty_dir_fail(self): | 69 | def test_to_existing_nonempty_dir_fail(self): |
587 | 58 | self.build_tree(['source/', 'source/a', 'source/b/', 'source/b/c']) | 70 | self.build_tree(['source/', 'source/a', 'source/b/', 'source/b/c']) |
588 | 59 | wt = self.make_branch_and_tree('source') | 71 | wt = self.make_branch_and_tree('source') |
589 | 60 | wt.add(['a', 'b', 'b/c']) | 72 | wt.add(['a', 'b', 'b/c']) |
590 | @@ -62,7 +74,7 @@ | |||
591 | 62 | self.build_tree(['target/', 'target/foo']) | 74 | self.build_tree(['target/', 'target/foo']) |
592 | 63 | self.assertRaises(errors.BzrError, export.export, wt, 'target', format="dir") | 75 | self.assertRaises(errors.BzrError, export.export, wt, 'target', format="dir") |
593 | 64 | 76 | ||
595 | 65 | def test_dir_export_existing_single_file(self): | 77 | def test_existing_single_file(self): |
596 | 66 | self.build_tree(['dir1/', 'dir1/dir2/', 'dir1/first', 'dir1/dir2/second']) | 78 | self.build_tree(['dir1/', 'dir1/dir2/', 'dir1/first', 'dir1/dir2/second']) |
597 | 67 | wtree = self.make_branch_and_tree('dir1') | 79 | wtree = self.make_branch_and_tree('dir1') |
598 | 68 | wtree.add(['dir2', 'first', 'dir2/second']) | 80 | wtree.add(['dir2', 'first', 'dir2/second']) |
599 | @@ -71,8 +83,8 @@ | |||
600 | 71 | self.failUnlessExists('target1/first') | 83 | self.failUnlessExists('target1/first') |
601 | 72 | export.export(wtree, 'target2', format='dir', subdir='dir2/second') | 84 | export.export(wtree, 'target2', format='dir', subdir='dir2/second') |
602 | 73 | self.failUnlessExists('target2/second') | 85 | self.failUnlessExists('target2/second') |
605 | 74 | 86 | ||
606 | 75 | def test_dir_export_files_same_timestamp(self): | 87 | def test_files_same_timestamp(self): |
607 | 76 | builder = self.make_branch_builder('source') | 88 | builder = self.make_branch_builder('source') |
608 | 77 | builder.start_series() | 89 | builder.start_series() |
609 | 78 | builder.build_snapshot(None, None, [ | 90 | builder.build_snapshot(None, None, [ |
610 | @@ -99,7 +111,7 @@ | |||
611 | 99 | # All files must be given the same mtime. | 111 | # All files must be given the same mtime. |
612 | 100 | self.assertEqual(st_a.st_mtime, st_b.st_mtime) | 112 | self.assertEqual(st_a.st_mtime, st_b.st_mtime) |
613 | 101 | 113 | ||
615 | 102 | def test_dir_export_files_per_file_timestamps(self): | 114 | def test_files_per_file_timestamps(self): |
616 | 103 | builder = self.make_branch_builder('source') | 115 | builder = self.make_branch_builder('source') |
617 | 104 | builder.start_series() | 116 | builder.start_series() |
618 | 105 | # Earliest allowable date on FAT32 filesystems is 1980-01-01 | 117 | # Earliest allowable date on FAT32 filesystems is 1980-01-01 |
619 | @@ -121,3 +133,134 @@ | |||
620 | 121 | t = self.get_transport('target') | 133 | t = self.get_transport('target') |
621 | 122 | self.assertEqual(a_time, t.stat('a').st_mtime) | 134 | self.assertEqual(a_time, t.stat('a').st_mtime) |
622 | 123 | self.assertEqual(b_time, t.stat('b').st_mtime) | 135 | self.assertEqual(b_time, t.stat('b').st_mtime) |
623 | 136 | |||
624 | 137 | |||
625 | 138 | class TarExporterTests(tests.TestCaseWithTransport): | ||
626 | 139 | |||
627 | 140 | def test_empty(self): | ||
628 | 141 | wt = self.make_branch_and_tree('.') | ||
629 | 142 | export.export(wt, 'target.tar', format="tar") | ||
630 | 143 | tf = tarfile.open('target.tar') | ||
631 | 144 | self.assertEquals([], tf.getnames()) | ||
632 | 145 | |||
633 | 146 | def test_xz(self): | ||
634 | 147 | wt = self.make_branch_and_tree('.') | ||
635 | 148 | self.build_tree(['a']) | ||
636 | 149 | wt.add(["a"]) | ||
637 | 150 | wt.commit("1") | ||
638 | 151 | try: | ||
639 | 152 | export.export(wt, 'target.tar.xz', format="txz") | ||
640 | 153 | except errors.DependencyNotPresent: | ||
641 | 154 | raise tests.TestSkipped("lzma module not available") | ||
642 | 155 | import lzma | ||
643 | 156 | tf = tarfile.open(fileobj=lzma.LZMAFile('target.tar.xz')) | ||
644 | 157 | self.assertEquals(["target/a"], tf.getnames()) | ||
645 | 158 | |||
646 | 159 | def test_lzma(self): | ||
647 | 160 | wt = self.make_branch_and_tree('.') | ||
648 | 161 | self.build_tree(['a']) | ||
649 | 162 | wt.add(["a"]) | ||
650 | 163 | wt.commit("1") | ||
651 | 164 | try: | ||
652 | 165 | export.export(wt, 'target.tar.lzma', format="tlzma") | ||
653 | 166 | except errors.DependencyNotPresent: | ||
654 | 167 | raise tests.TestSkipped("lzma module not available") | ||
655 | 168 | import lzma | ||
656 | 169 | tf = tarfile.open(fileobj=lzma.LZMAFile('target.tar.lzma')) | ||
657 | 170 | self.assertEquals(["target/a"], tf.getnames()) | ||
658 | 171 | |||
659 | 172 | def test_tgz(self): | ||
660 | 173 | wt = self.make_branch_and_tree('.') | ||
661 | 174 | self.build_tree(['a']) | ||
662 | 175 | wt.add(["a"]) | ||
663 | 176 | wt.commit("1") | ||
664 | 177 | export.export(wt, 'target.tar.gz', format="tgz") | ||
665 | 178 | tf = tarfile.open('target.tar.gz') | ||
666 | 179 | self.assertEquals(["target/a"], tf.getnames()) | ||
667 | 180 | |||
668 | 181 | def test_tgz_ignores_dest_path(self): | ||
669 | 182 | # The target path should not be a part of the target file. | ||
670 | 183 | # (bug #102234) | ||
671 | 184 | wt = self.make_branch_and_tree('.') | ||
672 | 185 | self.build_tree(['a']) | ||
673 | 186 | wt.add(["a"]) | ||
674 | 187 | wt.commit("1") | ||
675 | 188 | os.mkdir("testdir1") | ||
676 | 189 | os.mkdir("testdir2") | ||
677 | 190 | export.export(wt, 'testdir1/target.tar.gz', format="tgz", | ||
678 | 191 | per_file_timestamps=True) | ||
679 | 192 | export.export(wt, 'testdir2/target.tar.gz', format="tgz", | ||
680 | 193 | per_file_timestamps=True) | ||
681 | 194 | file1 = open('testdir1/target.tar.gz', 'r') | ||
682 | 195 | self.addCleanup(file1.close) | ||
683 | 196 | file2 = open('testdir1/target.tar.gz', 'r') | ||
684 | 197 | self.addCleanup(file2.close) | ||
685 | 198 | content1 = file1.read() | ||
686 | 199 | content2 = file2.read() | ||
687 | 200 | self.assertEqualDiff(content1, content2) | ||
688 | 201 | # the gzip module doesn't have a way to read back to the original | ||
689 | 202 | # filename, but it's stored as-is in the tarfile. | ||
690 | 203 | self.assertFalse("testdir1" in content1) | ||
691 | 204 | self.assertFalse("target.tar.gz" in content1) | ||
692 | 205 | self.assertTrue("target.tar" in content1) | ||
693 | 206 | |||
694 | 207 | def test_tbz2(self): | ||
695 | 208 | wt = self.make_branch_and_tree('.') | ||
696 | 209 | self.build_tree(['a']) | ||
697 | 210 | wt.add(["a"]) | ||
698 | 211 | wt.commit("1") | ||
699 | 212 | export.export(wt, 'target.tar.bz2', format="tbz2") | ||
700 | 213 | tf = tarfile.open('target.tar.bz2') | ||
701 | 214 | self.assertEquals(["target/a"], tf.getnames()) | ||
702 | 215 | |||
703 | 216 | def test_xz_stdout(self): | ||
704 | 217 | wt = self.make_branch_and_tree('.') | ||
705 | 218 | self.assertRaises(errors.BzrError, export.export, wt, '-', | ||
706 | 219 | format="txz") | ||
707 | 220 | |||
708 | 221 | def test_export_tarball(self): | ||
709 | 222 | wt = self.make_branch_and_tree('.') | ||
710 | 223 | self.build_tree(['a']) | ||
711 | 224 | wt.add(["a"]) | ||
712 | 225 | wt.commit("1", timestamp=42) | ||
713 | 226 | target = StringIO() | ||
714 | 227 | ball = tarfile.open(None, "w|", target) | ||
715 | 228 | wt.lock_read() | ||
716 | 229 | try: | ||
717 | 230 | export_tarball(wt, ball, "bar", subdir=None) | ||
718 | 231 | finally: | ||
719 | 232 | wt.unlock() | ||
720 | 233 | self.assertEquals(["bar/a"], ball.getnames()) | ||
721 | 234 | ball.close() | ||
722 | 235 | |||
723 | 236 | |||
724 | 237 | class ZipExporterTests(tests.TestCaseWithTransport): | ||
725 | 238 | |||
726 | 239 | def test_per_file_timestamps(self): | ||
727 | 240 | tree = self.make_branch_and_tree('.') | ||
728 | 241 | self.build_tree_contents([('har', 'foo')]) | ||
729 | 242 | tree.add('har') | ||
730 | 243 | # Earliest allowable date on FAT32 filesystems is 1980-01-01 | ||
731 | 244 | tree.commit('setup', timestamp=315532800) | ||
732 | 245 | export.export(tree.basis_tree(), 'test.zip', format='zip', | ||
733 | 246 | per_file_timestamps=True) | ||
734 | 247 | zfile = zipfile.ZipFile('test.zip') | ||
735 | 248 | info = zfile.getinfo("test/har") | ||
736 | 249 | self.assertEquals((1980, 1, 1, 1, 0, 0), info.date_time) | ||
737 | 250 | |||
738 | 251 | |||
739 | 252 | |||
740 | 253 | class RootNameTests(tests.TestCase): | ||
741 | 254 | |||
742 | 255 | def test_root_name(self): | ||
743 | 256 | self.assertEquals('mytest', get_root_name('../mytest.tar')) | ||
744 | 257 | self.assertEquals('mytar', get_root_name('mytar.tar')) | ||
745 | 258 | self.assertEquals('mytar', get_root_name('mytar.tar.bz2')) | ||
746 | 259 | self.assertEquals('tar.tar.tar', get_root_name('tar.tar.tar.tgz')) | ||
747 | 260 | self.assertEquals('bzr-0.0.5', get_root_name('bzr-0.0.5.tar.gz')) | ||
748 | 261 | self.assertEquals('bzr-0.0.5', get_root_name('bzr-0.0.5.zip')) | ||
749 | 262 | self.assertEquals('bzr-0.0.5', get_root_name('bzr-0.0.5')) | ||
750 | 263 | self.assertEquals('mytar', get_root_name('a/long/path/mytar.tgz')) | ||
751 | 264 | self.assertEquals('other', | ||
752 | 265 | get_root_name('../parent/../dir/other.tbz2')) | ||
753 | 266 | self.assertEquals('', get_root_name('-')) | ||
754 | 124 | 267 | ||
755 | === modified file 'doc/en/release-notes/bzr-2.4.txt' | |||
756 | --- doc/en/release-notes/bzr-2.4.txt 2011-03-11 15:36:12 +0000 | |||
757 | +++ doc/en/release-notes/bzr-2.4.txt 2011-03-14 12:24:22 +0000 | |||
758 | @@ -51,7 +51,20 @@ | |||
759 | 51 | * Branching, merging and pulling a branch now copies revisions named in | 51 | * Branching, merging and pulling a branch now copies revisions named in |
760 | 52 | tags, not just the tag metadata. (Andrew Bennetts, #309682) | 52 | tags, not just the tag metadata. (Andrew Bennetts, #309682) |
761 | 53 | 53 | ||
763 | 54 | * ``bzr cat-revision`` no longer requires a working tree. (Jelmer Vernooij, #704405) | 54 | * ``bzr cat-revision`` no longer requires a working tree. |
764 | 55 | (Jelmer Vernooij, #704405) | ||
765 | 56 | |||
766 | 57 | * ``bzr export --per-file-timestamps`` for .tar.gz files will now use the | ||
767 | 58 | root entry's last change time as the tar file mtime. This makes the | ||
768 | 59 | output of ``bzr export --per-file-timestamps`` for a particular tree | ||
769 | 60 | deterministic. (Jelmer Vernooij, #711226) | ||
770 | 61 | |||
771 | 62 | * ``bzr export --format=zip`` can now export to standard output, | ||
772 | 63 | like the other exporters can. (Jelmer Vernooij, #513752) | ||
773 | 64 | |||
774 | 65 | * ``bzr export`` can now create ``.tar.xz`` and ``.tar.lzma`` files. | ||
775 | 66 | (Jelmer Vernooij, #551714) | ||
776 | 67 | |||
777 | 55 | 68 | ||
778 | 56 | Bug Fixes | 69 | Bug Fixes |
779 | 57 | ********* | 70 | ********* |
780 | @@ -69,6 +82,9 @@ | |||
781 | 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. |
782 | 70 | (Jelmer Vernooij, #207253) | 83 | (Jelmer Vernooij, #207253) |
783 | 71 | 84 | ||
784 | 85 | * ``bzr export`` to tgz files will only write out the basename of the | ||
785 | 86 | tarfile to the gzip file. (Jelmer Vernooij, #102234) | ||
786 | 87 | |||
787 | 72 | * ``bzr push --overwrite`` with an older revision specified will now correctly | 88 | * ``bzr push --overwrite`` with an older revision specified will now correctly |
788 | 73 | roll back the target branch. (Jelmer Vernooij, #386576) | 89 | roll back the target branch. (Jelmer Vernooij, #386576) |
789 | 74 | 90 |
test_xz should be guarded with a Feature (ModuleAvailabl eFeature( '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.