diff options
author | Slava Shishkin <slavashishkin@gmail.com> | 2022-04-15 17:48:45 +0300 |
---|---|---|
committer | Slava Shishkin <slavashishkin@gmail.com> | 2022-04-15 17:48:45 +0300 |
commit | ae00f4b6ef0b05bbd40166f57f9c9ffbfe9e4cdf (patch) | |
tree | f3fba859d14ccc75a6b16ffacc1a715846142479 | |
parent | 8fd0053fe139282bc1887f6f6fdc959623e0441a (diff) | |
download | git-bz-ae00f4b6ef0b05bbd40166f57f9c9ffbfe9e4cdf.tar.gz |
Apply automated Python 2 to 3 code translation
-rwxr-xr-x | git-bz | 529 |
1 files changed, 322 insertions, 207 deletions
@@ -1,4 +1,4 @@ -#!/usr/bin/python2 +#!/usr/bin/python3 # -*- coding: utf_8 -*- # # git-bz - git subcommand to integrate with bugzilla @@ -33,7 +33,7 @@ # (generated from git-bz.txt in this directory.) # DEFAULT_CONFIG = \ -""" + """ default-assigned-to = default-op-sys = All default-platform = All @@ -43,26 +43,26 @@ default-version = unspecified CONFIG = {} CONFIG['bugs.freedesktop.org'] = \ -""" + """ https = true default-priority = medium """ CONFIG['bugs.gentoo.org'] = \ -""" + """ https = true default-priority = Normal default-product = Gentoo Linux """ CONFIG['bugzilla.gnome.org'] = \ -""" + """ https = true default-priority = Normal """ CONFIG['bugzilla.mozilla.org'] = \ -""" + """ https = true default-priority = --- """ @@ -80,27 +80,28 @@ git_config = { ################################################################################ import base64 -import cPickle as pickle -from ConfigParser import RawConfigParser, NoOptionError -import httplib -import urllib +import pickle as pickle +from configparser import RawConfigParser, NoOptionError +import http.client +import urllib.request, urllib.parse, urllib.error import optparse import os + try: from sqlite3 import dbapi2 as sqlite except ImportError: from pysqlite2 import dbapi2 as sqlite import re -from StringIO import StringIO +from io import StringIO from subprocess import Popen, CalledProcessError, PIPE import shutil import sys import tempfile import time import traceback -import xmlrpclib -import urllib -import urlparse +import xmlrpc.client +import urllib.request, urllib.parse, urllib.error +import urllib.parse from xml.etree.cElementTree import ElementTree import base64 import warnings @@ -109,7 +110,6 @@ import smtplib import random import string - # Globals # ======= @@ -117,6 +117,7 @@ import string global_options = None bugs_applied = [] + # Utility functions for git # ========================= @@ -139,7 +140,7 @@ def git_run(command, *args, **kwargs): input = None return_stderr = False strip = True - for (k,v) in kwargs.iteritems(): + for (k, v) in kwargs.items(): if k == '_quiet': quiet = True elif k == '_interactive': @@ -185,20 +186,25 @@ def git_run(command, *args, **kwargs): else: return output + # Wrapper to allow us to do git.<command>(...) instead of git_run() class Git: def __getattr__(self, command): def f(*args, **kwargs): return git_run(command, *args, **kwargs) + return f + git = Git() + class GitCommit: def __init__(self, id, subject): self.id = id self.subject = subject + def rev_list_commits(*args, **kwargs): kwargs_copy = dict(kwargs) kwargs_copy['pretty'] = 'format:%s' @@ -211,7 +217,7 @@ def rev_list_commits(*args, **kwargs): raise RuntimeException("git rev-list didn't return an even number of lines") result = [] - for i in xrange(0, len(lines), 2): + for i in range(0, len(lines), 2): m = re.match("commit\s+([A-Fa-f0-9]+)", lines[i]) if not m: raise RuntimeException("Can't parse commit it '%s'", lines[i]) @@ -221,6 +227,7 @@ def rev_list_commits(*args, **kwargs): return result + def get_commits(commit_or_revision_range): # We take specifying a single revision to mean everything since that # revision, while git-rev-list lists that revision and all ancestors @@ -238,18 +245,21 @@ def get_commits(commit_or_revision_range): return commits + def get_patch(commit): # We could pass through -M as an option, but I think you basically always # want it; showing renames as renames rather than removes/adds greatly # improves readability. return git.format_patch(commit.id + "^.." + commit.id, stdout=True, M=True) + def get_body(commit): body = git.log(commit.id + "^.." + commit.id, pretty="format:%b", _strip=False) # Preserve leading space, which tends to be indents, but strip off # the trailing newline and any other insignificant space at the end. return body.rstrip() + def commit_is_merge(commit): contents = git.cat_file("commit", commit.id) parent_count = 0 @@ -261,6 +271,7 @@ def commit_is_merge(commit): return parent_count > 1 + # Global configuration variables # ============================== @@ -278,12 +289,14 @@ def init_git_config(): git_config[name] = value + def get_tracker(): if global_options.bugzilla != None: return global_options.bugzilla return git_config['default-tracker'] + def get_default_product(): product = git_config['default-product'] if product is None: @@ -292,6 +305,7 @@ def get_default_product(): return product + def get_default_component(): component = git_config['default-component'] if component is None: @@ -300,6 +314,7 @@ def get_default_component(): return component + # Per-tracker configuration variables # =================================== @@ -309,6 +324,7 @@ def resolve_host_alias(alias): except CalledProcessError: return alias + def split_local_config(config_text): result = {} @@ -328,6 +344,7 @@ def split_local_config(config_text): return result + def get_git_config(name): try: name = name.replace(".", r"\.") @@ -349,10 +366,12 @@ def get_git_config(name): return result + # We only ever should be the config for one tracker in the course of a single run cached_config = None cached_config_tracker = None + def get_config(tracker): global cached_config global cached_config_tracker @@ -371,22 +390,26 @@ def get_config(tracker): return cached_config + def tracker_uses_https(tracker): config = get_config(tracker) return 'https' in config and config['https'] == 'true' + def tracker_get_path(tracker): config = get_config(tracker) if 'path' in config: return config['path'] return None + def tracker_get_auth_user(tracker): config = get_config(tracker) if 'auth-user' in config: return config['auth-user'] return None + def tracker_get_auth_password(tracker): config = get_config(tracker) if 'auth-password' in config: @@ -395,31 +418,35 @@ def tracker_get_auth_password(tracker): def merge_default_fields_from_dict(default_fields, d): - for key, value in d.iteritems(): + for key, value in d.items(): if key.startswith("default-"): param = key[8:].replace("-", "_") if param in ['tracker', 'product', 'component']: continue default_fields[param] = value + def tracker_get_bz_user(tracker): config = get_config(tracker) if 'bz-user' in config: return config['bz-user'] return None + def tracker_get_bz_password(tracker): config = get_config(tracker) if 'bz-password' in config: return config['bz-password'] return None + def tracker_get_use_git_credential(tracker): config = get_config(tracker) if 'use-git-credential' in config: return config['use-git-credential'] == 'true' return False + def get_default_fields(tracker): config = get_config(tracker) @@ -435,17 +462,20 @@ def get_default_fields(tracker): return default_fields + # Utility functions for bugzilla # ============================== class BugParseError(Exception): pass + # A BugHandle is the parsed form of a bug reference string; it # uniquely identifies a bug on a server, though until we try # to load it (and create a Bug) we don't know if it actually exists. class BugHandle: - def __init__(self, host, path, https, id, auth_user=None, auth_password=None, bz_user=None, bz_password=None, use_git_credential=False): + def __init__(self, host, path, https, id, auth_user=None, auth_password=None, bz_user=None, bz_password=None, + use_git_credential=False): self.host = host self.path = path self.https = https @@ -475,7 +505,7 @@ class BugHandle: @staticmethod def parse(bug_reference): - parseresult = urlparse.urlsplit (bug_reference) + parseresult = urllib.parse.urlsplit(bug_reference) if parseresult.scheme in ('http', 'https'): # Catch http://www.gnome.org and the oddball http:relative/path and http:/path @@ -512,7 +542,7 @@ class BugHandle: if bugid is not None: return BugHandle(host=parseresult.hostname, path=base_path, - https=parseresult.scheme=="https", + https=parseresult.scheme == "https", id=bugid, auth_user=user, auth_password=password, @@ -543,13 +573,14 @@ class BugHandle: if not re.match(r"^.*\.[a-zA-Z]{2,}$", host): raise BugParseError("'%s' doesn't look like a valid bugzilla host or alias" % host) - return BugHandle(host=host, path=path, https=https, id=id, auth_user=auth_user, auth_password=auth_password, bz_user=bz_user, bz_password=bz_password, use_git_credential=use_git_credential) + return BugHandle(host=host, path=path, https=https, id=id, auth_user=auth_user, auth_password=auth_password, + bz_user=bz_user, bz_password=bz_password, use_git_credential=use_git_credential) @staticmethod def parse_or_die(str): try: return BugHandle.parse(str) - except BugParseError, e: + except BugParseError as e: die(e.message) def __hash__(self): @@ -559,9 +590,11 @@ class BugHandle: return ((self.host, self.https, self.id) == (other.host, other.https, other.id)) + class CookieError(Exception): pass + def do_get_cookies_from_sqlite(host, cookies_sqlite, browser, query, chromium_time): result = {} # We use a timeout of 0 since we expect to hit the browser holding @@ -570,10 +603,10 @@ def do_get_cookies_from_sqlite(host, cookies_sqlite, browser, query, chromium_ti try: cursor = connection.cursor() - cursor.execute(query, { 'host': host }) + cursor.execute(query, {'host': host}) now = time.time() - for name,value,path,expiry in cursor.fetchall(): + for name, value, path, expiry in cursor.fetchall(): # Excessive caution: toss out values that need to be quoted in a cookie header expiry = float(expiry) if chromium_time: @@ -590,6 +623,7 @@ def do_get_cookies_from_sqlite(host, cookies_sqlite, browser, query, chromium_ti finally: connection.close() + # Firefox 3.5 keeps the cookies database permamently locked; as a workaround # hack, we make a copy, read from that, then delete the copy. Of course, # we may hit an inconsistent state of the database @@ -598,16 +632,17 @@ def get_cookies_from_sqlite_with_copy(host, cookies_sqlite, browser, *args, **kw shutil.copyfile(cookies_sqlite, db_copy) try: return do_get_cookies_from_sqlite(host, db_copy, browser, *args, **kwargs) - except sqlite.OperationalError, e: + except sqlite.OperationalError as e: raise CookieError("Cookie database was locked; temporary copy didn't work") finally: os.remove(db_copy) + def get_cookies_from_sqlite(host, cookies_sqlite, browser, query, chromium_time=False): try: result = do_get_cookies_from_sqlite(host, cookies_sqlite, browser, query, chromium_time=chromium_time) - except sqlite.OperationalError, e: + except sqlite.OperationalError as e: if "database is locked" in str(e): # Try making a temporary copy result = get_cookies_from_sqlite_with_copy(host, cookies_sqlite, browser, query, @@ -621,10 +656,12 @@ def get_cookies_from_sqlite(host, cookies_sqlite, browser, query, chromium_time= return result + def get_cookies_from_sqlite_xulrunner(host, cookies_sqlite, name): return get_cookies_from_sqlite(host, cookies_sqlite, name, "select name,value,path,expiry from moz_cookies where host = :host") + def get_bugzilla_cookies_ff3(host): profiles_dir = os.path.expanduser('~/.mozilla/firefox') profile_path = None @@ -636,7 +673,7 @@ def get_bugzilla_cookies_ff3(host): continue if (not profile_path or - (cp.has_option(section, "Default") and cp.get(section, "Default").strip() == "1")): + (cp.has_option(section, "Default") and cp.get(section, "Default").strip() == "1")): profile_path = os.path.join(profiles_dir, cp.get(section, "Path").strip()) if not profile_path: @@ -648,6 +685,7 @@ def get_bugzilla_cookies_ff3(host): return get_cookies_from_sqlite_xulrunner(host, cookies_sqlite, "Firefox") + def get_bugzilla_cookies_galeon(host): cookies_sqlite = os.path.expanduser('~/.galeon/mozilla/galeon/cookies.sqlite') if not os.path.exists(cookies_sqlite): @@ -655,6 +693,7 @@ def get_bugzilla_cookies_galeon(host): return get_cookies_from_sqlite_xulrunner(host, cookies_sqlite, "Galeon") + def get_bugzilla_cookies_epy(host): # epiphany-webkit migrated the cookie db to a different location, but the # format is the same @@ -673,6 +712,7 @@ def get_bugzilla_cookies_epy(host): return get_cookies_from_sqlite_xulrunner(host, cookies_sqlite, "Epiphany") + # Shared for Chromium and Google Chrome def get_bugzilla_cookies_chr(host, browser, config_dir): config_dir = os.path.expanduser(config_dir) @@ -683,25 +723,30 @@ def get_bugzilla_cookies_chr(host, browser, config_dir): "select name,value,path,expires_utc from cookies where host_key = :host", chromium_time=True) + def get_bugzilla_cookies_chromium(host): return get_bugzilla_cookies_chr(host, "Chromium", '~/.config/chromium/Default') + def get_bugzilla_cookies_google_chrome(host): return get_bugzilla_cookies_chr(host, "Google Chrome", '~/.config/google-chrome/Default') -browsers = { 'firefox3' : get_bugzilla_cookies_ff3, - 'epiphany' : get_bugzilla_cookies_epy, - 'galeon' : get_bugzilla_cookies_galeon, - 'chromium' : get_bugzilla_cookies_chromium, - 'google-chrome': get_bugzilla_cookies_google_chrome } + +browsers = {'firefox3': get_bugzilla_cookies_ff3, + 'epiphany': get_bugzilla_cookies_epy, + 'galeon': get_bugzilla_cookies_galeon, + 'chromium': get_bugzilla_cookies_chromium, + 'google-chrome': get_bugzilla_cookies_google_chrome} + def browser_list(): return ", ".join(sorted(browsers.keys())) + def get_bugzilla_cookies(host): browser = git_config['browser'] if browser in browsers: @@ -711,7 +756,7 @@ def get_bugzilla_cookies(host): try: return do_get_cookies(host) - except CookieError, e: + except CookieError as e: die("""Error getting login cookie from browser: %s @@ -719,6 +764,7 @@ Configured browser: %s (change with 'git config --global bz.browser <value>') Possible browsers: %s""" % (str(e), browser, browser_list())) + # Based on http://code.activestate.com/recipes/146306/ - Wade Leftwich def encode_multipart_formdata(fields, files=None): """ @@ -757,10 +803,12 @@ def encode_multipart_formdata(fields, files=None): content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body + # Cache of constant-responses per bugzilla server # =============================================== -CACHE_EXPIRY_TIME = 3600 * 24 # one day +CACHE_EXPIRY_TIME = 3600 * 24 # one day + class Cache(object): def __init__(self): @@ -793,8 +841,10 @@ class Cache(object): self.cfp.write(f) f.close() + cache = Cache() + # General Utility Functions # ========================= @@ -805,6 +855,7 @@ def make_filename(description): return filename + def edit_file(filename): editor = git.var("GIT_EDITOR") process = Popen(editor + " " + filename, shell=True) @@ -812,6 +863,7 @@ def edit_file(filename): if process.returncode != 0: die("Editor exited with non-zero return code") + def edit_template(template): # Prompts the user to edit the text 'template' and returns list of # lines with comments stripped @@ -824,11 +876,12 @@ def edit_template(template): edit_file(filename) f = open(filename, "r") - lines = filter(lambda x: not x.startswith("#"), f.readlines()) + lines = [x for x in f.readlines() if not x.startswith("#")] f.close() return lines + def split_subject_body(lines): # Splits the first line (subject) from the subsequent lines (body) @@ -842,22 +895,26 @@ def split_subject_body(lines): return subject, "".join(lines[i + 1:]).strip() + def _shortest_unique_abbreviation(full, l): - for i in xrange(1, len(full) + 1): + for i in range(1, len(full) + 1): abbrev = full[0:i] if not any((x != full and x.startswith(abbrev) for x in l)): return abbrev # Duplicate items or one item is a prefix of another raise ValueError("%s has no unique abbreviation in %s" % (full, l)) + def _abbreviation_item_help(full, l): abbrev = _shortest_unique_abbreviation(full, l) return '[%s]%s' % (abbrev, full[len(abbrev):]) + # Return '[a]pple, [pe]ar, [po]tato' def abbreviation_help_string(l): return ", ".join((_abbreviation_item_help(full, l) for full in l)) + # Find the unique element in l that starts with abbrev def expand_abbreviation(abbrev, l): for full in l: @@ -865,6 +922,7 @@ def expand_abbreviation(abbrev, l): return full raise ValueError("No unique abbreviation expansion") + def prompt(message): while True: # Using print here could result in Python adding a stray space @@ -876,6 +934,7 @@ def prompt(message): elif line == 'n' or line == 'N': return False + def prompt_multi(message, options): while True: # Using print here could result in Python adding a stray space @@ -886,13 +945,16 @@ def prompt_multi(message, options): if opt in options: return opt + def die(message): - print >>sys.stderr, message + print(message, file=sys.stderr) sys.exit(1) + def http_auth_header(user, password): return 'Basic ' + base64.encodestring("%s:%s" % (user, password)).strip() + # Classes for bug handling # ======================== @@ -900,25 +962,30 @@ class BugPatch(object): def __init__(self, attach_id): self.attach_id = attach_id + class NoXmlRpcError(Exception): pass + connections = {} + def get_connection(host, https): identifier = (host, https) if not identifier in connections: if https: - connection = httplib.HTTPSConnection(host, 443) + connection = http.client.HTTPSConnection(host, 443) else: - connection = httplib.HTTPConnection(host, 80) + connection = http.client.HTTPConnection(host, 80) connections[identifier] = connection return connections[identifier] + class BugServer(object): - def __init__(self, host, path, https, auth_user=None, auth_password=None, bz_user=None, bz_password=None, use_git_credential=False): + def __init__(self, host, path, https, auth_user=None, auth_password=None, bz_user=None, bz_password=None, + use_git_credential=False): self.host = host self.path = path self.https = https @@ -950,14 +1017,17 @@ class BugServer(object): connection.request("GET", self.path + "/index.cgi", '', headers) res = connection.getresponse() match = re.search(r'name="Bugzilla_login_token"[\s]+value="([^"]*)', res.read()) - login_token = match.group(1) + login_token = match.group(1) headers = dict({}) headers['Cookie'] = login_request_cookie headers['User-Agent'] = "git-bz" if self.use_git_credential: protocol = 'https' if self.https else 'http' - git_credential_input = "protocol={protocol}\nhost={host}\npath={path}\n\n".format(protocol=protocol, host=self.host, path=self.path.lstrip('/')) + git_credential_input = "protocol={protocol}\nhost={host}\npath={path}\n\n".format(protocol=protocol, + host=self.host, + path=self.path.lstrip( + '/')) process = Popen(["git", "credential", "fill"], stdout=PIPE, stdin=PIPE) @@ -972,7 +1042,9 @@ class BugServer(object): # now that we have both token and login request cookie # authentication should now work - connection.request("POST", self.path + "/index.cgi", urllib.urlencode({'Bugzilla_login':bz_user,'Bugzilla_password':bz_password,'Bugzilla_login_token':login_token}), headers) + connection.request("POST", self.path + "/index.cgi", urllib.parse.urlencode( + {'Bugzilla_login': bz_user, 'Bugzilla_password': bz_password, 'Bugzilla_login_token': login_token}), + headers) res = connection.getresponse() if self.use_git_credential: @@ -980,10 +1052,10 @@ class BugServer(object): # successful login match = re.search('Invalid Login', res.read()) if match: - process = Popen(["git", "credential", "reject"], stdin = PIPE) + process = Popen(["git", "credential", "reject"], stdin=PIPE) process.communicate(git_credential_output) else: - process = Popen(["git", "credential", "approve"], stdin = PIPE) + process = Popen(["git", "credential", "approve"], stdin=PIPE) process.communicate(git_credential_output) self.cookiestring = res.getheader('set-cookie') @@ -997,7 +1069,7 @@ class BugServer(object): def send_request(self, method, url, data=None, headers={}): headers = dict(headers) cookies = self.get_cookie_string() - if isinstance(cookies, unicode): + if isinstance(cookies, str): cookies = cookies.encode('UTF-8') headers['Cookie'] = cookies headers['User-Agent'] = "git-bz" @@ -1038,8 +1110,8 @@ class BugServer(object): if new_url in seen_urls or len(seen_urls) >= 10: die("Circular redirect or too many redirects") - old_split = urlparse.urlsplit(url) - new_split = urlparse.urlsplit(new_url) + old_split = urllib.parse.urlsplit(url) + new_split = urllib.parse.urlsplit(new_url) new_https = new_split.scheme == 'https' @@ -1054,9 +1126,8 @@ class BugServer(object): # attachment.cgi, though we alternatively could just exclude # attachment.cgi here. if (response.status in (301, 302) and - method == 'GET' and - old_split.path == '/show_bug.cgi' and new_split.path == '/show_bug.cgi'): - + method == 'GET' and + old_split.path == '/show_bug.cgi' and new_split.path == '/show_bug.cgi'): self.host = new_split.hostname self.https = new_https @@ -1068,13 +1139,13 @@ class BugServer(object): method = 'GET' # Get the relative component of the new URL - url = urlparse.urlunsplit((None, None, new_split.path, new_split.query, new_split.fragment)) + url = urllib.parse.urlunsplit((None, None, new_split.path, new_split.query, new_split.fragment)) else: return response def send_post(self, url, fields, files=None): content_type, body = encode_multipart_formdata(fields, files) - return self.send_request("POST", url, data=body, headers={ 'Content-Type': content_type }) + return self.send_request("POST", url, data=body, headers={'Content-Type': content_type}) def get_xmlrpc_proxy(self): if self._xmlrpc_proxy is None: @@ -1084,42 +1155,42 @@ class BugServer(object): transport = SafeBugTransport(self) else: transport = BugTransport(self) - self._xmlrpc_proxy = xmlrpclib.ServerProxy(uri, transport) + self._xmlrpc_proxy = xmlrpc.client.ServerProxy(uri, transport) return self._xmlrpc_proxy def _product_id(self, product_name): # This way works with newer bugzilla; older Bugzilla doesn't support names: try: - response = self.get_xmlrpc_proxy().Product.get({ 'names': product_name, 'include_fields': ['id', 'name'] }) + response = self.get_xmlrpc_proxy().Product.get({'names': product_name, 'include_fields': ['id', 'name']}) products = response['products'] if len(products) > 0: return products[0]['id'] - except xmlrpclib.Fault, e: + except xmlrpc.client.Fault as e: pass - except xmlrpclib.ProtocolError, e: + except xmlrpc.client.ProtocolError as e: pass # This should work with any bugzilla that supports xmlrpc, but will be slow - print >>sys.stderr, "Searching for product ID ...", + print("Searching for product ID ...", end=' ', file=sys.stderr) try: response = self.get_xmlrpc_proxy().Product.get_accessible_products({}) ids = response['ids'] - response = self.get_xmlrpc_proxy().Product.get_products({ 'ids': ids, 'include_fields': ['id', 'name']}) + response = self.get_xmlrpc_proxy().Product.get_products({'ids': ids, 'include_fields': ['id', 'name']}) for p in response['products']: if p['name'] == product_name: - print >>sys.stderr, "found it" + print("found it", file=sys.stderr) return p['id'] - except xmlrpclib.Fault, e: + except xmlrpc.client.Fault as e: pass - except xmlrpclib.ProtocolError, e: + except xmlrpc.client.ProtocolError as e: pass - print >>sys.stderr, "failed" + print("failed", file=sys.stderr) return None def product_id(self, product_name): - key = 'product_id_' + urllib.quote(product_name) + key = 'product_id_' + urllib.parse.quote(product_name) try: return cache.get(self.host, key) except IndexError: @@ -1132,17 +1203,17 @@ class BugServer(object): # array, or None if the query failed def _legal_values(self, field): try: - response = self.get_xmlrpc_proxy().Bug.legal_values({ 'field': field }) + response = self.get_xmlrpc_proxy().Bug.legal_values({'field': field}) cache.set(self.host, 'legal_' + field, response['values']) return response['values'] - except xmlrpclib.Fault, e: - if e.faultCode == -32000: # https://bugzilla.mozilla.org/show_bug.cgi?id=513511 + except xmlrpc.client.Fault as e: + if e.faultCode == -32000: # https://bugzilla.mozilla.org/show_bug.cgi?id=513511 return None raise - except xmlrpclib.ProtocolError, e: - if e.errcode == 500: # older bugzilla versions die this way + except xmlrpc.client.ProtocolError as e: + if e.errcode == 500: # older bugzilla versions die this way return None - elif e.errcode == 404: # really old bugzilla, no XML-RPC + elif e.errcode == 404: # really old bugzilla, no XML-RPC return None raise @@ -1154,28 +1225,33 @@ class BugServer(object): cache.set(self.host, 'legal_' + field, values) return values + # mixin for xmlrpclib.Transport classes to add cookies class CookieTransportMixin(object): def send_request(self, connection, *args): - xmlrpclib.Transport.send_request(self, connection, *args) + xmlrpc.client.Transport.send_request(self, connection, *args) cookie = self.server.get_cookie_string() - if isinstance(cookie, unicode): + if isinstance(cookie, str): cookie = cookie.encode('UTF-8') connection.putheader("Cookie", cookie) connection.putheader("Authorization", http_auth_header(self.server.auth_user, self.server.auth_password)) -class BugTransport(CookieTransportMixin, xmlrpclib.Transport): + +class BugTransport(CookieTransportMixin, xmlrpc.client.Transport): def __init__(self, server): - xmlrpclib.Transport.__init__(self) + xmlrpc.client.Transport.__init__(self) self.server = server -class SafeBugTransport(CookieTransportMixin, xmlrpclib.SafeTransport): + +class SafeBugTransport(CookieTransportMixin, xmlrpc.client.SafeTransport): def __init__(self, server): - xmlrpclib.SafeTransport.__init__(self) + xmlrpc.client.SafeTransport.__init__(self) self.server = server + servers = {} + # Note that if we detect that we are redirecting, we may rewrite the # host/https of the server to avoid doing too many redirections, and # so the host,https we connect to may be different than what we use @@ -1183,7 +1259,8 @@ servers = {} def get_bug_server(host, path, https, auth_user, auth_password, bz_user, bz_password, use_git_credential): identifier = (host, path, https) if not identifier in servers: - servers[identifier] = BugServer(host, path, https, auth_user, auth_password, bz_user, bz_password, use_git_credential) + servers[identifier] = BugServer(host, path, https, auth_user, auth_password, bz_user, bz_password, + use_git_credential) return servers[identifier] @@ -1201,7 +1278,6 @@ def get_bug_server(host, path, https, auth_user, auth_password, bz_user, bz_pass # that indicate success. Returns the matched regular expression # on success, None otherwise def check_for_success(response, response_data, *args): - if response.status != 200: return False @@ -1212,6 +1288,7 @@ def check_for_success(response, response_data, *args): return None + class Bug(object): def __init__(self, server): self.server = server @@ -1228,7 +1305,7 @@ class Bug(object): response = self.server.send_request("GET", url) if response.status != 200: - die ("Failed to retrieve bug information: %d" % response.status) + die("Failed to retrieve bug information: %d" % response.status) etree = ElementTree() etree.parse(response) @@ -1236,7 +1313,7 @@ class Bug(object): bug = etree.find("bug") error = bug.get("error") if error != None: - die ("Failed to retrieve bug information: %s" % error) + die("Failed to retrieve bug information: %s" % error) self.id = int(bug.find("bug_id").text) self.short_desc = bug.find("short_desc").text @@ -1248,10 +1325,10 @@ class Bug(object): patch_complexity = bug.find("cf_patch_complexity") self.patch_complexity = None if patch_complexity is None else patch_complexity.text depends = bug.findall("dependson") - self.depends = None if depends is None else ' '.join(map(lambda e: e.text, depends)) + self.depends = None if depends is None else ' '.join([e.text for e in depends]) for attachment in bug.findall("attachment"): - if attachment.get("ispatch") == "1" and not attachment.get("isobsolete") == "1" : + if attachment.get("ispatch") == "1" and not attachment.get("isobsolete") == "1": attach_id = int(attachment.find("attachid").text) patch = BugPatch(attach_id) # We have to save fields we might not otherwise care about @@ -1283,13 +1360,13 @@ class Bug(object): url = "/attachment.cgi?bugid=" + str(self.id) + "&action=enter" response = self.server.send_request("GET", url) if response.status != 200: - return None + return None match = re.search(r'name="token" value="([^"]+)', - response.read()) + response.read()) if not match: - return None - return match.group(1) + return None + return match.group(1) def _create_via_xmlrpc(self, product, component, short_desc, comment, default_fields): params = dict() @@ -1297,20 +1374,20 @@ class Bug(object): params['component'] = component params['summary'] = short_desc params['description'] = comment - for (field, value) in default_fields.iteritems(): + for (field, value) in default_fields.items(): params[field] = value try: response = self.server.get_xmlrpc_proxy().Bug.create(params) self.id = response['id'] - except xmlrpclib.Fault, e: + except xmlrpc.client.Fault as e: die(e.faultString) - except xmlrpclib.ProtocolError, e: + except xmlrpc.client.ProtocolError as e: if e.errcode == 404: raise NoXmlRpcError(e.errmsg) else: - print >>sys.stderr, "Problem filing bug via XML-RPC: %s (%d)\n" % (e.errmsg, e.errcode) - print >>sys.stderr, "falling back to form post\n" + print("Problem filing bug via XML-RPC: %s (%d)\n" % (e.errmsg, e.errcode), file=sys.stderr) + print("falling back to form post\n", file=sys.stderr) raise NoXmlRpcError("Communication error") def _create_with_form(self, product, component, short_desc, comment, default_fields): @@ -1322,7 +1399,7 @@ class Bug(object): # post_bug.cgi wants some names that are less congenial than the names # expected in XML-RPC. - for (field, value) in default_fields.iteritems(): + for (field, value) in default_fields.items(): if field == 'severity': field = 'bug_severity' elif field == 'platform': @@ -1348,7 +1425,7 @@ class Bug(object): m = check_for_success(response, response_data, r"<title>\s*Bug\s+([0-9]+)") if not m: - print response_data + print(response_data) die("Failed to create bug, status=%d" % response.status) self.id = int(m.group(1)) @@ -1359,16 +1436,16 @@ class Bug(object): except NoXmlRpcError: self._create_with_form(product, component, short_desc, comment, default_fields) - print "Successfully created" - print "Bug %d - %s" % (self.id, short_desc) - print self.get_url() + print("Successfully created") + print("Bug %d - %s" % (self.id, short_desc)) + print(self.get_url()) def create_patch(self, description, comment, filename, data, obsoletes=[], status='none'): fields = {} fields['bugid'] = str(self.id) token = self.get_attachment_token() if token: - fields['token'] = token + fields['token'] = token fields['action'] = 'insert' fields['ispatch'] = '1' fields['attachments.status'] = status @@ -1378,9 +1455,9 @@ class Bug(object): if obsoletes: # this will produce multiple parts in the encoded data with the # name 'obsolete' for each item in the list - fields['obsolete'] = map(str, obsoletes) + fields['obsolete'] = list(map(str, obsoletes)) - files = { 'data': (filename, 'text/plain; charset=UTF-8', data) } + files = {'data': (filename, 'text/plain; charset=UTF-8', data)} response = self.server.send_post("/attachment.cgi", fields, files) response_data = response.read() @@ -1389,23 +1466,23 @@ class Bug(object): r"<title>\s*Changes\s+Submitted", # Newer bugzilla's, use, instead: r"<title>\s*Attachment\s+\d+\s+added"): - print response_data - die ("Failed to attach patch to bug %d, status=%d" % (self.id, response.status)) + print(response_data) + die("Failed to attach patch to bug %d, status=%d" % (self.id, response.status)) - print "Attached %s" % filename + print("Attached %s" % filename) if global_options.mail: - N=6 + N = 6 tempfile = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(N)) - f = open('/tmp/'+tempfile, 'w') + f = open('/tmp/' + tempfile, 'w') f.write(data) f.close() mlist = "koha-patches@lists.koha-community.org" - str1 = "git send-email --quiet --confirm never --to '" + mlist +"' /tmp/"+tempfile + str1 = "git send-email --quiet --confirm never --to '" + mlist + "' /tmp/" + tempfile import os retvalue = os.system(str1) - print retvalue + print(retvalue) # Update specified fields of a bug; keyword arguments are interpreted # as field_name=value @@ -1420,11 +1497,10 @@ class Bug(object): response_data = response.read() if not check_for_success(response, response_data, r"<title>\s*[Bug]*[\S\s]*processed\s*</title>"): - # Mid-air collisions would be indicated by # "<title>Mid-air collision!</title>" - print response_data - die ("Failed to update bug %d, status=%d" % (self.id, response.status)) + print(response_data) + die("Failed to update bug %d, status=%d" % (self.id, response.status)) # Update specified fields of an attachment; keyword arguments are # interpreted as field_name=value @@ -1447,8 +1523,8 @@ class Bug(object): if patch.status is not None: fields['attachments.status'] = patch.status - for (field, value) in changes.iteritems(): - if field == 'status': # encapsulate oddball form field name + for (field, value) in changes.items(): + if field == 'status': # encapsulate oddball form field name field = 'attachments.status' fields[field] = value @@ -1456,10 +1532,10 @@ class Bug(object): response_data = response.read() if not check_for_success(response, response_data, r"<title>\s*Changes\s+Submitted"): - print response_data - die ("Failed to update attachment %d to bug %d, status=%d" % (patch.attach_id, - self.id, - response.status)) + print(response_data) + die("Failed to update attachment %d to bug %d, status=%d" % (patch.attach_id, + self.id, + response.status)) def get_url(self): return "%s://%s/show_bug.cgi?id=%d" % ("https" if self.server.https else "http", @@ -1468,7 +1544,9 @@ class Bug(object): @staticmethod def load(bug_reference, attachmentdata=False): - server = get_bug_server(bug_reference.host, bug_reference.path, bug_reference.https, bug_reference.auth_user, bug_reference.auth_password, bug_reference.bz_user, bug_reference.bz_password, bug_reference.use_git_credential) + server = get_bug_server(bug_reference.host, bug_reference.path, bug_reference.https, bug_reference.auth_user, + bug_reference.auth_password, bug_reference.bz_user, bug_reference.bz_password, + bug_reference.use_git_credential) bug = Bug(server) bug._load(bug_reference.id, attachmentdata) @@ -1492,25 +1570,27 @@ class Bug(object): return bug + # The Commands # ============= def commit_needs_url(commit, bug_id): - pat = re.compile(r"(?<!\d)%d(?!\d)" % bug_id) - return (pat.search(commit.subject) is None and - pat.search(get_body(commit)) is None) + pat = re.compile(r"(?<!\d)%d(?!\d)" % bug_id) + return (pat.search(commit.subject) is None and + pat.search(get_body(commit)) is None) + def check_add_url(commits, bug_id=None, is_add_url=False): if bug_id != None: # We only need to check the commits that we'll add the URL to commits = [commit for commit in commits if commit_needs_url(commit, bug_id)] - if len(commits) == 0: # Nothing to do + if len(commits) == 0: # Nothing to do return try: git.diff(exit_code=True, ignore_submodules=True, _quiet=True) - git.diff(exit_code=True, ignore_submodules=True, cached=True, _quiet=True) + git.diff(exit_code=True, ignore_submodules=True, cached=True, _quiet=True) except CalledProcessError: die("Cannot add bug reference to commit message(s); You must commit (or stash) all changes first") @@ -1523,13 +1603,13 @@ def check_add_url(commits, bug_id=None, is_add_url=False): # see if the commit is present in any remote branches remote_branches = git.branch(contains=commit.id, r=True) if remote_branches != "": - print commit.id[0:7], commit.subject - print "Commit is already in remote branch(es):", " ".join(remote_branches.split()) + print(commit.id[0:7], commit.subject) + print("Commit is already in remote branch(es):", " ".join(remote_branches.split())) if not prompt("Rewrite the commit add the bug URL anyways?"): if is_add_url: - print "Aborting." + print("Aborting.") else: - print "Aborting. You can use -n/--no-add-url to turn off adding the URL" + print("Aborting. You can use -n/--no-add-url to turn off adding the URL") sys.exit(0) # Check for merge commits @@ -1537,16 +1617,18 @@ def check_add_url(commits, bug_id=None, is_add_url=False): all_commits = rev_list_commits(commits[-1].id + "^..HEAD") for commit in all_commits: if commit_is_merge(commit): - print "Found merge commit:" - print commit.id[0:7], commit.subject - print "Can't rewrite this commit or an ancestor commit to add bug URL" + print("Found merge commit:") + print(commit.id[0:7], commit.subject) + print("Can't rewrite this commit or an ancestor commit to add bug URL") sys.exit(1) + def bad_url_method(add_url_method): die("""add-url-method '%s' is invalid Should be [subject-prepend|subject-append|body-prepend|body-append]:<format>""" % add_url_method) + def add_url_to_subject_body(subject, body, bug): add_url_method = git_config['add-url-method'] if not ':' in add_url_method: @@ -1581,10 +1663,12 @@ def add_url_to_subject_body(subject, body, bug): return subject, body + def validate_add_url_method(bug): # Dry run add_url_to_subject_body("", "", bug) + def add_url_to_head_commit(commit, bug): subject = commit.subject body = get_body(commit) @@ -1594,6 +1678,7 @@ def add_url_to_head_commit(commit, bug): input = subject + "\n\n" + body git.commit(file="-", amend=True, _input=input) + def add_url(bug, commits): commit_map = {} oldest_commit = None @@ -1617,7 +1702,7 @@ def add_url(bug, commits): branch_name = None try: # Detach HEAD from the branch; this gives a cleaner reflog for the branch - print "Moving to starting point" + print("Moving to starting point") git.checkout(oldest_commit.id + "^", q=True) for commit in reversed(all_commits): @@ -1626,14 +1711,14 @@ def add_url(bug, commits): commit = commit_map[commit.id] if commit.id in commit_map and commit_needs_url(commit, bug.id): - print "Adding bug reference ", commit.id[0:7], commit.subject + print("Adding bug reference ", commit.id[0:7], commit.subject) git.cherry_pick(commit.id) add_url_to_head_commit(commit, bug) else: if commit.id in commit_map: - print "Recommitting", commit.id[0:7], commit.subject, "(already has bug #)" + print("Recommitting", commit.id[0:7], commit.subject, "(already has bug #)") else: - print "Recommitting", commit.id[0:7], commit.subject + print("Recommitting", commit.id[0:7], commit.subject) git.cherry_pick(commit.id) # Get the commit ID; we update the commit with the new ID, so we in the case @@ -1645,12 +1730,13 @@ def add_url(bug, commits): branch_name, new_head) git.symbolic_ref("HEAD", branch_name) except: - print "Cleaning up back to original state on error" + print("Cleaning up back to original state on error") git.reset(orig_head, hard=True) if branch_name is not None: git.symbolic_ref("HEAD", branch_name) raise + def do_add_url(bug_reference, commit_or_revision_range): commits = get_commits(commit_or_revision_range) @@ -1658,34 +1744,36 @@ def do_add_url(bug_reference, commit_or_revision_range): check_add_url(commits, bug.id, is_add_url=True) - print "Bug %d - %s" % (bug.id, bug.short_desc) - print bug.get_url() - print + print("Bug %d - %s" % (bug.id, bug.short_desc)) + print(bug.get_url()) + print() found = False for commit in commits: if commit_needs_url(commit, bug.id): - print commit.id[0:7], commit.subject + print(commit.id[0:7], commit.subject) found = True else: - print "SKIPPING", commit.id[0:7], commit.subject + print("SKIPPING", commit.id[0:7], commit.subject) if not found: sys.exit(0) - print + print() if not prompt("Add bug URL to above commits?"): - print "Aborting" + print("Aborting") sys.exit(0) - print + print() add_url(bug, commits) + resolvemsg = '''When you have resolved this problem run "git bz apply --continue". If you would prefer to skip this patch, instead run "git bz apply --skip". To restore the original branch and stop patching run "git bz apply --abort".''' + def do_apply(*args): - git_dir = git.rev_parse(git_dir=True) + git_dir = git.rev_parse(git_dir=True).decode() resuming = global_options.resolved or global_options.skip or global_options.abort do_not_add_url = 0 @@ -1706,7 +1794,7 @@ def do_apply(*args): bug_ref = lines[0] orig_head = lines[1] need_amend = lines[2] == "True" - patch_ids = map(int, lines[3:]) + patch_ids = list(map(int, lines[3:])) f.close() except: die("Not inside a 'git bz apply' operation") @@ -1723,7 +1811,7 @@ def do_apply(*args): try: git.commit(amend=True, _interactive=True) except CalledProcessError: - print >>sys.stderr, "Warning: left dummy commit message" + print("Warning: left dummy commit message", file=sys.stderr) else: if resuming: @@ -1737,7 +1825,7 @@ def do_apply(*args): bug = Bug.load(BugHandle.parse_or_die(bug_ref), attachmentdata=True) if len(bug.patches) == 0: - print("No patches on bug %d" % bug.id) + print(("No patches on bug %d" % bug.id)) return patches = [] @@ -1758,38 +1846,38 @@ def do_apply(*args): continue dep_bug = Bug.load(BugHandle.parse_or_die(d_id), - attachmentdata=False) + attachmentdata=False) s = dep_bug.bug_status - if s == "Needs Signoff" \ - or s == 'Signed Off' \ - or s == 'Failed QA' \ - or s == 'Passed QA' \ - or s == 'BLOCKED': - print "\nBug %s Depends on bug %s (%s)" % ( bug.id, dep_bug.id, s) + if s == "Needs Signoff" \ + or s == 'Signed Off' \ + or s == 'Failed QA' \ + or s == 'Passed QA' \ + or s == 'BLOCKED': + print("\nBug %s Depends on bug %s (%s)" % (bug.id, dep_bug.id, s)) opt = prompt_multi("Follow? [(y)es, (n)o]", ["y", "n"]) if opt == "n": continue else: try: - applied = do_apply(d_id, { 'do_not_add_url': 1} ) + applied = do_apply(d_id, {'do_not_add_url': 1}) bugs_applied.extend(applied) except BaseException: die("\nCannot apply cleanly patches from bug %s. Everything will be left dirty.\ngit bz apply --continue will not continue the process if patches from other bug reports need to be applied." % d_id) orig_head = git.rev_parse("HEAD") - print "\nBug %d - %s" % (bug.id, bug.short_desc) - print + print("\nBug %d - %s" % (bug.id, bug.short_desc)) + print() for patch in bug.patches: if patch.status == 'committed' or patch.status == 'rejected': - print "%d (skipping, %s) - %s" % (patch.attach_id, patch.status, patch.description) + print("%d (skipping, %s) - %s" % (patch.attach_id, patch.status, patch.description)) else: patches.append(patch) for patch in patches: - print "%d - %s" % (patch.attach_id, patch.description) - print + print("%d - %s" % (patch.attach_id, patch.description)) + print() opt = prompt_multi("Apply? [(y)es, (n)o, (i)nteractive]", ["y", "n", "i"]) if opt == "n": @@ -1814,7 +1902,7 @@ def do_apply(*args): match = re.match('^(\d+)', line) if match: pid = int(match.group(1)) - if not patches_by_id.has_key(pid): + if pid not in patches_by_id: die("Unknown attachment id " + pid) patches.append(patches_by_id[pid]) @@ -1824,7 +1912,7 @@ def do_apply(*args): for patch in patches: if re.search(r'(^|\n)From ', patch.data) is None: # Plain diff... rewrite it into something git-am will accept - users = bug.server.get_xmlrpc_proxy().User.get({ 'names': [patch.attacher] })['users'] + users = bug.server.get_xmlrpc_proxy().User.get({'names': [patch.attacher]})['users'] name = users[0]['real_name'] email = users[0]['email'] headers = """From xxx @@ -1852,10 +1940,10 @@ FIXME: need commit message. try: if global_options.signoff: process = git.am("-3", "-s", filename, resolvemsg=resolvemsg, - _interactive=True) + _interactive=True) else: process = git.am("-3", filename, resolvemsg=resolvemsg, - _interactive=True) + _interactive=True) except CalledProcessError: if os.access(git_dir + "/rebase-apply", os.F_OK): # git-am saved its state for an abort or continue, @@ -1875,7 +1963,7 @@ FIXME: need commit message. try: git.commit(amend=True, _interactive=True) except CalledProcessError: - print >>sys.stderr, "Warning: left dummy commit message" + print("Warning: left dummy commit message", file=sys.stderr) if global_options.add_url and not do_not_add_url: # Slightly hacky. We could add the URLs as we go by using @@ -1888,6 +1976,7 @@ FIXME: need commit message. bugs_applied.append(bug_ref) return bug_ref + def strip_bug_url(bug, commit_body): # Strip off the trailing bug URLs we add with -u; we do this before # using commit body in as a comment; doing it by stripping right before @@ -1896,6 +1985,7 @@ def strip_bug_url(bug, commit_body): pattern = "\s*" + re.escape(bug.get_url()) + "\s*$" return re.sub(pattern, "", commit_body) + def edit_attachment_comment(bug, initial_description, initial_body): template = StringIO() template.write("# Attachment to Bug %d - %s\n\n" % (bug.id, bug.short_desc)) @@ -1939,10 +2029,11 @@ def edit_attachment_comment(bug, initial_description, initial_body): lines = edit_template(template.getvalue()) - obsoletes= [] - statuses= [] + obsoletes = [] + statuses = [] patch_complexities = [] depends = [] + def filter_line(line): m = re.match("^\s*Obsoletes\s*:\s*([\d]+)", line) if m: @@ -1962,7 +2053,7 @@ def edit_attachment_comment(bug, initial_description, initial_body): return False return True - lines = filter(filter_line, lines) + lines = list(filter(filter_line, lines)) description, comment = split_subject_body(lines) @@ -1971,6 +2062,7 @@ def edit_attachment_comment(bug, initial_description, initial_body): return description, comment, obsoletes, statuses, patch_complexities, depends + def attach_commits(bug, commits, include_comments=True, edit_comments=False, status='none'): # We want to attach the patches in chronological order commits = list(commits) @@ -1985,7 +2077,9 @@ def attach_commits(bug, commits, include_comments=True, edit_comments=False, sta else: body = None if edit_comments and is_first is True: - description, body, obsoletes, statuses, patch_complexities, depends = edit_attachment_comment(bug, commit.subject, body) + description, body, obsoletes, statuses, patch_complexities, depends = edit_attachment_comment(bug, + commit.subject, + body) else: description = commit.subject obsoletes = [] @@ -2011,16 +2105,17 @@ def attach_commits(bug, commits, include_comments=True, edit_comments=False, sta if len(statuses) > 0 or len(patch_complexities) > 0 or len(depends) > 0: bug.update(**bug_changes) if len(depends) > 0: - print "Updated depends to '%s'" % bug_changes['dependson'] + print("Updated depends to '%s'" % bug_changes['dependson']) if len(patch_complexities) > 0: - print "Updated patch complexity to '%s'" % bug_changes['cf_patch_complexity'] + print("Updated patch complexity to '%s'" % bug_changes['cf_patch_complexity']) if len(statuses) > 0: - print "Updated bug status to '%s'" % bug_changes['bug_status'] + print("Updated bug status to '%s'" % bug_changes['bug_status']) bug.create_patch(description, body, filename, patch, obsoletes=obsoletes, status=status) + def do_attach(*args): if len(args) == 1: commit_or_revision_range = args[0] @@ -2055,15 +2150,15 @@ def do_attach(*args): # For the common case of one attachment don't prompt if we are going # to give them a chance to edit and abort anyways. if len(commits) > 1 or not global_options.edit: - print "Bug %d - %s" % (bug.id, bug.short_desc) - print + print("Bug %d - %s" % (bug.id, bug.short_desc)) + print() for commit in reversed(commits): - print commit.id[0:7], commit.subject + print(commit.id[0:7], commit.subject) - print + print() if not prompt("Attach?"): - print "Aborting" + print("Aborting") sys.exit(0) if global_options.add_url: @@ -2073,11 +2168,12 @@ def do_attach(*args): # bug.update(addselfcc='1') attach_commits(bug, commits, edit_comments=global_options.edit) + # Sort the patches in the bug into categories based on a set of Git # git commits that we're considering to be newly applied. Matching # is done on exact git subject <=> patch description matches. def filter_patches(bug, applied_commits): - newly_applied_patches = dict() # maps to the commit object where it was applied + newly_applied_patches = dict() # maps to the commit object where it was applied obsoleted_patches = set() unapplied_patches = set() @@ -2101,6 +2197,7 @@ def filter_patches(bug, applied_commits): return newly_applied_patches, obsoleted_patches, unapplied_patches + def edit_bug(bug, applied_commits=None, fix_commits=None): if applied_commits is not None: newly_applied_patches, obsoleted_patches, unapplied_patches = filter_patches(bug, applied_commits) @@ -2222,7 +2319,7 @@ def edit_bug(bug, applied_commits=None, fix_commits=None): patch_complexities = [] depends = [] - lines = filter(filter_line, lines) + lines = list(filter(filter_line, lines)) comment = "".join(lines).strip() bug_status = statuses[0] if len(statuses) > 0 else None @@ -2230,8 +2327,9 @@ def edit_bug(bug, applied_commits=None, fix_commits=None): patch_complexity = patch_complexities[0] if len(patch_complexities) > 0 else None depends = ' '.join(depends) if len(depends) > 0 else None - if depends is None and patch_complexity is None and bug_status is None and resolution is None and len(changed_attachments) == 0 and comment == "": - print "No changes, not editing Bug %d - %s" % (bug.id, bug.short_desc) + if depends is None and patch_complexity is None and bug_status is None and resolution is None and len( + changed_attachments) == 0 and comment == "": + print("No changes, not editing Bug %d - %s" % (bug.id, bug.short_desc)) return False if fix_commits is not None: @@ -2271,7 +2369,7 @@ def edit_bug(bug, applied_commits=None, fix_commits=None): # We can add the comment when we submit the attachment change. # Bugzilla will add a helpful notation ad we'll only send out # one set of email - pass # We'll put the comment with the attachment + pass # We'll put the comment with the attachment else: bug_changes['comment'] = comment @@ -2282,7 +2380,7 @@ def edit_bug(bug, applied_commits=None, fix_commits=None): bug_changes['addselfcc'] = '1' bug.update(**bug_changes) - for (attachment_id, status) in changed_attachments.iteritems(): + for (attachment_id, status) in changed_attachments.items(): patch = None if patches_have_status: if legal_statuses: @@ -2300,7 +2398,7 @@ def edit_bug(bug, applied_commits=None, fix_commits=None): if not patch: die("%d is not a valid attachment ID for Bug %d" % (attachment_id, bug.id)) attachment_changes = {} - if comment != "" and not 'comment' in bug_changes: # See above + if comment != "" and not 'comment' in bug_changes: # See above attachment_changes['comment'] = comment if status == 'obsolete': attachment_changes['isobsolete'] = "1" @@ -2308,23 +2406,24 @@ def edit_bug(bug, applied_commits=None, fix_commits=None): attachment_changes['status'] = status bug.update_patch(patch, **attachment_changes) if status == 'obsolete': - print "Marked attachment as obsolete: %s - %s " % (patch.attach_id, patch.description) + print("Marked attachment as obsolete: %s - %s " % (patch.attach_id, patch.description)) else: - print "Changed status of attachment to %s: %s - %s" % (status, patch.attach_id, patch.description) + print("Changed status of attachment to %s: %s - %s" % (status, patch.attach_id, patch.description)) if fix_commits is not None: attach_commits(bug, fix_commits, status='committed') if resolution is not None: - print "Resolved as %s bug %d - %s" % (resolution, bug.id, bug.short_desc) + print("Resolved as %s bug %d - %s" % (resolution, bug.id, bug.short_desc)) elif len(changed_attachments) > 0: - print "Updated bug %d - %s" % (bug.id, bug.short_desc) + print("Updated bug %d - %s" % (bug.id, bug.short_desc)) else: - print "Added comment to bug %d - %s" % (bug.id, bug.short_desc) - print bug.get_url() + print("Added comment to bug %d - %s" % (bug.id, bug.short_desc)) + print(bug.get_url()) return True + LOG_BUG_REFERENCE = re.compile(r""" (\b[Ss]ee\s+(?:[^\s:/]+\s+){0,2})? (?:(https?://[^/]+/show_bug.cgi\?id=[^&\s]+) @@ -2332,6 +2431,7 @@ LOG_BUG_REFERENCE = re.compile(r""" [Bb]ug\s+\#?(\d+)) """, re.VERBOSE | re.DOTALL) + def extract_bugs_from_string(str): refs = [] for m in LOG_BUG_REFERENCE.finditer(str): @@ -2343,7 +2443,7 @@ def extract_bugs_from_string(str): # about some peripherally related bug. So, if the word see # occurs 0 to 2 words before the bug reference, we ignore it. if m.group(1) is not None: - print "Skipping cross-reference '%s'" % m.group(0) + print("Skipping cross-reference '%s'" % m.group(0)) continue if m.group(2) is not None: bug_reference = m.group(2) @@ -2352,8 +2452,9 @@ def extract_bugs_from_string(str): try: yield BugHandle.parse(bug_reference) - except BugParseError, e: - print "WARNING: cannot resolve bug reference '%s'" % bug_reference + except BugParseError as e: + print("WARNING: cannot resolve bug reference '%s'" % bug_reference) + def extract_bugs_from_commit(commit): for handle in extract_bugs_from_string(commit.subject): @@ -2361,6 +2462,7 @@ def extract_bugs_from_commit(commit): for handle in extract_bugs_from_string(get_body(commit)): yield handle + # Yields bug, [<list of commits where it is referenced>] for each bug # referenced in the list of commits. The order of bugs is the same as the # order of their first reference in the list of commits @@ -2378,6 +2480,7 @@ def extract_and_collate_bugs(commits): for bug in bugs: yield bug, bug_to_commits[bug] + def do_edit(bug_reference_or_revision_range): try: handle = BugHandle.parse(bug_reference_or_revision_range) @@ -2387,7 +2490,7 @@ def do_edit(bug_reference_or_revision_range): die("--fix requires commits to be specified") bug = Bug.load(handle) edit_bug(bug) - except BugParseError, e: + except BugParseError as e: try: commits = get_commits(bug_reference_or_revision_range) except CalledProcessError: @@ -2407,6 +2510,7 @@ def do_edit(bug_reference_or_revision_range): else: edit_bug(bug) + PRODUCT_COMPONENT_HELP = """ Use: @@ -2416,6 +2520,7 @@ Use: to configure a default product and/or component for this module.""" + def do_file(*args): if len(args) == 1: product_component, commit_or_revision_range = None, args[0] @@ -2453,13 +2558,13 @@ def do_file(*args): bug_references = [c for c in extract_and_collate_bugs(commits)] if len(bug_references) > 0: - print ("Found existing bug reference%s in commit message%s:" % + print(("Found existing bug reference%s in commit message%s:" % ("" if len(bug_references) == 1 else "s", - "" if len(commits) == 1 else "s")) + "" if len(commits) == 1 else "s"))) for reference, _ in bug_references: - print " ", reference.get_url() + print(" ", reference.get_url()) if not prompt("File anyway?"): - print "Aborting" + print("Aborting") sys.exit(0) if global_options.add_url: @@ -2476,7 +2581,7 @@ def do_file(*args): # Product: %(product)s # Component: %(component)s # Patches to be attached: -""" % { 'product': product, 'component': component }) +""" % {'product': product, 'component': component}) for commit in reversed(commits): template.write("# " + commit.id[0:7] + " " + commit.subject + "\n") @@ -2490,7 +2595,7 @@ def do_file(*args): # If we have only one patch and no other description for the bug was # specified, use the body of the commit as the the description for # the bug rather than the descriptionfor the attachment - include_comments=True + include_comments = True if len(commits) == 1: if description == "": description = get_body(commits[0]) @@ -2503,6 +2608,7 @@ def do_file(*args): attach_commits(bug, commits, include_comments=include_comments) + def run_push(*args, **kwargs): # Predicting what 'git pushes' pushes based on the command line # would be extraordinarily complex, but the interactive output goes @@ -2515,13 +2621,13 @@ def run_push(*args, **kwargs): if global_options.force: options['force'] = True try: - options['_return_stderr']=True + options['_return_stderr'] = True out, err = git.push(*args, **options) except CalledProcessError: return if not dry: # Echo the output so the user gets feedback about what happened - print >>sys.stderr, err + print(err, file=sys.stderr) commits = [] for line in err.strip().split("\n"): @@ -2549,6 +2655,7 @@ def run_push(*args, **kwargs): return unique_commits + def do_push(*args): if global_options.fix: handle = BugHandle.parse_or_die(global_options.fix) @@ -2558,7 +2665,7 @@ def do_push(*args): # We need to add the URLs to the commits before we push # We need to push in order to find out what commits we are pushing # So, we push --dry first - options = { 'dry' : True } + options = {'dry': True} commits = run_push(*args, **options) if edit_bug(bug, fix_commits=commits): run_push(*args) @@ -2568,6 +2675,7 @@ def do_push(*args): bug = Bug.load(handle) edit_bug(bug, commits) + def do_components(*args): tracker = get_tracker() host = resolve_host_alias(tracker) @@ -2596,12 +2704,13 @@ def do_components(*args): response = server.get_xmlrpc_proxy().Bug.legal_values({'product_id': product_id, 'field': 'component'}) components = response['values'] for component in components: - print component - except xmlrpclib.Fault, e: + print(component) + except xmlrpc.client.Fault as e: die(e.faultString) - except xmlrpclib.ProtocolError, e: + except xmlrpc.client.ProtocolError as e: die("Unable to retrieve components: %s" % e.errmsg) + ################################################################################ init_git_config() @@ -2617,28 +2726,34 @@ parser = optparse.OptionParser() parser.add_option("-b", "--bugzilla", metavar="<host or alias>", help="bug tracker to use") + def add_add_url_options(): parser.add_option("-u", "--add-url", action="store_true", help="rewrite commits to add the bug URL [default]") parser.add_option("-n", "--no-add-url", action="store_false", dest="add_url", help="don't rewrite commits to add the bug URL") + def add_edit_option(): parser.add_option("-e", "--edit", action="store_true", help="allow editing the bugzilla comment") + def add_mail_option(): parser.add_option("-m", "--mail", action="store_true", help="send email") + def add_fix_option(): parser.add_option("", "--fix", metavar="<bug reference>", help="attach commits and close bug") + def add_signoff_option(): parser.add_option("-s", "--signoff", action="store_true", help="sign off when applying") + if command == 'add-url': parser.set_usage("git bz add-url [options] <bug reference> (<commit> | <revision range>)"); min_args = max_args = 2 @@ -2689,9 +2804,9 @@ elif command == 'push': parser.add_option("-f", "--force", action="store_true", help="allow non-fast-forward commits") min_args = 0 - max_args = 1000 # no max + max_args = 1000 # no max else: - print >>sys.stderr, "Usage: git bz [add-url|apply|attach|components|edit|file|push] [options]" + print("Usage: git bz [add-url|apply|attach|components|edit|file|push] [options]", file=sys.stderr) sys.exit(1) global_options, args = parser.parse_args() |