series.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. # Copyright (c) 2011 The Chromium OS Authors.
  2. #
  3. # See file CREDITS for list of people who contributed to this
  4. # project.
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License as
  8. # published by the Free Software Foundation; either version 2 of
  9. # the License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software
  18. # Foundation, Inc., 59 Temple Place, Suite 330, Boston,
  19. # MA 02111-1307 USA
  20. #
  21. import itertools
  22. import os
  23. import get_maintainer
  24. import gitutil
  25. import terminal
  26. # Series-xxx tags that we understand
  27. valid_series = ['to', 'cc', 'version', 'changes', 'prefix', 'notes', 'name',
  28. 'cover-cc']
  29. class Series(dict):
  30. """Holds information about a patch series, including all tags.
  31. Vars:
  32. cc: List of aliases/emails to Cc all patches to
  33. commits: List of Commit objects, one for each patch
  34. cover: List of lines in the cover letter
  35. notes: List of lines in the notes
  36. changes: (dict) List of changes for each version, The key is
  37. the integer version number
  38. """
  39. def __init__(self):
  40. self.cc = []
  41. self.to = []
  42. self.cover_cc = []
  43. self.commits = []
  44. self.cover = None
  45. self.notes = []
  46. self.changes = {}
  47. # Written in MakeCcFile()
  48. # key: name of patch file
  49. # value: list of email addresses
  50. self._generated_cc = {}
  51. # These make us more like a dictionary
  52. def __setattr__(self, name, value):
  53. self[name] = value
  54. def __getattr__(self, name):
  55. return self[name]
  56. def AddTag(self, commit, line, name, value):
  57. """Add a new Series-xxx tag along with its value.
  58. Args:
  59. line: Source line containing tag (useful for debug/error messages)
  60. name: Tag name (part after 'Series-')
  61. value: Tag value (part after 'Series-xxx: ')
  62. """
  63. # If we already have it, then add to our list
  64. name = name.replace('-', '_')
  65. if name in self:
  66. values = value.split(',')
  67. values = [str.strip() for str in values]
  68. if type(self[name]) != type([]):
  69. raise ValueError("In %s: line '%s': Cannot add another value "
  70. "'%s' to series '%s'" %
  71. (commit.hash, line, values, self[name]))
  72. self[name] += values
  73. # Otherwise just set the value
  74. elif name in valid_series:
  75. self[name] = value
  76. else:
  77. raise ValueError("In %s: line '%s': Unknown 'Series-%s': valid "
  78. "options are %s" % (commit.hash, line, name,
  79. ', '.join(valid_series)))
  80. def AddCommit(self, commit):
  81. """Add a commit into our list of commits
  82. We create a list of tags in the commit subject also.
  83. Args:
  84. commit: Commit object to add
  85. """
  86. commit.CheckTags()
  87. self.commits.append(commit)
  88. def ShowActions(self, args, cmd, process_tags):
  89. """Show what actions we will/would perform
  90. Args:
  91. args: List of patch files we created
  92. cmd: The git command we would have run
  93. process_tags: Process tags as if they were aliases
  94. """
  95. col = terminal.Color()
  96. print 'Dry run, so not doing much. But I would do this:'
  97. print
  98. print 'Send a total of %d patch%s with %scover letter.' % (
  99. len(args), '' if len(args) == 1 else 'es',
  100. self.get('cover') and 'a ' or 'no ')
  101. # TODO: Colour the patches according to whether they passed checks
  102. for upto in range(len(args)):
  103. commit = self.commits[upto]
  104. print col.Color(col.GREEN, ' %s' % args[upto])
  105. cc_list = list(self._generated_cc[commit.patch])
  106. # Skip items in To list
  107. if 'to' in self:
  108. try:
  109. map(cc_list.remove, gitutil.BuildEmailList(self.to))
  110. except ValueError:
  111. pass
  112. for email in cc_list:
  113. if email == None:
  114. email = col.Color(col.YELLOW, "<alias '%s' not found>"
  115. % tag)
  116. if email:
  117. print ' Cc: ',email
  118. print
  119. for item in gitutil.BuildEmailList(self.get('to', '<none>')):
  120. print 'To:\t ', item
  121. for item in gitutil.BuildEmailList(self.cc):
  122. print 'Cc:\t ', item
  123. print 'Version: ', self.get('version')
  124. print 'Prefix:\t ', self.get('prefix')
  125. if self.cover:
  126. print 'Cover: %d lines' % len(self.cover)
  127. cover_cc = gitutil.BuildEmailList(self.get('cover_cc', ''))
  128. all_ccs = itertools.chain(cover_cc, *self._generated_cc.values())
  129. for email in set(all_ccs):
  130. print ' Cc: ',email
  131. if cmd:
  132. print 'Git command: %s' % cmd
  133. def MakeChangeLog(self, commit):
  134. """Create a list of changes for each version.
  135. Return:
  136. The change log as a list of strings, one per line
  137. Changes in v4:
  138. - Jog the dial back closer to the widget
  139. Changes in v3: None
  140. Changes in v2:
  141. - Fix the widget
  142. - Jog the dial
  143. etc.
  144. """
  145. final = []
  146. need_blank = False
  147. for change in sorted(self.changes, reverse=True):
  148. out = []
  149. for this_commit, text in self.changes[change]:
  150. if commit and this_commit != commit:
  151. continue
  152. out.append(text)
  153. line = 'Changes in v%d:' % change
  154. have_changes = len(out) > 0
  155. if have_changes:
  156. out.insert(0, line)
  157. else:
  158. out = [line + ' None']
  159. if need_blank:
  160. out.insert(0, '')
  161. final += out
  162. need_blank = have_changes
  163. if self.changes:
  164. final.append('')
  165. return final
  166. def DoChecks(self):
  167. """Check that each version has a change log
  168. Print an error if something is wrong.
  169. """
  170. col = terminal.Color()
  171. if self.get('version'):
  172. changes_copy = dict(self.changes)
  173. for version in range(1, int(self.version) + 1):
  174. if self.changes.get(version):
  175. del changes_copy[version]
  176. else:
  177. if version > 1:
  178. str = 'Change log missing for v%d' % version
  179. print col.Color(col.RED, str)
  180. for version in changes_copy:
  181. str = 'Change log for unknown version v%d' % version
  182. print col.Color(col.RED, str)
  183. elif self.changes:
  184. str = 'Change log exists, but no version is set'
  185. print col.Color(col.RED, str)
  186. def MakeCcFile(self, process_tags, cover_fname, raise_on_error):
  187. """Make a cc file for us to use for per-commit Cc automation
  188. Also stores in self._generated_cc to make ShowActions() faster.
  189. Args:
  190. process_tags: Process tags as if they were aliases
  191. cover_fname: If non-None the name of the cover letter.
  192. raise_on_error: True to raise an error when an alias fails to match,
  193. False to just print a message.
  194. Return:
  195. Filename of temp file created
  196. """
  197. # Look for commit tags (of the form 'xxx:' at the start of the subject)
  198. fname = '/tmp/patman.%d' % os.getpid()
  199. fd = open(fname, 'w')
  200. all_ccs = []
  201. for commit in self.commits:
  202. list = []
  203. if process_tags:
  204. list += gitutil.BuildEmailList(commit.tags,
  205. raise_on_error=raise_on_error)
  206. list += gitutil.BuildEmailList(commit.cc_list,
  207. raise_on_error=raise_on_error)
  208. list += get_maintainer.GetMaintainer(commit.patch)
  209. all_ccs += list
  210. print >>fd, commit.patch, ', '.join(list)
  211. self._generated_cc[commit.patch] = list
  212. if cover_fname:
  213. cover_cc = gitutil.BuildEmailList(self.get('cover_cc', ''))
  214. print >>fd, cover_fname, ', '.join(set(cover_cc + all_ccs))
  215. fd.close()
  216. return fname
  217. def AddChange(self, version, commit, info):
  218. """Add a new change line to a version.
  219. This will later appear in the change log.
  220. Args:
  221. version: version number to add change list to
  222. info: change line for this version
  223. """
  224. if not self.changes.get(version):
  225. self.changes[version] = []
  226. self.changes[version].append([commit, info])
  227. def GetPatchPrefix(self):
  228. """Get the patch version string
  229. Return:
  230. Patch string, like 'RFC PATCH v5' or just 'PATCH'
  231. """
  232. version = ''
  233. if self.get('version'):
  234. version = ' v%s' % self['version']
  235. # Get patch name prefix
  236. prefix = ''
  237. if self.get('prefix'):
  238. prefix = '%s ' % self['prefix']
  239. return '%sPATCH%s' % (prefix, version)