1d0825bca7fe65beaee391d30da42e937db621564Steve Block# Copyright (c) 2009 Google Inc. All rights reserved. 2d0825bca7fe65beaee391d30da42e937db621564Steve Block# Copyright (c) 2009 Apple Inc. All rights reserved. 3d0825bca7fe65beaee391d30da42e937db621564Steve Block# Copyright (c) 2010 Research In Motion Limited. All rights reserved. 4d0825bca7fe65beaee391d30da42e937db621564Steve Block# 5d0825bca7fe65beaee391d30da42e937db621564Steve Block# Redistribution and use in source and binary forms, with or without 6d0825bca7fe65beaee391d30da42e937db621564Steve Block# modification, are permitted provided that the following conditions are 7d0825bca7fe65beaee391d30da42e937db621564Steve Block# met: 8d0825bca7fe65beaee391d30da42e937db621564Steve Block# 9d0825bca7fe65beaee391d30da42e937db621564Steve Block# * Redistributions of source code must retain the above copyright 10d0825bca7fe65beaee391d30da42e937db621564Steve Block# notice, this list of conditions and the following disclaimer. 11d0825bca7fe65beaee391d30da42e937db621564Steve Block# * Redistributions in binary form must reproduce the above 12d0825bca7fe65beaee391d30da42e937db621564Steve Block# copyright notice, this list of conditions and the following disclaimer 13d0825bca7fe65beaee391d30da42e937db621564Steve Block# in the documentation and/or other materials provided with the 14d0825bca7fe65beaee391d30da42e937db621564Steve Block# distribution. 15d0825bca7fe65beaee391d30da42e937db621564Steve Block# * Neither the name of Google Inc. nor the names of its 16d0825bca7fe65beaee391d30da42e937db621564Steve Block# contributors may be used to endorse or promote products derived from 17d0825bca7fe65beaee391d30da42e937db621564Steve Block# this software without specific prior written permission. 18d0825bca7fe65beaee391d30da42e937db621564Steve Block# 19d0825bca7fe65beaee391d30da42e937db621564Steve Block# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20d0825bca7fe65beaee391d30da42e937db621564Steve Block# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21d0825bca7fe65beaee391d30da42e937db621564Steve Block# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22d0825bca7fe65beaee391d30da42e937db621564Steve Block# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23d0825bca7fe65beaee391d30da42e937db621564Steve Block# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24d0825bca7fe65beaee391d30da42e937db621564Steve Block# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25d0825bca7fe65beaee391d30da42e937db621564Steve Block# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26d0825bca7fe65beaee391d30da42e937db621564Steve Block# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27d0825bca7fe65beaee391d30da42e937db621564Steve Block# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28d0825bca7fe65beaee391d30da42e937db621564Steve Block# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29d0825bca7fe65beaee391d30da42e937db621564Steve Block# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30d0825bca7fe65beaee391d30da42e937db621564Steve Block# 31d0825bca7fe65beaee391d30da42e937db621564Steve Block# WebKit's Python module for interacting with Bugzilla 32d0825bca7fe65beaee391d30da42e937db621564Steve Block 3365f03d4f644ce73618e5f4f50dd694b26f55ae12Ben Murdochimport mimetypes 34dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockimport os.path 35d0825bca7fe65beaee391d30da42e937db621564Steve Blockimport re 3621939df44de1705786c545cd1bf519d47250322dBen Murdochimport StringIO 37f05b935882198ccf7d81675736e3aeb089c5113aBen Murdochimport urllib 38d0825bca7fe65beaee391d30da42e937db621564Steve Block 39d0825bca7fe65beaee391d30da42e937db621564Steve Blockfrom datetime import datetime # used in timestamp() 40d0825bca7fe65beaee391d30da42e937db621564Steve Block 416b70adc33054f8aee8c54d0f460458a9df11b8a5Russell Brennerfrom .attachment import Attachment 426b70adc33054f8aee8c54d0f460458a9df11b8a5Russell Brennerfrom .bug import Bug 436b70adc33054f8aee8c54d0f460458a9df11b8a5Russell Brenner 446b70adc33054f8aee8c54d0f460458a9df11b8a5Russell Brennerfrom webkitpy.common.system.deprecated_logging import log 45dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockfrom webkitpy.common.config import committers 46dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockfrom webkitpy.common.net.credentials import Credentials 47dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockfrom webkitpy.common.system.user import User 48dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockfrom webkitpy.thirdparty.autoinstalled.mechanize import Browser 492daae5fd11344eaa88a0d92b0f6d65f8d2255c00Ben Murdochfrom webkitpy.thirdparty.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, SoupStrainer 50d0825bca7fe65beaee391d30da42e937db621564Steve Block 51d0825bca7fe65beaee391d30da42e937db621564Steve Block 526b70adc33054f8aee8c54d0f460458a9df11b8a5Russell Brenner# FIXME: parse_bug_id should not be a free function. 53d0825bca7fe65beaee391d30da42e937db621564Steve Blockdef parse_bug_id(message): 54dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if not message: 55dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return None 562bde8e466a4451c7319e3a072d118917957d6554Steve Block match = re.search(Bugzilla.bug_url_short, message) 57d0825bca7fe65beaee391d30da42e937db621564Steve Block if match: 58d0825bca7fe65beaee391d30da42e937db621564Steve Block return int(match.group('bug_id')) 592bde8e466a4451c7319e3a072d118917957d6554Steve Block match = re.search(Bugzilla.bug_url_long, message) 60d0825bca7fe65beaee391d30da42e937db621564Steve Block if match: 61d0825bca7fe65beaee391d30da42e937db621564Steve Block return int(match.group('bug_id')) 62d0825bca7fe65beaee391d30da42e937db621564Steve Block return None 63d0825bca7fe65beaee391d30da42e937db621564Steve Block 64d0825bca7fe65beaee391d30da42e937db621564Steve Block 652bde8e466a4451c7319e3a072d118917957d6554Steve Block# FIXME: parse_bug_id_from_changelog should not be a free function. 662bde8e466a4451c7319e3a072d118917957d6554Steve Block# Parse the bug ID out of a Changelog message based on the format that is 672bde8e466a4451c7319e3a072d118917957d6554Steve Block# used by prepare-ChangeLog 682bde8e466a4451c7319e3a072d118917957d6554Steve Blockdef parse_bug_id_from_changelog(message): 692bde8e466a4451c7319e3a072d118917957d6554Steve Block if not message: 702bde8e466a4451c7319e3a072d118917957d6554Steve Block return None 712bde8e466a4451c7319e3a072d118917957d6554Steve Block match = re.search("^\s*" + Bugzilla.bug_url_short + "$", message, re.MULTILINE) 722bde8e466a4451c7319e3a072d118917957d6554Steve Block if match: 732bde8e466a4451c7319e3a072d118917957d6554Steve Block return int(match.group('bug_id')) 742bde8e466a4451c7319e3a072d118917957d6554Steve Block match = re.search("^\s*" + Bugzilla.bug_url_long + "$", message, re.MULTILINE) 752bde8e466a4451c7319e3a072d118917957d6554Steve Block if match: 762bde8e466a4451c7319e3a072d118917957d6554Steve Block return int(match.group('bug_id')) 772daae5fd11344eaa88a0d92b0f6d65f8d2255c00Ben Murdoch # We weren't able to find a bug URL in the format used by prepare-ChangeLog. Fall back to the 782daae5fd11344eaa88a0d92b0f6d65f8d2255c00Ben Murdoch # first bug URL found anywhere in the message. 792daae5fd11344eaa88a0d92b0f6d65f8d2255c00Ben Murdoch return parse_bug_id(message) 802bde8e466a4451c7319e3a072d118917957d6554Steve Block 81d0825bca7fe65beaee391d30da42e937db621564Steve Blockdef timestamp(): 82d0825bca7fe65beaee391d30da42e937db621564Steve Block return datetime.now().strftime("%Y%m%d%H%M%S") 83d0825bca7fe65beaee391d30da42e937db621564Steve Block 84d0825bca7fe65beaee391d30da42e937db621564Steve Block 852bde8e466a4451c7319e3a072d118917957d6554Steve Block# A container for all of the logic for making and parsing bugzilla queries. 86d0825bca7fe65beaee391d30da42e937db621564Steve Blockclass BugzillaQueries(object): 87d0825bca7fe65beaee391d30da42e937db621564Steve Block 88d0825bca7fe65beaee391d30da42e937db621564Steve Block def __init__(self, bugzilla): 89d0825bca7fe65beaee391d30da42e937db621564Steve Block self._bugzilla = bugzilla 90d0825bca7fe65beaee391d30da42e937db621564Steve Block 91f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _is_xml_bugs_form(self, form): 92f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # ClientForm.HTMLForm.find_control throws if the control is not found, 93f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # so we do a manual search instead: 94f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return "xml" in [control.id for control in form.controls] 95f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 96f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # This is kinda a hack. There is probably a better way to get this information from bugzilla. 97f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _parse_result_count(self, results_page): 98f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch result_count_text = BeautifulSoup(results_page).find(attrs={'class': 'bz_result_count'}).string 99f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch result_count_parts = result_count_text.strip().split(" ") 100f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if result_count_parts[0] == "Zarro": 101f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return 0 102f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if result_count_parts[0] == "One": 103f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return 1 104f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return int(result_count_parts[0]) 105f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 106f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # Note: _load_query, _fetch_bug and _fetch_bugs_from_advanced_query 107f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # are the only methods which access self._bugzilla. 108d0825bca7fe65beaee391d30da42e937db621564Steve Block 109d0825bca7fe65beaee391d30da42e937db621564Steve Block def _load_query(self, query): 110d0825bca7fe65beaee391d30da42e937db621564Steve Block self._bugzilla.authenticate() 111d0825bca7fe65beaee391d30da42e937db621564Steve Block full_url = "%s%s" % (self._bugzilla.bug_server_url, query) 112d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._bugzilla.browser.open(full_url) 113d0825bca7fe65beaee391d30da42e937db621564Steve Block 114f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _fetch_bugs_from_advanced_query(self, query): 115f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch results_page = self._load_query(query) 116f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if not self._parse_result_count(results_page): 117f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return [] 118f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # Bugzilla results pages have an "XML" submit button at the bottom 119f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # which can be used to get an XML page containing all of the <bug> elements. 120f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # This is slighty lame that this assumes that _load_query used 121f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # self._bugzilla.browser and that it's in an acceptable state. 122f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self._bugzilla.browser.select_form(predicate=self._is_xml_bugs_form) 123f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch bugs_xml = self._bugzilla.browser.submit() 124f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return self._bugzilla._parse_bugs_from_xml(bugs_xml) 125f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 126d0825bca7fe65beaee391d30da42e937db621564Steve Block def _fetch_bug(self, bug_id): 127d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._bugzilla.fetch_bug(bug_id) 128d0825bca7fe65beaee391d30da42e937db621564Steve Block 129d0825bca7fe65beaee391d30da42e937db621564Steve Block def _fetch_bug_ids_advanced_query(self, query): 130d0825bca7fe65beaee391d30da42e937db621564Steve Block soup = BeautifulSoup(self._load_query(query)) 131d0825bca7fe65beaee391d30da42e937db621564Steve Block # The contents of the <a> inside the cells in the first column happen 132d0825bca7fe65beaee391d30da42e937db621564Steve Block # to be the bug id. 133d0825bca7fe65beaee391d30da42e937db621564Steve Block return [int(bug_link_cell.find("a").string) 134d0825bca7fe65beaee391d30da42e937db621564Steve Block for bug_link_cell in soup('td', "first-child")] 135d0825bca7fe65beaee391d30da42e937db621564Steve Block 136d0825bca7fe65beaee391d30da42e937db621564Steve Block def _parse_attachment_ids_request_query(self, page): 137d0825bca7fe65beaee391d30da42e937db621564Steve Block digits = re.compile("\d+") 138d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_href = re.compile("attachment.cgi\?id=\d+&action=review") 139d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_links = SoupStrainer("a", href=attachment_href) 140d0825bca7fe65beaee391d30da42e937db621564Steve Block return [int(digits.search(tag["href"]).group(0)) 141d0825bca7fe65beaee391d30da42e937db621564Steve Block for tag in BeautifulSoup(page, parseOnlyThese=attachment_links)] 142d0825bca7fe65beaee391d30da42e937db621564Steve Block 143d0825bca7fe65beaee391d30da42e937db621564Steve Block def _fetch_attachment_ids_request_query(self, query): 144d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._parse_attachment_ids_request_query(self._load_query(query)) 145d0825bca7fe65beaee391d30da42e937db621564Steve Block 146dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block def _parse_quips(self, page): 147dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block soup = BeautifulSoup(page, convertEntities=BeautifulSoup.HTML_ENTITIES) 148dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block quips = soup.find(text=re.compile(r"Existing quips:")).findNext("ul").findAll("li") 149dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return [unicode(quip_entry.string) for quip_entry in quips] 150dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block 151dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block def fetch_quips(self): 152dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return self._parse_quips(self._load_query("/quips.cgi?action=show")) 153dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block 154d0825bca7fe65beaee391d30da42e937db621564Steve Block # List of all r+'d bugs. 155d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_bug_ids_from_pending_commit_list(self): 156d0825bca7fe65beaee391d30da42e937db621564Steve Block needs_commit_query_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=review%2B" 157d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._fetch_bug_ids_advanced_query(needs_commit_query_url) 158d0825bca7fe65beaee391d30da42e937db621564Steve Block 159f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def fetch_bugs_matching_quicksearch(self, search_string): 160f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # We may want to use a more explicit query than "quicksearch". 161f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # If quicksearch changes we should probably change to use 162f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # a normal buglist.cgi?query_format=advanced query. 163f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch quicksearch_url = "buglist.cgi?quicksearch=%s" % urllib.quote(search_string) 164f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return self._fetch_bugs_from_advanced_query(quicksearch_url) 165f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 166f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # Currently this returns all bugs across all components. 167f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # In the future we may wish to extend this API to construct more restricted searches. 168f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def fetch_bugs_matching_search(self, search_string, author_email=None): 169f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch query = "buglist.cgi?query_format=advanced" 170f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if search_string: 171f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch query += "&short_desc_type=allwordssubstr&short_desc=%s" % urllib.quote(search_string) 172f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if author_email: 173f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch query += "&emailreporter1=1&emailtype1=substring&email1=%s" % urllib.quote(search_string) 174f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return self._fetch_bugs_from_advanced_query(query) 175f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 176d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_patches_from_pending_commit_list(self): 177d0825bca7fe65beaee391d30da42e937db621564Steve Block return sum([self._fetch_bug(bug_id).reviewed_patches() 178d0825bca7fe65beaee391d30da42e937db621564Steve Block for bug_id in self.fetch_bug_ids_from_pending_commit_list()], []) 179d0825bca7fe65beaee391d30da42e937db621564Steve Block 180d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_bug_ids_from_commit_queue(self): 181d0825bca7fe65beaee391d30da42e937db621564Steve Block commit_queue_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=commit-queue%2B&order=Last+Changed" 182d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._fetch_bug_ids_advanced_query(commit_queue_url) 183d0825bca7fe65beaee391d30da42e937db621564Steve Block 184d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_patches_from_commit_queue(self): 185d0825bca7fe65beaee391d30da42e937db621564Steve Block # This function will only return patches which have valid committers 186d0825bca7fe65beaee391d30da42e937db621564Steve Block # set. It won't reject patches with invalid committers/reviewers. 187d0825bca7fe65beaee391d30da42e937db621564Steve Block return sum([self._fetch_bug(bug_id).commit_queued_patches() 188d0825bca7fe65beaee391d30da42e937db621564Steve Block for bug_id in self.fetch_bug_ids_from_commit_queue()], []) 189d0825bca7fe65beaee391d30da42e937db621564Steve Block 19028040489d744e0c5d475a88663056c9040ed5320Teng-Hui Zhu def fetch_bug_ids_from_review_queue(self): 191d0825bca7fe65beaee391d30da42e937db621564Steve Block review_queue_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=review?" 192d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._fetch_bug_ids_advanced_query(review_queue_url) 193d0825bca7fe65beaee391d30da42e937db621564Steve Block 194a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch # This method will make several requests to bugzilla. 195d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_patches_from_review_queue(self, limit=None): 196d0825bca7fe65beaee391d30da42e937db621564Steve Block # [:None] returns the whole array. 197d0825bca7fe65beaee391d30da42e937db621564Steve Block return sum([self._fetch_bug(bug_id).unreviewed_patches() 19828040489d744e0c5d475a88663056c9040ed5320Teng-Hui Zhu for bug_id in self.fetch_bug_ids_from_review_queue()[:limit]], []) 199d0825bca7fe65beaee391d30da42e937db621564Steve Block 200a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch # NOTE: This is the only client of _fetch_attachment_ids_request_query 201a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch # This method only makes one request to bugzilla. 202d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_attachment_ids_from_review_queue(self): 203d0825bca7fe65beaee391d30da42e937db621564Steve Block review_queue_url = "request.cgi?action=queue&type=review&group=type" 204d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._fetch_attachment_ids_request_query(review_queue_url) 205d0825bca7fe65beaee391d30da42e937db621564Steve Block 206d0825bca7fe65beaee391d30da42e937db621564Steve Block 207d0825bca7fe65beaee391d30da42e937db621564Steve Blockclass Bugzilla(object): 208d0825bca7fe65beaee391d30da42e937db621564Steve Block 209dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block def __init__(self, dryrun=False, committers=committers.CommitterList()): 210d0825bca7fe65beaee391d30da42e937db621564Steve Block self.dryrun = dryrun 211d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticated = False 212d0825bca7fe65beaee391d30da42e937db621564Steve Block self.queries = BugzillaQueries(self) 213d0825bca7fe65beaee391d30da42e937db621564Steve Block self.committers = committers 214dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.cached_quips = [] 215d0825bca7fe65beaee391d30da42e937db621564Steve Block 216d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: We should use some sort of Browser mock object when in dryrun 217d0825bca7fe65beaee391d30da42e937db621564Steve Block # mode (to prevent any mistakes). 218d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser = Browser() 219d0825bca7fe65beaee391d30da42e937db621564Steve Block # Ignore bugs.webkit.org/robots.txt until we fix it to allow this 220d0825bca7fe65beaee391d30da42e937db621564Steve Block # script. 221d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.set_handle_robots(False) 222d0825bca7fe65beaee391d30da42e937db621564Steve Block 2232daae5fd11344eaa88a0d92b0f6d65f8d2255c00Ben Murdoch # FIXME: Much of this should go into some sort of config module, 2242daae5fd11344eaa88a0d92b0f6d65f8d2255c00Ben Murdoch # such as common.config.urls. 225d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_server_host = "bugs.webkit.org" 226d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_server_regex = "https?://%s/" % re.sub('\.', '\\.', bug_server_host) 227d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_server_url = "https://%s/" % bug_server_host 2282bde8e466a4451c7319e3a072d118917957d6554Steve Block bug_url_long = bug_server_regex + r"show_bug\.cgi\?id=(?P<bug_id>\d+)(&ctype=xml)?" 2292bde8e466a4451c7319e3a072d118917957d6554Steve Block bug_url_short = r"http\://webkit\.org/b/(?P<bug_id>\d+)" 230dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block 231dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block def quips(self): 232dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block # We only fetch and parse the list of quips once per instantiation 233dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block # so that we do not burden bugs.webkit.org. 234dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if not self.cached_quips and not self.dryrun: 235dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.cached_quips = self.queries.fetch_quips() 236dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return self.cached_quips 237d0825bca7fe65beaee391d30da42e937db621564Steve Block 238d0825bca7fe65beaee391d30da42e937db621564Steve Block def bug_url_for_bug_id(self, bug_id, xml=False): 239dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if not bug_id: 240dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return None 241d0825bca7fe65beaee391d30da42e937db621564Steve Block content_type = "&ctype=xml" if xml else "" 242f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return "%sshow_bug.cgi?id=%s%s" % (self.bug_server_url, bug_id, content_type) 243d0825bca7fe65beaee391d30da42e937db621564Steve Block 244d0825bca7fe65beaee391d30da42e937db621564Steve Block def short_bug_url_for_bug_id(self, bug_id): 245dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if not bug_id: 246dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return None 247d0825bca7fe65beaee391d30da42e937db621564Steve Block return "http://webkit.org/b/%s" % bug_id 248d0825bca7fe65beaee391d30da42e937db621564Steve Block 249f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def add_attachment_url(self, bug_id): 250f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return "%sattachment.cgi?action=enter&bugid=%s" % (self.bug_server_url, bug_id) 251f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 252d0825bca7fe65beaee391d30da42e937db621564Steve Block def attachment_url_for_id(self, attachment_id, action="view"): 253dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if not attachment_id: 254dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return None 255d0825bca7fe65beaee391d30da42e937db621564Steve Block action_param = "" 256d0825bca7fe65beaee391d30da42e937db621564Steve Block if action and action != "view": 257d0825bca7fe65beaee391d30da42e937db621564Steve Block action_param = "&action=%s" % action 258d0825bca7fe65beaee391d30da42e937db621564Steve Block return "%sattachment.cgi?id=%s%s" % (self.bug_server_url, 259d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_id, 260d0825bca7fe65beaee391d30da42e937db621564Steve Block action_param) 261d0825bca7fe65beaee391d30da42e937db621564Steve Block 262d0825bca7fe65beaee391d30da42e937db621564Steve Block def _parse_attachment_flag(self, 263d0825bca7fe65beaee391d30da42e937db621564Steve Block element, 264d0825bca7fe65beaee391d30da42e937db621564Steve Block flag_name, 265d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment, 266d0825bca7fe65beaee391d30da42e937db621564Steve Block result_key): 267d0825bca7fe65beaee391d30da42e937db621564Steve Block flag = element.find('flag', attrs={'name': flag_name}) 268d0825bca7fe65beaee391d30da42e937db621564Steve Block if flag: 269d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment[flag_name] = flag['status'] 270d0825bca7fe65beaee391d30da42e937db621564Steve Block if flag['status'] == '+': 271d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment[result_key] = flag['setter'] 272e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # Sadly show_bug.cgi?ctype=xml does not expose the flag modification date. 273d0825bca7fe65beaee391d30da42e937db621564Steve Block 27421939df44de1705786c545cd1bf519d47250322dBen Murdoch def _string_contents(self, soup): 27521939df44de1705786c545cd1bf519d47250322dBen Murdoch # WebKit's bugzilla instance uses UTF-8. 2762daae5fd11344eaa88a0d92b0f6d65f8d2255c00Ben Murdoch # BeautifulStoneSoup always returns Unicode strings, however 27721939df44de1705786c545cd1bf519d47250322dBen Murdoch # the .string method returns a (unicode) NavigableString. 27821939df44de1705786c545cd1bf519d47250322dBen Murdoch # NavigableString can confuse other parts of the code, so we 27921939df44de1705786c545cd1bf519d47250322dBen Murdoch # convert from NavigableString to a real unicode() object using unicode(). 28021939df44de1705786c545cd1bf519d47250322dBen Murdoch return unicode(soup.string) 28121939df44de1705786c545cd1bf519d47250322dBen Murdoch 282e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # Example: 2010-01-20 14:31 PST 283e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # FIXME: Some bugzilla dates seem to have seconds in them? 284e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # Python does not support timezones out of the box. 285e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # Assume that bugzilla always uses PST (which is true for bugs.webkit.org) 286e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block _bugzilla_date_format = "%Y-%m-%d %H:%M" 287e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block 288e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block @classmethod 289e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block def _parse_date(cls, date_string): 290e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block (date, time, time_zone) = date_string.split(" ") 291e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # Ignore the timezone because python doesn't understand timezones out of the box. 292e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block date_string = "%s %s" % (date, time) 293e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block return datetime.strptime(date_string, cls._bugzilla_date_format) 29421939df44de1705786c545cd1bf519d47250322dBen Murdoch 295e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block def _date_contents(self, soup): 296e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block return self._parse_date(self._string_contents(soup)) 297e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block 298e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block def _parse_attachment_element(self, element, bug_id): 299d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment = {} 300d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment['bug_id'] = bug_id 301d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment['is_obsolete'] = (element.has_key('isobsolete') and element['isobsolete'] == "1") 302d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment['is_patch'] = (element.has_key('ispatch') and element['ispatch'] == "1") 303d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment['id'] = int(element.find('attachid').string) 304d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: No need to parse out the url here. 305d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment['url'] = self.attachment_url_for_id(attachment['id']) 306e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block attachment["attach_date"] = self._date_contents(element.find("date")) 30721939df44de1705786c545cd1bf519d47250322dBen Murdoch attachment['name'] = self._string_contents(element.find('desc')) 30821939df44de1705786c545cd1bf519d47250322dBen Murdoch attachment['attacher_email'] = self._string_contents(element.find('attacher')) 30921939df44de1705786c545cd1bf519d47250322dBen Murdoch attachment['type'] = self._string_contents(element.find('type')) 310d0825bca7fe65beaee391d30da42e937db621564Steve Block self._parse_attachment_flag( 311d0825bca7fe65beaee391d30da42e937db621564Steve Block element, 'review', attachment, 'reviewer_email') 312d0825bca7fe65beaee391d30da42e937db621564Steve Block self._parse_attachment_flag( 313d0825bca7fe65beaee391d30da42e937db621564Steve Block element, 'commit-queue', attachment, 'committer_email') 314d0825bca7fe65beaee391d30da42e937db621564Steve Block return attachment 315d0825bca7fe65beaee391d30da42e937db621564Steve Block 316f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _parse_bugs_from_xml(self, page): 317f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch soup = BeautifulSoup(page) 318f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # Without the unicode() call, BeautifulSoup occasionally complains of being 319f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # passed None for no apparent reason. 320f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return [Bug(self._parse_bug_dictionary_from_xml(unicode(bug_xml)), self) for bug_xml in soup('bug')] 321f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 322f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _parse_bug_dictionary_from_xml(self, page): 3232daae5fd11344eaa88a0d92b0f6d65f8d2255c00Ben Murdoch soup = BeautifulStoneSoup(page, convertEntities=BeautifulStoneSoup.XML_ENTITIES) 324d0825bca7fe65beaee391d30da42e937db621564Steve Block bug = {} 325d0825bca7fe65beaee391d30da42e937db621564Steve Block bug["id"] = int(soup.find("bug_id").string) 32621939df44de1705786c545cd1bf519d47250322dBen Murdoch bug["title"] = self._string_contents(soup.find("short_desc")) 32768513a70bcd92384395513322f1b801e7bf9c729Steve Block bug["bug_status"] = self._string_contents(soup.find("bug_status")) 328f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch dup_id = soup.find("dup_id") 329f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if dup_id: 330f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch bug["dup_id"] = self._string_contents(dup_id) 33121939df44de1705786c545cd1bf519d47250322dBen Murdoch bug["reporter_email"] = self._string_contents(soup.find("reporter")) 33221939df44de1705786c545cd1bf519d47250322dBen Murdoch bug["assigned_to_email"] = self._string_contents(soup.find("assigned_to")) 333f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch bug["cc_emails"] = [self._string_contents(element) for element in soup.findAll('cc')] 334d0825bca7fe65beaee391d30da42e937db621564Steve Block bug["attachments"] = [self._parse_attachment_element(element, bug["id"]) for element in soup.findAll('attachment')] 335d0825bca7fe65beaee391d30da42e937db621564Steve Block return bug 336d0825bca7fe65beaee391d30da42e937db621564Steve Block 337d0825bca7fe65beaee391d30da42e937db621564Steve Block # Makes testing fetch_*_from_bug() possible until we have a better 338d0825bca7fe65beaee391d30da42e937db621564Steve Block # BugzillaNetwork abstration. 339d0825bca7fe65beaee391d30da42e937db621564Steve Block 340d0825bca7fe65beaee391d30da42e937db621564Steve Block def _fetch_bug_page(self, bug_id): 341d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_url = self.bug_url_for_bug_id(bug_id, xml=True) 342d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Fetching: %s" % bug_url) 343d0825bca7fe65beaee391d30da42e937db621564Steve Block return self.browser.open(bug_url) 344d0825bca7fe65beaee391d30da42e937db621564Steve Block 345d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_bug_dictionary(self, bug_id): 346dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block try: 347f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return self._parse_bug_dictionary_from_xml(self._fetch_bug_page(bug_id)) 3485ddde30071f639962dd557c453f2ad01f8f0fd00Kristian Monsen except KeyboardInterrupt: 3495ddde30071f639962dd557c453f2ad01f8f0fd00Kristian Monsen raise 350dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block except: 351dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.authenticate() 352f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return self._parse_bug_dictionary_from_xml(self._fetch_bug_page(bug_id)) 353d0825bca7fe65beaee391d30da42e937db621564Steve Block 354d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: A BugzillaCache object should provide all these fetch_ methods. 355d0825bca7fe65beaee391d30da42e937db621564Steve Block 356d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_bug(self, bug_id): 357d0825bca7fe65beaee391d30da42e937db621564Steve Block return Bug(self.fetch_bug_dictionary(bug_id), self) 358d0825bca7fe65beaee391d30da42e937db621564Steve Block 35921939df44de1705786c545cd1bf519d47250322dBen Murdoch def fetch_attachment_contents(self, attachment_id): 36021939df44de1705786c545cd1bf519d47250322dBen Murdoch attachment_url = self.attachment_url_for_id(attachment_id) 36121939df44de1705786c545cd1bf519d47250322dBen Murdoch # We need to authenticate to download patches from security bugs. 36221939df44de1705786c545cd1bf519d47250322dBen Murdoch self.authenticate() 36321939df44de1705786c545cd1bf519d47250322dBen Murdoch return self.browser.open(attachment_url).read() 36421939df44de1705786c545cd1bf519d47250322dBen Murdoch 365d0825bca7fe65beaee391d30da42e937db621564Steve Block def _parse_bug_id_from_attachment_page(self, page): 366d0825bca7fe65beaee391d30da42e937db621564Steve Block # The "Up" relation happens to point to the bug. 367d0825bca7fe65beaee391d30da42e937db621564Steve Block up_link = BeautifulSoup(page).find('link', rel='Up') 368d0825bca7fe65beaee391d30da42e937db621564Steve Block if not up_link: 369d0825bca7fe65beaee391d30da42e937db621564Steve Block # This attachment does not exist (or you don't have permissions to 370d0825bca7fe65beaee391d30da42e937db621564Steve Block # view it). 371d0825bca7fe65beaee391d30da42e937db621564Steve Block return None 372d0825bca7fe65beaee391d30da42e937db621564Steve Block match = re.search("show_bug.cgi\?id=(?P<bug_id>\d+)", up_link['href']) 373d0825bca7fe65beaee391d30da42e937db621564Steve Block return int(match.group('bug_id')) 374d0825bca7fe65beaee391d30da42e937db621564Steve Block 375d0825bca7fe65beaee391d30da42e937db621564Steve Block def bug_id_for_attachment_id(self, attachment_id): 376d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 377d0825bca7fe65beaee391d30da42e937db621564Steve Block 378d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_url = self.attachment_url_for_id(attachment_id, 'edit') 379d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Fetching: %s" % attachment_url) 380d0825bca7fe65beaee391d30da42e937db621564Steve Block page = self.browser.open(attachment_url) 381d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._parse_bug_id_from_attachment_page(page) 382d0825bca7fe65beaee391d30da42e937db621564Steve Block 383d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: This should just return Attachment(id), which should be able to 384d0825bca7fe65beaee391d30da42e937db621564Steve Block # lazily fetch needed data. 385d0825bca7fe65beaee391d30da42e937db621564Steve Block 386d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_attachment(self, attachment_id): 387d0825bca7fe65beaee391d30da42e937db621564Steve Block # We could grab all the attachment details off of the attachment edit 388d0825bca7fe65beaee391d30da42e937db621564Steve Block # page but we already have working code to do so off of the bugs page, 389d0825bca7fe65beaee391d30da42e937db621564Steve Block # so re-use that. 390d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_id = self.bug_id_for_attachment_id(attachment_id) 391d0825bca7fe65beaee391d30da42e937db621564Steve Block if not bug_id: 392d0825bca7fe65beaee391d30da42e937db621564Steve Block return None 393d0825bca7fe65beaee391d30da42e937db621564Steve Block attachments = self.fetch_bug(bug_id).attachments(include_obsolete=True) 394d0825bca7fe65beaee391d30da42e937db621564Steve Block for attachment in attachments: 395d0825bca7fe65beaee391d30da42e937db621564Steve Block if attachment.id() == int(attachment_id): 396d0825bca7fe65beaee391d30da42e937db621564Steve Block return attachment 397d0825bca7fe65beaee391d30da42e937db621564Steve Block return None # This should never be hit. 398d0825bca7fe65beaee391d30da42e937db621564Steve Block 399d0825bca7fe65beaee391d30da42e937db621564Steve Block def authenticate(self): 400d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.authenticated: 401d0825bca7fe65beaee391d30da42e937db621564Steve Block return 402d0825bca7fe65beaee391d30da42e937db621564Steve Block 403d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 404d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Skipping log in for dry run...") 405d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticated = True 406d0825bca7fe65beaee391d30da42e937db621564Steve Block return 407d0825bca7fe65beaee391d30da42e937db621564Steve Block 408e14391e94c850b8bd03680c23b38978db68687a8John Reck credentials = Credentials(self.bug_server_host, git_prefix="bugzilla") 409e14391e94c850b8bd03680c23b38978db68687a8John Reck 410d0825bca7fe65beaee391d30da42e937db621564Steve Block attempts = 0 411d0825bca7fe65beaee391d30da42e937db621564Steve Block while not self.authenticated: 412d0825bca7fe65beaee391d30da42e937db621564Steve Block attempts += 1 413e14391e94c850b8bd03680c23b38978db68687a8John Reck username, password = credentials.read_credentials() 414d0825bca7fe65beaee391d30da42e937db621564Steve Block 415d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Logging in as %s..." % username) 416d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_server_url + 417d0825bca7fe65beaee391d30da42e937db621564Steve Block "index.cgi?GoAheadAndLogIn=1") 418d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="login") 419d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['Bugzilla_login'] = username 420d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['Bugzilla_password'] = password 421d0825bca7fe65beaee391d30da42e937db621564Steve Block response = self.browser.submit() 422d0825bca7fe65beaee391d30da42e937db621564Steve Block 423d0825bca7fe65beaee391d30da42e937db621564Steve Block match = re.search("<title>(.+?)</title>", response.read()) 424d0825bca7fe65beaee391d30da42e937db621564Steve Block # If the resulting page has a title, and it contains the word 425d0825bca7fe65beaee391d30da42e937db621564Steve Block # "invalid" assume it's the login failure page. 426d0825bca7fe65beaee391d30da42e937db621564Steve Block if match and re.search("Invalid", match.group(1), re.IGNORECASE): 427d0825bca7fe65beaee391d30da42e937db621564Steve Block errorMessage = "Bugzilla login failed: %s" % match.group(1) 428d0825bca7fe65beaee391d30da42e937db621564Steve Block # raise an exception only if this was the last attempt 429d0825bca7fe65beaee391d30da42e937db621564Steve Block if attempts < 5: 430d0825bca7fe65beaee391d30da42e937db621564Steve Block log(errorMessage) 431d0825bca7fe65beaee391d30da42e937db621564Steve Block else: 432d0825bca7fe65beaee391d30da42e937db621564Steve Block raise Exception(errorMessage) 433d0825bca7fe65beaee391d30da42e937db621564Steve Block else: 434d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticated = True 435e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block self.username = username 436d0825bca7fe65beaee391d30da42e937db621564Steve Block 437f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _commit_queue_flag(self, mark_for_landing, mark_for_commit_queue): 438f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if mark_for_landing: 439f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return '+' 440f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch elif mark_for_commit_queue: 441f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return '?' 442f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return 'X' 443f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 444f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # FIXME: mark_for_commit_queue and mark_for_landing should be joined into a single commit_flag argument. 445d0825bca7fe65beaee391d30da42e937db621564Steve Block def _fill_attachment_form(self, 446d0825bca7fe65beaee391d30da42e937db621564Steve Block description, 447f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_object, 448d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_review=False, 449d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_commit_queue=False, 450545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch mark_for_landing=False, 451f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch is_patch=False, 452f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename=None, 453f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch mimetype=None): 454d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['description'] = description 455f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if is_patch: 456f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser['ispatch'] = ("1",) 457f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # FIXME: Should this use self._find_select_element_for_flag? 458d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['flag_type-1'] = ('?',) if mark_for_review else ('X',) 459f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser['flag_type-3'] = (self._commit_queue_flag(mark_for_landing, mark_for_commit_queue),) 460f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 461f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename = filename or "%s.patch" % timestamp() 46265f03d4f644ce73618e5f4f50dd694b26f55ae12Ben Murdoch if not mimetype: 46365f03d4f644ce73618e5f4f50dd694b26f55ae12Ben Murdoch mimetypes.add_type('text/plain', '.patch') # Make sure mimetypes knows about .patch 46465f03d4f644ce73618e5f4f50dd694b26f55ae12Ben Murdoch mimetype, _ = mimetypes.guess_type(filename) 46565f03d4f644ce73618e5f4f50dd694b26f55ae12Ben Murdoch if not mimetype: 46665f03d4f644ce73618e5f4f50dd694b26f55ae12Ben Murdoch mimetype = "text/plain" # Bugzilla might auto-guess for us and we might not need this? 467f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser.add_file(file_object, mimetype, filename, 'data') 468f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 469f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _file_object_for_upload(self, file_or_string): 470f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if hasattr(file_or_string, 'read'): 471f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return file_or_string 472f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # Only if file_or_string is not already encoded do we want to encode it. 473f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if isinstance(file_or_string, unicode): 474f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_or_string = file_or_string.encode('utf-8') 475f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return StringIO.StringIO(file_or_string) 476f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 477f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # timestamp argument is just for unittests. 478f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _filename_for_upload(self, file_object, bug_id, extension="txt", timestamp=timestamp): 479f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if hasattr(file_object, "name"): 480f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return file_object.name 481f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return "bug-%s-%s.%s" % (bug_id, timestamp(), extension) 482f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 483f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def add_attachment_to_bug(self, 484f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch bug_id, 485f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_or_string, 486f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch description, 487f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename=None, 488f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch comment_text=None): 489f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.authenticate() 490f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch log('Adding attachment "%s" to %s' % (description, self.bug_url_for_bug_id(bug_id))) 491f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if self.dryrun: 492f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch log(comment_text) 493f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return 494d0825bca7fe65beaee391d30da42e937db621564Steve Block 495f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser.open(self.add_attachment_url(bug_id)) 496f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser.select_form(name="entryform") 497f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_object = self._file_object_for_upload(file_or_string) 498f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename = filename or self._filename_for_upload(file_object, bug_id) 499f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self._fill_attachment_form(description, file_object, filename=filename) 500f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if comment_text: 501f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch log(comment_text) 502f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser['comment'] = comment_text 503f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser.submit() 504d0825bca7fe65beaee391d30da42e937db621564Steve Block 505f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # FIXME: The arguments to this function should be simplified and then 506f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # this should be merged into add_attachment_to_bug 507d0825bca7fe65beaee391d30da42e937db621564Steve Block def add_patch_to_bug(self, 508d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_id, 509f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_or_string, 510d0825bca7fe65beaee391d30da42e937db621564Steve Block description, 511d0825bca7fe65beaee391d30da42e937db621564Steve Block comment_text=None, 512d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_review=False, 513d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_commit_queue=False, 514d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_landing=False): 515d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 516f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch log('Adding patch "%s" to %s' % (description, self.bug_url_for_bug_id(bug_id))) 517d0825bca7fe65beaee391d30da42e937db621564Steve Block 518d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 519d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 520d0825bca7fe65beaee391d30da42e937db621564Steve Block return 521d0825bca7fe65beaee391d30da42e937db621564Steve Block 522f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser.open(self.add_attachment_url(bug_id)) 523d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="entryform") 524f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_object = self._file_object_for_upload(file_or_string) 525f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename = self._filename_for_upload(file_object, bug_id, extension="patch") 526d0825bca7fe65beaee391d30da42e937db621564Steve Block self._fill_attachment_form(description, 527f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_object, 528d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_review=mark_for_review, 529d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_commit_queue=mark_for_commit_queue, 530d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_landing=mark_for_landing, 531f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch is_patch=True, 532f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename=filename) 533d0825bca7fe65beaee391d30da42e937db621564Steve Block if comment_text: 534d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 535d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['comment'] = comment_text 536d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 537d0825bca7fe65beaee391d30da42e937db621564Steve Block 538f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # FIXME: There has to be a more concise way to write this method. 539d0825bca7fe65beaee391d30da42e937db621564Steve Block def _check_create_bug_response(self, response_html): 540d0825bca7fe65beaee391d30da42e937db621564Steve Block match = re.search("<title>Bug (?P<bug_id>\d+) Submitted</title>", 541d0825bca7fe65beaee391d30da42e937db621564Steve Block response_html) 542d0825bca7fe65beaee391d30da42e937db621564Steve Block if match: 543d0825bca7fe65beaee391d30da42e937db621564Steve Block return match.group('bug_id') 544d0825bca7fe65beaee391d30da42e937db621564Steve Block 545d0825bca7fe65beaee391d30da42e937db621564Steve Block match = re.search( 546d0825bca7fe65beaee391d30da42e937db621564Steve Block '<div id="bugzilla-body">(?P<error_message>.+)<div id="footer">', 547d0825bca7fe65beaee391d30da42e937db621564Steve Block response_html, 548d0825bca7fe65beaee391d30da42e937db621564Steve Block re.DOTALL) 549d0825bca7fe65beaee391d30da42e937db621564Steve Block error_message = "FAIL" 550d0825bca7fe65beaee391d30da42e937db621564Steve Block if match: 551d0825bca7fe65beaee391d30da42e937db621564Steve Block text_lines = BeautifulSoup( 552d0825bca7fe65beaee391d30da42e937db621564Steve Block match.group('error_message')).findAll(text=True) 553d0825bca7fe65beaee391d30da42e937db621564Steve Block error_message = "\n" + '\n'.join( 554d0825bca7fe65beaee391d30da42e937db621564Steve Block [" " + line.strip() 555d0825bca7fe65beaee391d30da42e937db621564Steve Block for line in text_lines if line.strip()]) 556d0825bca7fe65beaee391d30da42e937db621564Steve Block raise Exception("Bug not created: %s" % error_message) 557d0825bca7fe65beaee391d30da42e937db621564Steve Block 558d0825bca7fe65beaee391d30da42e937db621564Steve Block def create_bug(self, 559d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_title, 560d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_description, 561d0825bca7fe65beaee391d30da42e937db621564Steve Block component=None, 56221939df44de1705786c545cd1bf519d47250322dBen Murdoch diff=None, 563d0825bca7fe65beaee391d30da42e937db621564Steve Block patch_description=None, 564d0825bca7fe65beaee391d30da42e937db621564Steve Block cc=None, 565dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block blocked=None, 566e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block assignee=None, 567d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_review=False, 568d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_commit_queue=False): 569d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 570d0825bca7fe65beaee391d30da42e937db621564Steve Block 571d0825bca7fe65beaee391d30da42e937db621564Steve Block log('Creating bug with title "%s"' % bug_title) 572d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 573d0825bca7fe65beaee391d30da42e937db621564Steve Block log(bug_description) 574f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # FIXME: This will make some paths fail, as they assume this returns an id. 575d0825bca7fe65beaee391d30da42e937db621564Steve Block return 576d0825bca7fe65beaee391d30da42e937db621564Steve Block 577d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_server_url + "enter_bug.cgi?product=WebKit") 578d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="Create") 579d0825bca7fe65beaee391d30da42e937db621564Steve Block component_items = self.browser.find_control('component').items 580d0825bca7fe65beaee391d30da42e937db621564Steve Block component_names = map(lambda item: item.name, component_items) 581d0825bca7fe65beaee391d30da42e937db621564Steve Block if not component: 582d0825bca7fe65beaee391d30da42e937db621564Steve Block component = "New Bugs" 583d0825bca7fe65beaee391d30da42e937db621564Steve Block if component not in component_names: 584dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block component = User.prompt_with_list("Please pick a component:", component_names) 585dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.browser["component"] = [component] 586d0825bca7fe65beaee391d30da42e937db621564Steve Block if cc: 587dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.browser["cc"] = cc 588dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if blocked: 58921939df44de1705786c545cd1bf519d47250322dBen Murdoch self.browser["blocked"] = unicode(blocked) 590f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if not assignee: 591e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block assignee = self.username 592545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch if assignee and not self.browser.find_control("assigned_to").disabled: 593e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block self.browser["assigned_to"] = assignee 594dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.browser["short_desc"] = bug_title 595dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.browser["comment"] = bug_description 596d0825bca7fe65beaee391d30da42e937db621564Steve Block 59721939df44de1705786c545cd1bf519d47250322dBen Murdoch if diff: 59821939df44de1705786c545cd1bf519d47250322dBen Murdoch # _fill_attachment_form expects a file-like object 59921939df44de1705786c545cd1bf519d47250322dBen Murdoch # Patch files are already binary, so no encoding needed. 60021939df44de1705786c545cd1bf519d47250322dBen Murdoch assert(isinstance(diff, str)) 60121939df44de1705786c545cd1bf519d47250322dBen Murdoch patch_file_object = StringIO.StringIO(diff) 602d0825bca7fe65beaee391d30da42e937db621564Steve Block self._fill_attachment_form( 603d0825bca7fe65beaee391d30da42e937db621564Steve Block patch_description, 604d0825bca7fe65beaee391d30da42e937db621564Steve Block patch_file_object, 605d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_review=mark_for_review, 606f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch mark_for_commit_queue=mark_for_commit_queue, 607f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch is_patch=True) 608d0825bca7fe65beaee391d30da42e937db621564Steve Block 609d0825bca7fe65beaee391d30da42e937db621564Steve Block response = self.browser.submit() 610d0825bca7fe65beaee391d30da42e937db621564Steve Block 611d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_id = self._check_create_bug_response(response.read()) 612d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Bug %s created." % bug_id) 613d0825bca7fe65beaee391d30da42e937db621564Steve Block log("%sshow_bug.cgi?id=%s" % (self.bug_server_url, bug_id)) 614d0825bca7fe65beaee391d30da42e937db621564Steve Block return bug_id 615d0825bca7fe65beaee391d30da42e937db621564Steve Block 616d0825bca7fe65beaee391d30da42e937db621564Steve Block def _find_select_element_for_flag(self, flag_name): 617d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: This will break if we ever re-order attachment flags 618d0825bca7fe65beaee391d30da42e937db621564Steve Block if flag_name == "review": 619d0825bca7fe65beaee391d30da42e937db621564Steve Block return self.browser.find_control(type='select', nr=0) 620545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch elif flag_name == "commit-queue": 621d0825bca7fe65beaee391d30da42e937db621564Steve Block return self.browser.find_control(type='select', nr=1) 622d0825bca7fe65beaee391d30da42e937db621564Steve Block raise Exception("Don't know how to find flag named \"%s\"" % flag_name) 623d0825bca7fe65beaee391d30da42e937db621564Steve Block 624d0825bca7fe65beaee391d30da42e937db621564Steve Block def clear_attachment_flags(self, 625d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_id, 626d0825bca7fe65beaee391d30da42e937db621564Steve Block additional_comment_text=None): 627d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 628d0825bca7fe65beaee391d30da42e937db621564Steve Block 629d0825bca7fe65beaee391d30da42e937db621564Steve Block comment_text = "Clearing flags on attachment: %s" % attachment_id 630d0825bca7fe65beaee391d30da42e937db621564Steve Block if additional_comment_text: 631d0825bca7fe65beaee391d30da42e937db621564Steve Block comment_text += "\n\n%s" % additional_comment_text 632d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 633d0825bca7fe65beaee391d30da42e937db621564Steve Block 634d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 635d0825bca7fe65beaee391d30da42e937db621564Steve Block return 636d0825bca7fe65beaee391d30da42e937db621564Steve Block 637d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.attachment_url_for_id(attachment_id, 'edit')) 638d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(nr=1) 639d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.set_value(comment_text, name='comment', nr=0) 640d0825bca7fe65beaee391d30da42e937db621564Steve Block self._find_select_element_for_flag('review').value = ("X",) 641d0825bca7fe65beaee391d30da42e937db621564Steve Block self._find_select_element_for_flag('commit-queue').value = ("X",) 642d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 643d0825bca7fe65beaee391d30da42e937db621564Steve Block 644d0825bca7fe65beaee391d30da42e937db621564Steve Block def set_flag_on_attachment(self, 645d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_id, 646d0825bca7fe65beaee391d30da42e937db621564Steve Block flag_name, 647d0825bca7fe65beaee391d30da42e937db621564Steve Block flag_value, 648545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch comment_text=None, 649545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch additional_comment_text=None): 650d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: We need a way to test this function on a live bugzilla 651d0825bca7fe65beaee391d30da42e937db621564Steve Block # instance. 652d0825bca7fe65beaee391d30da42e937db621564Steve Block 653d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 654d0825bca7fe65beaee391d30da42e937db621564Steve Block 655d0825bca7fe65beaee391d30da42e937db621564Steve Block if additional_comment_text: 656d0825bca7fe65beaee391d30da42e937db621564Steve Block comment_text += "\n\n%s" % additional_comment_text 657d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 658d0825bca7fe65beaee391d30da42e937db621564Steve Block 659d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 660d0825bca7fe65beaee391d30da42e937db621564Steve Block return 661d0825bca7fe65beaee391d30da42e937db621564Steve Block 662d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.attachment_url_for_id(attachment_id, 'edit')) 663d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(nr=1) 664545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch 665545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch if comment_text: 666545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch self.browser.set_value(comment_text, name='comment', nr=0) 667545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch 668d0825bca7fe65beaee391d30da42e937db621564Steve Block self._find_select_element_for_flag(flag_name).value = (flag_value,) 669d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 670d0825bca7fe65beaee391d30da42e937db621564Steve Block 671d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: All of these bug editing methods have a ridiculous amount of 672d0825bca7fe65beaee391d30da42e937db621564Steve Block # copy/paste code. 673d0825bca7fe65beaee391d30da42e937db621564Steve Block 674d0825bca7fe65beaee391d30da42e937db621564Steve Block def obsolete_attachment(self, attachment_id, comment_text=None): 675d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 676d0825bca7fe65beaee391d30da42e937db621564Steve Block 677d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Obsoleting attachment: %s" % attachment_id) 678d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 679d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 680d0825bca7fe65beaee391d30da42e937db621564Steve Block return 681d0825bca7fe65beaee391d30da42e937db621564Steve Block 682d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.attachment_url_for_id(attachment_id, 'edit')) 683d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(nr=1) 684d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.find_control('isobsolete').items[0].selected = True 685d0825bca7fe65beaee391d30da42e937db621564Steve Block # Also clear any review flag (to remove it from review/commit queues) 686d0825bca7fe65beaee391d30da42e937db621564Steve Block self._find_select_element_for_flag('review').value = ("X",) 687d0825bca7fe65beaee391d30da42e937db621564Steve Block self._find_select_element_for_flag('commit-queue').value = ("X",) 688d0825bca7fe65beaee391d30da42e937db621564Steve Block if comment_text: 689d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 690d0825bca7fe65beaee391d30da42e937db621564Steve Block # Bugzilla has two textareas named 'comment', one is somehow 691d0825bca7fe65beaee391d30da42e937db621564Steve Block # hidden. We want the first. 692d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.set_value(comment_text, name='comment', nr=0) 693d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 694d0825bca7fe65beaee391d30da42e937db621564Steve Block 695d0825bca7fe65beaee391d30da42e937db621564Steve Block def add_cc_to_bug(self, bug_id, email_address_list): 696d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 697d0825bca7fe65beaee391d30da42e937db621564Steve Block 698d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Adding %s to the CC list for bug %s" % (email_address_list, 699d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_id)) 700d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 701d0825bca7fe65beaee391d30da42e937db621564Steve Block return 702d0825bca7fe65beaee391d30da42e937db621564Steve Block 703d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_url_for_bug_id(bug_id)) 704d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="changeform") 705d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser["newcc"] = ", ".join(email_address_list) 706d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 707d0825bca7fe65beaee391d30da42e937db621564Steve Block 708d0825bca7fe65beaee391d30da42e937db621564Steve Block def post_comment_to_bug(self, bug_id, comment_text, cc=None): 709d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 710d0825bca7fe65beaee391d30da42e937db621564Steve Block 711d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Adding comment to bug %s" % bug_id) 712d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 713d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 714d0825bca7fe65beaee391d30da42e937db621564Steve Block return 715d0825bca7fe65beaee391d30da42e937db621564Steve Block 716d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_url_for_bug_id(bug_id)) 717d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="changeform") 718d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser["comment"] = comment_text 719d0825bca7fe65beaee391d30da42e937db621564Steve Block if cc: 720d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser["newcc"] = ", ".join(cc) 721d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 722d0825bca7fe65beaee391d30da42e937db621564Steve Block 723d0825bca7fe65beaee391d30da42e937db621564Steve Block def close_bug_as_fixed(self, bug_id, comment_text=None): 724d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 725d0825bca7fe65beaee391d30da42e937db621564Steve Block 726d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Closing bug %s as fixed" % bug_id) 727d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 728d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 729d0825bca7fe65beaee391d30da42e937db621564Steve Block return 730d0825bca7fe65beaee391d30da42e937db621564Steve Block 731d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_url_for_bug_id(bug_id)) 732d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="changeform") 733d0825bca7fe65beaee391d30da42e937db621564Steve Block if comment_text: 734d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['comment'] = comment_text 735d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['bug_status'] = ['RESOLVED'] 736d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['resolution'] = ['FIXED'] 737d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 738d0825bca7fe65beaee391d30da42e937db621564Steve Block 739d0825bca7fe65beaee391d30da42e937db621564Steve Block def reassign_bug(self, bug_id, assignee, comment_text=None): 740d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 741d0825bca7fe65beaee391d30da42e937db621564Steve Block 742d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Assigning bug %s to %s" % (bug_id, assignee)) 743d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 744d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 745d0825bca7fe65beaee391d30da42e937db621564Steve Block return 746d0825bca7fe65beaee391d30da42e937db621564Steve Block 747d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_url_for_bug_id(bug_id)) 748d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="changeform") 749d0825bca7fe65beaee391d30da42e937db621564Steve Block if comment_text: 750d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 751d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser["comment"] = comment_text 752d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser["assigned_to"] = assignee 753d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 754d0825bca7fe65beaee391d30da42e937db621564Steve Block 755d0825bca7fe65beaee391d30da42e937db621564Steve Block def reopen_bug(self, bug_id, comment_text): 756d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 757d0825bca7fe65beaee391d30da42e937db621564Steve Block 758d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Re-opening bug %s" % bug_id) 759d0825bca7fe65beaee391d30da42e937db621564Steve Block # Bugzilla requires a comment when re-opening a bug, so we know it will 760d0825bca7fe65beaee391d30da42e937db621564Steve Block # never be None. 761d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 762d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 763d0825bca7fe65beaee391d30da42e937db621564Steve Block return 764d0825bca7fe65beaee391d30da42e937db621564Steve Block 765d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_url_for_bug_id(bug_id)) 766d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="changeform") 767d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_status = self.browser.find_control("bug_status", type="select") 768d0825bca7fe65beaee391d30da42e937db621564Steve Block # This is a hack around the fact that ClientForm.ListControl seems to 769d0825bca7fe65beaee391d30da42e937db621564Steve Block # have no simpler way to ask if a control has an item named "REOPENED" 770d0825bca7fe65beaee391d30da42e937db621564Steve Block # without using exceptions for control flow. 771d0825bca7fe65beaee391d30da42e937db621564Steve Block possible_bug_statuses = map(lambda item: item.name, bug_status.items) 772d0825bca7fe65beaee391d30da42e937db621564Steve Block if "REOPENED" in possible_bug_statuses: 773d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_status.value = ["REOPENED"] 7746c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen # If the bug was never confirmed it will not have a "REOPENED" 7756c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen # state, but only an "UNCONFIRMED" state. 7766c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen elif "UNCONFIRMED" in possible_bug_statuses: 7776c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen bug_status.value = ["UNCONFIRMED"] 778d0825bca7fe65beaee391d30da42e937db621564Steve Block else: 7796c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen # FIXME: This logic is slightly backwards. We won't print this 7806c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen # message if the bug is already open with state "UNCONFIRMED". 7816c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen log("Did not reopen bug %s, it appears to already be open with status %s." % (bug_id, bug_status.value)) 782d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['comment'] = comment_text 783d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 784