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
1=== added file 'bzrlib/composite_tree.py'
2--- bzrlib/composite_tree.py 1970-01-01 00:00:00 +0000
3+++ bzrlib/composite_tree.py 2009-08-31 04:38:31 +0000
4@@ -0,0 +1,761 @@
5+# Copyright (C) 2007, 2008, 2009 Canonical Ltd
6+#
7+# This program is free software; you can redistribute it and/or modify
8+# it under the terms of the GNU General Public License as published by
9+# the Free Software Foundation; either version 2 of the License, or
10+# (at your option) any later version.
11+#
12+# This program is distributed in the hope that it will be useful,
13+# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+# GNU General Public License for more details.
16+#
17+# You should have received a copy of the GNU General Public License
18+# along with this program; if not, write to the Free Software
19+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20+
21+"""Tree-like class for manipulating nested trees to be like a single tree"""
22+
23+from bzrlib import (
24+ conflicts as _mod_conflicts,
25+ errors,
26+ osutils,
27+ transform,
28+ tree,
29+ )
30+
31+class NestedTrees(object):
32+ """Provide data about a collection of nested trees.
33+
34+ This is a sparse interface that has high fidelity to its underlying
35+ implementation. It may be used directly, or as the internal state of
36+ CompositeTree/_CompositeInventory.
37+ """
38+
39+ def __init__(self, root_tree, root_branch):
40+ self.root_tree = root_tree
41+ self.root_branch = root_branch
42+ self.lock_count = 0
43+ self.lock_type = None
44+ self._clear_cached_trees()
45+
46+ def _clear_cached_trees(self):
47+ if self.lock_type is not None:
48+ raise AssertionError('Cannot clear cached trees when locked.')
49+ self._subtrees_by_relpath = {}
50+ self._all_trees_scanned = False
51+ self._locked_trees = []
52+
53+ def lock_read(self):
54+ """Read-lock all open trees, open new trees read-locked."""
55+ if self.lock_count == 0:
56+ self.root_tree.lock_read()
57+ self.lock_type = 'read'
58+ self._locked_trees.append(self.root_tree)
59+ self.lock_count += 1
60+
61+ def lock_write(self):
62+ """Write-lock all open trees, open new trees write-locked."""
63+ if self.lock_count == 0:
64+ self.root_tree.lock_write()
65+ self.lock_type = 'write'
66+ self._locked_trees.append(self.root_tree)
67+ self.lock_count += 1
68+
69+ def lock_tree_write(self):
70+ """Write-lock all open trees, open new trees write-locked.
71+
72+ Branches are read-locked, instead of being write-locked as usual.
73+ """
74+ if self.lock_count == 0:
75+ self.root_tree.lock_tree_write()
76+ self.lock_type = 'tree_write'
77+ self._locked_trees.append(self.root_tree)
78+ self.lock_count += 1
79+
80+ def unlock(self):
81+ """Unlock open trees."""
82+ self.lock_count -= 1
83+ if self.lock_count < 0:
84+ raise AssertionError('lock count is negative.')
85+ if self.lock_count == 0:
86+ for tree in reversed(self._locked_trees):
87+ tree.unlock()
88+ self.lock_type = None
89+ self._clear_cached_trees()
90+
91+ def _must_be_locked(self):
92+ if self.lock_type is None:
93+ raise errors.ObjectNotLocked(self)
94+
95+ def paths_info(self, paths):
96+ """Return path, relpath, inventory entry, and tree for input paths.
97+
98+ The path is the path to the subtree relative to the root tree. The
99+ relpath is the path within the subtree. The inventory entry will be
100+ None if the path is not versioned. The tree is the subtree
101+ containing the relpath.
102+
103+ :param paths: A list of paths relative to the root tree.
104+ :return: a list of (path, relpath, entry, tree) tuples.
105+ """
106+ self._must_be_locked()
107+ return [self._path_info(p) for p in paths]
108+
109+ def _path_info(self, path):
110+ """Find the relpath, tree and inventory entry for a path.
111+
112+ The path is the path to the subtree relative to the root tree. The
113+ relpath is the path within the subtree. The inventory will be
114+ None if the path is not versioned. The tree is the subtree
115+ containing the relpath.
116+
117+ :param path: A path relative to the root tree.
118+ :return: a (path, relpath, entry, tree) tuple.
119+ """
120+ parts = osutils.splitpath(path)
121+ tree = self.root_tree
122+ branch = self.root_branch
123+ node = tree.inventory.root
124+ relpath = []
125+ for part in parts:
126+ relpath.append(part)
127+ children = getattr(node, 'children', {})
128+ node = children.get(part)
129+ if node is None:
130+ return path, osutils.pathjoin(*relpath), None, tree
131+ try:
132+ kind = tree.kind(node.file_id)
133+ except errors.NoSuchFile:
134+ pass
135+ else:
136+ if kind == 'tree-reference':
137+ tree, branch = self.get_tree_branch(branch, tree,
138+ node.file_id, osutils.pathjoin(*relpath))
139+ node = tree.inventory.root
140+ relpath = []
141+ continue
142+ if relpath == []:
143+ relpath_str = ''
144+ else:
145+ relpath_str = osutils.pathjoin(*relpath)
146+ return path, relpath_str, node, tree
147+
148+ def get_tree(self, file_id):
149+ """Return the Tree that contains a given file-id."""
150+ return self.get_tree_and_treepath(file_id)[0]
151+
152+ def get_tree_and_treepath(self, file_id, skip_references=False):
153+ """Get a tree containing file_id and path relative to the root tree.
154+
155+ The path is the path to the subtree, not the file in the subtree.
156+
157+ :skip_references: if True, only results where the file_id is not
158+ a tree reference will be supplied.
159+ """
160+ for path, tree in self.all_trees().iteritems():
161+ if file_id in tree.inventory:
162+ if skip_references:
163+ try:
164+ if tree.kind(file_id) == 'tree-reference':
165+ continue
166+ except errors.NoSuchFile:
167+ pass
168+ return tree, path
169+ else:
170+ raise errors.NoSuchId(self, file_id)
171+
172+ def get_path_info(self, path):
173+ """Determine the tree containing a path and the tree's pathname.
174+
175+ :return: a tuple of (tree, tree_path, relpath), where tree_path
176+ is the path to the tree, relative to the root_tree, and relpath
177+ is the path relative to the returned tree.
178+ """
179+ trees = self.all_trees()
180+ prefix = path
181+ while True:
182+ prefix = osutils.dirname(prefix)
183+ subpath = path[len(prefix):]
184+ subpath = subpath.lstrip('/')
185+ try:
186+ return trees[prefix], prefix, subpath
187+ except KeyError:
188+ pass
189+ if subpath == '':
190+ raise AssertionError('Subpath cannot be empty.')
191+
192+ def iter_pathinfo_by_tree(self, paths):
193+ """Iterate through the path information for provided paths.
194+
195+ Iterates through (tree, tree_path, relpaths) tuples, where
196+ tree is a tree containing some of the paths, tree_path is the relative
197+ path to the tree from the root_tree, and relpaths are the paths within
198+ the returned tree.
199+ """
200+ tree_path = None
201+ tree = None
202+ # Reverse sort should guarantee that all subtree paths are processed
203+ # before tree paths. So if path.startswith(tree_path), we're in
204+ # the same tree.
205+ for path in sorted(paths, reverse=True):
206+ if tree_path is None or not path.startswith(tree_path):
207+ if tree is not None:
208+ yield tree, tree_path, subpaths
209+ tree, tree_path, subpath = self.get_path_info(path)
210+ subpaths = [subpath]
211+ else:
212+ subpaths.append(path[len(tree_path):].lstrip('/'))
213+ if tree is not None:
214+ yield tree, tree_path, subpaths
215+
216+ def _scan_subtrees(self):
217+ self._must_be_locked()
218+ def do_scan(prefix, relpath, tree, containing_tree, branch):
219+ self._register_tree_relpath(tree, containing_tree, relpath)
220+ for path, file_id in tree.iter_references():
221+ tree_path = tree.id2path(file_id)
222+ composite_path = osutils.pathjoin(prefix, tree_path)
223+ subtree, subbranch = self.get_tree_branch(branch, tree,
224+ file_id, tree_path)
225+ do_scan(composite_path, tree_path, subtree, tree, subbranch)
226+ do_scan('', '', self.root_tree, None, self.root_branch)
227+
228+ def _register_tree_relpath(self, tree, containing_tree, relpath):
229+ """Register the relpath of a Tree in the relevant dicts."""
230+ tree_relpaths = self._subtrees_by_relpath.get(containing_tree, {})
231+ tree_relpaths[relpath] = tree
232+ self._subtrees_by_relpath[containing_tree] = tree_relpaths
233+
234+ def get_tree_branch(self, branch, tree, file_id, path):
235+ """Retrieve the tree and branch for a subtree path
236+
237+ :param branch: The branch associated with the containing tree
238+ :param tree: The containing tree
239+ :param file_id: The file id of the subtree
240+ :param path: The tree-relative path to the subtree
241+ """
242+ subbranch = branch.reference_parent(file_id, path)
243+ try:
244+ subtree = self._subtrees_by_relpath[tree][path]
245+ except KeyError:
246+ # Unknown tree, so we must try and retrieve it.
247+ subtree = tree.get_nested_tree(file_id, branch)
248+ # Tree must be locked to match the other components of this
249+ # NestedTree so that they all behave the same.
250+ if self.lock_type is not None:
251+ if self.lock_type == 'read':
252+ subtree.lock_read()
253+ elif self.lock_type == 'write':
254+ subtree.lock_write()
255+ elif self.lock_type == 'tree_write':
256+ subtree.lock_tree_write()
257+ self._locked_trees.append(subtree)
258+ self._register_tree_relpath(subtree, tree, path)
259+ return subtree, subbranch
260+
261+ def all_trees(self):
262+ """Return a dict of composite_path to tree for all subtrees."""
263+ if not self._all_trees_scanned:
264+ self._scan_subtrees()
265+ self._all_trees_scanned = True
266+ root_tree_paths = {}
267+ def do_tree(tree, root_tree_path):
268+ root_tree_paths[root_tree_path] = tree
269+ subtrees = self._subtrees_by_relpath.get(tree, {})
270+ for relpath, subtree in subtrees.iteritems():
271+ do_tree(subtree, osutils.pathjoin(root_tree_path, relpath))
272+ do_tree(self.root_tree, '')
273+ return root_tree_paths
274+
275+
276+class _CompositeInventory(object):
277+ """An inventory that combines the inventories of several subtrees.
278+
279+ Intended for use as CompositeTree.inventory, not for direct instantiation.
280+ """
281+
282+ def __init__(self, nested_trees):
283+ self._nested_trees = nested_trees
284+
285+ def _get_ci_path_info(self, path):
286+ file_id = None
287+ tree = self._nested_trees.root_tree
288+ node = tree.inventory.root
289+ canonical_path = []
290+ remaining_path = osutils.splitpath(path)
291+ while len(remaining_path) > 0:
292+ part = remaining_path[0]
293+ next_node = None
294+ children = getattr(node, 'children', {})
295+ for child in children.itervalues():
296+ if child.name.lower() == part.lower():
297+ next_node = child
298+ if child.name == part:
299+ break
300+ if next_node is None:
301+ break
302+ remaining_path.pop(0)
303+ canonical_path.append(next_node.name)
304+ node = next_node
305+ if node.kind == 'tree-reference':
306+ tree, path_ = self._nested_trees.get_tree_and_treepath(
307+ node.file_id, skip_references=True)
308+ node = tree.inventory.root
309+ if node is not None:
310+ file_id = node.file_id
311+ canonical_path.extend(remaining_path)
312+ if len(canonical_path) == 0:
313+ result = ''
314+ else:
315+ result = osutils.pathjoin(*canonical_path)
316+ return result, file_id
317+
318+ def path2id(self, path):
319+ """See Inventory.path2id."""
320+ path, relpath, entry, tree = self._nested_trees.paths_info([path])[0]
321+ if entry is None:
322+ return None
323+ return entry.file_id
324+
325+ def id2path(self, file_id):
326+ """See Inventory.id2path."""
327+ tree, path = self._nested_trees.get_tree_and_treepath(file_id)
328+ return osutils.pathjoin(*[path, tree.id2path(file_id)])
329+
330+ def get_file_kind(self, file_id):
331+ """See Inventory.get_file_kind."""
332+ tree = self._nested_trees.get_tree_and_treepath(file_id,
333+ skip_references=True)[0]
334+ return tree.inventory.get_file_kind(file_id)
335+
336+ def iter_entries_by_dir(self, specific_file_ids=None, _tree=None,
337+ _branch=None, _root=''):
338+ """See Inventory.iter_entries_by_dir."""
339+ tree = _tree
340+ root = _root
341+ branch = _branch
342+ if tree is None:
343+ tree = self._nested_trees.root_tree
344+ if branch is None:
345+ branch = self._nested_trees.root_branch
346+ for subpath, entry in tree.iter_entries_by_dir():
347+ if subpath == '':
348+ path = root
349+ else:
350+ path = osutils.pathjoin(root, subpath)
351+ try:
352+ kind = tree.kind(entry.file_id)
353+ except errors.NoSuchFile:
354+ kind = None
355+ if kind == 'tree-reference':
356+ subtree, subbranch = self._nested_trees.get_tree_branch(
357+ branch, tree, entry.file_id, path)
358+ for path, entry in self.iter_entries_by_dir(
359+ specific_file_ids, _tree=subtree,
360+ _branch=subbranch, _root=path):
361+ yield path, entry
362+ else:
363+ if (specific_file_ids is None or entry.file_id in
364+ specific_file_ids):
365+ yield path, entry
366+
367+ def iter_entries(self, from_dir=None):
368+ """See Tree.iter_entries.
369+
370+ Note: violates the expected output order, but this is not generally
371+ a problem.
372+ """
373+ if from_dir is not None:
374+ from_dir_path = self.id2path(from_dir)
375+ for path, entry in self.iter_entries_by_dir():
376+ if from_dir is not None:
377+ if not path.startswith(from_dir_path):
378+ continue
379+ path = path[len(from_dir_path):].lstrip('/')
380+ yield path, entry
381+
382+ def __getitem__(self, file_id):
383+ tree = self._nested_trees.get_tree(file_id)
384+ return tree.inventory[file_id]
385+
386+ def __contains__(self, file_id):
387+ try:
388+ self._nested_trees.get_tree(file_id)
389+ except errors.NoSuchId:
390+ return False
391+ else:
392+ return True
393+
394+
395+class CompositeTree(object):
396+ """Implements parts of Tree interface in terms of subtrees.
397+
398+ Essentially, this is a by-value representation of a set of nested
399+ subtrees.
400+ Care should be taken in its use-- not suitable for every problem.
401+ """
402+
403+ def __init__(self, root_tree, root_branch):
404+ self._nested_trees = NestedTrees(root_tree, root_branch)
405+ self.inventory = _CompositeInventory(self._nested_trees)
406+
407+ @property
408+ def basedir(self):
409+ """See WorkingTree.basedir."""
410+ return self._nested_trees.root_tree.basedir
411+
412+ def move(self, from_paths, to_dir, after=False, _to_names=None):
413+ """Rename files, possibly across trees"""
414+ self.lock_tree_write()
415+ try:
416+ paths_info = self._nested_trees.paths_info(from_paths+[to_dir])
417+ to_path, to_relpath, new_parent, to_tree = paths_info[-1]
418+ if _to_names is not None and paths_info[0][2] is None:
419+ raise errors.BzrRenameFailedError(from_paths[0],
420+ osutils.pathjoin(to_dir, _to_names[0]),
421+ errors.NotVersionedError(path=from_paths[0]))
422+ if new_parent is None:
423+ if _to_names is None:
424+ from_ = ''
425+ to = to_dir
426+ else:
427+ from_ = from_paths[0]
428+ to = osutils.pathjoin(to_dir, _to_names[0])
429+ raise errors.BzrMoveFailedError(from_, to,
430+ errors.NotVersionedError(path=to_dir))
431+ pending_trees = {}
432+ result = []
433+ for path, relpath, entry, tree, in paths_info:
434+ if entry is None:
435+ raise errors.BzrMoveFailedError(path, '',
436+ errors.NotVersionedError(path=str(path)))
437+ if tree.basedir not in pending_trees:
438+ pending_trees[tree.basedir] = tree
439+ pairs = []
440+ changes = []
441+
442+ for num, (from_path, from_relpath, entry, from_tree) in\
443+ enumerate(paths_info[:-1]):
444+ full_to_path = osutils.pathjoin(to_path, entry.name)
445+ pairs.append((from_path, full_to_path))
446+ new_entry = entry.copy()
447+ new_entry.parent_id = new_parent.file_id
448+ if _to_names is not None:
449+ new_entry.name = _to_names[num]
450+ changes.append((from_path, full_to_path, new_entry.file_id,
451+ new_entry))
452+ if not after:
453+ self._move_files(to_path, to_tree, _to_names, paths_info[:-1])
454+ self.apply_inventory_delta(changes)
455+ return pairs
456+ finally:
457+ self.unlock()
458+
459+ def _move_files(self, to_path, to_tree, to_names, paths_info):
460+ trans_id_tree_path = {}
461+ tt = transform.TreeTransform(self._nested_trees.root_tree)
462+ parent_trans_id = tt.trans_id_tree_path(to_path)
463+ for num, (from_path, from_relpath, entry, from_tree) in enumerate(
464+ paths_info):
465+ # If the source doesn't exist and the target does,
466+ # don't touch the files but just update the inventory.
467+ to_exists = to_tree.has_filename(osutils.pathjoin(to_path,
468+ entry.name))
469+ from_exists = from_tree.has_filename(from_relpath)
470+ if not from_exists and to_exists:
471+ continue
472+
473+ file_trans_id = tt.trans_id_tree_path(from_path)
474+ trans_id_tree_path[file_trans_id] = from_path
475+ if to_names is not None:
476+ to_name = to_names[num]
477+ else:
478+ to_name = entry.name
479+ tt.adjust_path(to_name, parent_trans_id, file_trans_id)
480+ try:
481+ try:
482+ tt.apply(skip_inventory=True)
483+ except errors.MalformedTransform, e:
484+ if e.conflicts[0][0] == 'duplicate':
485+ fp = transform.FinalPaths(tt)
486+ source = trans_id_tree_path[e.conflicts[0][1]]
487+ dest = fp.get_path(e.conflicts[0][1])
488+ raise errors.RenameFailedFilesExist(source, dest)
489+ else:
490+ raise
491+ finally:
492+ tt.finalize()
493+
494+ def rename_one(self, from_rel, to_rel, after=False):
495+ """See WorkingTree.rename_one."""
496+ # We could be moving a file over subtree boundaries, figure out which
497+ # trees are used.
498+ self.lock_tree_write()
499+ try:
500+ from_tree, from_prefix, from_relpath = (
501+ self._nested_trees.get_path_info(from_rel))
502+ to_tree, to_prefix, to_relpath = (
503+ self._nested_trees.get_path_info(to_rel))
504+ if from_tree == to_tree:
505+ self._nested_trees.root_tree.rename_one(from_rel, to_rel,
506+ after=after)
507+ else:
508+ file_id = self.path2id(from_rel)
509+ osutils.rename(osutils.pathjoin(from_prefix, from_relpath)
510+ , osutils.pathjoin(to_prefix, to_relpath))
511+ to_tree.add([to_relpath], ids=[file_id])
512+ from_tree.remove([from_relpath])
513+ finally:
514+ self.unlock()
515+
516+ def get_file(self, file_id):
517+ """See Tree.get_file."""
518+ return self._nested_trees.get_tree(file_id).get_file(file_id)
519+
520+ def get_file_text(self, file_id, path=None):
521+ """See Tree.get_file_text."""
522+ tree, relpath = self._find_tree_path(file_id, path)
523+ return tree.get_file_text(file_id, relpath)
524+
525+ def get_file_size(self, file_id):
526+ """See Tree.get_file_size."""
527+ return self._nested_trees.get_tree(file_id).get_file_size(file_id)
528+
529+ def _find_tree_path(self, file_id, path):
530+ if path is not None:
531+ path, relpath, entry, tree =\
532+ self._nested_trees.paths_info([path])[0]
533+ else:
534+ tree = self._nested_trees.get_tree(file_id)
535+ relpath = None
536+ return tree, relpath
537+
538+ def is_executable(self, file_id, path=None):
539+ """See RevisionTree.is_executable."""
540+ tree, path = self._find_tree_path(file_id, path)
541+ return tree.is_executable(file_id, path)
542+
543+ def has_id(self, file_id):
544+ """See Tree.has_id."""
545+ try:
546+ tree = self._nested_trees.get_tree(file_id)
547+ except errors.NoSuchId:
548+ return False
549+ if tree is None:
550+ return False
551+ return tree.has_id(file_id)
552+
553+ def changes_from(self, other, want_unchanged=False, specific_files=None,
554+ extra_trees=None, require_versioned=False, include_root=False,
555+ want_unversioned=False):
556+ """Return a TreeDelta of the changes from other to this tree.
557+
558+ :param other: A tree to compare with.
559+ :param specific_files: An optional list of file paths to restrict the
560+ comparison to. When mapping filenames to ids, all matches in all
561+ trees (including optional extra_trees) are used, and all children
562+ of matched directories are included.
563+ :param want_unchanged: An optional boolean requesting the inclusion
564+ of unchanged entries in the result.
565+ :param extra_trees: An optional list of additional trees to use when
566+ mapping the contents of specific_files (paths) to file_ids.
567+ :param require_versioned: An optional boolean (defaults to False).
568+ When supplied and True all the 'specific_files' must be
569+ versioned, or a PathsNotVersionedError will be thrown.
570+ :param want_unversioned: Scan for unversioned paths.
571+
572+ The comparison will be performed by an InterTree object looked up on
573+ self and other.
574+ """
575+ # Martin observes that Tree.changes_from returns a TreeDelta and this
576+ # may confuse people, because the class name of the returned object
577+ # is a synonym of the object referenced in the method name.
578+ return tree.InterTree.get(other, self).compare(
579+ want_unchanged=want_unchanged,
580+ specific_files=specific_files,
581+ extra_trees=extra_trees,
582+ require_versioned=require_versioned,
583+ include_root=include_root,
584+ want_unversioned=want_unversioned,
585+ )
586+
587+ def paths2ids(self, paths, trees=[], require_versioned=True):
588+ """See Tree.paths2ids."""
589+ return tree.find_ids_across_trees(paths, [self] + list(trees),
590+ require_versioned)
591+
592+ def path2id(self, path):
593+ """See Tree.path2id."""
594+ # FIXME -- shouldn't return missing files
595+ return self.inventory.path2id(path)
596+
597+ def iter_changes(self, from_tree, include_unchanged=False,
598+ specific_files=None, pb=None, extra_trees=None,
599+ require_versioned=True, want_unversioned=False):
600+ """See InterTree.iter_changes."""
601+ intertree = tree.InterTree.get(from_tree, self)
602+ return intertree.iter_changes(include_unchanged, specific_files, pb,
603+ extra_trees, require_versioned,
604+ want_unversioned=want_unversioned)
605+
606+ def lock_read(self):
607+ """See Tree.lock_read."""
608+ self._nested_trees.lock_read()
609+
610+ def lock_write(self):
611+ """See MutableTree.lock_write."""
612+ self._nested_trees.lock_write()
613+
614+ def lock_tree_write(self):
615+ """See MutableTree.lock_tree_write."""
616+ self._nested_trees.lock_tree_write()
617+
618+ def unlock(self):
619+ """See Tree.unlock."""
620+ self._nested_trees.unlock()
621+
622+ def extras(self):
623+ """See Tree.extras."""
624+ result = []
625+ for prefix, tree in self._nested_trees.all_trees().iteritems():
626+ result.extend(osutils.pathjoin(prefix, p)
627+ for p in tree.extras())
628+ return iter(result)
629+
630+ def iter_entries_by_dir(self, specific_file_ids=None, _tree=None,
631+ _branch=None, _root=''):
632+ """See Tree.iter_entries_by_dir."""
633+ return self.inventory.iter_entries_by_dir(specific_file_ids)
634+
635+ def conflicts(self):
636+ """See WorkingTree.conflicts."""
637+ # FIXME adjust paths in conflicts
638+ conflicts = []
639+ for prefix, tree in self._nested_trees.all_trees().iteritems():
640+ conflicts.extend(tree.conflicts())
641+ return _mod_conflicts.ConflictList(conflicts)
642+
643+ def all_file_ids(self):
644+ """See Tree.all_file_ids."""
645+ file_ids = set()
646+ for tree in self._nested_trees.all_trees().itervalues():
647+ file_ids.update(tree.all_file_ids())
648+ return file_ids
649+
650+ def is_ignored(self, filename):
651+ """See Tree.is_ignored."""
652+ tree, prefix, filename = self._nested_trees.get_path_info(filename)
653+ return tree.is_ignored(filename)
654+
655+ def kind(self, file_id):
656+ """See Tree.kind"""
657+ return self._nested_trees.get_tree(file_id).kind(file_id)
658+
659+ def has_filename(self, filename):
660+ """See Tree.has_filename."""
661+ tree, prefix, filename = self._nested_trees.get_path_info(filename)
662+ return tree.has_filename(filename)
663+
664+ def filter_unversioned_files(self, paths):
665+ """See Tree.filter_unversioned_files."""
666+ unversioned = set()
667+ for tree, tree_path, subpaths in \
668+ self._nested_trees.iter_pathinfo_by_tree(paths):
669+ unversioned.update(osutils.pathjoin(tree_path, s) for s
670+ in tree.filter_unversioned_files(subpaths))
671+ return unversioned
672+
673+ def _comparison_data(self, entry, path):
674+ tree, prefix, path = self._nested_trees.get_path_info(path)
675+ kind, executable, stat = tree._comparison_data(entry, path)
676+ if kind == 'tree-reference':
677+ kind = 'directory'
678+ return kind, executable, stat
679+
680+ def _file_size(self, entry, stat_value):
681+ return self._nested_trees.get_tree(entry.file_id)._file_size(
682+ entry, stat_value)
683+
684+ def get_canonical_inventory_path(self, path):
685+ """See Tree.get_canonical_inventory_path."""
686+ return self.inventory._get_ci_path_info(path)[0]
687+
688+ def get_canonical_inventory_paths(self, paths):
689+ """See Tree.get_canonical_inventory_paths."""
690+ return [self.get_canonical_inventory_path(p) for p in paths]
691+
692+ def get_parent_ids(self):
693+ """See Tree.get_parent_ids."""
694+ return self._nested_trees.root_tree.get_parent_ids()
695+
696+ def get_file_sha1(self, file_id, path=None, stat_value=None):
697+ """See Tree.get_file_sha1."""
698+ tree, relpath = self._find_tree_path(file_id, path)
699+ return tree.get_file_sha1(file_id, relpath, stat_value)
700+
701+ def get_file_mtime(self, file_id, path=None):
702+ """See Tree.get_file_mtime."""
703+ tree, relpath = self._find_tree_path(file_id, path)
704+ return tree.get_file_mtime( file_id, relpath)
705+
706+ def get_symlink_target(self, file_id):
707+ """See Tree.get_symlink_target."""
708+ return self._nested_trees.get_tree(file_id).get_symlink_target(file_id)
709+
710+ def iter_children(self, file_id):
711+ """See Tree.iter_children."""
712+ tree = self._nested_trees.get_tree_and_treepath(file_id,
713+ skip_references=True)[0]
714+ return tree.iter_children(file_id)
715+
716+ def id2path(self, file_id):
717+ """See Tree.id2path."""
718+ tree, path = self._nested_trees.get_tree_and_treepath(file_id)
719+ return osutils.pathjoin(path, tree.id2path(file_id))
720+
721+ def apply_inventory_delta(self, changes):
722+ """Apply an inventory delta to a set of subtrees.
723+
724+ The delta is rewritten into a collection of deltas, one for each
725+ affected subtree, then applied to each.
726+ """
727+ deltas = {}
728+ for old_path, new_path, file_id, new_entry in changes:
729+ if new_path is not None:
730+ if new_entry.kind == 'tree-reference':
731+ raise ValueError('Cannot introduce or change a'
732+ ' tree-reference in'
733+ ' CompositeTree.apply_inventory_delta.')
734+ new_tree, new_tree_path = \
735+ self._nested_trees.get_tree_and_treepath(
736+ new_entry.parent_id, skip_references=True)
737+ if new_tree_path != '':
738+ new_path = new_path[len(new_tree_path) + 1:]
739+ else:
740+ new_tree_path = None
741+ if old_path is not None:
742+ old_tree, old_tree_path = \
743+ self._nested_trees.get_tree_and_treepath(file_id)
744+ if old_tree_path != '':
745+ old_path = old_path[len(old_tree_path) + 1:]
746+ old_tree_delta, old_tree = deltas.setdefault(old_tree_path,
747+ ([], old_tree))
748+ if old_tree_path == new_tree_path:
749+ old_tree_delta.append((old_path, new_path, file_id,
750+ new_entry))
751+ else:
752+ old_tree_delta.append((old_path, None, file_id, None))
753+ else:
754+ old_tree_path = None
755+ if new_path is not None and new_tree_path != old_tree_path:
756+ new_tree_delta, new_tree = deltas.setdefault(new_tree_path,
757+ ([], new_tree))
758+ new_tree_delta.append((None, new_path, file_id, new_entry))
759+ for delta, tree in deltas.itervalues():
760+ tree.apply_inventory_delta(delta)
761+
762+ @property
763+ def case_sensitive(self):
764+ """If True, the underlying filesystem is case sensitive."""
765+ return self._nested_trees.root_tree.case_sensitive
766
767=== modified file 'bzrlib/export/__init__.py'
768--- bzrlib/export/__init__.py 2009-03-23 14:59:43 +0000
769+++ bzrlib/export/__init__.py 2009-08-31 04:38:31 +0000
770@@ -146,20 +146,20 @@
771 else:
772 subdir_id = inv.path2id(subdir)
773 entries = inv.iter_entries(subdir_id)
774- if subdir is None:
775- entries.next() # skip root
776- for entry in entries:
777+ for path, entry in entries:
778+ if path == '':
779+ continue
780 # The .bzr* namespace is reserved for "magic" files like
781 # .bzrignore and .bzrrules - do not export these
782- if entry[0].startswith(".bzr"):
783+ if path.startswith(".bzr"):
784 continue
785 if subdir is None:
786- if not tree.has_filename(entry[0]):
787+ if not tree.has_filename(path):
788 continue
789 else:
790- if not tree.has_filename(os.path.join(subdir, entry[0])):
791+ if not tree.has_filename(os.path.join(subdir, path)):
792 continue
793- yield entry
794+ yield path, entry
795
796
797 register_lazy_exporter(None, [], 'bzrlib.export.dir_exporter', 'dir_exporter')
798
799=== modified file 'bzrlib/revisiontree.py'
800--- bzrlib/revisiontree.py 2009-08-28 05:00:33 +0000
801+++ bzrlib/revisiontree.py 2009-08-31 04:38:31 +0000
802@@ -140,6 +140,9 @@
803 def get_reference_revision(self, file_id, path=None):
804 return self.inventory[file_id].reference_revision
805
806+ def get_nested_tree(self, file_id, branch, path=None):
807+ return self._get_nested_revision_tree(file_id, branch, path)
808+
809 def get_root_id(self):
810 if self.inventory.root:
811 return self.inventory.root.file_id
812
813=== modified file 'bzrlib/tests/__init__.py'
814--- bzrlib/tests/__init__.py 2009-08-28 21:05:31 +0000
815+++ bzrlib/tests/__init__.py 2009-08-31 04:38:32 +0000
816@@ -3697,6 +3697,183 @@
817 This function can be replaced if you need to change the default test
818 suite on a global basis, but it is not encouraged.
819 """
820+<<<<<<< TREE
821+=======
822+ testmod_names = [
823+ 'bzrlib.doc',
824+ 'bzrlib.tests.blackbox',
825+ 'bzrlib.tests.branch_implementations',
826+ 'bzrlib.tests.bzrdir_implementations',
827+ 'bzrlib.tests.commands',
828+ 'bzrlib.tests.interrepository_implementations',
829+ 'bzrlib.tests.intertree_implementations',
830+ 'bzrlib.tests.inventory_implementations',
831+ 'bzrlib.tests.per_interbranch',
832+ 'bzrlib.tests.per_lock',
833+ 'bzrlib.tests.per_repository',
834+ 'bzrlib.tests.per_repository_chk',
835+ 'bzrlib.tests.per_repository_reference',
836+ 'bzrlib.tests.test__chk_map',
837+ 'bzrlib.tests.test__dirstate_helpers',
838+ 'bzrlib.tests.test__groupcompress',
839+ 'bzrlib.tests.test__walkdirs_win32',
840+ 'bzrlib.tests.test_ancestry',
841+ 'bzrlib.tests.test_annotate',
842+ 'bzrlib.tests.test_api',
843+ 'bzrlib.tests.test_atomicfile',
844+ 'bzrlib.tests.test_bad_files',
845+ 'bzrlib.tests.test_bisect_multi',
846+ 'bzrlib.tests.test_branch',
847+ 'bzrlib.tests.test_branchbuilder',
848+ 'bzrlib.tests.test_btree_index',
849+ 'bzrlib.tests.test_bugtracker',
850+ 'bzrlib.tests.test_bundle',
851+ 'bzrlib.tests.test_bzrdir',
852+ 'bzrlib.tests.test__chunks_to_lines',
853+ 'bzrlib.tests.test_cache_utf8',
854+ 'bzrlib.tests.test_chk_map',
855+ 'bzrlib.tests.test_chunk_writer',
856+ 'bzrlib.tests.test_clean_tree',
857+ 'bzrlib.tests.test_commands',
858+ 'bzrlib.tests.test_commit',
859+ 'bzrlib.tests.test_commit_merge',
860+ 'bzrlib.tests.test_composite_tree',
861+ 'bzrlib.tests.test_config',
862+ 'bzrlib.tests.test_conflicts',
863+ 'bzrlib.tests.test_counted_lock',
864+ 'bzrlib.tests.test_decorators',
865+ 'bzrlib.tests.test_delta',
866+ 'bzrlib.tests.test_debug',
867+ 'bzrlib.tests.test_deprecated_graph',
868+ 'bzrlib.tests.test_diff',
869+ 'bzrlib.tests.test_directory_service',
870+ 'bzrlib.tests.test_dirstate',
871+ 'bzrlib.tests.test_email_message',
872+ 'bzrlib.tests.test_eol_filters',
873+ 'bzrlib.tests.test_errors',
874+ 'bzrlib.tests.test_export',
875+ 'bzrlib.tests.test_extract',
876+ 'bzrlib.tests.test_fetch',
877+ 'bzrlib.tests.test_fifo_cache',
878+ 'bzrlib.tests.test_filters',
879+ 'bzrlib.tests.test_ftp_transport',
880+ 'bzrlib.tests.test_foreign',
881+ 'bzrlib.tests.test_generate_docs',
882+ 'bzrlib.tests.test_generate_ids',
883+ 'bzrlib.tests.test_globbing',
884+ 'bzrlib.tests.test_gpg',
885+ 'bzrlib.tests.test_graph',
886+ 'bzrlib.tests.test_groupcompress',
887+ 'bzrlib.tests.test_hashcache',
888+ 'bzrlib.tests.test_help',
889+ 'bzrlib.tests.test_hooks',
890+ 'bzrlib.tests.test_http',
891+ 'bzrlib.tests.test_http_implementations',
892+ 'bzrlib.tests.test_http_response',
893+ 'bzrlib.tests.test_https_ca_bundle',
894+ 'bzrlib.tests.test_identitymap',
895+ 'bzrlib.tests.test_ignores',
896+ 'bzrlib.tests.test_index',
897+ 'bzrlib.tests.test_info',
898+ 'bzrlib.tests.test_inv',
899+ 'bzrlib.tests.test_inventory_delta',
900+ 'bzrlib.tests.test_knit',
901+ 'bzrlib.tests.test_lazy_import',
902+ 'bzrlib.tests.test_lazy_regex',
903+ 'bzrlib.tests.test_lockable_files',
904+ 'bzrlib.tests.test_lockdir',
905+ 'bzrlib.tests.test_log',
906+ 'bzrlib.tests.test_lru_cache',
907+ 'bzrlib.tests.test_lsprof',
908+ 'bzrlib.tests.test_mail_client',
909+ 'bzrlib.tests.test_memorytree',
910+ 'bzrlib.tests.test_merge',
911+ 'bzrlib.tests.test_merge3',
912+ 'bzrlib.tests.test_merge_core',
913+ 'bzrlib.tests.test_merge_directive',
914+ 'bzrlib.tests.test_missing',
915+ 'bzrlib.tests.test_msgeditor',
916+ 'bzrlib.tests.test_multiparent',
917+ 'bzrlib.tests.test_mutabletree',
918+ 'bzrlib.tests.test_nonascii',
919+ 'bzrlib.tests.test_options',
920+ 'bzrlib.tests.test_osutils',
921+ 'bzrlib.tests.test_osutils_encodings',
922+ 'bzrlib.tests.test_pack',
923+ 'bzrlib.tests.test_pack_repository',
924+ 'bzrlib.tests.test_patch',
925+ 'bzrlib.tests.test_patches',
926+ 'bzrlib.tests.test_permissions',
927+ 'bzrlib.tests.test_plugins',
928+ 'bzrlib.tests.test_progress',
929+ 'bzrlib.tests.test_read_bundle',
930+ 'bzrlib.tests.test_reconcile',
931+ 'bzrlib.tests.test_reconfigure',
932+ 'bzrlib.tests.test_registry',
933+ 'bzrlib.tests.test_remote',
934+ 'bzrlib.tests.test_rename_map',
935+ 'bzrlib.tests.test_repository',
936+ 'bzrlib.tests.test_revert',
937+ 'bzrlib.tests.test_revision',
938+ 'bzrlib.tests.test_revisionspec',
939+ 'bzrlib.tests.test_revisiontree',
940+ 'bzrlib.tests.test_rio',
941+ 'bzrlib.tests.test_rules',
942+ 'bzrlib.tests.test_sampler',
943+ 'bzrlib.tests.test_selftest',
944+ 'bzrlib.tests.test_serializer',
945+ 'bzrlib.tests.test_setup',
946+ 'bzrlib.tests.test_sftp_transport',
947+ 'bzrlib.tests.test_shelf',
948+ 'bzrlib.tests.test_shelf_ui',
949+ 'bzrlib.tests.test_smart',
950+ 'bzrlib.tests.test_smart_add',
951+ 'bzrlib.tests.test_smart_request',
952+ 'bzrlib.tests.test_smart_transport',
953+ 'bzrlib.tests.test_smtp_connection',
954+ 'bzrlib.tests.test_source',
955+ 'bzrlib.tests.test_ssh_transport',
956+ 'bzrlib.tests.test_status',
957+ 'bzrlib.tests.test_store',
958+ 'bzrlib.tests.test_strace',
959+ 'bzrlib.tests.test_subsume',
960+ 'bzrlib.tests.test_switch',
961+ 'bzrlib.tests.test_symbol_versioning',
962+ 'bzrlib.tests.test_tag',
963+ 'bzrlib.tests.test_testament',
964+ 'bzrlib.tests.test_textfile',
965+ 'bzrlib.tests.test_textmerge',
966+ 'bzrlib.tests.test_timestamp',
967+ 'bzrlib.tests.test_trace',
968+ 'bzrlib.tests.test_transactions',
969+ 'bzrlib.tests.test_transform',
970+ 'bzrlib.tests.test_transport',
971+ 'bzrlib.tests.test_transport_implementations',
972+ 'bzrlib.tests.test_transport_log',
973+ 'bzrlib.tests.test_tree',
974+ 'bzrlib.tests.test_treebuilder',
975+ 'bzrlib.tests.test_tsort',
976+ 'bzrlib.tests.test_tuned_gzip',
977+ 'bzrlib.tests.test_ui',
978+ 'bzrlib.tests.test_uncommit',
979+ 'bzrlib.tests.test_upgrade',
980+ 'bzrlib.tests.test_upgrade_stacked',
981+ 'bzrlib.tests.test_urlutils',
982+ 'bzrlib.tests.test_version',
983+ 'bzrlib.tests.test_version_info',
984+ 'bzrlib.tests.test_versionedfile',
985+ 'bzrlib.tests.test_weave',
986+ 'bzrlib.tests.test_whitebox',
987+ 'bzrlib.tests.test_win32utils',
988+ 'bzrlib.tests.test_workingtree',
989+ 'bzrlib.tests.test_workingtree_4',
990+ 'bzrlib.tests.test_wsgi',
991+ 'bzrlib.tests.test_xml',
992+ 'bzrlib.tests.tree_implementations',
993+ 'bzrlib.tests.workingtree_implementations',
994+ 'bzrlib.util.tests.test_bencode',
995+ ]
996+>>>>>>> MERGE-SOURCE
997
998 loader = TestUtil.TestLoader()
999
1000
1001=== modified file 'bzrlib/tests/per_workingtree/test_add_reference.py'
1002--- bzrlib/tests/per_workingtree/test_add_reference.py 2009-07-10 07:14:02 +0000
1003+++ bzrlib/tests/per_workingtree/test_add_reference.py 2009-08-31 04:38:32 +0000
1004@@ -63,7 +63,8 @@
1005 basis = tree.basis_tree()
1006 basis.lock_read()
1007 try:
1008- sub_tree = tree.get_nested_tree('sub-tree-root-id')
1009+ sub_tree = tree.get_nested_tree('sub-tree-root-id',
1010+ tree.branch)
1011 self.assertEqual(sub_tree.last_revision(),
1012 tree.get_reference_revision('sub-tree-root-id'))
1013 finally:
1014@@ -108,8 +109,9 @@
1015 tree, sub_tree = self.make_nested_trees()
1016 tree.lock_read()
1017 try:
1018- sub_tree2 = tree.get_nested_tree('sub-tree-root-id')
1019+ sub_tree2 = tree.get_nested_tree('sub-tree-root-id', tree.branch)
1020 self.assertEqual(sub_tree.basedir, sub_tree2.basedir)
1021- sub_tree2 = tree.get_nested_tree('sub-tree-root-id', 'sub-tree')
1022+ sub_tree2 = tree.get_nested_tree('sub-tree-root-id', tree.branch,
1023+ 'sub-tree')
1024 finally:
1025 tree.unlock()
1026
1027=== added file 'bzrlib/tests/test_composite_tree.py'
1028--- bzrlib/tests/test_composite_tree.py 1970-01-01 00:00:00 +0000
1029+++ bzrlib/tests/test_composite_tree.py 2009-08-31 04:38:32 +0000
1030@@ -0,0 +1,362 @@
1031+# Copyright (C) 2007, 2009 Canonical Ltd
1032+#
1033+# This program is free software; you can redistribute it and/or modify
1034+# it under the terms of the GNU General Public License as published by
1035+# the Free Software Foundation; either version 2 of the License, or
1036+# (at your option) any later version.
1037+#
1038+# This program is distributed in the hope that it will be useful,
1039+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1040+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1041+# GNU General Public License for more details.
1042+#
1043+# You should have received a copy of the GNU General Public License
1044+# along with this program; if not, write to the Free Software
1045+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1046+
1047+
1048+import os
1049+
1050+from bzrlib import (
1051+ composite_tree,
1052+ conflicts,
1053+ errors,
1054+ inventory,
1055+ tests,
1056+ )
1057+from bzrlib.export import export
1058+
1059+
1060+class TestNestedTrees(tests.TestCaseWithTransport):
1061+
1062+ def make_branch_and_tree(self, path, format='pack-0.92-subtree'):
1063+ return tests.TestCaseWithTransport.make_branch_and_tree(
1064+ self, path, format)
1065+
1066+ def test_paths_info(self):
1067+ tree = self.make_branch_and_tree('.')
1068+ subtree = self.make_branch_and_tree('sub')
1069+ subsubtree = self.make_branch_and_tree('sub/sub')
1070+ subsubtree.set_root_id('sub-sub-root-id')
1071+ subtree.add_reference(subsubtree)
1072+ tree.add_reference(subtree)
1073+ nested = composite_tree.NestedTrees(tree, tree.branch)
1074+ nested.lock_read()
1075+ self.addCleanup(nested.unlock)
1076+ fullpath, relpath, found_entry, found_tree = \
1077+ nested.paths_info(['sub/sub'])[0]
1078+ self.assertEqual('sub-sub-root-id', found_entry.file_id)
1079+ self.assertEqual(subsubtree.basedir, found_tree.basedir)
1080+ self.assertEqual('', relpath)
1081+ self.assertEqual('sub/sub', fullpath)
1082+
1083+ # Now testing an unversioned, non-existent path
1084+ fullpath, relpath, found_entry, found_tree = \
1085+ nested.paths_info(['sub/subb'])[0]
1086+ self.assertIs(None, found_entry)
1087+ self.assertIsNot(None, found_tree)
1088+
1089+ def lock_read(self, item):
1090+ item.lock_read()
1091+ self.addCleanup(item.unlock)
1092+
1093+ def test_all_trees(self):
1094+ tree = self.make_branch_and_tree('tree')
1095+ subtree = self.make_branch_and_tree('tree/subtree')
1096+ tree.add_reference(subtree)
1097+ subsubtree = self.make_branch_and_tree('tree/subtree/subsubtree')
1098+ subtree.add_reference(subsubtree)
1099+ nested = composite_tree.NestedTrees(tree, tree.branch)
1100+ self.lock_read(nested)
1101+ result = nested.all_trees()
1102+ expected = {'': tree, 'subtree': subtree,
1103+ 'subtree/subsubtree': subsubtree}
1104+ def bases(input):
1105+ return dict((y, x.basedir) for y, x in input.iteritems())
1106+ self.assertEqual(bases(expected), bases(result))
1107+
1108+
1109+class TestCompositeTree(tests.TestCaseWithTransport):
1110+
1111+ def make_branch_and_tree(self, path, format='pack-0.92-subtree'):
1112+ return tests.TestCaseWithTransport.make_branch_and_tree(
1113+ self, path, format)
1114+
1115+ def make_tree_files(self, subfile=False):
1116+ tree = self.make_branch_and_tree('.')
1117+ self.build_tree(['file'])
1118+ tree.set_root_id('tree-root')
1119+ tree.add(['file'], ['file-id'])
1120+ subtree = self.make_branch_and_tree('subtree')
1121+ subtree.set_root_id('subtree-id')
1122+ if subfile:
1123+ self.build_tree(['subtree/subfile'])
1124+ subtree.add('subfile', 'subfile-id')
1125+ tree.add_reference(subtree)
1126+ tree.lock_write()
1127+ self.addCleanup(tree.unlock)
1128+ return tree, subtree
1129+
1130+ def test_move_across_trees(self):
1131+ tree, subtree = self.make_tree_files()
1132+ composite = composite_tree.CompositeTree(tree, tree.branch)
1133+ pairs = composite.move(['file'], 'subtree')
1134+ subtree.lock_write()
1135+ self.addCleanup(subtree.unlock)
1136+ self.assertIs(None, tree.path2id('file'))
1137+ self.assertEqual('file-id', subtree.path2id('file'))
1138+ self.failUnlessExists('subtree/file')
1139+ self.failIfExists('file')
1140+ self.assertEqual([('file', 'subtree/file')], pairs)
1141+
1142+ def test_move_after(self):
1143+ tree, subtree = self.make_tree_files()
1144+ os.rename('file', 'subtree/file')
1145+ composite = composite_tree.CompositeTree(tree, tree.branch)
1146+ composite.move(['file'], 'subtree', after=True)
1147+ self.assertIs(None, tree.path2id('file'))
1148+ self.assertEqual('file-id', subtree.path2id('file'))
1149+
1150+ def test_move_needs_after(self):
1151+ tree, subtree = self.make_tree_files()
1152+ self.build_tree(['subtree/file'])
1153+ composite = composite_tree.CompositeTree(tree, tree.branch)
1154+ self.assertRaises(errors.RenameFailedFilesExist, composite.move,
1155+ ['file'], 'subtree')
1156+
1157+ def test_move_force_after(self):
1158+ tree, subtree = self.make_tree_files()
1159+ self.build_tree(['subtree/file'])
1160+ composite = composite_tree.CompositeTree(tree, tree.branch)
1161+ composite.move(['file'], 'subtree',
1162+ after=True)
1163+ self.assertIs(None, tree.path2id('file'))
1164+ self.assertEqual('file-id', subtree.path2id('file'))
1165+
1166+ def test_move_to_unversioned(self):
1167+ tree, subtree = self.make_tree_files()
1168+ tree.remove('subtree')
1169+ composite = composite_tree.CompositeTree(tree, tree.branch)
1170+ self.assertRaises(errors.BzrMoveFailedError, composite.move, ['file'],
1171+ 'subtree', after=True)
1172+
1173+ def test_move_from_unversioned(self):
1174+ tree, subtree = self.make_tree_files()
1175+ tree.remove('file')
1176+ composite = composite_tree.CompositeTree(tree, tree.branch)
1177+ self.assertRaises(errors.BzrMoveFailedError, composite.move, ['file'],
1178+ 'subtree', after=True)
1179+
1180+ def test_rename_into_subtree(self):
1181+ tree, subtree = self.make_tree_files()
1182+ composite = composite_tree.CompositeTree(tree, tree.branch)
1183+ composite.rename_one('file', 'subtree/file1')
1184+ self.failUnlessExists('subtree/file1')
1185+ self.assertEqual('file-id', subtree.path2id('file1'))
1186+
1187+ def test_iter_entries_by_dir(self):
1188+ tree, subtree = self.make_tree_files()
1189+ composite = self.get_locked_composite(tree)
1190+ for path, entry in composite.iter_entries_by_dir():
1191+ self.assertNotContainsRe(path, '/$')
1192+
1193+ def test_iter_entries_by_dir_follow_subtree(self):
1194+ """Ensure that iter_entries_by_dir follows subtrees.
1195+
1196+ This makes sure that even when the cached kind is directory, subtree
1197+ is followed.
1198+ """
1199+ tree = self.make_branch_and_tree('.')
1200+ self.build_tree(['subtree/', 'subtree/file'])
1201+ subtree = self.make_branch_and_tree('subtree')
1202+ self.build_tree(['subtree/file'])
1203+ subtree.add('file')
1204+ tree.add_reference(subtree)
1205+ composite = self.get_locked_composite(tree)
1206+ paths = [p for p, e in composite.iter_entries_by_dir()]
1207+ self.assertTrue('subtree' in paths)
1208+ self.assertTrue('subtree/file' in paths)
1209+
1210+ def test_iter_changes(self):
1211+ tree, subtree = self.make_tree_files()
1212+ new = self.get_locked_composite(tree)
1213+ old = self.get_locked_composite(tree.basis_tree(), tree.branch)
1214+ for file_id, paths, content, versioned, parent, name, kind, executable\
1215+ in new.iter_changes(old):
1216+ self.assertNotEqual('tree-reference', kind[1])
1217+
1218+ def test_iter_changes_accepts_specific_files_for_deletion(self):
1219+ """Test corner case requiring all_file_ids()"""
1220+ tree, subtree = self.make_tree_files()
1221+ old = self.get_locked_composite(tree)
1222+ new = self.get_locked_composite(tree.basis_tree(), tree.branch)
1223+ for file_id, paths, content, versioned, parent, name, kind, executable\
1224+ in new.iter_changes(old, specific_files=['subtree']):
1225+ self.assertNotEqual('tree-reference', kind[1])
1226+
1227+ def test_inventory(self):
1228+ tree, subtree = self.make_tree_files()
1229+ composite = self.get_locked_composite(tree)
1230+ self.assertRaises(errors.NoSuchId, lambda x: composite.inventory[x],
1231+ 'not-present')
1232+ subtree_entry = composite.inventory['subtree-id']
1233+ self.assertEqual('tree-reference', subtree_entry.kind)
1234+
1235+ def test_comparison_data(self):
1236+ tree, subtree = self.make_tree_files()
1237+ composite = self.get_locked_composite(tree)
1238+ entry = composite.inventory['subtree-id']
1239+ kind, exe, stat = composite._comparison_data(entry, 'subtree')
1240+ self.assertEqual('directory', kind)
1241+
1242+ def test_apply_inventory_delta(self):
1243+ tree, subtree = self.make_tree_files()
1244+ composite = composite_tree.CompositeTree(tree, tree.branch)
1245+ # Move an existing file into a new subtree.
1246+ new_entry = tree.inventory['file-id'].copy()
1247+ new_entry.parent_id = 'subtree-id'
1248+ composite.lock_write()
1249+ try:
1250+ composite.apply_inventory_delta([('file', 'subtree/file',
1251+ 'file-id', new_entry)])
1252+ finally:
1253+ composite.unlock()
1254+ self.assertEqual('file', subtree.id2path('file-id'))
1255+ self.assertEqual('file-id', subtree.path2id('file'))
1256+ self.assertEqual(None, tree.path2id('file'))
1257+
1258+ # Rename subtree/file to subtree/smile
1259+ new_entry = new_entry.copy()
1260+ new_entry.name = 'smile'
1261+ composite.lock_write()
1262+ try:
1263+ composite.apply_inventory_delta([('subtree/file', 'subtree/smile',
1264+ 'file-id', new_entry)])
1265+ finally:
1266+ composite.unlock()
1267+ self.assertEqual('file-id', subtree.path2id('smile'))
1268+
1269+ # Delete subtree/smile
1270+ composite.lock_write()
1271+ try:
1272+ composite.apply_inventory_delta([('subtree/smile', None, 'file-id',
1273+ None)])
1274+ finally:
1275+ composite.unlock()
1276+ self.assertEqual(None, subtree.path2id('smile'))
1277+
1278+ # Create subtree/smile
1279+ composite.lock_write()
1280+ try:
1281+ composite.apply_inventory_delta([(None, 'subtree/smile', 'file-id',
1282+ new_entry)])
1283+ finally:
1284+ composite.unlock()
1285+ self.assertEqual('file-id', subtree.path2id('smile'))
1286+
1287+ # Move subtree/smile back into root tree
1288+ new_entry = new_entry.copy()
1289+ new_entry.parent_id = tree.get_root_id()
1290+ composite.lock_write()
1291+ try:
1292+ composite.apply_inventory_delta([('subtree/smile', 'smile',
1293+ 'file-id', new_entry)])
1294+ finally:
1295+ composite.unlock()
1296+ self.assertEqual(None, subtree.path2id('smile'))
1297+ self.assertEqual('file-id', tree.path2id('smile'))
1298+
1299+ def test_apply_inventory_delta_tree_reference(self):
1300+ tree = self.make_branch_and_tree('.')
1301+ tree.set_root_id('root-id')
1302+ composite = composite_tree.CompositeTree(tree, tree.branch)
1303+ composite.lock_write()
1304+ self.addCleanup(composite.unlock)
1305+ foo = inventory.TreeReference(
1306+ 'foo-id', 'foo', 'root-id', reference_revision='foo-rev-id')
1307+ e = self.assertRaises(ValueError, composite.apply_inventory_delta,
1308+ [(None, 'foo', 'foo-id', foo)])
1309+ self.assertEqual('Cannot introduce or change a tree-reference in'
1310+ ' CompositeTree.apply_inventory_delta.', str(e))
1311+
1312+ def get_locked_composite(self, tree, branch=None):
1313+ if branch is None:
1314+ branch = tree.branch
1315+ composite = composite_tree.CompositeTree(tree, branch)
1316+ composite.lock_read()
1317+ self.addCleanup(composite.unlock)
1318+ return composite
1319+
1320+ def test_all_file_ids(self):
1321+ tree, subtree = self.make_tree_files(subfile=True)
1322+ composite = self.get_locked_composite(tree)
1323+ self.assertEqual(set(['tree-root', 'file-id', 'subtree-id',
1324+ 'subfile-id']), composite.all_file_ids())
1325+
1326+ def test_paths2ids(self):
1327+ tree, subtree = self.make_tree_files()
1328+ composite = self.get_locked_composite(tree)
1329+ self.assertEqual(set(['file-id', 'subtree-id']),
1330+ composite.paths2ids(['file', 'subtree']))
1331+
1332+ def test_iter_children(self):
1333+ tree, subtree = self.make_tree_files(subfile=True)
1334+ composite = self.get_locked_composite(tree)
1335+ self.assertEqual(set(['subfile-id']),
1336+ set(composite.iter_children('subtree-id')))
1337+ self.assertEqual(set(['file-id', 'subtree-id']),
1338+ set(composite.iter_children('tree-root')))
1339+
1340+ def test_case_sensitive(self):
1341+ tree, subtree = self.make_tree_files()
1342+ composite = self.get_locked_composite(tree)
1343+ self.assertEqual(tree.case_sensitive, composite.case_sensitive)
1344+ tree.case_sensitive = not tree.case_sensitive
1345+ self.assertEqual(tree.case_sensitive, composite.case_sensitive)
1346+
1347+ def make_ci_tree(self):
1348+ tree, subtree = self.make_tree_files()
1349+ self.build_tree(['subtree/subFile'])
1350+ subtree.add('subFile')
1351+ return self.get_locked_composite(tree)
1352+
1353+ def test_get_canonical_inventory_path(self):
1354+ """Finds subtree paths."""
1355+ composite = self.make_ci_tree()
1356+ self.assertEqual('subtree/subFile',
1357+ composite.get_canonical_inventory_path(
1358+ 'SUBTREE/SUBFILE'))
1359+
1360+ def test_get_canonical_inventory_path_preserves_missing(self):
1361+ """Returns unversion path segments verbatim."""
1362+ composite = self.make_ci_tree()
1363+ self.assertEqual('subtree/subFile/X',
1364+ composite.get_canonical_inventory_path(
1365+ 'SUBTREE/SUBFILE/X'))
1366+
1367+ def test_get_canonical_inventory_paths(self):
1368+ """Finds subtree paths."""
1369+ composite = self.make_ci_tree()
1370+ self.assertEqual(['subtree/subFile'],
1371+ composite.get_canonical_inventory_paths(
1372+ ['SUBTREE/SUBFILE']))
1373+
1374+ def test_get_file_kind(self):
1375+ composite = self.get_locked_composite(self.make_tree_files()[0])
1376+ inv = composite.inventory
1377+ self.assertEqual('file', inv.get_file_kind('file-id'))
1378+ self.assertEqual('directory', inv.get_file_kind('subtree-id'))
1379+
1380+ def test_export(self):
1381+ tree, subtree = self.make_tree_files(subfile=True)
1382+ composite = self.get_locked_composite(tree)
1383+ export(composite, 'output', None, None, 'subtree')
1384+ self.assertEqual(['subfile'], os.listdir('output'))
1385+
1386+ def test_conflicts_path(self):
1387+ tree, subtree = self.make_tree_files()
1388+ subtree.add_conflicts([conflicts.ContentsConflict('foo')])
1389+ composite = self.get_locked_composite(tree)
1390+ [conflict] = composite.conflicts()
1391+ self.expectFailure('Conflict reporting for composite trees is wonky.',
1392+ self.assertEqual, 'subtree/foo', conflict.path)
1393
1394=== modified file 'bzrlib/transform.py'
1395--- bzrlib/transform.py 2009-08-28 05:00:33 +0000
1396+++ bzrlib/transform.py 2009-08-31 04:38:31 +0000
1397@@ -1315,6 +1315,7 @@
1398 tree.case_sensitive)
1399 self._deletiondir = deletiondir
1400
1401+<<<<<<< TREE
1402 def canonical_path(self, path):
1403 """Get the canonical tree-relative path"""
1404 # don't follow final symlinks
1405@@ -1397,6 +1398,10 @@
1406 yield self.trans_id_tree_path(childpath)
1407
1408 def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1409+=======
1410+ def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None,
1411+ skip_inventory=False):
1412+>>>>>>> MERGE-SOURCE
1413 """Apply all changes to the inventory and filesystem.
1414
1415 If filesystem or inventory conflicts are present, MalformedTransform
1416@@ -1437,7 +1442,8 @@
1417 mover.apply_deletions()
1418 finally:
1419 child_pb.finished()
1420- self._tree.apply_inventory_delta(inventory_delta)
1421+ if not skip_inventory:
1422+ self._tree.apply_inventory_delta(inventory_delta)
1423 self._done = True
1424 self.finalize()
1425 return _TransformResults(modified_paths, self.rename_count)
1426
1427=== modified file 'bzrlib/workingtree_4.py'
1428--- bzrlib/workingtree_4.py 2009-08-25 04:43:21 +0000
1429+++ bzrlib/workingtree_4.py 2009-08-31 04:38:31 +0000
1430@@ -414,9 +414,9 @@
1431
1432 def get_reference_revision(self, file_id, path=None):
1433 # referenced tree's revision is whatever's currently there
1434- return self.get_nested_tree(file_id, path).last_revision()
1435+ return self.get_nested_tree(file_id, self.branch, path).last_revision()
1436
1437- def get_nested_tree(self, file_id, path=None):
1438+ def get_nested_tree(self, file_id, branch, path=None):
1439 if path is None:
1440 path = self.id2path(file_id)
1441 # else: check file_id is at path?
1442@@ -1806,6 +1806,9 @@
1443 target = target.decode('utf8')
1444 return target
1445
1446+ def get_nested_tree(self, file_id, branch, path=None):
1447+ return self._get_nested_revision_tree(file_id, branch, path)
1448+
1449 def get_revision_id(self):
1450 """Return the revision id for this tree."""
1451 return self._revision_id
1452
1453=== added file 'doc/developers/nested-trees.txt'
1454--- doc/developers/nested-trees.txt 1970-01-01 00:00:00 +0000
1455+++ doc/developers/nested-trees.txt 2009-08-31 04:38:32 +0000
1456@@ -0,0 +1,28 @@
1457+Nested Trees Implementation
1458+###########################
1459+
1460+CompositeTree
1461+-------------
1462+CompositeTree is a shim to enable subtree support for certain commands.
1463+
1464+It is constructed from a Tree (typically a RevisionTree or WorkingTree) and a
1465+Branch. It provides an alternate view of the Tree and its subtrees, in which
1466+the subtrees and their contents appear to be part of the root tree. The roots
1467+of subtrees are presented as directories, not as tree references.
1468+
1469+It can be used with export, diff and status. It should be tested with every
1470+command that uses it, because it is not guaranteed to be a complete
1471+implementation of the Tree API. It is not suitable as input to operations that
1472+must know about the locations of subtrees, such as revert.
1473+
1474+
1475+NestedTrees
1476+-----------
1477+NestedTrees is an API providing access to a set of nested trees. It is able to
1478+convert paths and file-ids into references to specific Trees. It provides
1479+caching, to avoid retrieving the same Tree multiple times. It provides
1480+locking, to ensure trees are locked and unlocked at appropriate times.
1481+
1482+It is used by CompositeTree, and is a recommended API for all other operations.
1483+It is expected to acquire more capabilities as the needs of sets of nested
1484+trees become clearer.