Merge lp:~abentley/bzr/nested-trees-composite-tree into lp:~bzr/bzr/trunk-old

Proposed by Aaron Bentley
Status: Work in progress
Proposed branch: lp:~abentley/bzr/nested-trees-composite-tree
Merge into: lp:~bzr/bzr/trunk-old
Diff against target: 1484 lines (has conflicts)
Text conflict in bzrlib/tests/__init__.py
Text conflict in bzrlib/transform.py
To merge this branch: bzr merge lp:~abentley/bzr/nested-trees-composite-tree
Reviewer Review Type Date Requested Status
Martin Pool Pending
Review via email: mp+6264@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

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

Hi Martin,

You wanted some documentation of CompositeTree to be included with the
patch that introduces it. Since NestedTrees is also introduced by this
patch and is arguably more important in the long run, I've included
documentation for it as well.

I hope this is the kind of thing you were looking for.

 reviewer mbp

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

iEYEARECAAYFAkoB0ogACgkQ0F+nu1YWqI3BFgCfTME8bivflaYQe2afjcdgVPeO
HdgAnRvPljssQet3zncVrShuQltoj+vB
=BAWZ
-----END PGP SIGNATURE-----

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

> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> Hi Martin,
>
> You wanted some documentation of CompositeTree to be included with the
> patch that introduces it. Since NestedTrees is also introduced by this
> patch and is arguably more important in the long run, I've included
> documentation for it as well.
>
> I hope this is the kind of thing you were looking for.

So yes, it is.

I wonder if this would be better described in the overview of major classes (in HACKING, I think?) than separated out into something only about nested trees. If you were going to add a new documentation file you should link it into the index, but here I think it's better not to add one.

I think for ReST you need a blank line between the heading underline and the following text?

In CompositeTree, I'd like to see something about how you obtain one.

It's good to write it up like this because it makes the issues more clear. For example, what will happen if code gets a CompositeTree and tries to treat it like a regular tree? Will it just error? (Some of these can be discussed in the thread about the general design but probably they should be answered in the docs.) If "by con

1279 +It is constructed from a Tree (typically a RevisionTree or WorkingTree) and a
1280 +Branch.

If you mean you just do

  CompositeTree(top_tree, branch)

then a code sample to say so would be unambiguous.

+
+NestedTrees
+-----------
+NestedTrees is an API providing access to a set of nested trees. It is able to
+convert paths and file-ids into references to specific Trees. It provides
+caching, to avoid retrieving the same Tree multiple times. It provides
+locking, to ensure trees are locked and unlocked at appropriate times.
+
+It is used by CompositeTree, and is a recommended API for all other operations.
+It is expected to acquire more capabilities as the needs of sets of nested
+trees become clearer.

Names that sound like they're plurals of some other class name are confusing. (Is this about ``NestedTree``, which sounds like a kind of tree?) Maybe NestedTreeSet or TreeNest?

Again I'd ask how you get one of them.

Thanks

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

How did you get on while I was away with the enhancement proposal for the whole thing? Reading that seems like the next step forward.

Revision history for this message
Aaron Bentley (abentley) wrote :

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

Martin Pool wrote:
> How did you get on while I was away with the enhancement proposal for the whole thing?

I'm working on launchpad stuff for a bit. My unfinished draft is here:

bzr+ssh://bazaar.launchpad.net/~abentley/bzr/devnotes
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAko5MSMACgkQ0F+nu1YWqI37XwCfeRKrkeKqJAjFX8vl4dnfKNbR
g+oAn2Ux8BctPmoq41Vsd1/LiudMGQR3
=KaIe
-----END PGP SIGNATURE-----

Revision history for this message
Alexander Belchenko (bialix) wrote :

PING. Why this one is blocked?

Unmerged revisions

1937. By Aaron Bentley

Add documentation of CompositeTree and NestedTrees

1936. By Aaron Bentley

Merge bzr.dev into composite-tree.

1935. By Aaron Bentley

Merge bzr.dev into composite-tree.

1934. By Aaron Bentley

Merge bzr.dev into composite-tree.

1933. By Aaron Bentley

Merge bzr.dev into composite-tree.

1932. By Aaron Bentley

Merge bzr.dev into composite-tree.

1931. By Aaron Bentley

Merge bzr.dev into composite-tree

1930. By Aaron Bentley

More updates from review

1929. By Aaron Bentley

More updates from review.

1928. By Aaron Bentley

Updates from review

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'bzrlib/composite_tree.py'
--- bzrlib/composite_tree.py 1970-01-01 00:00:00 +0000
+++ bzrlib/composite_tree.py 2009-08-31 04:38:31 +0000
@@ -0,0 +1,761 @@
1# Copyright (C) 2007, 2008, 2009 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Tree-like class for manipulating nested trees to be like a single tree"""
18
19from bzrlib import (
20 conflicts as _mod_conflicts,
21 errors,
22 osutils,
23 transform,
24 tree,
25 )
26
27class NestedTrees(object):
28 """Provide data about a collection of nested trees.
29
30 This is a sparse interface that has high fidelity to its underlying
31 implementation. It may be used directly, or as the internal state of
32 CompositeTree/_CompositeInventory.
33 """
34
35 def __init__(self, root_tree, root_branch):
36 self.root_tree = root_tree
37 self.root_branch = root_branch
38 self.lock_count = 0
39 self.lock_type = None
40 self._clear_cached_trees()
41
42 def _clear_cached_trees(self):
43 if self.lock_type is not None:
44 raise AssertionError('Cannot clear cached trees when locked.')
45 self._subtrees_by_relpath = {}
46 self._all_trees_scanned = False
47 self._locked_trees = []
48
49 def lock_read(self):
50 """Read-lock all open trees, open new trees read-locked."""
51 if self.lock_count == 0:
52 self.root_tree.lock_read()
53 self.lock_type = 'read'
54 self._locked_trees.append(self.root_tree)
55 self.lock_count += 1
56
57 def lock_write(self):
58 """Write-lock all open trees, open new trees write-locked."""
59 if self.lock_count == 0:
60 self.root_tree.lock_write()
61 self.lock_type = 'write'
62 self._locked_trees.append(self.root_tree)
63 self.lock_count += 1
64
65 def lock_tree_write(self):
66 """Write-lock all open trees, open new trees write-locked.
67
68 Branches are read-locked, instead of being write-locked as usual.
69 """
70 if self.lock_count == 0:
71 self.root_tree.lock_tree_write()
72 self.lock_type = 'tree_write'
73 self._locked_trees.append(self.root_tree)
74 self.lock_count += 1
75
76 def unlock(self):
77 """Unlock open trees."""
78 self.lock_count -= 1
79 if self.lock_count < 0:
80 raise AssertionError('lock count is negative.')
81 if self.lock_count == 0:
82 for tree in reversed(self._locked_trees):
83 tree.unlock()
84 self.lock_type = None
85 self._clear_cached_trees()
86
87 def _must_be_locked(self):
88 if self.lock_type is None:
89 raise errors.ObjectNotLocked(self)
90
91 def paths_info(self, paths):
92 """Return path, relpath, inventory entry, and tree for input paths.
93
94 The path is the path to the subtree relative to the root tree. The
95 relpath is the path within the subtree. The inventory entry will be
96 None if the path is not versioned. The tree is the subtree
97 containing the relpath.
98
99 :param paths: A list of paths relative to the root tree.
100 :return: a list of (path, relpath, entry, tree) tuples.
101 """
102 self._must_be_locked()
103 return [self._path_info(p) for p in paths]
104
105 def _path_info(self, path):
106 """Find the relpath, tree and inventory entry for a path.
107
108 The path is the path to the subtree relative to the root tree. The
109 relpath is the path within the subtree. The inventory will be
110 None if the path is not versioned. The tree is the subtree
111 containing the relpath.
112
113 :param path: A path relative to the root tree.
114 :return: a (path, relpath, entry, tree) tuple.
115 """
116 parts = osutils.splitpath(path)
117 tree = self.root_tree
118 branch = self.root_branch
119 node = tree.inventory.root
120 relpath = []
121 for part in parts:
122 relpath.append(part)
123 children = getattr(node, 'children', {})
124 node = children.get(part)
125 if node is None:
126 return path, osutils.pathjoin(*relpath), None, tree
127 try:
128 kind = tree.kind(node.file_id)
129 except errors.NoSuchFile:
130 pass
131 else:
132 if kind == 'tree-reference':
133 tree, branch = self.get_tree_branch(branch, tree,
134 node.file_id, osutils.pathjoin(*relpath))
135 node = tree.inventory.root
136 relpath = []
137 continue
138 if relpath == []:
139 relpath_str = ''
140 else:
141 relpath_str = osutils.pathjoin(*relpath)
142 return path, relpath_str, node, tree
143
144 def get_tree(self, file_id):
145 """Return the Tree that contains a given file-id."""
146 return self.get_tree_and_treepath(file_id)[0]
147
148 def get_tree_and_treepath(self, file_id, skip_references=False):
149 """Get a tree containing file_id and path relative to the root tree.
150
151 The path is the path to the subtree, not the file in the subtree.
152
153 :skip_references: if True, only results where the file_id is not
154 a tree reference will be supplied.
155 """
156 for path, tree in self.all_trees().iteritems():
157 if file_id in tree.inventory:
158 if skip_references:
159 try:
160 if tree.kind(file_id) == 'tree-reference':
161 continue
162 except errors.NoSuchFile:
163 pass
164 return tree, path
165 else:
166 raise errors.NoSuchId(self, file_id)
167
168 def get_path_info(self, path):
169 """Determine the tree containing a path and the tree's pathname.
170
171 :return: a tuple of (tree, tree_path, relpath), where tree_path
172 is the path to the tree, relative to the root_tree, and relpath
173 is the path relative to the returned tree.
174 """
175 trees = self.all_trees()
176 prefix = path
177 while True:
178 prefix = osutils.dirname(prefix)
179 subpath = path[len(prefix):]
180 subpath = subpath.lstrip('/')
181 try:
182 return trees[prefix], prefix, subpath
183 except KeyError:
184 pass
185 if subpath == '':
186 raise AssertionError('Subpath cannot be empty.')
187
188 def iter_pathinfo_by_tree(self, paths):
189 """Iterate through the path information for provided paths.
190
191 Iterates through (tree, tree_path, relpaths) tuples, where
192 tree is a tree containing some of the paths, tree_path is the relative
193 path to the tree from the root_tree, and relpaths are the paths within
194 the returned tree.
195 """
196 tree_path = None
197 tree = None
198 # Reverse sort should guarantee that all subtree paths are processed
199 # before tree paths. So if path.startswith(tree_path), we're in
200 # the same tree.
201 for path in sorted(paths, reverse=True):
202 if tree_path is None or not path.startswith(tree_path):
203 if tree is not None:
204 yield tree, tree_path, subpaths
205 tree, tree_path, subpath = self.get_path_info(path)
206 subpaths = [subpath]
207 else:
208 subpaths.append(path[len(tree_path):].lstrip('/'))
209 if tree is not None:
210 yield tree, tree_path, subpaths
211
212 def _scan_subtrees(self):
213 self._must_be_locked()
214 def do_scan(prefix, relpath, tree, containing_tree, branch):
215 self._register_tree_relpath(tree, containing_tree, relpath)
216 for path, file_id in tree.iter_references():
217 tree_path = tree.id2path(file_id)
218 composite_path = osutils.pathjoin(prefix, tree_path)
219 subtree, subbranch = self.get_tree_branch(branch, tree,
220 file_id, tree_path)
221 do_scan(composite_path, tree_path, subtree, tree, subbranch)
222 do_scan('', '', self.root_tree, None, self.root_branch)
223
224 def _register_tree_relpath(self, tree, containing_tree, relpath):
225 """Register the relpath of a Tree in the relevant dicts."""
226 tree_relpaths = self._subtrees_by_relpath.get(containing_tree, {})
227 tree_relpaths[relpath] = tree
228 self._subtrees_by_relpath[containing_tree] = tree_relpaths
229
230 def get_tree_branch(self, branch, tree, file_id, path):
231 """Retrieve the tree and branch for a subtree path
232
233 :param branch: The branch associated with the containing tree
234 :param tree: The containing tree
235 :param file_id: The file id of the subtree
236 :param path: The tree-relative path to the subtree
237 """
238 subbranch = branch.reference_parent(file_id, path)
239 try:
240 subtree = self._subtrees_by_relpath[tree][path]
241 except KeyError:
242 # Unknown tree, so we must try and retrieve it.
243 subtree = tree.get_nested_tree(file_id, branch)
244 # Tree must be locked to match the other components of this
245 # NestedTree so that they all behave the same.
246 if self.lock_type is not None:
247 if self.lock_type == 'read':
248 subtree.lock_read()
249 elif self.lock_type == 'write':
250 subtree.lock_write()
251 elif self.lock_type == 'tree_write':
252 subtree.lock_tree_write()
253 self._locked_trees.append(subtree)
254 self._register_tree_relpath(subtree, tree, path)
255 return subtree, subbranch
256
257 def all_trees(self):
258 """Return a dict of composite_path to tree for all subtrees."""
259 if not self._all_trees_scanned:
260 self._scan_subtrees()
261 self._all_trees_scanned = True
262 root_tree_paths = {}
263 def do_tree(tree, root_tree_path):
264 root_tree_paths[root_tree_path] = tree
265 subtrees = self._subtrees_by_relpath.get(tree, {})
266 for relpath, subtree in subtrees.iteritems():
267 do_tree(subtree, osutils.pathjoin(root_tree_path, relpath))
268 do_tree(self.root_tree, '')
269 return root_tree_paths
270
271
272class _CompositeInventory(object):
273 """An inventory that combines the inventories of several subtrees.
274
275 Intended for use as CompositeTree.inventory, not for direct instantiation.
276 """
277
278 def __init__(self, nested_trees):
279 self._nested_trees = nested_trees
280
281 def _get_ci_path_info(self, path):
282 file_id = None
283 tree = self._nested_trees.root_tree
284 node = tree.inventory.root
285 canonical_path = []
286 remaining_path = osutils.splitpath(path)
287 while len(remaining_path) > 0:
288 part = remaining_path[0]
289 next_node = None
290 children = getattr(node, 'children', {})
291 for child in children.itervalues():
292 if child.name.lower() == part.lower():
293 next_node = child
294 if child.name == part:
295 break
296 if next_node is None:
297 break
298 remaining_path.pop(0)
299 canonical_path.append(next_node.name)
300 node = next_node
301 if node.kind == 'tree-reference':
302 tree, path_ = self._nested_trees.get_tree_and_treepath(
303 node.file_id, skip_references=True)
304 node = tree.inventory.root
305 if node is not None:
306 file_id = node.file_id
307 canonical_path.extend(remaining_path)
308 if len(canonical_path) == 0:
309 result = ''
310 else:
311 result = osutils.pathjoin(*canonical_path)
312 return result, file_id
313
314 def path2id(self, path):
315 """See Inventory.path2id."""
316 path, relpath, entry, tree = self._nested_trees.paths_info([path])[0]
317 if entry is None:
318 return None
319 return entry.file_id
320
321 def id2path(self, file_id):
322 """See Inventory.id2path."""
323 tree, path = self._nested_trees.get_tree_and_treepath(file_id)
324 return osutils.pathjoin(*[path, tree.id2path(file_id)])
325
326 def get_file_kind(self, file_id):
327 """See Inventory.get_file_kind."""
328 tree = self._nested_trees.get_tree_and_treepath(file_id,
329 skip_references=True)[0]
330 return tree.inventory.get_file_kind(file_id)
331
332 def iter_entries_by_dir(self, specific_file_ids=None, _tree=None,
333 _branch=None, _root=''):
334 """See Inventory.iter_entries_by_dir."""
335 tree = _tree
336 root = _root
337 branch = _branch
338 if tree is None:
339 tree = self._nested_trees.root_tree
340 if branch is None:
341 branch = self._nested_trees.root_branch
342 for subpath, entry in tree.iter_entries_by_dir():
343 if subpath == '':
344 path = root
345 else:
346 path = osutils.pathjoin(root, subpath)
347 try:
348 kind = tree.kind(entry.file_id)
349 except errors.NoSuchFile:
350 kind = None
351 if kind == 'tree-reference':
352 subtree, subbranch = self._nested_trees.get_tree_branch(
353 branch, tree, entry.file_id, path)
354 for path, entry in self.iter_entries_by_dir(
355 specific_file_ids, _tree=subtree,
356 _branch=subbranch, _root=path):
357 yield path, entry
358 else:
359 if (specific_file_ids is None or entry.file_id in
360 specific_file_ids):
361 yield path, entry
362
363 def iter_entries(self, from_dir=None):
364 """See Tree.iter_entries.
365
366 Note: violates the expected output order, but this is not generally
367 a problem.
368 """
369 if from_dir is not None:
370 from_dir_path = self.id2path(from_dir)
371 for path, entry in self.iter_entries_by_dir():
372 if from_dir is not None:
373 if not path.startswith(from_dir_path):
374 continue
375 path = path[len(from_dir_path):].lstrip('/')
376 yield path, entry
377
378 def __getitem__(self, file_id):
379 tree = self._nested_trees.get_tree(file_id)
380 return tree.inventory[file_id]
381
382 def __contains__(self, file_id):
383 try:
384 self._nested_trees.get_tree(file_id)
385 except errors.NoSuchId:
386 return False
387 else:
388 return True
389
390
391class CompositeTree(object):
392 """Implements parts of Tree interface in terms of subtrees.
393
394 Essentially, this is a by-value representation of a set of nested
395 subtrees.
396 Care should be taken in its use-- not suitable for every problem.
397 """
398
399 def __init__(self, root_tree, root_branch):
400 self._nested_trees = NestedTrees(root_tree, root_branch)
401 self.inventory = _CompositeInventory(self._nested_trees)
402
403 @property
404 def basedir(self):
405 """See WorkingTree.basedir."""
406 return self._nested_trees.root_tree.basedir
407
408 def move(self, from_paths, to_dir, after=False, _to_names=None):
409 """Rename files, possibly across trees"""
410 self.lock_tree_write()
411 try:
412 paths_info = self._nested_trees.paths_info(from_paths+[to_dir])
413 to_path, to_relpath, new_parent, to_tree = paths_info[-1]
414 if _to_names is not None and paths_info[0][2] is None:
415 raise errors.BzrRenameFailedError(from_paths[0],
416 osutils.pathjoin(to_dir, _to_names[0]),
417 errors.NotVersionedError(path=from_paths[0]))
418 if new_parent is None:
419 if _to_names is None:
420 from_ = ''
421 to = to_dir
422 else:
423 from_ = from_paths[0]
424 to = osutils.pathjoin(to_dir, _to_names[0])
425 raise errors.BzrMoveFailedError(from_, to,
426 errors.NotVersionedError(path=to_dir))
427 pending_trees = {}
428 result = []
429 for path, relpath, entry, tree, in paths_info:
430 if entry is None:
431 raise errors.BzrMoveFailedError(path, '',
432 errors.NotVersionedError(path=str(path)))
433 if tree.basedir not in pending_trees:
434 pending_trees[tree.basedir] = tree
435 pairs = []
436 changes = []
437
438 for num, (from_path, from_relpath, entry, from_tree) in\
439 enumerate(paths_info[:-1]):
440 full_to_path = osutils.pathjoin(to_path, entry.name)
441 pairs.append((from_path, full_to_path))
442 new_entry = entry.copy()
443 new_entry.parent_id = new_parent.file_id
444 if _to_names is not None:
445 new_entry.name = _to_names[num]
446 changes.append((from_path, full_to_path, new_entry.file_id,
447 new_entry))
448 if not after:
449 self._move_files(to_path, to_tree, _to_names, paths_info[:-1])
450 self.apply_inventory_delta(changes)
451 return pairs
452 finally:
453 self.unlock()
454
455 def _move_files(self, to_path, to_tree, to_names, paths_info):
456 trans_id_tree_path = {}
457 tt = transform.TreeTransform(self._nested_trees.root_tree)
458 parent_trans_id = tt.trans_id_tree_path(to_path)
459 for num, (from_path, from_relpath, entry, from_tree) in enumerate(
460 paths_info):
461 # If the source doesn't exist and the target does,
462 # don't touch the files but just update the inventory.
463 to_exists = to_tree.has_filename(osutils.pathjoin(to_path,
464 entry.name))
465 from_exists = from_tree.has_filename(from_relpath)
466 if not from_exists and to_exists:
467 continue
468
469 file_trans_id = tt.trans_id_tree_path(from_path)
470 trans_id_tree_path[file_trans_id] = from_path
471 if to_names is not None:
472 to_name = to_names[num]
473 else:
474 to_name = entry.name
475 tt.adjust_path(to_name, parent_trans_id, file_trans_id)
476 try:
477 try:
478 tt.apply(skip_inventory=True)
479 except errors.MalformedTransform, e:
480 if e.conflicts[0][0] == 'duplicate':
481 fp = transform.FinalPaths(tt)
482 source = trans_id_tree_path[e.conflicts[0][1]]
483 dest = fp.get_path(e.conflicts[0][1])
484 raise errors.RenameFailedFilesExist(source, dest)
485 else:
486 raise
487 finally:
488 tt.finalize()
489
490 def rename_one(self, from_rel, to_rel, after=False):
491 """See WorkingTree.rename_one."""
492 # We could be moving a file over subtree boundaries, figure out which
493 # trees are used.
494 self.lock_tree_write()
495 try:
496 from_tree, from_prefix, from_relpath = (
497 self._nested_trees.get_path_info(from_rel))
498 to_tree, to_prefix, to_relpath = (
499 self._nested_trees.get_path_info(to_rel))
500 if from_tree == to_tree:
501 self._nested_trees.root_tree.rename_one(from_rel, to_rel,
502 after=after)
503 else:
504 file_id = self.path2id(from_rel)
505 osutils.rename(osutils.pathjoin(from_prefix, from_relpath)
506 , osutils.pathjoin(to_prefix, to_relpath))
507 to_tree.add([to_relpath], ids=[file_id])
508 from_tree.remove([from_relpath])
509 finally:
510 self.unlock()
511
512 def get_file(self, file_id):
513 """See Tree.get_file."""
514 return self._nested_trees.get_tree(file_id).get_file(file_id)
515
516 def get_file_text(self, file_id, path=None):
517 """See Tree.get_file_text."""
518 tree, relpath = self._find_tree_path(file_id, path)
519 return tree.get_file_text(file_id, relpath)
520
521 def get_file_size(self, file_id):
522 """See Tree.get_file_size."""
523 return self._nested_trees.get_tree(file_id).get_file_size(file_id)
524
525 def _find_tree_path(self, file_id, path):
526 if path is not None:
527 path, relpath, entry, tree =\
528 self._nested_trees.paths_info([path])[0]
529 else:
530 tree = self._nested_trees.get_tree(file_id)
531 relpath = None
532 return tree, relpath
533
534 def is_executable(self, file_id, path=None):
535 """See RevisionTree.is_executable."""
536 tree, path = self._find_tree_path(file_id, path)
537 return tree.is_executable(file_id, path)
538
539 def has_id(self, file_id):
540 """See Tree.has_id."""
541 try:
542 tree = self._nested_trees.get_tree(file_id)
543 except errors.NoSuchId:
544 return False
545 if tree is None:
546 return False
547 return tree.has_id(file_id)
548
549 def changes_from(self, other, want_unchanged=False, specific_files=None,
550 extra_trees=None, require_versioned=False, include_root=False,
551 want_unversioned=False):
552 """Return a TreeDelta of the changes from other to this tree.
553
554 :param other: A tree to compare with.
555 :param specific_files: An optional list of file paths to restrict the
556 comparison to. When mapping filenames to ids, all matches in all
557 trees (including optional extra_trees) are used, and all children
558 of matched directories are included.
559 :param want_unchanged: An optional boolean requesting the inclusion
560 of unchanged entries in the result.
561 :param extra_trees: An optional list of additional trees to use when
562 mapping the contents of specific_files (paths) to file_ids.
563 :param require_versioned: An optional boolean (defaults to False).
564 When supplied and True all the 'specific_files' must be
565 versioned, or a PathsNotVersionedError will be thrown.
566 :param want_unversioned: Scan for unversioned paths.
567
568 The comparison will be performed by an InterTree object looked up on
569 self and other.
570 """
571 # Martin observes that Tree.changes_from returns a TreeDelta and this
572 # may confuse people, because the class name of the returned object
573 # is a synonym of the object referenced in the method name.
574 return tree.InterTree.get(other, self).compare(
575 want_unchanged=want_unchanged,
576 specific_files=specific_files,
577 extra_trees=extra_trees,
578 require_versioned=require_versioned,
579 include_root=include_root,
580 want_unversioned=want_unversioned,
581 )
582
583 def paths2ids(self, paths, trees=[], require_versioned=True):
584 """See Tree.paths2ids."""
585 return tree.find_ids_across_trees(paths, [self] + list(trees),
586 require_versioned)
587
588 def path2id(self, path):
589 """See Tree.path2id."""
590 # FIXME -- shouldn't return missing files
591 return self.inventory.path2id(path)
592
593 def iter_changes(self, from_tree, include_unchanged=False,
594 specific_files=None, pb=None, extra_trees=None,
595 require_versioned=True, want_unversioned=False):
596 """See InterTree.iter_changes."""
597 intertree = tree.InterTree.get(from_tree, self)
598 return intertree.iter_changes(include_unchanged, specific_files, pb,
599 extra_trees, require_versioned,
600 want_unversioned=want_unversioned)
601
602 def lock_read(self):
603 """See Tree.lock_read."""
604 self._nested_trees.lock_read()
605
606 def lock_write(self):
607 """See MutableTree.lock_write."""
608 self._nested_trees.lock_write()
609
610 def lock_tree_write(self):
611 """See MutableTree.lock_tree_write."""
612 self._nested_trees.lock_tree_write()
613
614 def unlock(self):
615 """See Tree.unlock."""
616 self._nested_trees.unlock()
617
618 def extras(self):
619 """See Tree.extras."""
620 result = []
621 for prefix, tree in self._nested_trees.all_trees().iteritems():
622 result.extend(osutils.pathjoin(prefix, p)
623 for p in tree.extras())
624 return iter(result)
625
626 def iter_entries_by_dir(self, specific_file_ids=None, _tree=None,
627 _branch=None, _root=''):
628 """See Tree.iter_entries_by_dir."""
629 return self.inventory.iter_entries_by_dir(specific_file_ids)
630
631 def conflicts(self):
632 """See WorkingTree.conflicts."""
633 # FIXME adjust paths in conflicts
634 conflicts = []
635 for prefix, tree in self._nested_trees.all_trees().iteritems():
636 conflicts.extend(tree.conflicts())
637 return _mod_conflicts.ConflictList(conflicts)
638
639 def all_file_ids(self):
640 """See Tree.all_file_ids."""
641 file_ids = set()
642 for tree in self._nested_trees.all_trees().itervalues():
643 file_ids.update(tree.all_file_ids())
644 return file_ids
645
646 def is_ignored(self, filename):
647 """See Tree.is_ignored."""
648 tree, prefix, filename = self._nested_trees.get_path_info(filename)
649 return tree.is_ignored(filename)
650
651 def kind(self, file_id):
652 """See Tree.kind"""
653 return self._nested_trees.get_tree(file_id).kind(file_id)
654
655 def has_filename(self, filename):
656 """See Tree.has_filename."""
657 tree, prefix, filename = self._nested_trees.get_path_info(filename)
658 return tree.has_filename(filename)
659
660 def filter_unversioned_files(self, paths):
661 """See Tree.filter_unversioned_files."""
662 unversioned = set()
663 for tree, tree_path, subpaths in \
664 self._nested_trees.iter_pathinfo_by_tree(paths):
665 unversioned.update(osutils.pathjoin(tree_path, s) for s
666 in tree.filter_unversioned_files(subpaths))
667 return unversioned
668
669 def _comparison_data(self, entry, path):
670 tree, prefix, path = self._nested_trees.get_path_info(path)
671 kind, executable, stat = tree._comparison_data(entry, path)
672 if kind == 'tree-reference':
673 kind = 'directory'
674 return kind, executable, stat
675
676 def _file_size(self, entry, stat_value):
677 return self._nested_trees.get_tree(entry.file_id)._file_size(
678 entry, stat_value)
679
680 def get_canonical_inventory_path(self, path):
681 """See Tree.get_canonical_inventory_path."""
682 return self.inventory._get_ci_path_info(path)[0]
683
684 def get_canonical_inventory_paths(self, paths):
685 """See Tree.get_canonical_inventory_paths."""
686 return [self.get_canonical_inventory_path(p) for p in paths]
687
688 def get_parent_ids(self):
689 """See Tree.get_parent_ids."""
690 return self._nested_trees.root_tree.get_parent_ids()
691
692 def get_file_sha1(self, file_id, path=None, stat_value=None):
693 """See Tree.get_file_sha1."""
694 tree, relpath = self._find_tree_path(file_id, path)
695 return tree.get_file_sha1(file_id, relpath, stat_value)
696
697 def get_file_mtime(self, file_id, path=None):
698 """See Tree.get_file_mtime."""
699 tree, relpath = self._find_tree_path(file_id, path)
700 return tree.get_file_mtime( file_id, relpath)
701
702 def get_symlink_target(self, file_id):
703 """See Tree.get_symlink_target."""
704 return self._nested_trees.get_tree(file_id).get_symlink_target(file_id)
705
706 def iter_children(self, file_id):
707 """See Tree.iter_children."""
708 tree = self._nested_trees.get_tree_and_treepath(file_id,
709 skip_references=True)[0]
710 return tree.iter_children(file_id)
711
712 def id2path(self, file_id):
713 """See Tree.id2path."""
714 tree, path = self._nested_trees.get_tree_and_treepath(file_id)
715 return osutils.pathjoin(path, tree.id2path(file_id))
716
717 def apply_inventory_delta(self, changes):
718 """Apply an inventory delta to a set of subtrees.
719
720 The delta is rewritten into a collection of deltas, one for each
721 affected subtree, then applied to each.
722 """
723 deltas = {}
724 for old_path, new_path, file_id, new_entry in changes:
725 if new_path is not None:
726 if new_entry.kind == 'tree-reference':
727 raise ValueError('Cannot introduce or change a'
728 ' tree-reference in'
729 ' CompositeTree.apply_inventory_delta.')
730 new_tree, new_tree_path = \
731 self._nested_trees.get_tree_and_treepath(
732 new_entry.parent_id, skip_references=True)
733 if new_tree_path != '':
734 new_path = new_path[len(new_tree_path) + 1:]
735 else:
736 new_tree_path = None
737 if old_path is not None:
738 old_tree, old_tree_path = \
739 self._nested_trees.get_tree_and_treepath(file_id)
740 if old_tree_path != '':
741 old_path = old_path[len(old_tree_path) + 1:]
742 old_tree_delta, old_tree = deltas.setdefault(old_tree_path,
743 ([], old_tree))
744 if old_tree_path == new_tree_path:
745 old_tree_delta.append((old_path, new_path, file_id,
746 new_entry))
747 else:
748 old_tree_delta.append((old_path, None, file_id, None))
749 else:
750 old_tree_path = None
751 if new_path is not None and new_tree_path != old_tree_path:
752 new_tree_delta, new_tree = deltas.setdefault(new_tree_path,
753 ([], new_tree))
754 new_tree_delta.append((None, new_path, file_id, new_entry))
755 for delta, tree in deltas.itervalues():
756 tree.apply_inventory_delta(delta)
757
758 @property
759 def case_sensitive(self):
760 """If True, the underlying filesystem is case sensitive."""
761 return self._nested_trees.root_tree.case_sensitive
0762
=== modified file 'bzrlib/export/__init__.py'
--- bzrlib/export/__init__.py 2009-03-23 14:59:43 +0000
+++ bzrlib/export/__init__.py 2009-08-31 04:38:31 +0000
@@ -146,20 +146,20 @@
146 else:146 else:
147 subdir_id = inv.path2id(subdir)147 subdir_id = inv.path2id(subdir)
148 entries = inv.iter_entries(subdir_id)148 entries = inv.iter_entries(subdir_id)
149 if subdir is None:149 for path, entry in entries:
150 entries.next() # skip root150 if path == '':
151 for entry in entries:151 continue
152 # The .bzr* namespace is reserved for "magic" files like152 # The .bzr* namespace is reserved for "magic" files like
153 # .bzrignore and .bzrrules - do not export these153 # .bzrignore and .bzrrules - do not export these
154 if entry[0].startswith(".bzr"):154 if path.startswith(".bzr"):
155 continue155 continue
156 if subdir is None:156 if subdir is None:
157 if not tree.has_filename(entry[0]):157 if not tree.has_filename(path):
158 continue158 continue
159 else:159 else:
160 if not tree.has_filename(os.path.join(subdir, entry[0])):160 if not tree.has_filename(os.path.join(subdir, path)):
161 continue161 continue
162 yield entry162 yield path, entry
163163
164164
165register_lazy_exporter(None, [], 'bzrlib.export.dir_exporter', 'dir_exporter')165register_lazy_exporter(None, [], 'bzrlib.export.dir_exporter', 'dir_exporter')
166166
=== modified file 'bzrlib/revisiontree.py'
--- bzrlib/revisiontree.py 2009-08-28 05:00:33 +0000
+++ bzrlib/revisiontree.py 2009-08-31 04:38:31 +0000
@@ -140,6 +140,9 @@
140 def get_reference_revision(self, file_id, path=None):140 def get_reference_revision(self, file_id, path=None):
141 return self.inventory[file_id].reference_revision141 return self.inventory[file_id].reference_revision
142142
143 def get_nested_tree(self, file_id, branch, path=None):
144 return self._get_nested_revision_tree(file_id, branch, path)
145
143 def get_root_id(self):146 def get_root_id(self):
144 if self.inventory.root:147 if self.inventory.root:
145 return self.inventory.root.file_id148 return self.inventory.root.file_id
146149
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2009-08-28 21:05:31 +0000
+++ bzrlib/tests/__init__.py 2009-08-31 04:38:32 +0000
@@ -3697,6 +3697,183 @@
3697 This function can be replaced if you need to change the default test3697 This function can be replaced if you need to change the default test
3698 suite on a global basis, but it is not encouraged.3698 suite on a global basis, but it is not encouraged.
3699 """3699 """
3700<<<<<<< TREE
3701=======
3702 testmod_names = [
3703 'bzrlib.doc',
3704 'bzrlib.tests.blackbox',
3705 'bzrlib.tests.branch_implementations',
3706 'bzrlib.tests.bzrdir_implementations',
3707 'bzrlib.tests.commands',
3708 'bzrlib.tests.interrepository_implementations',
3709 'bzrlib.tests.intertree_implementations',
3710 'bzrlib.tests.inventory_implementations',
3711 'bzrlib.tests.per_interbranch',
3712 'bzrlib.tests.per_lock',
3713 'bzrlib.tests.per_repository',
3714 'bzrlib.tests.per_repository_chk',
3715 'bzrlib.tests.per_repository_reference',
3716 'bzrlib.tests.test__chk_map',
3717 'bzrlib.tests.test__dirstate_helpers',
3718 'bzrlib.tests.test__groupcompress',
3719 'bzrlib.tests.test__walkdirs_win32',
3720 'bzrlib.tests.test_ancestry',
3721 'bzrlib.tests.test_annotate',
3722 'bzrlib.tests.test_api',
3723 'bzrlib.tests.test_atomicfile',
3724 'bzrlib.tests.test_bad_files',
3725 'bzrlib.tests.test_bisect_multi',
3726 'bzrlib.tests.test_branch',
3727 'bzrlib.tests.test_branchbuilder',
3728 'bzrlib.tests.test_btree_index',
3729 'bzrlib.tests.test_bugtracker',
3730 'bzrlib.tests.test_bundle',
3731 'bzrlib.tests.test_bzrdir',
3732 'bzrlib.tests.test__chunks_to_lines',
3733 'bzrlib.tests.test_cache_utf8',
3734 'bzrlib.tests.test_chk_map',
3735 'bzrlib.tests.test_chunk_writer',
3736 'bzrlib.tests.test_clean_tree',
3737 'bzrlib.tests.test_commands',
3738 'bzrlib.tests.test_commit',
3739 'bzrlib.tests.test_commit_merge',
3740 'bzrlib.tests.test_composite_tree',
3741 'bzrlib.tests.test_config',
3742 'bzrlib.tests.test_conflicts',
3743 'bzrlib.tests.test_counted_lock',
3744 'bzrlib.tests.test_decorators',
3745 'bzrlib.tests.test_delta',
3746 'bzrlib.tests.test_debug',
3747 'bzrlib.tests.test_deprecated_graph',
3748 'bzrlib.tests.test_diff',
3749 'bzrlib.tests.test_directory_service',
3750 'bzrlib.tests.test_dirstate',
3751 'bzrlib.tests.test_email_message',
3752 'bzrlib.tests.test_eol_filters',
3753 'bzrlib.tests.test_errors',
3754 'bzrlib.tests.test_export',
3755 'bzrlib.tests.test_extract',
3756 'bzrlib.tests.test_fetch',
3757 'bzrlib.tests.test_fifo_cache',
3758 'bzrlib.tests.test_filters',
3759 'bzrlib.tests.test_ftp_transport',
3760 'bzrlib.tests.test_foreign',
3761 'bzrlib.tests.test_generate_docs',
3762 'bzrlib.tests.test_generate_ids',
3763 'bzrlib.tests.test_globbing',
3764 'bzrlib.tests.test_gpg',
3765 'bzrlib.tests.test_graph',
3766 'bzrlib.tests.test_groupcompress',
3767 'bzrlib.tests.test_hashcache',
3768 'bzrlib.tests.test_help',
3769 'bzrlib.tests.test_hooks',
3770 'bzrlib.tests.test_http',
3771 'bzrlib.tests.test_http_implementations',
3772 'bzrlib.tests.test_http_response',
3773 'bzrlib.tests.test_https_ca_bundle',
3774 'bzrlib.tests.test_identitymap',
3775 'bzrlib.tests.test_ignores',
3776 'bzrlib.tests.test_index',
3777 'bzrlib.tests.test_info',
3778 'bzrlib.tests.test_inv',
3779 'bzrlib.tests.test_inventory_delta',
3780 'bzrlib.tests.test_knit',
3781 'bzrlib.tests.test_lazy_import',
3782 'bzrlib.tests.test_lazy_regex',
3783 'bzrlib.tests.test_lockable_files',
3784 'bzrlib.tests.test_lockdir',
3785 'bzrlib.tests.test_log',
3786 'bzrlib.tests.test_lru_cache',
3787 'bzrlib.tests.test_lsprof',
3788 'bzrlib.tests.test_mail_client',
3789 'bzrlib.tests.test_memorytree',
3790 'bzrlib.tests.test_merge',
3791 'bzrlib.tests.test_merge3',
3792 'bzrlib.tests.test_merge_core',
3793 'bzrlib.tests.test_merge_directive',
3794 'bzrlib.tests.test_missing',
3795 'bzrlib.tests.test_msgeditor',
3796 'bzrlib.tests.test_multiparent',
3797 'bzrlib.tests.test_mutabletree',
3798 'bzrlib.tests.test_nonascii',
3799 'bzrlib.tests.test_options',
3800 'bzrlib.tests.test_osutils',
3801 'bzrlib.tests.test_osutils_encodings',
3802 'bzrlib.tests.test_pack',
3803 'bzrlib.tests.test_pack_repository',
3804 'bzrlib.tests.test_patch',
3805 'bzrlib.tests.test_patches',
3806 'bzrlib.tests.test_permissions',
3807 'bzrlib.tests.test_plugins',
3808 'bzrlib.tests.test_progress',
3809 'bzrlib.tests.test_read_bundle',
3810 'bzrlib.tests.test_reconcile',
3811 'bzrlib.tests.test_reconfigure',
3812 'bzrlib.tests.test_registry',
3813 'bzrlib.tests.test_remote',
3814 'bzrlib.tests.test_rename_map',
3815 'bzrlib.tests.test_repository',
3816 'bzrlib.tests.test_revert',
3817 'bzrlib.tests.test_revision',
3818 'bzrlib.tests.test_revisionspec',
3819 'bzrlib.tests.test_revisiontree',
3820 'bzrlib.tests.test_rio',
3821 'bzrlib.tests.test_rules',
3822 'bzrlib.tests.test_sampler',
3823 'bzrlib.tests.test_selftest',
3824 'bzrlib.tests.test_serializer',
3825 'bzrlib.tests.test_setup',
3826 'bzrlib.tests.test_sftp_transport',
3827 'bzrlib.tests.test_shelf',
3828 'bzrlib.tests.test_shelf_ui',
3829 'bzrlib.tests.test_smart',
3830 'bzrlib.tests.test_smart_add',
3831 'bzrlib.tests.test_smart_request',
3832 'bzrlib.tests.test_smart_transport',
3833 'bzrlib.tests.test_smtp_connection',
3834 'bzrlib.tests.test_source',
3835 'bzrlib.tests.test_ssh_transport',
3836 'bzrlib.tests.test_status',
3837 'bzrlib.tests.test_store',
3838 'bzrlib.tests.test_strace',
3839 'bzrlib.tests.test_subsume',
3840 'bzrlib.tests.test_switch',
3841 'bzrlib.tests.test_symbol_versioning',
3842 'bzrlib.tests.test_tag',
3843 'bzrlib.tests.test_testament',
3844 'bzrlib.tests.test_textfile',
3845 'bzrlib.tests.test_textmerge',
3846 'bzrlib.tests.test_timestamp',
3847 'bzrlib.tests.test_trace',
3848 'bzrlib.tests.test_transactions',
3849 'bzrlib.tests.test_transform',
3850 'bzrlib.tests.test_transport',
3851 'bzrlib.tests.test_transport_implementations',
3852 'bzrlib.tests.test_transport_log',
3853 'bzrlib.tests.test_tree',
3854 'bzrlib.tests.test_treebuilder',
3855 'bzrlib.tests.test_tsort',
3856 'bzrlib.tests.test_tuned_gzip',
3857 'bzrlib.tests.test_ui',
3858 'bzrlib.tests.test_uncommit',
3859 'bzrlib.tests.test_upgrade',
3860 'bzrlib.tests.test_upgrade_stacked',
3861 'bzrlib.tests.test_urlutils',
3862 'bzrlib.tests.test_version',
3863 'bzrlib.tests.test_version_info',
3864 'bzrlib.tests.test_versionedfile',
3865 'bzrlib.tests.test_weave',
3866 'bzrlib.tests.test_whitebox',
3867 'bzrlib.tests.test_win32utils',
3868 'bzrlib.tests.test_workingtree',
3869 'bzrlib.tests.test_workingtree_4',
3870 'bzrlib.tests.test_wsgi',
3871 'bzrlib.tests.test_xml',
3872 'bzrlib.tests.tree_implementations',
3873 'bzrlib.tests.workingtree_implementations',
3874 'bzrlib.util.tests.test_bencode',
3875 ]
3876>>>>>>> MERGE-SOURCE
37003877
3701 loader = TestUtil.TestLoader()3878 loader = TestUtil.TestLoader()
37023879
37033880
=== modified file 'bzrlib/tests/per_workingtree/test_add_reference.py'
--- bzrlib/tests/per_workingtree/test_add_reference.py 2009-07-10 07:14:02 +0000
+++ bzrlib/tests/per_workingtree/test_add_reference.py 2009-08-31 04:38:32 +0000
@@ -63,7 +63,8 @@
63 basis = tree.basis_tree()63 basis = tree.basis_tree()
64 basis.lock_read()64 basis.lock_read()
65 try:65 try:
66 sub_tree = tree.get_nested_tree('sub-tree-root-id')66 sub_tree = tree.get_nested_tree('sub-tree-root-id',
67 tree.branch)
67 self.assertEqual(sub_tree.last_revision(),68 self.assertEqual(sub_tree.last_revision(),
68 tree.get_reference_revision('sub-tree-root-id'))69 tree.get_reference_revision('sub-tree-root-id'))
69 finally:70 finally:
@@ -108,8 +109,9 @@
108 tree, sub_tree = self.make_nested_trees()109 tree, sub_tree = self.make_nested_trees()
109 tree.lock_read()110 tree.lock_read()
110 try:111 try:
111 sub_tree2 = tree.get_nested_tree('sub-tree-root-id')112 sub_tree2 = tree.get_nested_tree('sub-tree-root-id', tree.branch)
112 self.assertEqual(sub_tree.basedir, sub_tree2.basedir)113 self.assertEqual(sub_tree.basedir, sub_tree2.basedir)
113 sub_tree2 = tree.get_nested_tree('sub-tree-root-id', 'sub-tree')114 sub_tree2 = tree.get_nested_tree('sub-tree-root-id', tree.branch,
115 'sub-tree')
114 finally:116 finally:
115 tree.unlock()117 tree.unlock()
116118
=== added file 'bzrlib/tests/test_composite_tree.py'
--- bzrlib/tests/test_composite_tree.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/test_composite_tree.py 2009-08-31 04:38:32 +0000
@@ -0,0 +1,362 @@
1# Copyright (C) 2007, 2009 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17
18import os
19
20from bzrlib import (
21 composite_tree,
22 conflicts,
23 errors,
24 inventory,
25 tests,
26 )
27from bzrlib.export import export
28
29
30class TestNestedTrees(tests.TestCaseWithTransport):
31
32 def make_branch_and_tree(self, path, format='pack-0.92-subtree'):
33 return tests.TestCaseWithTransport.make_branch_and_tree(
34 self, path, format)
35
36 def test_paths_info(self):
37 tree = self.make_branch_and_tree('.')
38 subtree = self.make_branch_and_tree('sub')
39 subsubtree = self.make_branch_and_tree('sub/sub')
40 subsubtree.set_root_id('sub-sub-root-id')
41 subtree.add_reference(subsubtree)
42 tree.add_reference(subtree)
43 nested = composite_tree.NestedTrees(tree, tree.branch)
44 nested.lock_read()
45 self.addCleanup(nested.unlock)
46 fullpath, relpath, found_entry, found_tree = \
47 nested.paths_info(['sub/sub'])[0]
48 self.assertEqual('sub-sub-root-id', found_entry.file_id)
49 self.assertEqual(subsubtree.basedir, found_tree.basedir)
50 self.assertEqual('', relpath)
51 self.assertEqual('sub/sub', fullpath)
52
53 # Now testing an unversioned, non-existent path
54 fullpath, relpath, found_entry, found_tree = \
55 nested.paths_info(['sub/subb'])[0]
56 self.assertIs(None, found_entry)
57 self.assertIsNot(None, found_tree)
58
59 def lock_read(self, item):
60 item.lock_read()
61 self.addCleanup(item.unlock)
62
63 def test_all_trees(self):
64 tree = self.make_branch_and_tree('tree')
65 subtree = self.make_branch_and_tree('tree/subtree')
66 tree.add_reference(subtree)
67 subsubtree = self.make_branch_and_tree('tree/subtree/subsubtree')
68 subtree.add_reference(subsubtree)
69 nested = composite_tree.NestedTrees(tree, tree.branch)
70 self.lock_read(nested)
71 result = nested.all_trees()
72 expected = {'': tree, 'subtree': subtree,
73 'subtree/subsubtree': subsubtree}
74 def bases(input):
75 return dict((y, x.basedir) for y, x in input.iteritems())
76 self.assertEqual(bases(expected), bases(result))
77
78
79class TestCompositeTree(tests.TestCaseWithTransport):
80
81 def make_branch_and_tree(self, path, format='pack-0.92-subtree'):
82 return tests.TestCaseWithTransport.make_branch_and_tree(
83 self, path, format)
84
85 def make_tree_files(self, subfile=False):
86 tree = self.make_branch_and_tree('.')
87 self.build_tree(['file'])
88 tree.set_root_id('tree-root')
89 tree.add(['file'], ['file-id'])
90 subtree = self.make_branch_and_tree('subtree')
91 subtree.set_root_id('subtree-id')
92 if subfile:
93 self.build_tree(['subtree/subfile'])
94 subtree.add('subfile', 'subfile-id')
95 tree.add_reference(subtree)
96 tree.lock_write()
97 self.addCleanup(tree.unlock)
98 return tree, subtree
99
100 def test_move_across_trees(self):
101 tree, subtree = self.make_tree_files()
102 composite = composite_tree.CompositeTree(tree, tree.branch)
103 pairs = composite.move(['file'], 'subtree')
104 subtree.lock_write()
105 self.addCleanup(subtree.unlock)
106 self.assertIs(None, tree.path2id('file'))
107 self.assertEqual('file-id', subtree.path2id('file'))
108 self.failUnlessExists('subtree/file')
109 self.failIfExists('file')
110 self.assertEqual([('file', 'subtree/file')], pairs)
111
112 def test_move_after(self):
113 tree, subtree = self.make_tree_files()
114 os.rename('file', 'subtree/file')
115 composite = composite_tree.CompositeTree(tree, tree.branch)
116 composite.move(['file'], 'subtree', after=True)
117 self.assertIs(None, tree.path2id('file'))
118 self.assertEqual('file-id', subtree.path2id('file'))
119
120 def test_move_needs_after(self):
121 tree, subtree = self.make_tree_files()
122 self.build_tree(['subtree/file'])
123 composite = composite_tree.CompositeTree(tree, tree.branch)
124 self.assertRaises(errors.RenameFailedFilesExist, composite.move,
125 ['file'], 'subtree')
126
127 def test_move_force_after(self):
128 tree, subtree = self.make_tree_files()
129 self.build_tree(['subtree/file'])
130 composite = composite_tree.CompositeTree(tree, tree.branch)
131 composite.move(['file'], 'subtree',
132 after=True)
133 self.assertIs(None, tree.path2id('file'))
134 self.assertEqual('file-id', subtree.path2id('file'))
135
136 def test_move_to_unversioned(self):
137 tree, subtree = self.make_tree_files()
138 tree.remove('subtree')
139 composite = composite_tree.CompositeTree(tree, tree.branch)
140 self.assertRaises(errors.BzrMoveFailedError, composite.move, ['file'],
141 'subtree', after=True)
142
143 def test_move_from_unversioned(self):
144 tree, subtree = self.make_tree_files()
145 tree.remove('file')
146 composite = composite_tree.CompositeTree(tree, tree.branch)
147 self.assertRaises(errors.BzrMoveFailedError, composite.move, ['file'],
148 'subtree', after=True)
149
150 def test_rename_into_subtree(self):
151 tree, subtree = self.make_tree_files()
152 composite = composite_tree.CompositeTree(tree, tree.branch)
153 composite.rename_one('file', 'subtree/file1')
154 self.failUnlessExists('subtree/file1')
155 self.assertEqual('file-id', subtree.path2id('file1'))
156
157 def test_iter_entries_by_dir(self):
158 tree, subtree = self.make_tree_files()
159 composite = self.get_locked_composite(tree)
160 for path, entry in composite.iter_entries_by_dir():
161 self.assertNotContainsRe(path, '/$')
162
163 def test_iter_entries_by_dir_follow_subtree(self):
164 """Ensure that iter_entries_by_dir follows subtrees.
165
166 This makes sure that even when the cached kind is directory, subtree
167 is followed.
168 """
169 tree = self.make_branch_and_tree('.')
170 self.build_tree(['subtree/', 'subtree/file'])
171 subtree = self.make_branch_and_tree('subtree')
172 self.build_tree(['subtree/file'])
173 subtree.add('file')
174 tree.add_reference(subtree)
175 composite = self.get_locked_composite(tree)
176 paths = [p for p, e in composite.iter_entries_by_dir()]
177 self.assertTrue('subtree' in paths)
178 self.assertTrue('subtree/file' in paths)
179
180 def test_iter_changes(self):
181 tree, subtree = self.make_tree_files()
182 new = self.get_locked_composite(tree)
183 old = self.get_locked_composite(tree.basis_tree(), tree.branch)
184 for file_id, paths, content, versioned, parent, name, kind, executable\
185 in new.iter_changes(old):
186 self.assertNotEqual('tree-reference', kind[1])
187
188 def test_iter_changes_accepts_specific_files_for_deletion(self):
189 """Test corner case requiring all_file_ids()"""
190 tree, subtree = self.make_tree_files()
191 old = self.get_locked_composite(tree)
192 new = self.get_locked_composite(tree.basis_tree(), tree.branch)
193 for file_id, paths, content, versioned, parent, name, kind, executable\
194 in new.iter_changes(old, specific_files=['subtree']):
195 self.assertNotEqual('tree-reference', kind[1])
196
197 def test_inventory(self):
198 tree, subtree = self.make_tree_files()
199 composite = self.get_locked_composite(tree)
200 self.assertRaises(errors.NoSuchId, lambda x: composite.inventory[x],
201 'not-present')
202 subtree_entry = composite.inventory['subtree-id']
203 self.assertEqual('tree-reference', subtree_entry.kind)
204
205 def test_comparison_data(self):
206 tree, subtree = self.make_tree_files()
207 composite = self.get_locked_composite(tree)
208 entry = composite.inventory['subtree-id']
209 kind, exe, stat = composite._comparison_data(entry, 'subtree')
210 self.assertEqual('directory', kind)
211
212 def test_apply_inventory_delta(self):
213 tree, subtree = self.make_tree_files()
214 composite = composite_tree.CompositeTree(tree, tree.branch)
215 # Move an existing file into a new subtree.
216 new_entry = tree.inventory['file-id'].copy()
217 new_entry.parent_id = 'subtree-id'
218 composite.lock_write()
219 try:
220 composite.apply_inventory_delta([('file', 'subtree/file',
221 'file-id', new_entry)])
222 finally:
223 composite.unlock()
224 self.assertEqual('file', subtree.id2path('file-id'))
225 self.assertEqual('file-id', subtree.path2id('file'))
226 self.assertEqual(None, tree.path2id('file'))
227
228 # Rename subtree/file to subtree/smile
229 new_entry = new_entry.copy()
230 new_entry.name = 'smile'
231 composite.lock_write()
232 try:
233 composite.apply_inventory_delta([('subtree/file', 'subtree/smile',
234 'file-id', new_entry)])
235 finally:
236 composite.unlock()
237 self.assertEqual('file-id', subtree.path2id('smile'))
238
239 # Delete subtree/smile
240 composite.lock_write()
241 try:
242 composite.apply_inventory_delta([('subtree/smile', None, 'file-id',
243 None)])
244 finally:
245 composite.unlock()
246 self.assertEqual(None, subtree.path2id('smile'))
247
248 # Create subtree/smile
249 composite.lock_write()
250 try:
251 composite.apply_inventory_delta([(None, 'subtree/smile', 'file-id',
252 new_entry)])
253 finally:
254 composite.unlock()
255 self.assertEqual('file-id', subtree.path2id('smile'))
256
257 # Move subtree/smile back into root tree
258 new_entry = new_entry.copy()
259 new_entry.parent_id = tree.get_root_id()
260 composite.lock_write()
261 try:
262 composite.apply_inventory_delta([('subtree/smile', 'smile',
263 'file-id', new_entry)])
264 finally:
265 composite.unlock()
266 self.assertEqual(None, subtree.path2id('smile'))
267 self.assertEqual('file-id', tree.path2id('smile'))
268
269 def test_apply_inventory_delta_tree_reference(self):
270 tree = self.make_branch_and_tree('.')
271 tree.set_root_id('root-id')
272 composite = composite_tree.CompositeTree(tree, tree.branch)
273 composite.lock_write()
274 self.addCleanup(composite.unlock)
275 foo = inventory.TreeReference(
276 'foo-id', 'foo', 'root-id', reference_revision='foo-rev-id')
277 e = self.assertRaises(ValueError, composite.apply_inventory_delta,
278 [(None, 'foo', 'foo-id', foo)])
279 self.assertEqual('Cannot introduce or change a tree-reference in'
280 ' CompositeTree.apply_inventory_delta.', str(e))
281
282 def get_locked_composite(self, tree, branch=None):
283 if branch is None:
284 branch = tree.branch
285 composite = composite_tree.CompositeTree(tree, branch)
286 composite.lock_read()
287 self.addCleanup(composite.unlock)
288 return composite
289
290 def test_all_file_ids(self):
291 tree, subtree = self.make_tree_files(subfile=True)
292 composite = self.get_locked_composite(tree)
293 self.assertEqual(set(['tree-root', 'file-id', 'subtree-id',
294 'subfile-id']), composite.all_file_ids())
295
296 def test_paths2ids(self):
297 tree, subtree = self.make_tree_files()
298 composite = self.get_locked_composite(tree)
299 self.assertEqual(set(['file-id', 'subtree-id']),
300 composite.paths2ids(['file', 'subtree']))
301
302 def test_iter_children(self):
303 tree, subtree = self.make_tree_files(subfile=True)
304 composite = self.get_locked_composite(tree)
305 self.assertEqual(set(['subfile-id']),
306 set(composite.iter_children('subtree-id')))
307 self.assertEqual(set(['file-id', 'subtree-id']),
308 set(composite.iter_children('tree-root')))
309
310 def test_case_sensitive(self):
311 tree, subtree = self.make_tree_files()
312 composite = self.get_locked_composite(tree)
313 self.assertEqual(tree.case_sensitive, composite.case_sensitive)
314 tree.case_sensitive = not tree.case_sensitive
315 self.assertEqual(tree.case_sensitive, composite.case_sensitive)
316
317 def make_ci_tree(self):
318 tree, subtree = self.make_tree_files()
319 self.build_tree(['subtree/subFile'])
320 subtree.add('subFile')
321 return self.get_locked_composite(tree)
322
323 def test_get_canonical_inventory_path(self):
324 """Finds subtree paths."""
325 composite = self.make_ci_tree()
326 self.assertEqual('subtree/subFile',
327 composite.get_canonical_inventory_path(
328 'SUBTREE/SUBFILE'))
329
330 def test_get_canonical_inventory_path_preserves_missing(self):
331 """Returns unversion path segments verbatim."""
332 composite = self.make_ci_tree()
333 self.assertEqual('subtree/subFile/X',
334 composite.get_canonical_inventory_path(
335 'SUBTREE/SUBFILE/X'))
336
337 def test_get_canonical_inventory_paths(self):
338 """Finds subtree paths."""
339 composite = self.make_ci_tree()
340 self.assertEqual(['subtree/subFile'],
341 composite.get_canonical_inventory_paths(
342 ['SUBTREE/SUBFILE']))
343
344 def test_get_file_kind(self):
345 composite = self.get_locked_composite(self.make_tree_files()[0])
346 inv = composite.inventory
347 self.assertEqual('file', inv.get_file_kind('file-id'))
348 self.assertEqual('directory', inv.get_file_kind('subtree-id'))
349
350 def test_export(self):
351 tree, subtree = self.make_tree_files(subfile=True)
352 composite = self.get_locked_composite(tree)
353 export(composite, 'output', None, None, 'subtree')
354 self.assertEqual(['subfile'], os.listdir('output'))
355
356 def test_conflicts_path(self):
357 tree, subtree = self.make_tree_files()
358 subtree.add_conflicts([conflicts.ContentsConflict('foo')])
359 composite = self.get_locked_composite(tree)
360 [conflict] = composite.conflicts()
361 self.expectFailure('Conflict reporting for composite trees is wonky.',
362 self.assertEqual, 'subtree/foo', conflict.path)
0363
=== modified file 'bzrlib/transform.py'
--- bzrlib/transform.py 2009-08-28 05:00:33 +0000
+++ bzrlib/transform.py 2009-08-31 04:38:31 +0000
@@ -1315,6 +1315,7 @@
1315 tree.case_sensitive)1315 tree.case_sensitive)
1316 self._deletiondir = deletiondir1316 self._deletiondir = deletiondir
13171317
1318<<<<<<< TREE
1318 def canonical_path(self, path):1319 def canonical_path(self, path):
1319 """Get the canonical tree-relative path"""1320 """Get the canonical tree-relative path"""
1320 # don't follow final symlinks1321 # don't follow final symlinks
@@ -1397,6 +1398,10 @@
1397 yield self.trans_id_tree_path(childpath)1398 yield self.trans_id_tree_path(childpath)
13981399
1399 def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):1400 def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1401=======
1402 def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None,
1403 skip_inventory=False):
1404>>>>>>> MERGE-SOURCE
1400 """Apply all changes to the inventory and filesystem.1405 """Apply all changes to the inventory and filesystem.
14011406
1402 If filesystem or inventory conflicts are present, MalformedTransform1407 If filesystem or inventory conflicts are present, MalformedTransform
@@ -1437,7 +1442,8 @@
1437 mover.apply_deletions()1442 mover.apply_deletions()
1438 finally:1443 finally:
1439 child_pb.finished()1444 child_pb.finished()
1440 self._tree.apply_inventory_delta(inventory_delta)1445 if not skip_inventory:
1446 self._tree.apply_inventory_delta(inventory_delta)
1441 self._done = True1447 self._done = True
1442 self.finalize()1448 self.finalize()
1443 return _TransformResults(modified_paths, self.rename_count)1449 return _TransformResults(modified_paths, self.rename_count)
14441450
=== modified file 'bzrlib/workingtree_4.py'
--- bzrlib/workingtree_4.py 2009-08-25 04:43:21 +0000
+++ bzrlib/workingtree_4.py 2009-08-31 04:38:31 +0000
@@ -414,9 +414,9 @@
414414
415 def get_reference_revision(self, file_id, path=None):415 def get_reference_revision(self, file_id, path=None):
416 # referenced tree's revision is whatever's currently there416 # referenced tree's revision is whatever's currently there
417 return self.get_nested_tree(file_id, path).last_revision()417 return self.get_nested_tree(file_id, self.branch, path).last_revision()
418418
419 def get_nested_tree(self, file_id, path=None):419 def get_nested_tree(self, file_id, branch, path=None):
420 if path is None:420 if path is None:
421 path = self.id2path(file_id)421 path = self.id2path(file_id)
422 # else: check file_id is at path?422 # else: check file_id is at path?
@@ -1806,6 +1806,9 @@
1806 target = target.decode('utf8')1806 target = target.decode('utf8')
1807 return target1807 return target
18081808
1809 def get_nested_tree(self, file_id, branch, path=None):
1810 return self._get_nested_revision_tree(file_id, branch, path)
1811
1809 def get_revision_id(self):1812 def get_revision_id(self):
1810 """Return the revision id for this tree."""1813 """Return the revision id for this tree."""
1811 return self._revision_id1814 return self._revision_id
18121815
=== added file 'doc/developers/nested-trees.txt'
--- doc/developers/nested-trees.txt 1970-01-01 00:00:00 +0000
+++ doc/developers/nested-trees.txt 2009-08-31 04:38:32 +0000
@@ -0,0 +1,28 @@
1Nested Trees Implementation
2###########################
3
4CompositeTree
5-------------
6CompositeTree is a shim to enable subtree support for certain commands.
7
8It is constructed from a Tree (typically a RevisionTree or WorkingTree) and a
9Branch. It provides an alternate view of the Tree and its subtrees, in which
10the subtrees and their contents appear to be part of the root tree. The roots
11of subtrees are presented as directories, not as tree references.
12
13It can be used with export, diff and status. It should be tested with every
14command that uses it, because it is not guaranteed to be a complete
15implementation of the Tree API. It is not suitable as input to operations that
16must know about the locations of subtrees, such as revert.
17
18
19NestedTrees
20-----------
21NestedTrees is an API providing access to a set of nested trees. It is able to
22convert paths and file-ids into references to specific Trees. It provides
23caching, to avoid retrieving the same Tree multiple times. It provides
24locking, to ensure trees are locked and unlocked at appropriate times.
25
26It is used by CompositeTree, and is a recommended API for all other operations.
27It is expected to acquire more capabilities as the needs of sets of nested
28trees become clearer.