├── debian ├── compat ├── source │ └── format ├── changelog ├── trac-github.preinst ├── control └── rules ├── .gitignore ├── github ├── __init__.py ├── github.py └── hook.py ├── dev.sh ├── README.md ├── setup.py └── LICENSE /debian/compat: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 1.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | build 3 | dist 4 | -------------------------------------------------------------------------------- /github/__init__.py: -------------------------------------------------------------------------------- 1 | from github import GithubPlugin 2 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo python setup.py install && sudo /etc/init.d/apache2 force-reload 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Github Trac Hook 2 | ================ 3 | 4 | This project is deprecated, please use: 5 | 6 | https://github.com/aaugustin/trac-github 7 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | githubplugin (0.4-1) unstable; urgency=low 2 | 3 | * source package automatically created by stdeb 0.6.0 4 | 5 | -- Dav Glass Fri, 07 Sep 2012 17:51:08 +0200 6 | -------------------------------------------------------------------------------- /debian/trac-github.preinst: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | # This was added by stdeb to workaround Debian #479852. In a nutshell, 6 | # pycentral does not remove normally remove its symlinks on an 7 | # upgrade. Since we're using python-support, however, those symlinks 8 | # will be broken. This tells python-central to clean up any symlinks. 9 | if [ -e /var/lib/dpkg/info/trac-github.list ] && which pycentral >/dev/null 2>&1 10 | then 11 | pycentral pkgremove trac-github 12 | fi 13 | 14 | #DEBHELPER# 15 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: githubplugin 2 | Maintainer: Dav Glass 3 | Section: python 4 | Priority: optional 5 | Build-Depends: python-setuptools (>= 0.6b3), debhelper (>= 7), python-support (>= 0.8.4) 6 | Standards-Version: 3.8.4 7 | 8 | Package: trac-github 9 | Architecture: all 10 | Depends: ${misc:Depends}, ${python:Depends}, python-simplejson (>= 2.0.5), python-git (>= 0.1.6) 11 | XB-Python-Version: ${python:Versions} 12 | Provides: ${python:Provides} 13 | Description: Creates an entry point for a GitHub post-commit hook. 14 | 15 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # This file was automatically generated by stdeb 0.6.0 at 4 | # Fri, 07 Sep 2012 17:51:08 +0200 5 | 6 | # Unset the environment variables set by dpkg-buildpackage. (This is 7 | # necessary because distutils is brittle with compiler/linker flags 8 | # set. Specifically, packages using f2py will break without this.) 9 | unexport CPPFLAGS 10 | unexport CFLAGS 11 | unexport CXXFLAGS 12 | unexport FFLAGS 13 | unexport LDFLAGS 14 | 15 | #exports specified using stdeb Setup-Env-Vars: 16 | export DH_OPTIONS=--buildsystem=python_distutils 17 | 18 | 19 | %: 20 | dh $@ 21 | 22 | 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | # name can be any name. This name will be used to create .egg file. 4 | # name that is used in packages is the one that is used in the trac.ini file. 5 | # use package name as entry_points 6 | 7 | setup( 8 | name='GithubPlugin', 9 | version='0.4', 10 | author='Dav Glass', 11 | author_email='davglass@gmail.com', 12 | description = "Creates an entry point for a GitHub post-commit hook.", 13 | license = """Unknown Status""", 14 | url = "http://github.com/davglass/github-trac/tree/master", 15 | packages = find_packages(exclude=['*.tests*']), 16 | package_data={'github' : []}, 17 | 18 | install_requires = [ 19 | 'simplejson>=2.0.5', 20 | 'GitPython>=0.1.6', 21 | ], 22 | entry_points = { 23 | 'trac.plugins': [ 24 | 'github = github', 25 | 26 | ] 27 | } 28 | 29 | ) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Software License Agreement (BSD License) 2 | 3 | Copyright (c) 2008, Paolo Capriotti . 4 | Copyright (c) 2008, Dav Glass . 5 | All rights reserved. 6 | 7 | Redistribution and use of this software in source and binary forms, with or without modification, are 8 | permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above 11 | copyright notice, this list of conditions and the 12 | following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above 15 | copyright notice, this list of conditions and the 16 | following disclaimer in the documentation and/or other 17 | materials provided with the distribution. 18 | 19 | * The name of Dav Glass may not be used to endorse or promote products 20 | derived from this software without specific prior 21 | written permission of Dav Glass. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 24 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 25 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 29 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 30 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /github/github.py: -------------------------------------------------------------------------------- 1 | from trac.core import * 2 | from trac.config import Option, IntOption, ListOption, BoolOption 3 | from trac.web.api import IRequestFilter, IRequestHandler, Href 4 | from trac.util.translation import _ 5 | from trac.web.api import parse_query_string 6 | 7 | from hook import CommitHook 8 | 9 | import simplejson 10 | 11 | from git import Git 12 | 13 | class GithubPlugin(Component): 14 | implements(IRequestHandler, IRequestFilter) 15 | 16 | 17 | key = Option('github', 'apitoken', '', doc="""Your GitHub API Token found here: https://github.com/account, """) 18 | closestatus = Option('github', 'closestatus', '', doc="""This is the status used to close a ticket. It defaults to closed.""") 19 | browser = Option('github', 'browser', '', doc="""Place your GitHub Source Browser URL here to have the /browser entry point redirect to GitHub.""") 20 | autofetch = Option('github', 'autofetch', '', doc="""Should we auto fetch the repo when we get a commit hook from GitHub.""") 21 | branches = Option('github', 'branches', "all", doc="""Restrict commit hook to these branches. """ 22 | """Defaults to special value 'all', do not restrict commit hook""") 23 | comment_template = Option('github', 'comment_template', "Changeset: {commit[id]}", doc="""This will be appended to your commit message and used as trac comment""") 24 | repo = Option('trac', 'repository_dir', '', doc="""This is your repository dir""") 25 | 26 | def __init__(self): 27 | self.hook = CommitHook(self.env, self.comment_template) 28 | self.env.log.debug("API Token: %s" % self.key) 29 | self.env.log.debug("Browser: %s" % self.browser) 30 | self.processHook = False 31 | 32 | 33 | # IRequestHandler methods 34 | def match_request(self, req): 35 | self.env.log.debug("Match Request") 36 | serve = req.path_info.rstrip('/') == ('/github/%s' % self.key) and req.method == 'POST' 37 | if serve: 38 | self.processHook = True 39 | #This is hacky but it's the only way I found to let Trac post to this request 40 | # without a valid form_token 41 | req.form_token = None 42 | 43 | self.env.log.debug("Handle Request: %s" % serve) 44 | return serve 45 | 46 | def process_request(self, req): 47 | if self.processHook: 48 | self.processCommitHook(req) 49 | 50 | # This has to be done via the pre_process_request handler 51 | # Seems that the /browser request doesn't get routed to match_request :( 52 | def pre_process_request(self, req, handler): 53 | if self.browser: 54 | serve = req.path_info.startswith('/browser') 55 | self.env.log.debug("Handle Pre-Request /browser: %s" % serve) 56 | if serve: 57 | self.processBrowserURL(req) 58 | 59 | serve2 = req.path_info.startswith('/changeset') 60 | self.env.log.debug("Handle Pre-Request /changeset: %s" % serve2) 61 | if serve2: 62 | self.processChangesetURL(req) 63 | 64 | return handler 65 | 66 | 67 | def post_process_request(self, req, template, data, content_type): 68 | return (template, data, content_type) 69 | 70 | 71 | def processChangesetURL(self, req): 72 | self.env.log.debug("processChangesetURL") 73 | browser = self.browser.replace('/tree/master', '/commit/') 74 | 75 | url = req.path_info.replace('/changeset/', '') 76 | if not url: 77 | browser = self.browser 78 | url = '' 79 | 80 | redirect = '%s%s' % (browser, url) 81 | self.env.log.debug("Redirect URL: %s" % redirect) 82 | out = 'Going to GitHub: %s' % redirect 83 | 84 | req.redirect(redirect) 85 | 86 | 87 | def processBrowserURL(self, req): 88 | self.env.log.debug("processBrowserURL") 89 | browser = self.browser.replace('/master', '/') 90 | rev = req.args.get('rev') 91 | 92 | url = req.path_info.replace('/browser', '') 93 | if not rev: 94 | rev = '' 95 | 96 | redirect = '%s%s%s' % (browser, rev, url) 97 | self.env.log.debug("Redirect URL: %s" % redirect) 98 | out = 'Going to GitHub: %s' % redirect 99 | 100 | req.redirect(redirect) 101 | 102 | 103 | 104 | def processCommitHook(self, req): 105 | self.env.log.debug("processCommitHook") 106 | status = self.closestatus 107 | if not status: 108 | status = 'closed' 109 | 110 | data = req.args.get('payload') 111 | branches = (parse_query_string(req.query_string).get('branches') or self.branches).split(',') 112 | self.env.log.debug("Using branches: %s", branches) 113 | 114 | if data: 115 | jsondata = simplejson.loads(data) 116 | ref = jsondata['ref'].split('/')[-1] 117 | 118 | if ref in branches or 'all' in branches: 119 | for i in jsondata['commits']: 120 | self.hook.process(i, status, jsondata) 121 | else: 122 | self.env.log.debug("Not running hook, ref %s is not in %s", ref, branches) 123 | 124 | if self.autofetch: 125 | repo = Git(self.repo) 126 | 127 | try: 128 | repo.execute(['git', 'fetch']) 129 | except: 130 | self.env.log.debug("git fetch failed!") 131 | 132 | 133 | -------------------------------------------------------------------------------- /github/hook.py: -------------------------------------------------------------------------------- 1 | # trac-post-commit-hook 2 | # ---------------------------------------------------------------------------- 3 | # Copyright (c) 2004 Stephen Hansen 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | # ---------------------------------------------------------------------------- 23 | # 24 | # It searches commit messages for text in the form of: 25 | # command #1 26 | # command #1, #2 27 | # command #1 & #2 28 | # command #1 and #2 29 | # 30 | # Instead of the short-hand syntax "#1", "ticket:1" can be used as well, e.g.: 31 | # command ticket:1 32 | # command ticket:1, ticket:2 33 | # command ticket:1 & ticket:2 34 | # command ticket:1 and ticket:2 35 | # 36 | # In addition, the ':' character can be omitted and issue or bug can be used 37 | # instead of ticket. 38 | # 39 | # You can have more then one command in a message. The following commands 40 | # are supported. There is more then one spelling for each command, to make 41 | # this as user-friendly as possible. 42 | # 43 | # close, closed, closes, fix, fixed, fixes 44 | # The specified issue numbers are closed with the contents of this 45 | # commit message being added to it. 46 | # references, refs, ref, addresses, re, see 47 | # The specified issue numbers are left in their current status, but 48 | # the contents of this commit message are added to their notes. 49 | # 50 | # A fairly complicated example of what you can do is with a commit message 51 | # of: 52 | # 53 | # Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12. 54 | # 55 | # This will close #10 and #12, and add a note to #12. 56 | 57 | import re 58 | import os 59 | import sys 60 | from datetime import datetime 61 | 62 | from trac.env import open_environment 63 | from trac.ticket.notification import TicketNotifyEmail 64 | from trac.ticket import Ticket 65 | from trac.ticket.web_ui import TicketModule 66 | # TODO: move grouped_changelog_entries to model.py 67 | from trac.util.text import to_unicode 68 | from trac.util.datefmt import utc 69 | from trac.versioncontrol.api import NoSuchChangeset 70 | from trac.config import Option, IntOption, ListOption, BoolOption 71 | 72 | ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)' 73 | ticket_reference = ticket_prefix + '[0-9]+' 74 | ticket_command = (r'(?P[A-Za-z]*).?' 75 | '(?P%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' % 76 | (ticket_reference, ticket_reference)) 77 | 78 | command_re = re.compile(ticket_command) 79 | ticket_re = re.compile(ticket_prefix + '([0-9]+)') 80 | 81 | class CommitHook: 82 | _supported_cmds = {'close': '_cmdClose', 83 | 'closed': '_cmdClose', 84 | 'closes': '_cmdClose', 85 | 'fix': '_cmdClose', 86 | 'fixed': '_cmdClose', 87 | 'fixes': '_cmdClose', 88 | 'addresses': '_cmdRefs', 89 | 're': '_cmdRefs', 90 | 'references': '_cmdRefs', 91 | 'refs': '_cmdRefs', 92 | 'ref': '_cmdRefs', 93 | 'see': '_cmdRefs'} 94 | 95 | 96 | def __init__(self, env, comment_template): 97 | self.env = env 98 | self.comment_template = comment_template 99 | 100 | def process(self, commit, status, payload): 101 | self.closestatus = status 102 | 103 | self.env.log.debug("Processing Commit: %s", commit['id']) 104 | comment = (commit['message'] 105 | + "\n\n" 106 | + self.comment_template.format(commit=commit,**payload)) 107 | self.env.log.debug("Prepared Comment: %s", comment) 108 | author = commit['author']['name'] 109 | timestamp = datetime.now(utc) 110 | 111 | cmd_groups = command_re.findall(comment) 112 | self.env.log.debug("Function Handlers: %s" % cmd_groups) 113 | 114 | tickets = {} 115 | for cmd, tkts in cmd_groups: 116 | funcname = self.__class__._supported_cmds.get(cmd.lower(), '') 117 | self.env.log.debug("Function Handler: %s" % funcname) 118 | if funcname: 119 | for tkt_id in ticket_re.findall(tkts): 120 | func = getattr(self, funcname) 121 | tickets.setdefault(tkt_id, []).append(func) 122 | 123 | for tkt_id, cmds in tickets.iteritems(): 124 | try: 125 | db = self.env.get_db_cnx() 126 | 127 | ticket = Ticket(self.env, int(tkt_id), db) 128 | for cmd in cmds: 129 | cmd(ticket) 130 | 131 | # determine sequence number... 132 | cnum = 0 133 | tm = TicketModule(self.env) 134 | for change in tm.grouped_changelog_entries(ticket, db): 135 | if change['permanent']: 136 | cnum += 1 137 | 138 | ticket.save_changes(author, comment, timestamp, db, cnum+1) 139 | db.commit() 140 | 141 | tn = TicketNotifyEmail(self.env) 142 | tn.notify(ticket, newticket=0, modtime=timestamp) 143 | except Exception, e: 144 | import traceback 145 | traceback.print_exc(file=sys.stderr) 146 | #print>>sys.stderr, 'Unexpected error while processing ticket ' \ 147 | #'ID %s: %s' % (tkt_id, e) 148 | 149 | 150 | def _cmdClose(self, ticket): 151 | ticket['status'] = self.closestatus 152 | ticket['resolution'] = 'fixed' 153 | 154 | def _cmdRefs(self, ticket): 155 | pass 156 | --------------------------------------------------------------------------------