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