Rev 1: First draft of a plugin for 'per-file-remerge'. in http://bzr.arbash-meinel.com/plugins/per_file_remerge

John Arbash Meinel john at arbash-meinel.com
Wed Jun 18 06:29:23 BST 2008


At http://bzr.arbash-meinel.com/plugins/per_file_remerge

------------------------------------------------------------
revno: 1
revision-id: john at arbash-meinel.com-20080618052913-t3ww5vj3a5q75uj0
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: per_file_remerge
timestamp: Wed 2008-06-18 00:29:13 -0500
message:
  First draft of a plugin for 'per-file-remerge'.
  
  Basically, it tricks the Merger code a bit, so that we can use the per-file graph
  to do a remerge, rather than using the whole-tree graph.
-------------- next part --------------
=== added file '__init__.py'
--- a/__init__.py	1970-01-01 00:00:00 +0000
+++ b/__init__.py	2008-06-18 05:29:13 +0000
@@ -0,0 +1,161 @@
+# Copyright (C) 2008 Canonical Development 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""Recompute a merge, using the per-file graph."""
+
+from bzrlib import (
+    commands,
+    errors,
+    option,
+    )
+
+
+class cmd_per_file_remerge(commands.Command):
+    """Recompute the merge for a given file, using its per-file graph."""
+
+    takes_args = ['filename']
+    takes_options = ['merge-type']
+
+    def run(self, filename, merge_type=None):
+        from bzrlib import (
+            conflicts,
+            merge,
+            ui,
+            workingtree,
+            )
+        if merge_type is None:
+            merge_type = merge.Merge3Merger
+        tree, relpath = workingtree.WorkingTree.open_containing(filename)
+        tree.lock_tree_write()
+        try:
+            parent_ids = tree.get_parent_ids()
+            if len(parent_ids) != 2:
+                raise errors.BzrCommandError(
+                    "Sorry, per-file-remerge only works after normal"
+                    " merges.  Not cherrypicking or multi-merges.")
+            merge_revision_id = parent_ids[1]
+
+            # This tricks the merge algorithm into using the per-file graph,
+            # rather than using the global graph.
+            file_id = tree.path2id(relpath)
+            if file_id is None:
+                raise errors.NotVersionedError(relpath)
+
+            per_file_graph = self._get_per_file_graph(tree, file_id)
+            this_modifed, base_revision_id, other_modified = \
+                self._find_revision_ids(tree, file_id, merge_revision_id,
+                                        per_file_graph)
+
+            print this_modifed, base_revision_id, other_modified
+            self._restore_file_id_to_pristine(tree, file_id, relpath)
+
+            # At this point, we have reverted the content of the file, now lets
+            # do the merge
+            pb = ui.ui_factory.nested_progress_bar()
+            tree.set_parent_ids(parent_ids[:1])
+            try:
+                merger = merge.Merger.from_revision_ids(pb, tree,
+                    other=other_modified,
+                    # Should we fake it here, or just assume that it is going
+                    # to be handled properly?
+                    base=base_revision_id,
+                    # other_branch=???,
+                    # base_branch=???,
+                    revision_graph=per_file_graph,
+                    )
+                merger.interesting_ids = set([file_id])
+                merger.merge_type = merge_type
+                # merger.show_base = show_base
+                # merger.reprocess = reprocess
+                num_conflicts = merger.do_merge()
+            finally:
+                tree.set_parent_ids(parent_ids)
+                pb.finished()
+        finally:
+            tree.unlock()
+
+        if num_conflicts > 0:
+            return 1
+        else:
+            return 0
+
+    def _restore_file_id_to_pristine(self, tree, file_id, relpath):
+        """Restore the given file to its pristine state."""
+        from bzrlib import conflicts, merge
+        # Now that we have the file, restore it to the original value, and
+        # reset the conflict list
+        cur_conflicts = tree.conflicts()
+        other_conflicts, matching_conflicts = cur_conflicts.select_conflicts(
+            tree, [relpath])
+        merge.transform_tree(tree, tree.basis_tree(), [file_id])
+        tree.set_conflicts(conflicts.ConflictList(other_conflicts))
+        try:
+            conflicts.restore(tree.abspath(relpath))
+        except errors.NotConflicted:
+            pass
+
+    @staticmethod
+    def _get_per_file_graph(tree, file_id):
+        """Get a Graph object for this file's history."""
+        from bzrlib import graph
+        repo = tree.branch.repository
+        weave = repo.weave_store.get_weave(file_id, repo.get_transaction())
+        graph_obj = graph.Graph(weave)
+        return graph_obj
+
+    @staticmethod
+    def _find_revision_ids(tree, file_id, merge_revision_id, graph_obj):
+        """Find the revision ids that are going to be merged.
+
+        :param tree: The working tree in question
+        :param file_id: The file in question
+        :param merge_revision_id: The revision which is merged into tree
+        :param graph_obj: The per-file graph, given by _get_per_file_graph
+        :return: revision ids for (this, base, other)
+        """
+        # Use tree.inventory[file_id].revision to find the revision id in the
+        # per-file graph
+        # TODO: Is there a better way to get this than going to the inventory?
+        other_tree = tree.branch.repository.revision_tree(
+            merge_revision_id)
+        other_last_modified_revision_id = other_tree.inventory[file_id].revision
+
+        basis_tree = tree.basis_tree()
+        basis_tree.lock_read()
+        try:
+            this_last_modified_revision_id = basis_tree.inventory[file_id].revision
+        finally:
+            basis_tree.unlock()
+
+        base_revision_id, num_steps = graph_obj.find_unique_lca(
+            this_last_modified_revision_id,
+            other_last_modified_revision_id,
+            count_steps=True)
+        return (this_last_modified_revision_id, base_revision_id,
+                other_last_modified_revision_id)
+
+
+commands.register_command(cmd_per_file_remerge)
+
+
+def load_tests(basic_tests, module, loader):
+    suite = loader.suiteClass()
+
+    test_files = [__name__ + '.' + x for x in [
+        'test_per_file_remerge',
+    ]]
+    suite.addTests(loader.loadTestsFromModuleNames(test_files))
+    return suite

=== added file 'test_per_file_remerge.py'
--- a/test_per_file_remerge.py	1970-01-01 00:00:00 +0000
+++ b/test_per_file_remerge.py	2008-06-18 05:29:13 +0000
@@ -0,0 +1,118 @@
+# Copyright (C) 2008 Canonical Development 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""Recompute a merge, using the per-file graph."""
+
+from bzrlib import tests
+
+from bzrlib.plugins import per_file_remerge
+
+
+class TestPerFileRemerge(tests.TestCaseWithTransport):
+
+    def create_criss_cross_merge(self):
+        # Revision graph looks like:
+        #
+        #       base
+        #       /  \
+        #      A    B   B1 modifies A, B2 modifies B
+        #      |\  /|
+        #      | \/ |
+        #      | /\ |
+        #      |/  \|
+        #      C    D   Each merges the other's changes
+        #      |    |
+        #      E    F   And then modifies the file again
+        #
+        tree1 = self.make_branch_and_tree('tree1')
+        self.build_tree(['tree1/a', 'tree1/b'])
+        tree1.add(['a', 'b'], ['a-id', 'b-id'])
+        tree1.commit('base', rev_id='rev-base')
+
+        tree2 = tree1.bzrdir.sprout('tree2').open_workingtree()
+
+        self.build_tree_contents([('tree1/a', 'new a contents\n')])
+        tree1.commit('A', rev_id='rev-A')
+
+        self.build_tree_contents([('tree2/b', 'new b contents\n')])
+        tree2.commit('B', rev_id='rev-B')
+
+        tree1.merge_from_branch(tree2.branch)
+        tree2.merge_from_branch(tree1.branch)
+
+        tree1.commit('C', rev_id='rev-C')
+        tree2.commit('D', rev_id='rev-D')
+
+        self.build_tree_contents([('tree1/a', 'updated a contents\n'),
+                                  ('tree2/b', 'updated b contents\n')])
+        tree1.commit('E', rev_id='rev-E')
+        tree2.commit('F', rev_id='rev-F')
+        return tree1, tree2
+
+    def test_tree_contents(self):
+        tree1, tree2 = self.create_criss_cross_merge()
+        self.assertFileEqual('updated a contents\n', 'tree1/a')
+        self.assertFileEqual('new b contents\n', 'tree1/b')
+        self.assertFileEqual('new a contents\n', 'tree2/a')
+        self.assertFileEqual('updated b contents\n', 'tree2/b')
+
+    def test__get_per_file_graph(self):
+        tree1, tree2 = self.create_criss_cross_merge()
+        tree1.merge_from_branch(tree2.branch)
+        tree1.lock_read()
+        self.addCleanup(tree1.unlock)
+        graph_obj = per_file_remerge.cmd_per_file_remerge._get_per_file_graph(
+            tree1, 'a-id')
+        self.assertEqual({'rev-E': ('rev-A',), 'rev-A': ('rev-base',)},
+                         graph_obj.get_parent_map(['rev-E', 'rev-A']))
+        graph_obj = per_file_remerge.cmd_per_file_remerge._get_per_file_graph(
+            tree1, 'b-id')
+        self.assertEqual({'rev-F': ('rev-B',), 'rev-B': ('rev-base',)},
+                         graph_obj.get_parent_map(['rev-F', 'rev-B']))
+
+    def test__find_revision_ids(self):
+        tree1, tree2 = self.create_criss_cross_merge()
+        tree1.merge_from_branch(tree2.branch)
+        tree1.lock_read()
+        self.addCleanup(tree1.unlock)
+        graph_obj = per_file_remerge.cmd_per_file_remerge._get_per_file_graph(
+            tree1, 'a-id')
+        self.assertEqual(('rev-E', 'rev-A', 'rev-A'),
+            per_file_remerge.cmd_per_file_remerge._find_revision_ids(
+                tree1, 'a-id', 'rev-F', graph_obj))
+        graph_obj = per_file_remerge.cmd_per_file_remerge._get_per_file_graph(
+            tree1, 'b-id')
+        self.assertEqual(('rev-B', 'rev-B', 'rev-F'),
+            per_file_remerge.cmd_per_file_remerge._find_revision_ids(
+                tree1, 'b-id', 'rev-F', graph_obj))
+
+    def test_merge_criss_cross(self):
+        tree1, tree2 = self.create_criss_cross_merge()
+        # The default merge algorithm is going to warn about a criss-cross
+        # merge, and then conflict both 'a' and 'b'
+        num_conflicts = tree1.merge_from_branch(tree2.branch)
+        self.assertEqual(2, num_conflicts)
+
+        # Now we need to try to remerge using the per-file graph
+        self.run_bzr(['per-file-remerge', 'tree1/b'])
+        # This should fix the conflict in b
+        self.assertFileEqual('updated b contents\n', 'tree1/b')
+        # But leave the conflict in a alone
+        self.assertFileEqual('<<<<<<< TREE\n'
+                             'updated a contents\n'
+                             '=======\n'
+                             'new a contents\n'
+                             '>>>>>>> MERGE-SOURCE\n', 'tree1/a')



More information about the bazaar-commits mailing list