Tools/ChangeLog

 12011-10-14 Tom Zakrajsek <tomz@codeaurora.org>
 2
 3 Add a suggest-nominations command to webkit-patch for computing potential committer/reviewer nominations
 4 https://bugs.webkit.org/show_bug.cgi?id=62166
 5
 6 Reviewed by NOBODY (OOPS!).
 7
 8 Included options to control committer/reviewer patch count requirements,
 9 an age-limit on patches, and verbose output for more in-depth analysis.
 10
 11 * Scripts/webkitpy/common/checkout/changelog.py:
 12 * Scripts/webkitpy/tool/commands/__init__.py:
 13 * Scripts/webkitpy/tool/commands/suggestnominations.py: Added.
 14 * Scripts/webkitpy/tool/commands/suggestnominations_unittest.py: Added.
 15
1162011-10-14 Raphael Kubo da Costa <kubo@profusion.mobi>
217
318 [EFL] Add DumpRenderTreeSupportEfl

Tools/Scripts/webkitpy/common/checkout/changelog.py

@@class ChangeLogEntry(object):
7777 # e.g. * Source/WebCore/page/EventHandler.cpp: Implement FooBarQuux.
7878 touched_files_regexp = r'\s*\*\s*(?P<file>.+)\:'
7979
 80 # e.g. Reviewed by Darin Adler.
 81 # (Discard everything after the first period to match more invalid lines.)
 82 reviewed_by_regexp = r'Reviewed by (?P<reviewer>.*?)[\.,]?\s*$'
 83
8084 # e.g. == Rolled over to ChangeLog-2011-02-16 ==
8185 rolled_over_regexp = r'^== Rolled over to ChangeLog-\d{4}-\d{2}-\d{2} ==$'
8286
 87 # e.g. git-svn-id: http://svn.webkit.org/repository/webkit/trunk@96161 268f45cc-cd09-0410-ab3c-d52691b4dbfc
 88 svn_id_regexp = r'git-svn-id: http://svn.webkit.org/repository/webkit/trunk@(?P<svnid>\d+) '
 89
8390 def __init__(self, contents, committer_list=CommitterList()):
8491 self._contents = contents
8592 self._committer_list = committer_list

@@class ChangeLogEntry(object):
94101 self._author_name = match.group("name") if match else None
95102 self._author_email = match.group("email") if match else None
96103
97  match = re.search("^\s+Reviewed by (?P<reviewer>.*?)[\.,]?\s*$", self._contents, re.MULTILINE) # Discard everything after the first period
 104 match = re.search(self.reviewed_by_regexp, self._contents, re.MULTILINE)
98105 self._reviewer_text = match.group("reviewer") if match else None
99106
100107 self._reviewer = self._committer_list.committer_by_name(self._reviewer_text)

Tools/Scripts/webkitpy/tool/commands/__init__.py

@@from webkitpy.tool.commands.rebaselineserver import RebaselineServer
1818from webkitpy.tool.commands.roll import *
1919from webkitpy.tool.commands.sheriffbot import *
2020from webkitpy.tool.commands.upload import *
 21from webkitpy.tool.commands.suggestnominations import *

Tools/Scripts/webkitpy/tool/commands/suggestnominations.py

 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
 30from optparse import make_option
 31import re
 32
 33from webkitpy.common.checkout.changelog import ChangeLogEntry
 34from webkitpy.common.config.committers import CommitterList
 35from webkitpy.tool import steps
 36from webkitpy.tool.grammar import join_with_separators
 37from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
 38
 39
 40class SuggestNominations(AbstractDeclarativeCommand):
 41 name = "suggest-nominations"
 42 help_text = "Suggest contributors for committer/reviewer nominations"
 43
 44 def __init__(self):
 45 options = [
 46 make_option("--committer-minimum", action="store", dest="committer_minimum", type="int", default=10, help="Specify minimum patch count for Committer nominations."),
 47 make_option("--reviewer-minimum", action="store", dest="reviewer_minimum", type="int", default=80, help="Specify minimum patch count for Reviewer nominations."),
 48 make_option("--max-commit-age", action="store", dest="max_commit_age", type="int", default=9, help="Specify max commit age to consider for nominations (in months)."),
 49 make_option("--show-commits", action="store_true", dest="show_commits", default=False, help="Show commit history with nomination suggestions."),
 50 ]
 51
 52 AbstractDeclarativeCommand.__init__(self, options=options)
 53 # FIXME: This should probably be on the tool somewhere.
 54 self._committer_list = CommitterList()
 55
 56 _counters_by_name = {}
 57 _counters_by_email = {}
 58
 59 def _init_options(self, options):
 60 self.committer_minimum = options.committer_minimum
 61 self.reviewer_minimum = options.reviewer_minimum
 62 self.max_commit_age = options.max_commit_age
 63 self.show_commits = options.show_commits
 64 self.verbose = options.verbose
 65
 66 # FIXME: This should move to scm.py
 67 def _recent_commit_messages(self):
 68 git_log = self._tool.executive.run_command(['git', 'log', '--since="%s months ago"' % self.max_commit_age])
 69 match_git_svn_id = re.compile(r"\n\n git-svn-id:.*\n", re.MULTILINE)
 70 match_get_log_lines = re.compile(r"^\S.*\n", re.MULTILINE)
 71 match_leading_indent = re.compile(r"^[ ]{4}", re.MULTILINE)
 72
 73 messages = re.split(r"commit \w{40}", git_log)[1:] # Ignore the first message which will be empty.
 74 for message in messages:
 75 # Remove any lines from git and unindent all the lines
 76 (message, _) = match_git_svn_id.subn("", message)
 77 (message, _) = match_get_log_lines.subn("", message)
 78 (message, _) = match_leading_indent.subn("", message)
 79 yield message.lstrip() # Remove any leading newlines from the log message.
 80
 81 # e.g. Patch by Eric Seidel <eric@webkit.org> on 2011-09-15
 82 patch_by_regexp = r'^Patch by (?P<name>.+?)\s+<(?P<email>[^<>]+)> on (?P<date>\d{4}-\d{2}-\d{2})$'
 83
 84 def _count_recent_patches(self):
 85 # This entire block could be written as a map/reduce over the messages.
 86 for message in self._recent_commit_messages():
 87 # FIXME: This should use ChangeLogEntry to do the entire parse instead
 88 # of grabbing at its regexps.
 89 dateline_match = re.match(ChangeLogEntry.date_line_regexp, message, re.MULTILINE)
 90 if not dateline_match:
 91 # Modern commit messages don't just dump the ChangeLog entry, but rather
 92 # have a special Patch by line for non-committers.
 93 dateline_match = re.search(self.patch_by_regexp, message, re.MULTILINE)
 94 if not dateline_match:
 95 continue
 96
 97 author_email = dateline_match.group("email")
 98 if not author_email:
 99 continue
 100
 101 # We only care about reviewed patches, so make sure it has a valid reviewer line.
 102 reviewer_match = re.search(ChangeLogEntry.reviewed_by_regexp, message, re.MULTILINE)
 103 # We might also want to validate the reviewer name against the committer list.
 104 if not reviewer_match or not reviewer_match.group("reviewer"):
 105 continue
 106
 107 author_name = dateline_match.group("name")
 108 if not author_name:
 109 continue
 110
 111 if re.search("([^a-zA-Z]and[^a-zA-Z])|(,)|(@)", author_name):
 112 # This entry seems to have multiple reviewers, or invalid characters, so reject it.
 113 continue
 114
 115 svn_id_match = re.search(ChangeLogEntry.svn_id_regexp, message, re.MULTILINE)
 116 if svn_id_match:
 117 svn_id = svn_id_match.group("svnid")
 118 if not svn_id_match or not svn_id:
 119 svn_id = "unknown"
 120 commit_date = dateline_match.group("date")
 121
 122 # See if we already have a contributor with this name or email
 123 counter_by_name = self._counters_by_name.get(author_name)
 124 counter_by_email = self._counters_by_email.get(author_email)
 125 if counter_by_name:
 126 if counter_by_email:
 127 if counter_by_name != counter_by_email:
 128 # Merge these two counters This is for the case where we had
 129 # John Smith (jsmith@gmail.com) and Jonathan Smith (jsmith@apple.com)
 130 # and just found a John Smith (jsmith@apple.com). Now we know the
 131 # two names are the same person
 132 counter_by_name['names'] |= counter_by_email['names']
 133 counter_by_name['emails'] |= counter_by_email['emails']
 134 counter_by_name['count'] += counter_by_email.get('count', 0)
 135 self._counters_by_email[author_email] = counter_by_name
 136 else:
 137 # Add email to the existing counter
 138 self._counters_by_email[author_email] = counter_by_name
 139 counter_by_name['emails'] |= set([author_email])
 140 else:
 141 if counter_by_email:
 142 # Add name to the existing counter
 143 self._counters_by_name[author_name] = counter_by_email
 144 counter_by_email['names'] |= set([author_name])
 145 else:
 146 # Create new counter
 147 new_counter = {'names': set([author_name]), 'emails': set([author_email]), 'latest_name': author_name, 'latest_email': author_email, 'commits': ""}
 148 self._counters_by_name[author_name] = new_counter
 149 self._counters_by_email[author_email] = new_counter
 150
 151 assert(self._counters_by_name[author_name] == self._counters_by_email[author_email])
 152 counter = self._counters_by_name[author_name]
 153 counter['count'] = counter.get('count', 0) + 1
 154
 155 if svn_id.isdigit():
 156 svn_id = "http://trac.webkit.org/changeset/" + svn_id
 157 counter['commits'] += " commit: %s on %s by %s (%s)\n" % (svn_id, commit_date, author_name, author_email)
 158
 159 return self._counters_by_email
 160
 161 def _collect_nominations(self, counters_by_email):
 162 nominations = []
 163 for author_email, counter in counters_by_email.items():
 164 if author_email != counter['latest_email']:
 165 continue
 166 roles = []
 167
 168 contributor = self._committer_list.contributor_by_email(author_email)
 169
 170 author_name = counter['latest_name']
 171 patch_count = counter['count']
 172
 173 if patch_count >= self.committer_minimum and (not contributor or not contributor.can_commit):
 174 roles.append("committer")
 175 if patch_count >= self.reviewer_minimum and (not contributor or not contributor.can_review):
 176 roles.append("reviewer")
 177 if roles:
 178 nominations.append({
 179 'roles': roles,
 180 'author_name': author_name,
 181 'author_email': author_email,
 182 'patch_count': patch_count,
 183 })
 184 return nominations
 185
 186 def _print_nominations(self, nominations):
 187 def nomination_cmp(a_nomination, b_nomination):
 188 roles_result = cmp(a_nomination['roles'], b_nomination['roles'])
 189 if roles_result:
 190 return -roles_result
 191 count_result = cmp(a_nomination['patch_count'], b_nomination['patch_count'])
 192 if count_result:
 193 return -count_result
 194 return cmp(a_nomination['author_name'], b_nomination['author_name'])
 195
 196 for nomination in sorted(nominations, nomination_cmp):
 197 # This is a little bit of a hack, but its convienent to just pass the nomination dictionary to the formating operator.
 198 nomination['roles_string'] = join_with_separators(nomination['roles']).upper()
 199 print "%(roles_string)s: %(author_name)s (%(author_email)s) has %(patch_count)s reviewed patches" % nomination
 200 counter = self._counters_by_email[nomination['author_email']]
 201
 202 if self.show_commits:
 203 print counter['commits']
 204
 205 def _print_counts(self, counters_by_email):
 206 def counter_cmp(a_tuple, b_tuple):
 207 # split the tuples
 208 # the second element is the "counter" structure
 209 _, a_counter = a_tuple
 210 _, b_counter = b_tuple
 211
 212 count_result = cmp(a_counter['count'], b_counter['count'])
 213 if count_result:
 214 return -count_result
 215 return cmp(a_counter['latest_name'].lower(), b_counter['latest_name'].lower())
 216
 217 for author_email, counter in sorted(counters_by_email.items(), counter_cmp):
 218 if author_email != counter['latest_email']:
 219 continue
 220 contributor = self._committer_list.contributor_by_email(author_email)
 221 author_name = counter['latest_name']
 222 patch_count = counter['count']
 223 counter['names'] = counter['names'] - set([author_name])
 224 counter['emails'] = counter['emails'] - set([author_email])
 225
 226 alias_list = []
 227 for alias in counter['names']:
 228 alias_list.append(alias)
 229 for alias in counter['emails']:
 230 alias_list.append(alias)
 231 if alias_list:
 232 print "CONTRIBUTOR: %s (%s) has %d reviewed patches %s" % (author_name, author_email, patch_count, "(aliases: " + ", ".join(alias_list) + ")")
 233 else:
 234 print "CONTRIBUTOR: %s (%s) has %d reviewed patches" % (author_name, author_email, patch_count)
 235 return
 236
 237 def execute(self, options, args, tool):
 238 self._init_options(options)
 239 patch_counts = self._count_recent_patches()
 240 nominations = self._collect_nominations(patch_counts)
 241 self._print_nominations(nominations)
 242 if self.verbose:
 243 self._print_counts(patch_counts)
 244
 245
 246if __name__ == "__main__":
 247 SuggestNominations()

Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py

 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
 30from webkitpy.tool.commands.commandtest import CommandsTest
 31from webkitpy.tool.commands.suggestnominations import SuggestNominations
 32from webkitpy.tool.mocktool import MockOptions, MockTool
 33
 34
 35class SuggestNominationsTest(CommandsTest):
 36
 37 mock_git_output = """commit 60831dde5beb22f35aef305a87fca7b5f284c698
 38Author: fpizlo@apple.com <fpizlo@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
 39Date: Thu Sep 15 19:56:21 2011 +0000
 40
 41 Value profiles collect no information for global variables
 42 https://bugs.webkit.org/show_bug.cgi?id=68143
 43
 44 Reviewed by Geoffrey Garen.
 45
 46 git-svn-id: http://svn.webkit.org/repository/webkit/trunk@95219 268f45cc-cd09-0410-ab3c-d52691b4dbfc
 47"""
 48 mock_same_author_commit_message = """Value profiles collect no information for global variables
 49https://bugs.webkit.org/show_bug.cgi?id=68143
 50
 51Reviewed by Geoffrey Garen."""
 52
 53 def test_recent_commit_messages(self):
 54 tool = MockTool()
 55 suggest_nominations = SuggestNominations()
 56 suggest_nominations._init_options(options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False))
 57 suggest_nominations.bind_to_tool(tool)
 58
 59 tool.executive.run_command = lambda command: self.mock_git_output
 60 self.assertEqual(list(suggest_nominations._recent_commit_messages()), [self.mock_same_author_commit_message])
 61
 62 mock_non_committer_commit_message = """Let TestWebKitAPI work for chromium
 63https://bugs.webkit.org/show_bug.cgi?id=67756
 64
 65Patch by Xianzhu Wang <wangxianzhu@chromium.org> on 2011-09-15
 66Reviewed by Sam Weinig.
 67
 68Source/WebKit/chromium:
 69
 70* WebKit.gyp:"""
 71
 72 def test_basic(self):
 73 expected_stdout = "COMMITTER AND REVIEWER: Xianzhu Wang (wangxianzhu@chromium.org) has 88 reviewed patches\n"
 74 suggest_nominations = SuggestNominations()
 75 suggest_nominations._init_options(options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False))
 76 suggest_nominations._recent_commit_messages = lambda: [self.mock_non_committer_commit_message for _ in range(88)]
 77 self.assert_execute_outputs(suggest_nominations, [], expected_stdout=expected_stdout, options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False))