series.py 10 KB

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