class StatusBot: default_host = "webkit-commit-queue.appspot.com" def __init__(self, host=default_host): self.set_host(host) self.browser = Browser() def set_host(self, host): self.statusbot_host = host self.statusbot_server_url = "http://%s" % self.statusbot_host def results_url_for_status(self, status_id): return "%s/results/%s" % (self.statusbot_server_url, status_id) def update_status(self, queue_name, status, patch=None, results_file=None): # During unit testing, statusbot_host is None if not self.statusbot_host: return log(status) update_status_url = "%s/update-status" % self.statusbot_server_url self.browser.open(update_status_url) self.browser.select_form(name="update_status") self.browser['queue_name'] = queue_name if patch: if patch.get('bug_id'): self.browser['bug_id'] = str(patch['bug_id']) if patch.get('id'): self.browser['patch_id'] = str(patch['id']) self.browser['status'] = status if results_file: self.browser.add_file(results_file, "text/plain", "results.txt", 'results_file') response = self.browser.submit() return response.read( ) # This is the id of the newly created status object. def patch_status(self, queue_name, patch_id): update_status_url = "%s/patch-status/%s/%s" % ( self.statusbot_server_url, queue_name, patch_id) try: return urllib2.urlopen(update_status_url).read() except urllib2.HTTPError, e: if e.code == 404: return None raise e
class StatusBot: default_host = "webkit-commit-queue.appspot.com" def __init__(self, host=default_host): self.set_host(host) self.browser = Browser() def set_host(self, host): self.statusbot_host = host self.statusbot_server_url = "http://%s" % self.statusbot_host def results_url_for_status(self, status_id): return "%s/results/%s" % (self.statusbot_server_url, status_id) def update_status(self, queue_name, status, patch=None, results_file=None): # During unit testing, statusbot_host is None if not self.statusbot_host: return log(status) update_status_url = "%s/update-status" % self.statusbot_server_url self.browser.open(update_status_url) self.browser.select_form(name="update_status") self.browser['queue_name'] = queue_name if patch: if patch.get('bug_id'): self.browser['bug_id'] = str(patch['bug_id']) if patch.get('id'): self.browser['patch_id'] = str(patch['id']) self.browser['status'] = status if results_file: self.browser.add_file(results_file, "text/plain", "results.txt", 'results_file') response = self.browser.submit() return response.read() # This is the id of the newly created status object. def patch_status(self, queue_name, patch_id): update_status_url = "%s/patch-status/%s/%s" % (self.statusbot_server_url, queue_name, patch_id) try: return urllib2.urlopen(update_status_url).read() except urllib2.HTTPError, e: if e.code == 404: return None raise e
class Bugzilla(object): def __init__(self, dryrun=False, committers=CommitterList()): self.dryrun = dryrun self.authenticated = False self.queries = BugzillaQueries(self) # 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) self.committers = committers # 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 = "*****@*****.**" 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) 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)) def _parse_bug_id_from_attachment_page(self, page): up_link = BeautifulSoup(page).find( 'link', rel='Up') # The "Up" relation happens to point to the bug. if not up_link: return None # This attachment does not exist (or you don't have permissions to view it). 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): 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) # This should really return an Attachment object # which can lazily fetch any missing 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 for attachment in self.fetch_bug(bug_id).attachments( include_obsolete=True): # FIXME: Once we have a real Attachment class we shouldn't paper over this possible comparison failure # and we should remove the int() == int() hacks and leave it just ==. if int(attachment['id']) == int(attachment_id): self._validate_committer_and_reviewer(attachment) return attachment return None # This should never be hit. # fetch_patches_from_bug exists until we expose a Bug class outside of bugzilla.py def fetch_patches_from_bug(self, bug_id): return self.fetch_bug(bug_id).patches() # _view_source_link belongs in some sort of webkit_config.py module. def _view_source_link(self, local_path): return "http://trac.webkit.org/browser/trunk/%s" % local_path def _flag_permission_rejection_message(self, setter_email, flag_name): committer_list = "WebKitTools/Scripts/modules/committers.py" # This could be computed from CommitterList.__file__ contribution_guidlines_url = "http://webkit.org/coding/contributing.html" # Should come from some webkit_config.py queue_administrator = "*****@*****.**" # This could be queried from the status_bot. queue_name = "commit-queue" # This could be queried from the tool. rejection_message = "%s does not have %s permissions according to %s." % ( setter_email, flag_name, self._view_source_link(committer_list)) rejection_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_url) rejection_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) rejection_message += " Due to bug 30084 the %s will require a restart after your change." % queue_name rejection_message += " Please contact %s to request a %s restart." % ( queue_administrator, queue_name) rejection_message += " After restart the %s will correctly respect your %s rights." % ( queue_name, flag_name) return rejection_message def _validate_setter_email(self, patch, result_key, lookup_function, rejection_function, reject_invalid_patches): setter_email = patch.get(result_key + '_email') if not setter_email: return None committer = lookup_function(setter_email) if committer: patch[result_key] = committer.full_name return patch[result_key] if reject_invalid_patches: rejection_function( patch['id'], self._flag_permission_rejection_message( setter_email, result_key)) else: log("Warning, attachment %s on bug %s has invalid %s (%s)" % (patch['id'], patch['bug_id'], result_key, setter_email)) return None def _validate_reviewer(self, patch, reject_invalid_patches): return self._validate_setter_email(patch, 'reviewer', self.committers.reviewer_by_email, self.reject_patch_from_review_queue, reject_invalid_patches) def _validate_committer(self, patch, reject_invalid_patches): return self._validate_setter_email(patch, 'committer', self.committers.committer_by_email, self.reject_patch_from_commit_queue, reject_invalid_patches) # FIXME: This is a hack until we have a real Attachment object. # _validate_committer and _validate_reviewer fill in the 'reviewer' and 'committer' # keys which other parts of the code expect to be filled in. def _validate_committer_and_reviewer(self, patch): self._validate_reviewer(patch, reject_invalid_patches=False) self._validate_committer(patch, reject_invalid_patches=False) # FIXME: fetch_reviewed_patches_from_bug and fetch_commit_queue_patches_from_bug # should share more code and use list comprehensions. def fetch_reviewed_patches_from_bug(self, bug_id, reject_invalid_patches=False): reviewed_patches = [] for attachment in self.fetch_bug(bug_id).attachments(): if self._validate_reviewer(attachment, reject_invalid_patches): reviewed_patches.append(attachment) return reviewed_patches def fetch_commit_queue_patches_from_bug(self, bug_id, reject_invalid_patches=False): commit_queue_patches = [] for attachment in self.fetch_reviewed_patches_from_bug( bug_id, reject_invalid_patches): if self._validate_committer(attachment, reject_invalid_patches): commit_queue_patches.append(attachment) return commit_queue_patches def authenticate(self): if self.authenticated: return if self.dryrun: log("Skipping log in for dry run...") self.authenticated = True return (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): # FIXME: We could add the ability to try again on failure. raise Exception("Bugzilla login failed: %s" % match.group(1)) self.authenticated = True def _fill_attachment_form(self, description, patch_file_object, comment_text=None, mark_for_review=False, mark_for_commit_queue=False, bug_id=None): self.browser['description'] = description self.browser['ispatch'] = ("1", ) self.browser['flag_type-1'] = ('?', ) if mark_for_review else ('X', ) self.browser['flag_type-3'] = ('?', ) if mark_for_commit_queue else ( '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): self.authenticate() log('Adding patch "%s" to bug %s' % (description, 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, 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(raw_input("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 or 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() # FIXME: We need a way to test this on a live bugzilla instance. def _set_flag_on_attachment(self, attachment_id, flag_name, flag_value, comment_text, additional_comment_text): 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() def reject_patch_from_commit_queue(self, attachment_id, additional_comment_text=None): comment_text = "Rejecting patch %s from commit-queue." % attachment_id self._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._set_flag_on_attachment(attachment_id, 'review', '-', comment_text, additional_comment_text) # 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) log( comment_text ) # Bugzilla requires a comment when re-opening a bug, so we know it will never be None. if self.dryrun: return self.browser.open(self.bug_url_for_bug_id(bug_id)) self.browser.select_form(name="changeform") self.browser['bug_status'] = ['REOPENED'] self.browser['comment'] = comment_text self.browser.submit()
class Bugzilla(object): def __init__(self, dryrun=False, committers=CommitterList()): self.dryrun = dryrun self.authenticated = False self.queries = BugzillaQueries(self) # 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) self.committers = committers # 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 = "*****@*****.**" 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) 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)) def _parse_bug_id_from_attachment_page(self, page): up_link = BeautifulSoup(page).find("link", rel="Up") # The "Up" relation happens to point to the bug. if not up_link: return None # This attachment does not exist (or you don't have permissions to view it). 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): 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) # This should really return an Attachment object # which can lazily fetch any missing 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 for attachment in self.fetch_bug(bug_id).attachments(include_obsolete=True): # FIXME: Once we have a real Attachment class we shouldn't paper over this possible comparison failure # and we should remove the int() == int() hacks and leave it just ==. if int(attachment["id"]) == int(attachment_id): self._validate_committer_and_reviewer(attachment) return attachment return None # This should never be hit. # fetch_patches_from_bug exists until we expose a Bug class outside of bugzilla.py def fetch_patches_from_bug(self, bug_id): return self.fetch_bug(bug_id).patches() # _view_source_link belongs in some sort of webkit_config.py module. def _view_source_link(self, local_path): return "http://trac.webkit.org/browser/trunk/%s" % local_path def _flag_permission_rejection_message(self, setter_email, flag_name): committer_list = ( "WebKitTools/Scripts/modules/committers.py" ) # This could be computed from CommitterList.__file__ contribution_guidlines_url = ( "http://webkit.org/coding/contributing.html" ) # Should come from some webkit_config.py queue_administrator = "*****@*****.**" # This could be queried from the status_bot. queue_name = "commit-queue" # This could be queried from the tool. rejection_message = "%s does not have %s permissions according to %s." % ( setter_email, flag_name, self._view_source_link(committer_list), ) rejection_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_url) ) rejection_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) ) rejection_message += " Due to bug 30084 the %s will require a restart after your change." % queue_name rejection_message += " Please contact %s to request a %s restart." % (queue_administrator, queue_name) rejection_message += " After restart the %s will correctly respect your %s rights." % (queue_name, flag_name) return rejection_message def _validate_setter_email(self, patch, result_key, lookup_function, rejection_function, reject_invalid_patches): setter_email = patch.get(result_key + "_email") if not setter_email: return None committer = lookup_function(setter_email) if committer: patch[result_key] = committer.full_name return patch[result_key] if reject_invalid_patches: rejection_function(patch["id"], self._flag_permission_rejection_message(setter_email, result_key)) else: log( "Warning, attachment %s on bug %s has invalid %s (%s)" % (patch["id"], patch["bug_id"], result_key, setter_email) ) return None def _validate_reviewer(self, patch, reject_invalid_patches): return self._validate_setter_email( patch, "reviewer", self.committers.reviewer_by_email, self.reject_patch_from_review_queue, reject_invalid_patches, ) def _validate_committer(self, patch, reject_invalid_patches): return self._validate_setter_email( patch, "committer", self.committers.committer_by_email, self.reject_patch_from_commit_queue, reject_invalid_patches, ) # FIXME: This is a hack until we have a real Attachment object. # _validate_committer and _validate_reviewer fill in the 'reviewer' and 'committer' # keys which other parts of the code expect to be filled in. def _validate_committer_and_reviewer(self, patch): self._validate_reviewer(patch, reject_invalid_patches=False) self._validate_committer(patch, reject_invalid_patches=False) # FIXME: fetch_reviewed_patches_from_bug and fetch_commit_queue_patches_from_bug # should share more code and use list comprehensions. def fetch_reviewed_patches_from_bug(self, bug_id, reject_invalid_patches=False): reviewed_patches = [] for attachment in self.fetch_bug(bug_id).attachments(): if self._validate_reviewer(attachment, reject_invalid_patches): reviewed_patches.append(attachment) return reviewed_patches def fetch_commit_queue_patches_from_bug(self, bug_id, reject_invalid_patches=False): commit_queue_patches = [] for attachment in self.fetch_reviewed_patches_from_bug(bug_id, reject_invalid_patches): if self._validate_committer(attachment, reject_invalid_patches): commit_queue_patches.append(attachment) return commit_queue_patches def authenticate(self): if self.authenticated: return if self.dryrun: log("Skipping log in for dry run...") self.authenticated = True return (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): # FIXME: We could add the ability to try again on failure. raise Exception("Bugzilla login failed: %s" % match.group(1)) self.authenticated = True def _fill_attachment_form( self, description, patch_file_object, comment_text=None, mark_for_review=False, mark_for_commit_queue=False, bug_id=None, ): self.browser["description"] = description self.browser["ispatch"] = ("1",) self.browser["flag_type-1"] = ("?",) if mark_for_review else ("X",) self.browser["flag_type-3"] = ("?",) if mark_for_commit_queue else ("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, ): self.authenticate() log('Adding patch "%s" to bug %s' % (description, 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, 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(raw_input("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 or 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() # FIXME: We need a way to test this on a live bugzilla instance. def _set_flag_on_attachment(self, attachment_id, flag_name, flag_value, comment_text, additional_comment_text): 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() def reject_patch_from_commit_queue(self, attachment_id, additional_comment_text=None): comment_text = "Rejecting patch %s from commit-queue." % attachment_id self._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._set_flag_on_attachment(attachment_id, "review", "-", comment_text, additional_comment_text) # 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) log(comment_text) # Bugzilla requires a comment when re-opening a bug, so we know it will never be None. if self.dryrun: return self.browser.open(self.bug_url_for_bug_id(bug_id)) self.browser.select_form(name="changeform") self.browser["bug_status"] = ["REOPENED"] self.browser["comment"] = comment_text self.browser.submit()