Skip to content

Commit 142743b

Browse files
committed
🚧 BB gists and requests support ; 📼 Added integration tests and cassettes
Signed-off-by: Guyzmo <guyzmo+github@m0g.net>
1 parent 78cdaa3 commit 142743b

File tree

52 files changed

+4847
-109
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+4847
-109
lines changed

‎git_repo/services/ext/bitbucket.py

+143-65
Original file line numberDiff line numberDiff line change
@@ -8,56 +8,71 @@
88

99
from pybitbucket.bitbucket import Client, Bitbucket
1010
from pybitbucket.auth import BasicAuthenticator
11-
from pybitbucket.repository import Repository, RepositoryForkPolicy
12-
from pybitbucket.snippet import Snippet
11+
from pybitbucket.pullrequest import PullRequest, PullRequestPayload
12+
from pybitbucket.repository import (
13+
Repository, RepositoryPayload, RepositoryForkPayload,
14+
RepositoryForkPolicy, RepositoryType
15+
)
16+
from pybitbucket.snippet import Snippet, SnippetPayload
17+
from pybitbucket.user import User
1318

1419
from requests import Request, Session
1520
from requests.exceptions import HTTPError
16-
import json
21+
import os, json
1722

1823

1924
@register_target('bb', 'bitbucket')
2025
class BitbucketService(RepositoryService):
2126
fqdn = 'bitbucket.org'
2227

2328
def __init__(self, *args, **kwarg):
24-
self.bb_client = Client()
25-
self.bb = Bitbucket(self.bb_client)
29+
self.bb = Bitbucket(Client())
2630
super(BitbucketService, self).__init__(*args, **kwarg)
2731

2832
def connect(self):
2933
if not self._privatekey:
3034
raise ConnectionError('Could not connect to BitBucket. Please configure .gitconfig with your bitbucket credentials.')
3135
if not ':' in self._privatekey:
3236
raise ConnectionError('Could not connect to BitBucket. Please setup your private key with login:password')
33-
self.bb_client.config = BasicAuthenticator(*self._privatekey.split(':')+['z+git-repo+pub@m0g.net'])
34-
self.bb_client.session = self.bb_client.config.session
37+
auth = BasicAuthenticator(*self._privatekey.split(':')+['z+git-repo+pub@m0g.net'])
38+
self.bb.client.config = auth
39+
self.bb.client.session = self.bb.client.config.session = auth.start_http_session(self.bb.client.session)
3540
try:
36-
self.user
41+
_ = self.bb.client.config.who_am_i()
3742
except ResourceError as err:
3843
raise ConnectionError('Could not connect to BitBucket. Not authorized, wrong credentials.') from err
3944

4045
def create(self, user, repo, add=False):
4146
try:
4247
repo = Repository.create(
43-
repo,
44-
fork_policy=RepositoryForkPolicy.ALLOW_FORKS,
45-
is_private=False,
46-
client=self.bb_client
48+
RepositoryPayload(dict(
49+
fork_policy=RepositoryForkPolicy.ALLOW_FORKS,
50+
is_private=False
51+
)),
52+
repository_name=repo,
53+
owner=user,
54+
client=self.bb.client
4755
)
4856
if add:
49-
self.add(user=user, repo=repo, tracking=self.name)
57+
self.add(user=user, repo=repo.name, tracking=self.name)
5058
except HTTPError as err:
5159
if '400' in err.args[0].split(' '):
5260
raise ResourceExistsError('Project {} already exists on this account.'.format(repo)) from err
5361
raise ResourceError("Couldn't complete creation: {}".format(err)) from err
5462

5563
def fork(self, user, repo):
56-
raise NotImplementedError('No support yet by the underlying library.')
57-
try:
58-
self.get_repository(user, repo).fork()
59-
except HTTPError as err:
60-
raise ResourceError("Couldn't complete fork: {}".format(err)) from err
64+
# result = self.get_repository(user, repo).fork(
65+
# RepositoryForkPayload(dict(name=repo)),
66+
# owner=user)
67+
resp = self.bb.client.session.post(
68+
'https://api.bitbucket.org/1.0/repositories/{}/{}/fork'.format(user, repo),
69+
data={'name': repo}
70+
)
71+
if 404 == resp.status_code:
72+
raise ResourceNotFoundError("Couldn't complete fork: {}".format(resp.content.decode('utf-8')))
73+
elif 200 != resp.status_code:
74+
raise ResourceError("Couldn't complete fork: {}".format(resp.content.decode('utf-8')))
75+
result = resp.json()
6176
return '/'.join([result['owner'], result['slug']])
6277

6378
def delete(self, repo, user=None):
@@ -174,7 +189,7 @@ def gist_fetch(self, gist, fname=None):
174189
else:
175190
raise ResourceNotFoundError('Could not find file within gist.')
176191

177-
return self.bb_client.session.get(
192+
return self.bb.client.session.get(
178193
'https://bitbucket.org/!api/2.0/snippets/{}/{}/files/{}'.format(user, gist, gist_file)
179194
).content.decode('utf-8')
180195

@@ -198,8 +213,7 @@ def gist_clone(self, gist):
198213

199214
def gist_create(self, gist_pathes, description, secret=False):
200215
def load_file(fname, path='.'):
201-
with open(os.path.join(path, fname), 'r') as f:
202-
return {'content': f.read()}
216+
return open(os.path.join(path, fname), 'r')
203217

204218
gist_files = dict()
205219
for gist_path in gist_pathes:
@@ -210,66 +224,130 @@ def load_file(fname, path='.'):
210224
if not os.path.isdir(os.path.join(gist_path, gist_file)) and not gist_file.startswith('.'):
211225
gist_files[gist_file] = load_file(gist_file, gist_path)
212226

213-
gist = self.gh.create_gist(
214-
description=description,
227+
try:
228+
snippet = Snippet.create(
215229
files=gist_files,
216-
public=not secret # isn't it obvious? ☺
230+
payload=SnippetPayload(
231+
payload=dict(
232+
title=description,
233+
scm=RepositoryType.GIT,
234+
is_private=secret
235+
)
236+
),
237+
client=self.bb.client
217238
)
218239

219-
return gist.html_url
240+
return snippet.links['html']['href']
241+
except HTTPError as err:
242+
raise ResourceError("Couldn't create snippet: {}".format(err)) from err
220243

221244
def gist_delete(self, gist_id):
222-
gist = self.gh.gist(self._format_gist(gist_id))
223-
if not gist:
224-
raise ResourceNotFoundError('Could not find gist')
225-
gist.delete()
245+
try:
246+
snippet = next(self.bb.snippetByOwnerAndSnippetId(owner=self.user, snippet_id=gist_id)).delete()
247+
except HTTPError as err:
248+
if '404' in err.args[0].split(' '):
249+
raise ResourceNotFoundError("Could not find snippet {}.".format(gist_id)) from err
250+
raise ResourceError("Couldn't delete snippet: {}".format(err)) from err
226251

227252
def request_create(self, user, repo, local_branch, remote_branch, title, description=None):
228-
repository = self.gh.repository(user, repo)
229-
if not repository:
230-
raise ResourceNotFoundError('Could not find repository `{}/{}`!'.format(user, repo))
231-
if not remote_branch:
232-
remote_branch = self.repository.active_branch.name
233-
if not local_branch:
234-
local_branch = repository.master_branch or 'master'
235253
try:
236-
request = repository.create_pull(title,
237-
base=local_branch,
238-
head=':'.join([user, remote_branch]),
239-
body=description)
240-
except github3.models.GitHubError as err:
241-
if err.code == 422:
242-
if err.message == 'Validation Failed':
243-
for error in err.errors:
244-
if 'message' in error:
245-
raise ResourceError(error['message'])
246-
raise ResourceError("Unhandled formatting error: {}".format(err.errors))
247-
raise ResourceError(err.message)
248-
249-
return {'local': local_branch, 'remote': remote_branch, 'ref': request.number}
254+
repository = next(self.bb.repositoryByOwnerAndRepositoryName(owner=user, repository_name=repo))
255+
if not repository:
256+
raise ResourceNotFoundError('Could not find repository `{}/{}`!'.format(user, repo))
257+
if not remote_branch:
258+
try:
259+
remote_branch = next(repository.branches()).name
260+
except StopIteration:
261+
remote_branch = 'master'
262+
if not local_branch:
263+
local_branch = self.repository.active_branch.name
264+
request = PullRequest.create(
265+
PullRequestPayload(
266+
payload=dict(
267+
title=title,
268+
description=description or '',
269+
destination=dict(
270+
branch=dict(name=remote_branch)
271+
),
272+
source=dict(
273+
repository=dict(full_name='/'.join([self.user, repo])),
274+
branch=dict(name=local_branch)
275+
)
276+
)
277+
),
278+
repository_name=repo,
279+
owner=user,
280+
client=self.bb.client
281+
)
282+
except HTTPError as err:
283+
status_code = hasattr(err, 'code') and err.code or err.response.status_code
284+
if 404 == status_code:
285+
raise ResourceNotFoundError("Couldn't create request, project not found: {}".format(repo)) from err
286+
elif 400 == status_code and 'branch not found' in err.format_message():
287+
raise ResourceNotFoundError("Couldn't create request, branch not found: {}".format(local_branch)) from err
288+
raise ResourceError("Couldn't create request: {}".format(err)) from err
289+
290+
return {'local': local_branch, 'remote': remote_branch, 'ref': str(request.id)}
250291

251292
def request_list(self, user, repo):
252-
repository = self.gh.repository(user, repo)
253-
for pull in repository.iter_pulls():
254-
yield ( str(pull.number), pull.title, pull.links['issue'] )
293+
requests = set(
294+
(
295+
str(r.id),
296+
r.title,
297+
r.links['html']['href']
298+
) for r in self.bb.repositoryPullRequestsInState(
299+
owner=user,
300+
repository_name=repo,
301+
state='open'
302+
) if not isinstance(r, dict) # if no PR is empty, result is a dict
303+
)
304+
for pull in sorted(requests):
305+
try:
306+
yield pull
307+
except Exception as err:
308+
log.warn('Error while fetching request information: {}'.format(pull))
255309

256310
def request_fetch(self, user, repo, request, pull=False):
311+
log.warn('Bitbucket does not support fetching of PR using git. Use this command at your own risk.')
312+
if 'y' not in input('Are you sure to continue? [yN]> '):
313+
raise ResourceError('Command aborted.')
257314
if pull:
258315
raise NotImplementedError('Pull operation on requests for merge are not yet supported')
259316
try:
260-
for remote in self.repository.remotes:
261-
if remote.name == self.name:
262-
local_branch_name = 'request/{}'.format(request)
263-
self.fetch(
264-
remote,
265-
'pull/{}/head'.format(request),
266-
local_branch_name
267-
)
268-
return local_branch_name
269-
else:
270-
raise ResourceNotFoundError('Could not find remote {}'.format(self.name))
317+
repository = self.get_repository(user, repo)
318+
if self.repository.is_dirty():
319+
raise ResourceError('Please use this command after stashing your changes.')
320+
local_branch_name = 'requests/bitbucket/{}'.format(request)
321+
index = self.repository.index
322+
log.info('» Fetching pull request {}'.format(request))
323+
request = next(bb.repositoryPullRequestByPullRequestId(
324+
owner=user,
325+
repository_name=repo,
326+
pullrequest_id=request
327+
))
328+
commit = self.repository.rev_parse(request['destination']['commit']['hash'])
329+
self.repository.head.reference = commit
330+
log.info('» Creation of requests branch {}'.format(local_branch_name))
331+
# create new branch
332+
head = self.repository.create_head(local_branch_name)
333+
head.checkout()
334+
# fetch and apply patch
335+
log.info('» Fetching and writing the patch in current directory')
336+
patch = bb.client.session.get(request['links']['diff']['href']).content.decode('utf-8')
337+
with open('.tmp.patch', 'w') as f:
338+
f.write(patch)
339+
log.info('» Applying the patch')
340+
git.cmd.Git().apply('.tmp.patch', stat=True)
341+
os.unlink('.tmp.patch')
342+
log.info('» Going back to original branch')
343+
index.checkout() # back to former branch
344+
return local_branch_name
345+
except HTTPError as err:
346+
if '404' in err.args[0].split(' '):
347+
raise ResourceNotFoundError("Could not find snippet {}.".format(gist_id)) from err
348+
raise ResourceError("Couldn't delete snippet: {}".format(err)) from err
271349
except GitCommandError as err:
272-
if 'Error when fetching: fatal: Couldn\'t find remote ref' in err.command[0]:
350+
if 'Error when fetching: fatal: ' in err.command[0]:
273351
raise ResourceNotFoundError('Could not find opened request #{}'.format(request)) from err
274352
raise err
275353

‎tests/helpers.py

+26-19
Original file line numberDiff line numberDiff line change
@@ -671,8 +671,10 @@ def action_request_fetch(self, namespace, repository, request, pull=False, fail=
671671
self.service.request_fetch(repository, namespace, request)
672672

673673
def action_request_create(self,
674-
namespace, repository, branch,
674+
namespace, repository,
675675
title, description,
676+
source_branch='pr-test',
677+
target_branch='master',
676678
create_repository='test_create_requests',
677679
create_branch='pr-test'):
678680
'''
@@ -709,21 +711,25 @@ def prepare_project_for_test():
709711
# let's create a project and add it to current repository
710712
self.service.create(namespace, create_repository, add=True)
711713
# make a modification, commit and push it
712-
with open(os.path.join(self.repository.working_dir, 'first_file'), 'w') as test:
713-
test.write('he who makes a beast of himself gets rid of the pain of being a man. Dr Johnson')
714-
self.repository.git.add('first_file')
715-
self.repository.git.commit(message='First commit')
714+
with open(os.path.join(self.repository.working_dir, 'first_file'), 'w') as test:
715+
test.write('he who makes a beast of himself gets rid of the pain of being a man. Dr Johnson')
716+
self.repository.git.config('user.name', 'travis')
717+
self.repository.git.config('user.email', 'travis@fake.host')
718+
self.repository.git.add('first_file')
719+
self.repository.git.commit(message='First commit')
720+
if will_record:
716721
self.repository.git.push(self.service.name, 'master')
717-
# create a new branch
718-
new_branch = self.repository.create_head(create_branch, 'HEAD')
719-
self.repository.head.reference = new_branch
720-
self.repository.head.reset(index=True, working_tree=True)
721-
# make a modification, commit and push it to that branch
722-
with open(os.path.join(self.repository.working_dir, 'second_file'), 'w') as test:
723-
test.write('La meilleure façon de ne pas avancer est de suivre une idée fixe. J.Prévert')
724-
self.repository.git.add('second_file')
725-
self.repository.git.commit(message='Second commit')
726-
self.repository.git.push('github', create_branch)
722+
# create a new branch
723+
new_branch = self.repository.create_head(create_branch, 'HEAD')
724+
self.repository.head.reference = new_branch
725+
self.repository.head.reset(index=True, working_tree=True)
726+
# make a modification, commit and push it to that branch
727+
with open(os.path.join(self.repository.working_dir, 'second_file'), 'w') as test:
728+
test.write('La meilleure façon de ne pas avancer est de suivre une idée fixe. J.Prévert')
729+
self.repository.git.add('second_file')
730+
self.repository.git.commit(message='Second commit')
731+
if will_record:
732+
self.repository.git.push(self.service.name, create_branch)
727733
yield
728734
if will_record:
729735
self.service.delete(create_repository)
@@ -735,7 +741,8 @@ def prepare_project_for_test():
735741
request = self.service.request_create(
736742
namespace,
737743
repository,
738-
branch,
744+
source_branch,
745+
target_branch,
739746
title,
740747
description
741748
)
@@ -753,19 +760,19 @@ def action_gist_list(self, gist=None, gist_list_data=[]):
753760
for i, gf in enumerate(gist_list_data):
754761
assert gist_files[i] == gf
755762

756-
def action_gist_clone(self, gist):
763+
def action_gist_clone(self, gist, clone_url=None):
757764
with self.mockup_git(None, None):
758765
self.set_mock_popen_commands([
759766
('git version', b'git version 2.8.0', b'', 0),
760-
('git remote add gist {}.git'.format(gist), b'', b'', 0),
767+
('git remote add gist {}.git'.format(clone_url or gist), b'', b'', 0),
761768
('git pull --progress -v gist master', b'', b'\n'.join([
762769
b'POST git-upload-pack (140 bytes)',
763770
b'remote: Counting objects: 8318, done.',
764771
b'remote: Compressing objects: 100% (3/3), done.',
765772
b'remote: Total 8318 (delta 0), reused 0 (delta 0), pack-reused 8315',
766773
b'Receiving objects: 100% (8318/8318), 3.59 MiB | 974.00 KiB/s, done.',
767774
b'Resolving deltas: 100% (5126/5126), done.',
768-
bytes('From {}'.format(gist), 'utf-8'),
775+
bytes('From {}'.format(clone_url or gist), 'utf-8'),
769776
b' * branch master -> FETCH_HEAD']),
770777
0),
771778
])

0 commit comments

Comments
 (0)