~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to changelog_merge.py

Match file names, not paths, and tweak docs.

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
 
 
21
19
from bzrlib import merge
22
 
from bzrlib import debug
23
 
from bzrlib.merge3 import Merge3
24
 
from bzrlib.trace import mutter
25
20
 
26
21
 
27
22
def changelog_entries(lines):
72
67
        return tree.inventory[params.file_id].name
73
68
 
74
69
    def merge_text(self, params):
75
 
        """Merge changelog changes.
76
 
 
77
 
         * new entries from other will float to the top
78
 
         * edits to older entries are preserved
 
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
79
89
        """
80
90
        # Transform files into lists of changelog entries
81
91
        this_entries = changelog_entries(params.this_lines)
82
92
        other_entries = changelog_entries(params.other_lines)
83
93
        base_entries = changelog_entries(params.base_lines)
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
 
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
90
100
        # Transform the merged elements back into real blocks of lines.
91
101
        return 'success', entries_to_lines(result_entries)
92
102
 
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