diff --git a/reviewboard/hostingsvcs/github.py b/reviewboard/hostingsvcs/github.py
index ceef47528e4244bd82e4f68de63bfef662c6b7f4..1c162e0f00cd1b8ed748eded80f0f3e697345c57 100644
--- a/reviewboard/hostingsvcs/github.py
+++ b/reviewboard/hostingsvcs/github.py
@@ -1,6 +1,7 @@
 from __future__ import unicode_literals
 
 import hashlib
+import httplib
 import hmac
 import json
 import logging
@@ -11,9 +12,11 @@ from collections import defaultdict
 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
+from django.core.files.base import ContentFile
 from django.http import HttpResponse, HttpResponseBadRequest
 from django.template import RequestContext
 from django.template.loader import render_to_string
@@ -41,11 +44,13 @@ from reviewboard.hostingsvcs.service import (HostingService,
                                              HostingServiceClient)
 from reviewboard.hostingsvcs.utils.paginator import (APIPaginator,
                                                      ProxyPaginator)
+from reviewboard.reviews.forms import UploadDiffForm
+from reviewboard.reviews.models import ReviewRequest, ReviewRequestDraft
 from reviewboard.scmtools.core import Branch, Commit
 from reviewboard.scmtools.errors import FileNotFoundError, SCMError
+from reviewboard.scmtools.models import Repository
 from reviewboard.site.urlresolvers import local_site_reverse
 
-
 class GitHubPublicForm(HostingServiceForm):
     github_public_repo_name = forms.CharField(
         label=_('Repository name'),
@@ -472,6 +477,8 @@ class GitHub(HostingService, BugTracker):
     supports_repositories = True
     supports_two_factor_auth = True
     supports_list_remote_repositories = True
+    supports_pull_request_status_tagging = True
+    supports_pull_request_merge = True
     supported_scmtools = ['Git']
 
     has_repository_hook_instructions = True
@@ -479,11 +486,16 @@ class GitHub(HostingService, BugTracker):
     client_class = GitHubClient
 
     repository_url_patterns = patterns(
-        '',
+        'reviewboard.hostingsvcs.github',
 
         url(r'^hooks/close-submitted/$',
-            'reviewboard.hostingsvcs.github.post_receive_hook_close_submitted',
-            name='github-hooks-close-submitted')
+            'post_receive_hook_close_submitted',
+            name='github-hooks-close-submitted'),
+
+        url(r'^hooks/open-pull-request/$',
+            ('post_receive_hook_open_pull_request'),
+            name='github-hooks-open-pull-request'),
+
     )
 
     # This should be the prefix for every field on the plan forms.
@@ -572,6 +584,7 @@ class GitHub(HostingService, BugTracker):
                 'scopes': [
                     'user',
                     'repo',
+                    'repo:status',
                 ],
                 'note': note,
                 'note_url': site_url,
@@ -955,6 +968,22 @@ class GitHub(HostingService, BugTracker):
                 'hook_uuid': repository.get_or_create_hooks_uuid(),
             }))
 
+    def update_pull_request_status(self, review_request):
+        """
+        API call to sync pull request status to reflect review request's.
+
+        Args:
+            review_request (ReviewRequest):
+                Review Request used to synced with its corresponding pull
+                request
+        """
+        if review_request.approved:
+            status = 'success'
+        else:
+            status = 'pending'
+        self._tag_branch(review_request.extra_data['statuses_url'],
+                         status, review_request.repository, review_request.pk)
+
     def _reset_authorization(self, client_id, client_secret, token):
         """Resets the authorization info for an OAuth app-linked token.
 
@@ -1023,6 +1052,192 @@ class GitHub(HostingService, BugTracker):
     def _get_repository_name_raw(self, plan, extra_data):
         return self.get_plan_field(plan, extra_data, 'repo_name')
 
+    def _process_payload(self, payload):
+        """
+        Parse the payload for relevant information to open a review request.
+
+        Receives a payload from GitHub and extracts pull request description,
+        relevant URLs, and ID.
+
+        Args:
+            payload (Dictionary)
+                GitHub payload automatically delivered when webhook is
+                triggered.
+        """
+        return {
+            'summary': payload['pull_request']['title'],
+            'description': (payload['pull_request']['body'] +
+                            '\n \nLink to Pull Request:\n' +
+                            payload['pull_request']['html_url']),
+            'pull_request_id': payload['pull_request']['id'],
+        }
+
+    def _create_diffset_from_payload(self, payload, repository,
+                                     review_request):
+        """
+        Creates a diffset from GitHub payload's diff file.
+
+        Args:
+            payload (Dictionary):
+                GitHub payload automatically delivered when webhook is
+                triggered.
+
+            repository (Repository):
+                Used to get commit for file.
+
+            review_request (ReviewRequest):
+                Review request to associate Diff with.
+        """
+        commit = self.get_change(repository,
+                                 payload['pull_request']['head']['sha'])
+        file_name = 'pull-request-%s.diff' % payload['pull_request']['id']
+        form = UploadDiffForm(review_request, files={'path':
+                              ContentFile(commit.diff,
+                                          name=file_name)})
+        # Create a request object and set the files an object
+
+        if not form.is_valid():
+            raise Exception
+
+        try:
+            diffset = form.create(form.files['path'])
+        except Exception as e:
+            logging.exception("Error uploading new diff: %s", e)
+
+        return diffset
+
+    def _get_auth_token(self, repository):
+        """
+        Returns GitHubClient instance and auth token to make API calls.
+
+        Args:
+            repository (Repository):
+                Used to get hosting service to instantiate GitHubClient
+        """
+        ghc = GitHubClient(repository.hosting_service)
+        token = 'token ' + ghc.account.data['authorization']['token']
+        return ghc, token
+
+    def _tag_branch(self, status_url, state, repository, review_request_pk,
+                    target_url='http://localhost:8080/r/'):
+        """
+        Uses GitHubClient to make API call to tag branches.
+
+        Status tags include the review requests's PK in the description, so
+        that it can be looked up later when updating. Target URL is a pointer
+        to the review request. State can be one of: pending, success, error,
+        or failure.
+
+        Args:
+            status_url (String):
+                API route to send POST request to tag status.
+
+            state (String):
+                Can be pending, success, error, or failure.
+
+            repository (Repository):
+                Used to get GitHubClient instance and auth token.
+
+            review_request_pk (Integer):
+                Saved into the description of the commit tag to be used for
+                lookup.
+
+        Option Args:
+            target_url (String):
+                Pointer to the review request.
+        """
+        ghc, token = self._get_auth_token(repository)
+
+        # TODO: change the target URL
+
+        ghc.api_post(status_url,
+            json.dumps({
+                'state': state,
+                'target_url': target_url + str(review_request_pk),
+                'description': str(review_request_pk)
+            }),
+            headers={
+                'Authorization': token
+            })
+
+    def _get_review_request_pk(self, ghc, token, commits_url, status_url):
+        """
+        Gets the Review Request PK associated with a pull request
+
+        Uses Status API to look up description of commit status, where the
+        PK is stored, in _tag_branch.
+
+        Args:
+            ghc (GitHubClient):
+                GitHubClient instance used to make api calls.
+
+            token (String):
+                Authorization token used for GitHub API calls.
+
+            commits_url (String):
+                API route to look up commit details.
+
+            statuses_url (String):
+                API route to look up tagged commit status details.
+        """
+        commits = ghc.api_get(commits_url,
+                              headers={'Authorization': token})
+
+        # Gets first parent of latest commit to locate review request.
+        # TODO: Handle the case of multiple parents.
+        parent_sha = commits[-1]['parents'][0]['sha']
+
+        # -5 is to strip {sha} in: /repos/:user/:repo/statuses/{sha}.
+        # Result is the base URL of Status API: /repos/:user/:repo/statuses/
+        status_api_base = status_url[:-5]
+
+        # PK of the review request, since it's stored in description.
+        return int(ghc.api_get(status_api_base + parent_sha,
+                   headers={'Authorization': token})[0]['description'])
+
+    def merge_pull_request(self, review_request):
+        """
+        Make API call to merge pull request if review request is approved.
+
+        The pr_url should be in format: /repos/:owner/:repo/pulls/:number and
+        the HTTP Request must be PUT.
+
+        Args:
+            review_request (ReviewRequest):
+                Review Request and corresponding pull request that will be
+                merged.
+        """
+        if not(review_request.approved):
+            raise AttributeError("Review Request is not approved.")
+
+        repository = review_request.repository
+        ghc, token = self._get_auth_token(repository)
+        url = review_request.extra_data['pr_url'] + '/merge'
+
+        try:
+            response = ghc.http_request(url,
+                                        body=json.dumps(
+                                            {'commit_message':
+                                             'Merged with Review Board'}),
+                                        headers={'Authorization': token},
+                                        method='PUT')
+
+            # Response is tuple of strings. Convert to JSON.
+            response = json.loads(response[0])
+
+            if response['merged']:
+                review_request.close(ReviewRequest.SUBMITTED)
+
+            return response
+        except HTTPError as e:
+            if e.code == httplib.METHOD_NOT_ALLOWED:
+                logging.warning('Pull Request is not mergeable.')
+            elif e.code == httplib.CONFLICT:
+                logging.warning('Head branch was modified. Review and try \
+                                the merge again.')
+
+            raise SCMError(six.text_type(e))
+
 
 @require_POST
 def post_receive_hook_close_submitted(request, local_site_name=None,
@@ -1073,6 +1288,135 @@ def post_receive_hook_close_submitted(request, local_site_name=None,
     return HttpResponse()
 
 
+@require_POST
+def post_receive_hook_open_pull_request(request, local_site_name=None,
+                                        repository_id=None,
+                                        hosting_service_id=None):
+    """
+    Open or synchronize a review request when the webhook is triggered.
+
+    Uses parsed GitHub payload data to open/sync a review request. User is
+    queried using their GitHub email. Extra data stores statuses and pull
+    request url for updating purposes. Summary and description are taken from
+    the title and body of the pull request, respectively.
+
+    After the review request is opened/synced, the pull request will be tagged
+    with a status reflecting its review request approval status. The commit
+    status has a link to the review request and the PK of the review request
+    in the commit status description.
+
+    For updates, review request is found by looking at description field of the
+    commit status tag, since it contains the PK of the review request.
+    After the review request is found, all fields are updated with latest
+    payload data.
+
+    Args:
+        request (WSGIRequest):
+            Incoming request from GitHub. Request body is parsed to open/sync
+            review request.
+
+    Option Args:
+        local_site_name (String):
+            Local site name, passed from URL
+
+        repository_id (Integer):
+            PK of the repository, passed from URL
+
+        hosting_service_id (String):
+            Name of Hosting Service class, passed from URL
+    """
+    hook_event = request.META.get('HTTP_X_GITHUB_EVENT')
+
+    try:
+        payload = json.loads(request.body)
+    except ValueError as e:
+        logging.error('The payload is not in JSON format: %s', e)
+        return HttpResponseBadRequest('Invalid payload format')
+
+    if hook_event == 'pull_request':
+
+        # Get relevant objects and data to create/update review request.
+        git_path = payload['pull_request']['head']['repo']['git_url']
+        repository = Repository.objects.get(path=git_path)
+        gh = repository.hosting_service
+        data = gh._process_payload(payload)
+        summary, description = data['summary'], data['description']
+        ghc, token = gh._get_auth_token(repository)
+
+        if payload['action'] == 'opened':
+
+            gh_user = ghc.api_get('https://api.github.com/users/' +
+                                  payload['pull_request']['user']['login'],
+                                  headers={'Authorization': token})
+
+            try:
+                user = User.objects.get(email=gh_user['email'])
+            except Exception as e:
+                # Must handle case where they do not exist
+                logging.warning('User cannot be found.')
+                raise e
+
+            extra_data_dict = json.dumps({
+                'id': data['pull_request_id'],
+                'statuses_url': payload['pull_request']['statuses_url'],
+                'pr_url': payload['pull_request']['url']
+            })
+
+            review_request = \
+                ReviewRequest.objects.create(user,
+                                             repository,
+                                             local_site=local_site_name)
+            review_request.summary = summary
+            review_request.description = description
+            review_request.extra_data = extra_data_dict
+            review_request.save()
+
+            diffset = gh._create_diffset_from_payload(payload, repository,
+                                                      review_request)
+
+            review_request.diffset_history.diffsets.add(diffset)
+
+            # Tag the branch with pending status.
+            gh._tag_branch(payload['pull_request']['statuses_url'],
+                           'pending',
+                           repository,
+                           review_request.pk)
+        elif payload['action'] == 'synchronize':
+            # Lookup review request using info stored in Status API, then
+            # update all fields.
+            review_request_pk = gh._get_review_request_pk(
+                                   ghc,
+                                   token,
+                                   payload['pull_request']['commits_url'],
+                                   (payload['pull_request']['head']
+                                    ['repo']['statuses_url'])
+                                   )
+            review_request = ReviewRequest.objects.get(pk=review_request_pk)
+
+            review_request_draft = ReviewRequestDraft.create(review_request)
+
+            # Overwrite summary and description.
+            review_request_draft.summary = summary
+            review_request_draft.description = description
+            diffset = gh._create_diffset_from_payload(payload, repository,
+                                                      review_request)
+            review_request_draft.diffset = diffset
+            review_request_draft.save()
+
+            # Extra data updated to review request directly because it is not
+            # copied over in draft publishing.
+            review_request.extra_data['statuses_url'] = \
+                payload['pull_request']['statuses_url']
+            review_request.save()
+
+            gh._tag_branch(payload['pull_request']['statuses_url'],
+                           'pending',
+                           repository,
+                           review_request.pk)
+
+        return HttpResponse('Successfully opened a review request.')
+
+
 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.
 
diff --git a/reviewboard/hostingsvcs/service.py b/reviewboard/hostingsvcs/service.py
index 90aa9d884c3c7fd1256a035fcd3ab18b482af214..c98aa43a83f51999b5d1cbb8da0188cc345077ef 100644
--- a/reviewboard/hostingsvcs/service.py
+++ b/reviewboard/hostingsvcs/service.py
@@ -382,6 +382,18 @@ class HostingService(object):
         """
         raise NotImplementedError
 
+    def update_pull_request_status(self, review_request):
+        """Updates the pull request state of external repository services, if
+        the services have the capability for tracking commit or pull request
+        states.
+
+        Pull request state should reflect if review requests are
+        ready to be landed.
+
+        This should be implemented by subclasses.
+        """
+        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..8a4b964ad4a2ecfcd5a45e4d197ff65b4148fce7 100644
--- a/reviewboard/hostingsvcs/tests/test_github.py
+++ b/reviewboard/hostingsvcs/tests/test_github.py
@@ -15,9 +15,10 @@ from djblets.testing.decorators import add_fixtures
 
 from reviewboard.scmtools.core import Branch
 from reviewboard.hostingsvcs.models import HostingServiceAccount
+from reviewboard.hostingsvcs.github import GitHub
 from reviewboard.hostingsvcs.repository import RemoteRepository
 from reviewboard.hostingsvcs.tests.testcases import ServiceTests
-from reviewboard.reviews.models import ReviewRequest
+from reviewboard.reviews.models import ReviewRequest, ReviewRequestDraft
 from reviewboard.scmtools.errors import SCMError
 from reviewboard.scmtools.models import Repository
 from reviewboard.site.models import LocalSite
@@ -1132,6 +1133,66 @@ class GitHubTests(ServiceTests):
         self.assertEqual(review_request.status, review_request.PENDING_REVIEW)
         self.assertEqual(review_request.changedescs.count(), 0)
 
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_post_pull_request_open(self):
+        """
+        Testing GitHub.post_recieve_hook_open_pull_request properly creates
+        and synchronizes pull requests
+        """
+        account = self._get_hosting_account()
+        account.save()
+        service = account.service
+        repository = self.create_repository(hosting_account=account)
+        num_review_request_before = len(ReviewRequest.objects.all())
+        diffset = self.create_diffset(repository=repository)
+
+        url = local_site_reverse(
+            'github-hooks-open-pull-request',
+            kwargs={
+                'repository_id': repository.pk,
+                'hosting_service_id': 'github',
+            })
+
+        self.spy_on(Repository.objects.get,
+                    call_fake=lambda a, path: repository)
+        self.spy_on(GitHub._create_diffset_from_payload,
+                    call_fake=lambda a, b, c, d: diffset)
+        self.spy_on(GitHub._tag_branch, call_original=False)
+        self.spy_on(GitHub._get_auth_token,
+                    lambda a, b: (service.client, 'AUTH_TOKEN'))
+        self.spy_on(service.client.api_get,
+                    lambda a, b, headers: {'email': 'admin@example.com'})
+
+        # Send an open action payload.
+        self._post_pull_request_payload(url, 'opened', 'title',
+                                        'Pull request body')
+        num_review_request_after = len(ReviewRequest.objects.all())
+
+        # Get data from most recently created review request, which should
+        # be the one created from the pull request hook.
+        review_request = ReviewRequest.objects.last()
+        self.assertEqual(num_review_request_before + 1,
+                         num_review_request_after)
+        self.assertEqual(review_request.summary, 'title')
+
+        self.spy_on(GitHub._get_review_request_pk,
+                    lambda a, b, c, d, e: review_request.pk)
+
+        # Send a synchronize action payload.
+        self._post_pull_request_payload(url, 'synchronize', 'SYNC title',
+                                        'SYNC Pull request body')
+
+        draft = ReviewRequestDraft.create(review_request)
+        draft.publish()
+        # Need to re-load the object after it is saved.
+        review_request = ReviewRequest.objects.get(pk=review_request.pk)
+
+        # Check that we still have the same number of review requests. Only
+        # want to sync, not create new one.
+        self.assertEqual(num_review_request_after,
+                         len(ReviewRequest.objects.all()))
+        self.assertEqual(review_request.summary, 'SYNC title')
+
     def _test_post_commit_hook(self, local_site=None, publish=True):
         account = self._get_hosting_account(local_site=local_site)
         account.save()
@@ -1190,6 +1251,42 @@ class GitHubTests(ServiceTests):
             HTTP_X_GITHUB_EVENT=event,
             HTTP_X_HUB_SIGNATURE='sha1=%s' % m.hexdigest())
 
+    def _post_pull_request_payload(self, url, action, title, body,
+                                   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': '1',
+                'title': title,
+                'body': body,
+                'url': 'https://github.com/repos/user/repo_name/1',
+                'html_url': 'https://github.com/repos/user/repo_name',
+                'diff_url': 'https://github.com/repos/user/repo_name/1.diff',
+                'statuses_url':
+                    'https://github.com/repos/user/repo_name/status',
+                'commits_url':
+                    'https://github.com/repos/user/repo_name/commits',
+                'user': {
+                    'login': 'gh_user',
+                },
+                'head': {
+                    'repo': {
+                        'git_url': 'foo',
+                        'statuses_url':
+                            'https://github.com/repos/user/repo_name/status',
+                    }
+                }
+            }
+        })
+
+        return self.client.post(
+            url,
+            payload,
+            content_type='application/json',
+            HTTP_X_GITHUB_EVENT=event)
+
     def _test_check_repository(self, expected_user='myuser', **kwargs):
         def _http_get(service, url, *args, **kwargs):
             self.assertEqual(
diff --git a/reviewboard/reviews/models/base_comment.py b/reviewboard/reviews/models/base_comment.py
index 0e5866e89daa1ae81992fb4e297289433d5d3a9a..85cd63f0d6292e6a50ddb0898227d59f1d93fb81 100644
--- a/reviewboard/reviews/models/base_comment.py
+++ b/reviewboard/reviews/models/base_comment.py
@@ -161,6 +161,15 @@ class BaseComment(models.Model):
 
                 q = ReviewRequest.objects.filter(pk=review.review_request_id)
                 q.update(last_review_activity_timestamp=self.timestamp)
+                review_request = self.get_review_request()
+                hosting_service = review_request.repository.hosting_service
+
+                if not hosting_service:
+                    raise Exception('Hosting Service does not exist.')
+
+                if hosting_service.supports_pull_request_status_tagging:
+                    hosting_service.update_pull_request_status(review_request)
+
         except ObjectDoesNotExist:
             pass
 
diff --git a/reviewboard/reviews/models/review.py b/reviewboard/reviews/models/review.py
index 3ea9f5133a8a41651e64dfb59c101045a22c8533..b72ecb9cace315be74cbb304183d2c5b82ebc242 100644
--- a/reviewboard/reviews/models/review.py
+++ b/reviewboard/reviews/models/review.py
@@ -23,7 +23,6 @@ from reviewboard.reviews.models.screenshot_comment import ScreenshotComment
 from reviewboard.reviews.signals import (reply_publishing, reply_published,
                                          review_publishing, review_published)
 
-
 @python_2_unicode_compatible
 class Review(models.Model):
     """A review of a review request."""
@@ -271,6 +270,13 @@ class Review(models.Model):
                                   user=user, review=self,
                                   to_submitter_only=to_submitter_only)
 
+        hosting_service = self.review_request.repository.hosting_service
+
+        if not hosting_service:
+            raise Exception('Hosting Service does not exist.')
+        elif hosting_service.supports_pull_request_status_tagging:
+            hosting_service.update_pull_request_status(self.review_request)
+
     def delete(self):
         """Deletes this review.
 
diff --git a/reviewboard/reviews/urls.py b/reviewboard/reviews/urls.py
index 1403f0bfced9c60e56e81f9328d52c65144e6cb6..fbafc10ce0c463f1e980734329b5a037fa61eb46 100644
--- a/reviewboard/reviews/urls.py
+++ b/reviewboard/reviews/urls.py
@@ -70,6 +70,11 @@ review_request_urls = patterns(
     # Review request diffs
     url(r'^diff/', include(diffviewer_urls)),
 
+    # Review request github pull request merge
+    url(r'^merge',
+        'hostingsvcs_merge',
+        name='hostingsvcs-merge'),
+
     # Fragments
     url(r'^fragments/diff-comments/(?P<comment_ids>[0-9,]+)/$',
         'comment_diff_fragments'),
diff --git a/reviewboard/reviews/views.py b/reviewboard/reviews/views.py
index 91a51241dc598c214215d813bd252f500b44e841..306ce15dbc0b24b8da192ecf56723e375485ef45 100644
--- a/reviewboard/reviews/views.py
+++ b/reviewboard/reviews/views.py
@@ -47,6 +47,7 @@ from reviewboard.diffviewer.models import DiffSet
 from reviewboard.diffviewer.views import (DiffFragmentView, DiffViewerView,
                                           exception_traceback_string)
 from reviewboard.hostingsvcs.bugtracker import BugTracker
+from reviewboard.hostingsvcs.errors import HostingServiceError
 from reviewboard.reviews.ui.screenshot import LegacyScreenshotReviewUI
 from reviewboard.reviews.context import (comment_counts,
                                          diffsets_with_comments,
@@ -67,6 +68,7 @@ from reviewboard.site.urlresolvers import local_site_reverse
 from reviewboard.webapi.encoder import status_to_string
 
 
+
 #
 # Helper functions
 #
@@ -765,6 +767,10 @@ def review_detail(request,
 
     siteconfig = SiteConfiguration.objects.get_current()
 
+    hosting_service = review_request.repository.hosting_service
+    if not hosting_service:
+        raise Exception
+
     context_data = make_review_request_context(request, review_request, {
         'blocks': blocks,
         'draft': draft,
@@ -779,6 +785,8 @@ def review_detail(request,
         'close_description_rich_text': close_description_rich_text,
         'issues': issues,
         'has_diffs': (draft and draft.diffset_id) or len(diffsets) > 0,
+        'mergeable': review_request.approved and
+                     (hosting_service.supports_pull_request_merge),
         'file_attachments': latest_file_attachments,
         'all_file_attachments': file_attachments,
         'screenshots': screenshots,
@@ -791,6 +799,43 @@ def review_detail(request,
     return response
 
 
+@check_login_required
+@check_local_site_access
+def hostingsvcs_merge(request, review_request_id, local_site=None):
+    """
+    XXX
+    Merges pull request corresponding to review request, if services permits.
+
+    TEMPORARY: Uses url routing to land review request. This is temporary and
+    should be moved to use API to land review request.
+
+    Args:
+        review_request_id (Integer):
+            Used to look up review request to land.
+
+    Option Args:
+        local_site (LocalSite):
+            Used to look up review request to land.
+    """
+    review_request, response = _find_review_request(
+        request, review_request_id, local_site)
+    hosting_service = review_request.repository.hosting_service
+    # TODO: Case where not GitHub.
+    # Case where merge failed?
+
+    if not (hosting_service):
+        raise Exception
+    elif not (hosting_service.supports_pull_request_merge):
+        raise HostingServiceError('Hosting Service does not support \
+            pull request merge')
+
+    if review_request.approved:
+        hosting_service.merge_pull_request(review_request)
+        return root(request)
+
+    # TODO: Should return message if cannot be merged.
+    return root(request)
+
 class ReviewsDiffViewerView(DiffViewerView):
     """Renders the diff viewer for a review request.
 
diff --git a/reviewboard/templates/reviews/review_detail.html b/reviewboard/templates/reviews/review_detail.html
index 40153a43a669aeabab9b459171540e8ad96fa347..2334ea55255f4be1922647946959f7ff2e9efc80 100644
--- a/reviewboard/templates/reviews/review_detail.html
+++ b/reviewboard/templates/reviews/review_detail.html
@@ -46,6 +46,10 @@
 {%   if has_diffs %}
       <li><a href="diff/raw/">{% trans "Download Diff" %}</a></li>
 {%   endif %}
+{%   if mergeable %}
+      <li><a href="merge">{% trans "Merge" %}</a></li>
+{%   endif %}
+
 {%   include "reviews/review_request_actions_primary.html" %}
      </ul>
     </li>
