Skip to content
Browse files

Initial import of GithubPlugin.

Basic functionality implemented:
 * Tickets can be closed and addressed from git commits with the same syntax
   as the SVN post-commit hook.
 * Trivial wsgi proxy to work around the CSRF protection of Trac.
  • Loading branch information...
0 parents commit d3710b1d55dfefaff92e40ea63c71e8398df787d @pcapriotti committed
Showing with 237 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +1 −0 github/__init__.py
  3. +32 −0 github/github.py
  4. +143 −0 github/hook.py
  5. +46 −0 proxy.py
  6. +14 −0 setup.py
1 .gitignore
@@ -0,0 +1 @@
+*.egg-info
1 github/__init__.py
@@ -0,0 +1 @@
+from github import GithubPlugin
32 github/github.py
@@ -0,0 +1,32 @@
+from trac.core import *
+from trac.web import IRequestHandler
+from hook import CommitHook
+import json
+
+GITHUB_KEY = '9953d62bbc3905491971513876218f39'
+
+class GithubPlugin(Component):
+ implements(IRequestHandler)
+
+ def __init__(self):
+ self.hook = CommitHook(self.env)
+
+ # IRequestHandler methods
+ def match_request(self, req):
+ return req.path_info.rstrip('/') == ('/github/%s' % GITHUB_KEY) and req.method == 'POST'
+
+ def process_request(self, req):
+ try:
+ data = json.read(req.read())
+
+ for sha1, commit in data['commits'].items():
+ self.hook.process(commit)
+ req.send_response(200)
+ req.send_header('Content-Type', 'text/plain')
+ req.end_headers()
+ req.write('Hello world!')
+ except json.ReadException, e:
+ req.send_response(400)
+ req.send_header('Content-type', 'text/plain')
+ req.end_headers()
+ req.write(e.message)
143 github/hook.py
@@ -0,0 +1,143 @@
+# trac-post-commit-hook
+# ----------------------------------------------------------------------------
+# Copyright (c) 2004 Stephen Hansen
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+# ----------------------------------------------------------------------------
+#
+# It searches commit messages for text in the form of:
+# command #1
+# command #1, #2
+# command #1 & #2
+# command #1 and #2
+#
+# Instead of the short-hand syntax "#1", "ticket:1" can be used as well, e.g.:
+# command ticket:1
+# command ticket:1, ticket:2
+# command ticket:1 & ticket:2
+# command ticket:1 and ticket:2
+#
+# In addition, the ':' character can be omitted and issue or bug can be used
+# instead of ticket.
+#
+# You can have more then one command in a message. The following commands
+# are supported. There is more then one spelling for each command, to make
+# this as user-friendly as possible.
+#
+# close, closed, closes, fix, fixed, fixes
+# The specified issue numbers are closed with the contents of this
+# commit message being added to it.
+# references, refs, addresses, re, see
+# The specified issue numbers are left in their current status, but
+# the contents of this commit message are added to their notes.
+#
+# A fairly complicated example of what you can do is with a commit message
+# of:
+#
+# Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12.
+#
+# This will close #10 and #12, and add a note to #12.
+
+import re
+import os
+import sys
+from datetime import datetime
+
+from trac.env import open_environment
+from trac.ticket.notification import TicketNotifyEmail
+from trac.ticket import Ticket
+from trac.ticket.web_ui import TicketModule
+# TODO: move grouped_changelog_entries to model.py
+from trac.util.text import to_unicode
+from trac.util.datefmt import utc
+from trac.versioncontrol.api import NoSuchChangeset
+
+ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)'
+ticket_reference = ticket_prefix + '[0-9]+'
+ticket_command = (r'(?P<action>[A-Za-z]*).?'
+ '(?P<ticket>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
+ (ticket_reference, ticket_reference))
+
+command_re = re.compile(ticket_command)
+ticket_re = re.compile(ticket_prefix + '([0-9]+)')
+
+class CommitHook:
+ _supported_cmds = {'close': '_cmdClose',
+ 'closed': '_cmdClose',
+ 'closes': '_cmdClose',
+ 'fix': '_cmdClose',
+ 'fixed': '_cmdClose',
+ 'fixes': '_cmdClose',
+ 'addresses': '_cmdRefs',
+ 're': '_cmdRefs',
+ 'references': '_cmdRefs',
+ 'refs': '_cmdRefs',
+ 'see': '_cmdRefs'}
+
+ def __init__(self, env):
+ self.env = env
+
+ def process(self, commit):
+ msg = commit['message']
+ author = commit['author']['name']
+ timestamp = datetime.now(utc)
+
+ cmd_groups = command_re.findall(msg)
+
+ tickets = {}
+ for cmd, tkts in cmd_groups:
+ funcname = self.__class__._supported_cmds.get(cmd.lower(), '')
+ if funcname:
+ for tkt_id in ticket_re.findall(tkts):
+ func = getattr(self, funcname)
+ tickets.setdefault(tkt_id, []).append(func)
+
+ for tkt_id, cmds in tickets.iteritems():
+ try:
+ db = self.env.get_db_cnx()
+
+ ticket = Ticket(self.env, int(tkt_id), db)
+ for cmd in cmds:
+ cmd(ticket)
+
+ # determine sequence number...
+ cnum = 0
+ tm = TicketModule(self.env)
+ for change in tm.grouped_changelog_entries(ticket, db):
+ if change['permanent']:
+ cnum += 1
+
+ ticket.save_changes(author, msg, timestamp, db, cnum+1)
+ db.commit()
+
+ tn = TicketNotifyEmail(self.env)
+ tn.notify(ticket, newticket=0, modtime=timestamp)
+ except Exception, e:
+ import traceback
+ traceback.print_exc(file=sys.stderr)
+ #print>>sys.stderr, 'Unexpected error while processing ticket ' \
+ #'ID %s: %s' % (tkt_id, e)
+
+
+ def _cmdClose(self, ticket):
+ ticket['status'] = 'closed'
+ ticket['resolution'] = 'fixed'
+
+ def _cmdRefs(self, ticket):
+ pass
46 proxy.py
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+import sys
+import cgi
+from optparse import OptionParser
+from httplib import HTTPConnection
+
+parser = OptionParser()
+parser.add_option('-k', '--key', dest='key',
+ help='Github plugin private key')
+parser.add_option('-t', '--trac', dest='host',
+ help='Trac host',
+ default='localhost')
+parser.add_option('-u', '--url', dest='url',
+ default='/',
+ help='Trac base url')
+parser.add_option('-p', '--port', dest='port',
+ help='Port where to listen',
+ default=8000)
+
+options, args = parser.parse_args(sys.argv[1:])
+
+def proxy(environ, start_response):
+ if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/' + options.key):
+ form_data = environ['wsgi.input']
+ data = cgi.FieldStorage(form_data, environ=environ, keep_blank_values=True)
+ payload = data.getfirst('payload', '')
+
+ try:
+ conn = HTTPConnection(options.host)
+ conn.request('POST', options.url + '/github/' + options.key,
+ body=payload,
+ headers={ 'Content-Type' : 'application/json' })
+ conn.getresponse()
+ except socket.error, e:
+ start_response('500 Server Error', [('Content-Type', 'text/html')])
+ return ['error']
+
+ start_response('200 OK', [('Content-Type', 'text/html')])
+ return ['ok']
+ else:
+ start_response('404 Not Found', [('Content-Type', 'text/html')])
+ return ['not found']
+
+if __name__ == '__main__':
+ from paste import httpserver
+ httpserver.serve(proxy, host='127.0.0.1', port=options.port)
14 setup.py
@@ -0,0 +1,14 @@
+from setuptools import find_packages, setup
+
+# name can be any name. This name will be used to create .egg file.
+# name that is used in packages is the one that is used in the trac.ini file.
+# use package name as entry_points
+
+setup(
+ name='GithubPlugin', version='0.1',
+ packages=find_packages(exclude=['*.tests*']),
+ entry_points = """
+ [trac.plugins]
+ github = github
+ """,
+)

0 comments on commit d3710b1

Please sign in to comment.
Something went wrong with that request. Please try again.