bugzilla.py revision f05b935882198ccf7d81675736e3aeb089c5113a
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 33dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockimport os.path 34d0825bca7fe65beaee391d30da42e937db621564Steve Blockimport re 3521939df44de1705786c545cd1bf519d47250322dBen Murdochimport StringIO 36f05b935882198ccf7d81675736e3aeb089c5113aBen Murdochimport urllib 37d0825bca7fe65beaee391d30da42e937db621564Steve Block 38d0825bca7fe65beaee391d30da42e937db621564Steve Blockfrom datetime import datetime # used in timestamp() 39d0825bca7fe65beaee391d30da42e937db621564Steve Block 406b70adc33054f8aee8c54d0f460458a9df11b8a5Russell Brennerfrom .attachment import Attachment 416b70adc33054f8aee8c54d0f460458a9df11b8a5Russell Brennerfrom .bug import Bug 426b70adc33054f8aee8c54d0f460458a9df11b8a5Russell Brenner 436b70adc33054f8aee8c54d0f460458a9df11b8a5Russell Brennerfrom webkitpy.common.system.deprecated_logging import log 44dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockfrom webkitpy.common.config import committers 45dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockfrom webkitpy.common.net.credentials import Credentials 46dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockfrom webkitpy.common.system.user import User 47dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockfrom webkitpy.thirdparty.autoinstalled.mechanize import Browser 48dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Blockfrom webkitpy.thirdparty.BeautifulSoup import BeautifulSoup, SoupStrainer 49d0825bca7fe65beaee391d30da42e937db621564Steve Block 50d0825bca7fe65beaee391d30da42e937db621564Steve Block 516b70adc33054f8aee8c54d0f460458a9df11b8a5Russell Brenner# FIXME: parse_bug_id should not be a free function. 52d0825bca7fe65beaee391d30da42e937db621564Steve Blockdef parse_bug_id(message): 53dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if not message: 54dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return None 55d0825bca7fe65beaee391d30da42e937db621564Steve Block match = re.search("http\://webkit\.org/b/(?P<bug_id>\d+)", message) 56d0825bca7fe65beaee391d30da42e937db621564Steve Block if match: 57d0825bca7fe65beaee391d30da42e937db621564Steve Block return int(match.group('bug_id')) 58d0825bca7fe65beaee391d30da42e937db621564Steve Block match = re.search( 59d0825bca7fe65beaee391d30da42e937db621564Steve Block Bugzilla.bug_server_regex + "show_bug\.cgi\?id=(?P<bug_id>\d+)", 60d0825bca7fe65beaee391d30da42e937db621564Steve Block message) 61d0825bca7fe65beaee391d30da42e937db621564Steve Block if match: 62d0825bca7fe65beaee391d30da42e937db621564Steve Block return int(match.group('bug_id')) 63d0825bca7fe65beaee391d30da42e937db621564Steve Block return None 64d0825bca7fe65beaee391d30da42e937db621564Steve Block 65d0825bca7fe65beaee391d30da42e937db621564Steve Block 66d0825bca7fe65beaee391d30da42e937db621564Steve Blockdef timestamp(): 67d0825bca7fe65beaee391d30da42e937db621564Steve Block return datetime.now().strftime("%Y%m%d%H%M%S") 68d0825bca7fe65beaee391d30da42e937db621564Steve Block 69d0825bca7fe65beaee391d30da42e937db621564Steve Block 70d0825bca7fe65beaee391d30da42e937db621564Steve Block# A container for all of the logic for making and parsing buzilla queries. 71d0825bca7fe65beaee391d30da42e937db621564Steve Blockclass BugzillaQueries(object): 72d0825bca7fe65beaee391d30da42e937db621564Steve Block 73d0825bca7fe65beaee391d30da42e937db621564Steve Block def __init__(self, bugzilla): 74d0825bca7fe65beaee391d30da42e937db621564Steve Block self._bugzilla = bugzilla 75d0825bca7fe65beaee391d30da42e937db621564Steve Block 76f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _is_xml_bugs_form(self, form): 77f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # ClientForm.HTMLForm.find_control throws if the control is not found, 78f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # so we do a manual search instead: 79f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return "xml" in [control.id for control in form.controls] 80f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 81f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # This is kinda a hack. There is probably a better way to get this information from bugzilla. 82f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _parse_result_count(self, results_page): 83f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch result_count_text = BeautifulSoup(results_page).find(attrs={'class': 'bz_result_count'}).string 84f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch result_count_parts = result_count_text.strip().split(" ") 85f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if result_count_parts[0] == "Zarro": 86f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return 0 87f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if result_count_parts[0] == "One": 88f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return 1 89f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return int(result_count_parts[0]) 90f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 91f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # Note: _load_query, _fetch_bug and _fetch_bugs_from_advanced_query 92f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # are the only methods which access self._bugzilla. 93d0825bca7fe65beaee391d30da42e937db621564Steve Block 94d0825bca7fe65beaee391d30da42e937db621564Steve Block def _load_query(self, query): 95d0825bca7fe65beaee391d30da42e937db621564Steve Block self._bugzilla.authenticate() 96d0825bca7fe65beaee391d30da42e937db621564Steve Block full_url = "%s%s" % (self._bugzilla.bug_server_url, query) 97d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._bugzilla.browser.open(full_url) 98d0825bca7fe65beaee391d30da42e937db621564Steve Block 99f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _fetch_bugs_from_advanced_query(self, query): 100f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch results_page = self._load_query(query) 101f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if not self._parse_result_count(results_page): 102f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return [] 103f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # Bugzilla results pages have an "XML" submit button at the bottom 104f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # which can be used to get an XML page containing all of the <bug> elements. 105f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # This is slighty lame that this assumes that _load_query used 106f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # self._bugzilla.browser and that it's in an acceptable state. 107f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self._bugzilla.browser.select_form(predicate=self._is_xml_bugs_form) 108f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch bugs_xml = self._bugzilla.browser.submit() 109f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return self._bugzilla._parse_bugs_from_xml(bugs_xml) 110f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 111d0825bca7fe65beaee391d30da42e937db621564Steve Block def _fetch_bug(self, bug_id): 112d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._bugzilla.fetch_bug(bug_id) 113d0825bca7fe65beaee391d30da42e937db621564Steve Block 114d0825bca7fe65beaee391d30da42e937db621564Steve Block def _fetch_bug_ids_advanced_query(self, query): 115d0825bca7fe65beaee391d30da42e937db621564Steve Block soup = BeautifulSoup(self._load_query(query)) 116d0825bca7fe65beaee391d30da42e937db621564Steve Block # The contents of the <a> inside the cells in the first column happen 117d0825bca7fe65beaee391d30da42e937db621564Steve Block # to be the bug id. 118d0825bca7fe65beaee391d30da42e937db621564Steve Block return [int(bug_link_cell.find("a").string) 119d0825bca7fe65beaee391d30da42e937db621564Steve Block for bug_link_cell in soup('td', "first-child")] 120d0825bca7fe65beaee391d30da42e937db621564Steve Block 121d0825bca7fe65beaee391d30da42e937db621564Steve Block def _parse_attachment_ids_request_query(self, page): 122d0825bca7fe65beaee391d30da42e937db621564Steve Block digits = re.compile("\d+") 123d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_href = re.compile("attachment.cgi\?id=\d+&action=review") 124d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_links = SoupStrainer("a", href=attachment_href) 125d0825bca7fe65beaee391d30da42e937db621564Steve Block return [int(digits.search(tag["href"]).group(0)) 126d0825bca7fe65beaee391d30da42e937db621564Steve Block for tag in BeautifulSoup(page, parseOnlyThese=attachment_links)] 127d0825bca7fe65beaee391d30da42e937db621564Steve Block 128d0825bca7fe65beaee391d30da42e937db621564Steve Block def _fetch_attachment_ids_request_query(self, query): 129d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._parse_attachment_ids_request_query(self._load_query(query)) 130d0825bca7fe65beaee391d30da42e937db621564Steve Block 131dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block def _parse_quips(self, page): 132dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block soup = BeautifulSoup(page, convertEntities=BeautifulSoup.HTML_ENTITIES) 133dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block quips = soup.find(text=re.compile(r"Existing quips:")).findNext("ul").findAll("li") 134dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return [unicode(quip_entry.string) for quip_entry in quips] 135dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block 136dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block def fetch_quips(self): 137dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return self._parse_quips(self._load_query("/quips.cgi?action=show")) 138dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block 139d0825bca7fe65beaee391d30da42e937db621564Steve Block # List of all r+'d bugs. 140d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_bug_ids_from_pending_commit_list(self): 141d0825bca7fe65beaee391d30da42e937db621564Steve 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" 142d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._fetch_bug_ids_advanced_query(needs_commit_query_url) 143d0825bca7fe65beaee391d30da42e937db621564Steve Block 144f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def fetch_bugs_matching_quicksearch(self, search_string): 145f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # We may want to use a more explicit query than "quicksearch". 146f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # If quicksearch changes we should probably change to use 147f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # a normal buglist.cgi?query_format=advanced query. 148f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch quicksearch_url = "buglist.cgi?quicksearch=%s" % urllib.quote(search_string) 149f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return self._fetch_bugs_from_advanced_query(quicksearch_url) 150f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 151f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # Currently this returns all bugs across all components. 152f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # In the future we may wish to extend this API to construct more restricted searches. 153f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def fetch_bugs_matching_search(self, search_string, author_email=None): 154f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch query = "buglist.cgi?query_format=advanced" 155f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if search_string: 156f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch query += "&short_desc_type=allwordssubstr&short_desc=%s" % urllib.quote(search_string) 157f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if author_email: 158f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch query += "&emailreporter1=1&emailtype1=substring&email1=%s" % urllib.quote(search_string) 159f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return self._fetch_bugs_from_advanced_query(query) 160f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 161d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_patches_from_pending_commit_list(self): 162d0825bca7fe65beaee391d30da42e937db621564Steve Block return sum([self._fetch_bug(bug_id).reviewed_patches() 163d0825bca7fe65beaee391d30da42e937db621564Steve Block for bug_id in self.fetch_bug_ids_from_pending_commit_list()], []) 164d0825bca7fe65beaee391d30da42e937db621564Steve Block 165d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_bug_ids_from_commit_queue(self): 166d0825bca7fe65beaee391d30da42e937db621564Steve 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" 167d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._fetch_bug_ids_advanced_query(commit_queue_url) 168d0825bca7fe65beaee391d30da42e937db621564Steve Block 169d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_patches_from_commit_queue(self): 170d0825bca7fe65beaee391d30da42e937db621564Steve Block # This function will only return patches which have valid committers 171d0825bca7fe65beaee391d30da42e937db621564Steve Block # set. It won't reject patches with invalid committers/reviewers. 172d0825bca7fe65beaee391d30da42e937db621564Steve Block return sum([self._fetch_bug(bug_id).commit_queued_patches() 173d0825bca7fe65beaee391d30da42e937db621564Steve Block for bug_id in self.fetch_bug_ids_from_commit_queue()], []) 174d0825bca7fe65beaee391d30da42e937db621564Steve Block 17528040489d744e0c5d475a88663056c9040ed5320Teng-Hui Zhu def fetch_bug_ids_from_review_queue(self): 176d0825bca7fe65beaee391d30da42e937db621564Steve 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?" 177d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._fetch_bug_ids_advanced_query(review_queue_url) 178d0825bca7fe65beaee391d30da42e937db621564Steve Block 179a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch # This method will make several requests to bugzilla. 180d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_patches_from_review_queue(self, limit=None): 181d0825bca7fe65beaee391d30da42e937db621564Steve Block # [:None] returns the whole array. 182d0825bca7fe65beaee391d30da42e937db621564Steve Block return sum([self._fetch_bug(bug_id).unreviewed_patches() 18328040489d744e0c5d475a88663056c9040ed5320Teng-Hui Zhu for bug_id in self.fetch_bug_ids_from_review_queue()[:limit]], []) 184d0825bca7fe65beaee391d30da42e937db621564Steve Block 185a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch # NOTE: This is the only client of _fetch_attachment_ids_request_query 186a94275402997c11dd2e778633dacf4b7e630a35dBen Murdoch # This method only makes one request to bugzilla. 187d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_attachment_ids_from_review_queue(self): 188d0825bca7fe65beaee391d30da42e937db621564Steve Block review_queue_url = "request.cgi?action=queue&type=review&group=type" 189d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._fetch_attachment_ids_request_query(review_queue_url) 190d0825bca7fe65beaee391d30da42e937db621564Steve Block 191d0825bca7fe65beaee391d30da42e937db621564Steve Block 192d0825bca7fe65beaee391d30da42e937db621564Steve Blockclass Bugzilla(object): 193d0825bca7fe65beaee391d30da42e937db621564Steve Block 194dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block def __init__(self, dryrun=False, committers=committers.CommitterList()): 195d0825bca7fe65beaee391d30da42e937db621564Steve Block self.dryrun = dryrun 196d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticated = False 197d0825bca7fe65beaee391d30da42e937db621564Steve Block self.queries = BugzillaQueries(self) 198d0825bca7fe65beaee391d30da42e937db621564Steve Block self.committers = committers 199dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.cached_quips = [] 200d0825bca7fe65beaee391d30da42e937db621564Steve Block 201d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: We should use some sort of Browser mock object when in dryrun 202d0825bca7fe65beaee391d30da42e937db621564Steve Block # mode (to prevent any mistakes). 203d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser = Browser() 204d0825bca7fe65beaee391d30da42e937db621564Steve Block # Ignore bugs.webkit.org/robots.txt until we fix it to allow this 205d0825bca7fe65beaee391d30da42e937db621564Steve Block # script. 206d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.set_handle_robots(False) 207d0825bca7fe65beaee391d30da42e937db621564Steve Block 208d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: Much of this should go into some sort of config module: 209d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_server_host = "bugs.webkit.org" 210d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_server_regex = "https?://%s/" % re.sub('\.', '\\.', bug_server_host) 211d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_server_url = "https://%s/" % bug_server_host 212dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block 213dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block def quips(self): 214dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block # We only fetch and parse the list of quips once per instantiation 215dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block # so that we do not burden bugs.webkit.org. 216dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if not self.cached_quips and not self.dryrun: 217dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.cached_quips = self.queries.fetch_quips() 218dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return self.cached_quips 219d0825bca7fe65beaee391d30da42e937db621564Steve Block 220d0825bca7fe65beaee391d30da42e937db621564Steve Block def bug_url_for_bug_id(self, bug_id, xml=False): 221dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if not bug_id: 222dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return None 223d0825bca7fe65beaee391d30da42e937db621564Steve Block content_type = "&ctype=xml" if xml else "" 224f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return "%sshow_bug.cgi?id=%s%s" % (self.bug_server_url, bug_id, content_type) 225d0825bca7fe65beaee391d30da42e937db621564Steve Block 226d0825bca7fe65beaee391d30da42e937db621564Steve Block def short_bug_url_for_bug_id(self, bug_id): 227dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if not bug_id: 228dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return None 229d0825bca7fe65beaee391d30da42e937db621564Steve Block return "http://webkit.org/b/%s" % bug_id 230d0825bca7fe65beaee391d30da42e937db621564Steve Block 231f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def add_attachment_url(self, bug_id): 232f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return "%sattachment.cgi?action=enter&bugid=%s" % (self.bug_server_url, bug_id) 233f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 234d0825bca7fe65beaee391d30da42e937db621564Steve Block def attachment_url_for_id(self, attachment_id, action="view"): 235dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if not attachment_id: 236dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block return None 237d0825bca7fe65beaee391d30da42e937db621564Steve Block action_param = "" 238d0825bca7fe65beaee391d30da42e937db621564Steve Block if action and action != "view": 239d0825bca7fe65beaee391d30da42e937db621564Steve Block action_param = "&action=%s" % action 240d0825bca7fe65beaee391d30da42e937db621564Steve Block return "%sattachment.cgi?id=%s%s" % (self.bug_server_url, 241d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_id, 242d0825bca7fe65beaee391d30da42e937db621564Steve Block action_param) 243d0825bca7fe65beaee391d30da42e937db621564Steve Block 244d0825bca7fe65beaee391d30da42e937db621564Steve Block def _parse_attachment_flag(self, 245d0825bca7fe65beaee391d30da42e937db621564Steve Block element, 246d0825bca7fe65beaee391d30da42e937db621564Steve Block flag_name, 247d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment, 248d0825bca7fe65beaee391d30da42e937db621564Steve Block result_key): 249d0825bca7fe65beaee391d30da42e937db621564Steve Block flag = element.find('flag', attrs={'name': flag_name}) 250d0825bca7fe65beaee391d30da42e937db621564Steve Block if flag: 251d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment[flag_name] = flag['status'] 252d0825bca7fe65beaee391d30da42e937db621564Steve Block if flag['status'] == '+': 253d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment[result_key] = flag['setter'] 254e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # Sadly show_bug.cgi?ctype=xml does not expose the flag modification date. 255d0825bca7fe65beaee391d30da42e937db621564Steve Block 25621939df44de1705786c545cd1bf519d47250322dBen Murdoch def _string_contents(self, soup): 25721939df44de1705786c545cd1bf519d47250322dBen Murdoch # WebKit's bugzilla instance uses UTF-8. 25821939df44de1705786c545cd1bf519d47250322dBen Murdoch # BeautifulSoup always returns Unicode strings, however 25921939df44de1705786c545cd1bf519d47250322dBen Murdoch # the .string method returns a (unicode) NavigableString. 26021939df44de1705786c545cd1bf519d47250322dBen Murdoch # NavigableString can confuse other parts of the code, so we 26121939df44de1705786c545cd1bf519d47250322dBen Murdoch # convert from NavigableString to a real unicode() object using unicode(). 26221939df44de1705786c545cd1bf519d47250322dBen Murdoch return unicode(soup.string) 26321939df44de1705786c545cd1bf519d47250322dBen Murdoch 264e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # Example: 2010-01-20 14:31 PST 265e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # FIXME: Some bugzilla dates seem to have seconds in them? 266e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # Python does not support timezones out of the box. 267e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # Assume that bugzilla always uses PST (which is true for bugs.webkit.org) 268e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block _bugzilla_date_format = "%Y-%m-%d %H:%M" 269e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block 270e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block @classmethod 271e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block def _parse_date(cls, date_string): 272e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block (date, time, time_zone) = date_string.split(" ") 273e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block # Ignore the timezone because python doesn't understand timezones out of the box. 274e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block date_string = "%s %s" % (date, time) 275e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block return datetime.strptime(date_string, cls._bugzilla_date_format) 27621939df44de1705786c545cd1bf519d47250322dBen Murdoch 277e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block def _date_contents(self, soup): 278e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block return self._parse_date(self._string_contents(soup)) 279e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block 280e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block def _parse_attachment_element(self, element, bug_id): 281d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment = {} 282d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment['bug_id'] = bug_id 283d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment['is_obsolete'] = (element.has_key('isobsolete') and element['isobsolete'] == "1") 284d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment['is_patch'] = (element.has_key('ispatch') and element['ispatch'] == "1") 285d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment['id'] = int(element.find('attachid').string) 286d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: No need to parse out the url here. 287d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment['url'] = self.attachment_url_for_id(attachment['id']) 288e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block attachment["attach_date"] = self._date_contents(element.find("date")) 28921939df44de1705786c545cd1bf519d47250322dBen Murdoch attachment['name'] = self._string_contents(element.find('desc')) 29021939df44de1705786c545cd1bf519d47250322dBen Murdoch attachment['attacher_email'] = self._string_contents(element.find('attacher')) 29121939df44de1705786c545cd1bf519d47250322dBen Murdoch attachment['type'] = self._string_contents(element.find('type')) 292d0825bca7fe65beaee391d30da42e937db621564Steve Block self._parse_attachment_flag( 293d0825bca7fe65beaee391d30da42e937db621564Steve Block element, 'review', attachment, 'reviewer_email') 294d0825bca7fe65beaee391d30da42e937db621564Steve Block self._parse_attachment_flag( 295d0825bca7fe65beaee391d30da42e937db621564Steve Block element, 'commit-queue', attachment, 'committer_email') 296d0825bca7fe65beaee391d30da42e937db621564Steve Block return attachment 297d0825bca7fe65beaee391d30da42e937db621564Steve Block 298f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _parse_bugs_from_xml(self, page): 299f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch soup = BeautifulSoup(page) 300f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # Without the unicode() call, BeautifulSoup occasionally complains of being 301f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # passed None for no apparent reason. 302f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return [Bug(self._parse_bug_dictionary_from_xml(unicode(bug_xml)), self) for bug_xml in soup('bug')] 303f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 304f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _parse_bug_dictionary_from_xml(self, page): 305d0825bca7fe65beaee391d30da42e937db621564Steve Block soup = BeautifulSoup(page) 306d0825bca7fe65beaee391d30da42e937db621564Steve Block bug = {} 307d0825bca7fe65beaee391d30da42e937db621564Steve Block bug["id"] = int(soup.find("bug_id").string) 30821939df44de1705786c545cd1bf519d47250322dBen Murdoch bug["title"] = self._string_contents(soup.find("short_desc")) 30968513a70bcd92384395513322f1b801e7bf9c729Steve Block bug["bug_status"] = self._string_contents(soup.find("bug_status")) 310f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch dup_id = soup.find("dup_id") 311f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if dup_id: 312f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch bug["dup_id"] = self._string_contents(dup_id) 31321939df44de1705786c545cd1bf519d47250322dBen Murdoch bug["reporter_email"] = self._string_contents(soup.find("reporter")) 31421939df44de1705786c545cd1bf519d47250322dBen Murdoch bug["assigned_to_email"] = self._string_contents(soup.find("assigned_to")) 315f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch bug["cc_emails"] = [self._string_contents(element) for element in soup.findAll('cc')] 316d0825bca7fe65beaee391d30da42e937db621564Steve Block bug["attachments"] = [self._parse_attachment_element(element, bug["id"]) for element in soup.findAll('attachment')] 317d0825bca7fe65beaee391d30da42e937db621564Steve Block return bug 318d0825bca7fe65beaee391d30da42e937db621564Steve Block 319d0825bca7fe65beaee391d30da42e937db621564Steve Block # Makes testing fetch_*_from_bug() possible until we have a better 320d0825bca7fe65beaee391d30da42e937db621564Steve Block # BugzillaNetwork abstration. 321d0825bca7fe65beaee391d30da42e937db621564Steve Block 322d0825bca7fe65beaee391d30da42e937db621564Steve Block def _fetch_bug_page(self, bug_id): 323d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_url = self.bug_url_for_bug_id(bug_id, xml=True) 324d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Fetching: %s" % bug_url) 325d0825bca7fe65beaee391d30da42e937db621564Steve Block return self.browser.open(bug_url) 326d0825bca7fe65beaee391d30da42e937db621564Steve Block 327d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_bug_dictionary(self, bug_id): 328dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block try: 329f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return self._parse_bug_dictionary_from_xml(self._fetch_bug_page(bug_id)) 3305ddde30071f639962dd557c453f2ad01f8f0fd00Kristian Monsen except KeyboardInterrupt: 3315ddde30071f639962dd557c453f2ad01f8f0fd00Kristian Monsen raise 332dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block except: 333dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.authenticate() 334f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return self._parse_bug_dictionary_from_xml(self._fetch_bug_page(bug_id)) 335d0825bca7fe65beaee391d30da42e937db621564Steve Block 336d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: A BugzillaCache object should provide all these fetch_ methods. 337d0825bca7fe65beaee391d30da42e937db621564Steve Block 338d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_bug(self, bug_id): 339d0825bca7fe65beaee391d30da42e937db621564Steve Block return Bug(self.fetch_bug_dictionary(bug_id), self) 340d0825bca7fe65beaee391d30da42e937db621564Steve Block 34121939df44de1705786c545cd1bf519d47250322dBen Murdoch def fetch_attachment_contents(self, attachment_id): 34221939df44de1705786c545cd1bf519d47250322dBen Murdoch attachment_url = self.attachment_url_for_id(attachment_id) 34321939df44de1705786c545cd1bf519d47250322dBen Murdoch # We need to authenticate to download patches from security bugs. 34421939df44de1705786c545cd1bf519d47250322dBen Murdoch self.authenticate() 34521939df44de1705786c545cd1bf519d47250322dBen Murdoch return self.browser.open(attachment_url).read() 34621939df44de1705786c545cd1bf519d47250322dBen Murdoch 347d0825bca7fe65beaee391d30da42e937db621564Steve Block def _parse_bug_id_from_attachment_page(self, page): 348d0825bca7fe65beaee391d30da42e937db621564Steve Block # The "Up" relation happens to point to the bug. 349d0825bca7fe65beaee391d30da42e937db621564Steve Block up_link = BeautifulSoup(page).find('link', rel='Up') 350d0825bca7fe65beaee391d30da42e937db621564Steve Block if not up_link: 351d0825bca7fe65beaee391d30da42e937db621564Steve Block # This attachment does not exist (or you don't have permissions to 352d0825bca7fe65beaee391d30da42e937db621564Steve Block # view it). 353d0825bca7fe65beaee391d30da42e937db621564Steve Block return None 354d0825bca7fe65beaee391d30da42e937db621564Steve Block match = re.search("show_bug.cgi\?id=(?P<bug_id>\d+)", up_link['href']) 355d0825bca7fe65beaee391d30da42e937db621564Steve Block return int(match.group('bug_id')) 356d0825bca7fe65beaee391d30da42e937db621564Steve Block 357d0825bca7fe65beaee391d30da42e937db621564Steve Block def bug_id_for_attachment_id(self, attachment_id): 358d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 359d0825bca7fe65beaee391d30da42e937db621564Steve Block 360d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_url = self.attachment_url_for_id(attachment_id, 'edit') 361d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Fetching: %s" % attachment_url) 362d0825bca7fe65beaee391d30da42e937db621564Steve Block page = self.browser.open(attachment_url) 363d0825bca7fe65beaee391d30da42e937db621564Steve Block return self._parse_bug_id_from_attachment_page(page) 364d0825bca7fe65beaee391d30da42e937db621564Steve Block 365d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: This should just return Attachment(id), which should be able to 366d0825bca7fe65beaee391d30da42e937db621564Steve Block # lazily fetch needed data. 367d0825bca7fe65beaee391d30da42e937db621564Steve Block 368d0825bca7fe65beaee391d30da42e937db621564Steve Block def fetch_attachment(self, attachment_id): 369d0825bca7fe65beaee391d30da42e937db621564Steve Block # We could grab all the attachment details off of the attachment edit 370d0825bca7fe65beaee391d30da42e937db621564Steve Block # page but we already have working code to do so off of the bugs page, 371d0825bca7fe65beaee391d30da42e937db621564Steve Block # so re-use that. 372d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_id = self.bug_id_for_attachment_id(attachment_id) 373d0825bca7fe65beaee391d30da42e937db621564Steve Block if not bug_id: 374d0825bca7fe65beaee391d30da42e937db621564Steve Block return None 375d0825bca7fe65beaee391d30da42e937db621564Steve Block attachments = self.fetch_bug(bug_id).attachments(include_obsolete=True) 376d0825bca7fe65beaee391d30da42e937db621564Steve Block for attachment in attachments: 377d0825bca7fe65beaee391d30da42e937db621564Steve Block if attachment.id() == int(attachment_id): 378d0825bca7fe65beaee391d30da42e937db621564Steve Block return attachment 379d0825bca7fe65beaee391d30da42e937db621564Steve Block return None # This should never be hit. 380d0825bca7fe65beaee391d30da42e937db621564Steve Block 381d0825bca7fe65beaee391d30da42e937db621564Steve Block def authenticate(self): 382d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.authenticated: 383d0825bca7fe65beaee391d30da42e937db621564Steve Block return 384d0825bca7fe65beaee391d30da42e937db621564Steve Block 385d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 386d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Skipping log in for dry run...") 387d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticated = True 388d0825bca7fe65beaee391d30da42e937db621564Steve Block return 389d0825bca7fe65beaee391d30da42e937db621564Steve Block 390e14391e94c850b8bd03680c23b38978db68687a8John Reck credentials = Credentials(self.bug_server_host, git_prefix="bugzilla") 391e14391e94c850b8bd03680c23b38978db68687a8John Reck 392d0825bca7fe65beaee391d30da42e937db621564Steve Block attempts = 0 393d0825bca7fe65beaee391d30da42e937db621564Steve Block while not self.authenticated: 394d0825bca7fe65beaee391d30da42e937db621564Steve Block attempts += 1 395e14391e94c850b8bd03680c23b38978db68687a8John Reck username, password = credentials.read_credentials() 396d0825bca7fe65beaee391d30da42e937db621564Steve Block 397d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Logging in as %s..." % username) 398d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_server_url + 399d0825bca7fe65beaee391d30da42e937db621564Steve Block "index.cgi?GoAheadAndLogIn=1") 400d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="login") 401d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['Bugzilla_login'] = username 402d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['Bugzilla_password'] = password 403d0825bca7fe65beaee391d30da42e937db621564Steve Block response = self.browser.submit() 404d0825bca7fe65beaee391d30da42e937db621564Steve Block 405d0825bca7fe65beaee391d30da42e937db621564Steve Block match = re.search("<title>(.+?)</title>", response.read()) 406d0825bca7fe65beaee391d30da42e937db621564Steve Block # If the resulting page has a title, and it contains the word 407d0825bca7fe65beaee391d30da42e937db621564Steve Block # "invalid" assume it's the login failure page. 408d0825bca7fe65beaee391d30da42e937db621564Steve Block if match and re.search("Invalid", match.group(1), re.IGNORECASE): 409d0825bca7fe65beaee391d30da42e937db621564Steve Block errorMessage = "Bugzilla login failed: %s" % match.group(1) 410d0825bca7fe65beaee391d30da42e937db621564Steve Block # raise an exception only if this was the last attempt 411d0825bca7fe65beaee391d30da42e937db621564Steve Block if attempts < 5: 412d0825bca7fe65beaee391d30da42e937db621564Steve Block log(errorMessage) 413d0825bca7fe65beaee391d30da42e937db621564Steve Block else: 414d0825bca7fe65beaee391d30da42e937db621564Steve Block raise Exception(errorMessage) 415d0825bca7fe65beaee391d30da42e937db621564Steve Block else: 416d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticated = True 417e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block self.username = username 418d0825bca7fe65beaee391d30da42e937db621564Steve Block 419f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _commit_queue_flag(self, mark_for_landing, mark_for_commit_queue): 420f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if mark_for_landing: 421f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return '+' 422f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch elif mark_for_commit_queue: 423f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return '?' 424f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return 'X' 425f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 426f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # FIXME: mark_for_commit_queue and mark_for_landing should be joined into a single commit_flag argument. 427d0825bca7fe65beaee391d30da42e937db621564Steve Block def _fill_attachment_form(self, 428d0825bca7fe65beaee391d30da42e937db621564Steve Block description, 429f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_object, 430d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_review=False, 431d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_commit_queue=False, 432545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch mark_for_landing=False, 433f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch is_patch=False, 434f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename=None, 435f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch mimetype=None): 436d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['description'] = description 437f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if is_patch: 438f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser['ispatch'] = ("1",) 439f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # FIXME: Should this use self._find_select_element_for_flag? 440d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['flag_type-1'] = ('?',) if mark_for_review else ('X',) 441f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser['flag_type-3'] = (self._commit_queue_flag(mark_for_landing, mark_for_commit_queue),) 442f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 443f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename = filename or "%s.patch" % timestamp() 444f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch mimetype = mimetype or "text/plain" 445f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser.add_file(file_object, mimetype, filename, 'data') 446f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 447f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _file_object_for_upload(self, file_or_string): 448f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if hasattr(file_or_string, 'read'): 449f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return file_or_string 450f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # Only if file_or_string is not already encoded do we want to encode it. 451f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if isinstance(file_or_string, unicode): 452f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_or_string = file_or_string.encode('utf-8') 453f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return StringIO.StringIO(file_or_string) 454f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 455f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # timestamp argument is just for unittests. 456f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def _filename_for_upload(self, file_object, bug_id, extension="txt", timestamp=timestamp): 457f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if hasattr(file_object, "name"): 458f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return file_object.name 459f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return "bug-%s-%s.%s" % (bug_id, timestamp(), extension) 460f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch 461f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch def add_attachment_to_bug(self, 462f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch bug_id, 463f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_or_string, 464f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch description, 465f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename=None, 466f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch comment_text=None): 467f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.authenticate() 468f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch log('Adding attachment "%s" to %s' % (description, self.bug_url_for_bug_id(bug_id))) 469f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if self.dryrun: 470f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch log(comment_text) 471f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch return 472d0825bca7fe65beaee391d30da42e937db621564Steve Block 473f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser.open(self.add_attachment_url(bug_id)) 474f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser.select_form(name="entryform") 475f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_object = self._file_object_for_upload(file_or_string) 476f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename = filename or self._filename_for_upload(file_object, bug_id) 477f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self._fill_attachment_form(description, file_object, filename=filename) 478f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if comment_text: 479f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch log(comment_text) 480f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser['comment'] = comment_text 481f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser.submit() 482d0825bca7fe65beaee391d30da42e937db621564Steve Block 483f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # FIXME: The arguments to this function should be simplified and then 484f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # this should be merged into add_attachment_to_bug 485d0825bca7fe65beaee391d30da42e937db621564Steve Block def add_patch_to_bug(self, 486d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_id, 487f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_or_string, 488d0825bca7fe65beaee391d30da42e937db621564Steve Block description, 489d0825bca7fe65beaee391d30da42e937db621564Steve Block comment_text=None, 490d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_review=False, 491d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_commit_queue=False, 492d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_landing=False): 493d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 494f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch log('Adding patch "%s" to %s' % (description, self.bug_url_for_bug_id(bug_id))) 495d0825bca7fe65beaee391d30da42e937db621564Steve Block 496d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 497d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 498d0825bca7fe65beaee391d30da42e937db621564Steve Block return 499d0825bca7fe65beaee391d30da42e937db621564Steve Block 500f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch self.browser.open(self.add_attachment_url(bug_id)) 501d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="entryform") 502f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_object = self._file_object_for_upload(file_or_string) 503f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename = self._filename_for_upload(file_object, bug_id, extension="patch") 504d0825bca7fe65beaee391d30da42e937db621564Steve Block self._fill_attachment_form(description, 505f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch file_object, 506d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_review=mark_for_review, 507d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_commit_queue=mark_for_commit_queue, 508d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_landing=mark_for_landing, 509f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch is_patch=True, 510f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch filename=filename) 511d0825bca7fe65beaee391d30da42e937db621564Steve Block if comment_text: 512d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 513d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['comment'] = comment_text 514d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 515d0825bca7fe65beaee391d30da42e937db621564Steve Block 516f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # FIXME: There has to be a more concise way to write this method. 517d0825bca7fe65beaee391d30da42e937db621564Steve Block def _check_create_bug_response(self, response_html): 518d0825bca7fe65beaee391d30da42e937db621564Steve Block match = re.search("<title>Bug (?P<bug_id>\d+) Submitted</title>", 519d0825bca7fe65beaee391d30da42e937db621564Steve Block response_html) 520d0825bca7fe65beaee391d30da42e937db621564Steve Block if match: 521d0825bca7fe65beaee391d30da42e937db621564Steve Block return match.group('bug_id') 522d0825bca7fe65beaee391d30da42e937db621564Steve Block 523d0825bca7fe65beaee391d30da42e937db621564Steve Block match = re.search( 524d0825bca7fe65beaee391d30da42e937db621564Steve Block '<div id="bugzilla-body">(?P<error_message>.+)<div id="footer">', 525d0825bca7fe65beaee391d30da42e937db621564Steve Block response_html, 526d0825bca7fe65beaee391d30da42e937db621564Steve Block re.DOTALL) 527d0825bca7fe65beaee391d30da42e937db621564Steve Block error_message = "FAIL" 528d0825bca7fe65beaee391d30da42e937db621564Steve Block if match: 529d0825bca7fe65beaee391d30da42e937db621564Steve Block text_lines = BeautifulSoup( 530d0825bca7fe65beaee391d30da42e937db621564Steve Block match.group('error_message')).findAll(text=True) 531d0825bca7fe65beaee391d30da42e937db621564Steve Block error_message = "\n" + '\n'.join( 532d0825bca7fe65beaee391d30da42e937db621564Steve Block [" " + line.strip() 533d0825bca7fe65beaee391d30da42e937db621564Steve Block for line in text_lines if line.strip()]) 534d0825bca7fe65beaee391d30da42e937db621564Steve Block raise Exception("Bug not created: %s" % error_message) 535d0825bca7fe65beaee391d30da42e937db621564Steve Block 536d0825bca7fe65beaee391d30da42e937db621564Steve Block def create_bug(self, 537d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_title, 538d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_description, 539d0825bca7fe65beaee391d30da42e937db621564Steve Block component=None, 54021939df44de1705786c545cd1bf519d47250322dBen Murdoch diff=None, 541d0825bca7fe65beaee391d30da42e937db621564Steve Block patch_description=None, 542d0825bca7fe65beaee391d30da42e937db621564Steve Block cc=None, 543dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block blocked=None, 544e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block assignee=None, 545d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_review=False, 546d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_commit_queue=False): 547d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 548d0825bca7fe65beaee391d30da42e937db621564Steve Block 549d0825bca7fe65beaee391d30da42e937db621564Steve Block log('Creating bug with title "%s"' % bug_title) 550d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 551d0825bca7fe65beaee391d30da42e937db621564Steve Block log(bug_description) 552f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch # FIXME: This will make some paths fail, as they assume this returns an id. 553d0825bca7fe65beaee391d30da42e937db621564Steve Block return 554d0825bca7fe65beaee391d30da42e937db621564Steve Block 555d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_server_url + "enter_bug.cgi?product=WebKit") 556d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="Create") 557d0825bca7fe65beaee391d30da42e937db621564Steve Block component_items = self.browser.find_control('component').items 558d0825bca7fe65beaee391d30da42e937db621564Steve Block component_names = map(lambda item: item.name, component_items) 559d0825bca7fe65beaee391d30da42e937db621564Steve Block if not component: 560d0825bca7fe65beaee391d30da42e937db621564Steve Block component = "New Bugs" 561d0825bca7fe65beaee391d30da42e937db621564Steve Block if component not in component_names: 562dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block component = User.prompt_with_list("Please pick a component:", component_names) 563dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.browser["component"] = [component] 564d0825bca7fe65beaee391d30da42e937db621564Steve Block if cc: 565dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.browser["cc"] = cc 566dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block if blocked: 56721939df44de1705786c545cd1bf519d47250322dBen Murdoch self.browser["blocked"] = unicode(blocked) 568f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch if not assignee: 569e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block assignee = self.username 570545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch if assignee and not self.browser.find_control("assigned_to").disabled: 571e78cbe89e6f337f2f1fe40315be88f742b547151Steve Block self.browser["assigned_to"] = assignee 572dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.browser["short_desc"] = bug_title 573dcc8cf2e65d1aa555cce12431a16547e66b469eeSteve Block self.browser["comment"] = bug_description 574d0825bca7fe65beaee391d30da42e937db621564Steve Block 57521939df44de1705786c545cd1bf519d47250322dBen Murdoch if diff: 57621939df44de1705786c545cd1bf519d47250322dBen Murdoch # _fill_attachment_form expects a file-like object 57721939df44de1705786c545cd1bf519d47250322dBen Murdoch # Patch files are already binary, so no encoding needed. 57821939df44de1705786c545cd1bf519d47250322dBen Murdoch assert(isinstance(diff, str)) 57921939df44de1705786c545cd1bf519d47250322dBen Murdoch patch_file_object = StringIO.StringIO(diff) 580d0825bca7fe65beaee391d30da42e937db621564Steve Block self._fill_attachment_form( 581d0825bca7fe65beaee391d30da42e937db621564Steve Block patch_description, 582d0825bca7fe65beaee391d30da42e937db621564Steve Block patch_file_object, 583d0825bca7fe65beaee391d30da42e937db621564Steve Block mark_for_review=mark_for_review, 584f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch mark_for_commit_queue=mark_for_commit_queue, 585f05b935882198ccf7d81675736e3aeb089c5113aBen Murdoch is_patch=True) 586d0825bca7fe65beaee391d30da42e937db621564Steve Block 587d0825bca7fe65beaee391d30da42e937db621564Steve Block response = self.browser.submit() 588d0825bca7fe65beaee391d30da42e937db621564Steve Block 589d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_id = self._check_create_bug_response(response.read()) 590d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Bug %s created." % bug_id) 591d0825bca7fe65beaee391d30da42e937db621564Steve Block log("%sshow_bug.cgi?id=%s" % (self.bug_server_url, bug_id)) 592d0825bca7fe65beaee391d30da42e937db621564Steve Block return bug_id 593d0825bca7fe65beaee391d30da42e937db621564Steve Block 594d0825bca7fe65beaee391d30da42e937db621564Steve Block def _find_select_element_for_flag(self, flag_name): 595d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: This will break if we ever re-order attachment flags 596d0825bca7fe65beaee391d30da42e937db621564Steve Block if flag_name == "review": 597d0825bca7fe65beaee391d30da42e937db621564Steve Block return self.browser.find_control(type='select', nr=0) 598545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch elif flag_name == "commit-queue": 599d0825bca7fe65beaee391d30da42e937db621564Steve Block return self.browser.find_control(type='select', nr=1) 600d0825bca7fe65beaee391d30da42e937db621564Steve Block raise Exception("Don't know how to find flag named \"%s\"" % flag_name) 601d0825bca7fe65beaee391d30da42e937db621564Steve Block 602d0825bca7fe65beaee391d30da42e937db621564Steve Block def clear_attachment_flags(self, 603d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_id, 604d0825bca7fe65beaee391d30da42e937db621564Steve Block additional_comment_text=None): 605d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 606d0825bca7fe65beaee391d30da42e937db621564Steve Block 607d0825bca7fe65beaee391d30da42e937db621564Steve Block comment_text = "Clearing flags on attachment: %s" % attachment_id 608d0825bca7fe65beaee391d30da42e937db621564Steve Block if additional_comment_text: 609d0825bca7fe65beaee391d30da42e937db621564Steve Block comment_text += "\n\n%s" % additional_comment_text 610d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 611d0825bca7fe65beaee391d30da42e937db621564Steve Block 612d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 613d0825bca7fe65beaee391d30da42e937db621564Steve Block return 614d0825bca7fe65beaee391d30da42e937db621564Steve Block 615d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.attachment_url_for_id(attachment_id, 'edit')) 616d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(nr=1) 617d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.set_value(comment_text, name='comment', nr=0) 618d0825bca7fe65beaee391d30da42e937db621564Steve Block self._find_select_element_for_flag('review').value = ("X",) 619d0825bca7fe65beaee391d30da42e937db621564Steve Block self._find_select_element_for_flag('commit-queue').value = ("X",) 620d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 621d0825bca7fe65beaee391d30da42e937db621564Steve Block 622d0825bca7fe65beaee391d30da42e937db621564Steve Block def set_flag_on_attachment(self, 623d0825bca7fe65beaee391d30da42e937db621564Steve Block attachment_id, 624d0825bca7fe65beaee391d30da42e937db621564Steve Block flag_name, 625d0825bca7fe65beaee391d30da42e937db621564Steve Block flag_value, 626545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch comment_text=None, 627545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch additional_comment_text=None): 628d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: We need a way to test this function on a live bugzilla 629d0825bca7fe65beaee391d30da42e937db621564Steve Block # instance. 630d0825bca7fe65beaee391d30da42e937db621564Steve Block 631d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 632d0825bca7fe65beaee391d30da42e937db621564Steve Block 633d0825bca7fe65beaee391d30da42e937db621564Steve Block if additional_comment_text: 634d0825bca7fe65beaee391d30da42e937db621564Steve Block comment_text += "\n\n%s" % additional_comment_text 635d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 636d0825bca7fe65beaee391d30da42e937db621564Steve Block 637d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 638d0825bca7fe65beaee391d30da42e937db621564Steve Block return 639d0825bca7fe65beaee391d30da42e937db621564Steve Block 640d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.attachment_url_for_id(attachment_id, 'edit')) 641d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(nr=1) 642545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch 643545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch if comment_text: 644545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch self.browser.set_value(comment_text, name='comment', nr=0) 645545e470e52f0ac6a3a072bf559c796b42c6066b6Ben Murdoch 646d0825bca7fe65beaee391d30da42e937db621564Steve Block self._find_select_element_for_flag(flag_name).value = (flag_value,) 647d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 648d0825bca7fe65beaee391d30da42e937db621564Steve Block 649d0825bca7fe65beaee391d30da42e937db621564Steve Block # FIXME: All of these bug editing methods have a ridiculous amount of 650d0825bca7fe65beaee391d30da42e937db621564Steve Block # copy/paste code. 651d0825bca7fe65beaee391d30da42e937db621564Steve Block 652d0825bca7fe65beaee391d30da42e937db621564Steve Block def obsolete_attachment(self, attachment_id, comment_text=None): 653d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 654d0825bca7fe65beaee391d30da42e937db621564Steve Block 655d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Obsoleting attachment: %s" % attachment_id) 656d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 657d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 658d0825bca7fe65beaee391d30da42e937db621564Steve Block return 659d0825bca7fe65beaee391d30da42e937db621564Steve Block 660d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.attachment_url_for_id(attachment_id, 'edit')) 661d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(nr=1) 662d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.find_control('isobsolete').items[0].selected = True 663d0825bca7fe65beaee391d30da42e937db621564Steve Block # Also clear any review flag (to remove it from review/commit queues) 664d0825bca7fe65beaee391d30da42e937db621564Steve Block self._find_select_element_for_flag('review').value = ("X",) 665d0825bca7fe65beaee391d30da42e937db621564Steve Block self._find_select_element_for_flag('commit-queue').value = ("X",) 666d0825bca7fe65beaee391d30da42e937db621564Steve Block if comment_text: 667d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 668d0825bca7fe65beaee391d30da42e937db621564Steve Block # Bugzilla has two textareas named 'comment', one is somehow 669d0825bca7fe65beaee391d30da42e937db621564Steve Block # hidden. We want the first. 670d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.set_value(comment_text, name='comment', nr=0) 671d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 672d0825bca7fe65beaee391d30da42e937db621564Steve Block 673d0825bca7fe65beaee391d30da42e937db621564Steve Block def add_cc_to_bug(self, bug_id, email_address_list): 674d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 675d0825bca7fe65beaee391d30da42e937db621564Steve Block 676d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Adding %s to the CC list for bug %s" % (email_address_list, 677d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_id)) 678d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 679d0825bca7fe65beaee391d30da42e937db621564Steve Block return 680d0825bca7fe65beaee391d30da42e937db621564Steve Block 681d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_url_for_bug_id(bug_id)) 682d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="changeform") 683d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser["newcc"] = ", ".join(email_address_list) 684d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 685d0825bca7fe65beaee391d30da42e937db621564Steve Block 686d0825bca7fe65beaee391d30da42e937db621564Steve Block def post_comment_to_bug(self, bug_id, comment_text, cc=None): 687d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 688d0825bca7fe65beaee391d30da42e937db621564Steve Block 689d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Adding comment to bug %s" % bug_id) 690d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 691d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 692d0825bca7fe65beaee391d30da42e937db621564Steve Block return 693d0825bca7fe65beaee391d30da42e937db621564Steve Block 694d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_url_for_bug_id(bug_id)) 695d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="changeform") 696d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser["comment"] = comment_text 697d0825bca7fe65beaee391d30da42e937db621564Steve Block if cc: 698d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser["newcc"] = ", ".join(cc) 699d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 700d0825bca7fe65beaee391d30da42e937db621564Steve Block 701d0825bca7fe65beaee391d30da42e937db621564Steve Block def close_bug_as_fixed(self, bug_id, comment_text=None): 702d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 703d0825bca7fe65beaee391d30da42e937db621564Steve Block 704d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Closing bug %s as fixed" % bug_id) 705d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 706d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 707d0825bca7fe65beaee391d30da42e937db621564Steve Block return 708d0825bca7fe65beaee391d30da42e937db621564Steve Block 709d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_url_for_bug_id(bug_id)) 710d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="changeform") 711d0825bca7fe65beaee391d30da42e937db621564Steve Block if comment_text: 712d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['comment'] = comment_text 713d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['bug_status'] = ['RESOLVED'] 714d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['resolution'] = ['FIXED'] 715d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 716d0825bca7fe65beaee391d30da42e937db621564Steve Block 717d0825bca7fe65beaee391d30da42e937db621564Steve Block def reassign_bug(self, bug_id, assignee, comment_text=None): 718d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 719d0825bca7fe65beaee391d30da42e937db621564Steve Block 720d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Assigning bug %s to %s" % (bug_id, assignee)) 721d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 722d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 723d0825bca7fe65beaee391d30da42e937db621564Steve Block return 724d0825bca7fe65beaee391d30da42e937db621564Steve Block 725d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_url_for_bug_id(bug_id)) 726d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="changeform") 727d0825bca7fe65beaee391d30da42e937db621564Steve Block if comment_text: 728d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 729d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser["comment"] = comment_text 730d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser["assigned_to"] = assignee 731d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 732d0825bca7fe65beaee391d30da42e937db621564Steve Block 733d0825bca7fe65beaee391d30da42e937db621564Steve Block def reopen_bug(self, bug_id, comment_text): 734d0825bca7fe65beaee391d30da42e937db621564Steve Block self.authenticate() 735d0825bca7fe65beaee391d30da42e937db621564Steve Block 736d0825bca7fe65beaee391d30da42e937db621564Steve Block log("Re-opening bug %s" % bug_id) 737d0825bca7fe65beaee391d30da42e937db621564Steve Block # Bugzilla requires a comment when re-opening a bug, so we know it will 738d0825bca7fe65beaee391d30da42e937db621564Steve Block # never be None. 739d0825bca7fe65beaee391d30da42e937db621564Steve Block log(comment_text) 740d0825bca7fe65beaee391d30da42e937db621564Steve Block if self.dryrun: 741d0825bca7fe65beaee391d30da42e937db621564Steve Block return 742d0825bca7fe65beaee391d30da42e937db621564Steve Block 743d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.open(self.bug_url_for_bug_id(bug_id)) 744d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.select_form(name="changeform") 745d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_status = self.browser.find_control("bug_status", type="select") 746d0825bca7fe65beaee391d30da42e937db621564Steve Block # This is a hack around the fact that ClientForm.ListControl seems to 747d0825bca7fe65beaee391d30da42e937db621564Steve Block # have no simpler way to ask if a control has an item named "REOPENED" 748d0825bca7fe65beaee391d30da42e937db621564Steve Block # without using exceptions for control flow. 749d0825bca7fe65beaee391d30da42e937db621564Steve Block possible_bug_statuses = map(lambda item: item.name, bug_status.items) 750d0825bca7fe65beaee391d30da42e937db621564Steve Block if "REOPENED" in possible_bug_statuses: 751d0825bca7fe65beaee391d30da42e937db621564Steve Block bug_status.value = ["REOPENED"] 7526c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen # If the bug was never confirmed it will not have a "REOPENED" 7536c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen # state, but only an "UNCONFIRMED" state. 7546c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen elif "UNCONFIRMED" in possible_bug_statuses: 7556c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen bug_status.value = ["UNCONFIRMED"] 756d0825bca7fe65beaee391d30da42e937db621564Steve Block else: 7576c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen # FIXME: This logic is slightly backwards. We won't print this 7586c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen # message if the bug is already open with state "UNCONFIRMED". 7596c2af9490927c3c5959b5cb07461b646f8b32f6cKristian Monsen log("Did not reopen bug %s, it appears to already be open with status %s." % (bug_id, bug_status.value)) 760d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser['comment'] = comment_text 761d0825bca7fe65beaee391d30da42e937db621564Steve Block self.browser.submit() 762