changelog.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. # vim: set fileencoding=utf-8 :
  2. #
  3. # (C) 2014-2015 Intel Corporation <markus.lehtonen@linux.intel.com>
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, please see
  16. # <http://www.gnu.org/licenses/>
  17. """An RPM Changelog"""
  18. import locale
  19. import datetime
  20. import re
  21. from functools import wraps
  22. import gbp.log
  23. def c_locale(category):
  24. def _decorator(f):
  25. @wraps(f)
  26. def wrapper(*args, **kwargs):
  27. saved = locale.setlocale(category, None)
  28. locale.setlocale(category, 'C')
  29. ret = f(*args, **kwargs)
  30. locale.setlocale(category, saved)
  31. return ret
  32. return wrapper
  33. return _decorator
  34. class ChangelogError(Exception):
  35. """Problem parsing changelog"""
  36. pass
  37. class _ChangelogHeader(object):
  38. """The header part of one changelog section"""
  39. def __init__(self, pkgpolicy, time=None, **kwargs):
  40. self._pkgpolicy = pkgpolicy
  41. self._data = {'time': time}
  42. self._data.update(kwargs)
  43. def __contains__(self, key):
  44. return key in self._data
  45. def __getitem__(self, key):
  46. if key in self._data:
  47. return self._data[key]
  48. return None
  49. @c_locale(locale.LC_TIME)
  50. def __str__(self):
  51. keys = dict(self._data)
  52. keys['time'] = self._data['time'].strftime(
  53. self._pkgpolicy.Changelog.header_time_format)
  54. try:
  55. return self._pkgpolicy.Changelog.header_format % keys + '\n'
  56. except KeyError as err:
  57. raise ChangelogError("Unable to format changelog header, missing "
  58. "property %s" % err)
  59. class _ChangelogEntry(object):
  60. """An entry (one 'change') in an RPM changelog"""
  61. def __init__(self, pkgpolicy, author, text):
  62. """
  63. @param pkgpolicy: RPM packaging policy
  64. @type pkgpolicy: L{RpmPkgPolicy}
  65. @param author: author of the change
  66. @type author: C{str}
  67. @param text: message of the changelog entry
  68. @type text: C{str} or C{list} of C{str}
  69. """
  70. self._pkgpolicy = pkgpolicy
  71. self.author = author
  72. if isinstance(text, str):
  73. self._text = text.splitlines()
  74. else:
  75. self._text = text
  76. # Strip trailing empty lines
  77. while text and not text[-1].strip():
  78. text.pop()
  79. def __str__(self):
  80. # Currently no (re-)formatting, just raw text
  81. string = ""
  82. for line in self._text:
  83. string += line + '\n'
  84. return string
  85. class _ChangelogSection(object):
  86. """One section (set of changes) in an RPM changelog"""
  87. def __init__(self, pkgpolicy, *args, **kwargs):
  88. self._pkgpolicy = pkgpolicy
  89. self.header = _ChangelogHeader(pkgpolicy, *args, **kwargs)
  90. self.entries = []
  91. self._trailer = '\n'
  92. def __str__(self):
  93. text = str(self.header)
  94. for entry in self.entries:
  95. text += str(entry)
  96. # Add "section separator"
  97. text += self._trailer
  98. return text
  99. def set_header(self, *args, **kwargs):
  100. """Change the section header"""
  101. self.header = _ChangelogHeader(self._pkgpolicy, *args, **kwargs)
  102. def append_entry(self, entry):
  103. """Add a new entry to the end of the list of entries"""
  104. self.entries.append(entry)
  105. return entry
  106. class Changelog(object):
  107. """An RPM changelog"""
  108. def __init__(self, pkgpolicy):
  109. self._pkgpolicy = pkgpolicy
  110. self.sections = []
  111. def __str__(self):
  112. string = ""
  113. for section in self.sections:
  114. string += str(section)
  115. return string
  116. def create_entry(self, *args, **kwargs):
  117. """Create and return new entry object"""
  118. return _ChangelogEntry(self._pkgpolicy, *args, **kwargs)
  119. def add_section(self, *args, **kwargs):
  120. """Add new empty section"""
  121. section = _ChangelogSection(self._pkgpolicy, *args, **kwargs)
  122. self.sections.insert(0, section)
  123. return section
  124. class ChangelogParser(object):
  125. """Parser for RPM changelogs"""
  126. def __init__(self, pkgpolicy):
  127. self._pkgpolicy = pkgpolicy
  128. self.section_match_re = pkgpolicy.Changelog.section_match_re
  129. self.section_split_re = pkgpolicy.Changelog.section_split_re
  130. self.header_split_re = pkgpolicy.Changelog.header_split_re
  131. self.header_name_split_re = pkgpolicy.Changelog.header_name_split_re
  132. self.body_name_re = pkgpolicy.Changelog.body_name_re
  133. def raw_parse_string(self, string):
  134. """Parse changelog - only splits out raw changelog sections."""
  135. changelog = Changelog(self._pkgpolicy)
  136. ch_section = ""
  137. for line in string.splitlines():
  138. if re.match(self.section_match_re, line, re.M | re.S):
  139. if ch_section:
  140. changelog.sections.append(ch_section)
  141. ch_section = line + '\n'
  142. elif ch_section:
  143. ch_section += line + '\n'
  144. else:
  145. raise ChangelogError("First line in changelog is invalid")
  146. if ch_section:
  147. changelog.sections.append(ch_section)
  148. return changelog
  149. def raw_parse_file(self, changelog):
  150. """Parse changelog file - only splits out raw changelog sections."""
  151. try:
  152. with open(changelog) as ch_file:
  153. return self.raw_parse_string(ch_file.read())
  154. except IOError as err:
  155. raise ChangelogError("Unable to read changelog file: %s" % err)
  156. @c_locale(locale.LC_TIME)
  157. def _parse_section_header(self, text):
  158. """Parse one changelog section header"""
  159. # Try to split out time stamp and "changelog name"
  160. match = re.match(self.header_split_re, text, re.M)
  161. if not match:
  162. raise ChangelogError("Unable to parse changelog header: %s" % text)
  163. try:
  164. time = datetime.datetime.strptime(match.group('ch_time'),
  165. "%a %b %d %Y")
  166. except ValueError:
  167. raise ChangelogError("Unable to parse changelog header: invalid "
  168. "timestamp '%s'" % match.group('ch_time'))
  169. # Parse "name" part which consists of name and/or email and an optional
  170. # revision
  171. name_text = match.group('ch_name')
  172. match = re.match(self.header_name_split_re, name_text)
  173. if not match:
  174. raise ChangelogError("Unable to parse changelog header: invalid "
  175. "name / revision '%s'" % name_text)
  176. kwargs = match.groupdict()
  177. return _ChangelogSection(self._pkgpolicy, time=time, **kwargs)
  178. def _create_entry(self, author, text):
  179. """Create a new changelog entry"""
  180. return _ChangelogEntry(self._pkgpolicy, author=author, text=text)
  181. def _parse_section_entries(self, text, default_author):
  182. """Parse entries from a string and add them to a section"""
  183. entries = []
  184. entry_text = []
  185. author = default_author
  186. for line in text.splitlines():
  187. match = re.match(self.body_name_re, line)
  188. if match:
  189. if entry_text:
  190. entries.append(self._create_entry(author, entry_text))
  191. author = match.group('name')
  192. else:
  193. if line.startswith("-"):
  194. if entry_text:
  195. entries.append(self._create_entry(author, entry_text))
  196. entry_text = [line]
  197. else:
  198. if not entry_text:
  199. gbp.log.info("First changelog entry (%s) is garbled, "
  200. "entries should start with a dash ('-')" %
  201. line)
  202. entry_text.append(line)
  203. if entry_text:
  204. entries.append(self._create_entry(author, entry_text))
  205. return entries
  206. def parse_section(self, text):
  207. """Parse one section"""
  208. # Check that the first line(s) look like a changelog header
  209. match = re.match(self.section_split_re, text, re.M | re.S)
  210. if not match:
  211. raise ChangelogError("Doesn't look like changelog header: %s..." %
  212. text.splitlines()[0])
  213. # Parse header
  214. section = self._parse_section_header(match.group('ch_header'))
  215. header = section.header
  216. # Parse entries
  217. default_author = header['name'] if 'name' in header else header['email']
  218. for entry in self._parse_section_entries(match.group('ch_body'),
  219. default_author):
  220. section.append_entry(entry)
  221. return section