From 1db0178db74060e403612d812a8eee34ced92a29 Mon Sep 17 00:00:00 2001 From: Bryan Petty Date: Wed, 1 Apr 2015 07:44:45 +0000 Subject: [PATCH 1/6] Added GitHubIssueHook and hook secret verification. Extracted client_* config logic to GitHubMixin as well. It can now be used for any github.* config key, but mostly just helpful for "client_id", "client_secret", and "hook_secret". --- tracext/github.py | 95 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/tracext/github.py b/tracext/github.py index 6b2bcfd..95fbff9 100644 --- a/tracext/github.py +++ b/tracext/github.py @@ -1,4 +1,6 @@ import fnmatch +from hashlib import sha1 +import hmac import json import os import re @@ -14,7 +16,40 @@ from trac.web.auth import LoginModule -class GitHubLoginModule(LoginModule): +class GitHubMixin(object): + + def get_gh_repo(self, reponame): + key = 'repository' if is_default(reponame) else '%s.repository' % reponame + return self.config.get('github', key) + + def get_branches(self, reponame): + key = 'branches' if is_default(reponame) else '%s.branches' % reponame + return self.config.getlist('github', key, sep=' ') + + def _config(self, key): + assert key in ('client_id', 'client_secret', 'hook_secret') + value = self.config.get('github', key) + if re.match('[0-9a-f]+', value): + return value + elif value.isupper(): + return os.environ.get(value, '') + else: + with open(value) as f: + return f.read.strip() + + def verify_signature(self, req): + full_signature = req.get_header('X-Hub-Signature') + if not full_signature or not full_signature.find('='): + return False + sha_name, signature = req.get_header('X-Hub-Signature').split('=') + if sha_name != 'sha1': + return False + hook_secret = str(self._config('hook_secret')) + mac = hmac.new(hook_secret, msg = str(req.read()), digestmod = sha1) + return hmac.compare_digest(mac.hexdigest(), signature) + + +class GitHubLoginModule(GitHubMixin, LoginModule): # INavigationContributor methods @@ -59,7 +94,7 @@ def _do_login(self, req): def _do_oauth(self, req): oauth = self._oauth_session(req) authorization_response = req.abs_href(req.path_info) + '?' + req.query_string - client_secret = self._client_config('secret') + client_secret = self._config('client_secret') oauth.fetch_token( 'https://github.com/login/oauth/access_token', authorization_response=authorization_response, @@ -75,34 +110,12 @@ def _do_oauth(self, req): return super(GitHubLoginModule, self)._do_login(req) def _oauth_session(self, req): - client_id = self._client_config('id') + client_id = self._config('client_id') redirect_uri = req.abs_href.github('oauth') # Inner import to avoid a hard dependency on requests-oauthlib. from requests_oauthlib import OAuth2Session return OAuth2Session(client_id, redirect_uri=redirect_uri, scope=[]) - def _client_config(self, key): - assert key in ('id', 'secret') - value = self.config.get('github', 'client_' + key) - if re.match('[0-9a-f]+', value): - return value - elif value.isupper(): - return os.environ.get(value, '') - else: - with open(value) as f: - return f.read.strip() - - -class GitHubMixin(object): - - def get_gh_repo(self, reponame): - key = 'repository' if is_default(reponame) else '%s.repository' % reponame - return self.config.get('github', key) - - def get_branches(self, reponame): - key = 'branches' if is_default(reponame) else '%s.branches' % reponame - return self.config.getlist('github', key, sep=' ') - class GitHubBrowser(GitHubMixin, ChangesetModule): @@ -266,3 +279,35 @@ def describe_commits(revs): return u'commit %s' % revs[0] else: return u'commits %s' % u', '.join(revs) + + +class GitHubIssueHook(GitHubMixin, Component): + implements(IRequestHandler) + + _request_re = re.compile(r"/github-issues/?$") + + # IRequestHandler method + def match_request(self, req): + match = self._request_re.match(req.path_info) + if match: + return True + + # IRequestHandler method + def process_request(self, req): + if not self.verify_signature(req): + msg = u'Invalid hook signature from %s, ignoring request.\n' % req.remote_addr + self.log.warning(msg.rstrip('\n')) + req.send(msg.encode('utf-8'), 'text/plain', 400) + + event = req.get_header('X-GitHub-Event') + if event == 'ping': + payload = json.loads(req.read()) + req.send(payload['zen'].encode('utf-8'), 'text/plain', 200) + return + if event not in ['issue_comment', 'issues', 'pull_request', 'pull_request_review_comment']: + msg = u'Unsupported event recieved (%s), ignoring request.\n' % event + self.log.warning(msg.rstrip('\n')) + req.send(msg.encode('utf-8'), 'text/plain', 400) + return + + req.send(u'Running %s hook:' % event, 'text/plain', 200) From 1f4f42b9b9058863b04389a88390f1f4172eb6da Mon Sep 17 00:00:00 2001 From: Bryan Petty Date: Thu, 2 Apr 2015 22:15:40 +0000 Subject: [PATCH 2/6] Fleshed out pull_request hook events. - Creates a new ticket for all new pull requests. - Automatically attaches patch from pull request. - Attaches PR updates as new patches. --- tracext/github.py | 83 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/tracext/github.py b/tracext/github.py index 95fbff9..86fee1b 100644 --- a/tracext/github.py +++ b/tracext/github.py @@ -4,11 +4,14 @@ import json import os import re +import urllib2 from genshi.builder import tag +from trac.attachment import Attachment from trac.config import ListOption, Option from trac.core import Component, implements +from trac.ticket.model import Ticket from trac.util.translation import _ from trac.versioncontrol.api import is_default, NoSuchChangeset, RepositoryManager from trac.versioncontrol.web_ui.changeset import ChangesetModule @@ -37,15 +40,15 @@ def _config(self, key): with open(value) as f: return f.read.strip() - def verify_signature(self, req): + def verify_signature(self, req, body): full_signature = req.get_header('X-Hub-Signature') if not full_signature or not full_signature.find('='): return False - sha_name, signature = req.get_header('X-Hub-Signature').split('=') + sha_name, signature = full_signature.split('=') if sha_name != 'sha1': return False hook_secret = str(self._config('hook_secret')) - mac = hmac.new(hook_secret, msg = str(req.read()), digestmod = sha1) + mac = hmac.new(hook_secret, msg = str(body), digestmod = sha1) return hmac.compare_digest(mac.hexdigest(), signature) @@ -294,20 +297,84 @@ def match_request(self, req): # IRequestHandler method def process_request(self, req): - if not self.verify_signature(req): + body = req.read() + + if not self.verify_signature(req, body): msg = u'Invalid hook signature from %s, ignoring request.\n' % req.remote_addr self.log.warning(msg.rstrip('\n')) req.send(msg.encode('utf-8'), 'text/plain', 400) event = req.get_header('X-GitHub-Event') if event == 'ping': - payload = json.loads(req.read()) + payload = json.loads(body) req.send(payload['zen'].encode('utf-8'), 'text/plain', 200) - return + pass if event not in ['issue_comment', 'issues', 'pull_request', 'pull_request_review_comment']: msg = u'Unsupported event recieved (%s), ignoring request.\n' % event self.log.warning(msg.rstrip('\n')) req.send(msg.encode('utf-8'), 'text/plain', 400) - return + pass + + event_method = getattr(self, '_event_' + event) + event_method(req, json.loads(body)) + + def _event_issue_comment(self, req, data): + req.send(u'Running issue_comment hook', 'text/plain', 200) + pass + + def _event_issues(self, req, data): + req.send(u'Running issues hook', 'text/plain', 200) + pass + + def _event_pull_request(self, req, data): + pull = data['pull_request'] + author = data['sender']['login'] + ' (GitHub)' + + if data['action'] == 'opened': + ticket = Ticket(self.env) + ticket['reporter'] = author + ticket['summary'] = pull['title'] + ticket['description'] = pull['body'] + ticket['description'] += "\n\nPull Request: %s" % pull['html_url'] + ticket['status'] = 'new' + ticket_id = ticket.insert() + + response = urllib2.urlopen(pull['patch_url']) + self.create_attachment(ticket_id, pull['number'], response.read(), author) + + req.send('Synced to new ticket #%d' % ticket_id, 'text/plain', 200) + + elif data['action'] == 'synchronize': + # TODO: Fetch the associated Trac ticket based on pull request. + ticket_id = 6 + + response = urllib2.urlopen(pull['patch_url']) + self.create_attachment(ticket_id, pull['number'], response.read(), author) + + req.send('Synced new patch to ticket #%d' % ticket_id, 'text/plain', 200) + + elif data['action'] == 'closed': + pass + + elif data['action'] == 'reopened': + pass + + def _event_pull_request_review_comment(self, req, data): + req.send(u'Running pull_request_review_comment hook', 'text/plain', 200) + pass + + def create_attachment(self, ticket_id, pull_id, patch, author = None): + if len(patch) > self.env.config.get('attachment', 'max_size'): + self.log.warning('GitHub patch (#%d) too big to attach to ticket #%d' % (pull_id, ticket_id)) + pass + + # Create a temp file object for Trac attachments. + temp_fd = os.tmpfile() + temp_fd.write(patch) + temp_fd.seek(0) - req.send(u'Running %s hook:' % event, 'text/plain', 200) + attachment = Attachment(self.env, 'ticket', ticket_id) + if author: + attachment.author = author + filename = 'github-pull-%d.patch' % pull_id + attachment.insert(filename, temp_fd, len(patch)) From 08c0a6634e5d9a2b64d9eb43229ab858d032fe62 Mon Sep 17 00:00:00 2001 From: Bryan Petty Date: Fri, 3 Apr 2015 04:02:21 +0000 Subject: [PATCH 3/6] Mark ticket with GitHub issue using internal custom field. --- tracext/github.py | 53 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/tracext/github.py b/tracext/github.py index 86fee1b..d3146bc 100644 --- a/tracext/github.py +++ b/tracext/github.py @@ -9,6 +9,7 @@ from genshi.builder import tag from trac.attachment import Attachment +from trac.db import with_transaction from trac.config import ListOption, Option from trac.core import Component, implements from trac.ticket.model import Ticket @@ -329,6 +330,8 @@ def _event_issues(self, req, data): def _event_pull_request(self, req, data): pull = data['pull_request'] author = data['sender']['login'] + ' (GitHub)' + issue = data['repository']['full_name'] + '#' + str(pull['number']) + issue = issue.encode('utf-8') if data['action'] == 'opened': ticket = Ticket(self.env) @@ -339,19 +342,20 @@ def _event_pull_request(self, req, data): ticket['status'] = 'new' ticket_id = ticket.insert() + self.mark_github_issue(ticket_id, issue) + response = urllib2.urlopen(pull['patch_url']) self.create_attachment(ticket_id, pull['number'], response.read(), author) req.send('Synced to new ticket #%d' % ticket_id, 'text/plain', 200) elif data['action'] == 'synchronize': - # TODO: Fetch the associated Trac ticket based on pull request. - ticket_id = 6 + ticket_id = self.find_ticket(issue) - response = urllib2.urlopen(pull['patch_url']) - self.create_attachment(ticket_id, pull['number'], response.read(), author) - - req.send('Synced new patch to ticket #%d' % ticket_id, 'text/plain', 200) + if ticket_id: + response = urllib2.urlopen(pull['patch_url']) + self.create_attachment(ticket_id, pull['number'], response.read(), author) + req.send('Synced new patch to ticket #%d' % ticket_id, 'text/plain', 200) elif data['action'] == 'closed': pass @@ -363,6 +367,43 @@ def _event_pull_request_review_comment(self, req, data): req.send(u'Running pull_request_review_comment hook', 'text/plain', 200) pass + def mark_github_issue(self, ticket_id, github_issue): + """ + Manually save a custom field on the ticket (github_issue) with the + canonical GitHub address for the issue. + + We manually do this instead of instructing users to manually setup a + custom field since it's only necessary internally. End users can still + configure it if they want though. Hopefully, this also prevents ticket + split/copy plugins from duplicating the github_issue field since this + only supports syncing with one ticket. + + Several GitHub repos could be saving here, so don't only use number. + """ + + @with_transaction(self.env) + def sql_transaction(db): + cursor = db.cursor() + cursor.execute( + "DELETE FROM ticket_custom WHERE ticket = %d AND name = '%s'" % + (ticket_id, 'github_issue') + ) + cursor.execute( + "INSERT INTO ticket_custom VALUES (%d, '%s', '%s')" % + (ticket_id, 'github_issue', github_issue) + ) + + def find_ticket(self, github_issue): + """ + Return the ticket_id for the given GitHub issue if it exists. + """ + + rows = self.env.db_query( + "SELECT ticket FROM ticket_custom WHERE name = '%s' AND value = '%s'" % + ('github_issue', github_issue) + ) + return int(rows[0][0]) if rows else None + def create_attachment(self, ticket_id, pull_id, patch, author = None): if len(patch) > self.env.config.get('attachment', 'max_size'): self.log.warning('GitHub patch (#%d) too big to attach to ticket #%d' % (pull_id, ticket_id)) From 8d8e0c78ce34abd029e4fee3f54d618d58227692 Mon Sep 17 00:00:00 2001 From: Bryan Petty Date: Fri, 3 Apr 2015 06:17:12 +0000 Subject: [PATCH 4/6] Return instead of pass when appropriate. Also use DB variables for strings when necessary. --- tracext/github.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tracext/github.py b/tracext/github.py index d3146bc..36c49e0 100644 --- a/tracext/github.py +++ b/tracext/github.py @@ -304,28 +304,27 @@ def process_request(self, req): msg = u'Invalid hook signature from %s, ignoring request.\n' % req.remote_addr self.log.warning(msg.rstrip('\n')) req.send(msg.encode('utf-8'), 'text/plain', 400) + return event = req.get_header('X-GitHub-Event') if event == 'ping': payload = json.loads(body) req.send(payload['zen'].encode('utf-8'), 'text/plain', 200) - pass + return if event not in ['issue_comment', 'issues', 'pull_request', 'pull_request_review_comment']: msg = u'Unsupported event recieved (%s), ignoring request.\n' % event self.log.warning(msg.rstrip('\n')) req.send(msg.encode('utf-8'), 'text/plain', 400) - pass + return event_method = getattr(self, '_event_' + event) event_method(req, json.loads(body)) def _event_issue_comment(self, req, data): req.send(u'Running issue_comment hook', 'text/plain', 200) - pass def _event_issues(self, req, data): req.send(u'Running issues hook', 'text/plain', 200) - pass def _event_pull_request(self, req, data): pull = data['pull_request'] @@ -365,7 +364,6 @@ def _event_pull_request(self, req, data): def _event_pull_request_review_comment(self, req, data): req.send(u'Running pull_request_review_comment hook', 'text/plain', 200) - pass def mark_github_issue(self, ticket_id, github_issue): """ @@ -385,12 +383,12 @@ def mark_github_issue(self, ticket_id, github_issue): def sql_transaction(db): cursor = db.cursor() cursor.execute( - "DELETE FROM ticket_custom WHERE ticket = %d AND name = '%s'" % - (ticket_id, 'github_issue') + "DELETE FROM ticket_custom WHERE ticket = %d AND name = %%s" % + [ticket_id], ['github_issue'] ) cursor.execute( - "INSERT INTO ticket_custom VALUES (%d, '%s', '%s')" % - (ticket_id, 'github_issue', github_issue) + "INSERT INTO ticket_custom VALUES (%d, %%s, %%s)" % + [ticket_id], ['github_issue', github_issue] ) def find_ticket(self, github_issue): @@ -399,15 +397,15 @@ def find_ticket(self, github_issue): """ rows = self.env.db_query( - "SELECT ticket FROM ticket_custom WHERE name = '%s' AND value = '%s'" % - ('github_issue', github_issue) + "SELECT ticket FROM ticket_custom WHERE name = %s AND value = %s", + ['github_issue', github_issue] ) return int(rows[0][0]) if rows else None def create_attachment(self, ticket_id, pull_id, patch, author = None): if len(patch) > self.env.config.get('attachment', 'max_size'): self.log.warning('GitHub patch (#%d) too big to attach to ticket #%d' % (pull_id, ticket_id)) - pass + return # Create a temp file object for Trac attachments. temp_fd = os.tmpfile() From 5ae0dac7923aa6a0d7d58c2d8711877b0d26c9ab Mon Sep 17 00:00:00 2001 From: Bryan Petty Date: Fri, 3 Apr 2015 10:17:58 +0000 Subject: [PATCH 5/6] Sync new issues, and all comments everywhere. All issue and pull request comments are now posted to the appropriate tickets as well as inline patch comments. --- tracext/github.py | 73 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/tracext/github.py b/tracext/github.py index 36c49e0..b9cae57 100644 --- a/tracext/github.py +++ b/tracext/github.py @@ -321,10 +321,48 @@ def process_request(self, req): event_method(req, json.loads(body)) def _event_issue_comment(self, req, data): - req.send(u'Running issue_comment hook', 'text/plain', 200) + comment = data['comment'] + author = comment['user']['login'] + ' (GitHub)' + issue = data['repository']['full_name'] + '#' + str(data['issue']['number']) + issue = issue.encode('utf-8') + + ticket_id = self.find_ticket(issue) + + if data['action'] == 'created': + ticket = Ticket(self.env, ticket_id) + ticket.save_changes(author, comment['body']) + req.send('Comment added to ticket #%d.' % ticket_id, 'text/plain', 200) + + else: + req.send('No action taken for this hook.', 'text/plain', 200) def _event_issues(self, req, data): - req.send(u'Running issues hook', 'text/plain', 200) + author = data['sender']['login'] + ' (GitHub)' + issue = data['repository']['full_name'] + '#' + str(data['issue']['number']) + issue = issue.encode('utf-8') + + if data['action'] == 'opened': + ticket = Ticket(self.env) + ticket['reporter'] = author + ticket['summary'] = data['issue']['title'] + ticket['description'] = data['issue']['body'] + ticket['description'] += "\n\n!GitHub Issue: %s" % data['issue']['html_url'] + ticket['status'] = 'new' + ticket_id = ticket.insert() + + self.log.debug(str(ticket_id)) + self.mark_github_issue(ticket_id, issue) + + req.send('Synced to new ticket #%d' % ticket_id, 'text/plain', 200) + + elif data['action'] == 'closed': + pass + + elif data['action'] == 'reopened': + pass + + else: + req.send('No action taken for this hook.', 'text/plain', 200) def _event_pull_request(self, req, data): pull = data['pull_request'] @@ -354,7 +392,10 @@ def _event_pull_request(self, req, data): if ticket_id: response = urllib2.urlopen(pull['patch_url']) self.create_attachment(ticket_id, pull['number'], response.read(), author) - req.send('Synced new patch to ticket #%d' % ticket_id, 'text/plain', 200) + req.send('Attached new patch to ticket #%d.' % ticket_id, 'text/plain', 200) + + # This happens if sync is turned on after existing PRs were opened. + req.send('No ticket to sync patch for issue %s.' % issue, 'text/plain', 200) elif data['action'] == 'closed': pass @@ -362,8 +403,28 @@ def _event_pull_request(self, req, data): elif data['action'] == 'reopened': pass + else: + req.send('No action taken for this hook.', 'text/plain', 200) + def _event_pull_request_review_comment(self, req, data): - req.send(u'Running pull_request_review_comment hook', 'text/plain', 200) + comment = data['comment'] + pull = data['pull_request'] + author = comment['user']['login'] + ' (GitHub)' + issue = data['repository']['full_name'] + '#' + str(pull['number']) + issue = issue.encode('utf-8') + + ticket_id = self.find_ticket(issue) + + if data['action'] == 'created': + ticket = Ticket(self.env, ticket_id) + ticket.save_changes(author, + "%s\n\n[%s See Inline Patch Context]" % + (comment['body'], comment['html_url']) + ) + req.send('Comment added to ticket #%d.' % ticket_id, 'text/plain', 200) + + else: + req.send('No action taken for this hook.', 'text/plain', 200) def mark_github_issue(self, ticket_id, github_issue): """ @@ -384,11 +445,11 @@ def sql_transaction(db): cursor = db.cursor() cursor.execute( "DELETE FROM ticket_custom WHERE ticket = %d AND name = %%s" % - [ticket_id], ['github_issue'] + ticket_id, ['github_issue'] ) cursor.execute( "INSERT INTO ticket_custom VALUES (%d, %%s, %%s)" % - [ticket_id], ['github_issue', github_issue] + ticket_id, ['github_issue', github_issue] ) def find_ticket(self, github_issue): From 2f8b6b7505b14eebc669d5619e542f061838845c Mon Sep 17 00:00:00 2001 From: Bryan Petty Date: Sun, 5 Apr 2015 13:31:55 -0600 Subject: [PATCH 6/6] Documented issue sync feature and configuration. --- README.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++- tracext/github.py | 10 ++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8c02754..6b00e27 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ Trac - GitHub integration Features -------- -This Trac plugin performs three functions: +This Trac plugin performs four functions: 1. update the local git mirror used by Trac after each push to GitHub, and notify the new changesets to Trac; 2. authenticate users with their GitHub account; 3. replace Trac's built-in browser by GitHub's. +4. Sync any new GitHub issues and pull requests into Trac tickets. The notification of new changesets is strictly equivalent to the command described in Trac's setup guide: @@ -162,6 +163,75 @@ To enable it, edit `trac.ini` as follows: Since it replaces standard URLs of Trac, you must disable three components in `trac.versioncontrol.web_ui`, as shown above. +### Syncing Issues and Pull Requests + +**`tracext.github.GitHubIssueHook`** implements a a few GitHub hooks called +when a new issue or pull request is opened, commented on, or changed. + +It will open a new Trac ticket with the corresponding issue or pull request +title and description, setting the reporter to "username (GitHub)". It will use +all default ticket fields for everything else. It will also automatically +attach a patch file for any pull requests, along with any new patches when +someone pushes new commits to any existing pull request. + +Additionally, if any comments are left on the issue or pull request (including +inline patch comments on pulls) on GitHub, they will be posted to the Trac +ticket as well. + +Unlike the post-commit hook used for syncing any GitHub repos, these hooks are +required to configure a hook "secret" used to verify that the hooks were sent +from GitHub. This is to prevent spam tickets and comments. So first, you should +generate a random secret to be used with this hook. Ideally, it should be a +random string about 40 characters long containing only `[0-9a-f]` characters. +You can generate this on the command line by running this if you prefer: + + $ ruby -rsecurerandom -e 'puts SecureRandom.hex(20)' + +It should look like this (don't just use this string though!): +`cc6f7dddec47e4e10a423dcfbab5c102f506f72d` + +Save that somewhere safe, you will use this as your `hook_secret`. + +Edit your `trac.ini` as follows to configure syncing: + + [components] + tracext.github.GitHubIssueHook = enabled + + [github] + hook_secret = + +Reload the web server, browse to the home page of your project in Trac and +append `/github-issues` to the URL. You should see the following message: + + Endpoint is ready to accept GitHub notifications. + +This is the URL of the issues endpoint we'll use for the hook. + +If you get a Trac error page saying "No handler matched request to +/github-issues" instead, the plugin isn't installed properly. Make sure you've +followed the installation instructions correctly and search Trac's logs for +errors. + +This hook supports creating tickets for multiple GitHub repos if you want it to +watch issues and pull requests from several of them at the same time. So it's +possible to create an "organization hook", which will fire for all organization +repositories, or just create any number of "repository hooks" for the repos you +want it to watch. The configuration for either is exactly the same. + +Go to your organization's or repository's settings page on GitHub. In the +"Webhooks & Services" tab, click "Add webhook". Put the URL of the endpoint in +the "Payload URL" field, leave the "Content type" as "application/json", and set +the "Secret" to the 40 character `hook_secret` you generated earlier. Now select +the "Let me select individual events" radio option, and check the following +hooks: "Issue Comments", "Issues", "Pull Request", and "Pull Request review +comment". Then click "Add webhook", and you're done. + +If you click on the webhook you just created, at the bottom of the page, you +should see that a "ping" payload was successufully delivered to Trac. + +If you already have existing issues or pull requests, they will not be synced +to Trac. Only new issues and pull requests will be synced. + Advanced setup -------------- @@ -320,6 +390,10 @@ for git repositories. If you have an idea to fix it, please submit a patch! Changelog --------- +### 2.2 + +* Add support for syncing issues and pull requests. + ### 2.1 * Add support for GitHub login. diff --git a/tracext/github.py b/tracext/github.py index b9cae57..110a640 100644 --- a/tracext/github.py +++ b/tracext/github.py @@ -298,6 +298,15 @@ def match_request(self, req): # IRequestHandler method def process_request(self, req): + if req.method == 'GET': + req.send('Endpoint is ready to accept GitHub notifications.', 'text/plain', 200) + return + if req.method != 'POST': + msg = u'Method not allowed (%s)\n' % req.method + self.log.warning(msg.rstrip('\n')) + req.send(msg.encode('utf-8'), 'text/plain', 405) + return + body = req.read() if not self.verify_signature(req, body): @@ -350,7 +359,6 @@ def _event_issues(self, req, data): ticket['status'] = 'new' ticket_id = ticket.insert() - self.log.debug(str(ticket_id)) self.mark_github_issue(ticket_id, issue) req.send('Synced to new ticket #%d' % ticket_id, 'text/plain', 200)