diff options
Diffstat (limited to 'gbp/rpm/changelog.py')
-rw-r--r-- | gbp/rpm/changelog.py | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/gbp/rpm/changelog.py b/gbp/rpm/changelog.py new file mode 100644 index 0000000..3ed89f1 --- /dev/null +++ b/gbp/rpm/changelog.py @@ -0,0 +1,246 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2014-2015 Intel Corporation <markus.lehtonen@linux.intel.com> +# 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, please see +# <http://www.gnu.org/licenses/> +"""An RPM Changelog""" + +import datetime +import re + +import gbp.log + + +class ChangelogError(Exception): + """Problem parsing changelog""" + pass + + +class _ChangelogHeader(object): + """The header part of one changelog section""" + + def __init__(self, pkgpolicy, time=None, **kwargs): + self._pkgpolicy = pkgpolicy + self._data = {'time': time} + self._data.update(kwargs) + + def __contains__(self, key): + return key in self._data + + def __getitem__(self, key): + if key in self._data: + return self._data[key] + return None + + def __str__(self): + keys = dict(self._data) + keys['time'] = self._data['time'].strftime( + self._pkgpolicy.Changelog.header_time_format) + try: + return self._pkgpolicy.Changelog.header_format % keys + '\n' + except KeyError as err: + raise ChangelogError("Unable to format changelog header, missing " + "property %s" % err) + + +class _ChangelogEntry(object): + """An entry (one 'change') in an RPM changelog""" + + def __init__(self, pkgpolicy, author, text): + """ + @param pkgpolicy: RPM packaging policy + @type pkgpolicy: L{RpmPkgPolicy} + @param author: author of the change + @type author: C{str} + @param text: message of the changelog entry + @type text: C{str} or C{list} of C{str} + """ + self._pkgpolicy = pkgpolicy + self.author = author + if isinstance(text, str): + self._text = text.splitlines() + else: + self._text = text + # Strip trailing empty lines + while text and not text[-1].strip(): + text.pop() + + def __str__(self): + # Currently no (re-)formatting, just raw text + string = "" + for line in self._text: + string += line + '\n' + return string + + +class _ChangelogSection(object): + """One section (set of changes) in an RPM changelog""" + + def __init__(self, pkgpolicy, *args, **kwargs): + self._pkgpolicy = pkgpolicy + self.header = _ChangelogHeader(pkgpolicy, *args, **kwargs) + self.entries = [] + self._trailer = '\n' + + + def __str__(self): + text = str(self.header) + for entry in self.entries: + text += str(entry) + # Add "section separator" + text += self._trailer + return text + + def set_header(self, *args, **kwargs): + """Change the section header""" + self.header = _ChangelogHeader(self._pkgpolicy, *args, **kwargs) + + def append_entry(self, entry): + """Add a new entry to the end of the list of entries""" + self.entries.append(entry) + return entry + + +class Changelog(object): + """An RPM changelog""" + + def __init__(self, pkgpolicy): + self._pkgpolicy = pkgpolicy + self.sections = [] + + def __str__(self): + string = "" + for section in self.sections: + string += str(section) + return string + + def create_entry(self, *args, **kwargs): + """Create and return new entry object""" + return _ChangelogEntry(self._pkgpolicy, *args, **kwargs) + + def add_section(self, *args, **kwargs): + """Add new empty section""" + section = _ChangelogSection(self._pkgpolicy, *args, **kwargs) + self.sections.insert(0, section) + return section + + +class ChangelogParser(object): + """Parser for RPM changelogs""" + + def __init__(self, pkgpolicy): + self._pkgpolicy = pkgpolicy + self.section_match_re = pkgpolicy.Changelog.section_match_re + self.section_split_re = pkgpolicy.Changelog.section_split_re + self.header_split_re = pkgpolicy.Changelog.header_split_re + self.header_name_split_re = pkgpolicy.Changelog.header_name_split_re + self.body_name_re = pkgpolicy.Changelog.body_name_re + + def raw_parse_string(self, string): + """Parse changelog - only splits out raw changelog sections.""" + changelog = Changelog(self._pkgpolicy) + ch_section = "" + for line in string.splitlines(): + if re.match(self.section_match_re, line, re.M | re.S): + if ch_section: + changelog.sections.append(ch_section) + ch_section = line + '\n' + elif ch_section: + ch_section += line + '\n' + else: + raise ChangelogError("First line in changelog is invalid") + if ch_section: + changelog.sections.append(ch_section) + return changelog + + def raw_parse_file(self, changelog): + """Parse changelog file - only splits out raw changelog sections.""" + try: + with open(changelog) as ch_file: + return self.raw_parse_string(ch_file.read()) + except IOError as err: + raise ChangelogError("Unable to read changelog file: %s" % err) + + def _parse_section_header(self, text): + """Parse one changelog section header""" + # Try to split out time stamp and "changelog name" + match = re.match(self.header_split_re, text, re.M) + if not match: + raise ChangelogError("Unable to parse changelog header: %s" % text) + try: + time = datetime.datetime.strptime(match.group('ch_time'), + "%a %b %d %Y") + except ValueError: + raise ChangelogError("Unable to parse changelog header: invalid " + "timestamp '%s'" % match.group('ch_time')) + # Parse "name" part which consists of name and/or email and an optional + # revision + name_text = match.group('ch_name') + match = re.match(self.header_name_split_re, name_text) + if not match: + raise ChangelogError("Unable to parse changelog header: invalid " + "name / revision '%s'" % name_text) + kwargs = match.groupdict() + return _ChangelogSection(self._pkgpolicy, time=time, **kwargs) + + def _create_entry(self, author, text): + """Create a new changelog entry""" + return _ChangelogEntry(self._pkgpolicy, author=author, text=text) + + def _parse_section_entries(self, text, default_author): + """Parse entries from a string and add them to a section""" + entries = [] + entry_text = [] + author = default_author + for line in text.splitlines(): + match = re.match(self.body_name_re, line) + if match: + if entry_text: + entries.append(self._create_entry(author, entry_text)) + author = match.group('name') + else: + if line.startswith("-"): + if entry_text: + entries.append(self._create_entry(author, entry_text)) + entry_text = [line] + else: + if not entry_text: + gbp.log.info("First changelog entry (%s) is garbled, " + "entries should start with a dash ('-')" % + line) + entry_text.append(line) + if entry_text: + entries.append(self._create_entry(author, entry_text)) + + return entries + + + def parse_section(self, text): + """Parse one section""" + # Check that the first line(s) look like a changelog header + match = re.match(self.section_split_re, text, re.M | re.S) + if not match: + raise ChangelogError("Doesn't look like changelog header: %s..." % + text.splitlines()[0]) + # Parse header + section = self._parse_section_header(match.group('ch_header')) + header = section.header + # Parse entries + default_author = header['name'] if 'name' in header else header['email'] + for entry in self._parse_section_entries(match.group('ch_body'), + default_author): + section.append_entry(entry) + + return section + |