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