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."""
21
from bzrlib import merge
22
from bzrlib import debug
23
from bzrlib.merge3 import Merge3
24
from bzrlib.trace import mutter
27
def changelog_entries(lines):
28
"""Return a list of changelog entries.
30
:param lines: lines of a changelog file.
31
:returns: list of entries. Each entry is a tuple of lines.
35
if line[0] not in (' ', '\t', '\n'):
37
entries.append([line])
42
# Cope with leading blank lines.
46
return map(tuple, entries)
49
def entries_to_lines(entries):
50
"""Turn a list of entries into a flat iterable of lines."""
56
class ChangeLogMerger(merge.ConfigurableFileMerger):
57
"""Merge GNU-format ChangeLog files."""
59
name_prefix = "changelog"
61
def get_filepath(self, params, tree):
62
"""Calculate the path to the file in a tree.
64
This is overridden to return just the basename, rather than full path,
65
so that e.g. if the config says ``changelog_merge_files = ChangeLog``,
66
then all ChangeLog files in the tree will match (not just one in the
69
:param params: A MergeHookParams describing the file to merge
70
:param tree: a Tree, e.g. self.merger.this_tree.
72
return tree.inventory[params.file_id].name
74
def merge_text(self, params):
75
"""Merge changelog changes.
77
* new entries from other will float to the top
78
* edits to older entries are preserved
80
# Transform files into lists of changelog entries
81
this_entries = changelog_entries(params.this_lines)
82
other_entries = changelog_entries(params.other_lines)
83
base_entries = changelog_entries(params.base_lines)
85
result_entries = merge_entries(
86
base_entries, this_entries, other_entries)
88
return 'not_applicable' # XXX: generating a nice conflict file
90
# Transform the merged elements back into real blocks of lines.
91
return 'success', entries_to_lines(result_entries)
94
class EntryConflict(Exception):
98
def default_guess_edits(new_entries, deleted_entries, entry_as_str=''.join):
99
"""Default implementation of guess_edits param of merge_entries.
101
This algorithm does O(N^2 * logN) SequenceMatcher.ratio() calls, which is
102
pretty bad, but it shouldn't be used very often.
104
deleted_entries_as_strs = [
105
entry_as_str(entry) for entry in deleted_entries]
106
new_entries_as_strs = [
107
entry_as_str(entry) for entry in new_entries]
108
result_new = list(new_entries)
109
result_deleted = list(deleted_entries)
111
sm = difflib.SequenceMatcher()
116
for new_entry in new_entries:
117
new_entry_as_str = entry_as_str(new_entry)
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)
123
if best_score is None or score > best_score:
124
best = new_entry_as_str, old_entry_as_str
127
del_index = deleted_entries_as_strs.index(best[1])
128
new_index = new_entries_as_strs.index(best[0])
130
(result_deleted[del_index], result_new[new_index]))
131
del deleted_entries_as_strs[del_index], result_deleted[del_index]
132
del new_entries_as_strs[new_index], result_new[new_index]
135
return result_new, result_deleted, result_edits
138
def merge_entries(base_entries, this_entries, other_entries,
139
guess_edits=default_guess_edits):
140
"""Merge changelog given base, this, and other versions."""
141
m3 = Merge3(base_entries, this_entries, other_entries, allow_objects=True)
144
for group in m3.merge_groups():
145
if 'changelog_merge' in debug.debug_flags:
146
mutter('merge group:\n%r', group)
147
group_kind = group[0]
148
if group_kind == 'conflict':
149
_, base, this, other = group
152
entry for entry in other if entry not in base]
155
entry for entry in base if entry not in other]
156
if at_top and deleted_in_other:
157
# Magic! Compare deletions and additions to try spot edits
158
new_in_other, deleted_in_other, edits_in_other = guess_edits(
159
new_in_other, deleted_in_other)
161
# Changes not made at the top are always preserved as is, no
162
# need to try distinguish edits from adds and deletes.
164
if 'changelog_merge' in debug.debug_flags:
165
mutter('at_top: %r', at_top)
166
mutter('new_in_other: %r', new_in_other)
167
mutter('deleted_in_other: %r', deleted_in_other)
168
mutter('edits_in_other: %r', edits_in_other)
169
# Apply deletes and edits
171
entry for entry in this if entry not in deleted_in_other]
172
for old_entry, new_entry in edits_in_other:
174
index = updated_this.index(old_entry)
176
# edited entry no longer present in this! Just give up and
177
# declare a conflict.
178
raise EntryConflict()
179
updated_this[index] = new_entry
180
if 'changelog_merge' in debug.debug_flags:
181
mutter('updated_this: %r', updated_this)
183
# Float new entries from other to the top
184
result_entries = new_in_other + result_entries
186
result_entries.extend(new_in_other)
187
result_entries.extend(updated_this)
188
else: # unchanged, same, a, or b.
190
result_entries.extend(lines)
192
return result_entries