Skip to content

Commit 487fa72

Browse files
authored
Merge pull request #7310 from readthedocs/humitos/sync-remote-repositories-organizations
2 parents 23fb804 + bb25e9a commit 487fa72

File tree

4 files changed

+102
-52
lines changed

4 files changed

+102
-52
lines changed

readthedocs/oauth/services/base.py

+23-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from allauth.socialaccount.models import SocialAccount
77
from allauth.socialaccount.providers import registry
88
from django.conf import settings
9+
from django.db.models import Q
910
from django.utils import timezone
1011
from oauthlib.oauth2.rfc6749.errors import InvalidClientIdError
1112
from requests.exceptions import RequestException
@@ -181,11 +182,30 @@ def paginate(self, url, **kwargs):
181182
url,
182183
debug_data,
183184
)
184-
return []
185+
186+
return []
185187

186188
def sync(self):
187-
"""Sync repositories and organizations."""
188-
raise NotImplementedError
189+
"""
190+
Sync repositories (RemoteRepository) and organizations (RemoteOrganization).
191+
192+
- creates a new RemoteRepository/Organization per new repository
193+
- updates fields for existing RemoteRepository/Organization
194+
- deletes old RemoteRepository/Organization that are not present for this user
195+
"""
196+
repos = self.sync_repositories()
197+
organizations, organization_repos = self.sync_organizations()
198+
199+
# Delete RemoteRepository where the user doesn't have access anymore
200+
# (skip RemoteRepository tied to a Project on this user)
201+
repository_full_names = self.get_repository_full_names(repos + organization_repos)
202+
self.user.oauth_repositories.exclude(
203+
Q(full_name__in=repository_full_names) | Q(project__isnull=False)
204+
).delete()
205+
206+
# Delete RemoteOrganization where the user doesn't have access anymore
207+
organization_names = self.get_organization_names(organizations)
208+
self.user.oauth_organizations.exclude(name__in=organization_names).delete()
189209

190210
def create_repository(self, fields, privacy=None, organization=None):
191211
"""

readthedocs/oauth/services/bitbucket.py

+25-9
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,18 @@ class BitbucketService(Service):
3030
url_pattern = re.compile(r'bitbucket.org')
3131
https_url_pattern = re.compile(r'^https:\/\/[^@][email protected]/')
3232

33-
def sync(self):
34-
"""Sync repositories and teams from Bitbucket API."""
35-
self.sync_repositories()
36-
self.sync_teams()
37-
3833
def sync_repositories(self):
3934
"""Sync repositories from Bitbucket API."""
35+
repos = []
36+
4037
# Get user repos
4138
try:
4239
repos = self.paginate(
4340
'https://bitbucket.org/api/2.0/repositories/?role=member',
4441
)
4542
for repo in repos:
4643
self.create_repository(repo)
44+
4745
except (TypeError, ValueError):
4846
log.warning('Error syncing Bitbucket repositories')
4947
raise SyncServiceError(
@@ -58,37 +56,49 @@ def sync_repositories(self):
5856
resp = self.paginate(
5957
'https://bitbucket.org/api/2.0/repositories/?role=admin',
6058
)
61-
repos = (
59+
admin_repos = (
6260
RemoteRepository.objects.filter(
6361
users=self.user,
6462
full_name__in=[r['full_name'] for r in resp],
6563
account=self.account,
6664
)
6765
)
68-
for repo in repos:
66+
for repo in admin_repos:
6967
repo.admin = True
7068
repo.save()
7169
except (TypeError, ValueError):
7270
pass
7371

74-
def sync_teams(self):
75-
"""Sync Bitbucket teams and team repositories."""
72+
return repos
73+
74+
def sync_organizations(self):
75+
"""Sync Bitbucket teams (our RemoteOrganization) and team repositories."""
76+
teams = []
77+
repositories = []
78+
7679
try:
7780
teams = self.paginate(
7881
'https://api.bitbucket.org/2.0/teams/?role=member',
7982
)
8083
for team in teams:
8184
org = self.create_organization(team)
8285
repos = self.paginate(team['links']['repositories']['href'])
86+
87+
# Add organization's repositories to the result
88+
repositories.extend(repos)
89+
8390
for repo in repos:
8491
self.create_repository(repo, organization=org)
92+
8593
except ValueError:
8694
log.warning('Error syncing Bitbucket organizations')
8795
raise SyncServiceError(
8896
'Could not sync your Bitbucket team repositories, '
8997
'try reconnecting your account',
9098
)
9199

100+
return teams, repositories
101+
92102
def create_repository(self, fields, privacy=None, organization=None):
93103
"""
94104
Update or create a repository from Bitbucket API response.
@@ -180,6 +190,12 @@ def create_organization(self, fields):
180190
organization.save()
181191
return organization
182192

193+
def get_repository_full_names(self, repositories):
194+
return {repository.get('full_name') for repository in repositories}
195+
196+
def get_organization_names(self, organizations):
197+
return {organization.get('display_name') for organization in organizations}
198+
183199
def get_next_url_to_paginate(self, response):
184200
return response.json().get('next')
185201

readthedocs/oauth/services/github.py

+21-15
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from allauth.socialaccount.models import SocialToken
88
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
9+
910
from django.conf import settings
1011
from django.db.models import Q
1112
from django.urls import reverse
@@ -33,34 +34,27 @@ class GitHubService(Service):
3334
# TODO replace this with a less naive check
3435
url_pattern = re.compile(r'github\.com')
3536

36-
def sync(self):
37-
"""Sync repositories and organizations."""
38-
repos = self.sync_repositories()
39-
organization_repos = self.sync_organizations()
40-
41-
# Delete RemoteRepository where the user doesn't have access anymore
42-
# (skip RemoteRepository tied to a Project on this user)
43-
full_names = {repo.get('full_name') for repo in repos + organization_repos}
44-
self.user.oauth_repositories.exclude(
45-
Q(full_name__in=full_names) | Q(project__isnull=False)
46-
).delete()
47-
4837
def sync_repositories(self):
4938
"""Sync repositories from GitHub API."""
50-
repos = self.paginate('https://api.github.com/user/repos?per_page=100')
39+
repos = []
40+
5141
try:
42+
repos = self.paginate('https://api.github.com/user/repos?per_page=100')
5243
for repo in repos:
5344
self.create_repository(repo)
54-
return repos
5545
except (TypeError, ValueError):
5646
log.warning('Error syncing GitHub repositories')
5747
raise SyncServiceError(
5848
'Could not sync your GitHub repositories, '
5949
'try reconnecting your account'
6050
)
51+
return repos
6152

6253
def sync_organizations(self):
6354
"""Sync organizations from GitHub API."""
55+
orgs = []
56+
repositories = []
57+
6458
try:
6559
orgs = self.paginate('https://api.github.com/user/orgs')
6660
for org in orgs:
@@ -71,16 +65,22 @@ def sync_organizations(self):
7165
org_repos = self.paginate(
7266
'{org_url}/repos'.format(org_url=org['url']),
7367
)
68+
69+
# Add all the repositories for this organization to the result
70+
repositories.extend(org_repos)
71+
7472
for repo in org_repos:
7573
self.create_repository(repo, organization=org_obj)
76-
return org_repos
74+
7775
except (TypeError, ValueError):
7876
log.warning('Error syncing GitHub organizations')
7977
raise SyncServiceError(
8078
'Could not sync your GitHub organizations, '
8179
'try reconnecting your account'
8280
)
8381

82+
return orgs, repositories
83+
8484
def create_repository(self, fields, privacy=None, organization=None):
8585
"""
8686
Update or create a repository from GitHub API response.
@@ -170,6 +170,12 @@ def create_organization(self, fields):
170170
organization.save()
171171
return organization
172172

173+
def get_repository_full_names(self, repositories):
174+
return {repository.get('full_name') for repository in repositories}
175+
176+
def get_organization_names(self, organizations):
177+
return {organization.get('name') for organization in organizations}
178+
173179
def get_next_url_to_paginate(self, response):
174180
return response.links.get('next', {}).get('url')
175181

readthedocs/oauth/services/gitlab.py

+33-25
Original file line numberDiff line numberDiff line change
@@ -66,26 +66,18 @@ def get_next_url_to_paginate(self, response):
6666
def get_paginated_results(self, response):
6767
return response.json()
6868

69-
def sync(self):
70-
"""
71-
Sync repositories and organizations from GitLab API.
72-
73-
See: https://docs.gitlab.com/ce/api/projects.html
74-
"""
75-
self.sync_repositories()
76-
self.sync_organizations()
77-
7869
def sync_repositories(self):
79-
repos = self.paginate(
80-
'{url}/api/v4/projects'.format(url=self.adapter.provider_base_url),
81-
per_page=100,
82-
archived=False,
83-
order_by='path',
84-
sort='asc',
85-
membership=True,
86-
)
87-
70+
repos = []
8871
try:
72+
repos = self.paginate(
73+
'{url}/api/v4/projects'.format(url=self.adapter.provider_base_url),
74+
per_page=100,
75+
archived=False,
76+
order_by='path',
77+
sort='asc',
78+
membership=True,
79+
)
80+
8981
for repo in repos:
9082
self.create_repository(repo)
9183
except (TypeError, ValueError):
@@ -95,16 +87,20 @@ def sync_repositories(self):
9587
'try reconnecting your account'
9688
)
9789

90+
return repos
91+
9892
def sync_organizations(self):
99-
orgs = self.paginate(
100-
'{url}/api/v4/groups'.format(url=self.adapter.provider_base_url),
101-
per_page=100,
102-
all_available=False,
103-
order_by='path',
104-
sort='asc',
105-
)
93+
orgs = []
94+
repositories = []
10695

10796
try:
97+
orgs = self.paginate(
98+
'{url}/api/v4/groups'.format(url=self.adapter.provider_base_url),
99+
per_page=100,
100+
all_available=False,
101+
order_by='path',
102+
sort='asc',
103+
)
108104
for org in orgs:
109105
org_obj = self.create_organization(org)
110106
org_repos = self.paginate(
@@ -117,6 +113,10 @@ def sync_organizations(self):
117113
order_by='path',
118114
sort='asc',
119115
)
116+
117+
# Add organization's repositories to the result
118+
repositories.extend(org_repos)
119+
120120
for repo in org_repos:
121121
self.create_repository(repo, organization=org_obj)
122122
except (TypeError, ValueError):
@@ -126,6 +126,8 @@ def sync_organizations(self):
126126
'try reconnecting your account'
127127
)
128128

129+
return orgs, repositories
130+
129131
def is_owned_by(self, owner_id):
130132
return self.account.extra_data['id'] == owner_id
131133

@@ -230,6 +232,12 @@ def create_organization(self, fields):
230232
organization.save()
231233
return organization
232234

235+
def get_repository_full_names(self, repositories):
236+
return {repository.get('name_with_namespace') for repository in repositories}
237+
238+
def get_organization_names(self, organizations):
239+
return {organization.get('name') for organization in organizations}
240+
233241
def get_webhook_data(self, repo_id, project, integration):
234242
"""
235243
Get webhook JSON data to post to the API.

0 commit comments

Comments
 (0)