patch_series.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. # vim: set fileencoding=utf-8 :
  2. #
  3. # (C) 2011,2015 Guido Guenther <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. """Handle Patches and Patch Series"""
  18. import os
  19. import re
  20. import subprocess
  21. import tempfile
  22. from gbp.errors import GbpError
  23. class Patch(object):
  24. """
  25. A patch in a L{PatchSeries}
  26. @ivar path: path to the patch
  27. @type path: string
  28. @ivar topic: the topic of the patch (the directory component)
  29. @type topic: string
  30. @ivar strip: path components to strip (think patch -p<strip>)
  31. @type strip: integer
  32. @ivar info: Information retrieved from a RFC822 style patch header
  33. @type info: C{dict} with C{str} keys and values
  34. @ivar long_desc: the long description of the patch
  35. """
  36. patch_exts = ['diff', 'patch']
  37. def __init__(self, path, topic=None, strip=None):
  38. self.path = path
  39. self.topic = topic
  40. self.strip = strip
  41. self.info = None
  42. self.long_desc = None
  43. def __repr__(self):
  44. repr = "<gbp.patch_series.Patch path='%s' " % self.path
  45. if self.topic:
  46. repr += "topic='%s' " % self.topic
  47. if self.strip is not None:
  48. repr += "strip=%d " % self.strip
  49. repr += ">"
  50. return repr
  51. def _read_info(self):
  52. """
  53. Read patch information into a structured form
  54. using I{git mailinfo}
  55. """
  56. self.info = {}
  57. body = tempfile.NamedTemporaryFile(prefix='gbp_')
  58. pipe = subprocess.Popen("git mailinfo '%s' /dev/null 2>/dev/null < '%s'" %
  59. (body.name, self.path),
  60. shell=True,
  61. stdout=subprocess.PIPE).stdout
  62. for line in pipe:
  63. if ':' in line:
  64. rfc_header, value = line.split(" ", 1)
  65. header = rfc_header[:-1].lower()
  66. self.info[header] = value.strip()
  67. try:
  68. self.long_desc = "".join([line for line in body])
  69. body.close()
  70. except IOError as msg:
  71. raise GbpError("Failed to read patch header of '%s': %s" %
  72. (self.patch, msg))
  73. finally:
  74. if os.path.exists(body.name):
  75. os.unlink(body.name)
  76. def _get_subject_from_filename(self):
  77. """
  78. Determine the patch's subject based on the its filename
  79. >>> p = Patch('debian/patches/foo.patch')
  80. >>> p._get_subject_from_filename()
  81. 'foo'
  82. >>> Patch('foo.patch')._get_subject_from_filename()
  83. 'foo'
  84. >>> Patch('debian/patches/foo.bar')._get_subject_from_filename()
  85. 'foo.bar'
  86. >>> p = Patch('debian/patches/foo')
  87. >>> p._get_subject_from_filename()
  88. 'foo'
  89. >>> Patch('0123-foo.patch')._get_subject_from_filename()
  90. 'foo'
  91. >>> Patch('0123.patch')._get_subject_from_filename()
  92. '0123'
  93. >>> Patch('0123-foo-0123.patch')._get_subject_from_filename()
  94. 'foo-0123'
  95. @return: the patch's subject
  96. @rtype: C{str}
  97. """
  98. subject = os.path.basename(self.path)
  99. # Strip of .diff or .patch from patch name
  100. try:
  101. base, ext = subject.rsplit('.', 1)
  102. if ext in self.patch_exts:
  103. subject = base
  104. except ValueError:
  105. pass # No ext so keep subject as is
  106. return subject.lstrip('0123456789-') or subject
  107. def _get_info_field(self, key, get_val=None):
  108. """
  109. Return the key I{key} from the info C{dict}
  110. or use val if I{key} is not a valid key.
  111. Fill self.info if not already done.
  112. @param key: key to fetch
  113. @type key: C{str}
  114. @param get_val: alternate value if key is not in info dict
  115. @type get_val: C{str}
  116. """
  117. if self.info is None:
  118. self._read_info()
  119. if key in self.info:
  120. return self.info[key]
  121. else:
  122. return get_val() if get_val else None
  123. @property
  124. def subject(self):
  125. """
  126. The patch's subject, either from the patch header or from the filename.
  127. """
  128. return self._get_info_field('subject', self._get_subject_from_filename)
  129. @property
  130. def author(self):
  131. """The patch's author"""
  132. return self._get_info_field('author')
  133. @property
  134. def email(self):
  135. """The patch author's email address"""
  136. return self._get_info_field('email')
  137. @property
  138. def date(self):
  139. """The patch's modification time"""
  140. return self._get_info_field('date')
  141. class PatchSeries(list):
  142. """
  143. A series of L{Patch}es as read from a quilt series file).
  144. """
  145. @classmethod
  146. def read_series_file(klass, seriesfile):
  147. """Read a series file into L{Patch} objects"""
  148. patch_dir = os.path.dirname(seriesfile)
  149. if not os.path.exists(seriesfile):
  150. return []
  151. try:
  152. s = open(seriesfile)
  153. except Exception as err:
  154. raise GbpError("Cannot open series file: %s" % err)
  155. queue = klass._read_series(s, patch_dir)
  156. s.close()
  157. return queue
  158. @classmethod
  159. def _read_series(klass, series, patch_dir):
  160. """
  161. Read patch series
  162. >>> PatchSeries._read_series(['a/b', \
  163. 'a -p1', \
  164. 'a/b -p2'], '.') # doctest:+NORMALIZE_WHITESPACE
  165. [<gbp.patch_series.Patch path='./a/b' topic='a' >,
  166. <gbp.patch_series.Patch path='./a' strip=1 >,
  167. <gbp.patch_series.Patch path='./a/b' topic='a' strip=2 >]
  168. >>> PatchSeries._read_series(['# foo', 'a/b', '', '# bar'], '.')
  169. [<gbp.patch_series.Patch path='./a/b' topic='a' >]
  170. @param series: series of patches in quilt format
  171. @type series: iterable of strings
  172. @param patch_dir: path prefix to prepend to each patch path
  173. @type patch_dir: string
  174. """
  175. queue = PatchSeries()
  176. for line in series:
  177. try:
  178. if line[0] in ['\n', '#']:
  179. continue
  180. except IndexError:
  181. continue # ignore empty lines
  182. queue.append(klass._parse_line(line, patch_dir))
  183. return queue
  184. @staticmethod
  185. def _get_topic(line):
  186. """
  187. Get the topic from the patch's path
  188. >>> PatchSeries._get_topic("a/b c")
  189. 'a'
  190. >>> PatchSeries._get_topic("asdf")
  191. >>> PatchSeries._get_topic("/asdf")
  192. """
  193. topic = os.path.dirname(line)
  194. if topic in ['', '/']:
  195. topic = None
  196. return topic
  197. @staticmethod
  198. def _split_strip(line):
  199. """
  200. Separate the -p<num> option from the patch name
  201. >>> PatchSeries._split_strip("asdf -p1")
  202. ('asdf', 1)
  203. >>> PatchSeries._split_strip("a/nice/patch")
  204. ('a/nice/patch', None)
  205. >>> PatchSeries._split_strip("asdf foo")
  206. ('asdf foo', None)
  207. """
  208. patch = line
  209. strip = None
  210. split = line.rsplit(None, 1)
  211. if len(split) > 1:
  212. m = re.match('-p(?P<level>[0-9]+)', split[1])
  213. if m:
  214. patch = split[0]
  215. strip = int(m.group('level'))
  216. return (patch, strip)
  217. @classmethod
  218. def _parse_line(klass, line, patch_dir):
  219. """
  220. Parse a single line from a series file
  221. >>> PatchSeries._parse_line("a/b -p1", '/tmp/patches')
  222. <gbp.patch_series.Patch path='/tmp/patches/a/b' topic='a' strip=1 >
  223. >>> PatchSeries._parse_line("a/b", '.')
  224. <gbp.patch_series.Patch path='./a/b' topic='a' >
  225. """
  226. line = line.rstrip()
  227. topic = klass._get_topic(line)
  228. (patch, split) = klass._split_strip(line)
  229. return Patch(os.path.join(patch_dir, patch), topic, split)