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 |
Related bugs: |
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.
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-
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk4
XJYAn2he5twNFON
=vDXl
-----END PGP SIGNATURE-----
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.
Martin Pool (mbp) wrote : | # |
Thanks - I'll see if I can land these branches tomorrow.
Martin Pool (mbp) wrote : | # |
sent to pqm by email
Preview Diff
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 |
Refactoring filtering code from several places into a commond object reduces code and makes it easier to maintain, approved.