summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README15
-rwxr-xr-xgit-bz208
-rw-r--r--git-bz.txt5
3 files changed, 205 insertions, 23 deletions
diff --git a/README b/README
new file mode 100644
index 0000000..1149f33
--- /dev/null
+++ b/README
@@ -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.)
diff --git a/git-bz b/git-bz
index cdaf031..54ad695 100755
--- a/git-bz
+++ b/git-bz
@@ -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':
diff --git a/git-bz.txt b/git-bz.txt
index 02df8d1..0754fd3 100644
--- a/git-bz.txt
+++ b/git-bz.txt
@@ -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
---------------