{"action":"created","comment":{"url":"https://api.github.com/repos/edx/edx-django-release-util/pulls/comments/85821329","pull_request_review_id":6510346,"id":85821329,"diff_hunk":"@@ -132,164 +134,215 @@ def state(self):\n         return yaml.safe_dump(self.migration_state)\n \n \n+def _remove_escape_characters(s):\n+    ansi_escape = re.compile(r'(\\x9B|\\x1B\\[)[0-?]*[ -\\/]*[@-~]')\n+    return ansi_escape.sub('', s)\n+\n+\n+class MigrationSessionError(ValueError):\n+    pass\n+\n+\n class MigrationSession(object):\n     \"\"\"\n     Class which performs migrations while keeping track of the state of each migration.\n     Provides the state of all migrations post-migration.\n-    \"\"\"\n-    def __init__(self, stderr, database_name):\n-        self.to_apply = []\n-        self.migration_state = {\n-            'success': [],\n-            'failure': None,\n-            'unapplied': [],\n-            'rollback_commands': [],\n-        }\n-        self.timer = default_timer\n-        self.stderr = stderr\n-        self.database_name = database_name\n \n-        # Execute command to find/record the unapplied migrations.\n-        out = StringIO()\n-        call_command(\"show_unapplied_migrations\",\n-                     interactive=False,\n-                     stdout=out,\n-                     database=self.database_name,\n-                     standalone=False)\n+    If you want to run specific migrations, you can either initialize it with\n+    a list of migration tuples, or add migrations by calling add_migrations(),\n+    then call apply().\n \n-        # Load the passed-in YAML into a dictionary.\n-        self.input_migrations = yaml.safe_load(out.getvalue())\n+    If you want to run all migrations, simply call apply_all(). Calling\n+    apply_all() while a MigrationSession has migrations added to it will raise\n+    an error.\n \n-        # Build a list of migrations that will be applied.\n-        for migration in self.input_migrations['migrations']:\n-            self.to_apply.append(migration)\n+    Once either apply() or apply_all() has been called, calling apply(),\n+    apply_all(), or add_migrations() will raise an error.\n+\n+    The \"stderr\" argument is deprecated and not used.\n+    \"\"\"\n+    def __init__(self, stderr, database_name, migrations=None):\n+        self._to_apply = []\n+        self._migration_state = []\n+        self._timer = default_timer\n+        self._closed = False\n+\n+        self._database_name = database_name\n+        if migrations:\n+            self.add_migrations(migrations)\n \n         # Regex built to match migration output like this line:\n         #    Applying release_util.0001_initial...  OK\n         self.migration_regex = re.compile(r'Applying (?P<app_name>[^\\.]+)\\.(?P<migration_name>[^\\.]+)[\\. ]+(?P<success>[OK]*)$')\n \n-    def more_to_apply(self):\n+    def add_migrations(self, migrations):\n         \"\"\"\n-        Returns True if more migrations remain to apply in this session, else False.\n+        Add migrations to be applied. Migrations are of the form [(app, migration_name), ...]\n         \"\"\"\n-        return len(self.to_apply) > 0\n+        if self._closed:\n+            raise MigrationSessionError(\"Can't change applied session\")\n+        self._to_apply.extend(migrations)\n \n-    def _add_rollback_commands(self):\n+    def _gather_unapplied_migrations(self, loader):\n         \"\"\"\n-        Generate rollback commands for the apps that have had migrations applied.\n-        If an app's migration has failed, include that rollback command as well.\n+        Output a list of unapplied migrations in the form [['migration1', migration2'], ...].\n+        This implementation is mostly copied from the Django 'showmigrations' mgmt command.\n+        https://github.com/django/django/blob/stable/1.8.x/django/core/management/commands/showmigrations.py\n         \"\"\"\n-        apps_to_rollback = set()\n-        # Add the apps with successfully applied migrations.\n-        apps_to_rollback.update([m['migration'][0] for m in self.migration_state['success']])\n-        # If an app migration failed, include that rollback also.\n-        if self.migration_state['failure']:\n-            apps_to_rollback.add(self.migration_state['failure']['migration'][0])\n-        for app in apps_to_rollback:\n-            initial_app_state = None\n-            for initial in self.input_migrations['initial_states']:\n-                if app == initial[0]:\n-                    initial_app_state = initial\n-                    break\n-            if not initial_app_state:\n-                raise CommandError('App \"{}\" not found in initial migration states.'.format(app))\n-            self.migration_state['rollback_commands'].append(\n-                [\n-                    'python', 'manage.py', 'migrate',\n-                    app,\n-                    initial_app_state[1]\n-                ]\n-            )\n+        unapplied = []\n+        graph = loader.graph\n+        plan = []\n+        seen = set()\n+\n+        # Generate the plan, in the order that migrations have been/should be applied.\n+        for target in graph.leaf_nodes():\n+            for migration in graph.forwards_plan(target):\n+                if migration not in seen:\n+                    plan.append(graph.nodes[migration])\n+                    seen.add(migration)\n+\n+        # Remove the migrations that have already been applied.\n+        for migration in plan:\n+            if not (migration.app_label, migration.name) in loader.applied_migrations:\n+                # NOTE: Unicode Django application names are unsupported.\n+                unapplied.append([migration.app_label, str(migration.name)])\n+        return unapplied\n \n-    def _apply_all(self):\n+    def _gather_current_migration_state(self, loader, apps):\n         \"\"\"\n-        Applies all Django model migrations at once, recording the result.\n+        Extract the most recent migrations from the relevant apps.\n+        If no migrations have been performed, return 'zero' as the most recent migration for the app.\n         \"\"\"\n-        def _remove_escape_characters(s):\n-            ansi_escape = re.compile(r'(\\x9B|\\x1B\\[)[0-?]*[ -\\/]*[@-~]')\n-            return ansi_escape.sub('', s)\n+        # Only care about applied migrations for the passed-in apps.\n+        apps = set(apps)\n+        relevant_applied = [migration for migration in loader.applied_migrations if migration[0] in apps]\n+        # Sort them by the most recent migration and convert to a dictionary,\n+        # leaving apps as keys and most recent migration as values.\n+        # NB: this is a dirty trick\n+        most_recents = dict(sorted(relevant_applied, key=lambda m: m[1]))\n+        # Fill in the apps with no migrations with 'zero'.\n+        # NOTE: Unicode Django application names are unsupported.\n+        most_recents = [[app, 'zero' if app not in most_recents else str(most_recents[app])] for app in apps]\n+        return most_recents\n \n-        out = StringIO()\n-        start = self.timer()\n-        try:\n-            call_command(\"migrate\",\n-                         interactive=False,\n-                         stdout=out,\n-                         database=self.database_name)\n-        except (CommandError, DatabaseError) as e:\n-            time_to_fail = self.timer() - start\n-            exc_type, exc_value, exc_traceback = sys.exc_info()\n+    def list(self):\n+        \"\"\"\n+        Returns a tuple of unapplied, current\n \n-            # Parse the output to discover the last migration being applied before the exception.\n-            # Mark migrations:\n-            # - before exception migration as success\n-            # - exception migration as failed\n-            # - any remaining exceptions as unapplied\n-            for line in out.getvalue().split('\\n'):\n-                line = _remove_escape_characters(line).strip()\n-                line_match = self.migration_regex.match(line)\n-                if line_match:\n-                    migration = [line_match.group('app_name'), line_match.group('migration_name')]\n-                    if line_match.group('success') == 'OK':\n-                        # A migration succeeded.\n-                        self.migration_state['success'].append({\n-                            'migration': migration,\n-                            'output': line,\n-                        })\n-                    else:\n-                        # This migration failed.\n-                        failed_app, failed_migration = migration\n-                        self.migration_state['failure'] = {\n-                            'migration': migration,\n-                            'output': out.getvalue(),\n-                            'traceback': repr(traceback.format_exception(exc_type, exc_value, exc_traceback)),\n-                        }\n-                        # Save the total migration time until failure.\n-                        self.migration_state['duration'] = time_to_fail\n-                        break\n-\n-            # Remove any migrations that succeeded -or- failed.\n-            # Any leftover migrations were *not* applied.\n-            succeeded = [status['migration'] for status in self.migration_state['success']]\n-            failed = []\n-            if failed_app and failed_migration:\n-                failed = [failed_app, failed_migration]\n-            for migration in self.to_apply:\n-                if migration not in succeeded and migration != failed:\n-                    self.migration_state['unapplied'].append(migration)\n+        \"Unapplied\" is a list of unapplied migrations. \"Current\" is a list of the current migration\n+        states for apps with unapplied migrations.\n \n-            # Find the apps that have been applied -or- failed.\n-            # Include their initial migrations as commands.\n-            self._add_rollback_commands()\n-            raise CommandError(\"Migration failed for app '{}' - migration '{}'.\\n\".format(failed_app, failed_migration))\n+        Both are tuples of the form (app, migration_name).\n+        \"\"\"\n+        connection = connections[self._database_name]\n+        loader = MigrationLoader(connection, ignore_no_migrations=True)\n+        unapplied = self._gather_unapplied_migrations(loader)\n+        currents = self._gather_current_migration_state(loader, [u[0] for u in unapplied])\n+        return unapplied, currents\n \n-        # All migrations succeeded. Record the total time for all migrations.\n-        time_to_apply = self.timer() - start\n-        self.migration_state['duration'] = time_to_apply\n+    def _parse_migrate_output(self, output):\n+        \"\"\"\n+        Returns (succeeded:list(tuple), failed:tuple or None)\n+        Both tuples are of the form (app, migration)\n+        \"\"\"\n+        failed = None\n+        succeeded = []\n \n-        # Parse the output to find all migrations that were applied successfully.\n-        for line in out.getvalue().split('\\n'):\n+        # Mark migrations:\n+        # - before exception migration as success\n+        # - exception migration as failed\n+        # - any remaining exceptions as unapplied\n+        for line in output.split('\\n'):\n             line = _remove_escape_characters(line).strip()\n             line_match = self.migration_regex.match(line)\n             if line_match:\n-                migration = [line_match.group('app_name'), line_match.group('migration_name')]\n+                migration = (line_match.group('app_name'), line_match.group('migration_name'))\n                 if line_match.group('success') == 'OK':\n-                    # A migration succeeded.\n-                    self.migration_state['success'].append({\n-                        'migration': migration,\n-                        'output': line,\n-                    })\n+                    # The migration succeeded\n+                    succeeded.append(migration)\n+                else:\n+                    # The migration failed\n+                    failed = migration\n+                    break\n+        return succeeded, failed\n \n-    def apply_all(self):\n+    def _apply(self, migration=None, run_all=False):\n+        \"\"\" \n+        If a migration is supplied, runs that migration and appends to state.\n+        If run_all==True, runs all migrations.\n+        Raises a ValueError if neither \"migration\" nor \"run_all\" are provided.\n+        \"\"\"\n+        out = StringIO()\n+        failed = traceback = None\n+        \n+        migrate_kwargs = {\n+            'interactive': False,\n+            'stdout': out,\n+            'database': self._database_name,\n+        }\n+        if migration is not None:\n+            migrate_kwargs.update({\n+                'app_label': app,\n+                'migration_name': migration,\n+            })\n+        elif not run_all:\n+            raise ValueError('Either a migration must be provided or \"run_all\" must be True')","path":"release_util/management/commands/__init__.py","position":284,"original_position":284,"commit_id":"82521445b937ff3740998a7b9be50ac9201c79ea","original_commit_id":"82521445b937ff3740998a7b9be50ac9201c79ea","user":{"login":"macdiesel","id":67862,"avatar_url":"https://avatars.githubusercontent.com/u/67862?v=3","gravatar_id":"","url":"https://api.github.com/users/macdiesel","html_url":"https://github.com/macdiesel","followers_url":"https://api.github.com/users/macdiesel/followers","following_url":"https://api.github.com/users/macdiesel/following{/other_user}","gists_url":"https://api.github.com/users/macdiesel/gists{/gist_id}","starred_url":"https://api.github.com/users/macdiesel/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/macdiesel/subscriptions","organizations_url":"https://api.github.com/users/macdiesel/orgs","repos_url":"https://api.github.com/users/macdiesel/repos","events_url":"https://api.github.com/users/macdiesel/events{/privacy}","received_events_url":"https://api.github.com/users/macdiesel/received_events","type":"User","site_admin":false},"body":"single quotes.","created_at":"2016-10-31T20:05:01Z","updated_at":"2016-10-31T20:12:25Z","html_url":"https://github.com/edx/edx-django-release-util/pull/20#discussion_r85821329","pull_request_url":"https://api.github.com/repos/edx/edx-django-release-util/pulls/20","_links":{"self":{"href":"https://api.github.com/repos/edx/edx-django-release-util/pulls/comments/85821329"},"html":{"href":"https://github.com/edx/edx-django-release-util/pull/20#discussion_r85821329"},"pull_request":{"href":"https://api.github.com/repos/edx/edx-django-release-util/pulls/20"}}},"pull_request":{"url":"https://api.github.com/repos/edx/edx-django-release-util/pulls/20","id":91673963,"html_url":"https://github.com/edx/edx-django-release-util/pull/20","diff_url":"https://github.com/edx/edx-django-release-util/pull/20.diff","patch_url":"https://github.com/edx/edx-django-release-util/pull/20.patch","issue_url":"https://api.github.com/repos/edx/edx-django-release-util/issues/20","number":20,"state":"open","locked":false,"title":"Max/new migration rollback","user":{"login":"maxrothman","id":2607086,"avatar_url":"https://avatars.githubusercontent.com/u/2607086?v=3","gravatar_id":"","url":"https://api.github.com/users/maxrothman","html_url":"https://github.com/maxrothman","followers_url":"https://api.github.com/users/maxrothman/followers","following_url":"https://api.github.com/users/maxrothman/following{/other_user}","gists_url":"https://api.github.com/users/maxrothman/gists{/gist_id}","starred_url":"https://api.github.com/users/maxrothman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/maxrothman/subscriptions","organizations_url":"https://api.github.com/users/maxrothman/orgs","repos_url":"https://api.github.com/users/maxrothman/repos","events_url":"https://api.github.com/users/maxrothman/events{/privacy}","received_events_url":"https://api.github.com/users/maxrothman/received_events","type":"User","site_admin":false},"body":"","created_at":"2016-10-31T19:28:24Z","updated_at":"2016-10-31T20:12:25Z","closed_at":null,"merged_at":null,"merge_commit_sha":"cdf8f94288363717d1a3ab277185db810b5b41eb","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/edx/edx-django-release-util/pulls/20/commits","review_comments_url":"https://api.github.com/repos/edx/edx-django-release-util/pulls/20/comments","review_comment_url":"https://api.github.com/repos/edx/edx-django-release-util/pulls/comments{/number}","comments_url":"https://api.github.com/repos/edx/edx-django-release-util/issues/20/comments","statuses_url":"https://api.github.com/repos/edx/edx-django-release-util/statuses/82521445b937ff3740998a7b9be50ac9201c79ea","head":{"label":"edx:max/new-migration-rollback","ref":"max/new-migration-rollback","sha":"82521445b937ff3740998a7b9be50ac9201c79ea","user":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"repo":{"id":53428085,"name":"edx-django-release-util","full_name":"edx/edx-django-release-util","owner":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/edx/edx-django-release-util","description":"Release pipeline utilities for edX independently-deployable applications (IDAs) based on Django.","fork":false,"url":"https://api.github.com/repos/edx/edx-django-release-util","forks_url":"https://api.github.com/repos/edx/edx-django-release-util/forks","keys_url":"https://api.github.com/repos/edx/edx-django-release-util/keys{/key_id}","collaborators_url":"https://api.github.com/repos/edx/edx-django-release-util/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/edx/edx-django-release-util/teams","hooks_url":"https://api.github.com/repos/edx/edx-django-release-util/hooks","issue_events_url":"https://api.github.com/repos/edx/edx-django-release-util/issues/events{/number}","events_url":"https://api.github.com/repos/edx/edx-django-release-util/events","assignees_url":"https://api.github.com/repos/edx/edx-django-release-util/assignees{/user}","branches_url":"https://api.github.com/repos/edx/edx-django-release-util/branches{/branch}","tags_url":"https://api.github.com/repos/edx/edx-django-release-util/tags","blobs_url":"https://api.github.com/repos/edx/edx-django-release-util/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/edx/edx-django-release-util/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/edx/edx-django-release-util/git/refs{/sha}","trees_url":"https://api.github.com/repos/edx/edx-django-release-util/git/trees{/sha}","statuses_url":"https://api.github.com/repos/edx/edx-django-release-util/statuses/{sha}","languages_url":"https://api.github.com/repos/edx/edx-django-release-util/languages","stargazers_url":"https://api.github.com/repos/edx/edx-django-release-util/stargazers","contributors_url":"https://api.github.com/repos/edx/edx-django-release-util/contributors","subscribers_url":"https://api.github.com/repos/edx/edx-django-release-util/subscribers","subscription_url":"https://api.github.com/repos/edx/edx-django-release-util/subscription","commits_url":"https://api.github.com/repos/edx/edx-django-release-util/commits{/sha}","git_commits_url":"https://api.github.com/repos/edx/edx-django-release-util/git/commits{/sha}","comments_url":"https://api.github.com/repos/edx/edx-django-release-util/comments{/number}","issue_comment_url":"https://api.github.com/repos/edx/edx-django-release-util/issues/comments{/number}","contents_url":"https://api.github.com/repos/edx/edx-django-release-util/contents/{+path}","compare_url":"https://api.github.com/repos/edx/edx-django-release-util/compare/{base}...{head}","merges_url":"https://api.github.com/repos/edx/edx-django-release-util/merges","archive_url":"https://api.github.com/repos/edx/edx-django-release-util/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/edx/edx-django-release-util/downloads","issues_url":"https://api.github.com/repos/edx/edx-django-release-util/issues{/number}","pulls_url":"https://api.github.com/repos/edx/edx-django-release-util/pulls{/number}","milestones_url":"https://api.github.com/repos/edx/edx-django-release-util/milestones{/number}","notifications_url":"https://api.github.com/repos/edx/edx-django-release-util/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/edx/edx-django-release-util/labels{/name}","releases_url":"https://api.github.com/repos/edx/edx-django-release-util/releases{/id}","deployments_url":"https://api.github.com/repos/edx/edx-django-release-util/deployments","created_at":"2016-03-08T16:38:01Z","updated_at":"2016-03-30T18:39:46Z","pushed_at":"2016-10-31T19:28:24Z","git_url":"git://github.com/edx/edx-django-release-util.git","ssh_url":"git@github.com:edx/edx-django-release-util.git","clone_url":"https://github.com/edx/edx-django-release-util.git","svn_url":"https://github.com/edx/edx-django-release-util","homepage":null,"size":65,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":2,"mirror_url":null,"open_issues_count":1,"forks":2,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"edx:master","ref":"master","sha":"077cae6886c2a2d59e2bfea49618bde4e13dcc0e","user":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"repo":{"id":53428085,"name":"edx-django-release-util","full_name":"edx/edx-django-release-util","owner":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/edx/edx-django-release-util","description":"Release pipeline utilities for edX independently-deployable applications (IDAs) based on Django.","fork":false,"url":"https://api.github.com/repos/edx/edx-django-release-util","forks_url":"https://api.github.com/repos/edx/edx-django-release-util/forks","keys_url":"https://api.github.com/repos/edx/edx-django-release-util/keys{/key_id}","collaborators_url":"https://api.github.com/repos/edx/edx-django-release-util/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/edx/edx-django-release-util/teams","hooks_url":"https://api.github.com/repos/edx/edx-django-release-util/hooks","issue_events_url":"https://api.github.com/repos/edx/edx-django-release-util/issues/events{/number}","events_url":"https://api.github.com/repos/edx/edx-django-release-util/events","assignees_url":"https://api.github.com/repos/edx/edx-django-release-util/assignees{/user}","branches_url":"https://api.github.com/repos/edx/edx-django-release-util/branches{/branch}","tags_url":"https://api.github.com/repos/edx/edx-django-release-util/tags","blobs_url":"https://api.github.com/repos/edx/edx-django-release-util/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/edx/edx-django-release-util/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/edx/edx-django-release-util/git/refs{/sha}","trees_url":"https://api.github.com/repos/edx/edx-django-release-util/git/trees{/sha}","statuses_url":"https://api.github.com/repos/edx/edx-django-release-util/statuses/{sha}","languages_url":"https://api.github.com/repos/edx/edx-django-release-util/languages","stargazers_url":"https://api.github.com/repos/edx/edx-django-release-util/stargazers","contributors_url":"https://api.github.com/repos/edx/edx-django-release-util/contributors","subscribers_url":"https://api.github.com/repos/edx/edx-django-release-util/subscribers","subscription_url":"https://api.github.com/repos/edx/edx-django-release-util/subscription","commits_url":"https://api.github.com/repos/edx/edx-django-release-util/commits{/sha}","git_commits_url":"https://api.github.com/repos/edx/edx-django-release-util/git/commits{/sha}","comments_url":"https://api.github.com/repos/edx/edx-django-release-util/comments{/number}","issue_comment_url":"https://api.github.com/repos/edx/edx-django-release-util/issues/comments{/number}","contents_url":"https://api.github.com/repos/edx/edx-django-release-util/contents/{+path}","compare_url":"https://api.github.com/repos/edx/edx-django-release-util/compare/{base}...{head}","merges_url":"https://api.github.com/repos/edx/edx-django-release-util/merges","archive_url":"https://api.github.com/repos/edx/edx-django-release-util/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/edx/edx-django-release-util/downloads","issues_url":"https://api.github.com/repos/edx/edx-django-release-util/issues{/number}","pulls_url":"https://api.github.com/repos/edx/edx-django-release-util/pulls{/number}","milestones_url":"https://api.github.com/repos/edx/edx-django-release-util/milestones{/number}","notifications_url":"https://api.github.com/repos/edx/edx-django-release-util/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/edx/edx-django-release-util/labels{/name}","releases_url":"https://api.github.com/repos/edx/edx-django-release-util/releases{/id}","deployments_url":"https://api.github.com/repos/edx/edx-django-release-util/deployments","created_at":"2016-03-08T16:38:01Z","updated_at":"2016-03-30T18:39:46Z","pushed_at":"2016-10-31T19:28:24Z","git_url":"git://github.com/edx/edx-django-release-util.git","ssh_url":"git@github.com:edx/edx-django-release-util.git","clone_url":"https://github.com/edx/edx-django-release-util.git","svn_url":"https://github.com/edx/edx-django-release-util","homepage":null,"size":65,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":2,"mirror_url":null,"open_issues_count":1,"forks":2,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/edx/edx-django-release-util/pulls/20"},"html":{"href":"https://github.com/edx/edx-django-release-util/pull/20"},"issue":{"href":"https://api.github.com/repos/edx/edx-django-release-util/issues/20"},"comments":{"href":"https://api.github.com/repos/edx/edx-django-release-util/issues/20/comments"},"review_comments":{"href":"https://api.github.com/repos/edx/edx-django-release-util/pulls/20/comments"},"review_comment":{"href":"https://api.github.com/repos/edx/edx-django-release-util/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/edx/edx-django-release-util/pulls/20/commits"},"statuses":{"href":"https://api.github.com/repos/edx/edx-django-release-util/statuses/82521445b937ff3740998a7b9be50ac9201c79ea"}}}}
{"actor":{"display_login":"macdiesel"}}