blob: 4506af2e984431f374e7d685b300c5a28ed37c52 [file] [log] [blame]
# Copyright (c) 2009 Google Inc. All rights reserved.
# Copyright (c) 2009 Apple Inc. All rights reserved.
# Copyright (c) 2010 Research In Motion Limited. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# WebKit's Python module for interacting with Bugzilla
import re
import subprocess
from datetime import datetime # used in timestamp()
# Import WebKit-specific modules.
from webkitpy.webkit_logging import error, log
from webkitpy.committers import CommitterList
from webkitpy.credentials import Credentials
from webkitpy.user import User
# WebKit includes a built copy of BeautifulSoup in Scripts/webkitpy
# so this import should always succeed.
from .BeautifulSoup import BeautifulSoup, SoupStrainer
from mechanize import Browser
def parse_bug_id(message):
match = re.search("http\://webkit\.org/b/(?P<bug_id>\d+)", message)
if match:
return int(match.group('bug_id'))
match = re.search(
Bugzilla.bug_server_regex + "show_bug\.cgi\?id=(?P<bug_id>\d+)",
message)
if match:
return int(match.group('bug_id'))
return None
def timestamp():
return datetime.now().strftime("%Y%m%d%H%M%S")
class Attachment(object):
def __init__(self, attachment_dictionary, bug):
self._attachment_dictionary = attachment_dictionary
self._bug = bug
self._reviewer = None
self._committer = None
def _bugzilla(self):
return self._bug._bugzilla
def id(self):
return int(self._attachment_dictionary.get("id"))
def attacher_is_committer(self):
return self._bugzilla.committers.committer_by_email(
patch.attacher_email())
def attacher_email(self):
return self._attachment_dictionary.get("attacher_email")
def bug(self):
return self._bug
def bug_id(self):
return int(self._attachment_dictionary.get("bug_id"))
def is_patch(self):
return not not self._attachment_dictionary.get("is_patch")
def is_obsolete(self):
return not not self._attachment_dictionary.get("is_obsolete")
def name(self):
return self._attachment_dictionary.get("name")
def review(self):
return self._attachment_dictionary.get("review")
def commit_queue(self):
return self._attachment_dictionary.get("commit-queue")
def url(self):
# FIXME: This should just return
# self._bugzilla().attachment_url_for_id(self.id()). scm_unittest.py
# depends on the current behavior.
return self._attachment_dictionary.get("url")
def _validate_flag_value(self, flag):
email = self._attachment_dictionary.get("%s_email" % flag)
if not email:
return None
committer = getattr(self._bugzilla().committers,
"%s_by_email" % flag)(email)
if committer:
return committer
log("Warning, attachment %s on bug %s has invalid %s (%s)" % (
self._attachment_dictionary['id'],
self._attachment_dictionary['bug_id'], flag, email))
def reviewer(self):
if not self._reviewer:
self._reviewer = self._validate_flag_value("reviewer")
return self._reviewer
def committer(self):
if not self._committer:
self._committer = self._validate_flag_value("committer")
return self._committer
class Bug(object):
# FIXME: This class is kinda a hack for now. It exists so we have one
# place to hold bug logic, even if much of the code deals with
# dictionaries still.
def __init__(self, bug_dictionary, bugzilla):
self.bug_dictionary = bug_dictionary
self._bugzilla = bugzilla
def id(self):
return self.bug_dictionary["id"]
def assigned_to_email(self):
return self.bug_dictionary["assigned_to_email"]
# Rarely do we actually want obsolete attachments
def attachments(self, include_obsolete=False):
attachments = self.bug_dictionary["attachments"]
if not include_obsolete:
attachments = filter(lambda attachment:
not attachment["is_obsolete"], attachments)
return [Attachment(attachment, self) for attachment in attachments]
def patches(self, include_obsolete=False):
return [patch for patch in self.attachments(include_obsolete)
if patch.is_patch()]
def unreviewed_patches(self):
return [patch for patch in self.patches() if patch.review() == "?"]
def reviewed_patches(self, include_invalid=False):
patches = [patch for patch in self.patches() if patch.review() == "+"]
if include_invalid:
return patches
# Checking reviewer() ensures that it was both reviewed and has a valid
# reviewer.
return filter(lambda patch: patch.reviewer(), patches)
def commit_queued_patches(self, include_invalid=False):
patches = [patch for patch in self.patches()
if patch.commit_queue() == "+"]
if include_invalid:
return patches
# Checking committer() ensures that it was both commit-queue+'d and has
# a valid committer.
return filter(lambda patch: patch.committer(), patches)
# A container for all of the logic for making and parsing buzilla queries.
class BugzillaQueries(object):
def __init__(self, bugzilla):
self._bugzilla = bugzilla
# Note: _load_query and _fetch_bug are the only two methods which access
# self._bugzilla.
def _load_query(self, query):
self._bugzilla.authenticate()
full_url = "%s%s" % (self._bugzilla.bug_server_url, query)
return self._bugzilla.browser.open(full_url)
def _fetch_bug(self, bug_id):
return self._bugzilla.fetch_bug(bug_id)
def _fetch_bug_ids_advanced_query(self, query):
soup = BeautifulSoup(self._load_query(query))
# The contents of the <a> inside the cells in the first column happen
# to be the bug id.
return [int(bug_link_cell.find("a").string)
for bug_link_cell in soup('td', "first-child")]
def _parse_attachment_ids_request_query(self, page):
digits = re.compile("\d+")
attachment_href = re.compile("attachment.cgi\?id=\d+&action=review")
attachment_links = SoupStrainer("a", href=attachment_href)
return [int(digits.search(tag["href"]).group(0))
for tag in BeautifulSoup(page, parseOnlyThese=attachment_links)]
def _fetch_attachment_ids_request_query(self, query):
return self._parse_attachment_ids_request_query(self._load_query(query))
# List of all r+'d bugs.
def fetch_bug_ids_from_pending_commit_list(self):
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"
return self._fetch_bug_ids_advanced_query(needs_commit_query_url)
def fetch_patches_from_pending_commit_list(self):
return sum([self._fetch_bug(bug_id).reviewed_patches()
for bug_id in self.fetch_bug_ids_from_pending_commit_list()], [])
def fetch_bug_ids_from_commit_queue(self):
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"
return self._fetch_bug_ids_advanced_query(commit_queue_url)
def fetch_patches_from_commit_queue(self):
# This function will only return patches which have valid committers
# set. It won't reject patches with invalid committers/reviewers.
return sum([self._fetch_bug(bug_id).commit_queued_patches()
for bug_id in self.fetch_bug_ids_from_commit_queue()], [])
def _fetch_bug_ids_from_review_queue(self):
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?"
return self._fetch_bug_ids_advanced_query(review_queue_url)
def fetch_patches_from_review_queue(self, limit=None):
# [:None] returns the whole array.
return sum([self._fetch_bug(bug_id).unreviewed_patches()
for bug_id in self._fetch_bug_ids_from_review_queue()[:limit]], [])
# FIXME: Why do we have both fetch_patches_from_review_queue and
# fetch_attachment_ids_from_review_queue??
# NOTE: This is also the only client of _fetch_attachment_ids_request_query
def fetch_attachment_ids_from_review_queue(self):
review_queue_url = "request.cgi?action=queue&type=review&group=type"
return self._fetch_attachment_ids_request_query(review_queue_url)
class CommitterValidator(object):
def __init__(self, bugzilla):
self._bugzilla = bugzilla
# _view_source_url belongs in some sort of webkit_config.py module.
def _view_source_url(self, local_path):
return "http://trac.webkit.org/browser/trunk/%s" % local_path
def _flag_permission_rejection_message(self, setter_email, flag_name):
# This could be computed from CommitterList.__file__
committer_list = "WebKitTools/Scripts/webkitpy/committers.py"
# Should come from some webkit_config.py
contribution_guidlines = "http://webkit.org/coding/contributing.html"
# This could be queried from the status_server.
queue_administrator = "eseidel@chromium.org"
# This could be queried from the tool.
queue_name = "commit-queue"
message = "%s does not have %s permissions according to %s." % (
setter_email,
flag_name,
self._view_source_url(committer_list))
message += "\n\n- If you do not have %s rights please read %s for instructions on how to use bugzilla flags." % (
flag_name, contribution_guidlines)
message += "\n\n- If you have %s rights please correct the error in %s by adding yourself to the file (no review needed). " % (
flag_name, committer_list)
message += "Due to bug 30084 the %s will require a restart after your change. " % queue_name
message += "Please contact %s to request a %s restart. " % (
queue_administrator, queue_name)
message += "After restart the %s will correctly respect your %s rights." % (
queue_name, flag_name)
return message
def _validate_setter_email(self, patch, result_key, rejection_function):
committer = getattr(patch, result_key)()
# If the flag is set, and we don't recognize the setter, reject the
# flag!
setter_email = patch._attachment_dictionary.get("%s_email" % result_key)
if setter_email and not committer:
rejection_function(patch.id(),
self._flag_permission_rejection_message(setter_email,
result_key))
return False
return True
def patches_after_rejecting_invalid_commiters_and_reviewers(self, patches):
validated_patches = []
for patch in patches:
if (self._validate_setter_email(
patch, "reviewer", self.reject_patch_from_review_queue)
and self._validate_setter_email(
patch, "committer", self.reject_patch_from_commit_queue)):
validated_patches.append(patch)
return validated_patches
def reject_patch_from_commit_queue(self,
attachment_id,
additional_comment_text=None):
comment_text = "Rejecting patch %s from commit-queue." % attachment_id
self._bugzilla.set_flag_on_attachment(attachment_id,
"commit-queue",
"-",
comment_text,
additional_comment_text)
def reject_patch_from_review_queue(self,
attachment_id,
additional_comment_text=None):
comment_text = "Rejecting patch %s from review queue." % attachment_id
self._bugzilla.set_flag_on_attachment(attachment_id,
'review',
'-',
comment_text,
additional_comment_text)
class Bugzilla(object):
def __init__(self, dryrun=False, committers=CommitterList()):
self.dryrun = dryrun
self.authenticated = False
self.queries = BugzillaQueries(self)
self.committers = committers
# FIXME: We should use some sort of Browser mock object when in dryrun
# mode (to prevent any mistakes).
self.browser = Browser()
# Ignore bugs.webkit.org/robots.txt until we fix it to allow this
# script.
self.browser.set_handle_robots(False)
# FIXME: Much of this should go into some sort of config module:
bug_server_host = "bugs.webkit.org"
bug_server_regex = "https?://%s/" % re.sub('\.', '\\.', bug_server_host)
bug_server_url = "https://%s/" % bug_server_host
unassigned_email = "webkit-unassigned@lists.webkit.org"
def bug_url_for_bug_id(self, bug_id, xml=False):
content_type = "&ctype=xml" if xml else ""
return "%sshow_bug.cgi?id=%s%s" % (self.bug_server_url,
bug_id,
content_type)
def short_bug_url_for_bug_id(self, bug_id):
return "http://webkit.org/b/%s" % bug_id
def attachment_url_for_id(self, attachment_id, action="view"):
action_param = ""
if action and action != "view":
action_param = "&action=%s" % action
return "%sattachment.cgi?id=%s%s" % (self.bug_server_url,
attachment_id,
action_param)
def _parse_attachment_flag(self,
element,
flag_name,
attachment,
result_key):
flag = element.find('flag', attrs={'name': flag_name})
if flag:
attachment[flag_name] = flag['status']
if flag['status'] == '+':
attachment[result_key] = flag['setter']
def _parse_attachment_element(self, element, bug_id):
attachment = {}
attachment['bug_id'] = bug_id
attachment['is_obsolete'] = (element.has_key('isobsolete') and element['isobsolete'] == "1")
attachment['is_patch'] = (element.has_key('ispatch') and element['ispatch'] == "1")
attachment['id'] = int(element.find('attachid').string)
# FIXME: No need to parse out the url here.
attachment['url'] = self.attachment_url_for_id(attachment['id'])
attachment['name'] = unicode(element.find('desc').string)
attachment['attacher_email'] = str(element.find('attacher').string)
attachment['type'] = str(element.find('type').string)
self._parse_attachment_flag(
element, 'review', attachment, 'reviewer_email')
self._parse_attachment_flag(
element, 'commit-queue', attachment, 'committer_email')
return attachment
def _parse_bug_page(self, page):
soup = BeautifulSoup(page)
bug = {}
bug["id"] = int(soup.find("bug_id").string)
bug["title"] = unicode(soup.find("short_desc").string)
bug["reporter_email"] = str(soup.find("reporter").string)
bug["assigned_to_email"] = str(soup.find("assigned_to").string)
bug["cc_emails"] = [str(element.string)
for element in soup.findAll('cc')]
bug["attachments"] = [self._parse_attachment_element(element, bug["id"]) for element in soup.findAll('attachment')]
return bug
# Makes testing fetch_*_from_bug() possible until we have a better
# BugzillaNetwork abstration.
def _fetch_bug_page(self, bug_id):
bug_url = self.bug_url_for_bug_id(bug_id, xml=True)
log("Fetching: %s" % bug_url)
return self.browser.open(bug_url)
def fetch_bug_dictionary(self, bug_id):
return self._parse_bug_page(self._fetch_bug_page(bug_id))
# FIXME: A BugzillaCache object should provide all these fetch_ methods.
def fetch_bug(self, bug_id):
return Bug(self.fetch_bug_dictionary(bug_id), self)
def _parse_bug_id_from_attachment_page(self, page):
# The "Up" relation happens to point to the bug.
up_link = BeautifulSoup(page).find('link', rel='Up')
if not up_link:
# This attachment does not exist (or you don't have permissions to
# view it).
return None
match = re.search("show_bug.cgi\?id=(?P<bug_id>\d+)", up_link['href'])
return int(match.group('bug_id'))
def bug_id_for_attachment_id(self, attachment_id):
self.authenticate()
attachment_url = self.attachment_url_for_id(attachment_id, 'edit')
log("Fetching: %s" % attachment_url)
page = self.browser.open(attachment_url)
return self._parse_bug_id_from_attachment_page(page)
# FIXME: This should just return Attachment(id), which should be able to
# lazily fetch needed data.
def fetch_attachment(self, attachment_id):
# We could grab all the attachment details off of the attachment edit
# page but we already have working code to do so off of the bugs page,
# so re-use that.
bug_id = self.bug_id_for_attachment_id(attachment_id)
if not bug_id:
return None
attachments = self.fetch_bug(bug_id).attachments(include_obsolete=True)
for attachment in attachments:
if attachment.id() == int(attachment_id):
return attachment
return None # This should never be hit.
def authenticate(self):
if self.authenticated:
return
if self.dryrun:
log("Skipping log in for dry run...")
self.authenticated = True
return
attempts = 0
while not self.authenticated:
attempts += 1
(username, password) = Credentials(
self.bug_server_host, git_prefix="bugzilla").read_credentials()
log("Logging in as %s..." % username)
self.browser.open(self.bug_server_url +
"index.cgi?GoAheadAndLogIn=1")
self.browser.select_form(name="login")
self.browser['Bugzilla_login'] = username
self.browser['Bugzilla_password'] = password
response = self.browser.submit()
match = re.search("<title>(.+?)</title>", response.read())
# If the resulting page has a title, and it contains the word
# "invalid" assume it's the login failure page.
if match and re.search("Invalid", match.group(1), re.IGNORECASE):
errorMessage = "Bugzilla login failed: %s" % match.group(1)
# raise an exception only if this was the last attempt
if attempts < 5:
log(errorMessage)
else:
raise Exception(errorMessage)
else:
self.authenticated = True
def _fill_attachment_form(self,
description,
patch_file_object,
comment_text=None,
mark_for_review=False,
mark_for_commit_queue=False,
mark_for_landing=False, bug_id=None):
self.browser['description'] = description
self.browser['ispatch'] = ("1",)
self.browser['flag_type-1'] = ('?',) if mark_for_review else ('X',)
if mark_for_landing:
self.browser['flag_type-3'] = ('+',)
elif mark_for_commit_queue:
self.browser['flag_type-3'] = ('?',)
else:
self.browser['flag_type-3'] = ('X',)
if bug_id:
patch_name = "bug-%s-%s.patch" % (bug_id, timestamp())
else:
patch_name ="%s.patch" % timestamp()
self.browser.add_file(patch_file_object,
"text/plain",
patch_name,
'data')
def add_patch_to_bug(self,
bug_id,
patch_file_object,
description,
comment_text=None,
mark_for_review=False,
mark_for_commit_queue=False,
mark_for_landing=False):
self.authenticate()
log('Adding patch "%s" to %sshow_bug.cgi?id=%s' % (description,
self.bug_server_url,
bug_id))
if self.dryrun:
log(comment_text)
return
self.browser.open("%sattachment.cgi?action=enter&bugid=%s" % (
self.bug_server_url, bug_id))
self.browser.select_form(name="entryform")
self._fill_attachment_form(description,
patch_file_object,
mark_for_review=mark_for_review,
mark_for_commit_queue=mark_for_commit_queue,
mark_for_landing=mark_for_landing,
bug_id=bug_id)
if comment_text:
log(comment_text)
self.browser['comment'] = comment_text
self.browser.submit()
def prompt_for_component(self, components):
log("Please pick a component:")
i = 0
for name in components:
i += 1
log("%2d. %s" % (i, name))
result = int(User.prompt("Enter a number: ")) - 1
return components[result]
def _check_create_bug_response(self, response_html):
match = re.search("<title>Bug (?P<bug_id>\d+) Submitted</title>",
response_html)
if match:
return match.group('bug_id')
match = re.search(
'<div id="bugzilla-body">(?P<error_message>.+)<div id="footer">',
response_html,
re.DOTALL)
error_message = "FAIL"
if match:
text_lines = BeautifulSoup(
match.group('error_message')).findAll(text=True)
error_message = "\n" + '\n'.join(
[" " + line.strip()
for line in text_lines if line.strip()])
raise Exception("Bug not created: %s" % error_message)
def create_bug(self,
bug_title,
bug_description,
component=None,
patch_file_object=None,
patch_description=None,
cc=None,
mark_for_review=False,
mark_for_commit_queue=False):
self.authenticate()
log('Creating bug with title "%s"' % bug_title)
if self.dryrun:
log(bug_description)
return
self.browser.open(self.bug_server_url + "enter_bug.cgi?product=WebKit")
self.browser.select_form(name="Create")
component_items = self.browser.find_control('component').items
component_names = map(lambda item: item.name, component_items)
if not component:
component = "New Bugs"
if component not in component_names:
component = self.prompt_for_component(component_names)
self.browser['component'] = [component]
if cc:
self.browser['cc'] = cc
self.browser['short_desc'] = bug_title
self.browser['comment'] = bug_description
if patch_file_object:
self._fill_attachment_form(
patch_description,
patch_file_object,
mark_for_review=mark_for_review,
mark_for_commit_queue=mark_for_commit_queue)
response = self.browser.submit()
bug_id = self._check_create_bug_response(response.read())
log("Bug %s created." % bug_id)
log("%sshow_bug.cgi?id=%s" % (self.bug_server_url, bug_id))
return bug_id
def _find_select_element_for_flag(self, flag_name):
# FIXME: This will break if we ever re-order attachment flags
if flag_name == "review":
return self.browser.find_control(type='select', nr=0)
if flag_name == "commit-queue":
return self.browser.find_control(type='select', nr=1)
raise Exception("Don't know how to find flag named \"%s\"" % flag_name)
def clear_attachment_flags(self,
attachment_id,
additional_comment_text=None):
self.authenticate()
comment_text = "Clearing flags on attachment: %s" % attachment_id
if additional_comment_text:
comment_text += "\n\n%s" % additional_comment_text
log(comment_text)
if self.dryrun:
return
self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
self.browser.select_form(nr=1)
self.browser.set_value(comment_text, name='comment', nr=0)
self._find_select_element_for_flag('review').value = ("X",)
self._find_select_element_for_flag('commit-queue').value = ("X",)
self.browser.submit()
def set_flag_on_attachment(self,
attachment_id,
flag_name,
flag_value,
comment_text,
additional_comment_text):
# FIXME: We need a way to test this function on a live bugzilla
# instance.
self.authenticate()
if additional_comment_text:
comment_text += "\n\n%s" % additional_comment_text
log(comment_text)
if self.dryrun:
return
self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
self.browser.select_form(nr=1)
self.browser.set_value(comment_text, name='comment', nr=0)
self._find_select_element_for_flag(flag_name).value = (flag_value,)
self.browser.submit()
# FIXME: All of these bug editing methods have a ridiculous amount of
# copy/paste code.
def obsolete_attachment(self, attachment_id, comment_text=None):
self.authenticate()
log("Obsoleting attachment: %s" % attachment_id)
if self.dryrun:
log(comment_text)
return
self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
self.browser.select_form(nr=1)
self.browser.find_control('isobsolete').items[0].selected = True
# Also clear any review flag (to remove it from review/commit queues)
self._find_select_element_for_flag('review').value = ("X",)
self._find_select_element_for_flag('commit-queue').value = ("X",)
if comment_text:
log(comment_text)
# Bugzilla has two textareas named 'comment', one is somehow
# hidden. We want the first.
self.browser.set_value(comment_text, name='comment', nr=0)
self.browser.submit()
def add_cc_to_bug(self, bug_id, email_address_list):
self.authenticate()
log("Adding %s to the CC list for bug %s" % (email_address_list,
bug_id))
if self.dryrun:
return
self.browser.open(self.bug_url_for_bug_id(bug_id))
self.browser.select_form(name="changeform")
self.browser["newcc"] = ", ".join(email_address_list)
self.browser.submit()
def post_comment_to_bug(self, bug_id, comment_text, cc=None):
self.authenticate()
log("Adding comment to bug %s" % bug_id)
if self.dryrun:
log(comment_text)
return
self.browser.open(self.bug_url_for_bug_id(bug_id))
self.browser.select_form(name="changeform")
self.browser["comment"] = comment_text
if cc:
self.browser["newcc"] = ", ".join(cc)
self.browser.submit()
def close_bug_as_fixed(self, bug_id, comment_text=None):
self.authenticate()
log("Closing bug %s as fixed" % bug_id)
if self.dryrun:
log(comment_text)
return
self.browser.open(self.bug_url_for_bug_id(bug_id))
self.browser.select_form(name="changeform")
if comment_text:
log(comment_text)
self.browser['comment'] = comment_text
self.browser['bug_status'] = ['RESOLVED']
self.browser['resolution'] = ['FIXED']
self.browser.submit()
def reassign_bug(self, bug_id, assignee, comment_text=None):
self.authenticate()
log("Assigning bug %s to %s" % (bug_id, assignee))
if self.dryrun:
log(comment_text)
return
self.browser.open(self.bug_url_for_bug_id(bug_id))
self.browser.select_form(name="changeform")
if comment_text:
log(comment_text)
self.browser["comment"] = comment_text
self.browser["assigned_to"] = assignee
self.browser.submit()
def reopen_bug(self, bug_id, comment_text):
self.authenticate()
log("Re-opening bug %s" % bug_id)
# Bugzilla requires a comment when re-opening a bug, so we know it will
# never be None.
log(comment_text)
if self.dryrun:
return
self.browser.open(self.bug_url_for_bug_id(bug_id))
self.browser.select_form(name="changeform")
bug_status = self.browser.find_control("bug_status", type="select")
# This is a hack around the fact that ClientForm.ListControl seems to
# have no simpler way to ask if a control has an item named "REOPENED"
# without using exceptions for control flow.
possible_bug_statuses = map(lambda item: item.name, bug_status.items)
if "REOPENED" in possible_bug_statuses:
bug_status.value = ["REOPENED"]
else:
log("Did not reopen bug %s. " +
"It appears to already be open with status %s." % (
bug_id, bug_status.value))
self.browser['comment'] = comment_text
self.browser.submit()