Title: [97891] trunk/Tools
Revision
97891
Author
[email protected]
Date
2011-10-19 16:25:12 -0700 (Wed, 19 Oct 2011)

Log Message

Add a suggest-nominations command to webkit-patch for computing potential committer/reviewer nominations
https://bugs.webkit.org/show_bug.cgi?id=62166

Patch by Tom Zakrajsek <[email protected]> on 2011-10-19
Reviewed by Eric Seidel.

Included options to control committer/reviewer patch count requirements,
an age-limit on patches, and verbose output for more in-depth analysis.

* Scripts/webkitpy/common/checkout/changelog.py:
* Scripts/webkitpy/tool/commands/__init__.py:
* Scripts/webkitpy/tool/commands/suggestnominations.py: Added.
* Scripts/webkitpy/tool/commands/suggestnominations_unittest.py: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/Tools/ChangeLog (97890 => 97891)


--- trunk/Tools/ChangeLog	2011-10-19 23:02:57 UTC (rev 97890)
+++ trunk/Tools/ChangeLog	2011-10-19 23:25:12 UTC (rev 97891)
@@ -1,3 +1,18 @@
+2011-10-19  Tom Zakrajsek  <[email protected]>
+
+        Add a suggest-nominations command to webkit-patch for computing potential committer/reviewer nominations
+        https://bugs.webkit.org/show_bug.cgi?id=62166
+
+        Reviewed by Eric Seidel.
+
+        Included options to control committer/reviewer patch count requirements,
+        an age-limit on patches, and verbose output for more in-depth analysis.
+
+        * Scripts/webkitpy/common/checkout/changelog.py:
+        * Scripts/webkitpy/tool/commands/__init__.py:
+        * Scripts/webkitpy/tool/commands/suggestnominations.py: Added.
+        * Scripts/webkitpy/tool/commands/suggestnominations_unittest.py: Added.
+
 2011-10-19  Eric Seidel  <[email protected]>
 
         new-run-webkit-tests -2 was crashing due to both "java" and "java/" being listed in Skipped files.

Modified: trunk/Tools/Scripts/webkitpy/common/checkout/changelog.py (97890 => 97891)


--- trunk/Tools/Scripts/webkitpy/common/checkout/changelog.py	2011-10-19 23:02:57 UTC (rev 97890)
+++ trunk/Tools/Scripts/webkitpy/common/checkout/changelog.py	2011-10-19 23:25:12 UTC (rev 97891)
@@ -77,9 +77,16 @@
     # e.g. * Source/WebCore/page/EventHandler.cpp: Implement FooBarQuux.
     touched_files_regexp = r'\s*\*\s*(?P<file>.+)\:'
 
+    # e.g. Reviewed by Darin Adler.
+    # (Discard everything after the first period to match more invalid lines.)
+    reviewed_by_regexp = r'Reviewed by (?P<reviewer>.*?)[\.,]?\s*$'
+
     # e.g. == Rolled over to ChangeLog-2011-02-16 ==
     rolled_over_regexp = r'^== Rolled over to ChangeLog-\d{4}-\d{2}-\d{2} ==$'
 
+    # e.g. git-svn-id: http://svn.webkit.org/repository/webkit/trunk@96161 268f45cc-cd09-0410-ab3c-d52691b4dbfc
+    svn_id_regexp = r'git-svn-id: http://svn.webkit.org/repository/webkit/trunk@(?P<svnid>\d+) '
+
     def __init__(self, contents, committer_list=CommitterList()):
         self._contents = contents
         self._committer_list = committer_list
@@ -94,7 +101,7 @@
         self._author_name = match.group("name") if match else None
         self._author_email = match.group("email") if match else None
 
-        match = re.search("^\s+Reviewed by (?P<reviewer>.*?)[\.,]?\s*$", self._contents, re.MULTILINE) # Discard everything after the first period
+        match = re.search(self.reviewed_by_regexp, self._contents, re.MULTILINE)
         self._reviewer_text = match.group("reviewer") if match else None
 
         self._reviewer = self._committer_list.committer_by_name(self._reviewer_text)

Modified: trunk/Tools/Scripts/webkitpy/tool/commands/__init__.py (97890 => 97891)


--- trunk/Tools/Scripts/webkitpy/tool/commands/__init__.py	2011-10-19 23:02:57 UTC (rev 97890)
+++ trunk/Tools/Scripts/webkitpy/tool/commands/__init__.py	2011-10-19 23:25:12 UTC (rev 97891)
@@ -18,3 +18,4 @@
 from webkitpy.tool.commands.roll import *
 from webkitpy.tool.commands.sheriffbot import *
 from webkitpy.tool.commands.upload import *
+from webkitpy.tool.commands.suggestnominations import *

Added: trunk/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py (0 => 97891)


--- trunk/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py	                        (rev 0)
+++ trunk/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py	2011-10-19 23:25:12 UTC (rev 97891)
@@ -0,0 +1,247 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+# Copyright (c) 2011 Code Aurora Forum. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from optparse import make_option
+import re
+
+from webkitpy.common.checkout.changelog import ChangeLogEntry
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.tool import steps
+from webkitpy.tool.grammar import join_with_separators
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class SuggestNominations(AbstractDeclarativeCommand):
+    name = "suggest-nominations"
+    help_text = "Suggest contributors for committer/reviewer nominations"
+
+    def __init__(self):
+        options = [
+            make_option("--committer-minimum", action="" dest="committer_minimum", type="int", default=10, help="Specify minimum patch count for Committer nominations."),
+            make_option("--reviewer-minimum", action="" dest="reviewer_minimum", type="int", default=80, help="Specify minimum patch count for Reviewer nominations."),
+            make_option("--max-commit-age", action="" dest="max_commit_age", type="int", default=9, help="Specify max commit age to consider for nominations (in months)."),
+            make_option("--show-commits", action="" dest="show_commits", default=False, help="Show commit history with nomination suggestions."),
+        ]
+
+        AbstractDeclarativeCommand.__init__(self, options=options)
+        # FIXME: This should probably be on the tool somewhere.
+        self._committer_list = CommitterList()
+
+    _counters_by_name = {}
+    _counters_by_email = {}
+
+    def _init_options(self, options):
+        self.committer_minimum = options.committer_minimum
+        self.reviewer_minimum = options.reviewer_minimum
+        self.max_commit_age = options.max_commit_age
+        self.show_commits = options.show_commits
+        self.verbose = options.verbose
+
+    # FIXME: This should move to scm.py
+    def _recent_commit_messages(self):
+        git_log = self._tool.executive.run_command(['git', 'log', '--since="%s months ago"' % self.max_commit_age])
+        match_git_svn_id = re.compile(r"\n\n    git-svn-id:.*\n", re.MULTILINE)
+        match_get_log_lines = re.compile(r"^\S.*\n", re.MULTILINE)
+        match_leading_indent = re.compile(r"^[ ]{4}", re.MULTILINE)
+
+        messages = re.split(r"commit \w{40}", git_log)[1:]  # Ignore the first message which will be empty.
+        for message in messages:
+            # Remove any lines from git and unindent all the lines
+            (message, _) = match_git_svn_id.subn("", message)
+            (message, _) = match_get_log_lines.subn("", message)
+            (message, _) = match_leading_indent.subn("", message)
+            yield message.lstrip()  # Remove any leading newlines from the log message.
+
+    # e.g. Patch by Eric Seidel <[email protected]> on 2011-09-15
+    patch_by_regexp = r'^Patch by (?P<name>.+?)\s+<(?P<email>[^<>]+)> on (?P<date>\d{4}-\d{2}-\d{2})$'
+
+    def _count_recent_patches(self):
+        # This entire block could be written as a map/reduce over the messages.
+        for message in self._recent_commit_messages():
+            # FIXME: This should use ChangeLogEntry to do the entire parse instead
+            # of grabbing at its regexps.
+            dateline_match = re.match(ChangeLogEntry.date_line_regexp, message, re.MULTILINE)
+            if not dateline_match:
+                # Modern commit messages don't just dump the ChangeLog entry, but rather
+                # have a special Patch by line for non-committers.
+                dateline_match = re.search(self.patch_by_regexp, message, re.MULTILINE)
+                if not dateline_match:
+                    continue
+
+            author_email = dateline_match.group("email")
+            if not author_email:
+                continue
+
+            # We only care about reviewed patches, so make sure it has a valid reviewer line.
+            reviewer_match = re.search(ChangeLogEntry.reviewed_by_regexp, message, re.MULTILINE)
+            # We might also want to validate the reviewer name against the committer list.
+            if not reviewer_match or not reviewer_match.group("reviewer"):
+                continue
+
+            author_name = dateline_match.group("name")
+            if not author_name:
+                continue
+
+            if re.search("([^a-zA-Z]and[^a-zA-Z])|(,)|(@)", author_name):
+                # This entry seems to have multiple reviewers, or invalid characters, so reject it.
+                continue
+
+            svn_id_match = re.search(ChangeLogEntry.svn_id_regexp, message, re.MULTILINE)
+            if svn_id_match:
+                svn_id = svn_id_match.group("svnid")
+            if not svn_id_match or not svn_id:
+                svn_id = "unknown"
+            commit_date = dateline_match.group("date")
+
+            # See if we already have a contributor with this name or email
+            counter_by_name = self._counters_by_name.get(author_name)
+            counter_by_email = self._counters_by_email.get(author_email)
+            if counter_by_name:
+                if counter_by_email:
+                    if counter_by_name != counter_by_email:
+                        # Merge these two counters  This is for the case where we had
+                        # John Smith ([email protected]) and Jonathan Smith ([email protected])
+                        # and just found a John Smith ([email protected]).  Now we know the
+                        # two names are the same person
+                        counter_by_name['names'] |= counter_by_email['names']
+                        counter_by_name['emails'] |= counter_by_email['emails']
+                        counter_by_name['count'] += counter_by_email.get('count', 0)
+                        self._counters_by_email[author_email] = counter_by_name
+                else:
+                    # Add email to the existing counter
+                    self._counters_by_email[author_email] = counter_by_name
+                    counter_by_name['emails'] |= set([author_email])
+            else:
+                if counter_by_email:
+                    # Add name to the existing counter
+                    self._counters_by_name[author_name] = counter_by_email
+                    counter_by_email['names'] |= set([author_name])
+                else:
+                    # Create new counter
+                    new_counter = {'names': set([author_name]), 'emails': set([author_email]), 'latest_name': author_name, 'latest_email': author_email, 'commits': ""}
+                    self._counters_by_name[author_name] = new_counter
+                    self._counters_by_email[author_email] = new_counter
+
+            assert(self._counters_by_name[author_name] == self._counters_by_email[author_email])
+            counter = self._counters_by_name[author_name]
+            counter['count'] = counter.get('count', 0) + 1
+
+            if svn_id.isdigit():
+                svn_id = "http://trac.webkit.org/changeset/" + svn_id
+            counter['commits'] += "  commit: %s on %s by %s (%s)\n" % (svn_id, commit_date, author_name, author_email)
+
+        return self._counters_by_email
+
+    def _collect_nominations(self, counters_by_email):
+        nominations = []
+        for author_email, counter in counters_by_email.items():
+            if author_email != counter['latest_email']:
+                continue
+            roles = []
+
+            contributor = self._committer_list.contributor_by_email(author_email)
+
+            author_name = counter['latest_name']
+            patch_count = counter['count']
+
+            if patch_count >= self.committer_minimum and (not contributor or not contributor.can_commit):
+                roles.append("committer")
+            if patch_count >= self.reviewer_minimum  and (not contributor or not contributor.can_review):
+                roles.append("reviewer")
+            if roles:
+                nominations.append({
+                    'roles': roles,
+                    'author_name': author_name,
+                    'author_email': author_email,
+                    'patch_count': patch_count,
+                })
+        return nominations
+
+    def _print_nominations(self, nominations):
+        def nomination_cmp(a_nomination, b_nomination):
+            roles_result = cmp(a_nomination['roles'], b_nomination['roles'])
+            if roles_result:
+                return -roles_result
+            count_result = cmp(a_nomination['patch_count'], b_nomination['patch_count'])
+            if count_result:
+                return -count_result
+            return cmp(a_nomination['author_name'], b_nomination['author_name'])
+
+        for nomination in sorted(nominations, nomination_cmp):
+            # This is a little bit of a hack, but its convienent to just pass the nomination dictionary to the formating operator.
+            nomination['roles_string'] = join_with_separators(nomination['roles']).upper()
+            print "%(roles_string)s: %(author_name)s (%(author_email)s) has %(patch_count)s reviewed patches" % nomination
+            counter = self._counters_by_email[nomination['author_email']]
+
+            if self.show_commits:
+                print counter['commits']
+
+    def _print_counts(self, counters_by_email):
+        def counter_cmp(a_tuple, b_tuple):
+            # split the tuples
+            # the second element is the "counter" structure
+            _, a_counter = a_tuple
+            _, b_counter = b_tuple
+
+            count_result = cmp(a_counter['count'], b_counter['count'])
+            if count_result:
+                return -count_result
+            return cmp(a_counter['latest_name'].lower(), b_counter['latest_name'].lower())
+
+        for author_email, counter in sorted(counters_by_email.items(), counter_cmp):
+            if author_email != counter['latest_email']:
+                continue
+            contributor = self._committer_list.contributor_by_email(author_email)
+            author_name = counter['latest_name']
+            patch_count = counter['count']
+            counter['names'] = counter['names'] - set([author_name])
+            counter['emails'] = counter['emails'] - set([author_email])
+
+            alias_list = []
+            for alias in counter['names']:
+                alias_list.append(alias)
+            for alias in counter['emails']:
+                alias_list.append(alias)
+            if alias_list:
+                print "CONTRIBUTOR: %s (%s) has %d reviewed patches %s" % (author_name, author_email, patch_count, "(aliases: " + ", ".join(alias_list) + ")")
+            else:
+                print "CONTRIBUTOR: %s (%s) has %d reviewed patches" % (author_name, author_email, patch_count)
+        return
+
+    def execute(self, options, args, tool):
+        self._init_options(options)
+        patch_counts = self._count_recent_patches()
+        nominations = self._collect_nominations(patch_counts)
+        self._print_nominations(nominations)
+        if self.verbose:
+            self._print_counts(patch_counts)
+
+
+if __name__ == "__main__":
+    SuggestNominations()

Added: trunk/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py (0 => 97891)


--- trunk/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py	                        (rev 0)
+++ trunk/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py	2011-10-19 23:25:12 UTC (rev 97891)
@@ -0,0 +1,77 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+# Copyright (C) 2011 Code Aurora Forum. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.suggestnominations import SuggestNominations
+from webkitpy.tool.mocktool import MockOptions, MockTool
+
+
+class SuggestNominationsTest(CommandsTest):
+
+    mock_git_output = """commit 60831dde5beb22f35aef305a87fca7b5f284c698
+Author: [email protected] <[email protected]@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
+Date:   Thu Sep 15 19:56:21 2011 +0000
+
+    Value profiles collect no information for global variables
+    https://bugs.webkit.org/show_bug.cgi?id=68143
+
+    Reviewed by Geoffrey Garen.
+
+    git-svn-id: http://svn.webkit.org/repository/webkit/trunk@95219 268f45cc-cd09-0410-ab3c-d52691b4dbfc
+"""
+    mock_same_author_commit_message = """Value profiles collect no information for global variables
+https://bugs.webkit.org/show_bug.cgi?id=68143
+
+Reviewed by Geoffrey Garen."""
+
+    def test_recent_commit_messages(self):
+        tool = MockTool()
+        suggest_nominations = SuggestNominations()
+        suggest_nominations._init_options(options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False))
+        suggest_nominations.bind_to_tool(tool)
+
+        tool.executive.run_command = lambda command: self.mock_git_output
+        self.assertEqual(list(suggest_nominations._recent_commit_messages()), [self.mock_same_author_commit_message])
+
+    mock_non_committer_commit_message = """Let TestWebKitAPI work for chromium
+https://bugs.webkit.org/show_bug.cgi?id=67756
+
+Patch by Xianzhu Wang <[email protected]> on 2011-09-15
+Reviewed by Sam Weinig.
+
+Source/WebKit/chromium:
+
+* WebKit.gyp:"""
+
+    def test_basic(self):
+        expected_stdout = "COMMITTER AND REVIEWER: Xianzhu Wang ([email protected]) has 88 reviewed patches\n"
+        suggest_nominations = SuggestNominations()
+        suggest_nominations._init_options(options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False))
+        suggest_nominations._recent_commit_messages = lambda: [self.mock_non_committer_commit_message for _ in range(88)]
+        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))
_______________________________________________
webkit-changes mailing list
[email protected]
http://lists.webkit.org/mailman/listinfo.cgi/webkit-changes

Reply via email to