Merge lp:~mbp/bzr/filter-tree into lp:bzr

Proposed by Martin Pool
Status: Merged
Approved by: John A Meinel
Approved revision: no longer in the source branch.
Merged at revision: 6035
Proposed branch: lp:~mbp/bzr/filter-tree
Merge into: lp:bzr
Diff against target: 617 lines (+242/-110)
11 files modified
bzrlib/builtins.py (+17/-28)
bzrlib/export/__init__.py (+21/-11)
bzrlib/export/dir_exporter.py (+3/-11)
bzrlib/export/tar_exporter.py (+26/-41)
bzrlib/export/zip_exporter.py (+2/-13)
bzrlib/filter_tree.py (+82/-0)
bzrlib/tests/__init__.py (+1/-6)
bzrlib/tests/fixtures.py (+13/-0)
bzrlib/tests/test_filter_tree.py (+68/-0)
bzrlib/tree.py (+3/-0)
doc/en/release-notes/bzr-2.5.txt (+6/-0)
To merge this branch: bzr merge lp:~mbp/bzr/filter-tree
Reviewer Review Type Date Requested Status
John A Meinel Approve
Jonathan Riddell (community) Approve
Review via email: mp+67408@code.launchpad.net

Commit message

add ContentFilterTree decorator and use it for cat and export

Description of the change

This cleans up some of the content filtering and export code.

At the moment the code to apply filtering is a bit spread around the code that uses the results. This particularly sticks out in the export code ~xaav recently updated where a 'filtered' parameter is passed down through several levels.

Because there's so much of it, I've opted to support the old parameter only at the top level, and to rip it out of the lower level code.

This instead adds a ContentFilterTree decorator that can be created at a higher level and passed in. It doesn't yet provide a full tree interface, just enough to make these pass. I think we can build on it in fixing other content-filtering bugs.

 * Clean up file-id dwim code in cmd_cat

 * Add a tiny fixture to give you a tree with a versioned file, as a baby step towards fewer tests doing this inline

This probably shouldn't land in 2.4 since it's a small api break.

The news needs to move to the 2.5 file once that exists.

To post a comment you must log in.
Revision history for this message
Jonathan Riddell (jr) wrote :

Refactoring filtering code from several places into a commond object reduces code and makes it easier to maintain, approved.

review: Approve
Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 7/12/2011 12:49 PM, Jonathan Riddell wrote:
> Review: Approve
> Refactoring filtering code from several places into a commond object reduces code and makes it easier to maintain, approved.
>

 merge: approve

This can't actually be landed because it has release-notes conflicts,
but consider it approved-for-landing.

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk4cS7QACgkQJdeBCYSNAANxEQCfZQgSJMSaQN9O3cPsSHDvxJCx
XJYAn2he5twNFONGUH9iOeP2sXJaNCqV
=vDXl
-----END PGP SIGNATURE-----

review: Approve
Revision history for this message
Jonathan Riddell (jr) wrote :

This can be landed now 2.5 is open. Me and vila can do it this week as part of patch pilot if you don't object.

Revision history for this message
Martin Pool (mbp) wrote :

Thanks - I'll see if I can land these branches tomorrow.

Revision history for this message
Martin Pool (mbp) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/builtins.py'
2--- bzrlib/builtins.py 2011-07-12 11:09:57 +0000
3+++ bzrlib/builtins.py 2011-07-21 07:11:34 +0000
4@@ -3073,6 +3073,10 @@
5
6 old_file_id = rev_tree.path2id(relpath)
7
8+ # TODO: Split out this code to something that generically finds the
9+ # best id for a path across one or more trees; it's like
10+ # find_ids_across_trees but restricted to find just one. -- mbp
11+ # 20110705.
12 if name_from_revision:
13 # Try in revision if requested
14 if old_file_id is None:
15@@ -3080,41 +3084,26 @@
16 "%r is not present in revision %s" % (
17 filename, rev_tree.get_revision_id()))
18 else:
19- content = rev_tree.get_file_text(old_file_id)
20+ actual_file_id = old_file_id
21 else:
22 cur_file_id = tree.path2id(relpath)
23- found = False
24- if cur_file_id is not None:
25- # Then try with the actual file id
26- try:
27- content = rev_tree.get_file_text(cur_file_id)
28- found = True
29- except errors.NoSuchId:
30- # The actual file id didn't exist at that time
31- pass
32- if not found and old_file_id is not None:
33- # Finally try with the old file id
34- content = rev_tree.get_file_text(old_file_id)
35- found = True
36- if not found:
37- # Can't be found anywhere
38+ if cur_file_id is not None and rev_tree.has_id(cur_file_id):
39+ actual_file_id = cur_file_id
40+ elif old_file_id is not None:
41+ actual_file_id = old_file_id
42+ else:
43 raise errors.BzrCommandError(
44 "%r is not present in revision %s" % (
45 filename, rev_tree.get_revision_id()))
46 if filtered:
47- from bzrlib.filters import (
48- ContentFilterContext,
49- filtered_output_bytes,
50- )
51- filters = rev_tree._content_filter_stack(relpath)
52- chunks = content.splitlines(True)
53- content = filtered_output_bytes(chunks, filters,
54- ContentFilterContext(relpath, rev_tree))
55- self.cleanup_now()
56- self.outf.writelines(content)
57+ from bzrlib.filter_tree import ContentFilterTree
58+ filter_tree = ContentFilterTree(rev_tree,
59+ rev_tree._content_filter_stack)
60+ content = filter_tree.get_file_text(actual_file_id)
61 else:
62- self.cleanup_now()
63- self.outf.write(content)
64+ content = rev_tree.get_file_text(actual_file_id)
65+ self.cleanup_now()
66+ self.outf.write(content)
67
68
69 class cmd_local_time_offset(Command):
70
71=== modified file 'bzrlib/export/__init__.py'
72--- bzrlib/export/__init__.py 2011-06-13 16:34:53 +0000
73+++ bzrlib/export/__init__.py 2011-07-21 07:11:34 +0000
74@@ -19,6 +19,8 @@
75
76 import os
77 import time
78+import warnings
79+
80 from bzrlib import (
81 errors,
82 pyutils,
83@@ -58,10 +60,10 @@
84
85 When requesting a specific type of export, load the respective path.
86 """
87- def _loader(tree, dest, root, subdir, filtered, force_mtime, fileobj):
88+ def _loader(tree, dest, root, subdir, force_mtime, fileobj):
89 func = pyutils.get_named_object(module, funcname)
90- return func(tree, dest, root, subdir, filtered=filtered,
91- force_mtime=force_mtime, fileobj=fileobj)
92+ return func(tree, dest, root, subdir, force_mtime=force_mtime,
93+ fileobj=fileobj)
94
95 register_exporter(scheme, extensions, _loader)
96
97@@ -91,7 +93,8 @@
98 a directory to start exporting from.
99
100 :param filtered: If True, content filtering is applied to the exported
101- files.
102+ files. Deprecated in favour of passing a ContentFilterTree
103+ as the source.
104
105 :param per_file_timestamps: Whether to use the timestamp stored in the tree
106 rather than now(). This will do a revision lookup for every file so
107@@ -122,13 +125,20 @@
108
109 trace.mutter('export version %r', tree)
110
111+ if filtered:
112+ from bzrlib.filter_tree import ContentFilterTree
113+ warnings.warn(
114+ "passing filtered=True to export is deprecated in bzr 2.4",
115+ stacklevel=2)
116+ tree = ContentFilterTree(tree, tree._content_filter_stack)
117+ # We don't want things re-filtered by the specific exporter.
118+ filtered = False
119+
120+ tree.lock_read()
121 try:
122- tree.lock_read()
123-
124- for _ in _exporters[format](tree, dest, root, subdir,
125- filtered=filtered,
126- force_mtime=force_mtime, fileobj=fileobj):
127-
128+ for _ in _exporters[format](
129+ tree, dest, root, subdir,
130+ force_mtime=force_mtime, fileobj=fileobj):
131 yield
132 finally:
133 tree.unlock()
134@@ -153,7 +163,7 @@
135 the entire tree, and anything else should specify the relative path to
136 a directory to start exporting from.
137 :param filtered: If True, content filtering is applied to the
138- files exported.
139+ files exported. Deprecated in favor of passing an ContentFilterTree.
140 :param per_file_timestamps: Whether to use the timestamp stored in the
141 tree rather than now(). This will do a revision lookup
142 for every file so will be significantly slower.
143
144=== modified file 'bzrlib/export/dir_exporter.py'
145--- bzrlib/export/dir_exporter.py 2011-06-28 13:55:39 +0000
146+++ bzrlib/export/dir_exporter.py 2011-07-21 07:11:34 +0000
147@@ -21,13 +21,9 @@
148
149 from bzrlib import errors, osutils
150 from bzrlib.export import _export_iter_entries
151-from bzrlib.filters import (
152- ContentFilterContext,
153- filtered_output_bytes,
154- )
155-
156-
157-def dir_exporter_generator(tree, dest, root, subdir=None, filtered=False,
158+
159+
160+def dir_exporter_generator(tree, dest, root, subdir=None,
161 force_mtime=None, fileobj=None):
162 """Return a generator that exports this tree to a new directory.
163
164@@ -79,10 +75,6 @@
165 # the directories
166 flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | getattr(os, 'O_BINARY', 0)
167 for (relpath, executable), chunks in tree.iter_files_bytes(to_fetch):
168- if filtered:
169- filters = tree._content_filter_stack(relpath)
170- context = ContentFilterContext(relpath, tree, ie)
171- chunks = filtered_output_bytes(chunks, filters, context)
172 fullpath = osutils.pathjoin(dest, relpath)
173 # We set the mode and let the umask sort out the file info
174 mode = 0666
175
176=== modified file 'bzrlib/export/tar_exporter.py'
177--- bzrlib/export/tar_exporter.py 2011-07-11 00:59:24 +0000
178+++ bzrlib/export/tar_exporter.py 2011-07-21 07:11:34 +0000
179@@ -14,7 +14,7 @@
180 # along with this program; if not, write to the Free Software
181 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
182
183-"""Export a Tree to a non-versioned directory."""
184+"""Export a tree to a tarball."""
185
186 import os
187 import StringIO
188@@ -26,14 +26,9 @@
189 osutils,
190 )
191 from bzrlib.export import _export_iter_entries
192-from bzrlib.filters import (
193- ContentFilterContext,
194- filtered_output_bytes,
195- )
196-
197-
198-def prepare_tarball_item(tree, root, final_path, entry, filtered=False,
199- force_mtime=None):
200+
201+
202+def prepare_tarball_item(tree, root, final_path, entry, force_mtime=None):
203 """Prepare a tarball item for exporting
204
205 :param tree: Tree to export
206@@ -42,8 +37,6 @@
207
208 :param entry: Entry to export
209
210- :param filtered: Whether to apply filters
211-
212 :param force_mtime: Option mtime to force, instead of using tree
213 timestamps.
214
215@@ -61,17 +54,13 @@
216 item.mode = 0755
217 else:
218 item.mode = 0644
219- if filtered:
220- chunks = tree.get_file_lines(entry.file_id)
221- filters = tree._content_filter_stack(final_path)
222- context = ContentFilterContext(final_path, tree, entry)
223- contents = filtered_output_bytes(chunks, filters, context)
224- content = ''.join(contents)
225- item.size = len(content)
226- fileobj = StringIO.StringIO(content)
227- else:
228- item.size = tree.get_file_size(entry.file_id)
229- fileobj = tree.get_file(entry.file_id)
230+ # This brings the whole file into memory, but that's almost needed for
231+ # the tarfile contract, which wants the size of the file up front. We
232+ # want to make sure it doesn't change, and we need to read it in one
233+ # go for content filtering.
234+ content = tree.get_file_text(entry.file_id)
235+ item.size = len(content)
236+ fileobj = StringIO.StringIO(content)
237 elif entry.kind == "directory":
238 item.type = tarfile.DIRTYPE
239 item.name += '/'
240@@ -90,8 +79,7 @@
241 return (item, fileobj)
242
243
244-def export_tarball_generator(tree, ball, root, subdir=None, filtered=False,
245- force_mtime=None):
246+def export_tarball_generator(tree, ball, root, subdir=None, force_mtime=None):
247 """Export tree contents to a tarball.
248
249 :returns: A generator that will repeatedly produce None as each file is
250@@ -103,8 +91,6 @@
251 :param ball: Tarball to export to; it will be closed when writing is
252 complete.
253
254- :param filtered: Whether to apply filters
255-
256 :param subdir: Sub directory to export
257
258 :param force_mtime: Option mtime to force, instead of using tree
259@@ -113,15 +99,15 @@
260 try:
261 for final_path, entry in _export_iter_entries(tree, subdir):
262 (item, fileobj) = prepare_tarball_item(
263- tree, root, final_path, entry, filtered, force_mtime)
264+ tree, root, final_path, entry, force_mtime)
265 ball.addfile(item, fileobj)
266 yield
267 finally:
268 ball.close()
269
270
271-def tgz_exporter_generator(tree, dest, root, subdir, filtered=False,
272- force_mtime=None, fileobj=None):
273+def tgz_exporter_generator(tree, dest, root, subdir, force_mtime=None,
274+ fileobj=None):
275 """Export this tree to a new tar file.
276
277 `dest` will be created holding the contents of this tree; if it
278@@ -161,7 +147,7 @@
279 zipstream = gzip.GzipFile(basename, 'w', fileobj=stream)
280 ball = tarfile.open(None, 'w|', fileobj=zipstream)
281 for _ in export_tarball_generator(
282- tree, ball, root, subdir, filtered, force_mtime):
283+ tree, ball, root, subdir, force_mtime):
284 yield
285 # Closing zipstream may trigger writes to stream
286 zipstream.close()
287@@ -170,7 +156,7 @@
288 stream.close()
289
290
291-def tbz_exporter_generator(tree, dest, root, subdir, filtered=False,
292+def tbz_exporter_generator(tree, dest, root, subdir,
293 force_mtime=None, fileobj=None):
294 """Export this tree to a new tar file.
295
296@@ -189,12 +175,11 @@
297 # Python 2.6.5 and 2.7b1)
298 ball = tarfile.open(dest.encode(osutils._fs_enc), 'w:bz2')
299 return export_tarball_generator(
300- tree, ball, root, subdir, filtered, force_mtime)
301+ tree, ball, root, subdir, force_mtime)
302
303
304 def plain_tar_exporter_generator(tree, dest, root, subdir, compression=None,
305- filtered=False, force_mtime=None,
306- fileobj=None):
307+ force_mtime=None, fileobj=None):
308 """Export this tree to a new tar file.
309
310 `dest` will be created holding the contents of this tree; if it
311@@ -208,16 +193,16 @@
312 stream = open(dest, 'wb')
313 ball = tarfile.open(None, 'w|', stream)
314 return export_tarball_generator(
315- tree, ball, root, subdir, filtered, force_mtime)
316-
317-
318-def tar_xz_exporter_generator(tree, dest, root, subdir, filtered=False,
319+ tree, ball, root, subdir, force_mtime)
320+
321+
322+def tar_xz_exporter_generator(tree, dest, root, subdir,
323 force_mtime=None, fileobj=None):
324- return tar_lzma_exporter_generator(tree, dest, root, subdir, filtered,
325+ return tar_lzma_exporter_generator(tree, dest, root, subdir,
326 force_mtime, fileobj, "xz")
327
328
329-def tar_lzma_exporter_generator(tree, dest, root, subdir, filtered=False,
330+def tar_lzma_exporter_generator(tree, dest, root, subdir,
331 force_mtime=None, fileobj=None,
332 compression_format="alone"):
333 """Export this tree to a new .tar.lzma file.
334@@ -240,4 +225,4 @@
335 options={"format": compression_format})
336 ball = tarfile.open(None, 'w:', fileobj=stream)
337 return export_tarball_generator(
338- tree, ball, root, subdir, filtered=filtered, force_mtime=force_mtime)
339+ tree, ball, root, subdir, force_mtime=force_mtime)
340
341=== modified file 'bzrlib/export/zip_exporter.py'
342--- bzrlib/export/zip_exporter.py 2011-06-13 16:34:53 +0000
343+++ bzrlib/export/zip_exporter.py 2011-07-21 07:11:34 +0000
344@@ -27,10 +27,6 @@
345 osutils,
346 )
347 from bzrlib.export import _export_iter_entries
348-from bzrlib.filters import (
349- ContentFilterContext,
350- filtered_output_bytes,
351- )
352 from bzrlib.trace import mutter
353
354
355@@ -44,7 +40,7 @@
356 _DIR_ATTR = stat.S_IFDIR | ZIP_DIRECTORY_BIT | DIR_PERMISSIONS
357
358
359-def zip_exporter_generator(tree, dest, root, subdir=None, filtered=False,
360+def zip_exporter_generator(tree, dest, root, subdir=None,
361 force_mtime=None, fileobj=None):
362 """ Export this tree to a new zip file.
363
364@@ -77,14 +73,7 @@
365 date_time=date_time)
366 zinfo.compress_type = compression
367 zinfo.external_attr = _FILE_ATTR
368- if filtered:
369- chunks = tree.get_file_lines(file_id)
370- filters = tree._content_filter_stack(dp)
371- context = ContentFilterContext(dp, tree, ie)
372- contents = filtered_output_bytes(chunks, filters, context)
373- content = ''.join(contents)
374- else:
375- content = tree.get_file_text(file_id)
376+ content = tree.get_file_text(file_id)
377 zipf.writestr(zinfo, content)
378 elif ie.kind == "directory":
379 # Directories must contain a trailing slash, to indicate
380
381=== added file 'bzrlib/filter_tree.py'
382--- bzrlib/filter_tree.py 1970-01-01 00:00:00 +0000
383+++ bzrlib/filter_tree.py 2011-07-21 07:11:34 +0000
384@@ -0,0 +1,82 @@
385+# Copyright (C) 2011 Canonical Ltd
386+#
387+# This program is free software; you can redistribute it and/or modify
388+# it under the terms of the GNU General Public License as published by
389+# the Free Software Foundation; either version 2 of the License, or
390+# (at your option) any later version.
391+#
392+# This program is distributed in the hope that it will be useful,
393+# but WITHOUT ANY WARRANTY; without even the implied warranty of
394+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
395+# GNU General Public License for more details.
396+#
397+# You should have received a copy of the GNU General Public License
398+# along with this program; if not, write to the Free Software
399+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
400+
401+"""Content-filtered view of any tree.
402+"""
403+
404+
405+from bzrlib import (
406+ tree,
407+ )
408+from bzrlib.filters import (
409+ ContentFilter,
410+ ContentFilterContext,
411+ filtered_input_file,
412+ filtered_output_bytes,
413+ _get_filter_stack_for,
414+ _get_registered_names,
415+ internal_size_sha_file_byname,
416+ register_filter_stack_map,
417+ )
418+
419+
420+class ContentFilterTree(tree.Tree):
421+ """A virtual tree that applies content filters to an underlying tree.
422+
423+ Not every operation is supported yet.
424+ """
425+
426+ def __init__(self, backing_tree, filter_stack_callback):
427+ """Construct a new filtered tree view.
428+
429+ :param filter_stack_callback: A callable taking a path that returns
430+ the filter stack that should be used for that path.
431+ :param backing_tree: An underlying tree to wrap.
432+ """
433+ self.backing_tree = backing_tree
434+ self.filter_stack_callback = filter_stack_callback
435+
436+ def get_file_text(self, file_id, path=None):
437+ chunks = self.backing_tree.get_file_lines(file_id, path)
438+ filters = self.filter_stack_callback(path)
439+ if path is None:
440+ path = self.backing_tree.id2path(file_id)
441+ context = ContentFilterContext(path, self, None)
442+ contents = filtered_output_bytes(chunks, filters, context)
443+ content = ''.join(contents)
444+ return content
445+
446+ def has_filename(self, filename):
447+ return self.backing_tree.has_filename
448+
449+ def is_executable(self, file_id, path=None):
450+ return self.backing_tree.is_executable(file_id, path)
451+
452+ def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=None):
453+ # NB: This simply returns the parent tree's entries; the length may be
454+ # wrong but it can't easily be calculated without filtering the whole
455+ # text. Currently all callers cope with this; perhaps they should be
456+ # updated to a narrower interface that only provides things guaranteed
457+ # cheaply available across all trees. -- mbp 20110705
458+ return self.backing_tree.iter_entries_by_dir(
459+ specific_file_ids=specific_file_ids,
460+ yield_parents=yield_parents)
461+
462+ def lock_read(self):
463+ return self.backing_tree.lock_read()
464+
465+ def unlock(self):
466+ return self.backing_tree.unlock()
467
468=== modified file 'bzrlib/tests/__init__.py'
469--- bzrlib/tests/__init__.py 2011-07-15 09:22:16 +0000
470+++ bzrlib/tests/__init__.py 2011-07-21 07:11:34 +0000
471@@ -16,12 +16,6 @@
472
473 """Testing framework extensions"""
474
475-# TODO: Perhaps there should be an API to find out if bzr running under the
476-# test suite -- some plugins might want to avoid making intrusive changes if
477-# this is the case. However, we want behaviour under to test to diverge as
478-# little as possible, so this should be used rarely if it's added at all.
479-# (Suggestion from j-a-meinel, 2005-11-24)
480-
481 # NOTE: Some classes in here use camelCaseNaming() rather than
482 # underscore_naming(). That's for consistency with unittest; it's not the
483 # general style of bzrlib. Please continue that consistency when adding e.g.
484@@ -3896,6 +3890,7 @@
485 'bzrlib.tests.test_fixtures',
486 'bzrlib.tests.test_fifo_cache',
487 'bzrlib.tests.test_filters',
488+ 'bzrlib.tests.test_filter_tree',
489 'bzrlib.tests.test_ftp_transport',
490 'bzrlib.tests.test_foreign',
491 'bzrlib.tests.test_generate_docs',
492
493=== modified file 'bzrlib/tests/fixtures.py'
494--- bzrlib/tests/fixtures.py 2011-02-09 06:36:35 +0000
495+++ bzrlib/tests/fixtures.py 2011-07-21 07:11:34 +0000
496@@ -125,3 +125,16 @@
497 source.set_last_revision_info(1, 'rev-1')
498 return source
499
500+
501+def make_branch_and_populated_tree(testcase):
502+ """Make a simple branch and tree.
503+
504+ The tree holds some added but uncommitted files.
505+ """
506+ # TODO: Either accept or return the names of the files, so the caller
507+ # doesn't need to be bound to the particular files created? -- mbp
508+ # 20110705
509+ tree = testcase.make_branch_and_tree('t')
510+ testcase.build_tree_contents([('t/hello', 'hello world')])
511+ tree.add(['hello'], ['hello-id'])
512+ return tree
513
514=== added file 'bzrlib/tests/test_filter_tree.py'
515--- bzrlib/tests/test_filter_tree.py 1970-01-01 00:00:00 +0000
516+++ bzrlib/tests/test_filter_tree.py 2011-07-21 07:11:34 +0000
517@@ -0,0 +1,68 @@
518+# Copyright (C) 2011 Canonical Ltd
519+#
520+# This program is free software; you can redistribute it and/or modify
521+# it under the terms of the GNU General Public License as published by
522+# the Free Software Foundation; either version 2 of the License, or
523+# (at your option) any later version.
524+#
525+# This program is distributed in the hope that it will be useful,
526+# but WITHOUT ANY WARRANTY; without even the implied warranty of
527+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
528+# GNU General Public License for more details.
529+#
530+# You should have received a copy of the GNU General Public License
531+# along with this program; if not, write to the Free Software
532+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
533+
534+"""Tests for ContentFilterTree"""
535+
536+import tarfile
537+import zipfile
538+
539+from bzrlib import (
540+ export,
541+ filter_tree,
542+ tests,
543+ )
544+from bzrlib.tests import (
545+ fixtures,
546+ )
547+from bzrlib.tests.test_filters import _stack_1
548+
549+
550+class TestFilterTree(tests.TestCaseWithTransport):
551+
552+ def make_tree(self):
553+ self.underlying_tree = fixtures.make_branch_and_populated_tree(
554+ self)
555+ def stack_callback(path):
556+ return _stack_1
557+ self.filter_tree = filter_tree.ContentFilterTree(
558+ self.underlying_tree, stack_callback)
559+ return self.filter_tree
560+
561+ def test_get_file_text(self):
562+ self.make_tree()
563+ self.assertEquals(
564+ self.underlying_tree.get_file_text('hello-id'),
565+ 'hello world')
566+ self.assertEquals(
567+ self.filter_tree.get_file_text('hello-id'),
568+ 'HELLO WORLD')
569+
570+ def test_tar_export_content_filter_tree(self):
571+ # TODO: this could usefully be run generically across all exporters.
572+ self.make_tree()
573+ export.export(self.filter_tree, "out.tgz")
574+ ball = tarfile.open("out.tgz", "r:gz")
575+ self.assertEquals(
576+ 'HELLO WORLD',
577+ ball.extractfile('out/hello').read())
578+
579+ def test_zip_export_content_filter_tree(self):
580+ self.make_tree()
581+ export.export(self.filter_tree, 'out.zip')
582+ zipf = zipfile.ZipFile('out.zip', 'r')
583+ self.assertEquals(
584+ 'HELLO WORLD',
585+ zipf.read('out/hello'))
586
587=== modified file 'bzrlib/tree.py'
588--- bzrlib/tree.py 2011-06-19 02:24:39 +0000
589+++ bzrlib/tree.py 2011-07-21 07:11:34 +0000
590@@ -277,8 +277,11 @@
591
592 :param file_id: The file_id of the file.
593 :param path: The path of the file.
594+
595 If both file_id and path are supplied, an implementation may use
596 either one.
597+
598+ :returns: A single byte string for the whole file.
599 """
600 my_file = self.get_file(file_id, path)
601 try:
602
603=== modified file 'doc/en/release-notes/bzr-2.5.txt'
604--- doc/en/release-notes/bzr-2.5.txt 2011-07-19 13:06:44 +0000
605+++ doc/en/release-notes/bzr-2.5.txt 2011-07-21 07:11:34 +0000
606@@ -89,6 +89,12 @@
607 * Remove ``TransportListRegistry.set_default_transport``, as the concept of
608 a default transport is currently unused. (Jelmer Vernooij)
609
610+* There is a new class `ContentFilterTree` that provides a facade for
611+ content filtering. The `filtered` parameter to `export` is deprecated
612+ in favor of passing a filtered tree, and the specific exporter plugins
613+ no longer support it.
614+ (Martin Pool)
615+
616 Internals
617 *********
618