Maciej Szulik added the comment:
I'm uploading the current version of the patch, it's still not fully reviewed,
I've left a few TODO's here and there for what should be double checked.
_______________________________________________________
PSF Meta Tracker <metatrac...@psf.upfronthosting.co.za>
<http://psf.upfronthosting.co.za/roundup/meta/issue589>
_______________________________________________________
diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
--- a/roundup/cgi/client.py
+++ b/roundup/cgi/client.py
@@ -22,6 +22,7 @@
from roundup.mailer import Mailer, MessageSendError, encode_quopri
from roundup.cgi import accept_language
from roundup import xmlrpc
+from roundup.pull_request import GitHubHandler
from roundup.anypy.cookie_ import CookieError, BaseCookie, SimpleCookie, \
get_cookie_date
@@ -378,6 +379,8 @@
try:
if self.path == 'xmlrpc':
self.handle_xmlrpc()
+ elif self.path == 'pull_request':
+ self.handle_pull_request()
else:
self.inner_main()
finally:
@@ -385,6 +388,34 @@
self.db.close()
+ def handle_pull_request(self):
+ # Set the charset and language, since other parts of
+ # Roundup may depend upon that.
+ self.determine_charset()
+ self.determine_language()
+ # Open the database as the correct user.
+ self.determine_user()
+ self.check_anonymous_access()
+
+ try:
+ handler = GitHubHandler(self)
+ handler.dispatch()
+ except Unauthorised, message:
+ self.response_code = 403
+ self.write(message)
+ except UnsupportedMediaType, message:
+ self.response_code = 415
+ self.write(message)
+ except MethodNotAllowed, message:
+ self.response_code = 405
+ self.write(message)
+ except Reject, message:
+ self.response_code = 400
+ self.write(message)
+ else:
+ self.write("Done!")
+
+
def handle_xmlrpc(self):
if self.env.get('CONTENT_TYPE') != 'text/xml':
self.write("This is the endpoint of Roundup <a href='" +
diff --git a/roundup/exceptions.py b/roundup/exceptions.py
--- a/roundup/exceptions.py
+++ b/roundup/exceptions.py
@@ -21,6 +21,12 @@
"""
pass
+class UnsupportedMediaType(Exception):
+ pass
+
+class MethodNotAllowed(Exception):
+ pass
+
class UsageError(ValueError):
pass
diff --git a/roundup/pull_request.py b/roundup/pull_request.py
new file mode 100644
--- /dev/null
+++ b/roundup/pull_request.py
@@ -0,0 +1,241 @@
+from roundup.exceptions import *
+
+import hashlib
+import hmac
+import json
+import re
+import os
+import logging
+
+
+if hasattr(hmac, 'compare_digest'):
+ compare_digest = hmac.compare_digest
+else:
+ def compare_digest(a, b):
+ return a == b
+
+url_re = re.compile(r'https://github.com/python/cpython/pull/(?P<number>\d+)')
+issue_re = re.compile(r'fixes\s+(?:bpo(\d+))(?:,\s*bpo(\d+))*', re.I)
+issue_id_re = re.compile(r'bpo(\d+)', re.I)
+
+class GitHubHandler:
+ """
+ GitHubHandler is responsible for parsing and serving all events coming
+ from GitHub. Details about GitHub webhooks can be found at:
+ https://developer.github.com/webhooks/
+ """
+
+ def __init__(self, client):
+ self.db = client.db
+ self.request = client.request
+ self.form = client.form
+ self.env = client.env
+
+ def dispatch(self):
+ try:
+ self.verify_request()
+ self.validate_webhook_secret()
+ self.extract()
+ except Unauthorised, message:
+ logging.error(message, exc_info=True)
+ raise
+ except MethodNotAllowed, message:
+ logging.error(message, exc_info=True)
+ raise
+ except UnsupportedMediaType, message:
+ logging.error(message, exc_info=True)
+ raise
+ except Exception, message:
+ logging.error(message, exc_info=True)
+ raise Reject('Invalid Request')
+
+ def extract(self):
+ """
+ This method is responsible for extracting information from GitHub event
+ and decide what to do with it more. Currently it knows how to handle
+ pull requests and comments.
+ """
+ event = self.get_event()
+ if event not in ('pull_request', 'issue_comment'):
+ raise Reject('Unkown X-GitHub-Event {}'.format(event))
+ data = json.loads(self.form.value)
+ if event == 'pull_request':
+ handler = PullRequest(self.db, data)
+ handler.dispatch()
+ # TODO: verify if this is about gh issues or pr comments, not sure?
+ elif event == 'issue_comment':
+ handler = IssueComment(self.db, data)
+ handler.dispatch()
+
+ def validate_webhook_secret(self):
+ """
+ Validates request signature against SECRET_KEY environment variable.
+ This verification is based on HMAC hex digest calculated from the sent
+ payload. The value of SECRET_KEY should be exactly the same as the one
+ configured in GitHub webhook secret field.
+ """
+ key = os.environ['SECRET_KEY']
+ data = self.form.value
+ signature = 'sha1=' + hmac.new(key, data,
+ hashlib.sha1).hexdigest()
+ header_signature = self.request.headers.get('X-Hub-Signature', '')
+ if not compare_digest(signature, header_signature):
+ raise Unauthorised('The provided secret does not match')
+
+ def verify_request(self):
+ """
+ Verifies if request contains expected method, content type and event.
+ """
+ method = self.env.get('REQUEST_METHOD', None)
+ if method != 'POST':
+ raise MethodNotAllowed('unsupported HTTP method %s' % method)
+ content_type = self.env.get('CONTENT_TYPE', None)
+ if content_type != 'application/json':
+ raise UnsupportedMediaType('unsupported Content-Type %s' %
+ content_type)
+ if self.get_event() is None:
+ raise Reject('missing X-GitHub-Event header')
+
+ def get_event(self):
+ """
+ Extracts github event from header field.
+ """
+ return self.request.headers.get('X-GitHub-Event', None)
+
+
+class Event:
+ """
+ Event is base class for all GitHub events.
+ """
+
+ def handle_create(self, prid, title, issue_ids):
+ """
+ Helper method for linking GitHub pull request with an issue.
+ """
+ # search for an existing issue first
+ issue_exists = len(self.db.issue.filter(None, {'id': issue_ids})) ==
len(issue_ids)
+ if issue_exists:
+ for issue_id in issue_ids:
+ # verify if this PR is already linked
+ prs = self.db.issue.get(issue_id, 'pull_requests')
+ if set(prs).intersection(self.db.pull_request.filter(None,
{'number': prid})):
+ continue
+ # create a new link
+ newpr = None
+ if title:
+ newpr = self.db.pull_request.create(number=prid,
title=title)
+ else:
+ # TODO verify if we can get pr title here
+ newpr = self.db.pull_request.create(number=prid)
+ prs.append(newpr)
+ self.db.issue.set(issue_id, pull_requests=prs)
+ self.db.commit()
+
+ def get_issue_ids(self):
+ raise NotImplementedError
+
+ def get_pr_details(self):
+ raise NotImplementedError
+
+
+class PullRequest(Event):
+ """
+ Class responsible for handling pull request events.
+ """
+
+ def __init__(self, db, data):
+ self.db = db
+ self.data = data
+
+ def dispatch(self):
+ github_username = self.get_github_username()
+ user_id = self.db.user.filter(None, {'github': github_username})
+ # TODO set bpobot as userid when none is found
+ if len(user_id) == 1:
+ username = self.db.user.get(user_id[0], 'username')
+ self.db.setCurrentUser(username)
+ action = self.data['action'].encode('utf-8')
+ issue_id = self.get_issue_ids()
+ prid, title = self.get_pr_details()
+ if action == 'opened' and issue_id is not None and prid is not None:
+ self.handle_create(prid, title, issue_id)
+
+ def get_issue_ids(self):
+ """
+ Extract issue IDs from pull request comments.
+ """
+ title = self.data['pull_request']['title'].encode('utf-8')
+ body = self.data['pull_request']['body'].encode('utf-8')
+ match = issue_re.search(body) or issue_re.search(title)
+ if match:
+ return issue_id_re.findall(match.group())
+ # no issue id found
+ create_issue = os.environ.get('CREATE_ISSUE', False)
+ if create_issue:
+ # TODO we should fill in the issue with more details
+ return [self.db.issue.create(title=title)]
+ return None
+
+ def get_pr_details(self):
+ """
+ Extract pull request number and title.
+ """
+ number = self.data['pull_request']['number']
+ title = self.data['pull_request']['title'].encode('utf-8')
+ return str(number), title
+
+
+ def get_github_username(self):
+ """
+ Extract github username from a pull request.
+ """
+ return self.data['pull_request']['user']['login'].encode('utf-8')
+
+
+class IssueComment(Event):
+ """
+ Class responsible for handling comment events.
+ """
+
+ def __init__(self, db, data):
+ self.db = db
+ self.data = data
+
+ def dispatch(self):
+ github_username = self.get_github_username()
+ user_id = self.db.user.filter(None, {'github': github_username})
+ # TODO set bpobot as userid when none is found
+ if len(user_id) == 1:
+ username = self.db.user.get(user_id[0], 'username')
+ self.db.setCurrentUser(username)
+ action = self.data['action'].encode('utf-8')
+ issue_id = self.get_issue_ids()
+ prid, title = self.get_pr_details()
+ if action == 'created' and issue_id is not None and prid is not None:
+ self.handle_create(prid, title, issue_id)
+
+ def get_issue_ids(self):
+ """
+ Extract issue IDs from comments.
+ """
+ body = self.data['comment']['body'].encode('utf-8')
+ match = issue_re.search(body)
+ if match:
+ return issue_id_re.findall(match.group())
+ return None
+
+ def get_pr_details(self):
+ """
+ Extract pull request number and title.
+ """
+ url = self.data['issue']['pull_request']['html_url'].encode('utf-8')
+ prid_match = url_re.search(url)
+ if not prid_match:
+ return (None, None)
+ return prid_match.group('number'), None
+
+ def get_github_username(self):
+ """
+ Extract github username from a comment.
+ """
+ return self.data['issue']['user']['login'].encode('utf-8')
diff --git a/share/roundup/templates/classic/detectors/pull_request.py
b/share/roundup/templates/classic/detectors/pull_request.py
new file mode 100644
--- /dev/null
+++ b/share/roundup/templates/classic/detectors/pull_request.py
@@ -0,0 +1,69 @@
+# Auditor for GitHub URLs
+# Check if it is a valid GitHub Pull Request URL and extract PR number
+
+import re
+
+
+repo_number_re = re.compile(r'^#?(?P<number>\d+)$')
+url_re =
re.compile(r'(https?:\\)?github\.com/python/cpython/pull/(?P<number>\d+)')
+
+def validate_pr_uniqueness(db, cl, nodeid, newvalues):
+ """
+ Verifies if newly added PR isn't already attached to an issue.
+ This process is a 2-level action, first a pull_request object is created,
which
+ goes through validate_pr_number to extract the PR number in case an URL is
passed,
+ only then we validate PR uniqueness within a single issue.
+ """
+ newprs = set(newvalues.get('pull_requests',()))
+ if not newprs:
+ return
+ oldprs = set()
+ if nodeid:
+ # if this is an existing issue, get the list of existing prs
+ oldprs = set(db.issue.get(nodeid, 'pull_requests'))
+ newprs -= oldprs
+ try:
+ # get the newly created PR number
+ number = db.pull_request.get(newprs.pop(), 'number')
+ except KeyError:
+ return
+ # and compare with those already attached to an issue
+ for oldpr in oldprs:
+ oldnumber = db.pull_request.get(oldpr, 'number')
+ if number == oldnumber:
+ raise ValueError("GitHub PR already added to issue")
+
+def validate_pr_number(db, cl, nodeid, newvalues):
+ try:
+ number = extract_number(newvalues['number'])
+ if number:
+ newvalues['number'] = number
+ except KeyError:
+ pass
+
+def extract_number(input):
+ """
+ Extracts PR number from the following forms:
+ - #number
+ - number
+ - full url
+ and returns its number.
+ """
+ # try matching just the number
+ repo_number_match = repo_number_re.search(input)
+ if repo_number_match:
+ return repo_number_match.group('number')
+ # fallback to parsing the entire url
+ url_match = url_re.search(input)
+ if url_match:
+ return url_match.group('number')
+ # if nothing else raise error
+ raise ValueError("Unknown PR format, acceptable formats are: "
+ "full github URL, #pr_number, pr_number")
+
+
+def init(db):
+ db.issue.audit('create', validate_pr_uniqueness)
+ db.issue.audit('set', validate_pr_uniqueness)
+ db.pull_request.audit('create', validate_pr_number)
+ db.pull_request.audit('set', validate_pr_number)
diff --git a/share/roundup/templates/classic/schema.py
b/share/roundup/templates/classic/schema.py
--- a/share/roundup/templates/classic/schema.py
+++ b/share/roundup/templates/classic/schema.py
@@ -45,7 +45,8 @@
alternate_addresses=String(),
queries=Multilink('query'),
roles=String(), # comma-separated string of Role names
- timezone=String())
+ timezone=String(),
+ github=String())
user.setkey("username")
db.security.addPermission(name='Register', klass='user',
description='User is allowed to register new user')
@@ -65,6 +66,12 @@
file = FileClass(db, "file",
name=String())
+pull_request = Class(db, "pull_request",
+ number=String(),
+ title=String(),
+ )
+pull_request.setlabelprop('id')
+
# IssueClass automatically gets these properties in addition to the Class ones:
# title = String()
# messages = Multilink("msg")
@@ -75,7 +82,8 @@
assignedto=Link("user"),
keyword=Multilink("keyword"),
priority=Link("priority"),
- status=Link("status"))
+ status=Link("status"),
+ pull_requests=Multilink('pull_request'))
#
# TRACKER SECURITY SETTINGS
@@ -92,7 +100,7 @@
# Assign the access and edit Permissions for issue, file and message
# to regular users now
-for cl in 'issue', 'file', 'msg', 'keyword':
+for cl in 'issue', 'file', 'msg', 'keyword', 'pull_request':
db.security.addPermissionToRole('User', 'View', cl)
db.security.addPermissionToRole('User', 'Edit', cl)
db.security.addPermissionToRole('User', 'Create', cl)
@@ -101,7 +109,7 @@
# May users view other user information? Comment these lines out
# if you don't want them to
-p = db.security.addPermission(name='View', klass='user',
+p = db.security.addPermission(name='View', klass='user',
properties=('id', 'organisation', 'phone', 'realname', 'timezone',
'username'))
db.security.addPermissionToRole('User', p)
@@ -167,7 +175,7 @@
# Allow anonymous users access to view issues (and the related, linked
# information)
-for cl in 'issue', 'file', 'msg', 'keyword', 'priority', 'status':
+for cl in 'issue', 'file', 'msg', 'keyword', 'priority', 'status',
'pull_request':
db.security.addPermissionToRole('Anonymous', 'View', cl)
# [OPTIONAL]
diff --git a/test/data/issuecommentevent.txt b/test/data/issuecommentevent.txt
new file mode 100644
--- /dev/null
+++ b/test/data/issuecommentevent.txt
@@ -0,0 +1,11 @@
+POST /python-dev/pull_request HTTP/1.1
+Host: 2f3784dc.ngrok.io
+Accept: */*
+User-Agent: GitHub-Hookshot/9667e0c
+X-GitHub-Event: issue_comment
+X-GitHub-Delivery: c6d12700-270e-11e6-86c5-8e9c9c898175
+content-type: application/json
+X-Hub-Signature: sha1=8b34ff28b2b312be5c6439c267afc236ff49475d
+Content-Length: 8404
+
+{"action":"created","issue":{"url":"https://api.github.com/repos/python/cpython/issues/1","repository_url":"https://api.github.com/repos/python/cpython","labels_url":"https://api.github.com/repos/python/cpython/issues/1/labels{/name}","comments_url":"https://api.github.com/repos/python/cpython/issues/1/comments","events_url":"https://api.github.com/repos/python/cpython/issues/1/events","html_url":"https://github.com/python/cpython/pull/1","id":156471938,"number":1,"title":"Test","user":{"login":"AnishShah","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo
}","subscriptions_url":"https://api.github.com/users/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":false},"labels":[],"state":"closed","locked":false,"assignee":null,"milestone":null,"comments":1,"created_at":"2016-05-24T10:22:11Z","updated_at":"2016-05-31T09:05:10Z","closed_at":"2016-05-24T10:23:56Z","pull_request":{"url":"https://api.github.com/repos/python/cpython/pulls/1","html_url":"https://github.com/python/cpython/pull/1","diff_url":"https://github.com/python/cpython/pull/1.diff","patch_url":"https://github.com/python/cpython/pull/1.patch"},"body":""},"comment":{"url":"https://api.github.com/repos/python/cpython/issues/comments/222632341","html_url":"https://github.com/python/cpython/pull/1#i
ssuecomment-222632341","issue_url":"https://api.github.com/repos/python/cpython/issues/1","id":222632341,"user":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":fa
lse},"created_at":"2016-05-31T09:05:10Z","updated_at":"2016-05-31T09:05:10Z","body":"fixes
bpo1"},"repository":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_ev
ents","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_
url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_
comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github
.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-05-24T13:18:37Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":276521,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"},"sender":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https
://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false}}
diff --git a/test/data/no-github-event-header.txt
b/test/data/no-github-event-header.txt
new file mode 100644
--- /dev/null
+++ b/test/data/no-github-event-header.txt
@@ -0,0 +1,7 @@
+POST /python-dev/pull_request HTTP/1.1
+Host: 2f3784dc.ngrok.io
+Accept: */*
+User-Agent: GitHub-Hookshot/9667e0c
+X-GitHub-Delivery: 94a54100-20e0-11e6-8756-bf8bf3b91449
+content-type: application/json
+X-Hub-Signature: sha1=ffda9754ba91e91ec414c2308bd25cd151c69960
diff --git a/test/data/non-json-body.txt b/test/data/non-json-body.txt
new file mode 100644
--- /dev/null
+++ b/test/data/non-json-body.txt
@@ -0,0 +1,11 @@
+POST /python-dev/pull_request HTTP/1.1
+Host: 2f3784dc.ngrok.io
+Accept: */*
+User-Agent: GitHub-Hookshot/9667e0c
+X-GitHub-Event: ping
+X-GitHub-Delivery: 94a54100-20e0-11e6-8756-bf8bf3b91449
+content-type: application/json
+X-Hub-Signature: sha1=01f7eac733e0923dfc6af20b168765578fab9e20
+Content-Length: 11
+
+hello:world
diff --git a/test/data/pingevent.txt b/test/data/pingevent.txt
new file mode 100644
--- /dev/null
+++ b/test/data/pingevent.txt
@@ -0,0 +1,11 @@
+POST /python-dev/pull_request HTTP/1.1
+Host: 2f3784dc.ngrok.io
+Accept: */*
+User-Agent: GitHub-Hookshot/9667e0c
+X-GitHub-Event: ping
+X-GitHub-Delivery: 94a54100-20e0-11e6-8756-bf8bf3b91449
+content-type: application/json
+X-Hub-Signature: sha1=95df171f2551526018324c407deed0b4b58a4126
+Content-Length: 6007
+
+{"zen":"Mind your words, they are
important.","hook_id":8488302,"hook":{"type":"Repository","id":8488302,"name":"web","active":true,"events":["issue_comment","pull_request"],"config":{"content_type":"json","insecure_ssl":"0","secret":"********","url":"http://90dedf29.ngrok.io/python-dev/github_pullrequest_url"},"updated_at":"2016-05-23T12:19:22Z","created_at":"2016-05-23T12:19:22Z","url":"https://api.github.com/repos/python/cpython/hooks/8488302","test_url":"https://api.github.com/repos/python/cpython/hooks/8488302/test","ping_url":"https://api.github.com/repos/python/cpython/hooks/8488302/pings","last_response":{"code":null,"status":"unused","message":null}},"repository":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/u
sers/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.g
ithub.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/pyth
on/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https:/
/api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-05-23T06:00:19Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":335381,"stargazers_count":0,"watchers_count":0,"language":"P
ython","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"},"sender":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.gi
thub.com/users/python/received_events","type":"User","site_admin":false}}
diff --git a/test/data/pullrequestevent.txt b/test/data/pullrequestevent.txt
new file mode 100644
--- /dev/null
+++ b/test/data/pullrequestevent.txt
@@ -0,0 +1,11 @@
+POST /python-dev/pull_request HTTP/1.1
+Host: 3ab1787e.ngrok.io
+Accept: */*
+User-Agent: GitHub-Hookshot/98ea3cc
+X-GitHub-Event: pull_request
+X-GitHub-Delivery: 3d4b5180-5c89-11e6-88fd-1aa99d941991
+content-type: application/json
+X-Hub-Signature: sha1=ea33bfa58c828af071cc1c20d247879e0ba02bfe
+Content-Length: 18982
+
+{"action":"opened","number":11,"pull_request":{"url":"https://api.github.com/repos/python/cpython/pulls/11","id":80363608,"html_url":"https://github.com/python/cpython/pull/11","diff_url":"https://github.com/python/cpython/pull/11.diff","patch_url":"https://github.com/python/cpython/pull/11.patch","issue_url":"https://api.github.com/repos/python/cpython/issues/11","number":11,"state":"open","locked":false,"title":"fixes
bpo1","user":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions",
"organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"body":"","created_at":"2016-08-07T10:25:19Z","updated_at":"2016-08-07T10:25:19Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/python/cpython/pulls/11/commits","review_comments_url":"https://api.github.com/repos/python/cpython/pulls/11/comments","review_comment_url":"https://api.github.com/repos/python/cpython/pulls/comments{/number}","comments_url":"https://api.github.com/repos/python/cpython/issues/11/comments","statuses_url":"https://api.github.com/repos/python/cpython/statuses/170f8a1f0ffbb733de656cd4a332ecea0c526d53","head":{"label":"python:master","ref":"master","sha":"170f8
a1f0ffbb733de656cd4a332ecea0c526d53","user":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"repo":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","i
d":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial repository","fork":true,"u
rl":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpyth
on/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/
repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-08-07T10:25:19Z","git_url":"git://github.com/python/cpython.git","ssh_u
rl":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":205407,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"python:revert-1-test","ref":"revert-1-test","sha":"886390c8473a1d52e07d20fd97176a56c153b9a1","user":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users
/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"repo":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api
.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/numbe
r}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"htt
ps://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/num
ber}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-08-07T10:25:19Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":205407,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/python/cpytho
n/pulls/11"},"html":{"href":"https://github.com/python/cpython/pull/11"},"issue":{"href":"https://api.github.com/repos/python/cpython/issues/11"},"comments":{"href":"https://api.github.com/repos/python/cpython/issues/11/comments"},"review_comments":{"href":"https://api.github.com/repos/python/cpython/pulls/11/comments"},"review_comment":{"href":"https://api.github.com/repos/python/cpython/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/python/cpython/pulls/11/commits"},"statuses":{"href":"https://api.github.com/repos/python/cpython/statuses/170f8a1f0ffbb733de656cd4a332ecea0c526d53"}},"merged":false,"mergeable":null,"mergeable_state":"unknown","merged_by":null,"comments":0,"review_comments":0,"commits":1,"additions":2,"deletions":0,"changed_files":1},"repository":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":
"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cp
ython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuse
s_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpyth
on/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-08-07T10:25:19Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url"
:"https://github.com/python/cpython","homepage":"","size":205407,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"},"sender":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/p
ython/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false}}
diff --git a/test/data/pullrequestevent1.txt b/test/data/pullrequestevent1.txt
new file mode 100644
--- /dev/null
+++ b/test/data/pullrequestevent1.txt
@@ -0,0 +1,11 @@
+POST /python-dev/pull_request HTTP/1.1
+Host: 2f3784dc.ngrok.io
+Accept: */*
+User-Agent: GitHub-Hookshot/9667e0c
+X-GitHub-Event: pull_request
+X-GitHub-Delivery: 251f6480-3640-11e6-97d9-1d57a5a69e84
+content-type: application/json
+X-Hub-Signature: sha1=ce64c1178a662c30659d9ef7ea99b1ea77a1bbf0
+Content-Length: 19029
+
+{"action":"opened","number":3,"pull_request":{"url":"https://api.github.com/repos/python/cpython/pulls/3","id":74368420,"html_url":"https://github.com/python/cpython/pull/3","diff_url":"https://github.com/python/cpython/pull/3.diff","patch_url":"https://github.com/python/cpython/pull/3.patch","issue_url":"https://api.github.com/repos/python/cpython/issues/3","number":3,"state":"open","locked":false,"title":"Created
using GitHub
API","user":{"login":"AnishShah","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/
users/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":false},"body":"Fixes
bpo1","created_at":"2016-06-19T17:06:21Z","updated_at":"2016-06-19T17:06:21Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/python/cpython/pulls/3/commits","review_comments_url":"https://api.github.com/repos/python/cpython/pulls/3/comments","review_comment_url":"https://api.github.com/repos/python/cpython/pulls/comments{/number}","comments_url":"https://api.github.com/repos/python/cpython/issues/3/comments","statuses_url":"https://api.github.com/repos/python/cpython/statuses/17f854a76947193fadde6837d7d92123f47d7e16","head":{"label":"python:
test","ref":"test","sha":"17f854a76947193fadde6837d7d92123f47d7e16","user":{"login":"AnishShah","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":false},"repo":{"id":59477756,"name":
"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
r
ead-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/ta
gs{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/pyth
on/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-06-1
5T19:43:02Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":205466,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"python:master","ref":"master","sha":"170f8a1f0ffbb733de656cd4a332ecea0c526d53","user":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/
gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"repo":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/sta
rred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api
.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repo
s/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https:
//api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-06-15T19:43:02Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":205466,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self
":{"href":"https://api.github.com/repos/python/cpython/pulls/3"},"html":{"href":"https://github.com/python/cpython/pull/3"},"issue":{"href":"https://api.github.com/repos/python/cpython/issues/3"},"comments":{"href":"https://api.github.com/repos/python/cpython/issues/3/comments"},"review_comments":{"href":"https://api.github.com/repos/python/cpython/pulls/3/comments"},"review_comment":{"href":"https://api.github.com/repos/python/cpython/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/python/cpython/pulls/3/commits"},"statuses":{"href":"https://api.github.com/repos/python/cpython/statuses/17f854a76947193fadde6837d7d92123f47d7e16"}},"merged":false,"mergeable":null,"mergeable_state":"unknown","merged_by":null,"comments":0,"review_comments":0,"commits":1,"additions":2,"deletions":0,"changed_files":1},"repository":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercon
tent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","for
ks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com
/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_
url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-06-15T19:43:02Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"h
ttps://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":205466,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"},"sender":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/or
gs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false}}
diff --git a/test/data/pullrequestevent2.txt b/test/data/pullrequestevent2.txt
new file mode 100644
--- /dev/null
+++ b/test/data/pullrequestevent2.txt
@@ -0,0 +1,11 @@
+POST /python-dev/pull_request HTTP/1.1
+Host: 8900ffb8.ngrok.io
+Accept: */*
+User-Agent: GitHub-Hookshot/0269732
+X-GitHub-Event: pull_request
+X-GitHub-Delivery: 60418b80-2199-11e6-98bc-9ed2b8e584c9
+content-type: application/json
+X-Hub-Signature: sha1=af2367e9d82d817e41266ab4aa16af009e4a52f9
+Content-Length: 18873
+
+{"action":"opened","number":1,"pull_request":{"url":"https://api.github.com/repos/python/cpython/pulls/1","id":71194519,"html_url":"https://github.com/python/cpython/pull/1","diff_url":"https://github.com/python/cpython/pull/1.diff","patch_url":"https://github.com/python/cpython/pull/1.patch","issue_url":"https://api.github.com/repos/python/cpython/issues/1","number":1,"state":"open","locked":false,"title":"Test","user":{"login":"AnishShah","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/AnishShah/followers","following_url":"https://api.github.com/AnishShah/following{/other_user}","gists_url":"https://api.github.com/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/AnishShah/subscriptions","organizations_url":"http
s://api.github.com/AnishShah/orgs","repos_url":"https://api.github.com/AnishShah/repos","events_url":"https://api.github.com/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/AnishShah/received_events","type":"User","site_admin":false},"body":"","created_at":"2016-05-24T10:22:11Z","updated_at":"2016-05-24T10:22:11Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/python/cpython/pulls/1/commits","review_comments_url":"https://api.github.com/repos/python/cpython/pulls/1/comments","review_comment_url":"https://api.github.com/repos/python/cpython/pulls/comments{/number}","comments_url":"https://api.github.com/repos/python/cpython/issues/1/comments","statuses_url":"https://api.github.com/repos/python/cpython/statuses/17f854a76947193fadde6837d7d92123f47d7e16","head":{"label":"python:test","ref":"test","sha":"17f854a76947193fadde6837d7d92123f47d7e16","user":{"login":"pytho
n","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/AnishShah","html_url":"https://github.com/python","followers_url":"https://api.github.com/AnishShah/followers","following_url":"https://api.github.com/AnishShah/following{/other_user}","gists_url":"https://api.github.com/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/AnishShah/subscriptions","organizations_url":"https://api.github.com/AnishShah/orgs","repos_url":"https://api.github.com/AnishShah/repos","events_url":"https://api.github.com/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/AnishShah/received_events","type":"User","site_admin":false},"repo":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_i
d":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/rep
os/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sh
a}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/
python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-05-24T10:19:54Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.gi
t","svn_url":"https://github.com/python/cpython","homepage":"","size":276517,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"python:master","ref":"master","sha":"b998f589f341718f24013cc79e8358bec03dfde3","user":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions"
,"organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"repo":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs",
"repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.gith
ub.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/pyth
on/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,partici
pating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-05-24T10:19:54Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":276517,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/python/cpython/pulls/1"},"html":{"href":"https://github.com/python/cpython/pull/1"},"issue":{"href":"https://api.git
hub.com/repos/python/cpython/issues/1"},"comments":{"href":"https://api.github.com/repos/python/cpython/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/python/cpython/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/python/cpython/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/python/cpython/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/python/cpython/statuses/17f854a76947193fadde6837d7d92123f47d7e16"}},"merged":false,"mergeable":null,"mergeable_state":"unknown","merged_by":null,"comments":0,"review_comments":0,"commits":1,"additions":2,"deletions":0,"changed_files":1},"repository":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.git
hub.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"http
s://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/r
epos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url"
:"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-05-24T10:19:54Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":276517,"stargazers_count":0,"watchers_count":0,"lan
guage":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"},"sender":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https
://api.github.com/users/python/received_events","type":"User","site_admin":false}}
diff --git a/test/data/pullrequestevent3.txt b/test/data/pullrequestevent3.txt
new file mode 100644
--- /dev/null
+++ b/test/data/pullrequestevent3.txt
@@ -0,0 +1,11 @@
+POST /python-dev/pull_request HTTP/1.1
+Host: 3ab1787e.ngrok.io
+Accept: */*
+User-Agent: GitHub-Hookshot/98ea3cc
+X-GitHub-Event: pull_request
+X-GitHub-Delivery: afc32e00-5c97-11e6-8ffa-5bf6b88808be
+content-type: application/json
+X-Hub-Signature: sha1=e99e42e4fb3d8b5148b498b9ae2dd0398bcb8dd5
+Content-Length: 19063
+
+{"action":"opened","number":13,"pull_request":{"url":"https://api.github.com/repos/python/cpython/pulls/13","id":80365455,"html_url":"https://github.com/python/cpython/pull/13","diff_url":"https://github.com/python/cpython/pull/13.diff","patch_url":"https://github.com/python/cpython/pull/13.patch","issue_url":"https://api.github.com/repos/python/cpython/issues/13","number":13,"state":"open","locked":false,"title":"fixes
bpo1, bpo2,
bpo3","user":{"login":"AnishShah","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github
.com/users/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":false},"body":"","created_at":"2016-08-07T12:08:44Z","updated_at":"2016-08-07T12:08:44Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/python/cpython/pulls/13/commits","review_comments_url":"https://api.github.com/repos/python/cpython/pulls/13/comments","review_comment_url":"https://api.github.com/repos/python/cpython/pulls/comments{/number}","comments_url":"https://api.github.com/repos/python/cpython/issues/13/comments","statuses_url":"https://api.github.com/repos/python/cpython/statuses/170f8a1f0ffbb733de656cd4a332ecea0c526d53","head":{"la
bel":"python:master","ref":"master","sha":"170f8a1f0ffbb733de656cd4a332ecea0c526d53","user":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":false},"repo":{"id":59
477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"S
emi-official read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/
cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.
com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_
at":"2016-08-07T12:07:47Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":205407,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"python:revert-1-test","ref":"revert-1-test","sha":"886390c8473a1d52e07d20fd97176a56c153b9a1","user":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.git
hub.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"repo":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api
.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","is
sue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":
"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/numbe
r}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-08-07T12:07:47Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":205407,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch
":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/python/cpython/pulls/13"},"html":{"href":"https://github.com/python/cpython/pull/13"},"issue":{"href":"https://api.github.com/repos/python/cpython/issues/13"},"comments":{"href":"https://api.github.com/repos/python/cpython/issues/13/comments"},"review_comments":{"href":"https://api.github.com/repos/python/cpython/pulls/13/comments"},"review_comment":{"href":"https://api.github.com/repos/python/cpython/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/python/cpython/pulls/13/commits"},"statuses":{"href":"https://api.github.com/repos/python/cpython/statuses/170f8a1f0ffbb733de656cd4a332ecea0c526d53"}},"merged":false,"mergeable":null,"mergeable_state":"unknown","merged_by":null,"comments":0,"review_comments":0,"commits":1,"additions":2,"deletions":0,"changed_files":1},"repository":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_u
rl":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.git
hub.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","
trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython
/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-08-07T12:07:47Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:
python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":205407,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"},"sender":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"http
s://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false}}
diff --git a/test/data/pullrequestevent4.txt b/test/data/pullrequestevent4.txt
new file mode 100644
--- /dev/null
+++ b/test/data/pullrequestevent4.txt
@@ -0,0 +1,11 @@
+POST /python-dev/pull_request HTTP/1.1
+Host: 7a1eec53.ngrok.io
+Accept: */*
+User-Agent: GitHub-Hookshot/c80b888
+X-GitHub-Event: pull_request
+X-GitHub-Delivery: 95867d00-5e5c-11e6-97c7-ddc89938af3a
+content-type: application/json
+X-Hub-Signature: sha1=4f865da24d53daf4bb040cd8fe1a31466d9d94ac
+Content-Length: 19080
+
+{"action":"opened","number":14,"pull_request":{"url":"https://api.github.com/repos/python/cpython/pulls/14","id":80667637,"html_url":"https://github.com/python/cpython/pull/14","diff_url":"https://github.com/python/cpython/pull/14.diff","patch_url":"https://github.com/python/cpython/pull/14.patch","issue_url":"https://api.github.com/repos/python/cpython/issues/14","number":14,"state":"open","locked":false,"title":"update
.gitignore","user":{"login":"AnishShah","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/use
rs/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":false},"body":"fixes
bpo1, bpo2,
bpo3","created_at":"2016-08-09T18:10:42Z","updated_at":"2016-08-09T18:10:42Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/python/cpython/pulls/14/commits","review_comments_url":"https://api.github.com/repos/python/cpython/pulls/14/comments","review_comment_url":"https://api.github.com/repos/python/cpython/pulls/comments{/number}","comments_url":"https://api.github.com/repos/python/cpython/issues/14/comments","statuses_url":"https://api.github.com/repos/python/cpython/statuses/170f8a1f0ffbb733de656cd4a332ecea0c526d5
3","head":{"label":"python:master","ref":"master","sha":"170f8a1f0ffbb733de656cd4a332ecea0c526d53","user":{"login":"AnishShah","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":false
},"repo":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython"
,"description":"Semi-official read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.
com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"ht
tps://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11
:39:38Z","pushed_at":"2016-08-09T18:10:42Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":205407,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"python:revert-1-test","ref":"revert-1-test","sha":"886390c8473a1d52e07d20fd97176a56c153b9a1","user":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url"
:"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"repo":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_
url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial
repository","fork":true,"url":"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/c
python/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","
subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpy
thon/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-08-09T18:10:42Z","git_url":"git://github.com/python/cpython.git","ssh_url":"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":205407,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":
0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/python/cpython/pulls/14"},"html":{"href":"https://github.com/python/cpython/pull/14"},"issue":{"href":"https://api.github.com/repos/python/cpython/issues/14"},"comments":{"href":"https://api.github.com/repos/python/cpython/issues/14/comments"},"review_comments":{"href":"https://api.github.com/repos/python/cpython/pulls/14/comments"},"review_comment":{"href":"https://api.github.com/repos/python/cpython/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/python/cpython/pulls/14/commits"},"statuses":{"href":"https://api.github.com/repos/python/cpython/statuses/170f8a1f0ffbb733de656cd4a332ecea0c526d53"}},"merged":false,"mergeable":null,"mergeable_state":"unknown","merged_by":null,"comments":0,"review_comments":0,"commits":1,"additions":2,"deletions":0,"changed_files":1},"repository":{"id":59477756,"name":"cpython","full_name":"python/cpython","owner":{"login":"python","id":
3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organizations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/python/cpython","description":"Semi-official
read-only mirror of the CPython Mercurial repository","fork":true,"url"
:"https://api.github.com/repos/python/cpython","forks_url":"https://api.github.com/repos/python/cpython/forks","keys_url":"https://api.github.com/repos/python/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/python/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/python/cpython/teams","hooks_url":"https://api.github.com/repos/python/cpython/hooks","issue_events_url":"https://api.github.com/repos/python/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/python/cpython/events","assignees_url":"https://api.github.com/repos/python/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/python/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/python/cpython/tags","blobs_url":"https://api.github.com/repos/python/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/python/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/python/cpython/
git/refs{/sha}","trees_url":"https://api.github.com/repos/python/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/python/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/python/cpython/languages","stargazers_url":"https://api.github.com/repos/python/cpython/stargazers","contributors_url":"https://api.github.com/repos/python/cpython/contributors","subscribers_url":"https://api.github.com/repos/python/cpython/subscribers","subscription_url":"https://api.github.com/repos/python/cpython/subscription","commits_url":"https://api.github.com/repos/python/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/python/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/python/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/python/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/python/cpython/contents/{+path}","compare_url":"https://api.github.com/rep
os/python/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/python/cpython/merges","archive_url":"https://api.github.com/repos/python/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/python/cpython/downloads","issues_url":"https://api.github.com/repos/python/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/python/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/python/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/python/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/python/cpython/labels{/name}","releases_url":"https://api.github.com/repos/python/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/python/cpython/deployments","created_at":"2016-05-23T11:39:11Z","updated_at":"2016-05-23T11:39:38Z","pushed_at":"2016-08-09T18:10:42Z","git_url":"git://github.com/python/cpython.git","ssh_url"
:"g...@github.com:python/cpython.git","clone_url":"https://github.com/python/cpython.git","svn_url":"https://github.com/python/cpython","homepage":"","size":205407,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"},"sender":{"login":"python","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/python","html_url":"https://github.com/python","followers_url":"https://api.github.com/users/python/followers","following_url":"https://api.github.com/users/python/following{/other_user}","gists_url":"https://api.github.com/users/python/gists{/gist_id}","starred_url":"https://api.github.com/users/python/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/python/subscriptions","organiz
ations_url":"https://api.github.com/users/python/orgs","repos_url":"https://api.github.com/users/python/repos","events_url":"https://api.github.com/users/python/events{/privacy}","received_events_url":"https://api.github.com/users/python/received_events","type":"User","site_admin":false}}
diff --git a/test/test_pull_request.py b/test/test_pull_request.py
new file mode 100644
--- /dev/null
+++ b/test/test_pull_request.py
@@ -0,0 +1,223 @@
+import unittest
+import os
+import db_test_base
+import cgi
+from BaseHTTPServer import BaseHTTPRequestHandler
+from StringIO import StringIO
+from roundup.cgi import client
+from roundup.backends import list_backends
+from roundup.pull_request import GitHubHandler
+from roundup.exceptions import *
+
+NEEDS_INSTANCE = 1
+
+
+class HTTPRequest(BaseHTTPRequestHandler):
+ def __init__(self, filename):
+ path = os.path.dirname(os.path.abspath(__file__)) + "/data/" + filename
+ request_file = open(path, 'r')
+ request_text = request_file.read()
+ request_file.close()
+ self.rfile = StringIO(request_text)
+ self.raw_requestline = self.rfile.readline()
+ self.error_code = self.error_message = None
+ self.parse_request()
+
+
+class TestCase(unittest.TestCase):
+
+ backend = None
+
+ def setUp(self):
+ # instance
+ self.dirname = '_test_pull_request'
+ self.instance = db_test_base.setupTracker(self.dirname, self.backend)
+ self.env = {
+ 'PATH_INFO': 'http://localhost/pull_request',
+ 'HTTP_HOST': 'localhost',
+ 'TRACKER_NAME': 'test',
+ 'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': 'application/json'
+ }
+ os.environ['SECRET_KEY'] = "abcd"
+ os.environ['CREATE_ISSUE'] = 'yes'
+
+ def _make_client(self, filename):
+ request = HTTPRequest(filename)
+ form = cgi.FieldStorage(fp=request.rfile, environ=self.env,
+ headers=request.headers)
+ dummy_client = client.Client(self.instance, request, self.env, form)
+ dummy_client.opendb("anonymous")
+ self.db = dummy_client.db
+ self.db.issue.create(title="Hello")
+ return dummy_client
+
+ def testMissingSecretKey(self):
+ os.environ.pop('SECRET_KEY')
+ dummy_client = self._make_client("pingevent.txt")
+ with self.assertRaises(Reject) as context:
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ self.assertEqual(str(context.exception),
+ "Invalid Request")
+
+ def testMissingGitHubEventHeader(self):
+ dummy_client = self._make_client("no-github-event-header.txt")
+ with self.assertRaises(Reject) as context:
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ self.assertEqual(str(context.exception),
+ "Invalid Request")
+
+ def testNonJSONRequestBody(self):
+ dummy_client = self._make_client("non-json-body.txt")
+ with self.assertRaises(Reject) as context:
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ self.assertEqual(str(context.exception),
+ "Invalid Request")
+
+ def testSecretKey(self):
+ os.environ['SECRET_KEY'] = "1234"
+ dummy_client = self._make_client("pingevent.txt")
+ with self.assertRaises(Unauthorised) as context:
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ self.assertEqual(str(context.exception),
+ "The provided secret does not match")
+
+ def testUnsupportedMediaType(self):
+ dummy_client = self._make_client("pingevent.txt")
+ dummy_client.env['CONTENT_TYPE'] = 'application/xml'
+ with self.assertRaises(UnsupportedMediaType) as context:
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ self.assertEqual(str(context.exception),
+ "unsupported Content-Type application/xml")
+
+ def testMethodNotAllowed(self):
+ dummy_client = self._make_client("pingevent.txt")
+ dummy_client.env['REQUEST_METHOD'] = 'GET'
+ with self.assertRaises(MethodNotAllowed) as context:
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ self.assertEqual(str(context.exception),
+ "unsupported HTTP method GET")
+
+ def testPingEvent(self):
+ dummy_client = self._make_client("pingevent.txt")
+ with self.assertRaises(Reject) as context:
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ self.assertEqual(str(context.exception), "Invalid Request")
+
+ def testIssueCommentEvent(self):
+ dummy_client = self._make_client("issuecommentevent.txt")
+ self.db.user.create(username="anish.shah", github="AnishShah")
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ prs = self.db.issue.get('1', 'pull_requests')
+ self.assertTrue(len(prs) == 1)
+ number = self.db.pull_request.get(prs[0], 'number')
+ self.assertEqual(number, '1')
+ user_id = self.db.pull_request.get(prs[0], 'creator')
+ self.assertEqual(self.db.user.get(user_id, 'github'), 'AnishShah')
+
+ def testPullRequestEventForTitle(self):
+ # When the title of a PR has string "fixes bpo123"
+ dummy_client = self._make_client("pullrequestevent.txt")
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ prs = self.db.issue.get('1', 'pull_requests')
+ self.assertTrue(len(prs) == 1)
+ number = self.db.pull_request.get(prs[0], 'number')
+ self.assertEqual(number, '11')
+ user_id = self.db.pull_request.get(prs[0], 'creator')
+ self.assertEqual(self.db.user.get(user_id, 'username'), 'anonymous')
+
+ def testPullRequestEventForBody(self):
+ # When the body of a PR has string "fixes bpo123"
+ dummy_client = self._make_client("pullrequestevent1.txt")
+ self.db.user.create(username="anish.shah", github="AnishShah")
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ prs = self.db.issue.get('1', 'pull_requests')
+ self.assertTrue(len(prs) == 1)
+ number = self.db.pull_request.get(prs[0], 'number')
+ self.assertEqual(number, '3')
+ title = self.db.pull_request.get(prs[0], 'title')
+ self.assertEqual(title, 'Created using GitHub API')
+ user_id = self.db.pull_request.get(prs[0], 'creator')
+ self.assertEqual(self.db.user.get(user_id, 'github'), 'AnishShah')
+
+ def testPullRequestEventForMultipleIssueReferenceInTitle(self):
+ dummy_client = self._make_client("pullrequestevent3.txt")
+ self.db.issue.create(title="Python")
+ self.db.issue.create(title="CPython")
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ for i in range(1, 4):
+ prs = self.db.issue.get(str(i), 'pull_requests')
+ self.assertTrue(len(prs) == 1)
+ number = self.db.pull_request.get(prs[0], 'number')
+ self.assertEqual(number, '13')
+ title = self.db.pull_request.get(prs[0], 'title')
+ self.assertEqual(title, 'fixes bpo1, bpo2, bpo3')
+
+ def testPullRequestEventForMultipleIssueReferenceInBody(self):
+ dummy_client = self._make_client("pullrequestevent4.txt")
+ self.db.issue.create(title="Python")
+ self.db.issue.create(title="CPython")
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ for i in range(1, 4):
+ prs = self.db.issue.get(str(i), 'pull_requests')
+ self.assertTrue(len(prs) == 1)
+ number = self.db.pull_request.get(prs[0], 'number')
+ self.assertEqual(number, '14')
+ title = self.db.pull_request.get(prs[0], 'title')
+ self.assertEqual(title, 'update .gitignore')
+
+ def testPullRequestEventWithoutIssueReference(self):
+ # When no issue is referenced in PR and environment variable is set
+ dummy_client = self._make_client("pullrequestevent2.txt")
+ self.assertEqual(self.db.issue.count(), 1)
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ self.assertEqual(self.db.issue.count(), 2)
+ user_id = self.db.issue.get('2', 'creator')
+ self.assertEqual(self.db.user.get(user_id, 'username'), 'anonymous')
+
+ def testPullRequestFromGitHubUserWithoutIssueReference(self):
+ # When no issue is referenced in PR and environment variable is set
+ # and Github field of b.p.o user is set
+ dummy_client = self._make_client("pullrequestevent2.txt")
+ self.db.user.create(username="anish.shah", github="AnishShah")
+ self.assertEqual(self.db.issue.count(), 1)
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ self.assertEqual(self.db.issue.count(), 2)
+ user_id = self.db.issue.get('2', 'creator')
+ self.assertEqual(self.db.user.get(user_id, 'username'), 'anish.shah')
+
+ def testPullRequestEventWithoutIssueReferenceAndEnvVariable(self):
+ # When no issue is referenced in PR and no environment variable is set
+ os.environ.pop('CREATE_ISSUE')
+ dummy_client = self._make_client("pullrequestevent2.txt")
+ self.assertEqual(self.db.issue.count(), 1)
+ handler = GitHubHandler(dummy_client)
+ handler.dispatch()
+ self.assertEqual(self.db.issue.count(), 1)
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ for l in list_backends():
+ dct = dict(backend=l)
+ subcls = type(TestCase)('TestCase_%s' % l, (TestCase,), dct)
+ suite.addTest(unittest.makeSuite(subcls))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
_______________________________________________
Tracker-discuss mailing list
Tracker-discuss@python.org
https://mail.python.org/mailman/listinfo/tracker-discuss
Code of Conduct: https://www.python.org/psf/codeofconduct/