# Copyright (C) 2006 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

"""Tests of the parent related functions of WorkingTrees."""

from errno import EEXIST
import os

from bzrlib import (
    errors,
    osutils,
    revision as _mod_revision,
    symbol_versioning,
    tests,
    )
from bzrlib.inventory import (
    Inventory,
    InventoryFile,
    InventoryDirectory,
    InventoryLink,
    )
from bzrlib.revision import Revision
from bzrlib.tests.per_workingtree import TestCaseWithWorkingTree
from bzrlib.uncommit import uncommit


class TestParents(TestCaseWithWorkingTree):

    def assertConsistentParents(self, expected, tree):
        """Check that the parents found are as expected.

        This test helper also checks that they are consistent with
        the pre-get_parent_ids() api - which is now deprecated.
        """
        self.assertEqual(expected, tree.get_parent_ids())
        if expected == []:
            self.assertEqual(_mod_revision.NULL_REVISION,
                             _mod_revision.ensure_null(tree.last_revision()))
        else:
            self.assertEqual(expected[0], tree.last_revision())


class TestGetParents(TestParents):

    def test_get_parents(self):
        t = self.make_branch_and_tree('.')
        self.assertEqual([], t.get_parent_ids())


class TestSetParents(TestParents):

    def test_set_no_parents(self):
        t = self.make_branch_and_tree('.')
        t.set_parent_trees([])
        self.assertEqual([], t.get_parent_ids())
        # now give it a real parent, and then set it to no parents again.
        t.commit('first post')
        t.set_parent_trees([])
        self.assertConsistentParents([], t)

    def test_set_null_parent(self):
        t = self.make_branch_and_tree('.')
        self.assertRaises(errors.ReservedId, t.set_parent_ids, ['null:'],
                          allow_leftmost_as_ghost=True)
        self.assertRaises(errors.ReservedId, t.set_parent_trees,
                          [('null:', None)], allow_leftmost_as_ghost=True)

    def test_set_one_ghost_parent_rejects(self):
        t = self.make_branch_and_tree('.')
        self.assertRaises(errors.GhostRevisionUnusableHere,
            t.set_parent_trees, [('missing-revision-id', None)])

    def test_set_one_ghost_parent_force(self):
        t = self.make_branch_and_tree('.')
        t.set_parent_trees([('missing-revision-id', None)],
            allow_leftmost_as_ghost=True)
        self.assertConsistentParents(['missing-revision-id'], t)

    def test_set_two_parents_one_ghost(self):
        t = self.make_branch_and_tree('.')
        revision_in_repo = t.commit('first post')
        # remove the tree's history
        uncommit(t.branch, tree=t)
        rev_tree = t.branch.repository.revision_tree(revision_in_repo)
        t.set_parent_trees([(revision_in_repo, rev_tree),
            ('another-missing', None)])
        self.assertConsistentParents([revision_in_repo, 'another-missing'], t)

    def test_set_three_parents(self):
        t = self.make_branch_and_tree('.')
        first_revision = t.commit('first post')
        uncommit(t.branch, tree=t)
        second_revision = t.commit('second post')
        uncommit(t.branch, tree=t)
        third_revision = t.commit('third post')
        uncommit(t.branch, tree=t)
        rev_tree1 = t.branch.repository.revision_tree(first_revision)
        rev_tree2 = t.branch.repository.revision_tree(second_revision)
        rev_tree3 = t.branch.repository.revision_tree(third_revision)
        t.set_parent_trees([(first_revision, rev_tree1),
            (second_revision, rev_tree2),
            (third_revision, rev_tree3)])
        self.assertConsistentParents(
            [first_revision, second_revision, third_revision], t)

    def test_set_no_parents_ids(self):
        t = self.make_branch_and_tree('.')
        t.set_parent_ids([])
        self.assertEqual([], t.get_parent_ids())
        # now give it a real parent, and then set it to no parents again.
        t.commit('first post')
        t.set_parent_ids([])
        self.assertConsistentParents([], t)

    def test_set_one_ghost_parent_ids_rejects(self):
        t = self.make_branch_and_tree('.')
        self.assertRaises(errors.GhostRevisionUnusableHere,
            t.set_parent_ids, ['missing-revision-id'])

    def test_set_one_ghost_parent_ids_force(self):
        t = self.make_branch_and_tree('.')
        t.set_parent_ids(['missing-revision-id'],
            allow_leftmost_as_ghost=True)
        self.assertConsistentParents(['missing-revision-id'], t)

    def test_set_two_parents_one_ghost_ids(self):
        t = self.make_branch_and_tree('.')
        revision_in_repo = t.commit('first post')
        # remove the tree's history
        uncommit(t.branch, tree=t)
        rev_tree = t.branch.repository.revision_tree(revision_in_repo)
        t.set_parent_ids([revision_in_repo, 'another-missing'])
        self.assertConsistentParents([revision_in_repo, 'another-missing'], t)

    def test_set_three_parents_ids(self):
        t = self.make_branch_and_tree('.')
        first_revision = t.commit('first post')
        uncommit(t.branch, tree=t)
        second_revision = t.commit('second post')
        uncommit(t.branch, tree=t)
        third_revision = t.commit('third post')
        uncommit(t.branch, tree=t)
        rev_tree1 = t.branch.repository.revision_tree(first_revision)
        rev_tree2 = t.branch.repository.revision_tree(second_revision)
        rev_tree3 = t.branch.repository.revision_tree(third_revision)
        t.set_parent_ids([first_revision, second_revision, third_revision])
        self.assertConsistentParents(
            [first_revision, second_revision, third_revision], t)

    def test_set_duplicate_parent_ids(self):
        t = self.make_branch_and_tree('.')
        rev1 = t.commit('first post')
        uncommit(t.branch, tree=t)
        rev2 = t.commit('second post')
        uncommit(t.branch, tree=t)
        rev3 = t.commit('third post')
        uncommit(t.branch, tree=t)
        t.set_parent_ids([rev1, rev2, rev2, rev3])
        # We strip the duplicate, but preserve the ordering
        self.assertConsistentParents([rev1, rev2, rev3], t)

    def test_set_duplicate_parent_trees(self):
        t = self.make_branch_and_tree('.')
        rev1 = t.commit('first post')
        uncommit(t.branch, tree=t)
        rev2 = t.commit('second post')
        uncommit(t.branch, tree=t)
        rev3 = t.commit('third post')
        uncommit(t.branch, tree=t)
        rev_tree1 = t.branch.repository.revision_tree(rev1)
        rev_tree2 = t.branch.repository.revision_tree(rev2)
        rev_tree3 = t.branch.repository.revision_tree(rev3)
        t.set_parent_trees([(rev1, rev_tree1), (rev2, rev_tree2),
                            (rev2, rev_tree2), (rev3, rev_tree3)])
        # We strip the duplicate, but preserve the ordering
        self.assertConsistentParents([rev1, rev2, rev3], t)

    def test_set_parent_ids_in_ancestry(self):
        t = self.make_branch_and_tree('.')
        rev1 = t.commit('first post')
        rev2 = t.commit('second post')
        rev3 = t.commit('third post')
        # Reset the tree, back to rev1
        t.set_parent_ids([rev1])
        t.branch.set_last_revision_info(1, rev1)
        self.assertConsistentParents([rev1], t)
        t.set_parent_ids([rev1, rev2, rev3])
        # rev2 is in the ancestry of rev3, so it will be filtered out
        self.assertConsistentParents([rev1, rev3], t)
        # Order should be preserved, and the first revision should always be
        # kept
        t.set_parent_ids([rev2, rev3, rev1])
        self.assertConsistentParents([rev2, rev3], t)

    def test_set_parent_trees_in_ancestry(self):
        t = self.make_branch_and_tree('.')
        rev1 = t.commit('first post')
        rev2 = t.commit('second post')
        rev3 = t.commit('third post')
        # Reset the tree, back to rev1
        t.set_parent_ids([rev1])
        t.branch.set_last_revision_info(1, rev1)
        self.assertConsistentParents([rev1], t)
        rev_tree1 = t.branch.repository.revision_tree(rev1)
        rev_tree2 = t.branch.repository.revision_tree(rev2)
        rev_tree3 = t.branch.repository.revision_tree(rev3)
        t.set_parent_trees([(rev1, rev_tree1), (rev2, rev_tree2),
                            (rev3, rev_tree3)])
        # rev2 is in the ancestry of rev3, so it will be filtered out
        self.assertConsistentParents([rev1, rev3], t)
        # Order should be preserved, and the first revision should always be
        # kept
        t.set_parent_trees([(rev2, rev_tree2), (rev1, rev_tree1),
                            (rev3, rev_tree3)])
        self.assertConsistentParents([rev2, rev3], t)

    def test_unicode_symlink(self):
        # this tests bug #272444
        self.requireFeature(tests.SymlinkFeature)
        self.requireFeature(tests.UnicodeFilenameFeature)

        tree = self.make_branch_and_tree('tree1')

        # The link points to a file whose name is an omega
        # U+03A9 GREEK CAPITAL LETTER OMEGA
        # UTF-8: ce a9  UTF-16BE: 03a9  Decimal: &#937;
        target = u'\u03a9'
        link_name = u'\N{Euro Sign}link'
        os.symlink(target, 'tree1/' + link_name)
        tree.add([link_name],['link-id'])

        revision1 = tree.commit('added a link to a Unicode target')
        revision2 = tree.commit('this revision will be discarded')
        tree.set_parent_ids([revision1])
        tree.lock_read()
        self.addCleanup(tree.unlock)
        # Check that the symlink target is safely round-tripped in the trees.
        self.assertEqual(target, tree.get_symlink_target('link-id'))
        basis = tree.basis_tree()
        self.assertEqual(target, basis.get_symlink_target('link-id'))


class TestAddParent(TestParents):

    def test_add_first_parent_id(self):
        """Test adding the first parent id"""
        tree = self.make_branch_and_tree('.')
        first_revision = tree.commit('first post')
        uncommit(tree.branch, tree=tree)
        tree.add_parent_tree_id(first_revision)
        self.assertConsistentParents([first_revision], tree)

    def test_add_first_parent_id_ghost_rejects(self):
        """Test adding the first parent id - as a ghost"""
        tree = self.make_branch_and_tree('.')
        self.assertRaises(errors.GhostRevisionUnusableHere,
            tree.add_parent_tree_id, 'first-revision')

    def test_add_first_parent_id_ghost_force(self):
        """Test adding the first parent id - as a ghost"""
        tree = self.make_branch_and_tree('.')
        tree.add_parent_tree_id('first-revision', allow_leftmost_as_ghost=True)
        self.assertConsistentParents(['first-revision'], tree)

    def test_add_second_parent_id_with_ghost_first(self):
        """Test adding the second parent when the first is a ghost."""
        tree = self.make_branch_and_tree('.')
        tree.add_parent_tree_id('first-revision', allow_leftmost_as_ghost=True)
        tree.add_parent_tree_id('second')
        self.assertConsistentParents(['first-revision', 'second'], tree)

    def test_add_second_parent_id(self):
        """Test adding the second parent id"""
        tree = self.make_branch_and_tree('.')
        first_revision = tree.commit('first post')
        uncommit(tree.branch, tree=tree)
        second_revision = tree.commit('second post')
        tree.add_parent_tree_id(first_revision)
        self.assertConsistentParents([second_revision, first_revision], tree)

    def test_add_second_parent_id_ghost(self):
        """Test adding the second parent id - as a ghost"""
        tree = self.make_branch_and_tree('.')
        first_revision = tree.commit('first post')
        tree.add_parent_tree_id('second')
        self.assertConsistentParents([first_revision, 'second'], tree)

    def test_add_first_parent_tree(self):
        """Test adding the first parent id"""
        tree = self.make_branch_and_tree('.')
        first_revision = tree.commit('first post')
        uncommit(tree.branch, tree=tree)
        tree.add_parent_tree((first_revision,
            tree.branch.repository.revision_tree(first_revision)))
        self.assertConsistentParents([first_revision], tree)

    def test_add_first_parent_tree_ghost_rejects(self):
        """Test adding the first parent id - as a ghost"""
        tree = self.make_branch_and_tree('.')
        self.assertRaises(errors.GhostRevisionUnusableHere,
            tree.add_parent_tree, ('first-revision', None))

    def test_add_first_parent_tree_ghost_force(self):
        """Test adding the first parent id - as a ghost"""
        tree = self.make_branch_and_tree('.')
        tree.add_parent_tree(('first-revision', None),
            allow_leftmost_as_ghost=True)
        self.assertConsistentParents(['first-revision'], tree)

    def test_add_second_parent_tree(self):
        """Test adding the second parent id"""
        tree = self.make_branch_and_tree('.')
        first_revision = tree.commit('first post')
        uncommit(tree.branch, tree=tree)
        second_revision = tree.commit('second post')
        tree.add_parent_tree((first_revision,
            tree.branch.repository.revision_tree(first_revision)))
        self.assertConsistentParents([second_revision, first_revision], tree)

    def test_add_second_parent_tree_ghost(self):
        """Test adding the second parent id - as a ghost"""
        tree = self.make_branch_and_tree('.')
        first_revision = tree.commit('first post')
        tree.add_parent_tree(('second', None))
        self.assertConsistentParents([first_revision, 'second'], tree)


class UpdateToOneParentViaDeltaTests(TestCaseWithWorkingTree):
    """Tests for the update_basis_by_delta call.

    This is intuitively defined as 'apply an inventory delta to the basis and
    discard other parents', but for trees that have an inventory that is not
    managed as a tree-by-id, the implementation requires roughly duplicated
    tests with those for apply_inventory_delta on the main tree.
    """

    def assertDeltaApplicationResultsInExpectedBasis(self, tree, revid, delta,
        expected_inventory):
        tree.lock_write()
        try:
            tree.update_basis_by_delta(revid, delta)
        finally:
            tree.unlock()
        # check the last revision was adjusted to rev_id
        self.assertEqual(revid, tree.last_revision())
        # check the parents are what we expect
        self.assertEqual([revid], tree.get_parent_ids())
        # check that the basis tree has the inventory we expect from applying
        # the delta.
        result_basis = tree.basis_tree()
        result_basis.lock_read()
        try:
            self.assertEqual(expected_inventory, result_basis.inventory)
        finally:
            result_basis.unlock()

    def make_inv_delta(self, old, new):
        """Make an inventory delta from two inventories."""
        old_ids = set(old._byid.iterkeys())
        new_ids = set(new._byid.iterkeys())
        adds = new_ids - old_ids
        deletes = old_ids - new_ids
        common = old_ids.intersection(new_ids)
        delta = []
        for file_id in deletes:
            delta.append((old.id2path(file_id), None, file_id, None))
        for file_id in adds:
            delta.append((None, new.id2path(file_id), file_id, new[file_id]))
        for file_id in common:
            if old[file_id] != new[file_id]:
                delta.append((old.id2path(file_id), new.id2path(file_id),
                    file_id, new[file_id]))
        return delta

    def fake_up_revision(self, tree, revid, shape):
        tree.lock_write()
        try:
            tree.branch.repository.start_write_group()
            try:
                if shape.root.revision is None:
                    shape.root.revision = revid
                # Create the text records for this inventory.
                for path, ie in shape.iter_entries():
                    if ie.text_size:
                        lines = ['a' * ie.text_size]
                    else:
                        lines = []
                    tree.branch.repository.texts.add_lines(
                        (ie.file_id, ie.revision), [], lines)
                sha1 = tree.branch.repository.add_inventory(revid, shape, [])
                rev = Revision(timestamp=0,
                               timezone=None,
                               committer="Foo Bar <foo@example.com>",
                               message="Message",
                               inventory_sha1=sha1,
                               revision_id=revid)
                tree.branch.repository.add_revision(revid, rev)
                tree.branch.repository.commit_write_group()
            except:
                tree.branch.repository.abort_write_group()
                raise
        finally:
            tree.unlock()

    def add_entry(self, inv, rev_id, entry):
        entry.revision = rev_id
        inv.add(entry)

    def add_dir(self, inv, rev_id, file_id, parent_id, name):
        new_dir = InventoryDirectory(file_id, name, parent_id)
        self.add_entry(inv, rev_id, new_dir)

    def add_file(self, inv, rev_id, file_id, parent_id, name, sha, size):
        new_file = InventoryFile(file_id, name, parent_id)
        new_file.text_sha1 = sha
        new_file.text_size = size
        self.add_entry(inv, rev_id, new_file)

    def add_link(self, inv, rev_id, file_id, parent_id, name, target):
        new_link = InventoryLink(file_id, name, parent_id)
        new_link.symlink_target = target
        self.add_entry(inv, rev_id, new_link)

    def add_new_root(self, new_shape, old_revid, new_revid):
        if self.bzrdir_format.repository_format.rich_root_data:
            self.add_dir(new_shape, old_revid, 'root-id', None, '')
        else:
            self.add_dir(new_shape, new_revid, 'root-id', None, '')

    def assertTransitionFromBasisToShape(self, basis_shape, basis_revid,
        new_shape, new_revid, extra_parent=None):
        # set the inventory revision ids.
        basis_shape.revision_id = basis_revid
        new_shape.revision_id = new_revid
        delta = self.make_inv_delta(basis_shape, new_shape)
        tree = self.make_branch_and_tree('tree')
        # the shapes need to be in the tree's repository to be able to set them
        # as a parent, but the file content is not needed.
        if basis_revid is not None:
            self.fake_up_revision(tree, basis_revid, basis_shape)
            parents = [basis_revid]
            if extra_parent is not None:
                parents.append(extra_parent)
            tree.set_parent_ids(parents)
        self.fake_up_revision(tree, new_revid, new_shape)
        # give tree an inventory of new_shape
        tree._write_inventory(new_shape)
        self.assertDeltaApplicationResultsInExpectedBasis(tree, new_revid,
            delta, new_shape)
        # The tree should be internally consistent; while this is a moderately
        # large hammer, this is a particularly sensitive area of code, so the
        # extra assurance is well worth it.
        tree._validate()
        osutils.rmtree('tree')

    def test_no_parents_just_root(self):
        """Test doing an empty commit - no parent, set a root only."""
        basis_shape = Inventory(root_id=None) # empty tree
        new_shape = Inventory() # tree with a root
        self.assertTransitionFromBasisToShape(basis_shape, None, new_shape,
            'new_parent')

    def test_no_parents_full_tree(self):
        """Test doing a regular initial commit with files and dirs."""
        basis_shape = Inventory(root_id=None) # empty tree
        revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_dir(new_shape, revid, 'root-id', None, '')
        self.add_link(new_shape, revid, 'link-id', 'root-id', 'link', 'target')
        self.add_file(new_shape, revid, 'file-id', 'root-id', 'file', '1' * 32,
            12)
        self.add_dir(new_shape, revid, 'dir-id', 'root-id', 'dir')
        self.add_file(new_shape, revid, 'subfile-id', 'dir-id', 'subfile',
            '2' * 32, 24)
        self.assertTransitionFromBasisToShape(basis_shape, None, new_shape,
            revid)

    def test_file_content_change(self):
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_file(basis_shape, old_revid, 'file-id', 'root-id', 'file',
            '1' * 32, 12)
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        self.add_file(new_shape, new_revid, 'file-id', 'root-id', 'file',
            '2' * 32, 24)
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_link_content_change(self):
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_link(basis_shape, old_revid, 'link-id', 'root-id', 'link',
            'old-target')
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        self.add_link(new_shape, new_revid, 'link-id', 'root-id', 'link',
            'new-target')
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_kind_changes(self):
        def do_file(inv, revid):
            self.add_file(inv, revid, 'path-id', 'root-id', 'path', '1' * 32,
                12)
        def do_link(inv, revid):
            self.add_link(inv, revid, 'path-id', 'root-id', 'path', 'target')
        def do_dir(inv, revid):
            self.add_dir(inv, revid, 'path-id', 'root-id', 'path')
        for old_factory in (do_file, do_link, do_dir):
            for new_factory in (do_file, do_link, do_dir):
                if old_factory == new_factory:
                    continue
                old_revid = 'old-parent'
                basis_shape = Inventory(root_id=None)
                self.add_dir(basis_shape, old_revid, 'root-id', None, '')
                old_factory(basis_shape, old_revid)
                new_revid = 'new-parent'
                new_shape = Inventory(root_id=None)
                self.add_new_root(new_shape, old_revid, new_revid)
                new_factory(new_shape, new_revid)
                self.assertTransitionFromBasisToShape(basis_shape, old_revid,
                    new_shape, new_revid)

    def test_content_from_second_parent_is_dropped(self):
        left_revid = 'left-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, left_revid, 'root-id', None, '')
        self.add_link(basis_shape, left_revid, 'link-id', 'root-id', 'link',
            'left-target')
        # the right shape has content - file, link, subdir with a child,
        # that should all be discarded by the call.
        right_revid = 'right-parent'
        right_shape = Inventory(root_id=None)
        self.add_dir(right_shape, left_revid, 'root-id', None, '')
        self.add_link(right_shape, right_revid, 'link-id', 'root-id', 'link',
            'some-target')
        self.add_dir(right_shape, right_revid, 'subdir-id', 'root-id', 'dir')
        self.add_file(right_shape, right_revid, 'file-id', 'subdir-id', 'file',
            '2' * 32, 24)
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, left_revid, new_revid)
        self.add_link(new_shape, new_revid, 'link-id', 'root-id', 'link',
            'new-target')
        self.assertTransitionFromBasisToShape(basis_shape, left_revid,
            new_shape, new_revid, right_revid)

    def test_parent_id_changed(self):
        # test that when the only change to an entry is its parent id changing
        # that it is handled correctly (that is it keeps the same path)
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_dir(basis_shape, old_revid, 'orig-parent-id', 'root-id', 'dir')
        self.add_dir(basis_shape, old_revid, 'dir-id', 'orig-parent-id', 'dir')
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        self.add_dir(new_shape, new_revid, 'new-parent-id', 'root-id', 'dir')
        self.add_dir(new_shape, new_revid, 'dir-id', 'new-parent-id', 'dir')
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_name_changed(self):
        # test that when the only change to an entry is its name changing that
        # it is handled correctly (that is it keeps the same parent id)
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_dir(basis_shape, old_revid, 'parent-id', 'root-id', 'origdir')
        self.add_dir(basis_shape, old_revid, 'dir-id', 'parent-id', 'olddir')
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        self.add_dir(new_shape, new_revid, 'parent-id', 'root-id', 'newdir')
        self.add_dir(new_shape, new_revid, 'dir-id', 'parent-id', 'newdir')
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_parent_child_swap(self):
        # test a A->A/B and A/B->A path swap.
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_dir(basis_shape, old_revid, 'dir-id-A', 'root-id', 'A')
        self.add_dir(basis_shape, old_revid, 'dir-id-B', 'dir-id-A', 'B')
        self.add_link(basis_shape, old_revid, 'link-id-C', 'dir-id-B', 'C', 'C')
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        self.add_dir(new_shape, new_revid, 'dir-id-B', 'root-id', 'A')
        self.add_dir(new_shape, new_revid, 'dir-id-A', 'dir-id-B', 'B')
        self.add_link(new_shape, new_revid, 'link-id-C', 'dir-id-A', 'C', 'C')
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_parent_deleted_child_renamed(self):
        # test a A->None and A/B->A.
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_dir(basis_shape, old_revid, 'dir-id-A', 'root-id', 'A')
        self.add_dir(basis_shape, old_revid, 'dir-id-B', 'dir-id-A', 'B')
        self.add_link(basis_shape, old_revid, 'link-id-C', 'dir-id-B', 'C', 'C')
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        self.add_dir(new_shape, new_revid, 'dir-id-B', 'root-id', 'A')
        self.add_link(new_shape, old_revid, 'link-id-C', 'dir-id-B', 'C', 'C')
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_dir_to_root(self):
        # test a A->''.
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_dir(basis_shape, old_revid, 'dir-id-A', 'root-id', 'A')
        self.add_link(basis_shape, old_revid, 'link-id-B', 'dir-id-A', 'B', 'B')
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_dir(new_shape, new_revid, 'dir-id-A', None, '')
        self.add_link(new_shape, old_revid, 'link-id-B', 'dir-id-A', 'B', 'B')
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_path_swap(self):
        # test a A->B and B->A path swap.
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_dir(basis_shape, old_revid, 'dir-id-A', 'root-id', 'A')
        self.add_dir(basis_shape, old_revid, 'dir-id-B', 'root-id', 'B')
        self.add_link(basis_shape, old_revid, 'link-id-C', 'root-id', 'C', 'C')
        self.add_link(basis_shape, old_revid, 'link-id-D', 'root-id', 'D', 'D')
        self.add_file(basis_shape, old_revid, 'file-id-E', 'root-id', 'E',
            '1' * 32, 12)
        self.add_file(basis_shape, old_revid, 'file-id-F', 'root-id', 'F',
            '2' * 32, 24)
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        self.add_dir(new_shape, new_revid, 'dir-id-A', 'root-id', 'B')
        self.add_dir(new_shape, new_revid, 'dir-id-B', 'root-id', 'A')
        self.add_link(new_shape, new_revid, 'link-id-C', 'root-id', 'D', 'C')
        self.add_link(new_shape, new_revid, 'link-id-D', 'root-id', 'C', 'D')
        self.add_file(new_shape, new_revid, 'file-id-E', 'root-id', 'F',
            '1' * 32, 12)
        self.add_file(new_shape, new_revid, 'file-id-F', 'root-id', 'E',
            '2' * 32, 24)
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_adds(self):
        # test adding paths and dirs, including adding to a newly added dir.
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        # with a root, so its a commit after the first.
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        self.add_dir(new_shape, new_revid, 'dir-id-A', 'root-id', 'A')
        self.add_link(new_shape, new_revid, 'link-id-B', 'root-id', 'B', 'C')
        self.add_file(new_shape, new_revid, 'file-id-C', 'root-id', 'C',
            '1' * 32, 12)
        self.add_file(new_shape, new_revid, 'file-id-D', 'dir-id-A', 'D',
            '2' * 32, 24)
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_removes(self):
        # test removing paths, including paths that are within other also
        # removed paths.
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_dir(basis_shape, old_revid, 'dir-id-A', 'root-id', 'A')
        self.add_link(basis_shape, old_revid, 'link-id-B', 'root-id', 'B', 'C')
        self.add_file(basis_shape, old_revid, 'file-id-C', 'root-id', 'C',
            '1' * 32, 12)
        self.add_file(basis_shape, old_revid, 'file-id-D', 'dir-id-A', 'D',
            '2' * 32, 24)
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_move_to_added_dir(self):
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_link(basis_shape, old_revid, 'link-id-B', 'root-id', 'B', 'C')
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        self.add_dir(new_shape, new_revid, 'dir-id-A', 'root-id', 'A')
        self.add_link(new_shape, new_revid, 'link-id-B', 'dir-id-A', 'B', 'C')
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_move_from_removed_dir(self):
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_dir(basis_shape, old_revid, 'dir-id-A', 'root-id', 'A')
        self.add_link(basis_shape, old_revid, 'link-id-B', 'dir-id-A', 'B', 'C')
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        self.add_link(new_shape, new_revid, 'link-id-B', 'root-id', 'B', 'C')
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)

    def test_move_moves_children_recursively(self):
        old_revid = 'old-parent'
        basis_shape = Inventory(root_id=None)
        self.add_dir(basis_shape, old_revid, 'root-id', None, '')
        self.add_dir(basis_shape, old_revid, 'dir-id-A', 'root-id', 'A')
        self.add_dir(basis_shape, old_revid, 'dir-id-B', 'dir-id-A', 'B')
        self.add_link(basis_shape, old_revid, 'link-id-C', 'dir-id-B', 'C', 'D')
        new_revid = 'new-parent'
        new_shape = Inventory(root_id=None)
        self.add_new_root(new_shape, old_revid, new_revid)
        # the moved path:
        self.add_dir(new_shape, new_revid, 'dir-id-A', 'root-id', 'B')
        # unmoved children.
        self.add_dir(new_shape, old_revid, 'dir-id-B', 'dir-id-A', 'B')
        self.add_link(new_shape, old_revid, 'link-id-C', 'dir-id-B', 'C', 'D')
        self.assertTransitionFromBasisToShape(basis_shape, old_revid,
            new_shape, new_revid)
