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