changelog.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. # vim: set fileencoding=utf-8 :
  2. #
  3. # (C) 2011 Guido Günther <agx@sigxcpu.org>
  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. """A Debian Changelog"""
  18. from __future__ import print_function
  19. import email
  20. import os
  21. import subprocess
  22. from gbp.command_wrappers import Command
  23. class NoChangeLogError(Exception):
  24. """No changelog found"""
  25. pass
  26. class ParseChangeLogError(Exception):
  27. """Problem parsing changelog"""
  28. pass
  29. class ChangeLogSection(object):
  30. """A section in the changelog describing one particular version"""
  31. def __init__(self, package, version):
  32. self._package = package
  33. self._version = version
  34. @property
  35. def package(self):
  36. return self._package
  37. @property
  38. def version(self):
  39. return self._version
  40. @classmethod
  41. def parse(klass, section):
  42. """
  43. Parse one changelog section
  44. @param section: a changelog section
  45. @type section: C{str}
  46. @returns: the parse changelog section
  47. @rtype: L{ChangeLogSection}
  48. """
  49. header = section.split('\n')[0]
  50. package = header.split()[0]
  51. version = header.split()[1][1:-1]
  52. return klass(package, version)
  53. class ChangeLog(object):
  54. """A Debian changelog"""
  55. def __init__(self, contents=None, filename=None):
  56. """
  57. @param contents: the contents of the changelog
  58. @type contents: C{str}
  59. @param filename: the filename of the changelog
  60. @param filename: C{str}
  61. """
  62. self._contents = ''
  63. self._cp = None
  64. self._filename = filename
  65. # Check that either contents or filename is passed (but not both)
  66. if (not filename and not contents) or (filename and contents):
  67. raise Exception("Either filename or contents must be passed")
  68. if filename and not os.access(filename, os.F_OK):
  69. raise NoChangeLogError("Changelog %s not found" % (filename, ))
  70. if contents:
  71. self._contents = contents[:]
  72. else:
  73. self._read()
  74. self._parse()
  75. def _parse(self):
  76. """Parse a changelog based on the already read contents."""
  77. cmd = subprocess.Popen(['dpkg-parsechangelog', '-l-'],
  78. stdin=subprocess.PIPE,
  79. stdout=subprocess.PIPE,
  80. stderr=subprocess.PIPE)
  81. (output, errors) = cmd.communicate(self._contents)
  82. if cmd.returncode:
  83. raise ParseChangeLogError("Failed to parse changelog. "
  84. "dpkg-parsechangelog said:\n%s" % (errors, ))
  85. # Parse the result of dpkg-parsechangelog (which looks like
  86. # email headers)
  87. cp = email.message_from_string(output)
  88. try:
  89. if ':' in cp['Version']:
  90. cp['Epoch'], cp['NoEpoch-Version'] = cp['Version'].split(':', 1)
  91. else:
  92. cp['NoEpoch-Version'] = cp['Version']
  93. if '-' in cp['NoEpoch-Version']:
  94. cp['Upstream-Version'], cp['Debian-Version'] = cp['NoEpoch-Version'].rsplit('-', 1)
  95. else:
  96. cp['Debian-Version'] = cp['NoEpoch-Version']
  97. except TypeError:
  98. raise ParseChangeLogError(output.split('\n')[0])
  99. self._cp = cp
  100. def _read(self):
  101. with open(self.filename) as f:
  102. self._contents = f.read()
  103. def __getitem__(self, item):
  104. return self._cp[item]
  105. def __setitem__(self, item, value):
  106. self._cp[item] = value
  107. @property
  108. def filename(self):
  109. """The filename (path) of the changelog"""
  110. return self._filename
  111. @property
  112. def name(self):
  113. """The packages name"""
  114. return self._cp['Source']
  115. @property
  116. def version(self):
  117. """The full version string"""
  118. return self._cp['Version']
  119. @property
  120. def upstream_version(self):
  121. """The upstream version"""
  122. return self._cp['Upstream-Version']
  123. @property
  124. def debian_version(self):
  125. """The Debian part of the version number"""
  126. return self._cp['Debian-Version']
  127. @property
  128. def epoch(self):
  129. """The package's epoch"""
  130. return self._cp['Epoch']
  131. @property
  132. def noepoch(self):
  133. """The version string without the epoch"""
  134. return self._cp['NoEpoch-Version']
  135. def has_epoch(self):
  136. """
  137. Whether the version has an epoch
  138. @return: C{True} if the version has an epoch, C{False} otherwise
  139. @rtype: C{bool}
  140. """
  141. return 'Epoch' in self._cp
  142. @property
  143. def author(self):
  144. """
  145. The author of the last modification
  146. """
  147. return email.utils.parseaddr(self._cp['Maintainer'])[0]
  148. @property
  149. def email(self):
  150. """
  151. The author's email
  152. """
  153. return email.utils.parseaddr(self._cp['Maintainer'])[1]
  154. @property
  155. def date(self):
  156. """
  157. The date of the last modification as rfc822 date
  158. """
  159. return self._cp['Date']
  160. @property
  161. def sections_iter(self):
  162. """
  163. Iterate over sections in the changelog
  164. """
  165. section = ''
  166. for line in self._contents.split('\n'):
  167. if line and line[0] not in [ ' ', '\t' ]:
  168. section += line
  169. else:
  170. if section:
  171. yield ChangeLogSection.parse(section)
  172. section = ''
  173. @property
  174. def sections(self):
  175. """
  176. Get sections in the changelog
  177. """
  178. return list(self.sections_iter)
  179. @staticmethod
  180. def spawn_dch(msg=[], author=None, email=None, newversion=False, version=None,
  181. release=False, distribution=None, dch_options=[]):
  182. """
  183. Spawn dch
  184. @param author: committers name
  185. @type author: C{str}
  186. @param email: committers email
  187. @type email: C{str}
  188. @param newversion: start a new version
  189. @type newversion: C{bool}
  190. @param version: the verion to use
  191. @type version: C{str}
  192. @param release: finalize changelog for releaze
  193. @type release: C{bool}
  194. @param distribution: distribution to use
  195. @type distribution: C{str}
  196. @param dch_options: options passed verbatim to dch
  197. @type dch_options: C{list}
  198. """
  199. env = {}
  200. args = ['--no-auto-nmu']
  201. if newversion:
  202. if version:
  203. try:
  204. args.append(version['increment'])
  205. except KeyError:
  206. args.append('--newversion=%s' % version['version'])
  207. else:
  208. args.append('-i')
  209. elif release:
  210. args.extend(["--release", "--no-force-save-on-release"])
  211. msg = None
  212. if author:
  213. env['DEBFULLNAME'] = author
  214. if email:
  215. env['DEBEMAIL'] = email
  216. if distribution:
  217. args.append("--distribution=%s" % distribution)
  218. args.extend(dch_options)
  219. args.append('--')
  220. if msg:
  221. args.append('[[[insert-git-dch-commit-message-here]]]')
  222. else:
  223. args.append('')
  224. dch = Command('debchange', args, extra_env=env)
  225. dch.call([])
  226. if msg:
  227. old_cl = open("debian/changelog", "r")
  228. new_cl = open("debian/changelog.bak", "w")
  229. for line in old_cl:
  230. if line == " * [[[insert-git-dch-commit-message-here]]]\n":
  231. print(" * " + msg[0], file=new_cl)
  232. for line in msg[1:]:
  233. print(" " + line, file=new_cl)
  234. else:
  235. print(line, end='', file=new_cl)
  236. os.rename("debian/changelog.bak", "debian/changelog")
  237. def add_entry(self, msg, author=None, email=None, dch_options=[]):
  238. """Add a single changelog entry
  239. @param msg: log message to add
  240. @type msg: C{str}
  241. @param author: name of the author of the log message
  242. @type author: C{str}
  243. @param email: email of the author of the log message
  244. @type email: C{str}
  245. @param dch_options: options passed verbatim to dch
  246. @type dch_options: C{list}
  247. """
  248. self.spawn_dch(msg=msg, author=author, email=email, dch_options=dch_options)
  249. def add_section(self, msg, distribution, author=None, email=None,
  250. version={}, dch_options=[]):
  251. """Add a new section to the changelog
  252. @param msg: log message to add
  253. @type msg: C{str}
  254. @param distribution: distribution to set for the new changelog entry
  255. @type distribution: C{str}
  256. @param author: name of the author of the log message
  257. @type author: C{str}
  258. @param email: email of the author of the log message
  259. @type email: C{str}
  260. @param version: version to set for the new changelog entry
  261. @param version: C{dict}
  262. @param dch_options: options passed verbatim to dch
  263. @type dch_options: C{list}
  264. """
  265. self.spawn_dch(msg=msg, newversion=True, version=version, author=author,
  266. email=email, distribution=distribution, dch_options=dch_options)