1
# Copyright (C) 2010 Canonical Ltd
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.
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.
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
17
"""Merge logic for changelog_merge plugin."""
19
from __future__ import absolute_import
28
from bzrlib.merge3 import Merge3
29
from bzrlib.trace import mutter
32
def changelog_entries(lines):
33
"""Return a list of changelog entries.
35
:param lines: lines of a changelog file.
36
:returns: list of entries. Each entry is a tuple of lines.
40
if line[0] not in (' ', '\t', '\n'):
42
entries.append([line])
47
# Cope with leading blank lines.
51
return map(tuple, entries)
54
def entries_to_lines(entries):
55
"""Turn a list of entries into a flat iterable of lines."""
61
class ChangeLogMerger(merge.ConfigurableFileMerger):
62
"""Merge GNU-format ChangeLog files."""
64
name_prefix = "changelog"
66
def get_filepath(self, params, tree):
67
"""Calculate the path to the file in a tree.
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
74
:param params: A MergeHookParams describing the file to merge
75
:param tree: a Tree, e.g. self.merger.this_tree.
77
return urlutils.basename(tree.id2path(params.file_id))
79
def merge_text(self, params):
80
"""Merge changelog changes.
82
* new entries from other will float to the top
83
* edits to older entries are preserved
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)
90
result_entries = merge_entries(
91
base_entries, this_entries, other_entries)
93
# XXX: generating a nice conflict file would be better
94
return 'not_applicable', None
95
# Transform the merged elements back into real blocks of lines.
96
return 'success', entries_to_lines(result_entries)
99
class EntryConflict(Exception):
103
def default_guess_edits(new_entries, deleted_entries, entry_as_str=''.join):
104
"""Default implementation of guess_edits param of merge_entries.
106
This algorithm does O(N^2 * logN) SequenceMatcher.ratio() calls, which is
107
pretty bad, but it shouldn't be used very often.
109
deleted_entries_as_strs = map(entry_as_str, deleted_entries)
110
new_entries_as_strs = map(entry_as_str, new_entries)
111
result_new = list(new_entries)
112
result_deleted = list(deleted_entries)
114
sm = difflib.SequenceMatcher()
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:
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)
125
if score > best_score:
126
best = new_entry_as_str, old_entry_as_str
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.
132
del_index = deleted_entries_as_strs.index(best[1])
133
new_index = new_entries_as_strs.index(best[0])
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]
139
# No match better than CUTOFF exists in the remaining new and old
142
return result_new, result_deleted, result_edits
145
def merge_entries(base_entries, this_entries, other_entries,
146
guess_edits=default_guess_edits):
147
"""Merge changelog given base, this, and other versions."""
148
m3 = Merge3(base_entries, this_entries, other_entries, allow_objects=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
159
entry for entry in other if entry not in base]
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)
168
# Changes not made at the top are always preserved as is, no
169
# need to try distinguish edits from adds and deletes.
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
178
entry for entry in this if entry not in deleted_in_other]
179
for old_entry, new_entry in edits_in_other:
181
index = updated_this.index(old_entry)
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)
190
# Float new entries from other to the top
191
result_entries = new_in_other + result_entries
193
result_entries.extend(new_in_other)
194
result_entries.extend(updated_this)
195
else: # unchanged, same, a, or b.
197
result_entries.extend(lines)
199
return result_entries