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