#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright © 2008 Canonical Ltd. # Author: Scott James Remnant . # Hacked up by: Bryce Harrington # Change merge_changelog to merge-changelog: Ryan Kavanagh # # # This program is free software: you can redistribute it and/or modify # it under the terms of version 3 of the GNU General Public License as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import re import sys def usage(exit_code=1): print '''Usage: merge-changelog merge-changelog takes two changelogs that once shared a common source, merges them back together, and prints the merged result to stdout. This is useful if you need to manually merge a ubuntu package with a new Debian release of the package. ''' sys.exit(exit_code) ######################################################################## # Changelog Management ######################################################################## # Regular expression for top of debian/changelog CL_RE = re.compile(r'^(\w[-+0-9a-z.]*) \(([^\(\) \t]+)\)((\s+[-0-9a-z]+)+)\;', re.IGNORECASE) def merge_changelog(left_changelog, right_changelog): """Merge a changelog file.""" left_cl = read_changelog(left_changelog) right_cl = read_changelog(right_changelog) for right_ver, right_text in right_cl: while len(left_cl) and left_cl[0][0] > right_ver: (left_ver, left_text) = left_cl.pop(0) print left_text while len(left_cl) and left_cl[0][0] == right_ver: (left_ver, left_text) = left_cl.pop(0) print right_text for _, left_text in left_cl: print left_text return False def read_changelog(filename): """Return a parsed changelog file.""" entries = [] changelog_file = open(filename) try: (ver, text) = (None, "") for line in changelog_file: match = CL_RE.search(line) if match: try: ver = Version(match.group(2)) except ValueError: ver = None text += line elif line.startswith(" -- "): if ver is None: ver = Version("0") text += line entries.append((ver, text)) (ver, text) = (None, "") elif len(line.strip()) or ver is not None: text += line finally: changelog_file.close() if len(text): entries.append((ver, text)) return entries ######################################################################## # Version parsing code ######################################################################## # Regular expressions make validating things easy VALID_EPOCH = re.compile(r'^[0-9]+$') VALID_UPSTREAM = re.compile(r'^[A-Za-z0-9+:.~-]*$') VALID_REVISION = re.compile(r'^[A-Za-z0-9+.~]+$') # Character comparison table for upstream and revision components CMP_TABLE = "~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-.:" class Version(object): """Debian version number. This class is designed to be reasonably transparent and allow you to write code like: | s.version >= '1.100-1' The comparison will be done according to Debian rules, so '1.2' will compare lower. Properties: epoch Epoch upstream Upstream version revision Debian/local revision """ def __init__(self, ver): """Parse a string or number into the three components.""" self.epoch = 0 self.upstream = None self.revision = None ver = str(ver) if not len(ver): raise ValueError # Epoch is component before first colon idx = ver.find(":") if idx != -1: self.epoch = ver[:idx] if not len(self.epoch): raise ValueError if not VALID_EPOCH.search(self.epoch): raise ValueError ver = ver[idx+1:] # Revision is component after last hyphen idx = ver.rfind("-") if idx != -1: self.revision = ver[idx+1:] if not len(self.revision): raise ValueError if not VALID_REVISION.search(self.revision): raise ValueError ver = ver[:idx] # Remaining component is upstream self.upstream = ver if not len(self.upstream): raise ValueError if not VALID_UPSTREAM.search(self.upstream): raise ValueError self.epoch = int(self.epoch) def get_without_epoch(self): """Return the version without the epoch.""" string = self.upstream if self.revision is not None: string += "-%s" % (self.revision,) return string without_epoch = property(get_without_epoch) def __str__(self): """Return the class as a string for printing.""" string = "" if self.epoch > 0: string += "%d:" % (self.epoch,) string += self.upstream if self.revision is not None: string += "-%s" % (self.revision,) return string def __repr__(self): """Return a debugging representation of the object.""" return "<%s epoch: %d, upstream: %r, revision: %r>" \ % (self.__class__.__name__, self.epoch, self.upstream, self.revision) def __cmp__(self, other): """Compare two Version classes.""" other = Version(other) result = cmp(self.epoch, other.epoch) if result != 0: return result result = deb_cmp(self.upstream, other.upstream) if result != 0: return result result = deb_cmp(self.revision or "", other.revision or "") if result != 0: return result return 0 def strcut(string, idx, accept): """Cut characters from string that are entirely in accept.""" ret = "" while idx < len(string) and string[idx] in accept: ret += string[idx] idx += 1 return (ret, idx) def deb_order(string, idx): """Return the comparison order of two characters.""" if idx >= len(string): return 0 elif string[idx] == "~": return -1 else: return CMP_TABLE.index(string[idx]) def deb_cmp_str(x, y): """Compare two strings in a deb version.""" idx = 0 while (idx < len(x)) or (idx < len(y)): result = deb_order(x, idx) - deb_order(y, idx) if result < 0: return -1 elif result > 0: return 1 idx += 1 return 0 def deb_cmp(x, y): """Implement the string comparison outlined by Debian policy.""" x_idx = y_idx = 0 while x_idx < len(x) or y_idx < len(y): # Compare strings (x_str, x_idx) = strcut(x, x_idx, CMP_TABLE) (y_str, y_idx) = strcut(y, y_idx, CMP_TABLE) result = deb_cmp_str(x_str, y_str) if result != 0: return result # Compare numbers (x_str, x_idx) = strcut(x, x_idx, "0123456789") (y_str, y_idx) = strcut(y, y_idx, "0123456789") result = cmp(int(x_str or "0"), int(y_str or "0")) if result != 0: return result return 0 def main(): if len(sys.argv) > 1 and sys.argv[1] in ('-h', '--help'): usage(0) if len(sys.argv) != 3: usage(1) left_changelog = sys.argv[1] right_changelog = sys.argv[2] merge_changelog(left_changelog, right_changelog) sys.exit(0) if __name__ == '__main__': main()