summaryrefslogtreecommitdiff
path: root/debian/foo/changelog.py
diff options
context:
space:
mode:
Diffstat (limited to 'debian/foo/changelog.py')
-rw-r--r--debian/foo/changelog.py609
1 files changed, 609 insertions, 0 deletions
diff --git a/debian/foo/changelog.py b/debian/foo/changelog.py
new file mode 100644
index 0000000..e99c06a
--- /dev/null
+++ b/debian/foo/changelog.py
@@ -0,0 +1,609 @@
+# changelog.py -- Python module for Debian changelogs
+# Copyright (C) 2006-7 James Westby <jw+debian@jameswestby.net>
+# Copyright (C) 2008 Canonical Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+# The parsing code is based on that from dpkg which is:
+# Copyright 1996 Ian Jackson
+# Copyright 2005 Frank Lichtenheld <frank@lichtenheld.de>
+# and licensed under the same license as above.
+
+"""This module implements facilities to deal with Debian changelogs."""
+
+from __future__ import absolute_import
+
+import os
+import pwd
+import re
+import socket
+import warnings
+import sys
+
+import six
+
+from debian import debian_support
+
+# Python 3 doesn't have StandardError, but let's avoid changing our
+# exception inheritance hierarchy for Python 2.
+try:
+ _base_exception_class = StandardError
+except NameError:
+ _base_exception_class = Exception
+
+class ChangelogParseError(_base_exception_class):
+ """Indicates that the changelog could not be parsed"""
+ is_user_error = True
+
+ def __init__(self, line):
+ self._line=line
+
+ def __str__(self):
+ return "Could not parse changelog: "+self._line
+
+class ChangelogCreateError(_base_exception_class):
+ """Indicates that changelog could not be created, as all the information
+ required was not given"""
+
+class VersionError(_base_exception_class):
+ """Indicates that the version does not conform to the required format"""
+
+ is_user_error = True
+
+ def __init__(self, version):
+ self._version=version
+
+ def __str__(self):
+ return "Could not parse version: "+self._version
+
+# TODO(jsw): Remove this in favor of using debian_support.Version directly. I
+# don't think we gain anything by using this empty subclass.
+class Version(debian_support.Version):
+ """Represents a version of a Debian package."""
+ # debian_support.Version now has all the functionality we need
+
+class ChangeBlock(object):
+ """Holds all the information about one block from the changelog."""
+
+ def __init__(self, package=None, version=None, distributions=None,
+ urgency=None, urgency_comment=None, changes=None,
+ author=None, date=None, other_pairs=None, encoding='utf-8'):
+ self._raw_version = None
+ self._set_version(version)
+ self.package = package
+ self.distributions = distributions
+ self.urgency = urgency or "unknown"
+ self.urgency_comment = urgency_comment or ''
+ self._changes = changes
+ self.author = author
+ self.date = date
+ self._trailing = []
+ self.other_pairs = other_pairs or {}
+ self._encoding = encoding
+ self._no_trailer = False
+ self._trailer_separator = " "
+
+ def _set_version(self, version):
+ if version is not None:
+ self._raw_version = str(version)
+
+ def _get_version(self):
+ return Version(self._raw_version)
+
+ version = property(_get_version, _set_version)
+
+ def other_keys_normalised(self):
+ norm_dict = {}
+ for (key, value) in other_pairs.items():
+ key = key[0].upper() + key[1:].lower()
+ m = xbcs_re.match(key)
+ if m is None:
+ key = "XS-%s" % key
+ norm_dict[key] = value
+ return norm_dict
+
+ def changes(self):
+ return self._changes
+
+ def add_trailing_line(self, line):
+ self._trailing.append(line)
+
+ def add_change(self, change):
+ if self._changes is None:
+ self._changes = [change]
+ else:
+ #Bit of trickery to keep the formatting nicer with a blank
+ #line at the end if there is one
+ changes = self._changes
+ changes.reverse()
+ added = False
+ for i in range(len(changes)):
+ m = blankline.match(changes[i])
+ if m is None:
+ changes.insert(i, change)
+ added = True
+ break
+ changes.reverse()
+ if not added:
+ changes.append(change)
+ self._changes = changes
+
+ def _format(self):
+ # TODO(jsw): Switch to StringIO or a list to join at the end.
+ block = ""
+ if self.package is None:
+ raise ChangelogCreateError("Package not specified")
+ block += self.package + " "
+ if self._raw_version is None:
+ raise ChangelogCreateError("Version not specified")
+ block += "(" + self._raw_version + ") "
+ if self.distributions is None:
+ raise ChangelogCreateError("Distribution not specified")
+ block += self.distributions + "; "
+ if self.urgency is None:
+ raise ChangelogCreateError("Urgency not specified")
+ block += "urgency=" + self.urgency + self.urgency_comment
+ for (key, value) in self.other_pairs.items():
+ block += ", %s=%s" % (key, value)
+ block += '\n'
+ if self.changes() is None:
+ raise ChangelogCreateError("Changes not specified")
+ for change in self.changes():
+ block += change + "\n"
+ if not self._no_trailer:
+ if self.author is None:
+ raise ChangelogCreateError("Author not specified")
+ if self.date is None:
+ raise ChangelogCreateError("Date not specified")
+ block += " -- " + self.author + self._trailer_separator \
+ + self.date + "\n"
+ for line in self._trailing:
+ block += line + "\n"
+ return block
+
+ if sys.version >= '3':
+ __str__ = _format
+
+ def __bytes__(self):
+ return str(self).encode(self._encoding)
+ else:
+ __unicode__ = _format
+
+ def __str__(self):
+ return unicode(self).encode(self._encoding)
+
+topline = re.compile(r'^(\w%(name_chars)s*) \(([^\(\) \t]+)\)'
+ '((\s+%(name_chars)s+)+)\;'
+ % {'name_chars': '[-+0-9a-z.]'},
+ re.IGNORECASE)
+blankline = re.compile('^\s*$')
+change = re.compile('^\s\s+.*$')
+endline = re.compile('^ -- (.*) <(.*)>( ?)((\w+\,\s*)?\d{1,2}\s+\w+\s+'
+ '\d{4}\s+\d{1,2}:\d\d:\d\d\s+[-+]\d{4}(\s+\([^\\\(\)]\))?\s*)$')
+endline_nodetails = re.compile('^ --(?: (.*) <(.*)>( ?)((\w+\,\s*)?\d{1,2}'
+ '\s+\w+\s+\d{4}\s+\d{1,2}:\d\d:\d\d\s+[-+]\d{4}'
+ '(\s+\([^\\\(\)]\))?))?\s*$')
+keyvalue= re.compile('^([-0-9a-z]+)=\s*(.*\S)$', re.IGNORECASE)
+value_re = re.compile('^([-0-9a-z]+)((\s+.*)?)$', re.IGNORECASE)
+xbcs_re = re.compile('^X[BCS]+-', re.IGNORECASE)
+emacs_variables = re.compile('^(;;\s*)?Local variables:', re.IGNORECASE)
+vim_variables = re.compile('^vim:', re.IGNORECASE)
+cvs_keyword = re.compile('^\$\w+:.*\$')
+comments = re.compile('^\# ')
+more_comments = re.compile('^/\*.*\*/')
+
+old_format_re1 = re.compile('^(\w+\s+\w+\s+\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}'
+ '\s+[\w\s]*\d{4})\s+(.*)\s+(<|\()(.*)(\)|>)')
+old_format_re2 = re.compile('^(\w+\s+\w+\s+\d{1,2},?\s*\d{4})\s+(.*)'
+ '\s+(<|\()(.*)(\)|>)')
+old_format_re3 = re.compile('^(\w[-+0-9a-z.]*) \(([^\(\) \t]+)\)\;?',
+ re.IGNORECASE)
+old_format_re4 = re.compile('^([\w.+-]+)(-| )(\S+) Debian (\S+)',
+ re.IGNORECASE)
+old_format_re5 = re.compile('^Changes from version (.*) to (.*):',
+ re.IGNORECASE)
+old_format_re6 = re.compile('^Changes for [\w.+-]+-[\w.+-]+:?\s*$',
+ re.IGNORECASE)
+old_format_re7 = re.compile('^Old Changelog:\s*$', re.IGNORECASE)
+old_format_re8 = re.compile('^(?:\d+:)?\w[\w.+~-]*:?\s*$')
+
+
+class Changelog(object):
+ """Represents a debian/changelog file."""
+
+ # TODO(jsw): Avoid masking the 'file' built-in.
+ def __init__(self, file=None, max_blocks=None,
+ allow_empty_author=False, strict=True, encoding='utf-8'):
+ """Initializer.
+
+ Args:
+ file: The contents of the changelog, either as a str, unicode object,
+ or an iterator of lines (each of which is either a str or unicode)
+ max_blocks: The maximum number of blocks to parse from the input.
+ (Default: no limit)
+ allow_empty_author: Whether to allow an empty author in the trailer
+ line of a change block. (Default: False)
+ strict: Whether to raise an exception if there are errors. (Default:
+ use a warning)
+ encoding: If the input is a str or iterator of str, the encoding to
+ use when interpreting the input.
+ """
+ self._encoding = encoding
+ self._blocks = []
+ self.initial_blank_lines = []
+ if file is not None:
+ try:
+ self.parse_changelog(file, max_blocks=max_blocks,
+ allow_empty_author=allow_empty_author,
+ strict=strict)
+ except ChangelogParseError:
+ pass
+
+ def _parse_error(self, message, strict):
+ if strict:
+ raise ChangelogParseError(message)
+ else:
+ warnings.warn(message)
+
+ def parse_changelog(self, file, max_blocks=None,
+ allow_empty_author=False, strict=True, encoding=None):
+ first_heading = "first heading"
+ next_heading_or_eof = "next heading of EOF"
+ start_of_change_data = "start of change data"
+ more_changes_or_trailer = "more change data or trailer"
+ slurp_to_end = "slurp to end"
+
+ encoding = encoding or self._encoding
+
+ if file is None:
+ self._parse_error('Empty changelog file.', strict)
+ return
+
+ self._blocks = []
+ self.initial_blank_lines = []
+
+ current_block = ChangeBlock(encoding=encoding)
+ changes = []
+
+ state = first_heading
+ old_state = None
+ if isinstance(file, bytes):
+ file = file.decode(encoding)
+ if isinstance(file, six.string_types):
+ # Make sure the changelog file is not empty.
+ if len(file.strip()) == 0:
+ self._parse_error('Empty changelog file.', strict)
+ return
+
+ file = file.splitlines()
+ for line in file:
+ if not isinstance(line, six.text_type):
+ line = line.decode(encoding)
+ # Support both lists of lines without the trailing newline and
+ # those with trailing newlines (e.g. when given a file object
+ # directly)
+ line = line.rstrip('\n')
+ if state == first_heading or state == next_heading_or_eof:
+ top_match = topline.match(line)
+ blank_match = blankline.match(line)
+ if top_match is not None:
+ if (max_blocks is not None
+ and len(self._blocks) >= max_blocks):
+ return
+ current_block.package = top_match.group(1)
+ current_block._raw_version = top_match.group(2)
+ current_block.distributions = top_match.group(3).lstrip()
+
+ pairs = line.split(";", 1)[1]
+ all_keys = {}
+ other_pairs = {}
+ for pair in pairs.split(','):
+ pair = pair.strip()
+ kv_match = keyvalue.match(pair)
+ if kv_match is None:
+ self._parse_error("Invalid key-value "
+ "pair after ';': %s" % pair, strict)
+ continue
+ key = kv_match.group(1)
+ value = kv_match.group(2)
+ if key.lower() in all_keys:
+ self._parse_error("Repeated key-value: "
+ "%s" % key.lower(), strict)
+ all_keys[key.lower()] = value
+ if key.lower() == "urgency":
+ val_match = value_re.match(value)
+ if val_match is None:
+ self._parse_error("Badly formatted "
+ "urgency value: %s" % value, strict)
+ else:
+ current_block.urgency = val_match.group(1)
+ comment = val_match.group(2)
+ if comment is not None:
+ current_block.urgency_comment = comment
+ else:
+ other_pairs[key] = value
+ current_block.other_pairs = other_pairs
+ state = start_of_change_data
+ elif blank_match is not None:
+ if state == first_heading:
+ self.initial_blank_lines.append(line)
+ else:
+ self._blocks[-1].add_trailing_line(line)
+ else:
+ emacs_match = emacs_variables.match(line)
+ vim_match = vim_variables.match(line)
+ cvs_match = cvs_keyword.match(line)
+ comments_match = comments.match(line)
+ more_comments_match = more_comments.match(line)
+ if ((emacs_match is not None or vim_match is not None)
+ and state != first_heading):
+ self._blocks[-1].add_trailing_line(line)
+ old_state = state
+ state = slurp_to_end
+ continue
+ if (cvs_match is not None or comments_match is not None
+ or more_comments_match is not None):
+ if state == first_heading:
+ self.initial_blank_lines.append(line)
+ else:
+ self._blocks[-1].add_trailing_line(line)
+ continue
+ if ((old_format_re1.match(line) is not None
+ or old_format_re2.match(line) is not None
+ or old_format_re3.match(line) is not None
+ or old_format_re4.match(line) is not None
+ or old_format_re5.match(line) is not None
+ or old_format_re6.match(line) is not None
+ or old_format_re7.match(line) is not None
+ or old_format_re8.match(line) is not None)
+ and state != first_heading):
+ self._blocks[-1].add_trailing_line(line)
+ old_state = state
+ state = slurp_to_end
+ continue
+ self._parse_error("Unexpected line while looking "
+ "for %s: %s" % (state, line), strict)
+ if state == first_heading:
+ self.initial_blank_lines.append(line)
+ else:
+ self._blocks[-1].add_trailing_line(line)
+ elif (state == start_of_change_data
+ or state == more_changes_or_trailer):
+ change_match = change.match(line)
+ end_match = endline.match(line)
+ end_no_details_match = endline_nodetails.match(line)
+ blank_match = blankline.match(line)
+ if change_match is not None:
+ changes.append(line)
+ state = more_changes_or_trailer
+ elif end_match is not None:
+ if end_match.group(3) != ' ':
+ self._parse_error("Badly formatted trailer "
+ "line: %s" % line, strict)
+ current_block._trailer_separator = end_match.group(3)
+ current_block.author = "%s <%s>" \
+ % (end_match.group(1), end_match.group(2))
+ current_block.date = end_match.group(4)
+ current_block._changes = changes
+ self._blocks.append(current_block)
+ changes = []
+ current_block = ChangeBlock(encoding=encoding)
+ state = next_heading_or_eof
+ elif end_no_details_match is not None:
+ if not allow_empty_author:
+ self._parse_error("Badly formatted trailer "
+ "line: %s" % line, strict)
+ continue
+ current_block._changes = changes
+ self._blocks.append(current_block)
+ changes = []
+ current_block = ChangeBlock(encoding=encoding)
+ state = next_heading_or_eof
+ elif blank_match is not None:
+ changes.append(line)
+ else:
+ cvs_match = cvs_keyword.match(line)
+ comments_match = comments.match(line)
+ more_comments_match = more_comments.match(line)
+ if (cvs_match is not None or comments_match is not None
+ or more_comments_match is not None):
+ changes.append(line)
+ continue
+ self._parse_error("Unexpected line while looking "
+ "for %s: %s" % (state, line), strict)
+ changes.append(line)
+ elif state == slurp_to_end:
+ if old_state == next_heading_or_eof:
+ self._blocks[-1].add_trailing_line(line)
+ else:
+ changes.append(line)
+ else:
+ assert False, "Unknown state: %s" % state
+
+ if ((state != next_heading_or_eof and state != slurp_to_end)
+ or (state == slurp_to_end and old_state != next_heading_or_eof)):
+ self._parse_error("Found eof where expected %s" % state,
+ strict)
+ current_block._changes = changes
+ current_block._no_trailer = True
+ self._blocks.append(current_block)
+
+ def get_version(self):
+ """Return a Version object for the last version"""
+ return self._blocks[0].version
+
+ def set_version(self, version):
+ """Set the version of the last changelog block
+
+ version can be a full version string, or a Version object
+ """
+ self._blocks[0].version = Version(version)
+
+ version = property(get_version, set_version,
+ doc="Version object for last changelog block""")
+
+ ### For convenience, let's expose some of the version properties
+ full_version = property(lambda self: self.version.full_version)
+ epoch = property(lambda self: self.version.epoch)
+ debian_version = property(lambda self: self.version.debian_revision)
+ debian_revision = property(lambda self: self.version.debian_revision)
+ upstream_version = property(lambda self: self.version.upstream_version)
+
+ def get_package(self):
+ """Returns the name of the package in the last version."""
+ return self._blocks[0].package
+
+ def set_package(self, package):
+ self._blocks[0].package = package
+
+ package = property(get_package, set_package,
+ doc="Name of the package in the last version")
+
+ def get_versions(self):
+ """Returns a list of version objects that the package went through."""
+ return [block.version for block in self._blocks]
+
+ versions = property(get_versions,
+ doc="List of version objects the package went through")
+
+ def _raw_versions(self):
+ return [block._raw_version for block in self._blocks]
+
+ def _format(self):
+ pieces = []
+ pieces.append(six.u('\n').join(self.initial_blank_lines))
+ for block in self._blocks:
+ pieces.append(six.text_type(block))
+ return six.u('').join(pieces)
+
+ if sys.version >= '3':
+ __str__ = _format
+
+ def __bytes__(self):
+ return str(self).encode(self._encoding)
+ else:
+ __unicode__ = _format
+
+ def __str__(self):
+ return unicode(self).encode(self._encoding)
+
+ def __iter__(self):
+ return iter(self._blocks)
+
+ def __len__(self):
+ return len(self._blocks)
+
+ def set_distributions(self, distributions):
+ self._blocks[0].distributions = distributions
+ distributions = property(lambda self: self._blocks[0].distributions,
+ set_distributions)
+
+ def set_urgency(self, urgency):
+ self._blocks[0].urgency = urgency
+ urgency = property(lambda self: self._blocks[0].urgency, set_urgency)
+
+ def add_change(self, change):
+ self._blocks[0].add_change(change)
+
+ def set_author(self, author):
+ self._blocks[0].author = author
+ author = property(lambda self: self._blocks[0].author, set_author)
+
+ def set_date(self, date):
+ self._blocks[0].date = date
+ date = property(lambda self: self._blocks[0].date, set_date)
+
+ def new_block(self, **kwargs):
+ kwargs.setdefault('encoding', self._encoding)
+ block = ChangeBlock(**kwargs)
+ block.add_trailing_line('')
+ self._blocks.insert(0, block)
+
+ def write_to_open_file(self, file):
+ file.write(self.__str__())
+
+
+def get_maintainer():
+ """Get the maintainer information in the same manner as dch.
+
+ This function gets the information about the current user for
+ the maintainer field using environment variables of gecos
+ informations as approriate.
+
+ It uses the same methods as dch to get the information, namely
+ DEBEMAIL, DEBFULLNAME, EMAIL, NAME, /etc/mailname and gecos.
+
+ :returns: a tuple of the full name, email pair as strings.
+ Either of the pair may be None if that value couldn't
+ be determined.
+ """
+ env = os.environ
+ regex = re.compile(r"^(.*)\s+<(.*)>$")
+
+ # Split email and name
+ if 'DEBEMAIL' in env:
+ match_obj = regex.match(env['DEBEMAIL'])
+ if match_obj:
+ if not 'DEBFULLNAME' in env:
+ env['DEBFULLNAME'] = match_obj.group(1)
+ env['DEBEMAIL'] = match_obj.group(2)
+ if 'DEBEMAIL' not in env or 'DEBFULLNAME' not in env:
+ if 'EMAIL' in env:
+ match_obj = regex.match(env['EMAIL'])
+ if match_obj:
+ if not 'DEBFULLNAME' in env:
+ env['DEBFULLNAME'] = match_obj.group(1)
+ env['EMAIL'] = match_obj.group(2)
+
+ # Get maintainer's name
+ if 'DEBFULLNAME' in env:
+ maintainer = env['DEBFULLNAME']
+ elif 'NAME' in env:
+ maintainer = env['NAME']
+ else:
+ # Use password database if no data in environment variables
+ try:
+ maintainer = re.sub(r',.*', '', pwd.getpwuid(os.getuid()).pw_gecos)
+ except (KeyError, AttributeError):
+ maintainer = None
+
+ # Get maintainer's mail address
+ if 'DEBEMAIL' in env:
+ email = env['DEBEMAIL']
+ elif 'EMAIL' in env:
+ email = env['EMAIL']
+ else:
+ addr = None
+ if os.path.exists('/etc/mailname'):
+ f = open('/etc/mailname')
+ try:
+ addr = f.readline().strip()
+ finally:
+ f.close()
+ if not addr:
+ addr = socket.getfqdn()
+ if addr:
+ user = pwd.getpwuid(os.getuid()).pw_name
+ if not user:
+ addr = None
+ else:
+ addr = "%s@%s" % (user, addr)
+
+ if addr:
+ email = addr
+ else:
+ email = None
+
+ return (maintainer, email)