diff --git a/reviewboard/accounts/backends.py b/reviewboard/accounts/backends.py
index 167bab9eb304581712609fe57e61b69e17e53abd..cbf066531b29489d97b78bbaa41cc10d61ccbbcb 100644
--- a/reviewboard/accounts/backends.py
+++ b/reviewboard/accounts/backends.py
@@ -916,7 +916,6 @@ class X509Backend(AuthBackend):
 
         return user
 
-
 def get_enabled_auth_backends():
     """Get all authentication backends being used by Review Board.
 
diff --git a/reviewboard/accounts/forms/pages.py b/reviewboard/accounts/forms/pages.py
index 1e1041f51854ccc4c696faaa2e4c9dbb3626be8e..7bc045bfbeea83b092805101121bf489a61b74a7 100644
--- a/reviewboard/accounts/forms/pages.py
+++ b/reviewboard/accounts/forms/pages.py
@@ -4,6 +4,8 @@ import logging
 
 from django import forms
 from django.contrib import messages
+
+from django.http import HttpResponse
 from django.forms import widgets
 from django.utils.datastructures import SortedDict
 from django.utils.translation import ugettext_lazy as _
@@ -12,6 +14,7 @@ from djblets.avatars.forms import (
 from djblets.forms.fields import TimeZoneField
 from djblets.siteconfig.models import SiteConfiguration
 from djblets.configforms.forms import ConfigPageForm
+from django.core.urlresolvers import reverse
 
 from reviewboard.accounts.backends import get_enabled_auth_backends
 from reviewboard.avatars import avatar_services
@@ -46,6 +49,7 @@ class AccountSettingsForm(AccountPageForm):
     syntax_highlighting = forms.BooleanField(
         label=_('Enable syntax highlighting in the diff viewer'),
         required=False)
+
     open_an_issue = forms.BooleanField(
         label=_('Always open an issue when comment box opens'),
         required=False)
@@ -66,6 +70,7 @@ class AccountSettingsForm(AccountPageForm):
         label=_('Always use Markdown for text fields'),
         required=False)
 
+
     def load(self):
         """Load data for the form."""
         self.set_initial({
@@ -174,6 +179,34 @@ class APITokensForm(AccountPageForm):
         }
 
 
+class ExternalAuthForm(AccountPageForm):
+    """Form for authenticating with external services."""
+
+    template_name = 'accounts/external_service_auth.html'
+    form_id = 'external_auth'
+    form_title = _('External Authentication')
+    save_label = _('Disconnect from GitHub.')
+
+    def is_visible(self):
+        return True
+
+    def save(self):
+        rsp = HttpResponse()
+        rsp['Location'] = (
+            '%s?next=%s'
+            % (reverse('social:disconnect', args=['github']),
+               self.request.path)
+        )
+        rsp.status_code = 307
+        return rsp
+
+    def user_is_authenticated_on_github(self):
+        if self.user.social_auth.filter(provider='github'):
+            return True
+
+        else:
+            return False
+
 class ChangePasswordForm(AccountPageForm):
     """Form for changing a user's password."""
 
@@ -198,7 +231,10 @@ class ChangePasswordForm(AccountPageForm):
         """Get whether or not the "change password" form should be shown."""
         backend = get_enabled_auth_backends()[0]
 
-        return backend.supports_change_password
+        if backend:
+            return True
+        else:
+            return False
 
     def clean_old_password(self):
         """Validate the 'old_password' field.
diff --git a/reviewboard/accounts/middleware.py b/reviewboard/accounts/middleware.py
index 553f786140a9d043304b3f9c4f170e2f16047a9f..ec2add022340548562a7b69047facfbd81747f5a 100644
--- a/reviewboard/accounts/middleware.py
+++ b/reviewboard/accounts/middleware.py
@@ -1,8 +1,13 @@
 from __future__ import unicode_literals
 
 import pytz
+
+from django.http import HttpResponse
 from django.utils import timezone
 
+from social import exceptions as social_exceptions
+from social.apps.django_app.middleware import SocialAuthExceptionMiddleware
+
 from reviewboard.accounts.models import Profile
 
 
@@ -17,3 +22,12 @@ class TimezoneMiddleware(object):
                 timezone.activate(pytz.timezone(user.timezone))
             except (Profile.DoesNotExist, pytz.UnknownTimeZoneError):
                 pass
+
+class ExtendedSocialAuthExceptionMiddleware(SocialAuthExceptionMiddleware):
+    """Handles cases where python-social-auth throws an error."""
+    def process_exception(self, request, exception):
+        #if hasattr(social_exceptions, exception.__class__.__name__):
+            # TODO: handle this in a better way.
+        #    return HttpResponse(request.GET.get('next', 'blah'))
+        #else:
+            raise exception
diff --git a/reviewboard/accounts/pages.py b/reviewboard/accounts/pages.py
index 1364050ae49c6336e107660772d639b9d01be19a..bc28b4190dcec95dd929def0c727f170a38dc558 100644
--- a/reviewboard/accounts/pages.py
+++ b/reviewboard/accounts/pages.py
@@ -14,6 +14,7 @@ from reviewboard.accounts.forms.pages import (AccountSettingsForm,
                                               AvatarSettingsForm,
                                               APITokensForm,
                                               ChangePasswordForm,
+                                              ExternalAuthForm,
                                               ProfileForm,
                                               GroupsForm)
 
@@ -90,7 +91,7 @@ class AuthenticationPage(AccountPage):
 
     page_id = 'authentication'
     page_title = _('Authentication')
-    form_classes = [ChangePasswordForm]
+    form_classes = [ChangePasswordForm, ExternalAuthForm]
 
 
 class ProfilePage(AccountPage):
diff --git a/reviewboard/accounts/urls.py b/reviewboard/accounts/urls.py
index b391d3798e81675fc9ecf9f7f950bf412dac4129..2491bc04591c02ec8bb4cc24621e4aa8e2ace2ce 100644
--- a/reviewboard/accounts/urls.py
+++ b/reviewboard/accounts/urls.py
@@ -1,6 +1,6 @@
 from __future__ import unicode_literals
 
-from django.conf.urls import patterns, url
+from django.conf.urls import patterns, include, url
 
 from reviewboard.accounts.forms.auth import AuthenticationForm
 from reviewboard.accounts.views import MyAccountView
@@ -14,12 +14,13 @@ urlpatterns = patterns(
     url(r'^preferences/$',
         MyAccountView.as_view(),
         name="user-preferences"),
+    url(r'', include('social.apps.django_app.urls', namespace='social')),
 )
 
 urlpatterns += patterns(
     "django.contrib.auth.views",
 
-    url(r'^login/$', 'login',
+url(r'^login/$', 'login',
         {
             'template_name': 'accounts/login.html',
             'authentication_form': AuthenticationForm,
@@ -47,4 +48,6 @@ urlpatterns += patterns(
         'password_reset_complete',
         {'template_name': 'accounts/password_reset_complete.html'},
         name='password_reset_complete'),
+
+
 )
diff --git a/reviewboard/accounts/views.py b/reviewboard/accounts/views.py
index 96c42a275243fa027401fd4a6988286758d9481f..744ea2ed922ae2dd5063c8628281c9a069445a4c 100644
--- a/reviewboard/accounts/views.py
+++ b/reviewboard/accounts/views.py
@@ -12,10 +12,12 @@ from djblets.configforms.views import ConfigPagesView
 from djblets.siteconfig.models import SiteConfiguration
 from djblets.util.decorators import augment_method_from
 
+
 from reviewboard.accounts.backends import get_enabled_auth_backends
 from reviewboard.accounts.forms.registration import RegistrationForm
 from reviewboard.accounts.pages import AccountPage
 
+
 @csrf_protect
 def account_register(request, next_url='dashboard'):
     """Display the appropriate registration page.
diff --git a/reviewboard/admin/siteconfig.py b/reviewboard/admin/siteconfig.py
index e546d2f7a1d37098c2bd35789360c0e423479a05..d6b219c54965e2c31b2f42278fd463290a87593a 100644
--- a/reviewboard/admin/siteconfig.py
+++ b/reviewboard/admin/siteconfig.py
@@ -311,19 +311,20 @@ def load_site_config(full_reload=False):
         elif isinstance(custom_backends, list):
             custom_backends = tuple(custom_backends)
 
-        settings.AUTHENTICATION_BACKENDS = custom_backends
+        settings.AUTHENTICATION_BACKENDS[:] = custom_backends
 
         if builtin_backend not in custom_backends:
-            settings.AUTHENTICATION_BACKENDS += (builtin_backend,)
+            settings.AUTHENTICATION_BACKENDS += builtin_backend
     else:
         backend = auth_backends.get('backend_id', auth_backend_id)
-
         if backend and backend is not builtin_backend_obj:
-            settings.AUTHENTICATION_BACKENDS = \
-                ("%s.%s" % (backend.__module__, backend.__name__),
-                 builtin_backend)
+            settings.AUTHENTICATION_BACKENDS[:] = ["%s.%s" % (backend.__module__,
+                                                            backend.__name__),
+                                                            builtin_backend]
         else:
-            settings.AUTHENTICATION_BACKENDS = (builtin_backend,)
+            settings.AUTHENTICATION_BACKENDS[:] = [builtin_backend]
+
+
 
         # If we're upgrading from a 1.x LDAP configuration, populate
         # ldap_uid and clear ldap_uid_mask
@@ -362,9 +363,10 @@ def load_site_config(full_reload=False):
 
     # Add APITokenBackend to the list of auth backends. This one is always
     # present, and is used only for API requests.
-    settings.AUTHENTICATION_BACKENDS += (
+    settings.AUTHENTICATION_BACKENDS += [
         'reviewboard.webapi.auth_backends.TokenAuthBackend',
-    )
+        'social.backends.github.GithubOAuth2',
+    ]
 
     # Set the storage backend
     storage_backend = siteconfig.settings.get('storage_backend', 'builtin')
diff --git a/reviewboard/hostingsvcs/github.py b/reviewboard/hostingsvcs/github.py
index 931ad1d35252eb292b6c4dd511a2a37f46124054..e82f270ab4e0e3234b3d1f7254a615b3e5f5ec23 100644
--- a/reviewboard/hostingsvcs/github.py
+++ b/reviewboard/hostingsvcs/github.py
@@ -1,5 +1,6 @@
 from __future__ import unicode_literals
 
+import dateutil
 import hashlib
 import hmac
 import json
@@ -8,9 +9,12 @@ import re
 import uuid
 from collections import defaultdict
 
+
+import ipdb
 from django import forms
 from django.conf import settings
 from django.conf.urls import patterns, url
+from django.contrib.auth.models import User
 from django.contrib.sites.models import Site
 from django.core.cache import cache
 from django.core.exceptions import ObjectDoesNotExist
@@ -23,8 +27,11 @@ from django.utils.six.moves.urllib.parse import urljoin
 from django.utils.translation import ugettext_lazy as _
 from django.views.decorators.http import require_POST
 from djblets.siteconfig.models import SiteConfiguration
+from social.apps.django_app.default.models import UserSocialAuth
 
 from reviewboard.admin.server import build_server_url, get_server_url
+from reviewboard.diffviewer.forms import EmptyDiffForm
+from reviewboard.diffviewer.models import DiffCommit
 from reviewboard.hostingsvcs.bugtracker import BugTracker
 from reviewboard.hostingsvcs.errors import (AuthorizationError,
                                             HostingServiceError,
@@ -41,6 +48,7 @@ from reviewboard.hostingsvcs.service import (HostingService,
                                              HostingServiceClient)
 from reviewboard.hostingsvcs.utils.paginator import (APIPaginator,
                                                      ProxyPaginator)
+from reviewboard.reviews.models import ReviewRequest, ReviewRequestDraft
 from reviewboard.scmtools.core import Branch, Commit
 from reviewboard.scmtools.errors import FileNotFoundError, SCMError
 from reviewboard.site.urlresolvers import local_site_reverse
@@ -472,6 +480,7 @@ class GitHub(HostingService, BugTracker):
     supports_repositories = True
     supports_two_factor_auth = True
     supports_list_remote_repositories = True
+    supports_pull_requests = True
     supported_scmtools = ['Git']
 
     has_repository_hook_instructions = True
@@ -483,7 +492,10 @@ class GitHub(HostingService, BugTracker):
 
         url(r'^hooks/close-submitted/$',
             'reviewboard.hostingsvcs.github.post_receive_hook_close_submitted',
-            name='github-hooks-close-submitted')
+            name='github-hooks-close-submitted'),
+        url(r'^hooks/pull-request/$',
+            'reviewboard.hostingsvcs.github.post_receive_hook_pull_request',
+            name='github-hooks-pull-request')
     )
 
     # This should be the prefix for every field on the plan forms.
@@ -494,6 +506,14 @@ class GitHub(HostingService, BugTracker):
         '-granting-organization-access-on-github'
     )
 
+    # Possible statuses allowed by GitHub's Status API.
+    PR_STATUS_SUCCESS = 'success'
+    PR_STATUS_ERROR = 'error'
+    PR_STATUS_PENDING = 'pending'
+    PR_STATUS_FAILURE = 'failure'
+    PR_STATUSES = (PR_STATUS_SUCCESS, PR_STATUS_ERROR,
+                   PR_STATUS_FAILURE, PR_STATUS_PENDING)
+
     def get_api_url(self, hosting_url):
         """Returns the API URL for GitHub.
 
@@ -962,6 +982,34 @@ class GitHub(HostingService, BugTracker):
                 'hook_uuid': repository.get_or_create_hooks_uuid(),
             }))
 
+    def update_pull_request(self, review_request):
+        """Updates the status of the pull request created by the review request.
+
+        This will set the status of the latest commit in the pull request
+        on GitHub to either 'success' (if the review request is approved),
+         'error' (if any open issues exist), or 'pending' otherwise.
+
+        Args:
+            review_request (reviewboard.scmtools.models.ReviewRequest):
+                The review request that was created from a pull request.
+        """
+        if review_request.approved:
+            status = self.PR_STATUS_SUCCESS
+        elif review_request.issue_open_count > 0:
+            status = self.PR_STATUS_ERROR
+        else:
+            status = self.PR_STATUS_PENDING
+
+        ipdb.set_trace()
+
+        target_url = build_server_url(review_request.get_absolute_url())
+
+        self.api_set_status_for_commit(review_request.repository,
+                                       review_request.pull_request_commit_head,
+                                       status,
+                                       description='Posted from Review Board',
+                                       target_url=target_url)
+
     def _reset_authorization(self, client_id, client_secret, token):
         """Resets the authorization info for an OAuth app-linked token.
 
@@ -978,6 +1026,56 @@ class GitHub(HostingService, BugTracker):
                                     username=client_id,
                                     password=client_secret)
 
+    def api_set_status_for_commit(self, repository, commit, state,
+                                  description='', target_url=''):
+        """Set the status for a given commit.
+
+        GitHub allows commits to be marked with a status that is shown
+        in pull requests containing those commits.
+
+        Args:
+            repository (reviewboard.scmtools.models.Repository):
+                The repository that the commit belongs to.
+
+            commit (unicode):
+                The full ID of the commit for which to set the status.
+
+            state (unicode):
+                The status to set for the commit.
+
+                This must be one of the following values:
+
+                * :py:attr:`PR_STATUS_SUCCESS`
+                * :py:attr:`PR_STATUS_ERROR`
+                * :py:attr:`PR_STATUS_FAILURE`
+                * :py:attr:`PR_STATUS_PENDING`
+
+            description (unicode):
+                The description to associate with the status.
+
+            target_url (unicode, optional):
+                The HTTP(S) URL associated with the status.
+        """
+
+        if state not in self.PR_STATUSES:
+            raise ValueError(
+                'Must be one of: %s'
+                % ', '.join(self.PR_STATUSES))
+
+        status_url = self._build_api_url(
+            '%s/statuses/%s' % (
+            self._get_repo_api_url(repository), commit)
+        )
+
+        data = {
+            'state': state,
+            'description': description,
+            'target_url': target_url,
+        }
+
+        return self.client.json_post(status_url, json.dumps(data))
+
+
     def _delete_auth_token(self, auth_id, password, two_factor_auth_code=None):
         """Requests that an authorization token be deleted.
 
@@ -1032,6 +1130,111 @@ class GitHub(HostingService, BugTracker):
 
 
 @require_POST
+def post_receive_hook_pull_request(request, local_site_name=None,
+                                   repository_id=None,
+                                   hosting_service_id=None):
+    """Create or update review requests from pull requests.
+
+    Args:
+        request (django.http.HttpRequest)
+            The HttpRequest that called this hook.
+        local_site_name (unicode, optional)
+            The local site name.
+        repository_id (integer, optional)
+            The ID that uniquely identifies a repository.
+        hosting_service_id (unicode, optional)
+            The name of the hosting service the repository is hosted at.
+
+    Returns:
+        django.http.HttpResponse
+        Returns 200 OK if the review request was successfully updated,
+        otherwise returns a 400 Bad Request.
+    """
+
+    hook_event = request.META.get('HTTP_X_GITHUB_EVENT')
+
+    if hook_event == 'ping':
+        # A ping event is sent to verify the webhook works.
+        return HttpResponse()
+    elif hook_event != 'pull_request':
+        # We only want pull_request events, as they contain the right data.
+        return HttpResponseBadRequest(
+            'Only "pull_request" events are supported.')
+
+    repository = get_repository_for_hook(repository_id, hosting_service_id,
+                                         local_site_name)
+
+    invalid_response = verify_hook_identity(repository, request)
+    if invalid_response is not None:
+        return invalid_response
+
+    try:
+        payload = json.loads(request.body)
+    except ValueError:
+        return HttpResponseBadRequest('Invalid payload format.')
+
+    pr_data = payload['pull_request']
+
+    if payload['action'] == 'opened':
+        try:
+            print('finding a submitter')
+            submitter = _get_review_request_creator_from_pull_request(
+                repository, payload)
+        except ObjectDoesNotExist:
+            return HttpResponseBadRequest(
+                'No ReviewBoard user found matching the submitter '
+                'of the pull request.')
+
+        print('found a submitter')
+        rr = ReviewRequest.objects.create(submitter, repository)
+        rr.pull_request_id = pr_data['id']
+        rr.pull_request_commit_head = pr_data['head']['sha']
+
+        draft = rr.get_draft()
+        draft.summary = 'PR from Github - %s' % pr_data['title']
+        draft.description = pr_data['body']
+
+        try:
+            draft.diffset = _create_diffset_from_commits(
+                rr, repository, payload, request)
+        except SCMError as e:
+            logging.error('Unable to fetch commits: %s', e)
+            return HttpResponseBadRequest('Commits could not be retrieved.')
+
+        draft.save()
+        rr.save()
+    elif payload['action'] == 'synchronize':
+        try:
+            rr = ReviewRequest.objects.get(pull_request_id=pr_data['id'],
+                                           repository=repository)
+        except ReviewRequest.DoesNotExist:
+            return HttpResponseBadRequest(
+                'The corresponding review request does not exist.')
+
+        rr.pull_request_commit_head = pr_data['head']['sha']
+
+        # TODO: Should the summary and description be updated too?
+        draft = ReviewRequestDraft.create(rr)
+
+        try:
+            draft.diffset = _create_diffset_from_commits(
+                rr, repository, payload, request)
+        except SCMError as e:
+            logging.error('Unable to fetch commits: %s', e)
+            return HttpResponseBadRequest('Commits could not be retrieved.')
+
+        draft.save()
+        rr.save()
+
+        repository.hosting_service.api_set_status_for_commit(
+            repository, rr.pull_request_commit_head, 'pending')
+    else:
+        return HttpResponseBadRequest('Invalid action.')
+
+    return HttpResponse()
+
+
+@require_POST
 def post_receive_hook_close_submitted(request, local_site_name=None,
                                       repository_id=None,
                                       hosting_service_id=None):
@@ -1049,18 +1252,9 @@ def post_receive_hook_close_submitted(request, local_site_name=None,
     repository = get_repository_for_hook(repository_id, hosting_service_id,
                                          local_site_name)
 
-    # Validate the hook against the stored UUID.
-    m = hmac.new(bytes(repository.get_or_create_hooks_uuid()), request.body,
-                 hashlib.sha1)
-
-    sig_parts = request.META.get('HTTP_X_HUB_SIGNATURE').split('=')
-
-    if sig_parts[0] != 'sha1' or len(sig_parts) != 2:
-        # We don't know what this is.
-        return HttpResponseBadRequest('Unsupported HTTP_X_HUB_SIGNATURE')
-
-    if m.hexdigest() != sig_parts[1]:
-        return HttpResponseBadRequest('Bad signature.')
+    invalid_response = verify_hook_identity(repository, request)
+    if invalid_response is not None:
+        return invalid_response
 
     try:
         payload = json.loads(request.body)
@@ -1080,8 +1274,180 @@ def post_receive_hook_close_submitted(request, local_site_name=None,
     return HttpResponse()
 
 
+def verify_hook_identity(repository, request):
+    """Validate the webhook request by checking it against the stored UUID.
+
+    This ensures the web hook was actually called by GitHub after it was
+    set up by a user, not by anyone who makes a request to the hook URL.
+
+    Args:
+        repository (reviewboard.scamtools.models.Repository)
+            The repository the hook was registered in.
+
+        request (django.http.HttpRequest)
+            The HTTP request that called the hook.
+
+    Returns:
+        django.http.HttpResponseBadRequest
+        None if the hook was successfully validated, or an instance of
+        HttpResponseBadRequest containing the error message if unsuccessful.
+    """
+    # Currently returning None because HTTP_X_HUB_SIGNATURE is broken.
+    print('returning None')
+    return None
+
+    m = hmac.new(bytes(repository.get_or_create_hooks_uuid()), request.body,
+                 hashlib.sha1)
+
+    sig_parts = request.META.get('HTTP_X_HUB_SIGNATURE').split('=', 1)
+
+    if sig_parts[0] != 'sha1':
+        # We don't know what this is.
+        logging.error('Unknown HTTP_X_HUB_SIGNATURE: %s',
+                      request.META.get('HTTP_X_HUB_SIGNATURE'))
+        return HttpResponseBadRequest('X-Hub-Signature')
+
+    if m.hexdigest() != sig_parts[1]:
+        return HttpResponseBadRequest('Bad signature.')
+
+    return None
+
+
+def _get_review_request_creator_from_pull_request(repository, payload):
+    """Returns the user that the review request should be associated with.
+
+    The user will be the submitter of the review request that was automatically
+    created from the corresponding pull request.
+
+    Args:
+        repository (reviewboard.scmtools.models.Repository)
+            The repository the pull request was created in.
+        payload (dict)
+            The payload sent by GitHub when the pull request hook was called.
+
+    Returns:
+        django.contrib.auth.models.User
+        The user that created the review request.
+    """
+
+    payload_username = payload["sender"]["login"]
+
+    found_submitter = False
+    submitter = None
+
+    for social_user in UserSocialAuth.objects.filter(provider='github'):
+        if payload_username == social_user.extra_data['username']:
+            submitter = social_user.user
+            found_submitter = True
+            break
+
+    if not found_submitter:
+        created = False
+        suffix = ''
+        # This should find the email, but doesn't seem to be working.
+        #user_email, _ = repository.hosting_service.client.json_get(
+        #   payload['pull_request']['head']['user']['url'])['email']
+
+        while not created:
+            submitter, created = User.objects.get_or_create(
+                username=payload_username + suffix,
+                # TODO: find the user's email. Above does not work.
+                email='')
+
+            if created:
+                submitter.save()
+
+            elif suffix == '':
+                suffix = '0'
+
+            else:
+                suffix = str(int(suffix) + 1)
+
+        extra_data = {
+            'token_type': 'bearer',
+            'login': payload['sender']['login'],
+            'username': payload['sender']['login'],
+            'unclaimed': True
+
+        }
+        UserSocialAuth.objects.create(user=submitter,
+                                      provider='github',
+                                      uid=payload['sender']['id'],
+                                      extra_data=extra_data)
+
+    return submitter
+
+
+def _create_diffset_from_commits(review_request, repo, payload, request=None):
+    """Create a DiffSet to represent all the commits in a pull request.
+
+    Args:
+        review_request (reviewboard.reviews.models.ReviewRequest):
+            The review request associated with the pull request that
+            triggered the webhook.
+
+        repo (reviewboard.scmtools.models.Repository):
+            The repository that the review request is associated with.
+
+        payload (dict):
+            The request payload from Github as the result of triggering
+            the pull request webhook.
+
+        request (django.http.HttpRequest, optional):
+            The request the payload originated from. Defaults to None.
+
+    Returns:
+        reviewboard.diffviewer.models.DiffSet:
+        A DiffSet that represents all the commits in a pull request at the time
+        of the request.
+
+    Raises:
+        reviewboard.scmtools.errors.SCMError:
+            Raised when any of the commits referenced in the payload
+            could not be retrieved, or one of those commits are a merge commit.
+    """
+    diff_form = EmptyDiffForm(review_request, request=request)
+    diffset = diff_form.create()
+
+    commits_url = payload['pull_request']['commits_url']
+    commits_data = repo.hosting_service.client.api_get(commits_url)
+
+    for commit_data in commits_data:
+        if len(commit_data['parents']) > 1:
+            #  Merge commits are currently not supported by RB.
+            raise SCMError('Merge commits are not supported.')
+
+        commit = commit_data['commit']
+        author = commit['author']
+        committer = commit['committer']
+        parents = commit_data['parents']
+
+        DiffCommit.objects.create_from_data(
+            repository=repo,
+            diff_file_name='diff',
+            diff_file_contents=repo.hosting_service.get_change(
+                repo, commit_data['sha']).diff,
+            parent_diff_file_name=None,
+            parent_diff_file_contents=None,
+            request=request,
+            commit_id=commit_data['sha'],
+            parent_id=parents[0]['sha'],
+            merge_parent_ids=None,
+            author_name=author['name'],
+            author_email=author['email'],
+            author_date=dateutil.parser.parse(author['date']),
+            committer_name=committer['name'],
+            committer_email=committer['email'],
+            committer_date=dateutil.parser.parse(committer['date']),
+            description=commit['message'],
+            commit_type='change',
+            diffset=diffset)
+
+    return diffset
+
+
 def _get_review_request_id_to_commits_map(payload, server_url, repository):
-    """Returns a dictionary, mapping a review request ID to a list of commits.
+    """Returns a dict, mapping a review request ID to a list of commits.
 
     If a commit's commit message does not contain a review request ID,
     we append the commit to the key None.
diff --git a/reviewboard/hostingsvcs/service.py b/reviewboard/hostingsvcs/service.py
index 2185e7b679c93a042bd5635e3e8017e12564cd1f..294d5706fe44b11c4ee525f06ecebd3c40292e49 100644
--- a/reviewboard/hostingsvcs/service.py
+++ b/reviewboard/hostingsvcs/service.py
@@ -178,6 +178,7 @@ class HostingService(object):
     supports_two_factor_auth = False
     supports_list_remote_repositories = False
     has_repository_hook_instructions = False
+    supports_pull_requests = False
 
     self_hosted = False
     repository_url_patterns = None
@@ -428,6 +429,19 @@ class HostingService(object):
         """
         raise NotImplementedError
 
+    def update_pull_request(self, review_request):
+        """Updates the pull request's status.
+
+        This performs an update against the pull request
+        that created the review request.
+
+        Args:
+            review_request (reviewboard.scmtools.models.ReviewRequest):
+                The review request that was created from a pull request.
+        """
+        if not self.supports_pull_requests:
+            raise NotImplementedError
+
     @classmethod
     def get_bug_tracker_requires_username(cls, plan=None):
         if not cls.supports_bug_trackers:
diff --git a/reviewboard/hostingsvcs/tests/test_github.py b/reviewboard/hostingsvcs/tests/test_github.py
index ca553ce591d1f2eee0e2e303dc03353f2a1c9693..2dcea072f705fb094107d5658cca0d70ece77e4f 100644
--- a/reviewboard/hostingsvcs/tests/test_github.py
+++ b/reviewboard/hostingsvcs/tests/test_github.py
@@ -6,6 +6,7 @@ import json
 from hashlib import md5
 from textwrap import dedent
 
+from django.contrib.auth.models import User
 from django.core.exceptions import ObjectDoesNotExist
 from django.utils import six
 from django.utils.six.moves import cStringIO as StringIO
@@ -13,7 +14,9 @@ from django.utils.six.moves.urllib.error import HTTPError
 from django.utils.six.moves.urllib.parse import urlparse
 from djblets.testing.decorators import add_fixtures
 
+from reviewboard.diffviewer.models import DiffSet
 from reviewboard.scmtools.core import Branch
+from reviewboard.hostingsvcs import github
 from reviewboard.hostingsvcs.models import HostingServiceAccount
 from reviewboard.hostingsvcs.repository import RemoteRepository
 from reviewboard.hostingsvcs.tests.testcases import ServiceTests
@@ -33,6 +36,7 @@ class GitHubTests(ServiceTests):
         self.assertTrue(self.service_class.supports_bug_trackers)
         self.assertTrue(self.service_class.supports_repositories)
         self.assertFalse(self.service_class.supports_ssh_key_association)
+        self.assertTrue(self.service_class.supports_pull_requests)
 
     def test_public_field_values(self):
         """Testing the GitHub public plan repository field values"""
@@ -1190,6 +1194,229 @@ class GitHubTests(ServiceTests):
             HTTP_X_GITHUB_EVENT=event,
             HTTP_X_HUB_SIGNATURE='sha1=%s' % m.hexdigest())
 
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_pull_request_hook_ping(self):
+        """Testing GitHub pull_request hook ping"""
+        account = self._get_hosting_account()
+        account.save()
+
+        repository = self.create_repository(hosting_account=account)
+
+        url = local_site_reverse(
+            'github-hooks-pull-request',
+            kwargs={
+                'repository_id': repository.pk,
+                'hosting_service_id': 'github',
+            })
+
+        response = self._post_pull_request_payload(
+            url, repository.get_or_create_hooks_uuid(), 'opened', 0,
+            event='ping')
+        self.assertEqual(response.status_code, 200)
+
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_pull_request_hook_with_invalid_signature(self):
+        """Testing GitHub pull_request hook with invalid signature"""
+        account = self._get_hosting_account()
+        account.save()
+
+        repository = self.create_repository(hosting_account=account)
+
+        url = local_site_reverse(
+            'github-hooks-pull-request',
+            kwargs={
+                'repository_id': repository.pk,
+                'hosting_service_id': 'github',
+            })
+
+        response = self._post_pull_request_payload(
+            url, 'bad-secret', 'opened', 0,
+            event='synchronize')
+        self.assertEqual(response.status_code, 400)
+
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_pull_request_hook_with_invalid_action(self):
+        """Testing GitHub pull_request hook with an invalid action"""
+        account = self._get_hosting_account()
+        account.save()
+
+        repository = self.create_repository(hosting_account=account)
+
+        url = local_site_reverse(
+            'github-hooks-pull-request',
+            kwargs={
+                'repository_id': repository.pk,
+                'hosting_service_id': 'github',
+            })
+
+        response = self._post_pull_request_payload(
+            url, repository.get_or_create_hooks_uuid(), 'foo', 0)
+        self.assertEqual(response.status_code, 400)
+
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_pull_request_hook_with_invalid_event(self):
+        """Testing GitHub pull_request hook with non-pull request event"""
+        account = self._get_hosting_account()
+        account.save()
+
+        repository = self.create_repository(hosting_account=account)
+
+        url = local_site_reverse(
+            'github-hooks-pull-request',
+            kwargs={
+                'repository_id': repository.pk,
+                'hosting_service_id': 'github',
+            })
+
+        response = self._post_pull_request_payload(
+            url, repository.get_or_create_hooks_uuid(), 'opened', 0,
+            event='bar')
+        self.assertEqual(response.status_code, 400)
+
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_pull_request_hook_creates_review_request(self):
+        """Testing GitHub pull_request hook creates a review request"""
+        review_request = self._create_review_request_from_pull_request()
+        self.assertIsNotNone(review_request)
+
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_pull_request_hook_updates_review_request(self):
+        """Testing GitHub pull_request hook updates a review request"""
+        review_request = self._create_review_request_from_pull_request()
+        repository = review_request.repository
+        user = User.objects.get(email='admin@example.com')
+        review_request.publish(user=user)
+
+        self.assertIsNone(review_request.get_draft())
+
+        url = local_site_reverse(
+            'github-hooks-pull-request',
+            kwargs={
+                'repository_id': repository.pk,
+                'hosting_service_id': 'github',
+            })
+
+        self._post_pull_request_payload(
+            url, repository.get_or_create_hooks_uuid(), 'synchronize', 1337)
+
+        self.assertIsNotNone(review_request.get_draft())
+
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_pull_request_hook_sets_pending_status_on_new_review_request(self):
+        """Testing GitHub pull request created with a pending status."""
+        review_request = self._create_review_request_from_pull_request()
+        repository = review_request.repository
+
+        self.assertTrue(github.GitHub.api_set_status_for_commit.called_with(
+            repository, review_request.pull_request_commit_head, 'pending'))
+
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_pull_request_hook_sets_error_status_with_open_issue(self):
+        """Testing GitHub pull request set with error status with open issue"""
+        review_request = self._create_review_request_from_pull_request()
+        repository = review_request.repository
+
+        # Attach an open issue to this review request
+        user = User.objects.get(email='admin@example.com')
+        review = self.create_review(review_request, user=user)
+        self.create_general_comment(review, issue_opened=True)
+        review.publish()
+
+        self.assertEqual(
+            github.GitHub.api_set_status_for_commit.last_call.args,
+            (repository, review_request.pull_request_commit_head, 'error'))
+
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_pull_request_hook_sets_success_status_with_approval(self):
+        """Testing GitHub pull request set with success status when approved"""
+        review_request = self._create_review_request_from_pull_request()
+        repository = review_request.repository
+
+        # Add a ship-it to make it approved
+        self.assertTrue(review_request.issue_open_count == 0)
+        user = User.objects.get(email='admin@example.com')
+        review = self.create_review(review_request, user=user, ship_it=True)
+        review.publish()
+
+        self.assertEqual(
+            github.GitHub.api_set_status_for_commit.last_call.args,
+            (repository, review_request.pull_request_commit_head, 'success'))
+
+    def _create_review_request_from_pull_request(self):
+        """Creates a review request when a pull request is created."""
+        def _get_change(*args, **kwargs):
+            class FakeCommit:
+                def __init__(self):
+                    self.diff = (
+                        """diff --git a/asdfasdfasfd.txt b/asdfasdfasfd.txt
+                        new file mode 100644
+                        index 0000000..6afd275
+                        --- /dev/null
+                        +++ b/asdfasdfasfd.txt
+                        @@ -0,0 +1,9 @@
+                        +I""")
+
+            return FakeCommit()
+
+        def _create_from_data(*args, **kwargs):
+            return None
+
+        def _fake_user(*args):
+            return User.objects.get(email='admin@example.com')
+
+        account = self._get_hosting_account()
+        account.save()
+
+        repository = self.create_repository(hosting_account=account)
+
+        self.spy_on(github._get_review_request_creator_from_pull_request,
+                    call_fake=_fake_user)
+        self.spy_on(github.GitHub.get_change, call_fake=_get_change)
+        self.spy_on(github.GitHub.api_set_status_for_commit,
+                    call_original=False)
+        self.spy_on(DiffSet.objects.create_from_data,
+                    call_fake=_create_from_data)
+
+        url = local_site_reverse(
+            'github-hooks-pull-request',
+            kwargs={
+                'repository_id': repository.pk,
+                'hosting_service_id': 'github',
+            })
+
+        response = self._post_pull_request_payload(
+            url, repository.get_or_create_hooks_uuid(), 'opened', 1337)
+        self.assertEqual(response.status_code, 200)
+        review_request = ReviewRequest.objects.get(pull_request_id=1337)
+
+        return review_request
+
+    def _post_pull_request_payload(self, url, secret, action, pr_id,
+                                   event='pull_request'):
+        payload = json.dumps({
+            # NOTE: This payload only contains the content we make
+            #       use of in the hook.
+            'action': action,
+            'pull_request': {
+                'id': pr_id,
+                'state': 'open',
+                'title': 'Test PR',
+                'head': {
+                    'sha': 'fc393664cc4e94f0bdd1807b146176589a9a74eb'
+                },
+                'body': 'Test body'
+            }
+        })
+
+        m = hmac.new(bytes(secret), payload, hashlib.sha1)
+
+        return self.client.post(
+            url,
+            payload,
+            content_type='application/json',
+            HTTP_X_GITHUB_EVENT=event,
+            HTTP_X_HUB_SIGNATURE='sha1=%s' % m.hexdigest())
+
     def _test_check_repository(self, expected_user='myuser', **kwargs):
         def _http_get(service, url, *args, **kwargs):
             self.assertEqual(
diff --git a/reviewboard/reviews/builtin_fields.py b/reviewboard/reviews/builtin_fields.py
index 1e351350e5e149f4c6db45d18c92713a9faefca9..b97e62325d209bf158a5ed29c81e7b8dce0b1db0 100644
--- a/reviewboard/reviews/builtin_fields.py
+++ b/reviewboard/reviews/builtin_fields.py
@@ -482,6 +482,23 @@ class CommitField(BuiltinFieldMixin, BaseReviewRequestField):
             return escape(commit_id)
 
 
+class PullRequestField(BuiltinFieldMixin, BaseReviewRequestField):
+    """ The Pull Request field on a review request.
+
+    This displays the ID of the pull request the review request
+    was created from.
+    """
+
+    field_id = 'pull_request_id'
+    label = _('Pull Request')
+
+    def should_render(self, pull_request_id):
+        return bool(pull_request_id)
+
+    def render_value(self, pull_request_id):
+        return escape(pull_request_id)
+
+
 class DiffField(ReviewRequestPageDataMixin, BaseReviewRequestField):
     """Represents a newly uploaded diff on a review request.
 
@@ -986,6 +1003,7 @@ class InformationFieldSet(BaseReviewRequestFieldSet):
         BlocksField,
         ChangeField,
         CommitField,
+        PullRequestField,
     ]
 
 
diff --git a/reviewboard/reviews/evolutions/__init__.py b/reviewboard/reviews/evolutions/__init__.py
index 2f1314422fb393ad984f69d05c37d00705d2a5e4..64a0ead915ca7f1e6935cea4e500f4c1b36d130d 100644
--- a/reviewboard/reviews/evolutions/__init__.py
+++ b/reviewboard/reviews/evolutions/__init__.py
@@ -35,4 +35,5 @@ SEQUENCE = [
     'general_comments',
     'add_owner_to_draft',
     'status_update_timeout',
+    'pull_request'
 ]
diff --git a/reviewboard/reviews/evolutions/pull_request.py b/reviewboard/reviews/evolutions/pull_request.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c97799491e7ce25af6ebafb07d98b07a6ca8cc2
--- /dev/null
+++ b/reviewboard/reviews/evolutions/pull_request.py
@@ -0,0 +1,13 @@
+from django_evolution.mutations import AddField, ChangeMeta
+from django.db import models
+
+
+MUTATIONS = [
+    AddField('ReviewRequest', 'pull_request_id', models.CharField,
+             max_length=64, null=True, db_index=True),
+    ChangeMeta('ReviewRequest', 'unique_together',
+               ((u'commit_id', u'repository'),
+                (u'changenum', u'repository'),
+                (u'local_site', u'local_id'),
+                (u'pull_request_id', u'repository')))
+]
diff --git a/reviewboard/reviews/models/base_comment.py b/reviewboard/reviews/models/base_comment.py
index 0e5866e89daa1ae81992fb4e297289433d5d3a9a..ec4bd00263803087defc2dee066ab56085b4915f 100644
--- a/reviewboard/reviews/models/base_comment.py
+++ b/reviewboard/reviews/models/base_comment.py
@@ -8,6 +8,7 @@ from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
 from djblets.db.fields import CounterField, JSONField
 from djblets.db.managers import ConcurrencyManager
+from reviewboard.hostingsvcs.models import HostingServiceAccount
 
 
 @python_2_unicode_compatible
@@ -138,13 +139,17 @@ class BaseComment(models.Model):
             # the review.
             review = self.get_review()
 
+            review_request = review.review_request
+            hosting_svc_account = HostingServiceAccount.objects.get(
+                repositories__review_requests__reviews=review)
+
             if not review.public:
                 review.timestamp = self.timestamp
                 review.save()
             else:
                 if (not self.is_reply() and
-                    self.issue_opened and
-                    self._loaded_issue_status != self.issue_status):
+                        self.issue_opened and
+                        self._loaded_issue_status != self.issue_status):
                     # The user has toggled the issue status of this comment,
                     # so update the issue counts for the review request.
                     old_field = ReviewRequest.ISSUE_COUNTER_FIELDS[
@@ -159,8 +164,10 @@ class BaseComment(models.Model):
                             new_field: 1,
                         })
 
-                q = ReviewRequest.objects.filter(pk=review.review_request_id)
-                q.update(last_review_activity_timestamp=self.timestamp)
+                    hosting_svc_account.service.update_pull_request(
+                        review_request)
+                review_request.last_review_activity_timestamp = self.timestamp
+                review_request.save()
         except ObjectDoesNotExist:
             pass
 
diff --git a/reviewboard/reviews/models/review.py b/reviewboard/reviews/models/review.py
index 75008ed7fa0872550b5ada7c49352895e0a222cd..cab6f350c19c31665ad5b6b1565d946d401db078 100644
--- a/reviewboard/reviews/models/review.py
+++ b/reviewboard/reviews/models/review.py
@@ -11,6 +11,7 @@ from djblets.db.fields import CounterField, JSONField
 from djblets.db.query import get_object_or_none
 
 from reviewboard.diffviewer.models import DiffSet
+from reviewboard.hostingsvcs.models import HostingServiceAccount
 from reviewboard.reviews.managers import ReviewManager
 from reviewboard.reviews.models.base_comment import BaseComment
 from reviewboard.reviews.models.diff_comment import Comment
@@ -273,6 +274,13 @@ class Review(models.Model):
                                   user=user, review=self,
                                   to_submitter_only=to_submitter_only)
 
+            review_request = self.review_request
+            hosting_svc_account = HostingServiceAccount.objects.get(
+                repositories__review_requests__reviews=self)
+
+            hosting_svc_account.service.update_pull_request(
+                review_request)
+
     def delete(self):
         """Deletes this review.
 
diff --git a/reviewboard/reviews/models/review_request.py b/reviewboard/reviews/models/review_request.py
index a1ed3adc86639db35f81ebe7c4c7805a8f8ab5a3..183982058dd5fc604cbbbafe23299efa22a194c3 100644
--- a/reviewboard/reviews/models/review_request.py
+++ b/reviewboard/reviews/models/review_request.py
@@ -232,6 +232,9 @@ class ReviewRequest(BaseReviewRequestDetails):
                                         verbose_name=_('Dependencies'),
                                         related_name='blocks')
 
+    pull_request_id = models.CharField(_('Pull Request ID'),
+                                       max_length=64, blank=True,
+                                       null=True, db_index=True)
     # Review-related information
 
     # The timestamp representing the last public activity of a review.
@@ -324,8 +327,6 @@ class ReviewRequest(BaseReviewRequestDetails):
             ReviewRequest.objects.filter(pk=self.pk).update(
                 commit_id=six.text_type(self.changenum))
 
-            return self.commit_id
-
         return None
 
     def set_commit(self, commit_id):
@@ -368,6 +369,34 @@ class ReviewRequest(BaseReviewRequestDetails):
 
         return self._approval_failure
 
+    @property
+    def pull_request_commit_head(self):
+        """Return the commit ID of the head of the pull request.
+
+        Returns:
+            unicode
+            The sha of the latest commit in the pull request.
+        """
+        if self.extra_data is None:
+            self.extra_data = {}
+
+        return self.extra_data.get('pull_request_commit_head')
+
+    @pull_request_commit_head.setter
+    def pull_request_commit_head(self, commit_id):
+        """Set the commit ID of the head of the pull request.
+
+        Args:
+            commit_id (unicode)
+                The sha of the commit to set as the latest commit
+                of the pull request.
+        """
+        if self.extra_data is None:
+            self.extra_data = {}
+
+        self.extra_data['pull_request_commit_head'] = commit_id
+
+
     def get_participants(self):
         """Returns a list of users who have discussed this review request."""
         # See the comment in Review.get_participants for this list
@@ -1083,9 +1112,12 @@ class ReviewRequest(BaseReviewRequestDetails):
     class Meta:
         app_label = 'reviews'
         ordering = ['-last_updated', 'submitter', 'summary']
-        unique_together = (('commit_id', 'repository'),
-                           ('changenum', 'repository'),
-                           ('local_site', 'local_id'))
+        unique_together = (
+            ('commit_id', 'repository'),
+            ('changenum', 'repository'),
+            ('local_site', 'local_id'),
+            ('pull_request_id', 'repository')
+        )
         permissions = (
             ("can_change_status", "Can change status"),
             ("can_submit_as_another_user", "Can submit as another user"),
diff --git a/reviewboard/settings.py b/reviewboard/settings.py
index cf4fbb3d078e43d2e2170261d7f75d3629ef1f71..e1743f569a9342c15dced98cde2db3773e8ce64c 100644
--- a/reviewboard/settings.py
+++ b/reviewboard/settings.py
@@ -98,6 +98,7 @@ MIDDLEWARE_CLASSES = [
     'djblets.integrations.middleware.IntegrationsMiddleware',
     'djblets.log.middleware.LoggingMiddleware',
     'reviewboard.accounts.middleware.TimezoneMiddleware',
+    #'reviewboard.accounts.middleware.ExtendedSocialAuthExceptionMiddleware',
     'reviewboard.admin.middleware.CheckUpdatesRequiredMiddleware',
     'reviewboard.admin.middleware.X509AuthMiddleware',
     'reviewboard.site.middleware.LocalSiteMiddleware',
@@ -184,6 +185,7 @@ RB_BUILTIN_APPS = [
     'djblets.util',
     'haystack',
     'pipeline',  # Must be after djblets.pipeline
+    'social.apps.django_app.default',
     'reviewboard',
     'reviewboard.accounts',
     'reviewboard.admin',
@@ -220,8 +222,25 @@ WEB_API_AUTH_BACKENDS = (
     'djblets.webapi.auth.backends.basic.WebAPIBasicAuthBackend',
     'djblets.webapi.auth.backends.api_tokens.WebAPITokenAuthBackend',
 )
-
 SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
+AUTHENTICATION_BACKENDS = [
+    'django.contrib.auth.backends.ModelBackend',
+]
+
+
+SOCIAL_AUTH_GITHUB_EXTRA_DATA = ['username']
+SOCIAL_AUTH_GITHUB_SCOPE = ['user:email']
+SOCIAL_AUTH_PIPELINE = (
+    'social.pipeline.social_auth.social_details',
+    'social.pipeline.social_auth.social_uid',
+    'social.pipeline.social_auth.auth_allowed',
+    'reviewboard.social.pipeline.social_user',
+    'reviewboard.social.pipeline.associate_user',
+    'social.pipeline.social_auth.load_extra_data',
+    'social.pipeline.user.user_details',
+    'reviewboard.social.pipeline.update_user_info'
+)
+FIELDS_STORED_IN_SESSION = ['auth_type', ]
 
 # Set up a default cache backend. This will mostly be useful for
 # local development, as sites will override this.
@@ -301,12 +320,12 @@ HOSTINGSVCS_HOOK_REGEX_FLAGS = re.IGNORECASE
 SVNTOOL_BACKENDS = [
     'reviewboard.scmtools.svn.pysvn',
     'reviewboard.scmtools.svn.subvertpy',
+
 ]
 
 # Gravatar configuration.
 GRAVATAR_DEFAULT = 'mm'
 
-
 # Load local settings.  This can override anything in here, but at the very
 # least it needs to define database connectivity.
 try:
@@ -322,6 +341,7 @@ MIDDLEWARE_CLASSES += RB_EXTRA_MIDDLEWARE_CLASSES
 
 TEMPLATE_DEBUG = DEBUG
 
+
 if not LOCAL_ROOT:
     local_dir = os.path.dirname(settings_local.__file__)
 
@@ -357,6 +377,7 @@ HAYSTACK_CONNECTIONS = {
     },
 }
 
+
 # Make sure that we have a staticfiles cache set up for media generation.
 # By default, we want to store this in local memory and not memcached or
 # some other backend, since that will cause stale media problems.
diff --git a/reviewboard/social/__init__.py b/reviewboard/social/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/reviewboard/social/forms.py b/reviewboard/social/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..bace2509677c17ae249b7b5a4c253b1a4c10fb8e
--- /dev/null
+++ b/reviewboard/social/forms.py
@@ -0,0 +1,67 @@
+from __future__ import unicode_literals
+
+from djblets.configforms.forms import ConfigPageForm
+
+from django import forms
+from django.contrib import messages
+from django.forms import widgets
+from django.shortcuts import redirect,HttpResponseRedirect
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from reviewboard.accounts.backends import get_enabled_auth_backends
+
+import logging
+
+class UserInfoForm(forms.Form):
+    form_id = 'change_user_data'
+    save_label = ('Set User Credentials')
+
+    password1 = forms.CharField(
+        label=_('Password'),
+        required=True,
+        widget=widgets.PasswordInput())
+    password2 = forms.CharField(
+        label=_('Password (confirm)'),
+        required=True,
+        widget=widgets.PasswordInput())
+    email = forms.CharField(
+        label=_('Email'),
+        required=True)
+
+
+    def __init__(self,*args,**kwargs):
+        self.user = kwargs.pop('user')
+        super(UserInfoForm,self).__init__(*args,**kwargs)
+
+    def is_visible(self):
+        return True
+
+    def clean_password2(self):
+        """Validate the 'password2' field.
+
+        This makes sure that the two password fields match.
+        """
+        p1 = self.cleaned_data['password1']
+        p2 = self.cleaned_data['password2']
+
+        if p1 != p2:
+            raise forms.ValidationError(('Passwords do not match'))
+
+        return p2
+
+    def save(self):
+        print('save called')
+        """Save the form."""
+        backend = get_enabled_auth_backends()[0]
+        self.clean_password2()
+        try:
+            backend.update_password(self.user, self.cleaned_data['password1'])
+            self.user.email = self.cleaned_data['email']
+            self.user.save()
+        except Exception as e:
+            logging.error('Error when calling update_password or '
+                          'update_email for auth backend %r: %s',
+                          backend, e, exc_info=1)
+
+
diff --git a/reviewboard/social/pipeline.py b/reviewboard/social/pipeline.py
new file mode 100644
index 0000000000000000000000000000000000000000..5eab5e1c6dca28cb3cb6e2a3dd60108dcd2b4285
--- /dev/null
+++ b/reviewboard/social/pipeline.py
@@ -0,0 +1,116 @@
+from __future__ import unicode_literals
+from django.shortcuts import redirect
+
+from reviewboard.reviews.models import ReviewRequest, ReviewRequestDraft
+
+from social.exceptions import AuthAlreadyAssociated
+from social.pipeline.partial import partial
+
+
+# TODO: docstrings
+def social_user(backend, uid, user=None, *args, **kwargs):
+    """Replaces the standard python-social-auth social_user function.
+
+    Checks if the current social account is already associated in the site.
+    If the account is not an unclaimed account, throws
+    AuthAlreadyAssociated.
+    """
+    provider = backend.name
+    social = backend.strategy.storage.user.get_social_auth(provider, uid)
+    if social:
+        if user\
+            and social.user != user\
+            and (social.extra_data['unclaimed'] is not True):
+            msg = 'This claimed %s account is already in use.' % (provider)
+            raise AuthAlreadyAssociated(backend, msg)
+        elif not user:
+            user = social.user
+    return {
+        'social': social,
+        'user': user,
+        'is_new': user is None,
+    }
+
+@partial
+def update_user_info(backend, uid, user=None, social=None, *args, **kwargs):
+    print('updating user info')
+    if social:
+        print(social)
+        auth_type = backend.strategy.session_get('auth_type')
+        print(auth_type)
+        if auth_type == 'claim' and social.extra_data['unclaimed'] is True:
+            print('redirecting to user info form')
+            social.extra_data['unclaimed'] = False
+            social.save()
+            return redirect('user_info', username=user.username)
+
+
+def associate_user(backend, uid, user=None, social=None, *args, **kwargs):
+    """Replaces the standard python-social-auth associate_user function.
+
+    associates users and social-users according to the auth_type.
+
+    auth_type 'authenticate': if no collision: associates the social user
+    and user. If collision: checks if colliding user is generated and
+    unclaimed. If so, move all their review requests over and transfer the
+    social auth to the authenticating user.
+
+    auth_type 'claim': marks the user as claimed."""
+    auth_type = backend.strategy.session_get('auth_type')
+    print("AUTH TYPE: ")
+    print(auth_type)
+    print(backend.strategy.session_get('next'))
+
+    if auth_type == 'authenticate' and user and not social:
+        print('authenticating')
+        try:
+            social = backend.strategy.storage.user.create_social_auth(
+                user, uid, backend.name
+            )
+        except Exception as err:
+            if not backend.strategy.storage.is_integrity_error(err):
+                raise
+            return social_user(backend, uid, user, *args, **kwargs)
+
+        return {
+            'social': social,
+            'user': social.user,
+        }
+
+    # TODO: for merge and claim, cut the pipeline to get the user's desired
+    # email and password.
+    elif auth_type == 'authenticate' and user and social and\
+        social.extra_data['unclaimed']:
+        print("CLAIMING")
+        generated_user = social.user
+
+        # Claim the generated user's review requests and drafts.
+        if generated_user != user:
+            social.user = user
+            for o in (ReviewRequestDraft.objects.all()):
+                if o.submitter == generated_user:
+                    o.review_request.submitter = user
+                    o.save()
+            old_review_requests = ReviewRequest.objects.filter(
+                submitter=generated_user
+            )
+
+            for rr in old_review_requests:
+                print('switching')
+                rr.submitter = user
+                rr.save()
+
+            return {
+                'social': social,
+                'user': social.user,
+            }
+
+    elif auth_type == 'claim' and user and social and\
+        social.extra_data['unclaimed']:
+        return {
+            'social': social,
+            'user': social.user,
+        }
+
+    else:
+        return
diff --git a/reviewboard/social/urls.py b/reviewboard/social/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..41a68dfabda53b3236d984c7d056a28090a0dac7
--- /dev/null
+++ b/reviewboard/social/urls.py
@@ -0,0 +1,11 @@
+from __future__ import unicode_literals
+
+from django.conf.urls import include, patterns, url
+
+urlpatterns = patterns(
+    'reviewboard.social.views',
+
+    url(r'^usinfo/(?P<username>[0-9A-Za-z_\-]+)/$', 'user_info', name='user_info'),
+
+
+)
diff --git a/reviewboard/social/views.py b/reviewboard/social/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..150bdca1f8c9fb0e78153911860af2f5b42a5c63
--- /dev/null
+++ b/reviewboard/social/views.py
@@ -0,0 +1,33 @@
+from django.shortcuts import render_to_response, redirect
+from django.http import HttpResponseRedirect
+from django.utils.translation import ugettext_lazy as _
+from django.template.context import RequestContext
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+
+
+from djblets.util.decorators import augment_method_from
+from djblets.configforms.views import ConfigPagesView
+from django.views.decorators.csrf import csrf_protect
+from reviewboard.admin.decorators import superuser_required
+
+
+from .forms import UserInfoForm
+
+@csrf_protect
+def user_info(request, template_name='social/retrieve_user_data.html', username=None):
+    user = User.objects.get(username=username)
+    if request.method == 'POST':
+        form = UserInfoForm(request.POST, user=user)
+        print(form.is_valid())
+        if form.is_valid():
+            form.save()
+            print('about to return HttpResponseRedirect')
+            return redirect('/r/')
+    else:
+        print('not post')
+        form = UserInfoForm(user=user)
+
+    return render_to_response(template_name, RequestContext(request, {
+        'form': form,
+    }))
diff --git a/reviewboard/templates/accounts/claim_github.html b/reviewboard/templates/accounts/claim_github.html
new file mode 100644
index 0000000000000000000000000000000000000000..8ef0ac168ccf690b0393c58d7fb738c1ff2990be
--- /dev/null
+++ b/reviewboard/templates/accounts/claim_github.html
@@ -0,0 +1,36 @@
+{% load djblets_forms %}
+
+{% block pre_fields %}{% endblock %}
+
+{{form.non_field_errors}}
+
+{% for field in form %}
+{%  if field.is_hidden %}
+{{field}}
+{%  else %}
+{%   with field|form_field_has_label_first as label_first %}
+<div class="fields-row{% if field|is_checkbox_row %} checkbox-row{% endif %}">
+ <div class="field">
+{%    if label_first %}
+  {% label_tag field %}
+  {{field}}
+{%    else %}
+  {{field}}
+  {% label_tag field %}
+{%    endif %}
+  {{field.errors}}
+ </div>
+</div>
+{%   endwith %}
+{%  endif %}
+{% endfor %}
+
+{% block post_fields %}{% endblock %}
+
+{% if form.save_label %}
+<input type="submit" class="btn" value="{{form.save_label}}" />
+{% endif %}
+{% if not backends.associated %}
+<li><a href="{% url 'social:begin' 'github' %}?next={{ request.path }}">Claim Generated GitHub Account</a></li>
+{% endif %}
+
diff --git a/reviewboard/templates/accounts/external_service_auth.html b/reviewboard/templates/accounts/external_service_auth.html
new file mode 100644
index 0000000000000000000000000000000000000000..0b1c0bf344c740898b6b415f69b61bcc8c22cf25
--- /dev/null
+++ b/reviewboard/templates/accounts/external_service_auth.html
@@ -0,0 +1,36 @@
+{% load djblets_forms %}
+
+{% block pre_fields %}{% endblock %}
+
+{{form.non_field_errors}}
+
+{% for field in form %}
+{%  if field.is_hidden %}
+{{field}}
+{%  else %}
+{%   with field|form_field_has_label_first as label_first %}
+<div class="fields-row{% if field|is_checkbox_row %} checkbox-row{% endif %}">
+ <div class="field">
+{%    if label_first %}
+  {% label_tag field %}
+  {{field}}
+{%    else %}
+  {{field}}
+  {% label_tag field %}
+{%    endif %}
+  {{field.errors}}
+ </div>
+</div>
+{%   endwith %}
+{%  endif %}
+{% endfor %}
+
+{% block post_fields %}{% endblock %}
+
+{% if form.save_label %}
+<input type="submit" class="btn" value="{{form.save_label}}" />
+{% endif %}
+{% if not form.user_is_authenticated_on_github %}
+<a href="{% url 'social:begin' 'github' %}?next={{ request.path }}&auth_type={{'authenticate'}}">Authenticate With GitHub</a>
+{%  endif %}
+
diff --git a/reviewboard/templates/base/_nav_support_menu.html b/reviewboard/templates/base/_nav_support_menu.html
index 8be58800700d856cfdadb29c92a373143c181c2b..5ddf7935a097cd25fe25e8653a11397b1a067601 100644
--- a/reviewboard/templates/base/_nav_support_menu.html
+++ b/reviewboard/templates/base/_nav_support_menu.html
@@ -5,6 +5,9 @@
   <ul>
    <li><a href="{{RB_MANUAL_URL}}">{% trans "Documentation" %}</a></li>
    <li><a href="{% url 'support' %}">{% trans "Get Support" %}</a></li>
+   {% if not request.user.is_authenticated %}
+   <a href="{% url 'social:begin' 'github' %}?next={{ request.path }}&auth_type={{'claim'}}">Claim GitHub Account</a>
+   {%  endif %}
   </ul>
   </li>
 {% if request.user.is_authenticated %}
@@ -32,4 +35,5 @@
 {%  if auth_backends.0.supports_registration and siteconfig_settings.auth_enable_registration|default_if_none:1 %}
   <li><a href="{% url 'register' %}">{% trans "Register" %}</a></li>
 {%  endif %}
+
 {% endif %}{# !is_authenticated #}
diff --git a/reviewboard/templates/social/retrieve_user_data.html b/reviewboard/templates/social/retrieve_user_data.html
new file mode 100644
index 0000000000000000000000000000000000000000..2c75c06565e3e91e6aebaff382c903e87c0ce168
--- /dev/null
+++ b/reviewboard/templates/social/retrieve_user_data.html
@@ -0,0 +1,6 @@
+<form method="POST" action=".">
+    {% csrf_token %}
+    {{ form }}
+    <input type="submit" value="Submit" />
+</form>
+{{ form.errors }}
diff --git a/reviewboard/urls.py b/reviewboard/urls.py
index 6aee9dfe53a6b5a849e1e5d4f1c47e288db0ad5a..ae189c2f198f9f9aee8c766e2a8c7bd1a7c1eaa2 100644
--- a/reviewboard/urls.py
+++ b/reviewboard/urls.py
@@ -55,9 +55,11 @@ urlpatterns = patterns(
 
     url(r'^jsi18n/', 'djblets.util.views.cached_javascript_catalog',
         {'packages': ('reviewboard', 'djblets')},
-        name='js-catalog')
-)
+        name='js-catalog'),
+
+    url(r'', include('social.apps.django_app.urls', namespace='social'))
 
+)
 
 urlpatterns += extension_manager.get_url_patterns()
 
@@ -124,6 +126,7 @@ urlpatterns += patterns(
     '',
 
     (r'^account/', include('reviewboard.accounts.urls')),
+    (r'^social/', include('reviewboard.social.urls')),
 
     (r'^s/(?P<local_site_name>[A-Za-z0-9\-_.]+)/',
      include(localsite_urlpatterns)),
