Rev 2795: (robertc) Deprecated find_previous_heads and add Graph.heads detangling some of the inventory related commit code. (Robert Collins) in file:///home/pqm/archives/thelove/bzr/%2Btrunk/

Canonical.com Patch Queue Manager pqm at pqm.ubuntu.com
Wed Sep 5 01:17:01 BST 2007


At file:///home/pqm/archives/thelove/bzr/%2Btrunk/

------------------------------------------------------------
revno: 2795
revision-id: pqm at pqm.ubuntu.com-20070905001648-0iigag4tq1u8mywn
parent: pqm at pqm.ubuntu.com-20070904035759-iv4xl6d7ez69txba
parent: robertc at robertcollins.net-20070904041207-zb3l8hfco0sp6hu8
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Wed 2007-09-05 01:16:48 +0100
message:
  (robertc) Deprecated find_previous_heads and add Graph.heads detangling some of the inventory related commit code. (Robert Collins)
modified:
  NEWS                           NEWS-20050323055033-4e00b5db738777ff
  bzrlib/bzrdir.py               bzrdir.py-20060131065624-156dfea39c4387cb
  bzrlib/graph.py                graph_walker.py-20070525030359-y852guab65d4wtn0-1
  bzrlib/inventory.py            inventory.py-20050309040759-6648b84ca2005b37
  bzrlib/repository.py           rev_storage.py-20051111201905-119e9401e46257e3
  bzrlib/tests/__init__.py       selftest.py-20050531073622-8d0e3c8845c97a64
  bzrlib/tests/repository_implementations/test_commit_builder.py test_commit_builder.py-20060606110838-76e3ra5slucqus81-1
  bzrlib/tests/test_graph.py     test_graph_walker.py-20070525030405-enq4r60hhi9xrujc-1
  bzrlib/tests/tree_implementations/test_inv.py test_inv.py-20070312023226-0cdvk5uwhutis9vg-1
    ------------------------------------------------------------
    revno: 2776.1.6
    merged: robertc at robertcollins.net-20070904041207-zb3l8hfco0sp6hu8
    parent: robertc at robertcollins.net-20070904035307-y5jacs7zl15v2nvo
    parent: pqm at pqm.ubuntu.com-20070904035759-iv4xl6d7ez69txba
    committer: Robert Collins <robertc at robertcollins.net>
    branch nick: commit
    timestamp: Tue 2007-09-04 14:12:07 +1000
    message:
      Merge bzr.dev.
    ------------------------------------------------------------
    revno: 2776.1.5
    merged: robertc at robertcollins.net-20070904035307-y5jacs7zl15v2nvo
    parent: robertc at robertcollins.net-20070903221720-9k1y25x0x60rk38b
    committer: Robert Collins <robertc at robertcollins.net>
    branch nick: commit
    timestamp: Tue 2007-09-04 13:53:07 +1000
    message:
      Add reasonably comprehensive tests for path last modified and per file graph behaviour.
    ------------------------------------------------------------
    revno: 2776.1.4
    merged: robertc at robertcollins.net-20070903221720-9k1y25x0x60rk38b
    parent: robertc at robertcollins.net-20070903202305-yj4obhvvw3j7t4jc
    parent: robertc at robertcollins.net-20070903015712-oo8vn04vryo75xjb
    committer: Robert Collins <robertc at robertcollins.net>
    branch nick: commit
    timestamp: Tue 2007-09-04 08:17:20 +1000
    message:
      Trivial review feedback changes.
    ------------------------------------------------------------
    revno: 2776.3.1
    merged: robertc at robertcollins.net-20070903015712-oo8vn04vryo75xjb
    parent: pqm at pqm.ubuntu.com-20070901160444-hcr66zejwyy0jezc
    committer: Robert Collins <robertc at robertcollins.net>
    branch nick: commit
    timestamp: Mon 2007-09-03 11:57:12 +1000
    message:
      * Deprecated method ``find_previous_heads`` on
        ``bzrlib.inventory.InventoryEntry``. This has been superceded by the use
        of ``parent_candidates`` and a separate heads check via the repository
        API. (Robert Collins)
      
      * New public method ``heads`` on the ``bzrlib.graph.Graph`` class. This is
        a simple rename of a previously internal method which implements the same
        semantics as heads(). (Robert Collins, John A Meinel)
=== modified file 'NEWS'
--- a/NEWS	2007-09-04 01:20:26 +0000
+++ b/NEWS	2007-09-04 04:12:07 +0000
@@ -204,6 +204,11 @@
      This is done in order to mix the commit messages (which is a unicode
      string), and the diff which is a raw string. (Goffredo Baroncelli)
 
+    * Deprecated method ``find_previous_heads`` on
+      ``bzrlib.inventory.InventoryEntry``. This has been superseded by the use
+      of ``parent_candidates`` and a separate heads check via the repository
+      API. (Robert Collins)
+
    * New trace function ``mutter_callsite`` will print out a subset of the
      stack to the log, which can be useful for gathering debug details.
      (Robert Collins)

=== modified file 'bzrlib/bzrdir.py'
--- a/bzrlib/bzrdir.py	2007-08-20 05:48:40 +0000
+++ b/bzrlib/bzrdir.py	2007-09-04 03:53:07 +0000
@@ -35,6 +35,7 @@
 import bzrlib
 from bzrlib import (
     errors,
+    graph,
     lockable_files,
     lockdir,
     registry,
@@ -1934,18 +1935,24 @@
             w = Weave(file_id)
             self.text_weaves[file_id] = w
         text_changed = False
-        previous_entries = ie.find_previous_heads(parent_invs,
-                                                  None,
-                                                  None,
-                                                  entry_vf=w)
-        for old_revision in previous_entries:
-                # if this fails, its a ghost ?
-                assert old_revision in self.converted_revs, \
-                    "Revision {%s} not in converted_revs" % old_revision
+        parent_candiate_entries = ie.parent_candidates(parent_invs)
+        for old_revision in parent_candiate_entries.keys():
+            # if this fails, its a ghost ?
+            assert old_revision in self.converted_revs, \
+                "Revision {%s} not in converted_revs" % old_revision
+        heads = graph.Graph(self).heads(parent_candiate_entries.keys())
+        # XXX: Note that this is unordered - and this is tolerable because 
+        # the previous code was also unordered.
+        previous_entries = dict((head, parent_candiate_entries[head]) for head
+            in heads)
         self.snapshot_ie(previous_entries, ie, w, rev_id)
         del ie.text_id
         assert getattr(ie, 'revision', None) is not None
 
+    def get_parents(self, revision_ids):
+        for revision_id in revision_ids:
+            yield self.revisions[revision_id].parent_ids
+
     def snapshot_ie(self, previous_revisions, ie, w, rev_id):
         # TODO: convert this logic, which is ~= snapshot to
         # a call to:. This needs the path figured out. rather than a work_tree

=== modified file 'bzrlib/graph.py'
--- a/bzrlib/graph.py	2007-08-07 15:26:26 +0000
+++ b/bzrlib/graph.py	2007-09-03 22:17:20 +0000
@@ -134,7 +134,10 @@
            ancestor of all border ancestors.
         """
         border_common, common, sides = self._find_border_ancestors(revisions)
-        return self._filter_candidate_lca(border_common)
+        # We may have common ancestors that can be reached from each other.
+        # - ask for the heads of them to filter it down to only ones that
+        # cannot be reached from each other - phase 2.
+        return self.heads(border_common)
 
     def find_difference(self, left_revision, right_revision):
         """Determine the graph difference between two revisions"""
@@ -212,23 +215,24 @@
                     for searcher in searchers:
                         update_common(searcher, revision)
 
-    def _filter_candidate_lca(self, candidate_lca):
-        """Remove candidates which are ancestors of other candidates.
-
-        This is done by searching the ancestries of each border ancestor.  It
-        is perfomed on the principle that a border ancestor that is not an
-        ancestor of any other border ancestor is a lowest common ancestor.
-
-        Searches are stopped when they find a node that is determined to be a
-        common ancestor of all border ancestors, because this shows that it
-        cannot be a descendant of any border ancestor.
-
-        This will scale with the number of candidate ancestors and the length
-        of the shortest path from a candidate to an ancestor common to all
-        candidates.
+    def heads(self, keys):
+        """Return the heads from amongst keys.
+
+        This is done by searching the ancestries of each key.  Any key that is
+        reachable from another key is not returned; all the others are.
+
+        This operation scales with the relative depth between any two keys. If
+        any two keys are completely disconnected all ancestry of both sides
+        will be retrieved.
+
+        :param keys: An iterable of keys.
+        :return: A set of the heads. Note that as a set there is no ordering
+            information. Callers will need to filter their input to create
+            order if they need it.
         """
+        candidate_heads = set(keys)
         searchers = dict((c, self._make_breadth_first_searcher([c]))
-                          for c in candidate_lca)
+                          for c in candidate_heads)
         active_searchers = dict(searchers)
         # skip over the actual candidate for each searcher
         for searcher in active_searchers.itervalues():
@@ -248,8 +252,8 @@
                     del active_searchers[candidate]
                     continue
                 for ancestor in ancestors:
-                    if ancestor in candidate_lca:
-                        candidate_lca.remove(ancestor)
+                    if ancestor in candidate_heads:
+                        candidate_heads.remove(ancestor)
                         del searchers[ancestor]
                         if ancestor in active_searchers:
                             del active_searchers[ancestor]
@@ -264,7 +268,7 @@
                             seen_ancestors =\
                                 searcher.find_seen_ancestors(ancestor)
                             searcher.stop_searching_any(seen_ancestors)
-        return candidate_lca
+        return candidate_heads
 
     def find_unique_lca(self, left_revision, right_revision):
         """Find a unique LCA.

=== modified file 'bzrlib/inventory.py'
--- a/bzrlib/inventory.py	2007-08-31 05:54:21 +0000
+++ b/bzrlib/inventory.py	2007-09-04 03:53:07 +0000
@@ -50,6 +50,7 @@
     BzrCheckError,
     BzrError,
     )
+from bzrlib.symbol_versioning import deprecated_method, zero_ninetyone
 from bzrlib.trace import mutter
 
 
@@ -161,41 +162,23 @@
     def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
              output_to, reverse=False):
         """Perform a diff between two entries of the same kind."""
-
-    def find_previous_heads(self, previous_inventories,
-                            versioned_file_store,
-                            transaction,
-                            entry_vf=None):
-        """Return the revisions and entries that directly precede this.
-
-        Returned as a map from revision to inventory entry.
-
-        This is a map containing the file revisions in all parents
-        for which the file exists, and its revision is not a parent of
-        any other. If the file is new, the set will be empty.
-
-        :param versioned_file_store: A store where ancestry data on this
-                                     file id can be queried.
-        :param transaction: The transaction that queries to the versioned 
-                            file store should be completed under.
-        :param entry_vf: The entry versioned file, if its already available.
+    
+    def parent_candidates(self, previous_inventories):
+        """Find possible per-file graph parents.
+
+        This is currently defined by:
+         - Select the last changed revision in the parent inventory.
+         - Do deal with a short lived bug in bzr 0.8's development two entries
+           that have the same last changed but different 'x' bit settings are
+           changed in-place.
         """
-        def get_ancestors(weave, entry):
-            return set(weave.get_ancestry(entry.revision, topo_sorted=False))
         # revision:ie mapping for each ie found in previous_inventories.
         candidates = {}
-        # revision:ie mapping with one revision for each head.
-        heads = {}
-        # revision: ancestor list for each head
-        head_ancestors = {}
         # identify candidate head revision ids.
         for inv in previous_inventories:
             if self.file_id in inv:
                 ie = inv[self.file_id]
                 assert ie.file_id == self.file_id
-                if ie.kind != self.kind:
-                    # Can't be a candidate if the kind has changed.
-                    continue
                 if ie.revision in candidates:
                     # same revision value in two different inventories:
                     # correct possible inconsistencies:
@@ -212,19 +195,52 @@
                 else:
                     # add this revision as a candidate.
                     candidates[ie.revision] = ie
-
+        return candidates
+
+    @deprecated_method(zero_ninetyone)
+    def find_previous_heads(self, previous_inventories,
+                            versioned_file_store,
+                            transaction,
+                            entry_vf=None):
+        """Return the revisions and entries that directly precede this.
+
+        Returned as a map from revision to inventory entry.
+
+        This is a map containing the file revisions in all parents
+        for which the file exists, and its revision is not a parent of
+        any other. If the file is new, the set will be empty.
+
+        :param versioned_file_store: A store where ancestry data on this
+                                     file id can be queried.
+        :param transaction: The transaction that queries to the versioned 
+                            file store should be completed under.
+        :param entry_vf: The entry versioned file, if its already available.
+        """
+        candidates = self.parent_candidates(previous_inventories)
+
+        # revision:ie mapping with one revision for each head.
+        heads = {}
         # common case optimisation
         if len(candidates) == 1:
             # if there is only one candidate revision found
-            # then we can opening the versioned file to access ancestry:
+            # then we can avoid opening the versioned file to access ancestry:
             # there cannot be any ancestors to eliminate when there is 
             # only one revision available.
-            heads[ie.revision] = ie
-            return heads
+            return candidates
+        
+        # --- what follows is now encapsulated in repository.get_graph.heads(), 
+        #     but that is not accessible from here as we have no repository
+        #     pointer. Note that the repository.get_graph.heads() call can return
+        #     different results *at the moment* because of the kind-changing check
+        #     we have in parent_candidates().
 
         # eliminate ancestors amongst the available candidates:
         # heads are those that are not an ancestor of any other candidate
         # - this provides convergence at a per-file level.
+        def get_ancestors(weave, entry):
+            return set(weave.get_ancestry(entry.revision, topo_sorted=False))
+        # revision: ancestor list for each head
+        head_ancestors = {}
         for ie in candidates.values():
             # may be an ancestor of a known head:
             already_present = 0 != len(

=== modified file 'bzrlib/repository.py'
--- a/bzrlib/repository.py	2007-09-03 03:03:26 +0000
+++ b/bzrlib/repository.py	2007-09-03 22:17:20 +0000
@@ -2184,44 +2184,57 @@
         if self._new_revision_id is None:
             self._new_revision_id = self._gen_revision_id()
 
-    def record_entry_contents(self, ie, parent_invs, path, tree):
-        """Record the content of ie from tree into the commit if needed.
-
-        Side effect: sets ie.revision when unchanged
-
-        :param ie: An inventory entry present in the commit.
+    def _check_root(self, ie, parent_invs, tree):
+        """Helper for record_entry_contents.
+
+        :param ie: An entry being added.
         :param parent_invs: The inventories of the parent revisions of the
             commit.
-        :param path: The path the entry is at in the tree.
-        :param tree: The tree which contains this entry and should be used to 
-        obtain content.
+        :param tree: The tree that is being committed.
         """
-        if self.new_inventory.root is None and ie.parent_id is not None:
+        if ie.parent_id is not None:
+            # if ie is not root, add a root automatically.
             symbol_versioning.warn('Root entry should be supplied to'
                 ' record_entry_contents, as of bzr 0.10.',
                  DeprecationWarning, stacklevel=2)
             self.record_entry_contents(tree.inventory.root.copy(), parent_invs,
                                        '', tree)
-        self.new_inventory.add(ie)
-
-        # ie.revision is always None if the InventoryEntry is considered
-        # for committing. ie.snapshot will record the correct revision 
-        # which may be the sole parent if it is untouched.
-        if ie.revision is not None:
-            return
-
-        # In this revision format, root entries have no knit or weave
-        if ie is self.new_inventory.root:
-            # When serializing out to disk and back in
-            # root.revision is always _new_revision_id
+        else:
+            # In this revision format, root entries have no knit or weave When
+            # serializing out to disk and back in root.revision is always
+            # _new_revision_id
             ie.revision = self._new_revision_id
+
+    def record_entry_contents(self, ie, parent_invs, path, tree):
+        """Record the content of ie from tree into the commit if needed.
+
+        Side effect: sets ie.revision when unchanged
+
+        :param ie: An inventory entry present in the commit.
+        :param parent_invs: The inventories of the parent revisions of the
+            commit.
+        :param path: The path the entry is at in the tree.
+        :param tree: The tree which contains this entry and should be used to 
+        obtain content.
+        """
+        if self.new_inventory.root is None:
+            self._check_root(ie, parent_invs, tree)
+        self.new_inventory.add(ie)
+
+        # ie.revision is always None if the InventoryEntry is considered
+        # for committing. ie.snapshot will record the correct revision 
+        # which may be the sole parent if it is untouched.
+        if ie.revision is not None:
             return
-        previous_entries = ie.find_previous_heads(
-            parent_invs,
-            self.repository.weave_store,
-            self.repository.get_transaction())
-        # we are creating a new revision for ie in the history store
-        # and inventory.
+
+        parent_candiate_entries = ie.parent_candidates(parent_invs)
+        heads = self.repository.get_graph().heads(parent_candiate_entries.keys())
+        # XXX: Note that this is unordered - and this is tolerable because 
+        # the previous code was also unordered.
+        previous_entries = dict((head, parent_candiate_entries[head]) for head
+            in heads)
+        # we are creating a new revision for ie in the history store and
+        # inventory.
         ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
 
     def modified_directory(self, file_id, file_parents):
@@ -2302,34 +2315,16 @@
     
     record_root_entry = True
 
-    def record_entry_contents(self, ie, parent_invs, path, tree):
-        """Record the content of ie from tree into the commit if needed.
-
-        Side effect: sets ie.revision when unchanged
-
-        :param ie: An inventory entry present in the commit.
+    def _check_root(self, ie, parent_invs, tree):
+        """Helper for record_entry_contents.
+
+        :param ie: An entry being added.
         :param parent_invs: The inventories of the parent revisions of the
             commit.
-        :param path: The path the entry is at in the tree.
-        :param tree: The tree which contains this entry and should be used to 
-        obtain content.
+        :param tree: The tree that is being committed.
         """
-        assert self.new_inventory.root is not None or ie.parent_id is None
-        self.new_inventory.add(ie)
-
-        # ie.revision is always None if the InventoryEntry is considered
-        # for committing. ie.snapshot will record the correct revision 
-        # which may be the sole parent if it is untouched.
-        if ie.revision is not None:
-            return
-
-        previous_entries = ie.find_previous_heads(
-            parent_invs,
-            self.repository.weave_store,
-            self.repository.get_transaction())
-        # we are creating a new revision for ie in the history store
-        # and inventory.
-        ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
+        # ie must be root for this builder
+        assert ie.parent_id is None
 
 
 _unescape_map = {

=== modified file 'bzrlib/tests/__init__.py'
--- a/bzrlib/tests/__init__.py	2007-09-03 07:02:12 +0000
+++ b/bzrlib/tests/__init__.py	2007-09-04 04:12:07 +0000
@@ -2614,6 +2614,17 @@
         return self.__class__.__name__
 
 
+class _SymlinkFeature(Feature):
+
+    def _probe(self):
+        return osutils.has_symlinks()
+
+    def feature_name(self):
+        return 'symlinks'
+
+SymlinkFeature = _SymlinkFeature()
+
+
 class TestScenarioApplier(object):
     """A tool to apply scenarios to tests."""
 

=== modified file 'bzrlib/tests/repository_implementations/test_commit_builder.py'
--- a/bzrlib/tests/repository_implementations/test_commit_builder.py	2007-08-28 01:58:42 +0000
+++ b/bzrlib/tests/repository_implementations/test_commit_builder.py	2007-09-04 03:53:07 +0000
@@ -16,6 +16,9 @@
 
 """Tests for repository commit builder."""
 
+from errno import EISDIR
+import os
+
 from bzrlib import inventory
 from bzrlib.errors import NonAsciiRevisionId, CannotSetRevisionId
 from bzrlib.repository import CommitBuilder
@@ -173,3 +176,268 @@
         self.addCleanup(basis_tree.unlock)
         self.assertEqual(rev_id, basis_tree.inventory.root.revision)
 
+    def _get_revtrees(self, tree, revision_ids):
+        trees = list(tree.branch.repository.revision_trees(revision_ids))
+        for tree in trees:
+            tree.lock_read()
+            self.addCleanup(tree.unlock)
+        return trees
+
+    def test_last_modified_revision_after_commit_root_unchanged(self):
+        # commiting without changing the root does not change the 
+        # last modified except on non-rich-root-repositories.
+        tree = self.make_branch_and_tree('.')
+        rev1 = tree.commit('')
+        rev2 = tree.commit('')
+        tree1, tree2 = self._get_revtrees(tree, [rev1, rev2])
+        self.assertEqual(rev1, tree1.inventory.root.revision)
+        if tree.branch.repository.supports_rich_root():
+            self.assertEqual(rev1, tree2.inventory.root.revision)
+        else:
+            self.assertEqual(rev2, tree2.inventory.root.revision)
+
+    def _add_commit_check_unchanged(self, tree, name):
+        tree.add([name], [name + 'id'])
+        rev1 = tree.commit('')
+        rev2 = tree.commit('')
+        tree1, tree2 = self._get_revtrees(tree, [rev1, rev2])
+        self.assertEqual(rev1, tree1.inventory[name + 'id'].revision)
+        self.assertEqual(rev1, tree2.inventory[name + 'id'].revision)
+        self.assertFileAncestry([rev1], tree, name)
+
+    def test_last_modified_revision_after_commit_dir_unchanged(self):
+        # committing without changing a dir does not change the last modified.
+        tree = self.make_branch_and_tree('.')
+        self.build_tree(['dir/'])
+        self._add_commit_check_unchanged(tree, 'dir')
+
+    def test_last_modified_revision_after_commit_dir_contents_unchanged(self):
+        # committing without changing a dir does not change the last modified
+        # of the dir even the dirs contents are changed.
+        tree = self.make_branch_and_tree('.')
+        self.build_tree(['dir/'])
+        tree.add(['dir'], ['dirid'])
+        rev1 = tree.commit('')
+        self.build_tree(['dir/content'])
+        tree.add(['dir/content'], ['contentid'])
+        rev2 = tree.commit('')
+        tree1, tree2 = self._get_revtrees(tree, [rev1, rev2])
+        self.assertEqual(rev1, tree1.inventory['dirid'].revision)
+        self.assertEqual(rev1, tree2.inventory['dirid'].revision)
+        self.assertFileAncestry([rev1], tree, 'dir')
+
+    def test_last_modified_revision_after_commit_file_unchanged(self):
+        # committing without changing a file does not change the last modified.
+        tree = self.make_branch_and_tree('.')
+        self.build_tree(['file'])
+        self._add_commit_check_unchanged(tree, 'file')
+
+    def test_last_modified_revision_after_commit_link_unchanged(self):
+        # committing without changing a link does not change the last modified.
+        self.requireFeature(tests.SymlinkFeature)
+        tree = self.make_branch_and_tree('.')
+        os.symlink('target', 'link')
+        self._add_commit_check_unchanged(tree, 'link')
+
+    def _add_commit_renamed_check_changed(self, tree, name):
+        def rename():
+            tree.rename_one(name, 'new_' + name)
+        self._add_commit_change_check_changed(tree, name, rename)
+
+    def test_last_modified_revision_after_rename_dir_changes(self):
+        # renaming a dir changes the last modified.
+        tree = self.make_branch_and_tree('.')
+        self.build_tree(['dir/'])
+        self._add_commit_renamed_check_changed(tree, 'dir')
+
+    def test_last_modified_revision_after_rename_file_changes(self):
+        # renaming a file changes the last modified.
+        tree = self.make_branch_and_tree('.')
+        self.build_tree(['file'])
+        self._add_commit_renamed_check_changed(tree, 'file')
+
+    def test_last_modified_revision_after_rename_link_changes(self):
+        # renaming a link changes the last modified.
+        self.requireFeature(tests.SymlinkFeature)
+        tree = self.make_branch_and_tree('.')
+        os.symlink('target', 'link')
+        self._add_commit_renamed_check_changed(tree, 'link')
+
+    def _add_commit_reparent_check_changed(self, tree, name):
+        self.build_tree(['newparent/'])
+        tree.add(['newparent'])
+        def reparent():
+            tree.rename_one(name, 'newparent/new_' + name)
+        self._add_commit_change_check_changed(tree, name, reparent)
+
+    def test_last_modified_revision_after_reparent_dir_changes(self):
+        # reparenting a dir changes the last modified.
+        tree = self.make_branch_and_tree('.')
+        self.build_tree(['dir/'])
+        self._add_commit_reparent_check_changed(tree, 'dir')
+
+    def test_last_modified_revision_after_reparent_file_changes(self):
+        # reparenting a file changes the last modified.
+        tree = self.make_branch_and_tree('.')
+        self.build_tree(['file'])
+        self._add_commit_reparent_check_changed(tree, 'file')
+
+    def test_last_modified_revision_after_reparent_link_changes(self):
+        # reparenting a link changes the last modified.
+        self.requireFeature(tests.SymlinkFeature)
+        tree = self.make_branch_and_tree('.')
+        os.symlink('target', 'link')
+        self._add_commit_reparent_check_changed(tree, 'link')
+
+    def _add_commit_change_check_changed(self, tree, name, changer):
+        tree.add([name], [name + 'id'])
+        rev1 = tree.commit('')
+        changer()
+        rev2 = tree.commit('')
+        tree1, tree2 = self._get_revtrees(tree, [rev1, rev2])
+        self.assertEqual(rev1, tree1.inventory[name + 'id'].revision)
+        self.assertEqual(rev2, tree2.inventory[name + 'id'].revision)
+        self.assertFileAncestry([rev1, rev2], tree, name)
+
+    def assertFileAncestry(self, ancestry, tree, name, alt_ancestry=None):
+        # all the changes that have occured should be in the ancestry
+        # (closest to a public per-file graph API we have today)
+        tree.lock_read()
+        self.addCleanup(tree.unlock)
+        vw = tree.branch.repository.weave_store.get_weave(name + 'id',
+            tree.branch.repository.get_transaction())
+        result = vw.get_ancestry([ancestry[-1]])
+        if alt_ancestry is None:
+            self.assertEqual(ancestry, result)
+        else:
+            self.assertSubset([tuple(result)],
+                [tuple(ancestry), tuple(alt_ancestry)])
+
+    def test_last_modified_revision_after_content_file_changes(self):
+        # altering a file changes the last modified.
+        tree = self.make_branch_and_tree('.')
+        self.build_tree(['file'])
+        def change_file():
+            tree.put_file_bytes_non_atomic('fileid', 'new content')
+        self._add_commit_change_check_changed(tree, 'file', change_file)
+
+    def test_last_modified_revision_after_content_link_changes(self):
+        # changing a link changes the last modified.
+        self.requireFeature(tests.SymlinkFeature)
+        tree = self.make_branch_and_tree('.')
+        os.symlink('target', 'link')
+        def change_link():
+            os.unlink('link')
+            os.symlink('newtarget', 'link')
+        self._add_commit_change_check_changed(tree, 'link', change_link)
+
+    def _commit_sprout(self, tree, name):
+        tree.add([name], [name + 'id'])
+        rev_id = tree.commit('')
+        return rev_id, tree.bzrdir.sprout('t2').open_workingtree()
+
+    def _rename_in_tree(self, tree, name):
+        tree.rename_one(name, 'new_' + name)
+        return tree.commit('')
+
+    def _commit_sprout_rename_merge(self, tree1, name):
+        rev1, tree2 = self._commit_sprout(tree1, name)
+        # change both sides equally
+        rev2 = self._rename_in_tree(tree1, name)
+        rev3 = self._rename_in_tree(tree2, name)
+        tree1.merge_from_branch(tree2.branch)
+        rev4 = tree1.commit('')
+        tree3, = self._get_revtrees(tree1, [rev4])
+        self.assertEqual(rev4, tree3.inventory[name + 'id'].revision)
+        self.assertFileAncestry([rev1, rev2, rev3, rev4], tree1, name,
+            [rev1, rev3, rev2, rev4])
+
+    def test_last_modified_revision_after_merge_dir_changes(self):
+        # merge a dir changes the last modified.
+        tree1 = self.make_branch_and_tree('t1')
+        self.build_tree(['t1/dir/'])
+        self._commit_sprout_rename_merge(tree1, 'dir')
+
+    def test_last_modified_revision_after_merge_file_changes(self):
+        # merge a file changes the last modified.
+        tree1 = self.make_branch_and_tree('t1')
+        self.build_tree(['t1/file'])
+        self._commit_sprout_rename_merge(tree1, 'file')
+
+    def test_last_modified_revision_after_merge_link_changes(self):
+        # merge a link changes the last modified.
+        self.requireFeature(tests.SymlinkFeature)
+        tree1 = self.make_branch_and_tree('t1')
+        os.symlink('target', 't1/link')
+        self._commit_sprout_rename_merge(tree1, 'link')
+
+    def _commit_sprout_rename_merge_converged(self, tree1, name):
+        rev1, tree2 = self._commit_sprout(tree1, name)
+        # change on the other side to merge back
+        rev2 = self._rename_in_tree(tree2, name)
+        tree1.merge_from_branch(tree2.branch)
+        rev3 = tree1.commit('')
+        tree3, = self._get_revtrees(tree1, [rev2])
+        self.assertEqual(rev2, tree3.inventory[name + 'id'].revision)
+        self.assertFileAncestry([rev1, rev2], tree1, name)
+
+    def test_last_modified_revision_after_converged_merge_dir_changes(self):
+        # merge a dir changes the last modified.
+        tree1 = self.make_branch_and_tree('t1')
+        self.build_tree(['t1/dir/'])
+        self._commit_sprout_rename_merge_converged(tree1, 'dir')
+
+    def test_last_modified_revision_after_converged_merge_file_changes(self):
+        # merge a file changes the last modified.
+        tree1 = self.make_branch_and_tree('t1')
+        self.build_tree(['t1/file'])
+        self._commit_sprout_rename_merge_converged(tree1, 'file')
+
+    def test_last_modified_revision_after_converged_merge_link_changes(self):
+        # merge a link changes the last modified.
+        self.requireFeature(tests.SymlinkFeature)
+        tree1 = self.make_branch_and_tree('t1')
+        os.symlink('target', 't1/link')
+        self._commit_sprout_rename_merge_converged(tree1, 'link')
+
+    def make_dir(self, name):
+        self.build_tree([name + '/'])
+
+    def make_file(self, name):
+        self.build_tree([name])
+
+    def make_link(self, name):
+        self.requireFeature(tests.SymlinkFeature)
+        os.symlink('target', name)
+
+    def _check_kind_change(self, make_before, make_after):
+        tree = self.make_branch_and_tree('.')
+        path = 'name'
+        make_before(path)
+        def change_kind():
+            try:
+                os.unlink(path)
+            except OSError, e:
+                if e.errno != EISDIR:
+                    raise
+                os.rmdir(path)
+            make_after(path)
+        self._add_commit_change_check_changed(tree, path, change_kind)
+
+    def test_last_modified_dir_file(self):
+        self._check_kind_change(self.make_dir, self.make_file)
+
+    def test_last_modified_dir_link(self):
+        self._check_kind_change(self.make_dir, self.make_link)
+
+    def test_last_modified_link_file(self):
+        self._check_kind_change(self.make_link, self.make_file)
+
+    def test_last_modified_link_dir(self):
+        self._check_kind_change(self.make_link, self.make_dir)
+
+    def test_last_modified_file_dir(self):
+        self._check_kind_change(self.make_file, self.make_dir)
+
+    def test_last_modified_file_link(self):
+        self._check_kind_change(self.make_file, self.make_link)

=== modified file 'bzrlib/tests/test_graph.py'
--- a/bzrlib/tests/test_graph.py	2007-08-07 20:45:47 +0000
+++ b/bzrlib/tests/test_graph.py	2007-09-03 22:17:20 +0000
@@ -373,4 +373,83 @@
         #      c
         graph = self.make_graph({'c': ['b', 'd'], 'd': ['e'], 'b': ['a'],
                                  'a': [NULL_REVISION], 'e': [NULL_REVISION]})
-        self.assertEqual(['c'], graph._filter_candidate_lca(['a', 'c', 'e']))
+        self.assertEqual(set(['c']), graph.heads(['a', 'c', 'e']))
+
+    def test_heads_null(self):
+        graph = self.make_graph(ancestry_1)
+        self.assertEqual(set(['null:']), graph.heads(['null:']))
+        self.assertEqual(set(['rev1']), graph.heads(['null:', 'rev1']))
+        self.assertEqual(set(['rev1']), graph.heads(['rev1', 'null:']))
+        self.assertEqual(set(['rev1']), graph.heads(set(['rev1', 'null:'])))
+        self.assertEqual(set(['rev1']), graph.heads(('rev1', 'null:')))
+
+    def test_heads_one(self):
+        # A single node will alwaya be a head
+        graph = self.make_graph(ancestry_1)
+        self.assertEqual(set(['null:']), graph.heads(['null:']))
+        self.assertEqual(set(['rev1']), graph.heads(['rev1']))
+        self.assertEqual(set(['rev2a']), graph.heads(['rev2a']))
+        self.assertEqual(set(['rev2b']), graph.heads(['rev2b']))
+        self.assertEqual(set(['rev3']), graph.heads(['rev3']))
+        self.assertEqual(set(['rev4']), graph.heads(['rev4']))
+
+    def test_heads_single(self):
+        graph = self.make_graph(ancestry_1)
+        self.assertEqual(set(['rev4']), graph.heads(['null:', 'rev4']))
+        self.assertEqual(set(['rev2a']), graph.heads(['rev1', 'rev2a']))
+        self.assertEqual(set(['rev2b']), graph.heads(['rev1', 'rev2b']))
+        self.assertEqual(set(['rev3']), graph.heads(['rev1', 'rev3']))
+        self.assertEqual(set(['rev4']), graph.heads(['rev1', 'rev4']))
+        self.assertEqual(set(['rev4']), graph.heads(['rev2a', 'rev4']))
+        self.assertEqual(set(['rev4']), graph.heads(['rev2b', 'rev4']))
+        self.assertEqual(set(['rev4']), graph.heads(['rev3', 'rev4']))
+
+    def test_heads_two_heads(self):
+        graph = self.make_graph(ancestry_1)
+        self.assertEqual(set(['rev2a', 'rev2b']),
+                         graph.heads(['rev2a', 'rev2b']))
+        self.assertEqual(set(['rev3', 'rev2b']),
+                         graph.heads(['rev3', 'rev2b']))
+
+    def test_heads_criss_cross(self):
+        graph = self.make_graph(criss_cross)
+        self.assertEqual(set(['rev2a']),
+                         graph.heads(['rev2a', 'rev1']))
+        self.assertEqual(set(['rev2b']),
+                         graph.heads(['rev2b', 'rev1']))
+        self.assertEqual(set(['rev3a']),
+                         graph.heads(['rev3a', 'rev1']))
+        self.assertEqual(set(['rev3b']),
+                         graph.heads(['rev3b', 'rev1']))
+        self.assertEqual(set(['rev2a', 'rev2b']),
+                         graph.heads(['rev2a', 'rev2b']))
+        self.assertEqual(set(['rev3a']),
+                         graph.heads(['rev3a', 'rev2a']))
+        self.assertEqual(set(['rev3a']),
+                         graph.heads(['rev3a', 'rev2b']))
+        self.assertEqual(set(['rev3a']),
+                         graph.heads(['rev3a', 'rev2a', 'rev2b']))
+        self.assertEqual(set(['rev3b']),
+                         graph.heads(['rev3b', 'rev2a']))
+        self.assertEqual(set(['rev3b']),
+                         graph.heads(['rev3b', 'rev2b']))
+        self.assertEqual(set(['rev3b']),
+                         graph.heads(['rev3b', 'rev2a', 'rev2b']))
+        self.assertEqual(set(['rev3a', 'rev3b']),
+                         graph.heads(['rev3a', 'rev3b']))
+        self.assertEqual(set(['rev3a', 'rev3b']),
+                         graph.heads(['rev3a', 'rev3b', 'rev2a', 'rev2b']))
+
+    def test_heads_shortcut(self):
+        graph = self.make_graph(history_shortcut)
+
+        self.assertEqual(set(['rev2a', 'rev2b', 'rev2c']),
+                         graph.heads(['rev2a', 'rev2b', 'rev2c']))
+        self.assertEqual(set(['rev3a', 'rev3b']),
+                         graph.heads(['rev3a', 'rev3b']))
+        self.assertEqual(set(['rev3a', 'rev3b']),
+                         graph.heads(['rev2a', 'rev3a', 'rev3b']))
+        self.assertEqual(set(['rev2a', 'rev3b']),
+                         graph.heads(['rev2a', 'rev3b']))
+        self.assertEqual(set(['rev2c', 'rev3a']),
+                         graph.heads(['rev2c', 'rev3a']))

=== modified file 'bzrlib/tests/tree_implementations/test_inv.py'
--- a/bzrlib/tests/tree_implementations/test_inv.py	2007-04-14 11:55:51 +0000
+++ b/bzrlib/tests/tree_implementations/test_inv.py	2007-09-03 01:57:12 +0000
@@ -25,6 +25,7 @@
 from bzrlib.osutils import (
     has_symlinks,
     )
+from bzrlib.symbol_versioning import zero_ninetyone
 from bzrlib.tests import TestSkipped
 from bzrlib.tests.tree_implementations import TestCaseWithTree
 from bzrlib.uncommit import uncommit
@@ -177,8 +178,9 @@
             self.branch.repository.get_transaction())
         
     def get_previous_heads(self, inventories):
-        return self.file_active.find_previous_heads(
-            inventories, 
+        return self.applyDeprecated(zero_ninetyone,
+            self.file_active.find_previous_heads,
+            inventories,
             self.branch.repository.weave_store,
             self.branch.repository.get_transaction())
         




More information about the bazaar-commits mailing list