{"action":"created","comment":{"url":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/comments/48761322","id":48761322,"diff_hunk":"@@ -0,0 +1,210 @@\n+\"\"\"OAuth utility functions\"\"\"\n+\n+import logging\n+import json\n+import re\n+\n+from django.conf import settings\n+from requests.exceptions import RequestException\n+from allauth.socialaccount.providers.bitbucket_oauth2.views import (\n+    BitbucketOAuth2Adapter)\n+\n+from readthedocs.builds import utils as build_utils\n+\n+from ..models import RemoteOrganization, RemoteRepository\n+from .base import Service\n+\n+\n+DEFAULT_PRIVACY_LEVEL = getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public')\n+\n+log = logging.getLogger(__name__)\n+\n+\n+class BitbucketService(Service):\n+\n+    \"\"\"Provider service for Bitbucket\"\"\"\n+\n+    adapter = BitbucketOAuth2Adapter\n+    # TODO replace this with a less naive check\n+    url_pattern = re.compile(r'bitbucket.org\\/')\n+    https_url_pattern = re.compile(r'^https:\\/\\/[^@]+@bitbucket.org/')\n+\n+    def sync(self, sync):\n+        \"\"\"Sync repositories and teams from Bitbucket API\"\"\"\n+        if sync:\n+            self.sync_repositories()\n+            self.sync_teams()\n+\n+    def sync_repositories(self):\n+        \"\"\"Sync repositories from Bitbucket API\"\"\"\n+        # Get user repos\n+        try:\n+            repos = self.paginate(\n+                'https://bitbucket.org/api/2.0/repositories/?role=member')\n+            for repo in repos:\n+                self.create_repository(repo)\n+        except (TypeError, ValueError) as e:\n+            log.error('Error syncing Bitbucket repositories: %s',\n+                      str(e), exc_info=True)\n+            raise Exception('Could not sync your Bitbucket repositories, '\n+                            'try reconnecting your account')\n+\n+        # Because privileges aren't returned with repository data, run query\n+        # again for repositories that user has admin role for, and update\n+        # existing repositories.\n+        try:\n+            resp = self.paginate(\n+                'https://bitbucket.org/api/2.0/repositories/?role=admin')\n+            repos = (\n+                RemoteRepository.objects\n+                .filter(users=self.user,\n+                        full_name__in=[r['full_name'] for r in resp],\n+                        account=self.account)\n+            )\n+            for repo in repos:\n+                repo.admin = True\n+                repo.save()\n+        except (TypeError, ValueError):\n+            pass\n+\n+    def sync_teams(self):\n+        \"\"\"Sync Bitbucket teams and team repositories\"\"\"\n+        try:\n+            teams = self.paginate(\n+                'https://api.bitbucket.org/2.0/teams/?role=member'\n+            )\n+            for team in teams:\n+                org = self.create_organization(team)\n+                repos = self.paginate(team['links']['repositories']['href'])\n+                for repo in repos:\n+                    self.create_repository(repo, organization=org)\n+        except ValueError as e:\n+            log.error('Error syncing Bitbucket organizations: %s',\n+                      str(e), exc_info=True)\n+            raise Exception('Could not sync your Bitbucket team repositories, '\n+                            'try reconnecting your account')\n+\n+    def create_repository(self, fields, privacy=DEFAULT_PRIVACY_LEVEL,\n+                          organization=None):\n+        \"\"\"Update or create a repository from Bitbucket API response\n+\n+        .. note::\n+            The :py:data:`admin` property is not set during creation, as\n+            permissions are not part of the returned repository data from\n+            Bitbucket.\n+\n+        :param fields: dictionary of response data from API\n+        :param privacy: privacy level to support\n+        :param organization: remote organization to associate with\n+        :type organization: RemoteOrganization\n+        :rtype: RemoteRepository\n+        \"\"\"\n+        if (fields['is_private'] is True and privacy == 'private' or\n+                fields['is_private'] is False and privacy == 'public'):\n+            repo, _ = RemoteRepository.objects.get_or_create(\n+                full_name=fields['full_name'],\n+                account=self.account,\n+            )\n+            if repo.organization and repo.organization != organization:\n+                log.debug('Not importing %s because mismatched orgs' %\n+                          fields['name'])\n+                return None\n+            else:\n+                repo.organization = organization\n+            repo.users.add(self.user)\n+            repo.name = fields['name']\n+            repo.description = fields['description']\n+            repo.private = fields['is_private']\n+            for link in fields['links']['clone']:\n+                if link['name'] == 'https':\n+                    repo.clone_url = link['href']\n+                    # Remove user name from HTTPS clone url","path":"readthedocs/oauth/services/bitbucket.py","position":121,"original_position":121,"commit_id":"c17ca6b085e621fbce957c343ad9b144a2a6ff24","original_commit_id":"c17ca6b085e621fbce957c343ad9b144a2a6ff24","user":{"login":"ericholscher","id":25510,"avatar_url":"https://avatars.githubusercontent.com/u/25510?v=3","gravatar_id":"","url":"https://api.github.com/users/ericholscher","html_url":"https://github.com/ericholscher","followers_url":"https://api.github.com/users/ericholscher/followers","following_url":"https://api.github.com/users/ericholscher/following{/other_user}","gists_url":"https://api.github.com/users/ericholscher/gists{/gist_id}","starred_url":"https://api.github.com/users/ericholscher/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ericholscher/subscriptions","organizations_url":"https://api.github.com/users/ericholscher/orgs","repos_url":"https://api.github.com/users/ericholscher/repos","events_url":"https://api.github.com/users/ericholscher/events{/privacy}","received_events_url":"https://api.github.com/users/ericholscher/received_events","type":"User","site_admin":false},"body":"Janky. Do they really not return a clean https cloning URL? >:x","created_at":"2016-01-04T18:21:25Z","updated_at":"2016-01-04T18:21:25Z","html_url":"https://github.com/rtfd/readthedocs.org/pull/1850#discussion_r48761322","pull_request_url":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/1850","_links":{"self":{"href":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/comments/48761322"},"html":{"href":"https://github.com/rtfd/readthedocs.org/pull/1850#discussion_r48761322"},"pull_request":{"href":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/1850"}}},"pull_request":{"url":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/1850","id":52730932,"html_url":"https://github.com/rtfd/readthedocs.org/pull/1850","diff_url":"https://github.com/rtfd/readthedocs.org/pull/1850.diff","patch_url":"https://github.com/rtfd/readthedocs.org/pull/1850.patch","issue_url":"https://api.github.com/repos/rtfd/readthedocs.org/issues/1850","number":1850,"state":"open","locked":false,"title":"Changes to support Bitbucket OAuth2","user":{"login":"agjohnson","id":1140183,"avatar_url":"https://avatars.githubusercontent.com/u/1140183?v=3","gravatar_id":"","url":"https://api.github.com/users/agjohnson","html_url":"https://github.com/agjohnson","followers_url":"https://api.github.com/users/agjohnson/followers","following_url":"https://api.github.com/users/agjohnson/following{/other_user}","gists_url":"https://api.github.com/users/agjohnson/gists{/gist_id}","starred_url":"https://api.github.com/users/agjohnson/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/agjohnson/subscriptions","organizations_url":"https://api.github.com/users/agjohnson/orgs","repos_url":"https://api.github.com/users/agjohnson/repos","events_url":"https://api.github.com/users/agjohnson/events{/privacy}","received_events_url":"https://api.github.com/users/agjohnson/received_events","type":"User","site_admin":false},"body":"Changes most API calls to use APIv2 and relies on OAuth2 for sessions. Bitbucket's OAuth2\r\nimplementation expires tokens after 2 hours, so this also implements token refreshing on\r\nthe general session creation.\r\n\r\nThis updates the UI around connecting accounts, namely:\r\n\r\n* Use our common list styling for list of connected services\r\n* Add a basic warning element for guiding users to reconnect accounts\r\n* Make connect services buttons themed buttons instead of basic links\r\n* Updated some of the text from \"Social Accounts\" to \"Connected Services\".\r\n\r\n![screen shot 2016-01-01 at 3 50 13 pm](https://cloud.githubusercontent.com/assets/1140183/12072649/634f9ba2-b09f-11e5-9d4d-c9a900ed1e6e.png)\r\n\r\nThe project import page will now warn when deprecated accounts are connected, the new API sync code for Bitbucket uses version 2 of the API for almost all requests. Our API will filter out repos/orgs from these deprecated services, but the services themselves are kept around to allow for user administration on the connected services page.\r\n\r\nRefs pennersr/django-allauth#1215\r\nRefs pennersr/django-allauth#1212\r\nFixes #1675 \r\nI believe this fixes #1864\r\nFixes #1696\r\nRefs #1893 - repin django-allauth next release\r\nFixes #1684 - \"social accounts\" rename\r\nFixes #1668 - social account page 500 error\r\nFixes #1446 - add bitbucket tests\r\n\r\nOutstanding:\r\n- [x] Test handling of existing Bitbucket social accounts\r\n- [x] How to handle migration fo existing Bitbucket social accounts\r\n- [x] UI and styling bits to point users to reconnect\r\n- [x] Some more test cases around bitbucket\r\n- [x] Import form uses HTTP, but complains about \"this is a private repo\"","created_at":"2015-12-04T22:39:57Z","updated_at":"2016-01-04T18:21:25Z","closed_at":null,"merged_at":null,"merge_commit_sha":"d47dbab186527e246d6e6864d5d2a222de90831a","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/1850/commits","review_comments_url":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/1850/comments","review_comment_url":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/comments{/number}","comments_url":"https://api.github.com/repos/rtfd/readthedocs.org/issues/1850/comments","statuses_url":"https://api.github.com/repos/rtfd/readthedocs.org/statuses/c17ca6b085e621fbce957c343ad9b144a2a6ff24","head":{"label":"rtfd:bitbucket-oauth2","ref":"bitbucket-oauth2","sha":"c17ca6b085e621fbce957c343ad9b144a2a6ff24","user":{"login":"rtfd","id":366329,"avatar_url":"https://avatars.githubusercontent.com/u/366329?v=3","gravatar_id":"","url":"https://api.github.com/users/rtfd","html_url":"https://github.com/rtfd","followers_url":"https://api.github.com/users/rtfd/followers","following_url":"https://api.github.com/users/rtfd/following{/other_user}","gists_url":"https://api.github.com/users/rtfd/gists{/gist_id}","starred_url":"https://api.github.com/users/rtfd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rtfd/subscriptions","organizations_url":"https://api.github.com/users/rtfd/orgs","repos_url":"https://api.github.com/users/rtfd/repos","events_url":"https://api.github.com/users/rtfd/events{/privacy}","received_events_url":"https://api.github.com/users/rtfd/received_events","type":"Organization","site_admin":false},"repo":{"id":841835,"name":"readthedocs.org","full_name":"rtfd/readthedocs.org","owner":{"login":"rtfd","id":366329,"avatar_url":"https://avatars.githubusercontent.com/u/366329?v=3","gravatar_id":"","url":"https://api.github.com/users/rtfd","html_url":"https://github.com/rtfd","followers_url":"https://api.github.com/users/rtfd/followers","following_url":"https://api.github.com/users/rtfd/following{/other_user}","gists_url":"https://api.github.com/users/rtfd/gists{/gist_id}","starred_url":"https://api.github.com/users/rtfd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rtfd/subscriptions","organizations_url":"https://api.github.com/users/rtfd/orgs","repos_url":"https://api.github.com/users/rtfd/repos","events_url":"https://api.github.com/users/rtfd/events{/privacy}","received_events_url":"https://api.github.com/users/rtfd/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/rtfd/readthedocs.org","description":"source code to readthedocs.org","fork":false,"url":"https://api.github.com/repos/rtfd/readthedocs.org","forks_url":"https://api.github.com/repos/rtfd/readthedocs.org/forks","keys_url":"https://api.github.com/repos/rtfd/readthedocs.org/keys{/key_id}","collaborators_url":"https://api.github.com/repos/rtfd/readthedocs.org/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/rtfd/readthedocs.org/teams","hooks_url":"https://api.github.com/repos/rtfd/readthedocs.org/hooks","issue_events_url":"https://api.github.com/repos/rtfd/readthedocs.org/issues/events{/number}","events_url":"https://api.github.com/repos/rtfd/readthedocs.org/events","assignees_url":"https://api.github.com/repos/rtfd/readthedocs.org/assignees{/user}","branches_url":"https://api.github.com/repos/rtfd/readthedocs.org/branches{/branch}","tags_url":"https://api.github.com/repos/rtfd/readthedocs.org/tags","blobs_url":"https://api.github.com/repos/rtfd/readthedocs.org/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/rtfd/readthedocs.org/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/rtfd/readthedocs.org/git/refs{/sha}","trees_url":"https://api.github.com/repos/rtfd/readthedocs.org/git/trees{/sha}","statuses_url":"https://api.github.com/repos/rtfd/readthedocs.org/statuses/{sha}","languages_url":"https://api.github.com/repos/rtfd/readthedocs.org/languages","stargazers_url":"https://api.github.com/repos/rtfd/readthedocs.org/stargazers","contributors_url":"https://api.github.com/repos/rtfd/readthedocs.org/contributors","subscribers_url":"https://api.github.com/repos/rtfd/readthedocs.org/subscribers","subscription_url":"https://api.github.com/repos/rtfd/readthedocs.org/subscription","commits_url":"https://api.github.com/repos/rtfd/readthedocs.org/commits{/sha}","git_commits_url":"https://api.github.com/repos/rtfd/readthedocs.org/git/commits{/sha}","comments_url":"https://api.github.com/repos/rtfd/readthedocs.org/comments{/number}","issue_comment_url":"https://api.github.com/repos/rtfd/readthedocs.org/issues/comments{/number}","contents_url":"https://api.github.com/repos/rtfd/readthedocs.org/contents/{+path}","compare_url":"https://api.github.com/repos/rtfd/readthedocs.org/compare/{base}...{head}","merges_url":"https://api.github.com/repos/rtfd/readthedocs.org/merges","archive_url":"https://api.github.com/repos/rtfd/readthedocs.org/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/rtfd/readthedocs.org/downloads","issues_url":"https://api.github.com/repos/rtfd/readthedocs.org/issues{/number}","pulls_url":"https://api.github.com/repos/rtfd/readthedocs.org/pulls{/number}","milestones_url":"https://api.github.com/repos/rtfd/readthedocs.org/milestones{/number}","notifications_url":"https://api.github.com/repos/rtfd/readthedocs.org/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/rtfd/readthedocs.org/labels{/name}","releases_url":"https://api.github.com/repos/rtfd/readthedocs.org/releases{/id}","created_at":"2010-08-16T19:18:06Z","updated_at":"2016-01-04T17:48:51Z","pushed_at":"2016-01-01T23:45:54Z","git_url":"git://github.com/rtfd/readthedocs.org.git","ssh_url":"git@github.com:rtfd/readthedocs.org.git","clone_url":"https://github.com/rtfd/readthedocs.org.git","svn_url":"https://github.com/rtfd/readthedocs.org","homepage":"http://rtfd.org/","size":36157,"stargazers_count":2330,"watchers_count":2330,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1038,"mirror_url":null,"open_issues_count":218,"forks":1038,"open_issues":218,"watchers":2330,"default_branch":"master"}},"base":{"label":"rtfd:master","ref":"master","sha":"21d6a8533ad5211f17e3c4ecff3861d16b4ff66b","user":{"login":"rtfd","id":366329,"avatar_url":"https://avatars.githubusercontent.com/u/366329?v=3","gravatar_id":"","url":"https://api.github.com/users/rtfd","html_url":"https://github.com/rtfd","followers_url":"https://api.github.com/users/rtfd/followers","following_url":"https://api.github.com/users/rtfd/following{/other_user}","gists_url":"https://api.github.com/users/rtfd/gists{/gist_id}","starred_url":"https://api.github.com/users/rtfd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rtfd/subscriptions","organizations_url":"https://api.github.com/users/rtfd/orgs","repos_url":"https://api.github.com/users/rtfd/repos","events_url":"https://api.github.com/users/rtfd/events{/privacy}","received_events_url":"https://api.github.com/users/rtfd/received_events","type":"Organization","site_admin":false},"repo":{"id":841835,"name":"readthedocs.org","full_name":"rtfd/readthedocs.org","owner":{"login":"rtfd","id":366329,"avatar_url":"https://avatars.githubusercontent.com/u/366329?v=3","gravatar_id":"","url":"https://api.github.com/users/rtfd","html_url":"https://github.com/rtfd","followers_url":"https://api.github.com/users/rtfd/followers","following_url":"https://api.github.com/users/rtfd/following{/other_user}","gists_url":"https://api.github.com/users/rtfd/gists{/gist_id}","starred_url":"https://api.github.com/users/rtfd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rtfd/subscriptions","organizations_url":"https://api.github.com/users/rtfd/orgs","repos_url":"https://api.github.com/users/rtfd/repos","events_url":"https://api.github.com/users/rtfd/events{/privacy}","received_events_url":"https://api.github.com/users/rtfd/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/rtfd/readthedocs.org","description":"source code to readthedocs.org","fork":false,"url":"https://api.github.com/repos/rtfd/readthedocs.org","forks_url":"https://api.github.com/repos/rtfd/readthedocs.org/forks","keys_url":"https://api.github.com/repos/rtfd/readthedocs.org/keys{/key_id}","collaborators_url":"https://api.github.com/repos/rtfd/readthedocs.org/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/rtfd/readthedocs.org/teams","hooks_url":"https://api.github.com/repos/rtfd/readthedocs.org/hooks","issue_events_url":"https://api.github.com/repos/rtfd/readthedocs.org/issues/events{/number}","events_url":"https://api.github.com/repos/rtfd/readthedocs.org/events","assignees_url":"https://api.github.com/repos/rtfd/readthedocs.org/assignees{/user}","branches_url":"https://api.github.com/repos/rtfd/readthedocs.org/branches{/branch}","tags_url":"https://api.github.com/repos/rtfd/readthedocs.org/tags","blobs_url":"https://api.github.com/repos/rtfd/readthedocs.org/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/rtfd/readthedocs.org/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/rtfd/readthedocs.org/git/refs{/sha}","trees_url":"https://api.github.com/repos/rtfd/readthedocs.org/git/trees{/sha}","statuses_url":"https://api.github.com/repos/rtfd/readthedocs.org/statuses/{sha}","languages_url":"https://api.github.com/repos/rtfd/readthedocs.org/languages","stargazers_url":"https://api.github.com/repos/rtfd/readthedocs.org/stargazers","contributors_url":"https://api.github.com/repos/rtfd/readthedocs.org/contributors","subscribers_url":"https://api.github.com/repos/rtfd/readthedocs.org/subscribers","subscription_url":"https://api.github.com/repos/rtfd/readthedocs.org/subscription","commits_url":"https://api.github.com/repos/rtfd/readthedocs.org/commits{/sha}","git_commits_url":"https://api.github.com/repos/rtfd/readthedocs.org/git/commits{/sha}","comments_url":"https://api.github.com/repos/rtfd/readthedocs.org/comments{/number}","issue_comment_url":"https://api.github.com/repos/rtfd/readthedocs.org/issues/comments{/number}","contents_url":"https://api.github.com/repos/rtfd/readthedocs.org/contents/{+path}","compare_url":"https://api.github.com/repos/rtfd/readthedocs.org/compare/{base}...{head}","merges_url":"https://api.github.com/repos/rtfd/readthedocs.org/merges","archive_url":"https://api.github.com/repos/rtfd/readthedocs.org/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/rtfd/readthedocs.org/downloads","issues_url":"https://api.github.com/repos/rtfd/readthedocs.org/issues{/number}","pulls_url":"https://api.github.com/repos/rtfd/readthedocs.org/pulls{/number}","milestones_url":"https://api.github.com/repos/rtfd/readthedocs.org/milestones{/number}","notifications_url":"https://api.github.com/repos/rtfd/readthedocs.org/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/rtfd/readthedocs.org/labels{/name}","releases_url":"https://api.github.com/repos/rtfd/readthedocs.org/releases{/id}","created_at":"2010-08-16T19:18:06Z","updated_at":"2016-01-04T17:48:51Z","pushed_at":"2016-01-01T23:45:54Z","git_url":"git://github.com/rtfd/readthedocs.org.git","ssh_url":"git@github.com:rtfd/readthedocs.org.git","clone_url":"https://github.com/rtfd/readthedocs.org.git","svn_url":"https://github.com/rtfd/readthedocs.org","homepage":"http://rtfd.org/","size":36157,"stargazers_count":2330,"watchers_count":2330,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1038,"mirror_url":null,"open_issues_count":218,"forks":1038,"open_issues":218,"watchers":2330,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/1850"},"html":{"href":"https://github.com/rtfd/readthedocs.org/pull/1850"},"issue":{"href":"https://api.github.com/repos/rtfd/readthedocs.org/issues/1850"},"comments":{"href":"https://api.github.com/repos/rtfd/readthedocs.org/issues/1850/comments"},"review_comments":{"href":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/1850/comments"},"review_comment":{"href":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/rtfd/readthedocs.org/pulls/1850/commits"},"statuses":{"href":"https://api.github.com/repos/rtfd/readthedocs.org/statuses/c17ca6b085e621fbce957c343ad9b144a2a6ff24"}}}}