series.py 9.2 KB

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