PageRenderTime 58ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/src/qt/qtwebkit/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py

https://gitlab.com/x33n/phantomjs
Python | 305 lines | 263 code | 11 blank | 31 comment | 6 complexity | a184cc45abc0e50ea0d0b2779aff1e15 MD5 | raw file
  1. # Copyright (c) 2011 Google Inc. All rights reserved.
  2. # Copyright (c) 2011 Code Aurora Forum. All rights reserved.
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions are
  6. # met:
  7. #
  8. # * Redistributions of source code must retain the above copyright
  9. # notice, this list of conditions and the following disclaimer.
  10. # * Redistributions in binary form must reproduce the above
  11. # copyright notice, this list of conditions and the following disclaimer
  12. # in the documentation and/or other materials provided with the
  13. # distribution.
  14. # * Neither the name of Google Inc. nor the names of its
  15. # contributors may be used to endorse or promote products derived from
  16. # this software without specific prior written permission.
  17. #
  18. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  19. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  20. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  21. # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  22. # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  23. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  24. # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  25. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  26. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  28. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. from optparse import make_option
  30. import re
  31. from webkitpy.common.checkout.changelog import ChangeLogEntry
  32. from webkitpy.common.config.committers import CommitterList
  33. from webkitpy.tool.grammar import join_with_separators
  34. from webkitpy.tool.multicommandtool import Command
  35. class CommitLogError(Exception):
  36. def __init__(self):
  37. Exception.__init__(self)
  38. class CommitLogMissingReviewer(CommitLogError):
  39. def __init__(self):
  40. CommitLogError.__init__(self)
  41. class AbstractCommitLogCommand(Command):
  42. _leading_indent_regexp = re.compile(r"^[ ]{4}", re.MULTILINE)
  43. _reviewed_by_regexp = re.compile(ChangeLogEntry.reviewed_by_regexp, re.MULTILINE)
  44. _patch_by_regexp = re.compile(r'^Patch by (?P<name>.+?)\s+<(?P<email>[^<>]+)> on (?P<date>\d{4}-\d{2}-\d{2})$', re.MULTILINE)
  45. _committer_regexp = re.compile(r'^Author: (?P<email>\S+)\s+<[^>]+>$', re.MULTILINE)
  46. _date_regexp = re.compile(r'^Date: (?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) [\+\-]\d{4}$', re.MULTILINE)
  47. _revision_regexp = re.compile(r'^git-svn-id: http://svn.webkit.org/repository/webkit/trunk@(?P<svnid>\d+) (?P<gitid>[0-9a-f\-]{36})$', re.MULTILINE)
  48. def __init__(self, options=None):
  49. options = options or []
  50. options += [
  51. make_option("--max-commit-age", action="store", dest="max_commit_age", type="int", default=9, help="Specify maximum commit age to consider (in months)."),
  52. ]
  53. options = sorted(options, cmp=lambda a, b: cmp(a._long_opts, b._long_opts))
  54. super(AbstractCommitLogCommand, self).__init__(options=options)
  55. # FIXME: This should probably be on the tool somewhere.
  56. self._committer_list = CommitterList()
  57. def _init_options(self, options):
  58. self.verbose = options.verbose
  59. self.max_commit_age = options.max_commit_age
  60. # FIXME: This should move to scm.py
  61. def _recent_commit_messages(self):
  62. git_log = self._tool.executive.run_command(['git', 'log', '--date=iso', '--since="%s months ago"' % self.max_commit_age])
  63. messages = re.compile(r"^commit \w{40}$", re.MULTILINE).split(git_log)[1:] # Ignore the first message which will be empty.
  64. for message in messages:
  65. # Unindent all the lines
  66. (message, _) = self._leading_indent_regexp.subn("", message)
  67. yield message.lstrip() # Remove any leading newlines from the log message.
  68. def _author_name_from_email(self, email):
  69. contributor = self._committer_list.contributor_by_email(email)
  70. return contributor.full_name if contributor else None
  71. def _contributor_from_email(self, email):
  72. contributor = self._committer_list.contributor_by_email(email)
  73. return contributor if contributor else None
  74. def _parse_commit_message(self, commit_message):
  75. committer_match = self._committer_regexp.search(commit_message)
  76. if not committer_match:
  77. raise CommitLogError
  78. committer_email = committer_match.group('email')
  79. if not committer_email:
  80. raise CommitLogError
  81. committer = self._contributor_from_email(committer_email)
  82. if not committer:
  83. raise CommitLogError
  84. commit_date_match = self._date_regexp.search(commit_message)
  85. if not commit_date_match:
  86. raise CommitLogError
  87. commit_date = commit_date_match.group('date')
  88. revision_match = self._revision_regexp.search(commit_message)
  89. if not revision_match:
  90. raise CommitLogError
  91. revision = revision_match.group('svnid')
  92. # Look for "Patch by" line first, which is used for non-committer contributors;
  93. # otherwise, use committer info determined above.
  94. author_match = self._patch_by_regexp.search(commit_message)
  95. if not author_match:
  96. author_match = committer_match
  97. author_email = author_match.group('email')
  98. if not author_email:
  99. author_email = committer_email
  100. author_name = author_match.group('name') if 'name' in author_match.groupdict() else None
  101. if not author_name:
  102. author_name = self._author_name_from_email(author_email)
  103. if not author_name:
  104. raise CommitLogError
  105. contributor = self._contributor_from_email(author_email)
  106. if contributor and author_name != contributor.full_name and contributor.full_name:
  107. author_name = contributor.full_name
  108. reviewer_match = self._reviewed_by_regexp.search(commit_message)
  109. if not reviewer_match:
  110. raise CommitLogMissingReviewer
  111. reviewers = reviewer_match.group('reviewer')
  112. return {
  113. 'committer': committer,
  114. 'commit_date': commit_date,
  115. 'revision': revision,
  116. 'author_email': author_email,
  117. 'author_name': author_name,
  118. 'contributor': contributor,
  119. 'reviewers': reviewers,
  120. }
  121. class SuggestNominations(AbstractCommitLogCommand):
  122. name = "suggest-nominations"
  123. help_text = "Suggest contributors for committer/reviewer nominations"
  124. def __init__(self):
  125. options = [
  126. make_option("--committer-minimum", action="store", dest="committer_minimum", type="int", default=10, help="Specify minimum patch count for Committer nominations."),
  127. make_option("--reviewer-minimum", action="store", dest="reviewer_minimum", type="int", default=80, help="Specify minimum patch count for Reviewer nominations."),
  128. make_option("--show-commits", action="store_true", dest="show_commits", default=False, help="Show commit history with nomination suggestions."),
  129. ]
  130. super(SuggestNominations, self).__init__(options=options)
  131. def _init_options(self, options):
  132. super(SuggestNominations, self)._init_options(options)
  133. self.committer_minimum = options.committer_minimum
  134. self.reviewer_minimum = options.reviewer_minimum
  135. self.show_commits = options.show_commits
  136. def _count_commit(self, commit, analysis):
  137. author_name = commit['author_name']
  138. author_email = commit['author_email']
  139. revision = commit['revision']
  140. commit_date = commit['commit_date']
  141. # See if we already have a contributor with this author_name or email
  142. counter_by_name = analysis['counters_by_name'].get(author_name)
  143. counter_by_email = analysis['counters_by_email'].get(author_email)
  144. if counter_by_name:
  145. if counter_by_email:
  146. if counter_by_name != counter_by_email:
  147. # Merge these two counters This is for the case where we had
  148. # John Smith (jsmith@gmail.com) and Jonathan Smith (jsmith@apple.com)
  149. # and just found a John Smith (jsmith@apple.com). Now we know the
  150. # two names are the same person
  151. counter_by_name['names'] |= counter_by_email['names']
  152. counter_by_name['emails'] |= counter_by_email['emails']
  153. counter_by_name['count'] += counter_by_email.get('count', 0)
  154. analysis['counters_by_email'][author_email] = counter_by_name
  155. else:
  156. # Add email to the existing counter
  157. analysis['counters_by_email'][author_email] = counter_by_name
  158. counter_by_name['emails'] |= set([author_email])
  159. else:
  160. if counter_by_email:
  161. # Add name to the existing counter
  162. analysis['counters_by_name'][author_name] = counter_by_email
  163. counter_by_email['names'] |= set([author_name])
  164. else:
  165. # Create new counter
  166. new_counter = {'names': set([author_name]), 'emails': set([author_email]), 'latest_name': author_name, 'latest_email': author_email, 'commits': ""}
  167. analysis['counters_by_name'][author_name] = new_counter
  168. analysis['counters_by_email'][author_email] = new_counter
  169. assert(analysis['counters_by_name'][author_name] == analysis['counters_by_email'][author_email])
  170. counter = analysis['counters_by_name'][author_name]
  171. counter['count'] = counter.get('count', 0) + 1
  172. if revision.isdigit():
  173. revision = "http://trac.webkit.org/changeset/" + revision
  174. counter['commits'] += " commit: %s on %s by %s (%s)\n" % (revision, commit_date, author_name, author_email)
  175. def _count_recent_patches(self):
  176. analysis = {
  177. 'counters_by_name': {},
  178. 'counters_by_email': {},
  179. }
  180. for commit_message in self._recent_commit_messages():
  181. try:
  182. self._count_commit(self._parse_commit_message(commit_message), analysis)
  183. except CommitLogError, exception:
  184. continue
  185. return analysis['counters_by_email']
  186. def _collect_nominations(self, counters_by_email):
  187. nominations = []
  188. for author_email, counter in counters_by_email.items():
  189. if author_email != counter['latest_email']:
  190. continue
  191. roles = []
  192. contributor = self._committer_list.contributor_by_email(author_email)
  193. author_name = counter['latest_name']
  194. patch_count = counter['count']
  195. if patch_count >= self.committer_minimum and (not contributor or not contributor.can_commit):
  196. roles.append("committer")
  197. if patch_count >= self.reviewer_minimum and contributor and contributor.can_commit and not contributor.can_review:
  198. roles.append("reviewer")
  199. if roles:
  200. nominations.append({
  201. 'roles': roles,
  202. 'author_name': author_name,
  203. 'author_email': author_email,
  204. 'patch_count': patch_count,
  205. })
  206. return nominations
  207. def _print_nominations(self, nominations, counters_by_email):
  208. def nomination_cmp(a_nomination, b_nomination):
  209. roles_result = cmp(a_nomination['roles'], b_nomination['roles'])
  210. if roles_result:
  211. return -roles_result
  212. count_result = cmp(a_nomination['patch_count'], b_nomination['patch_count'])
  213. if count_result:
  214. return -count_result
  215. return cmp(a_nomination['author_name'], b_nomination['author_name'])
  216. for nomination in sorted(nominations, nomination_cmp):
  217. # This is a little bit of a hack, but its convienent to just pass the nomination dictionary to the formating operator.
  218. nomination['roles_string'] = join_with_separators(nomination['roles']).upper()
  219. print "%(roles_string)s: %(author_name)s (%(author_email)s) has %(patch_count)s reviewed patches" % nomination
  220. counter = counters_by_email[nomination['author_email']]
  221. if self.show_commits:
  222. print counter['commits']
  223. def _print_counts(self, counters_by_email):
  224. def counter_cmp(a_tuple, b_tuple):
  225. # split the tuples
  226. # the second element is the "counter" structure
  227. _, a_counter = a_tuple
  228. _, b_counter = b_tuple
  229. count_result = cmp(a_counter['count'], b_counter['count'])
  230. if count_result:
  231. return -count_result
  232. return cmp(a_counter['latest_name'].lower(), b_counter['latest_name'].lower())
  233. for author_email, counter in sorted(counters_by_email.items(), counter_cmp):
  234. if author_email != counter['latest_email']:
  235. continue
  236. contributor = self._committer_list.contributor_by_email(author_email)
  237. author_name = counter['latest_name']
  238. patch_count = counter['count']
  239. counter['names'] = counter['names'] - set([author_name])
  240. counter['emails'] = counter['emails'] - set([author_email])
  241. alias_list = []
  242. for alias in counter['names']:
  243. alias_list.append(alias)
  244. for alias in counter['emails']:
  245. alias_list.append(alias)
  246. if alias_list:
  247. print "CONTRIBUTOR: %s (%s) has %d reviewed patches %s" % (author_name, author_email, patch_count, "(aliases: " + ", ".join(alias_list) + ")")
  248. else:
  249. print "CONTRIBUTOR: %s (%s) has %d reviewed patches" % (author_name, author_email, patch_count)
  250. return
  251. def execute(self, options, args, tool):
  252. self._init_options(options)
  253. patch_counts = self._count_recent_patches()
  254. nominations = self._collect_nominations(patch_counts)
  255. self._print_nominations(nominations, patch_counts)
  256. if self.verbose:
  257. self._print_counts(patch_counts)
  258. if __name__ == "__main__":
  259. SuggestNominations()