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