2
# -*- coding: utf-8 -*-
3
# Copyright ? 2008 Canonical Ltd.
4
# Author: Scott James Remnant <scott@ubuntu.com>.
5
# Hacked up by: Bryce Harrington <bryce@ubuntu.com>
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of version 3 of the GNU General Public License as
9
# published by the Free Software Foundation.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program. If not, see <http://www.gnu.org/licenses/>.
25
class ChangeLogFileMerge(merge.ConfigurableFileMerger):
27
name_prefix = 'deb_changelog'
28
default_files = ['debian/changelog']
30
def merge_text(self, params):
31
return 'success', merge_changelog(params.this_lines, params.other_lines)
34
########################################################################
35
# Changelog Management
36
########################################################################
38
# Regular expression for top of debian/changelog
39
CL_RE = re.compile(r'^(\w[-+0-9a-z.]*) \(([^\(\) \t]+)\)((\s+[-0-9a-z]+)+)\;',
42
def merge_changelog(left_changelog_lines, right_changelog_lines):
43
"""Merge a changelog file."""
45
left_cl = read_changelog(left_changelog_lines)
46
right_cl = read_changelog(right_changelog_lines)
49
# TODO: This is not a 3-way merge, but a 2-way merge
50
# The resolution is currently 'if left and right have texts that have
51
# the same "version" string, use left', aka "prefer-mine".
52
# We could introduce BASE, and cause conflicts, or appropriately
54
# Note also that this code is only invoked when there is a
55
# left-and-right change, so merging a pure-right change will take all
57
for right_ver, right_text in right_cl:
58
while len(left_cl) and left_cl[0][0] > right_ver:
59
(left_ver, left_text) = left_cl.pop(0)
60
content.append(left_text)
63
while len(left_cl) and left_cl[0][0] == right_ver:
64
(left_ver, left_text) = left_cl.pop(0)
66
content.append(right_text)
69
for left_ver, left_text in left_cl:
70
content.append(left_text)
76
def read_changelog(lines):
77
"""Return a parsed changelog file."""
80
(ver, text) = (None, "")
82
match = CL_RE.search(line)
85
ver = Version(match.group(2))
90
elif line.startswith(" -- "):
95
entries.append((ver, text))
96
(ver, text) = (None, "")
97
elif len(line.strip()) or ver is not None:
101
entries.append((ver, text))
105
########################################################################
106
# Version parsing code
107
########################################################################
108
# Regular expressions make validating things easy
109
valid_epoch = re.compile(r'^[0-9]+$')
110
valid_upstream = re.compile(r'^[A-Za-z0-9+:.~-]*$')
111
valid_revision = re.compile(r'^[A-Za-z0-9+.~]+$')
113
# Character comparison table for upstream and revision components
114
cmp_table = "~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-.:"
117
class Version(object):
118
"""Debian version number.
120
This class is designed to be reasonably transparent and allow you
123
| s.version >= '1.100-1'
125
The comparison will be done according to Debian rules, so '1.2' will
130
upstream Upstream version
131
revision Debian/local revision
134
def __init__(self, ver):
135
"""Parse a string or number into the three components."""
144
# Epoch is component before first colon
147
self.epoch = ver[:idx]
148
if not len(self.epoch):
150
if not valid_epoch.search(self.epoch):
154
# Revision is component after last hyphen
157
self.revision = ver[idx+1:]
158
if not len(self.revision):
160
if not valid_revision.search(self.revision):
164
# Remaining component is upstream
166
if not len(self.upstream):
168
if not valid_upstream.search(self.upstream):
171
self.epoch = int(self.epoch)
173
def getWithoutEpoch(self):
174
"""Return the version without the epoch."""
176
if self.revision is not None:
177
str += "-%s" % (self.revision,)
180
without_epoch = property(getWithoutEpoch)
183
"""Return the class as a string for printing."""
186
str += "%d:" % (self.epoch,)
188
if self.revision is not None:
189
str += "-%s" % (self.revision,)
193
"""Return a debugging representation of the object."""
194
return "<%s epoch: %d, upstream: %r, revision: %r>" \
195
% (self.__class__.__name__, self.epoch,
196
self.upstream, self.revision)
198
def __cmp__(self, other):
199
"""Compare two Version classes."""
200
other = Version(other)
202
result = cmp(self.epoch, other.epoch)
203
if result != 0: return result
205
result = deb_cmp(self.upstream, other.upstream)
206
if result != 0: return result
208
result = deb_cmp(self.revision or "", other.revision or "")
209
if result != 0: return result
214
def strcut(str, idx, accept):
215
"""Cut characters from str that are entirely in accept."""
217
while idx < len(str) and str[idx] in accept:
223
def deb_order(str, idx):
224
"""Return the comparison order of two characters."""
227
elif str[idx] == "~":
230
return cmp_table.index(str[idx])
232
def deb_cmp_str(x, y):
233
"""Compare two strings in a deb version."""
235
while (idx < len(x)) or (idx < len(y)):
236
result = deb_order(x, idx) - deb_order(y, idx)
247
"""Implement the string comparison outlined by Debian policy."""
249
while x_idx < len(x) or y_idx < len(y):
251
(x_str, x_idx) = strcut(x, x_idx, cmp_table)
252
(y_str, y_idx) = strcut(y, y_idx, cmp_table)
253
result = deb_cmp_str(x_str, y_str)
254
if result != 0: return result
257
(x_str, x_idx) = strcut(x, x_idx, "0123456789")
258
(y_str, y_idx) = strcut(y, y_idx, "0123456789")
259
result = cmp(int(x_str or "0"), int(y_str or "0"))
260
if result != 0: return result