Rev 5726: (spiv) Add 'changelog_merge' plugin. (Andrew Bennetts) in file:///home/pqm/archives/thelove/bzr/%2Btrunk/
Canonical.com Patch Queue Manager
pqm at pqm.ubuntu.com
Wed Mar 16 07:33:08 UTC 2011
At file:///home/pqm/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 5726 [merge]
revision-id: pqm at pqm.ubuntu.com-20110316073305-1r89t4gis3nawye3
parent: pqm at pqm.ubuntu.com-20110315110549-4e8dv1v95wbjbkz7
parent: andrew.bennetts at canonical.com-20110315080704-qrgq855gtmdw8we5
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Wed 2011-03-16 07:33:05 +0000
message:
(spiv) Add 'changelog_merge' plugin. (Andrew Bennetts)
added:
bzrlib/plugins/changelog_merge/ changelog_merge-20110315075052-dfnfubojfup1895e-1
bzrlib/plugins/changelog_merge/__init__.py __init__.py-20100920024628-tdrjysnxjm1q96oq-1
bzrlib/plugins/changelog_merge/changelog_merge.py changelog_merge.py-20100920024628-tdrjysnxjm1q96oq-2
bzrlib/plugins/changelog_merge/tests/ tests-20110310080015-9c3muqni567c1qux-1
bzrlib/plugins/changelog_merge/tests/__init__.py __init__.py-20110310080015-9c3muqni567c1qux-2
bzrlib/plugins/changelog_merge/tests/test_changelog_merge.py test_changelog_merge-20110310080015-9c3muqni567c1qux-3
modified:
doc/en/release-notes/bzr-2.4.txt bzr2.4.txt-20110114053217-k7ym9jfz243fddjm-1
doc/en/whats-new/whats-new-in-2.4.txt whatsnewin2.4.txt-20110114044330-nipk1og7j729fy89-1
=== added directory 'bzrlib/plugins/changelog_merge'
=== added file 'bzrlib/plugins/changelog_merge/__init__.py'
--- a/bzrlib/plugins/changelog_merge/__init__.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/plugins/changelog_merge/__init__.py 2011-03-15 07:56:42 +0000
@@ -0,0 +1,87 @@
+# Copyright (C) 2010 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
+
+__doc__ = """Merge hook for GNU-format ChangeLog files
+
+To enable this plugin, add a section to your location.conf
+like::
+
+ [/home/user/proj]
+ changelog_merge_files = ChangeLog
+
+Or add an entry to your branch.conf like::
+
+ changelog_merge_files = ChangeLog
+
+The changelog_merge_files config option takes a list of file names (not paths),
+separated by commas. (This is unlike the news_merge plugin, which matches
+paths.) e.g. the above config examples would match both
+``src/foolib/ChangeLog`` and ``docs/ChangeLog``.
+
+The algorithm used to merge the changes can be summarised as:
+
+ * new entries added to the top of OTHER are emitted first
+ * all other additions, deletions and edits from THIS and OTHER are preserved
+ * edits (e.g. to fix typos) at the top of OTHER are hard to distinguish from
+ adding and deleting independent entries; the algorithm tries to guess which
+ based on how similar the old and new entries are.
+
+Caveats
+-------
+
+Most changes can be merged, but conflicts are possible if the plugin finds
+edits at the top of OTHER to entries that have been deleted (or also edited) by
+THIS. In that case the plugin gives up and bzr's default merge logic will be
+used.
+
+No effort is made to deduplicate entries added by both sides.
+
+The results depend on the choice of the 'base' version, so it might give
+strange results if there is a criss-cross merge.
+"""
+
+# Since we are a built-in plugin we share the bzrlib version
+from bzrlib import version_info
+
+# Put most of the code in a separate module that we lazy-import to keep the
+# overhead of this plugin as minimal as possible.
+from bzrlib.lazy_import import lazy_import
+lazy_import(globals(), """
+from bzrlib.plugins.changelog_merge import changelog_merge as _mod_changelog_merge
+""")
+
+from bzrlib.merge import Merger
+
+
+def changelog_merge_hook(merger):
+ """Merger.merge_file_content hook for GNU-format ChangeLog files."""
+ return _mod_changelog_merge.ChangeLogMerger(merger)
+
+
+def install_hook():
+ Merger.hooks.install_named_hook(
+ 'merge_file_content', changelog_merge_hook, 'GNU ChangeLog file merge')
+install_hook()
+
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'tests',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
+
=== added file 'bzrlib/plugins/changelog_merge/changelog_merge.py'
--- a/bzrlib/plugins/changelog_merge/changelog_merge.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/plugins/changelog_merge/changelog_merge.py 2011-03-15 07:54:39 +0000
@@ -0,0 +1,192 @@
+# Copyright (C) 2010 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
+
+"""Merge logic for changelog_merge plugin."""
+
+import difflib
+
+from bzrlib import merge
+from bzrlib import debug
+from bzrlib.merge3 import Merge3
+from bzrlib.trace import mutter
+
+
+def changelog_entries(lines):
+ """Return a list of changelog entries.
+
+ :param lines: lines of a changelog file.
+ :returns: list of entries. Each entry is a tuple of lines.
+ """
+ entries = []
+ for line in lines:
+ if line[0] not in (' ', '\t', '\n'):
+ # new entry
+ entries.append([line])
+ else:
+ try:
+ entry = entries[-1]
+ except IndexError:
+ # Cope with leading blank lines.
+ entries.append([])
+ entry = entries[-1]
+ entry.append(line)
+ return map(tuple, entries)
+
+
+def entries_to_lines(entries):
+ """Turn a list of entries into a flat iterable of lines."""
+ for entry in entries:
+ for line in entry:
+ yield line
+
+
+class ChangeLogMerger(merge.ConfigurableFileMerger):
+ """Merge GNU-format ChangeLog files."""
+
+ name_prefix = "changelog"
+
+ def get_filepath(self, params, tree):
+ """Calculate the path to the file in a tree.
+
+ This is overridden to return just the basename, rather than full path,
+ so that e.g. if the config says ``changelog_merge_files = ChangeLog``,
+ then all ChangeLog files in the tree will match (not just one in the
+ root of the tree).
+
+ :param params: A MergeHookParams describing the file to merge
+ :param tree: a Tree, e.g. self.merger.this_tree.
+ """
+ return tree.inventory[params.file_id].name
+
+ def merge_text(self, params):
+ """Merge changelog changes.
+
+ * new entries from other will float to the top
+ * edits to older entries are preserved
+ """
+ # Transform files into lists of changelog entries
+ this_entries = changelog_entries(params.this_lines)
+ other_entries = changelog_entries(params.other_lines)
+ base_entries = changelog_entries(params.base_lines)
+ try:
+ result_entries = merge_entries(
+ base_entries, this_entries, other_entries)
+ except EntryConflict:
+ return 'not_applicable' # XXX: generating a nice conflict file
+ # would be better
+ # Transform the merged elements back into real blocks of lines.
+ return 'success', entries_to_lines(result_entries)
+
+
+class EntryConflict(Exception):
+ pass
+
+
+def default_guess_edits(new_entries, deleted_entries, entry_as_str=''.join):
+ """Default implementation of guess_edits param of merge_entries.
+
+ This algorithm does O(N^2 * logN) SequenceMatcher.ratio() calls, which is
+ pretty bad, but it shouldn't be used very often.
+ """
+ deleted_entries_as_strs = [
+ entry_as_str(entry) for entry in deleted_entries]
+ new_entries_as_strs = [
+ entry_as_str(entry) for entry in new_entries]
+ result_new = list(new_entries)
+ result_deleted = list(deleted_entries)
+ result_edits = []
+ sm = difflib.SequenceMatcher()
+ CUTOFF = 0.8
+ while True:
+ best = None
+ best_score = None
+ for new_entry in new_entries:
+ new_entry_as_str = entry_as_str(new_entry)
+ sm.set_seq1(new_entry_as_str)
+ for old_entry_as_str in deleted_entries_as_strs:
+ sm.set_seq2(old_entry_as_str)
+ score = sm.ratio()
+ if score > CUTOFF:
+ if best_score is None or score > best_score:
+ best = new_entry_as_str, old_entry_as_str
+ best_score = score
+ if best is not None:
+ del_index = deleted_entries_as_strs.index(best[1])
+ new_index = new_entries_as_strs.index(best[0])
+ result_edits.append(
+ (result_deleted[del_index], result_new[new_index]))
+ del deleted_entries_as_strs[del_index], result_deleted[del_index]
+ del new_entries_as_strs[new_index], result_new[new_index]
+ else:
+ break
+ return result_new, result_deleted, result_edits
+
+
+def merge_entries(base_entries, this_entries, other_entries,
+ guess_edits=default_guess_edits):
+ """Merge changelog given base, this, and other versions."""
+ m3 = Merge3(base_entries, this_entries, other_entries, allow_objects=True)
+ result_entries = []
+ at_top = True
+ for group in m3.merge_groups():
+ if 'changelog_merge' in debug.debug_flags:
+ mutter('merge group:\n%r', group)
+ group_kind = group[0]
+ if group_kind == 'conflict':
+ _, base, this, other = group
+ # Find additions
+ new_in_other = [
+ entry for entry in other if entry not in base]
+ # Find deletions
+ deleted_in_other = [
+ entry for entry in base if entry not in other]
+ if at_top and deleted_in_other:
+ # Magic! Compare deletions and additions to try spot edits
+ new_in_other, deleted_in_other, edits_in_other = guess_edits(
+ new_in_other, deleted_in_other)
+ else:
+ # Changes not made at the top are always preserved as is, no
+ # need to try distinguish edits from adds and deletes.
+ edits_in_other = []
+ if 'changelog_merge' in debug.debug_flags:
+ mutter('at_top: %r', at_top)
+ mutter('new_in_other: %r', new_in_other)
+ mutter('deleted_in_other: %r', deleted_in_other)
+ mutter('edits_in_other: %r', edits_in_other)
+ # Apply deletes and edits
+ updated_this = [
+ entry for entry in this if entry not in deleted_in_other]
+ for old_entry, new_entry in edits_in_other:
+ try:
+ index = updated_this.index(old_entry)
+ except ValueError:
+ # edited entry no longer present in this! Just give up and
+ # declare a conflict.
+ raise EntryConflict()
+ updated_this[index] = new_entry
+ if 'changelog_merge' in debug.debug_flags:
+ mutter('updated_this: %r', updated_this)
+ if at_top:
+ # Float new entries from other to the top
+ result_entries = new_in_other + result_entries
+ else:
+ result_entries.extend(new_in_other)
+ result_entries.extend(updated_this)
+ else: # unchanged, same, a, or b.
+ lines = group[1]
+ result_entries.extend(lines)
+ at_top = False
+ return result_entries
=== added directory 'bzrlib/plugins/changelog_merge/tests'
=== added file 'bzrlib/plugins/changelog_merge/tests/__init__.py'
--- a/bzrlib/plugins/changelog_merge/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/plugins/changelog_merge/tests/__init__.py 2011-03-10 08:02:30 +0000
@@ -0,0 +1,24 @@
+# Copyright (C) 2011 by 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
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'test_changelog_merge',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
+
=== added file 'bzrlib/plugins/changelog_merge/tests/test_changelog_merge.py'
--- a/bzrlib/plugins/changelog_merge/tests/test_changelog_merge.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/plugins/changelog_merge/tests/test_changelog_merge.py 2011-03-11 06:20:40 +0000
@@ -0,0 +1,140 @@
+# Copyright (C) 2011 by 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
+
+from bzrlib import (
+ tests,
+ )
+from bzrlib.plugins.changelog_merge import changelog_merge
+
+
+sample_base_entries = [
+ 'Base entry B1',
+ 'Base entry B2',
+ 'Base entry B3',
+ ]
+
+sample_this_entries = [
+ 'This entry T1',
+ 'This entry T2',
+ #'Base entry B1 updated',
+ 'Base entry B1',
+ 'Base entry B2',
+ 'Base entry B3',
+ ]
+
+sample_other_entries = [
+ 'Other entry O1',
+ #'Base entry B1',
+ 'Base entry B1',
+ 'Base entry B2 updated',
+ 'Base entry B3',
+ ]
+
+
+sample2_base_entries = [
+ 'Base entry B1',
+ 'Base entry B2',
+ 'Base entry B3',
+ ]
+
+sample2_this_entries = [
+ 'This entry T1',
+ 'This entry T2',
+ #'Base entry B1 updated',
+ 'Base entry B1',
+ 'Base entry B2',
+ ]
+
+sample2_other_entries = [
+ 'Other entry O1',
+ #'Base entry B1',
+ 'Base entry B1 edit', # > 80% similar according to difflib
+ 'Base entry B2',
+ ]
+
+
+class TestMergeCoreLogic(tests.TestCase):
+
+ def test_new_in_other_floats_to_top(self):
+ """Changes at the top of 'other' float to the top.
+
+ Given a changelog in THIS containing::
+
+ NEW-1
+ OLD-1
+
+ and a changelog in OTHER containing::
+
+ NEW-2
+ OLD-1
+
+ it will merge as::
+
+ NEW-2
+ NEW-1
+ OLD-1
+ """
+ base_entries = ['OLD-1']
+ this_entries = ['NEW-1', 'OLD-1']
+ other_entries = ['NEW-2', 'OLD-1']
+ result_entries = changelog_merge.merge_entries(
+ base_entries, this_entries, other_entries)
+ self.assertEqual(
+ ['NEW-2', 'NEW-1', 'OLD-1'], result_entries)
+
+ def test_acceptance_bug_723968(self):
+ """Merging a branch that:
+
+ 1. adds a new entry, and
+ 2. edits an old entry (e.g. to fix a typo or twiddle formatting)
+
+ will:
+
+ 1. add the new entry to the top
+ 2. keep the edit, without duplicating the edited entry or moving it.
+ """
+ result_entries = changelog_merge.merge_entries(
+ sample_base_entries, sample_this_entries, sample_other_entries)
+ self.assertEqual([
+ 'Other entry O1',
+ 'This entry T1',
+ 'This entry T2',
+ 'Base entry B1',
+ 'Base entry B2 updated',
+ 'Base entry B3',
+ ],
+ list(result_entries))
+
+ def test_more_complex_conflict(self):
+ """Like test_acceptance_bug_723968, but with a more difficult conflict:
+ the new entry and the edited entry are adjacent.
+ """
+ def guess_edits(new, deleted):
+ #import pdb; pdb.set_trace()
+ return changelog_merge.default_guess_edits(new, deleted,
+ entry_as_str=lambda x: x)
+ result_entries = changelog_merge.merge_entries(
+ sample2_base_entries, sample2_this_entries, sample2_other_entries,
+ guess_edits=guess_edits)
+ self.assertEqual([
+ 'Other entry O1',
+ 'This entry T1',
+ 'This entry T2',
+ 'Base entry B1 edit',
+ 'Base entry B2',
+ ],
+ list(result_entries))
+
=== modified file 'doc/en/release-notes/bzr-2.4.txt'
--- a/doc/en/release-notes/bzr-2.4.txt 2011-03-14 14:21:29 +0000
+++ b/doc/en/release-notes/bzr-2.4.txt 2011-03-15 08:07:04 +0000
@@ -20,12 +20,10 @@
.. New commands, options, etc that users may wish to try out.
-* The ``lp:`` directory service now supports Launchpad's QA staging.
- (Jelmer Vernooij, #667483)
-
-* External merge tools can now be configured in bazaar.conf. See
- ``bzr help configuration`` for more information. (Gordon Tyler, #489915)
-
+* Added ``changelog_merge`` plugin for merging changes to ``Changelog`` files
+ in GNU format. See ``bzr help changelog_merge`` for details.
+ (Andrew Bennetts)
+
* Configuration options can now use references to other options in the same
file by enclosing them with curly brackets (``{other_opt}``). This makes it
possible to use, for example,
@@ -34,6 +32,12 @@
this feature. It can be activated by declaring ``bzr.config.expand = True``
in ``bazaar.conf``. (Vincent Ladeuil)
+* External merge tools can now be configured in bazaar.conf. See
+ ``bzr help configuration`` for more information. (Gordon Tyler, #489915)
+
+* The ``lp:`` directory service now supports Launchpad's QA staging.
+ (Jelmer Vernooij, #667483)
+
Improvements
************
=== modified file 'doc/en/whats-new/whats-new-in-2.4.txt'
--- a/doc/en/whats-new/whats-new-in-2.4.txt 2011-02-25 12:12:39 +0000
+++ b/doc/en/whats-new/whats-new-in-2.4.txt 2011-03-15 08:07:04 +0000
@@ -40,6 +40,14 @@
during the beta period controlled by the ``bzr.config.expand`` option that
should be declared in ``bazaar.conf`` and no other file.
+Changelog merge plugin
+**********************
+
+The ``changelog_merge`` plugin has been added. It provides a merge hook
+to automate merging of changes to ``ChangeLog`` files in GNU's change log
+format. Refer to ``bzr help changelog_merge`` for documentation on how to
+enable it and what it can do.
+
Further information
*******************
@@ -54,3 +62,6 @@
* :doc:`whats-new-in-2.1`
* :doc:`whats-new-in-2.2`
* :doc:`whats-new-in-2.3`
+
+..
+ vim: tw=74 ft=rst ff=unix
More information about the bazaar-commits
mailing list