diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 76a904a..826b2a0 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -1,11 +1,13 @@
-from mitxmako.shortcuts import render_to_response
-from xmodule.modulestore.django import modulestore
-from django_future.csrf import ensure_csrf_cookie
-from django.http import HttpResponse
 import json
 
+from django.http import HttpResponse
+from django_future.csrf import ensure_csrf_cookie
 from fs.osfs import OSFS
 
+from mitxmako.shortcuts import render_to_response
+from xmodule.modulestore.django import modulestore
+
+
 @ensure_csrf_cookie
 def index(request):
     # TODO (cpennington): These need to be read in from the active user
diff --git a/cms/djangoapps/github_sync/__init__.py b/cms/djangoapps/github_sync/__init__.py
new file mode 100644
index 0000000..c6bbca2
--- /dev/null
+++ b/cms/djangoapps/github_sync/__init__.py
@@ -0,0 +1,40 @@
+from git import Repo
+from contentstore import import_from_xml
+from fs.osfs import OSFS
+
+
+def import_from_github(repo_settings):
+    """
+    Imports data into the modulestore based on the XML stored on github
+
+    repo_settings is a dictionary with the following keys:
+        path: file system path to the local git repo
+        branch: name of the branch to track on github
+        org: name of the 
+    """
+    repo_path = repo_settings['path']
+    git_repo = Repo(repo_path)
+    origin = git_repo.remotes.origin
+    origin.fetch()
+
+    # Do a hard reset to the remote branch so that we have a clean import
+    git_repo.heads[repo_settings['branch']].checkout()
+    git_repo.head.reset('origin/%s' % repo_settings['branch'], index=True, working_tree=True)
+
+    return git_repo.head.commit.hexsha, import_from_xml(repo_settings['org'], repo_settings['course'], repo_path)
+
+
+def export_to_github(course, repo_path, commit_message):
+    fs = OSFS(repo_path)
+    xml = course.export_to_xml(fs)
+
+    with fs.open('course.xml', 'w') as course_xml:
+        course_xml.write(xml)
+
+    git_repo = Repo(repo_path)
+    if git_repo.is_dirty():
+        git_repo.git.add(A=True)
+        git_repo.git.commit(m=commit_message)
+
+        origin = git_repo.remotes.origin
+        origin.push()
diff --git a/cms/djangoapps/github_sync/views.py b/cms/djangoapps/github_sync/views.py
new file mode 100644
index 0000000..8bf654f
--- /dev/null
+++ b/cms/djangoapps/github_sync/views.py
@@ -0,0 +1,52 @@
+import logging
+import json
+
+from django.http import HttpResponse
+from django.conf import settings
+from django_future.csrf import csrf_exempt
+
+from . import import_from_github, export_to_github
+
+log = logging.getLogger()
+
+
+@csrf_exempt
+def github_post_receive(request):
+    """
+    This view recieves post-receive requests from github whenever one of
+    the watched repositiories changes.
+
+    It is responsible for updating the relevant local git repo,
+    importing the new version of the course (if anything changed),
+    and then pushing back to github any changes that happened as part of the
+    import.
+
+    The github request format is described here: https://help.github.com/articles/post-receive-hooks
+    """
+
+    payload = json.loads(request.POST['payload'])
+
+    ref = payload['ref']
+
+    if not ref.startswith('refs/heads/'):
+        log.info('Ignore changes to non-branch ref %s' % ref)
+        return HttpResponse('Ignoring non-branch')
+
+    branch_name = ref.replace('refs/heads/', '', 1)
+
+    repo_name = payload['repository']['name']
+
+    if repo_name not in settings.REPOS:
+        log.info('No repository matching %s found' % repo_name)
+        return HttpResponse('No Repo Found')
+
+    repo = settings.REPOS[repo_name]
+
+    if repo['branch'] != branch_name:
+        log.info('Ignoring changes to non-tracked branch %s in repo %s' % (branch_name, repo_name))
+        return HttpResponse('Ignoring non-tracked branch')
+
+    revision, course = import_from_github(repo)
+    export_to_github(course, repo['path'], "Changes from cms import of revision %s" % revision)
+
+    return HttpResponse('Push recieved')
diff --git a/cms/envs/dev.py b/cms/envs/dev.py
index b4bcbfa..8ff2a35 100644
--- a/cms/envs/dev.py
+++ b/cms/envs/dev.py
@@ -29,8 +29,19 @@ DATABASES = {
     }
 }
 
+REPO_ROOT = ENV_ROOT / "content"
+
+REPOS = {
+    'edx4edx': {
+        'path': REPO_ROOT / "edx4edx",
+        'org': 'edx',
+        'course': 'edx4edx',
+        'branch': 'for_cms'
+    }
+}
+
 CACHES = {
-    # This is the cache used for most things. Askbot will not work without a 
+    # This is the cache used for most things. Askbot will not work without a
     # functioning cache -- it relies on caching to load its settings in places.
     # In staging/prod envs, the sessions also live here.
     'default': {
diff --git a/cms/urls.py b/cms/urls.py
index 9d827c3..ad5a69b 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -8,5 +8,6 @@ urlpatterns = patterns('',
     url(r'^$', 'contentstore.views.index', name='index'),
     url(r'^edit_item$', 'contentstore.views.edit_item', name='edit_item'),
     url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
-    url(r'^temp_force_export$', 'contentstore.views.temp_force_export')
+    url(r'^temp_force_export$', 'contentstore.views.temp_force_export'),
+    url(r'^github_service_hook$', 'github_sync.views.github_post_receive'),
 )
diff --git a/requirements.txt b/requirements.txt
index a72f72a..ddf218a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -28,3 +28,4 @@ pymongo
 django_nose
 nosexcover
 rednose
+GitPython >= 0.3