Initial gitzilla commit.

master
Devendra Gera 2010-03-07 03:35:51 +05:30
commit c0abda84b7
9 changed files with 708 additions and 0 deletions

4
MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include *.txt
include *.py
recursive-include etc *

82
Makefile Normal file
View File

@ -0,0 +1,82 @@
NAME=gitzilla
PY2DSC=$(shell which py2dsc)
BUILDCMD=$(shell which dpkg-buildpackage)
DEPENDENCIES=python-setuptools
BUILDOPTS= -rfakeroot -uc -us
VERSION:=$(shell perl -ne 'print $$1 if /version=.(\S+).,/' setup.py)
ETC_DIR=/etc
define CONFIG_REPLACEMENT_STUB
binary: build \
\n\tdh install --until dh_install\
\n\tmkdir -p debian/python-${NAME}${ETC_DIR}\
\n\tcp etc/* debian/python-${NAME}${ETC_DIR}\
\n\tdh install --after dh_install\
\n\tdh binary
endef
DEPS_REPLACEMENT:="s/$$/, ${DEPENDENCIES}/ if /^Depends: /"
CONF_REPLACEMENT:="s(binary: build.*$$)(${CONFIG_REPLACEMENT_STUB})"
.PHONY: check-prerequisites deb debianize-source edit-source build clean bumpversion
deb: check-prerequisites build
$(info collecting packages ...)
@mkdir -p debian
@mv ${TARGETDIR}/deb_dist/python-${NAME}*.deb debian/
@mv ${TARGETDIR}/deb_dist/*.orig.tar.gz debian/
@mv ${TARGETDIR}/deb_dist/*.diff.gz debian/
@mv ${TARGETDIR}/deb_dist/*.dsc debian/
@rm -rf ${TARGETDIR}
dist/${NAME}-${VERSION}.tar.gz: setup.py
$(info preparing source distribution ...)
@python setup.py sdist
@rm -rf ${NAME}.egg-info
debianize-source: dist/${NAME}-${VERSION}.tar.gz
$(info debianizing ${NAME} via py2dsc ...)
@cd ${TARGETDIR} ; \
cp ${CURDIR}/dist/${NAME}-${VERSION}.tar.gz . ; \
${PY2DSC} *.tar.gz ;
edit-source: debianize-source
$(info changing build dependencies for ${NAME} ...)
@cd ${TARGETDIR}/deb_dist/${NAME}-${VERSION} ; \
perl -pi -e 's/python-all-dev/python-dev/' debian/control ; \
perl -pi -e ${DEPS_REPLACEMENT} debian/control ; \
BUILD_STR=`grep 'binary: build' debian/rules 2>/dev/null` ; \
if [ x"$$BUILD_STR" == x ]; then \
echo -e "${CONFIG_REPLACEMENT_STUB}" >> debian/rules ; \
else \
perl -pi -e ${CONF_REPLACEMENT} debian/rules ; \
fi
build: edit-source
$(info building ${NAME} version ${VERSION} ...)
@cd ${TARGETDIR}/deb_dist/${NAME}-${VERSION} ; \
${BUILDCMD} ${BUILDOPTS}
check-prerequisites:
ifeq ($(PY2DSC), )
$(error no py2dsc found)
else ifeq ($(BUILDCMD), )
$(error no dpkg-buildpackage found)
else ifeq ($(VERSION), )
$(error could not determine version from setup.py)
else
TARGETDIR:=$(shell mktemp -d)
endif
clean:
@rm -rf ${NAME}.egg-info
@rm -rf dist
@rm -rf ${TARGETDIR}
@rm -rf debian
bumpversion:
@${EDITOR} setup.py

41
README Normal file
View File

@ -0,0 +1,41 @@
GitZilla
========
GitZilla is Python magic to support Git-Bugzilla integration. There are
various ways of using GitZilla.
Note that GitZilla must be installed on the machine receiving commits from
everyone - home to the the "official" or the "central" repository.
Simple ready scripts
--------------------
To quickly start using GitZilla:
* Install GitZilla. You may choose the .deb for easy installation on
Debian/Ubuntu systems. Otherwise, just unpack the source and install in
the usual setuptools way:
sudo python setup.py install
* Switch to the hooks directory (/path/to/repository/.git/hooks) and delete
the 'post-receive' and 'update' hooks.
* Link (or copy) the gitzilla provided hooks:
ln -s $(which gitzilla-post-receive) post-receive
ln -s $(which gitzilla-update) update
* Read and edit the config file at /etc/gitzillarc
* Commit away!
Custom GitZilla
---------------
Coming soon. Till then, import the modules gitzilla and gitzilla.hooks in a
python interactive shell and dooc at the module help.

59
__init__.py Normal file
View File

@ -0,0 +1,59 @@
"""
GitZilla
Git-Bugzilla integration in a Python module.
Requirements
------------
- Python (tested with 2.6, should work with >= 2.5)
- pybugz (tested with 0.8.0)
pybugs can be obtained from http://github.com/ColdWind/pybugz/downloads
"""
__version__ = '1.0'
__author__ = 'Devendra Gera <gera@theoldmonk.net>'
__license__ = """Copyright 2010, Devendra Gera <gera@theoldmonk.net>,
All rights reserved.
This is Free Software, released under the terms of the GNU General Public
License, version 3. A copy of the license can be obtained by emailing the
author, or from http://www.gnu.org/licenses/gpl-3.0.html
As noted in the License, this software does not come with any warranty,
explicit or implied, to the extent permissible by law.
This program might, and would be buggy. Use it at your own risk.
"""
sDefaultSeparator = "~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~."
sDefaultFormatSpec = """
commit %H
parents %P
Author %aN (%aE)
Date %aD
Commit By %cN (%cE)
Commit Date %cD
%s
%b
""".replace("\n", "%n")
import re
oDefaultBugRegex = re.compile(r"bug\s*(?:#|)\s*(?P<bug>\d+)",
re.MULTILINE | re.DOTALL | re.IGNORECASE)
import logging
class NullHandler(logging.Handler):
def emit(self, record):
pass
NullLogger = logging.getLogger("gitzilla")
NullLogger.addHandler(NullHandler())

90
etc/gitzillarc Normal file
View File

@ -0,0 +1,90 @@
# The gitzilla config file.
# This file is in the ConfigParser format. This is something like:
#
# [Section_Name]
# item: value
# another_item: value2
#
# Each git repository should have it's own section. The global config
# at /etc/gitzillarc MUST specify bugzilla_url, bugzilla_user and
# bugzilla_password
#
# The bugzilla_user and bugzilla_password may be overridden by user
# specific config files at ~/.gitzillarc
#
# Note that the global config would be readable by all and is required
# to contain a username/password for bugzilla. If you think this is a
# problem, you could put in dummy values. In that case however, users
# MUST put their own credentials in their ~/.gitzillarc files if the
# update hook is being used with the allowed_bug_states option set. If
# that is not the case, users can get away by not having their
# credentials available, and their commits would not show up in bugziila
# comments.
#
# The format of the user specific files is the same, and they must
# have a section for each repository to be configured.
#
# Mandatory values for /etc/gitzillarc:
#
# * bugzilla_url
# * bugzilla_user
# * bugzilla_password
#
#
# Optional values for /etc/gitzillarc:
#
# * allowed_bug_states
#
# a comma separated set of states that a bug must be in, in order
# for the commit to be allowed by the update hook. If this is set,
# working bugzilla credentials are required.
#
# * formatspec
#
# appended to '--format=format:' in 'git whatchanged'. See the
# 'git whatchanged' manpage for more info.
#
# * separator
#
# a string which would never occur in commit messages. You should
# not need to set this, as it is already at a dafe default.
#
# * bug_regex
#
# the (Python) regex for capturing bug numbers. MUST capture all
# the digits of the bug id in a named group called 'bug'. This
# regex is compiled internally with the MULTILINE, DOTALL, and
# IGNORECASE options set. The default regex captures the
# following forms:
# - bug 123
# - Bug # 123
# - BUG123
# - bug# 123
# - Bug #123
#
# * logfile
#
# the file to log to. Must be writable by the uid of the git
# process. In case of ssh pushes, it usually means that it should
# be writable by all.
#
# * loglevel
#
# can be 'info' or 'debug' - defaults to 'debug'.
#
#
#
# The user specific files are entirely optional. The only values
# they override are bugzilla_user and bugzilla_password.
#
# sample configuration:
#
# [/path/to/repository/.git]
# bugzilla_url: https://repo.example.com/bugzilla/
# bugzilla_user: foo@example.com
# bugzilla_password: barbarblah

173
hooks.py Normal file
View File

@ -0,0 +1,173 @@
"""
hooks - git hooks provided by gitzilla.
"""
import re
import sys
from utils import get_changes, post_to_bugzilla, get_bug_status, notify_and_exit
from gitzilla import sDefaultSeparator, sDefaultFormatSpec, oDefaultBugRegex
from gitzilla import NullLogger
import bugz.bugzilla
import traceback
def post_receive(sBZUrl, sBZUser, sBZPasswd, sFormatSpec=None, oBugRegex=None, sSeparator=None, logger=None):
"""
a post-recieve hook handler which extracts bug ids and adds the commit
info to the comment. If multiple bug ids are found, the comment is added
to each of those bugs.
sBZUrl is the base URL for the Bugzilla installation.
oBugRegex specifies the regex used to search for the bug id in the commit
messages. It MUST provide a named group called 'bug' which contains the bug
id (all digits only). If oBugRegex is None, a default bug regex is used,
which is:
r"bug\s*(?:#|)\s*(?P<bug>\d+)"
This matches forms such as:
- bug 123
- bug #123
- BUG # 123
- Bug#123
- bug123
The format spec is appended to "--format=format:" and passed to
"git whatchanged". See the git whatchanged manpage for more info on the
format spec.
If sFormatSpec is None, a default format spec is used.
The separator is a string that would never occur in a commit message.
If sSeparator is None, a default separator is used, which should be
good enough for everyone.
If a logger is provided, it would be used for all the logging. If logger
is None, logging will be disabled. The logger must be a Python
logging.Logger instance.
"""
if sFormatSpec is None:
sFormatSpec = sDefaultFormatSpec
if sSeparator is None:
sSeparator = sDefaultSeparator
if oBugRegex is None:
oBugRegex = oDefaultBugRegex
if logger is None:
logger = NullLogger
sPrevRev = None
for sLine in iter(sys.stdin.readline, ""):
(sOldRev, sNewRev, sRefName) = sLine.split(" ")
if sPrevRev is None:
sPrevRev = sOldRev
logger.debug("oldrev: '%s', newrev: '%s'", (sOldRev, sNewRev))
asChangeLogs = get_changes(sOldRev, sNewRev, sFormatSpec, sSeparator)
for sMessage in asChangeLogs:
logger.debug("Considering commit:\n%s", (sMessage,))
oMatch = re.search(oBugRegex, sMessage)
if oMatch is None:
logger.info("Bug id not found in commit:\n%s", (sMessage,))
continue
for oMatch in re.finditer(oBugRegex, sMessage):
iBugId = int(oMatch.group("bug"))
logger.debug("Found bugid %d", (iBugId,))
try:
post_to_bugzilla(iBugId, sMessage, sBZUrl, sBZUser, sBZPasswd)
except Exception, e:
logger.exception("Could not add comment to bug %d", (iBugId,))
def update(oBugRegex=None, asAllowedStatuses=None, sSeparator=None, sBZUrl=None, sBZUser=None, sBZPasswd=None, logger=None):
"""
an update hook handler which rejects commits without a bug reference.
This looks at the sys.argv array, so make sure you don't modify it before
calling this function.
oBugRegex specifies the regex used to search for the bug id in the commit
messages. It MUST provide a named group called 'bug' which contains the bug
id (all digits only). If oBugRegex is None, a default bug regex is used,
which is:
r"bug\s*(?:#|)\s*(?P<bug>\d+)"
This matches forms such as:
- bug 123
- bug #123
- BUG # 123
- Bug#123
- bug123
asAllowedStatuses is an array containing allowed statuses for the found
bugs. If a bug is not in one of these states, the commit will be rejected.
If asAllowedStatuses is None, status checking is diabled.
The separator is a string that would never occur in a commit message.
If sSeparator is None, a default separator is used, which should be
good enough for everyone.
If status checking is enabled, sBZUrl specifies the base URL for the
Bugzilla installation.
If a logger is provided, it would be used for all the logging. If logger
is None, logging will be disabled. The logger must be a Python
logging.Logger instance.
"""
if oBugRegex is None:
oBugRegex = oDefaultBugRegex
if sSeparator is None:
sSeparator = sDefaultSeparator
if logger is None:
logger = NullLogger
sFormatSpec = sDefaultFormatSpec
if asAllowedStatuses is not None:
# sanity checking
for item in (sBZUrl, sBZUser):
if item is None:
raise ValueError("Bugzilla info required for status checks")
# create and cache bugzilla instance
oBZ = bugz.bugzilla.Bugz(sBZUrl, user=sBZUser, password=sBZPasswd)
(sOldRev, sNewRev) = sys.argv[2:4]
logger.debug("oldrev: '%s', newrev: '%s'", (sOldRev, sNewRev))
asChangeLogs = get_changes(sOldRev, sNewRev, sFormatSpec, sSeparator)
for sMessage in asChangeLogs:
logger.debug("Checking for bug refs in commit:\n%s", (sMessage,))
oMatch = re.search(oBugRegex, sMessage)
if oMatch is None:
logger.error("No bug ref found in commit:\n%s", (sMessage,))
print "No bug ref found in commit:\n%s" % (sMessage,)
sys.exit(1)
else:
if asAllowedStatuses is not None:
# check all bug statuses
for oMatch in re.finditer(oBugRegex, sMessage):
iBugId = int(oMatch.group("bug"))
logger.debug("Found bug id %d", (iBugId,))
try:
sStatus = get_bug_status(oBZ, iBugId)
if sStatus is None:
notify_and_exit("Bug %d does not exist" % (iBugId,))
except Exception, e:
logger.exception("Could not get status for bug %d", (iBugId,))
notify_and_exit("Could not get staus for bug %d" % (iBugId,))
logger.debug("status for bug %d is %s", (iBugId, sStatus))
if sStatus not in asAllowedStatuses:
logger.info("Cannot accept commit for bug %d in state %s", (iBugId, sStatus))
notify_and_exit("Bug %d['%s'] is not in %s" % (iBugId, sStatus, asAllowedStatuses))

138
hookscripts.py Normal file
View File

@ -0,0 +1,138 @@
"""
hookscripts - ready to use hook scripts for gitzilla.
These pick up configuration values from the environment.
"""
import os
import sys
import gitzilla.hooks
import logging
import ConfigParser
def get_or_default(conf, section, option, default=None):
if conf.has_option(section, option):
return conf.get(section, option)
return None
def get_bz_data(siteconfig, userconfig):
sRepo = os.getcwd()
try:
sBZUrl = siteconfig.get(sRepo, "bugzilla_url")
sBZUser = siteconfig.get(sRepo, "bugzilla_user")
sBZPasswd = siteconfig.get(sRepo, "bugzilla_password")
except:
print "missing/incomplete bugzilla conf"
sys.exit(1)
if userconfig.has_section(sRepo):
if userconfig.has_option(sRepo, "bugzilla_user") and \
userconfig.has_option(sRepo, "bugzilla_password"):
sBZUser = userconfig.get(sRepo, "bugzilla_user")
sBZPasswd = userconfig.get(sRepo, "bugzilla_password")
return (sBZUrl, sBZUser, sBZPasswd)
def get_logger(siteconfig):
sRepo = os.getcwd()
logger = None
if siteconfig.has_option(sRepo, "logfile"):
logger = logging.getLogger("gitzilla")
logger.addHandler(logging.FileHandler(siteconfig.get(sRepo, "logfile")))
# default to debug, but switch to info if asked.
sLogLevel = get_or_default(siteconfig, sRepo, "loglevel", "debug")
logger.setLevel({"info": logging.INFO}.get(sLogLevel, logging.DEBUG))
return logger
def get_bug_regex(siteconfig):
sRepo = os.getcwd()
oBugRegex = None
if siteconfig.has_option(sRepo, "bug_regex"):
oBugRegex = re.compile(siteconfig.get(sRepo, "bug_regex"),
re.MULTILINE | re.DOTALL | re.IGNORECASE)
return oBugRegex
def post_receive():
"""
The gitzilla-post-receive hook script.
The configuration is picked up from /etc/gitzillarc and ~/.gitzillarc
The user specific configuration is allowed to override the bugzilla
username and password.
"""
siteconfig = ConfigParser.SafeConfigParser()
siteconfig.readfp(file("/etc/gitzillarc"))
sRepo = os.getcwd()
if not siteconfig.has_section(sRepo):
print "No %s section found in /etc/gitzillarc" % (sRepo,)
sys.exit(1)
userconfig = ConfigParser.SafeConfigParser()
userconfig.read(os.path.expanduser("~/.gitzillarc"))
(sBZUrl, sBZUser, sBZPasswd) = get_bz_data(siteconfig, userconfig)
logger = get_logger(siteconfig)
oBugRegex = get_bug_regex(siteconfig)
sSeparator = get_or_default(siteconfig, sRepo, "separator")
sFormatSpec = get_or_default(siteconfig, sRepo, "formatspec")
gitzilla.hooks.post_receive(sBZUrl, sBZUser, sBZPasswd, sFormatSpec,
oBugRegex, sSeparator, logger)
def update():
"""
The gitzilla-update hook script.
The configuration is picked up from /etc/gitzillarc and ~/.gitzillarc
The user specific configuration is allowed to override the bugzilla
username and password.
"""
siteconfig = ConfigParser.SafeConfigParser()
siteconfig.readfp(file("/etc/gitzillarc"))
sRepo = os.getcwd()
if not siteconfig.has_section(sRepo):
print "No %s section found in /etc/gitzillarc" % (sRepo,)
sys.exit(1)
logger = get_logger(siteconfig)
oBugRegex = get_bug_regex(siteconfig)
sSeparator = get_or_default(siteconfig, sRepo, "separator")
sFormatSpec = get_or_default(siteconfig, sRepo, "formatspec")
sBZUrl = None
sBZUser = None
sBZPasswd = None
asAllowedStatuses = None
if siteconfig.has_option(sRepo, "allowed_bug_states"):
asAllowedStatuses = map(lambda x: x.strip(),
siteconfig.get(sRepo, "allowed_bug_states").split(","))
# and then we need the bugzilla info as well.
userconfig = ConfigParser.SafeConfigParser()
userconfig.read(os.path.expanduser("~/.gitzillarc"))
(sBZUrl, sBZUser, sBZPasswd) = get_bz_data(siteconfig, userconfig)
gitzilla.hooks.update(oBugRegex, asAllowedStatuses, sSeparator,
sBZUrl, sBZUser, sBZPasswd, logger)

29
setup.py Normal file
View File

@ -0,0 +1,29 @@
# fix broken behaviour - hardlinks are a FS attribute, not an OS attribute
# without doing this, the builds fail on encfs, AFS, NFS etc.
import os
if hasattr(os, 'link'):
delattr(os, 'link')
from setuptools import setup
args = dict(
name='gitzilla',
description='Git-Bugzilla integration',
author='Devendra Gera',
author_email='gera@theoldmonk.net',
url='http://www.theoldmonk.net/gitzilla/',
version='1.0',
requires=['pybugz'],
package_dir={'gitzilla': '.'},
packages=['gitzilla'],
package_data={'': ['etc/*']},
include_package_data=True,
entry_points={
'console_scripts': [
'gitzilla-post-receive = gitzilla.hookscripts:post_receive',
'gitzilla-update = gitzilla.hookscripts:update',
],
}
)
setup(**args)

92
utils.py Normal file
View File

@ -0,0 +1,92 @@
"""
utils module for gitzilla
"""
import os
import sys
import subprocess
import bugz.bugzilla
def execute(asCommand, bSplitLines=False, bIgnoreErrors=False):
"""
Utility function to execute a command and return the output.
"""
p = subprocess.Popen(asCommand,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=False,
close_fds=True,
universal_newlines=True,
env=None)
if bSplitLines:
data = p.stdout.readlines()
else:
data = p.stdout.read()
iRetCode = p.wait()
if iRetCode and not bIgnoreErrors:
print >>sys.stderr, 'Failed to execute command: %s\n%s' % (command, data)
sys.exit(-1)
return data
def get_changes(sOldRev, sNewRev, sFormatSpec, sSeparator):
"""
returns an array of chronological changes, between sOldRev and sNewRev,
according to the format spec sFormatSpec.
"""
sChangeLog = execute(["git", "whatchanged",
"--format=format:%s%s" % (sSeparator, sFormatSpec),
"%s..%s" % (sOldRev, sNewRev)])
asChangeLogs = sChangeLog.split(sSeparator)
asChangeLogs.reverse()
return asChangeLogs[:-1]
def post_to_bugzilla(iBugId, sComment, sBZUrl, sBZUser, sBZPasswd):
"""
posts the comment to the given bug id.
"""
for item in (sBZUrl, sBZUser, sBZPasswd):
if item is None:
raise ValueError("Bad bugzilla info")
oBZ = bugz.bugzilla.Bugz(sBZUrl, user=sBZUser, password=sBZPasswd)
oBZ.modify(iBugId, comment=sComment)
def get_bug_status(oBugz, iBugId):
"""
given the bugz.bugzilla.Bugz instance and the bug id, returns the bug
status.
"""
oBug = oBugz.get(iBugId)
if oBug is None:
return None
return oBug.getroot().find("bug/bug_status").text
def notify_and_exit(sMsg):
"""
notifies the error and exits.
"""
print """
======================================================================
Cannot accept commit.
%s
======================================================================
""" % (sMsg,)
sys.exit(1)