~bzr-pqm/bzr/bzr.dev

0.38.1 by Andrew Bennetts
Simple plugin for merging GNU ChangeLog files.
1
# Copyright (C) 2010 Canonical Ltd
2
#
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
6379.6.7 by Jelmer Vernooij
Move importing from future until after doc string, otherwise the doc string will disappear.
17
"""Merge logic for changelog_merge plugin."""
18
6379.6.3 by Jelmer Vernooij
Use absolute_import.
19
from __future__ import absolute_import
20
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
21
import difflib
22
0.38.1 by Andrew Bennetts
Simple plugin for merging GNU ChangeLog files.
23
from bzrlib import merge
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
24
from bzrlib import debug
25
from bzrlib.merge3 import Merge3
26
from bzrlib.trace import mutter
0.38.1 by Andrew Bennetts
Simple plugin for merging GNU ChangeLog files.
27
28
29
def changelog_entries(lines):
30
    """Return a list of changelog entries.
31
32
    :param lines: lines of a changelog file.
33
    :returns: list of entries.  Each entry is a tuple of lines.
34
    """
35
    entries = []
36
    for line in lines:
37
        if line[0] not in (' ', '\t', '\n'):
38
            # new entry
39
            entries.append([line])
40
        else:
41
            try:
42
                entry = entries[-1]
43
            except IndexError:
44
                # Cope with leading blank lines.
45
                entries.append([])
46
                entry = entries[-1]
47
            entry.append(line)
48
    return map(tuple, entries)
49
50
51
def entries_to_lines(entries):
52
    """Turn a list of entries into a flat iterable of lines."""
53
    for entry in entries:
54
        for line in entry:
55
            yield line
56
57
58
class ChangeLogMerger(merge.ConfigurableFileMerger):
59
    """Merge GNU-format ChangeLog files."""
60
61
    name_prefix = "changelog"
62
0.38.3 by Andrew Bennetts
Match file names, not paths, and tweak docs.
63
    def get_filepath(self, params, tree):
64
        """Calculate the path to the file in a tree.
65
66
        This is overridden to return just the basename, rather than full path,
67
        so that e.g. if the config says ``changelog_merge_files = ChangeLog``,
68
        then all ChangeLog files in the tree will match (not just one in the
69
        root of the tree).
70
        
71
        :param params: A MergeHookParams describing the file to merge
72
        :param tree: a Tree, e.g. self.merger.this_tree.
73
        """
74
        return tree.inventory[params.file_id].name
75
0.38.1 by Andrew Bennetts
Simple plugin for merging GNU ChangeLog files.
76
    def merge_text(self, params):
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
77
        """Merge changelog changes.
78
79
         * new entries from other will float to the top
80
         * edits to older entries are preserved
0.38.1 by Andrew Bennetts
Simple plugin for merging GNU ChangeLog files.
81
        """
82
        # Transform files into lists of changelog entries
83
        this_entries = changelog_entries(params.this_lines)
84
        other_entries = changelog_entries(params.other_lines)
85
        base_entries = changelog_entries(params.base_lines)
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
86
        try:
87
            result_entries = merge_entries(
88
                base_entries, this_entries, other_entries)
89
        except EntryConflict:
5851.1.1 by Andrew Bennetts
Fix 'not_applicable' return in ChangeLogMerger.merge_text. Also add more tests and fix another bug they reveal.
90
            # XXX: generating a nice conflict file would be better
91
            return 'not_applicable', None
0.38.1 by Andrew Bennetts
Simple plugin for merging GNU ChangeLog files.
92
        # Transform the merged elements back into real blocks of lines.
93
        return 'success', entries_to_lines(result_entries)
94
0.39.1 by Andrew Bennetts
Prototype of fancier logic, and start of some tests. Simple case of bug 723968 passes, harder case does not. Probably regresses some old behaviour.
95
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
96
class EntryConflict(Exception):
97
    pass
98
99
100
def default_guess_edits(new_entries, deleted_entries, entry_as_str=''.join):
0.38.5 by Andrew Bennetts
Do a spot of code gardening.
101
    """Default implementation of guess_edits param of merge_entries.
102
103
    This algorithm does O(N^2 * logN) SequenceMatcher.ratio() calls, which is
104
    pretty bad, but it shouldn't be used very often.
105
    """
5851.1.1 by Andrew Bennetts
Fix 'not_applicable' return in ChangeLogMerger.merge_text. Also add more tests and fix another bug they reveal.
106
    deleted_entries_as_strs = map(entry_as_str, deleted_entries)
107
    new_entries_as_strs = map(entry_as_str, new_entries)
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
108
    result_new = list(new_entries)
109
    result_deleted = list(deleted_entries)
110
    result_edits = []
111
    sm = difflib.SequenceMatcher()
0.39.3 by Andrew Bennetts
Tighten the fuzzy matching cutoff to 80%.
112
    CUTOFF = 0.8
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
113
    while True:
114
        best = None
5851.1.1 by Andrew Bennetts
Fix 'not_applicable' return in ChangeLogMerger.merge_text. Also add more tests and fix another bug they reveal.
115
        best_score = CUTOFF
116
        # Compare each new entry with each old entry to find the best match
117
        for new_entry_as_str in new_entries_as_strs:
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
118
            sm.set_seq1(new_entry_as_str)
119
            for old_entry_as_str in deleted_entries_as_strs:
120
                sm.set_seq2(old_entry_as_str)
121
                score = sm.ratio()
5851.1.1 by Andrew Bennetts
Fix 'not_applicable' return in ChangeLogMerger.merge_text. Also add more tests and fix another bug they reveal.
122
                if score > best_score:
123
                    best = new_entry_as_str, old_entry_as_str
124
                    best_score = score
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
125
        if best is not None:
5851.1.1 by Andrew Bennetts
Fix 'not_applicable' return in ChangeLogMerger.merge_text. Also add more tests and fix another bug they reveal.
126
            # Add the best match to the list of edits, and remove it from the
127
            # the list of new/old entries.  Also remove it from the new/old
128
            # lists for the next round.
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
129
            del_index = deleted_entries_as_strs.index(best[1])
130
            new_index = new_entries_as_strs.index(best[0])
131
            result_edits.append(
132
                (result_deleted[del_index], result_new[new_index]))
133
            del deleted_entries_as_strs[del_index], result_deleted[del_index]
134
            del new_entries_as_strs[new_index], result_new[new_index]
135
        else:
5851.1.1 by Andrew Bennetts
Fix 'not_applicable' return in ChangeLogMerger.merge_text. Also add more tests and fix another bug they reveal.
136
            # No match better than CUTOFF exists in the remaining new and old
137
            # entries.
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
138
            break
139
    return result_new, result_deleted, result_edits
140
141
0.38.5 by Andrew Bennetts
Do a spot of code gardening.
142
def merge_entries(base_entries, this_entries, other_entries,
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
143
        guess_edits=default_guess_edits):
0.38.5 by Andrew Bennetts
Do a spot of code gardening.
144
    """Merge changelog given base, this, and other versions."""
145
    m3 = Merge3(base_entries, this_entries, other_entries, allow_objects=True)
0.39.1 by Andrew Bennetts
Prototype of fancier logic, and start of some tests. Simple case of bug 723968 passes, harder case does not. Probably regresses some old behaviour.
146
    result_entries = []
147
    at_top = True
148
    for group in m3.merge_groups():
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
149
        if 'changelog_merge' in debug.debug_flags:
150
            mutter('merge group:\n%r', group)
0.39.1 by Andrew Bennetts
Prototype of fancier logic, and start of some tests. Simple case of bug 723968 passes, harder case does not. Probably regresses some old behaviour.
151
        group_kind = group[0]
152
        if group_kind == 'conflict':
153
            _, base, this, other = group
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
154
            # Find additions
0.39.1 by Andrew Bennetts
Prototype of fancier logic, and start of some tests. Simple case of bug 723968 passes, harder case does not. Probably regresses some old behaviour.
155
            new_in_other = [
156
                entry for entry in other if entry not in base]
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
157
            # Find deletions
158
            deleted_in_other = [
159
                entry for entry in base if entry not in other]
160
            if at_top and deleted_in_other:
161
                # Magic!  Compare deletions and additions to try spot edits
162
                new_in_other, deleted_in_other, edits_in_other = guess_edits(
163
                    new_in_other, deleted_in_other)
164
            else:
165
                # Changes not made at the top are always preserved as is, no
166
                # need to try distinguish edits from adds and deletes.
167
                edits_in_other = []
168
            if 'changelog_merge' in debug.debug_flags:
169
                mutter('at_top: %r', at_top)
170
                mutter('new_in_other: %r', new_in_other)
171
                mutter('deleted_in_other: %r', deleted_in_other)
172
                mutter('edits_in_other: %r', edits_in_other)
173
            # Apply deletes and edits
174
            updated_this = [
175
                entry for entry in this if entry not in deleted_in_other]
176
            for old_entry, new_entry in edits_in_other:
177
                try:
178
                    index = updated_this.index(old_entry)
179
                except ValueError:
180
                    # edited entry no longer present in this!  Just give up and
181
                    # declare a conflict.
182
                    raise EntryConflict()
183
                updated_this[index] = new_entry
184
            if 'changelog_merge' in debug.debug_flags:
185
                mutter('updated_this: %r', updated_this)
186
            if at_top:
187
                # Float new entries from other to the top
188
                result_entries = new_in_other + result_entries
189
            else:
190
                result_entries.extend(new_in_other)
191
            result_entries.extend(updated_this)
0.39.1 by Andrew Bennetts
Prototype of fancier logic, and start of some tests. Simple case of bug 723968 passes, harder case does not. Probably regresses some old behaviour.
192
        else: # unchanged, same, a, or b.
193
            lines = group[1]
194
            result_entries.extend(lines)
195
        at_top = False
196
    return result_entries