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