~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
6445.2.6 by Jelmer Vernooij
Review feedback.
23
from bzrlib import (
24
    debug,
25
    merge,
26
    urlutils,
27
    )
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
28
from bzrlib.merge3 import Merge3
29
from bzrlib.trace import mutter
0.38.1 by Andrew Bennetts
Simple plugin for merging GNU ChangeLog files.
30
31
32
def changelog_entries(lines):
33
    """Return a list of changelog entries.
34
35
    :param lines: lines of a changelog file.
36
    :returns: list of entries.  Each entry is a tuple of lines.
37
    """
38
    entries = []
39
    for line in lines:
40
        if line[0] not in (' ', '\t', '\n'):
41
            # new entry
42
            entries.append([line])
43
        else:
44
            try:
45
                entry = entries[-1]
46
            except IndexError:
47
                # Cope with leading blank lines.
48
                entries.append([])
49
                entry = entries[-1]
50
            entry.append(line)
51
    return map(tuple, entries)
52
53
54
def entries_to_lines(entries):
55
    """Turn a list of entries into a flat iterable of lines."""
56
    for entry in entries:
57
        for line in entry:
58
            yield line
59
60
61
class ChangeLogMerger(merge.ConfigurableFileMerger):
62
    """Merge GNU-format ChangeLog files."""
63
64
    name_prefix = "changelog"
65
0.38.3 by Andrew Bennetts
Match file names, not paths, and tweak docs.
66
    def get_filepath(self, params, tree):
67
        """Calculate the path to the file in a tree.
68
69
        This is overridden to return just the basename, rather than full path,
70
        so that e.g. if the config says ``changelog_merge_files = ChangeLog``,
71
        then all ChangeLog files in the tree will match (not just one in the
72
        root of the tree).
73
        
74
        :param params: A MergeHookParams describing the file to merge
75
        :param tree: a Tree, e.g. self.merger.this_tree.
76
        """
6405.2.7 by Jelmer Vernooij
Fix more tests.
77
        return urlutils.basename(tree.id2path(params.file_id))
0.38.3 by Andrew Bennetts
Match file names, not paths, and tweak docs.
78
0.38.1 by Andrew Bennetts
Simple plugin for merging GNU ChangeLog files.
79
    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.
80
        """Merge changelog changes.
81
82
         * new entries from other will float to the top
83
         * edits to older entries are preserved
0.38.1 by Andrew Bennetts
Simple plugin for merging GNU ChangeLog files.
84
        """
85
        # Transform files into lists of changelog entries
86
        this_entries = changelog_entries(params.this_lines)
87
        other_entries = changelog_entries(params.other_lines)
88
        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.
89
        try:
90
            result_entries = merge_entries(
91
                base_entries, this_entries, other_entries)
92
        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.
93
            # XXX: generating a nice conflict file would be better
94
            return 'not_applicable', None
0.38.1 by Andrew Bennetts
Simple plugin for merging GNU ChangeLog files.
95
        # Transform the merged elements back into real blocks of lines.
96
        return 'success', entries_to_lines(result_entries)
97
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.
98
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
99
class EntryConflict(Exception):
100
    pass
101
102
103
def default_guess_edits(new_entries, deleted_entries, entry_as_str=''.join):
0.38.5 by Andrew Bennetts
Do a spot of code gardening.
104
    """Default implementation of guess_edits param of merge_entries.
105
106
    This algorithm does O(N^2 * logN) SequenceMatcher.ratio() calls, which is
107
    pretty bad, but it shouldn't be used very often.
108
    """
5851.1.1 by Andrew Bennetts
Fix 'not_applicable' return in ChangeLogMerger.merge_text. Also add more tests and fix another bug they reveal.
109
    deleted_entries_as_strs = map(entry_as_str, deleted_entries)
110
    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.
111
    result_new = list(new_entries)
112
    result_deleted = list(deleted_entries)
113
    result_edits = []
114
    sm = difflib.SequenceMatcher()
0.39.3 by Andrew Bennetts
Tighten the fuzzy matching cutoff to 80%.
115
    CUTOFF = 0.8
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
116
    while True:
117
        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.
118
        best_score = CUTOFF
119
        # Compare each new entry with each old entry to find the best match
120
        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.
121
            sm.set_seq1(new_entry_as_str)
122
            for old_entry_as_str in deleted_entries_as_strs:
123
                sm.set_seq2(old_entry_as_str)
124
                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.
125
                if score > best_score:
126
                    best = new_entry_as_str, old_entry_as_str
127
                    best_score = score
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
128
        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.
129
            # Add the best match to the list of edits, and remove it from the
130
            # the list of new/old entries.  Also remove it from the new/old
131
            # 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.
132
            del_index = deleted_entries_as_strs.index(best[1])
133
            new_index = new_entries_as_strs.index(best[0])
134
            result_edits.append(
135
                (result_deleted[del_index], result_new[new_index]))
136
            del deleted_entries_as_strs[del_index], result_deleted[del_index]
137
            del new_entries_as_strs[new_index], result_new[new_index]
138
        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.
139
            # No match better than CUTOFF exists in the remaining new and old
140
            # entries.
0.39.2 by Andrew Bennetts
Implement fancy logic to try satisfy the old and new use cases, including another test.
141
            break
142
    return result_new, result_deleted, result_edits
143
144
0.38.5 by Andrew Bennetts
Do a spot of code gardening.
145
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.
146
        guess_edits=default_guess_edits):
0.38.5 by Andrew Bennetts
Do a spot of code gardening.
147
    """Merge changelog given base, this, and other versions."""
148
    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.
149
    result_entries = []
150
    at_top = True
151
    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.
152
        if 'changelog_merge' in debug.debug_flags:
153
            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.
154
        group_kind = group[0]
155
        if group_kind == 'conflict':
156
            _, 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.
157
            # 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.
158
            new_in_other = [
159
                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.
160
            # Find deletions
161
            deleted_in_other = [
162
                entry for entry in base if entry not in other]
163
            if at_top and deleted_in_other:
164
                # Magic!  Compare deletions and additions to try spot edits
165
                new_in_other, deleted_in_other, edits_in_other = guess_edits(
166
                    new_in_other, deleted_in_other)
167
            else:
168
                # Changes not made at the top are always preserved as is, no
169
                # need to try distinguish edits from adds and deletes.
170
                edits_in_other = []
171
            if 'changelog_merge' in debug.debug_flags:
172
                mutter('at_top: %r', at_top)
173
                mutter('new_in_other: %r', new_in_other)
174
                mutter('deleted_in_other: %r', deleted_in_other)
175
                mutter('edits_in_other: %r', edits_in_other)
176
            # Apply deletes and edits
177
            updated_this = [
178
                entry for entry in this if entry not in deleted_in_other]
179
            for old_entry, new_entry in edits_in_other:
180
                try:
181
                    index = updated_this.index(old_entry)
182
                except ValueError:
183
                    # edited entry no longer present in this!  Just give up and
184
                    # declare a conflict.
185
                    raise EntryConflict()
186
                updated_this[index] = new_entry
187
            if 'changelog_merge' in debug.debug_flags:
188
                mutter('updated_this: %r', updated_this)
189
            if at_top:
190
                # Float new entries from other to the top
191
                result_entries = new_in_other + result_entries
192
            else:
193
                result_entries.extend(new_in_other)
194
            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.
195
        else: # unchanged, same, a, or b.
196
            lines = group[1]
197
            result_entries.extend(lines)
198
        at_top = False
199
    return result_entries