diff options
-rw-r--r-- | README | 15 | ||||
-rwxr-xr-x | git-bz | 208 | ||||
-rw-r--r-- | git-bz.txt | 5 |
3 files changed, 205 insertions, 23 deletions
@@ -0,0 +1,15 @@ +This branch contains a version of git-bz that has been rebased against the +upstream repository at http://git.fishsoup.net/cgit/git-bz/ + +This version of git-bz has been customized for use by the Koha project. +In particular, the following changes have been made compared to the "regular" +git-bz available elsewhere: +1) The authentication code now plays nicely with the Bugzilla instance on + bugs.koha-community.org +2) The ability to update bug statuses to those statuses used by the Koha + project has been added +3) A -m/--mail option will send patches to the koha-patches mailing list when + attaching them to bugs +4) A -s/--signoff option will sign off on patches as they are being applied + (NOTE THAT THIS OPTION IS INTENDED FOR USE WHEN PUSHING BRANCHES. DO + NOT USE THIS OPTION AS AN ALTERNATIVE TO ACTUALLY TESTING CODE.) @@ -82,6 +82,7 @@ import base64 import cPickle as pickle from ConfigParser import RawConfigParser, NoOptionError import httplib +import urllib import optparse import os try: @@ -101,6 +102,12 @@ import urllib import urlparse from xml.etree.cElementTree import ElementTree import base64 +import warnings + +import smtplib +import random +import string + # Globals # ======= @@ -384,6 +391,7 @@ def tracker_get_auth_password(tracker): return config['auth-password'] return None + def merge_default_fields_from_dict(default_fields, d): for key, value in d.iteritems(): if key.startswith("default-"): @@ -392,6 +400,18 @@ def merge_default_fields_from_dict(default_fields, d): 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 get_default_fields(tracker): config = get_config(tracker) @@ -417,13 +437,15 @@ class BugParseError(Exception): # 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): + def __init__(self, host, path, https, id, auth_user=None, auth_password=None, bz_user=None, bz_password=None): self.host = host self.path = path self.https = https self.id = id self.auth_user = auth_user self.auth_password = auth_password + self.bz_user = bz_user + self.bz_password = bz_password # ensure that the path to the bugzilla installation is an absolute path # so that it will still work even if their config option specifies @@ -484,7 +506,9 @@ class BugHandle: https=parseresult.scheme=="https", id=bugid, auth_user=user, - auth_password=password) + auth_password=password, + bz_user=tracker_get_bz_user(parseresult.hostname), + bz_password=tracker_get_bz_password(parseresult.hostname)) colon = bug_reference.find(":") if colon > 0: @@ -502,11 +526,13 @@ class BugHandle: path = tracker_get_path(tracker) auth_user = tracker_get_auth_user(tracker) auth_password = tracker_get_auth_password(tracker) + bz_user = tracker_get_bz_user(tracker) + bz_password = tracker_get_bz_password(tracker) 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) + 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) @staticmethod def parse_or_die(str): @@ -781,7 +807,7 @@ def edit_template(template): handle, filename = tempfile.mkstemp(".txt", "git-bz-") f = os.fdopen(handle, "w") - f.write(template.encode("UTF-8")) + f.write(template.encode("utf-8")) f.close() edit_file(filename) @@ -881,20 +907,32 @@ def get_connection(host, https): return connections[identifier] class BugServer(object): - def __init__(self, host, path, https, auth_user=None, auth_password=None): + def __init__(self, host, path, https, auth_user=None, auth_password=None, bz_user=None, bz_password=None): self.host = host self.path = path self.https = https self.auth_user = auth_user self.auth_password = auth_password + self.bz_password = bz_password + self.bz_user = bz_user - self.cookies = get_bugzilla_cookies(host) + self.cookiestring = '' self._xmlrpc_proxy = None def get_cookie_string(self): - return ("Bugzilla_login=%s; Bugzilla_logincookie=%s" % - (self.cookies['Bugzilla_login'], self.cookies['Bugzilla_logincookie'])) + if self.cookiestring == '': + if self.bz_user and self.bz_password: + connection = get_connection(self.host, self.https) + connection.request("POST", self.path + "/index.cgi", urllib.urlencode({'Bugzilla_login':self.bz_user,'Bugzilla_password':self.bz_password})) + res = connection.getresponse() + self.cookiestring = res.getheader('set-cookie') + connection.close() + else: + self.cookies = get_bugzilla_cookies(host) + self.cookiestring = ("Bugzilla_login=%s; Bugzilla_logincookie=%s" % + (self.cookies['Bugzilla_login'], self.cookies['Bugzilla_logincookie'])) + return self.cookiestring def send_request(self, method, url, data=None, headers={}): headers = dict(headers) @@ -1082,13 +1120,14 @@ servers = {} # 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 # to look up the server. -def get_bug_server(host, path, https, auth_user, auth_password): +def get_bug_server(host, path, https, auth_user, auth_password, bz_user, bz_password): identifier = (host, path, https) if not identifier in servers: - servers[identifier] = BugServer(host, path, https, auth_user, auth_password) + servers[identifier] = BugServer(host, path, https, auth_user, auth_password, bz_user, bz_password) return servers[identifier] + # Unfortunately, Bugzilla doesn't set a useful status code for # form posts. Because it's very confusing to claim we succeeded # but not, we look for text in the response indicating success, @@ -1146,6 +1185,8 @@ class Bug(object): self.resolution = bug.find("resolution").text token = bug.find("token") self.token = None if token is None else token.text + patch_complexity = bug.find("cf_patch_complexity") + self.patch_complexity = None if patch_complexity is None else patch_complexity.text for attachment in bug.findall("attachment"): if attachment.get("ispatch") == "1" and not attachment.get("isobsolete") == "1" : @@ -1172,6 +1213,22 @@ class Bug(object): self.patches.append(patch) + def get_attachment_token(self): + + # Bugzilla now requires a separate token from the attachment creation + # page to create an attachment. There doesn't appear to be an XML + # interface to it, so we scrape away. + url = "/attachment.cgi?bugid=" + str(self.id) + "&action=enter" + response = self.server.send_request("GET", url) + if response.status != 200: + return None + + match = re.search(r'name="token" value="([^"]+)', + response.read()) + if not match: + return None + return match.group(1) + def _create_via_xmlrpc(self, product, component, short_desc, comment, default_fields): params = dict() params['product'] = product @@ -1247,6 +1304,9 @@ class Bug(object): 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['action'] = 'insert' fields['ispatch'] = '1' fields['attachments.status'] = status @@ -1272,6 +1332,19 @@ class Bug(object): print "Attached %s" % filename + if global_options.mail: + N=6 + tempfile = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(N)) + 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 + + import os + retvalue = os.system(str1) + print retvalue + # Update specified fields of a bug; keyword arguments are interpreted # as field_name=value def update(self, **changes): @@ -1334,7 +1407,7 @@ 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) + 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 = Bug(server) bug._load(bug_reference.id, attachmentdata) @@ -1347,9 +1420,11 @@ class Bug(object): path = tracker_get_path(tracker) auth_user = tracker_get_auth_user(tracker) auth_password = tracker_get_auth_password(tracker) + bz_user = tracker_get_bz_user(tracker) + bz_password = tracker_get_bz_password(tracker) default_fields = get_default_fields(tracker) - server = get_bug_server(host, path, https, auth_user, auth_password) + server = get_bug_server(host, path, https, auth_user, auth_password, bz_user, bz_password) bug = Bug(server) bug._create(product, component, short_desc, comment, default_fields) @@ -1680,8 +1755,12 @@ FIXME: need commit message. f.close() try: - process = git.am("-3", filename, resolvemsg=resolvemsg, - _interactive=True) + if global_options.signoff: + process = git.am("-3", "-s", filename, resolvemsg=resolvemsg, + _interactive=True) + else: + process = git.am("-3", filename, resolvemsg=resolvemsg, + _interactive=True) except CalledProcessError: if os.access(git_dir + "/rebase-apply", os.F_OK): # git-am saved its state for an abort or continue, @@ -1733,6 +1812,22 @@ def edit_attachment_comment(bug, initial_description, initial_body): template.write("%sObsoletes: %d - %s\n" % ("" if obsoleted else "#", patch.attach_id, patch.description)) template.write("\n") + template.write("# Current status: %s\n" % bug.bug_status) + template.write("# Status: Needs Signoff\n") + template.write("# Status: Signed Off\n") + template.write("# Status: Passed QA\n") + template.write("# Status: Pushed to Master\n") + template.write("# Status: Pushed to Stable\n") + template.write("\n") + + template.write("# Current patch-complexity: %s\n" % bug.patch_complexity) + template.write("# Patch-complexity: String patch\n") + template.write("# Patch-complexity: Trivial patch\n") + template.write("# Patch-complexity: Small patch\n") + template.write("# Patch-complexity: Medium patch\n") + template.write("# Patch-complexity: Large patch\n") + template.write("\n") + template.write("""# Please edit the description (first line) and comment (other lines). Lines # starting with '#' will be ignored. Delete everything to abort. """) @@ -1742,22 +1837,31 @@ def edit_attachment_comment(bug, initial_description, initial_body): lines = edit_template(template.getvalue()) obsoletes= [] - def filter_obsolete(line): + statuses= [] + patch_complexities = [] + def filter_line(line): m = re.match("^\s*Obsoletes\s*:\s*([\d]+)", line) if m: obsoletes.append(int(m.group(1))) return False - else: - return True + m = re.match("^\s*Status\s*:\s*(.+)", line) + if m: + statuses.append(m.group(1)) + return False + m = re.match("^\s*Patch-complexity\s*:\s*(.+)", line) + if m: + patch_complexities.append(m.group(1)) + return False + return True - lines = filter(filter_obsolete, lines) + lines = filter(filter_line, lines) description, comment = split_subject_body(lines) if description == "": die("Empty description, aborting") - return description, comment, obsoletes + return description, comment, obsoletes, statuses, patch_complexities def attach_commits(bug, commits, include_comments=True, edit_comments=False, status='none'): # We want to attach the patches in chronological order @@ -1772,13 +1876,31 @@ def attach_commits(bug, commits, include_comments=True, edit_comments=False, sta else: body = None if edit_comments: - description, body, obsoletes = edit_attachment_comment(bug, commit.subject, body) + description, body, obsoletes, statuses, patch_complexities = edit_attachment_comment(bug, commit.subject, body) else: description = commit.subject obsoletes = [] for attachment in bug.patches: if attachment.description == commit.subject: obsoletes.append(attachment.attach_id) + statuses = [] + patch_complexities = [] + + bug_changes = {} + + if len(statuses) > 0: + bug_changes['bug_status'] = statuses[0] + + if len(patch_complexities) > 0: + bug_changes['cf_patch_complexity'] = patch_complexities[0] + + if len(statuses) > 0 or len(patch_complexities) > 0: + bug.update(**bug_changes) + if len(patch_complexities) > 0: + 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'] bug.create_patch(description, body, filename, patch, obsoletes=obsoletes, status=status) @@ -1831,7 +1953,7 @@ def do_attach(*args): add_url(bug, commits) # as in edit_bug we need to update the bug first while our token is still valid - bug.update(addselfcc='1') + # 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 @@ -1892,6 +2014,19 @@ def edit_bug(bug, applied_commits=None, fix_commits=None): commit = newly_applied_patches[patch] template.write("Attachment %d pushed as %s - %s\n" % (patch.attach_id, commit.id[0:7], commit.subject)) + template.write("# Status: Needs Signoff\n") + template.write("# Status: Signed Off\n") + template.write("# Status: Passed QA\n") + template.write("# Status: Pushed to Master\n") + template.write("# Status: Pushed to Stable\n") + template.write("# Status: In Discussion\n") + + template.write("# Patch-complexity: String patch\n") + template.write("# Patch-complexity: Trivial patch\n") + template.write("# Patch-complexity: Small patch\n") + template.write("# Patch-complexity: Medium patch\n") + template.write("# Patch-complexity: Large patch\n") + if mark_resolved: template.write("# Comment to keep bug open\n") elif bug.bug_status == "RESOLVED": @@ -1941,22 +2076,34 @@ def edit_bug(bug, applied_commits=None, fix_commits=None): if m: resolutions.append(m.group(1)) return False + m = re.match("^\s*Status\s*:\s*(.+)", line) + if m: + statuses.append(m.group(1)) + return False m = re.match("^\s*(\S+)\s*@\s*(\d+)", line) if m: status = m.group(1) changed_attachments[int(m.group(2))] = status return False + m = re.match("^\s*Patch-complexity\s*:\s*(.+)", line) + if m: + patch_complexities.append(m.group(1)) + return False return True changed_attachments = {} resolutions = [] + statuses = [] + patch_complexities = [] lines = filter(filter_line, lines) comment = "".join(lines).strip() + bug_status = statuses[0] if len(statuses) > 0 else None resolution = resolutions[0] if len(resolutions) > 0 else None + patch_complexity = patch_complexities[0] if len(patch_complexities) > 0 else None - if resolution is None and len(changed_attachments) == 0 and comment == "": + if 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 @@ -1974,6 +2121,12 @@ def edit_bug(bug, applied_commits=None, fix_commits=None): comment = comment.replace(old_id, new_id) bug_changes = {} + if patch_complexity is not None: + bug_changes['cf_patch_complexity'] = patch_complexity + + if bug_status is not None: + bug_changes['bug_status'] = bug_status + if resolution is not None: if legal_resolutions: try: @@ -2341,10 +2494,18 @@ 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 @@ -2363,10 +2524,13 @@ elif command == 'apply': add_add_url_options() min_args = 0 max_args = 1 + add_signoff_option() + elif command == 'attach': parser.set_usage("git bz attach [options] [<bug reference>] (<commit> | <revision range>)"); add_add_url_options() add_edit_option() + add_mail_option() min_args = 1 max_args = 2 elif command == 'components': @@ -27,12 +27,15 @@ applying patches in bugs to your current tree, and closing bugs once you've pushed the fixes publicly can be done completely from the command line without having to go to your web browser. -Authentication for git-bz is done by reading the cookies for the +Authentication for git-bz can be done by reading the cookies for the Bugzilla host from your web browser. In order to do this, git-bz needs to know how to access the cookies for your web browser; git-bz currently is able to do this for Firefox, Epiphany, Galeon and Chromium on Linux. +Alternatively, you can set the bz-user and bz-password in the +git config for each tracker. + EXAMPLE SESSION --------------- |