[ubuntu-x] Script to merge changelogs

Bryce Harrington bryce at canonical.com
Tue Jun 30 21:32:30 BST 2009


For anyone who does a lot of merges of stuff from debian (such as for
xorg-edgers), one irritating chore is syncing up the changelogs so they
carry the ubuntu change history.

Attached is a script that does this.  I extracted it from Merge-o-Matic,
because it had pretty smart changelog merge heuristics, and made a few
tweaks to make it handle epoch stuff correctly, etc.

Example of use:

  merge_changelog xserver-xorg-video-ati-6.12.2/debian/changelog \
                  xserver-xorg-video-ati-6.12.99+git20090629.f39cafc5/debian/changelog \
                > xserver-xorg-video-ati-6.12.99+git20090629.f39cafc5/debian/changelog.new

Then review the changelog.new and if it looks good, 

  mv debian/changelog.new debian/changelog

Bryce
-------------- next part --------------
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright ? 2008 Canonical Ltd.
# Author: Scott James Remnant <scott at ubuntu.com>.
# Hacked up by: Bryce Harrington <bryce at ubuntu.com>
#
# 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 <http://www.gnu.org/licenses/>.

import os, sys, re, time, logging

from stat import *
from textwrap import fill

def usage():
    print '''Usage: merge_changelog <left changelog> <right 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(1)

########################################################################
# 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_ver, left_text in left_cl:
        print left_text
	    
    return False

def read_changelog(filename):
    """Return a parsed changelog file."""
    entries = []

    cl = open(filename)
    try:
        (ver, text) = (None, "")
        for line in cl:
            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:
        cl.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 getWithoutEpoch(self):
        """Return the version without the epoch."""
        str = self.upstream
        if self.revision is not None:
            str += "-%s" % (self.revision,)
        return str

    without_epoch = property(getWithoutEpoch)

    def __str__(self):
        """Return the class as a string for printing."""
        str = ""
        if self.epoch > 0:
            str += "%d:" % (self.epoch,)
        str += self.upstream
        if self.revision is not None:
            str += "-%s" % (self.revision,)
        return str

    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(str, idx, accept):
    """Cut characters from str that are entirely in accept."""
    ret = ""
    while idx < len(str) and str[idx] in accept:
        ret += str[idx]
        idx += 1

    return (ret, idx)

def deb_order(str, idx):
    """Return the comparison order of two characters."""
    if idx >= len(str):
        return 0
    elif str[idx] == "~":
        return -1
    else:
        return cmp_table.index(str[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


if __name__ == '__main__':
    if len(sys.argv) != 3:
        usage()
    
    left_changelog = sys.argv[1]
    right_changelog = sys.argv[2]

    merge_changelog(left_changelog, right_changelog)
    sys.exit(0)


More information about the Ubuntu-x mailing list