Diff
Modified: trunk/Tools/ChangeLog (95237 => 95238)
--- trunk/Tools/ChangeLog 2011-09-15 22:06:31 UTC (rev 95237)
+++ trunk/Tools/ChangeLog 2011-09-15 22:10:50 UTC (rev 95238)
@@ -1,5 +1,28 @@
2011-09-15 Eric Seidel <e...@webkit.org>
+ Reviewed by Adam Barth.
+
+ webkit-patch should be able to find users and add them to bugzilla groups
+ https://bugs.webkit.org/show_bug.cgi?id=63351
+
+ These are both very basic commands. But it's now possible to find
+ all users matching a regexp, as well as add all users matching a regexp
+ to a set of groups.
+
+ bugzilla.py already knew how to find users (for validate-committer-lists)
+ but now it has the ability to modify the user records.
+
+ I split some of the logic out into a new EditUsersParser class
+ to try and reduce the amount of code in Bugzilla/BugzillaQueries.
+
+ * Scripts/webkitpy/common/net/bugzilla/bugzilla.py:
+ * Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py:
+ * Scripts/webkitpy/tool/commands/__init__.py:
+ * Scripts/webkitpy/tool/commands/adduserstogroups.py: Added.
+ * Scripts/webkitpy/tool/commands/findusers.py: Added.
+
+2011-09-15 Eric Seidel <e...@webkit.org>
+
Remove ENABLE(SVG_AS_IMAGE) since all major ports have it on by default
https://bugs.webkit.org/show_bug.cgi?id=68182
Modified: trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py (95237 => 95238)
--- trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py 2011-09-15 22:06:31 UTC (rev 95237)
+++ trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py 2011-09-15 22:10:50 UTC (rev 95238)
@@ -1,4 +1,4 @@
-# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2011 Google Inc. All rights reserved.
# Copyright (c) 2009 Apple Inc. All rights reserved.
# Copyright (c) 2010 Research In Motion Limited. All rights reserved.
#
@@ -49,6 +49,75 @@
from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, SoupStrainer
+class EditUsersParser(object):
+ def __init__(self):
+ self._group_name_to_group_string_cache = {}
+
+ def _login_and_uid_from_row(self, row):
+ first_cell = row.find("td")
+ # The first row is just headers, we skip it.
+ if not first_cell:
+ return None
+ # When there were no results, we have a fake "<none>" entry in the table.
+ if first_cell.find(text="<none>"):
+ return None
+ # Otherwise the <td> contains a single <a> which contains the login name or a single <i> with the string "<none>".
+ anchor_tag = first_cell.find("a")
+ login = unicode(anchor_tag.string).strip()
+ user_id = int(re.search(r"userid=(\d+)", str(anchor_tag['href'])).group(1))
+ return (login, user_id)
+
+ def login_userid_pairs_from_edit_user_results(self, results_page):
+ soup = BeautifulSoup(results_page, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
+ results_table = soup.find(id="admin_table")
+ login_userid_pairs = [self._login_and_uid_from_row(row) for row in results_table('tr')]
+ # Filter out None from the logins.
+ return filter(lambda pair: bool(pair), login_userid_pairs)
+
+ def _group_name_and_string_from_row(self, row):
+ label_element = row.find('label')
+ group_string = unicode(label_element['for'])
+ group_name = unicode(label_element.find('strong').string).rstrip(':')
+ return (group_name, group_string)
+
+ def user_dict_from_edit_user_page(self, page):
+ soup = BeautifulSoup(page, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
+ user_table = soup.find("table", {'class': 'main'})
+ user_dict = {}
+ for row in user_table('tr'):
+ label_element = row.find('label')
+ if not label_element:
+ continue # This must not be a row we know how to parse.
+ if row.find('table'):
+ continue # Skip the <tr> holding the groups table.
+
+ key = label_element['for']
+ if "group" in key:
+ key = "groups"
+ value = user_dict.get('groups', set())
+ # We must be parsing a "tr" inside the inner group table.
+ (group_name, _) = self._group_name_and_string_from_row(row)
+ if row.find('input', {'type': 'checkbox', 'checked': 'checked'}):
+ value.add(group_name)
+ else:
+ value = unicode(row.find('td').string).strip()
+ user_dict[key] = value
+ return user_dict
+
+ def _group_rows_from_edit_user_page(self, edit_user_page):
+ soup = BeautifulSoup(edit_user_page, convertEntities=BeautifulSoup.HTML_ENTITIES)
+ return soup('td', {'class': 'groupname'})
+
+ def group_string_from_name(self, edit_user_page, group_name):
+ # Bugzilla uses "group_NUMBER" strings, which may be different per install
+ # so we just look them up once and cache them.
+ if not self._group_name_to_group_string_cache:
+ rows = self._group_rows_from_edit_user_page(edit_user_page)
+ name_string_pairs = map(self._group_name_and_string_from_row, rows)
+ self._group_name_to_group_string_cache = dict(name_string_pairs)
+ return self._group_name_to_group_string_cache[group_name]
+
+
def timestamp():
return datetime.now().strftime("%Y%m%d%H%M%S")
@@ -174,50 +243,51 @@
review_queue_url = "request.cgi?action=""
return self._fetch_attachment_ids_request_query(review_queue_url)
- def _login_from_row(self, row):
- first_cell = row.find("td")
- # The first row is just headers, we skip it.
- if not first_cell:
- return None
- # When there were no results, we have a fake "<none>" entry in the table.
- if first_cell.find(text="<none>"):
- return None
- # Otherwise the <td> contains a single <a> which contains the login name or a single <i> with the string "<none>".
- return str(first_cell.find("a").string).strip()
-
- def _parse_logins_from_editusers_results(self, results_page):
- soup = BeautifulSoup(results_page, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
- results_table = soup.find(id="admin_table")
- logins = [self._login_from_row(row) for row in results_table('tr')]
- # Filter out None from the logins.
- return filter(lambda login: bool(login), logins)
-
# This only works if your account has edituser privileges.
# We could easily parse https://bugs.webkit.org/userprefs.cgi?tab=permissions to
# check permissions, but bugzilla will just return an error if we don't have them.
- def fetch_logins_matching_substring(self, search_string):
+ def fetch_login_userid_pairs_matching_substring(self, search_string):
review_queue_url = "editusers.cgi?action="" % urllib.quote(search_string)
results_page = self._load_query(review_queue_url)
- return self._parse_logins_from_editusers_results(results_page)
+ # We could pull the EditUsersParser off Bugzilla if needed.
+ return EditUsersParser().login_userid_pairs_from_edit_user_results(results_page)
+ # FIXME: We should consider adding a BugzillaUser class.
+ def fetch_logins_matching_substring(self, search_string):
+ pairs = self.fetch_login_userid_pairs_matching_substring(search_string)
+ return map(lambda pair: pair[0], pairs)
+
class Bugzilla(object):
-
def __init__(self, dryrun=False, committers=committers.CommitterList()):
self.dryrun = dryrun
self.authenticated = False
self.queries = BugzillaQueries(self)
self.committers = committers
self.cached_quips = []
+ self.edit_user_parser = EditUsersParser()
# FIXME: We should use some sort of Browser mock object when in dryrun
# mode (to prevent any mistakes).
from webkitpy.thirdparty.autoinstalled.mechanize import Browser
self.browser = Browser()
- # Ignore bugs.webkit.org/robots.txt until we fix it to allow this
- # script.
+ # Ignore bugs.webkit.org/robots.txt until we fix it to allow this script.
self.browser.set_handle_robots(False)
+ def fetch_user(self, user_id):
+ self.authenticate()
+ edit_user_page = self.browser.open(self.edit_user_url_for_id(user_id))
+ return self.edit_user_parser.user_dict_from_edit_user_page(edit_user_page)
+
+ def add_user_to_groups(self, user_id, group_names):
+ self.authenticate()
+ user_edit_page = self.browser.open(self.edit_user_url_for_id(user_id))
+ self.browser.select_form(nr=1)
+ for group_name in group_names:
+ group_string = self.edit_user_parser.group_string_from_name(user_edit_page, group_name)
+ self.browser.find_control(group_string).items[0].selected = True
+ self.browser.submit()
+
def quips(self):
# We only fetch and parse the list of quips once per instantiation
# so that we do not burden bugs.webkit.org.
@@ -249,6 +319,9 @@
attachment_id,
action_param)
+ def edit_user_url_for_id(self, user_id):
+ return "%seditusers.cgi?action="" % (config_urls.bug_server_url, user_id)
+
def _parse_attachment_flag(self,
element,
flag_name,
Modified: trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py (95237 => 95238)
--- trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py 2011-09-15 22:06:31 UTC (rev 95237)
+++ trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py 2011-09-15 22:10:50 UTC (rev 95238)
@@ -1,4 +1,4 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2011 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
@@ -30,7 +30,7 @@
import datetime
import StringIO
-from .bugzilla import Bugzilla, BugzillaQueries
+from .bugzilla import Bugzilla, BugzillaQueries, EditUsersParser
from webkitpy.common.checkout.changelog import parse_bug_id
from webkitpy.common.system.outputcapture import OutputCapture
@@ -392,35 +392,37 @@
queries = BugzillaQueries(Mock())
queries._load_query("request.cgi?action=""
+
+class EditUsersParserTest(unittest.TestCase):
_example_user_results = """
- <div id="bugzilla-body">
- <p>1 user found.</p>
- <table id="admin_table" border="1" cellpadding="4" cellspacing="0">
- <tr bgcolor="#6666FF">
- <th align="left">Edit user...
- </th>
- <th align="left">Real name
- </th>
- <th align="left">Account History
- </th>
- </tr>
- <tr>
- <td >
- <a href=""
- abarth@webkit.org
- </a>
- </td>
- <td >
- Adam Barth
- </td>
- <td >
- <a href=""
- View
- </a>
- </td>
- </tr>
- </table>
-"""
+ <div id="bugzilla-body">
+ <p>1 user found.</p>
+ <table id="admin_table" border="1" cellpadding="4" cellspacing="0">
+ <tr bgcolor="#6666FF">
+ <th align="left">Edit user...
+ </th>
+ <th align="left">Real name
+ </th>
+ <th align="left">Account History
+ </th>
+ </tr>
+ <tr>
+ <td >
+ <a href=""
+ abarth@webkit.org
+ </a>
+ </td>
+ <td >
+ Adam Barth
+ </td>
+ <td >
+ <a href=""
+ View
+ </a>
+ </td>
+ </tr>
+ </table>
+ """
_example_empty_user_results = """
<div id="bugzilla-body">
@@ -438,11 +440,72 @@
</table>
"""
- def _assert_parsed_logins(self, results_page, expected_logins):
- queries = BugzillaQueries(None)
- logins = queries._parse_logins_from_editusers_results(results_page)
+ def _assert_login_userid_pairs(self, results_page, expected_logins):
+ parser = EditUsersParser()
+ logins = parser.login_userid_pairs_from_edit_user_results(results_page)
self.assertEquals(logins, expected_logins)
- def test_parse_logins_from_editusers_results(self):
- self._assert_parsed_logins(self._example_user_results, ["aba...@webkit.org"])
- self._assert_parsed_logins(self._example_empty_user_results, [])
+ def test_logins_from_editusers_results(self):
+ self._assert_login_userid_pairs(self._example_user_results, [("aba...@webkit.org", 1234)])
+ self._assert_login_userid_pairs(self._example_empty_user_results, [])
+
+ _example_user_page = """<table class="main"><tr>
+ <th><label for="" name:</label></th>
+ <td>eric@webkit.org
+ </td>
+</tr>
+<tr>
+ <th><label for="" name:</label></th>
+ <td>Eric Seidel
+ </td>
+</tr>
+ <tr>
+ <th>Group access:</th>
+ <td>
+ <table class="groups">
+ <tr>
+ </tr>
+ <tr>
+ <th colspan="2">User is a member of these groups</th>
+ </tr>
+ <tr class="direct">
+ <td class="checkbox"><input type="checkbox"
+ id="group_7"
+ name="group_7"
+ value="1" checked="checked" /></td>
+ <td class="groupname">
+ <label for=""
+ <strong>canconfirm:</strong>
+ Can confirm a bug.
+ </label>
+ </td>
+ </tr>
+ <tr class="direct">
+ <td class="checkbox"><input type="checkbox"
+ id="group_6"
+ name="group_6"
+ value="1" /></td>
+ <td class="groupname">
+ <label for=""
+ <strong>editbugs:</strong>
+ Can edit all aspects of any bug.
+ /label>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ <th>Product responsibilities:</th>
+ <td>
+ <em>none</em>
+ </td>
+ </tr>
+</table>"""
+
+ def test_user_dict_from_edit_user_page(self):
+ parser = EditUsersParser()
+ user_dict = parser.user_dict_from_edit_user_page(self._example_user_page)
+ expected_user_dict = {u'login': u'e...@webkit.org', u'groups': set(['canconfirm']), u'name': u'Eric Seidel'}
+ self.assertEqual(expected_user_dict, user_dict)
Modified: trunk/Tools/Scripts/webkitpy/tool/commands/__init__.py (95237 => 95238)
--- trunk/Tools/Scripts/webkitpy/tool/commands/__init__.py 2011-09-15 22:06:31 UTC (rev 95237)
+++ trunk/Tools/Scripts/webkitpy/tool/commands/__init__.py 2011-09-15 22:10:50 UTC (rev 95238)
@@ -1,10 +1,12 @@
# Required for Python to search this directory for module files
+from webkitpy.tool.commands.adduserstogroups import AddUsersToGroups
from webkitpy.tool.commands.bugfortest import BugForTest
from webkitpy.tool.commands.bugsearch import BugSearch
from webkitpy.tool.commands.download import *
from webkitpy.tool.commands.earlywarningsystem import *
from webkitpy.tool.commands.expectations import OptimizeExpectations
+from webkitpy.tool.commands.findusers import FindUsers
from webkitpy.tool.commands.gardenomatic import GardenOMatic
from webkitpy.tool.commands.openbugs import OpenBugs
from webkitpy.tool.commands.prettydiff import PrettyDiff
Added: trunk/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py (0 => 95238)
--- trunk/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py (rev 0)
+++ trunk/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py 2011-09-15 22:10:50 UTC (rev 95238)
@@ -0,0 +1,65 @@
+# Copyright (c) 2011 Google Inc. 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.multicommandtool import AbstractDeclarativeCommand
+
+
+class AddUsersToGroups(AbstractDeclarativeCommand):
+ name = "add-users-to-groups"
+ help_text = "Add users matching subtring to specified groups"
+
+ # This probably belongs in bugzilla.py
+ known_groups = ['canconfirm', 'editbugs']
+
+ def execute(self, options, args, tool):
+ search_string = args[0]
+ # FIXME: We could allow users to specify groups on the command line.
+ list_title = 'Add users matching "%s" which groups?' % search_string
+ # FIXME: Need a way to specify that "none" is not allowed.
+ # FIXME: We could lookup what groups the current user is able to grant from bugzilla.
+ groups = tool.user.prompt_with_list(list_title, self.known_groups, can_choose_multiple=True)
+ if not groups:
+ print "No groups specified."
+ return
+
+ login_userid_pairs = tool.bugs.queries.fetch_login_userid_pairs_matching_substring(search_string)
+ if not login_userid_pairs:
+ print "No users found matching '%s'" % search_string
+ return
+
+ print "Found %s users matching %s:" % (len(login_userid_pairs), search_string)
+ for (login, user_id) in login_userid_pairs:
+ print "%s (%s)" % (login, user_id)
+
+ confirm_message = "Are you sure you want add %s users to groups %s? (This action cannot be undone using webkit-patch.)" % (len(login_userid_pairs), groups)
+ if not tool.user.confirm(confirm_message):
+ return
+
+ for (login, user_id) in login_userid_pairs:
+ print "Adding %s to %s" % (login, groups)
+ tool.bugs.add_user_to_groups(user_id, groups)
Added: trunk/Tools/Scripts/webkitpy/tool/commands/findusers.py (0 => 95238)
--- trunk/Tools/Scripts/webkitpy/tool/commands/findusers.py (rev 0)
+++ trunk/Tools/Scripts/webkitpy/tool/commands/findusers.py 2011-09-15 22:10:50 UTC (rev 95238)
@@ -0,0 +1,44 @@
+# Copyright (c) 2011 Google Inc. 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.multicommandtool import AbstractDeclarativeCommand
+
+
+class FindUsers(AbstractDeclarativeCommand):
+ name = "find-users"
+ help_text = "Find users matching substring"
+
+ def execute(self, options, args, tool):
+ search_string = args[0]
+ login_userid_pairs = tool.bugs.queries.fetch_login_userid_pairs_matching_substring(search_string)
+ for (login, user_id) in login_userid_pairs:
+ user = tool.bugs.fetch_user(user_id)
+ groups_string = ", ".join(user['groups']) if user['groups'] else "none"
+ print "%s <%s> (%s) (%s)" % (user['name'], user['login'], user_id, groups_string)
+ else:
+ print "No users found matching '%s'" % search_string