~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to changelog_merge.py

Merge non-head-edits-723968: Better handling of edits to old entries (e.g. typo fixes), and start building a test suite.  Fixes LP #723968.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
"""Merge logic for changelog_merge plugin."""
18
18
 
 
19
import difflib
 
20
 
19
21
from bzrlib import merge
 
22
from bzrlib import debug
 
23
from bzrlib.merge3 import Merge3
 
24
from bzrlib.trace import mutter
20
25
 
21
26
 
22
27
def changelog_entries(lines):
67
72
        return tree.inventory[params.file_id].name
68
73
 
69
74
    def merge_text(self, params):
70
 
        """Float new changelog sections from other to the top of the changelog.
71
 
 
72
 
        e.g. Given a changelog in THIS containing::
73
 
 
74
 
          NEW-1
75
 
          OLD-2
76
 
          OLD-1
77
 
 
78
 
        and a changelog in OTHER containing::
79
 
 
80
 
          NEW-2
81
 
          OLD-1
82
 
 
83
 
        it will merge as::
84
 
 
85
 
          NEW-2
86
 
          NEW-1
87
 
          OLD-2
88
 
          OLD-1
 
75
        """Merge changelog changes.
 
76
 
 
77
         * new entries from other will float to the top
 
78
         * edits to older entries are preserved
89
79
        """
90
80
        # Transform files into lists of changelog entries
91
81
        this_entries = changelog_entries(params.this_lines)
92
82
        other_entries = changelog_entries(params.other_lines)
93
83
        base_entries = changelog_entries(params.base_lines)
94
 
        # Determine which entries have been added by base
95
 
        base_entries = frozenset(base_entries)
96
 
        new_in_other = [
97
 
            entry for entry in other_entries if entry not in base_entries]
98
 
        # Prepend them to the entries in this
99
 
        result_entries = new_in_other + this_entries
 
84
        try:
 
85
            result_entries = merge_entries(
 
86
                base_entries, this_entries, other_entries)
 
87
        except EntryConflict:
 
88
            return 'not_applicable' # XXX: generating a nice conflict file
 
89
                                    # would be better
100
90
        # Transform the merged elements back into real blocks of lines.
101
91
        return 'success', entries_to_lines(result_entries)
102
92
 
 
93
 
 
94
class EntryConflict(Exception):
 
95
    pass
 
96
 
 
97
 
 
98
def merge_entries_old(base_entries, this_entries, other_entries):
 
99
    # Determine which entries have been added by other (compared to base)
 
100
    base_entries = frozenset(base_entries)
 
101
    new_in_other = [
 
102
        entry for entry in other_entries if entry not in base_entries]
 
103
    # Prepend them to the entries in this
 
104
    result_entries = new_in_other + this_entries
 
105
    return result_entries
 
106
 
 
107
 
 
108
def default_guess_edits(new_entries, deleted_entries, entry_as_str=''.join):
 
109
    # This algorithm does O(N^2 * logN) SequenceMatcher.ratio() calls, which is
 
110
    # pretty bad, but it shouldn't be used very often.
 
111
    deleted_entries_as_strs = [
 
112
        entry_as_str(entry) for entry in deleted_entries]
 
113
    new_entries_as_strs = [
 
114
        entry_as_str(entry) for entry in new_entries]
 
115
    result_new = list(new_entries)
 
116
    result_deleted = list(deleted_entries)
 
117
    result_edits = []
 
118
    sm = difflib.SequenceMatcher()
 
119
    CUTOFF = 0.8
 
120
    while True:
 
121
        best = None
 
122
        best_score = None
 
123
        for new_entry in new_entries:
 
124
            new_entry_as_str = entry_as_str(new_entry)
 
125
            sm.set_seq1(new_entry_as_str)
 
126
            for old_entry_as_str in deleted_entries_as_strs:
 
127
                sm.set_seq2(old_entry_as_str)
 
128
                score = sm.ratio()
 
129
                if score > CUTOFF:
 
130
                    if best_score is None or score > best_score:
 
131
                        best = new_entry_as_str, old_entry_as_str
 
132
                        best_score = score
 
133
        if best is not None:
 
134
            del_index = deleted_entries_as_strs.index(best[1])
 
135
            new_index = new_entries_as_strs.index(best[0])
 
136
            result_edits.append(
 
137
                (result_deleted[del_index], result_new[new_index]))
 
138
            del deleted_entries_as_strs[del_index], result_deleted[del_index]
 
139
            del new_entries_as_strs[new_index], result_new[new_index]
 
140
        else:
 
141
            break
 
142
    return result_new, result_deleted, result_edits
 
143
 
 
144
 
 
145
def merge_entries_new(base_entries, this_entries, other_entries,
 
146
        guess_edits=default_guess_edits):
 
147
    m3 = Merge3(base_entries, this_entries, other_entries,
 
148
        allow_objects=True)
 
149
    result_entries = []
 
150
    at_top = True
 
151
    for group in m3.merge_groups():
 
152
        if 'changelog_merge' in debug.debug_flags:
 
153
            mutter('merge group:\n%r', group)
 
154
        group_kind = group[0]
 
155
        if group_kind == 'conflict':
 
156
            _, base, this, other = group
 
157
            # Find additions
 
158
            new_in_other = [
 
159
                entry for entry in other if entry not in base]
 
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)
 
195
        else: # unchanged, same, a, or b.
 
196
            lines = group[1]
 
197
            result_entries.extend(lines)
 
198
        at_top = False
 
199
    return result_entries
 
200
 
 
201
 
 
202
merge_entries = merge_entries_new