Skip to content

Commit 595597b

Browse files
committed
🚧 Refactored fetch command for bitbucket using remotes hack
implementation of the fetch command for bitbucket with a hack based on remotes to keep the same format as usual, but using remotes to track changes from source repositories.
1 parent c43cc32 commit 595597b

11 files changed

+293
-162
lines changed

git_repo/services/ext/bitbucket.py

Lines changed: 43 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import logging
44
log = logging.getLogger('git_repo.bitbucket')
55

6-
from ..service import register_target, RepositoryService
6+
from ..service import register_target, RepositoryService, ProgressBar
77
from ...exceptions import ResourceError, ResourceExistsError, ResourceNotFoundError
8+
from ...tools import columnize
89

910
from pybitbucket.bitbucket import Client, Bitbucket
1011
from pybitbucket.auth import BasicAuthenticator
@@ -18,6 +19,9 @@
1819

1920
from requests import Request, Session
2021
from requests.exceptions import HTTPError
22+
23+
from git.exc import GitCommandError
24+
2125
from lxml import html
2226
import os, json, platform
2327

@@ -92,65 +96,46 @@ def delete(self, repo, user=None):
9296
raise ResourceError("Couldn't complete deletion: {}".format(err)) from err
9397

9498
def list(self, user, _long=False):
95-
import shutil, sys
96-
from datetime import datetime
97-
term_width = shutil.get_terminal_size((80, 20)).columns
98-
def col_print(lines, indent=0, pad=2):
99-
# prints a list of items in a fashion similar to the dir command
100-
# borrowed from https://gist.github.com/critiqjo/2ca84db26daaeb1715e1
101-
n_lines = len(lines)
102-
if n_lines == 0:
103-
return
104-
col_width = max(len(line) for line in lines)
105-
n_cols = int((term_width + pad - indent)/(col_width + pad))
106-
n_cols = min(n_lines, max(1, n_cols))
107-
col_len = int(n_lines/n_cols) + (0 if n_lines % n_cols == 0 else 1)
108-
if (n_cols - 1) * col_len >= n_lines:
109-
n_cols -= 1
110-
cols = [lines[i*col_len : i*col_len + col_len] for i in range(n_cols)]
111-
rows = list(zip(*cols))
112-
rows_missed = zip(*[col[len(rows):] for col in cols[:-1]])
113-
rows.extend(rows_missed)
114-
for row in rows:
115-
print(" "*indent + (" "*pad).join(line.ljust(col_width) for line in row))
116-
11799
try:
118100
user = User.find_user_by_username(user)
119101
except HTTPError as err:
120102
raise ResourceNotFoundError("User {} does not exists.".format(user)) from err
121103

122104
repositories = user.repositories()
123105
if not _long:
106+
yield "{}"
124107
repositories = list(repositories)
125-
col_print(["/".join([user.username, repo.name]) for repo in repositories])
108+
yield ("Total repositories: {}".format(len(repositories)),)
109+
yield from columnize(["/".join([user.username, repo.name]) for repo in repositories])
126110
else:
127-
print('Status\tCommits\tReqs\tIssues\tForks\tCoders\tWatch\tLikes\tLang\tModif\t\tName', file=sys.stderr)
111+
yield "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{:12}\t{}"
112+
yield ['Status', 'Commits', 'Reqs', 'Issues', 'Forks', 'Coders', 'Watch', 'Likes', 'Lang', 'Modif', 'Name']
128113
for repo in repositories:
129114
# if repo.updated_at.year < datetime.now().year:
130115
# date_fmt = "%b %d %Y"
131116
# else:
132117
# date_fmt = "%b %d %H:%M"
133118

134119
status = ''.join([
135-
'F' if getattr(repo, 'parent', None) else ' ', # is a fork?
136-
'P' if repo.is_private else ' ', # is private?
120+
'F' if getattr(repo, 'parent', None) else ' ', # is a fork?
121+
'P' if repo.is_private else ' ', # is private?
137122
])
138-
print('\t'.join([
123+
yield [
139124
# status
140125
status,
141126
# stats
142-
str(len(list(repo.commits()))), # number of commits
143-
str(len(list(repo.pullrequests()))), # number of pulls
144-
str('N.A.'), # number of issues
145-
str(len(list(repo.forks()))), # number of forks
146-
str('N.A.'), # number of contributors
147-
str(len(list(repo.watchers()))), # number of subscribers
148-
str('N.A.'), # number of ♥
127+
str(len(list(repo.commits()))), # number of commits
128+
str(len(list(repo.pullrequests()))), # number of pulls
129+
str('N.A.'), # number of issues
130+
str(len(list(repo.forks()))), # number of forks
131+
str('N.A.'), # number of contributors
132+
str(len(list(repo.watchers()))), # number of subscribers
133+
str('N.A.'), # number of ♥
149134
# info
150-
repo.language or '?', # language
151-
repo.updated_on, # date
152-
'/'.join([user.username, repo.name]), # name
153-
]))
135+
repo.language or '?', # language
136+
repo.updated_on, # date
137+
'/'.join([user.username, repo.name]), # name
138+
]
154139

155140
def get_repository(self, user, repo):
156141
try:
@@ -314,43 +299,36 @@ def request_list(self, user, repo):
314299
log.warn('Error while fetching request information: {}'.format(pull))
315300

316301
def request_fetch(self, user, repo, request, pull=False):
317-
log.warn('Bitbucket does not support fetching of PR using git. Use this command at your own risk.')
318-
if 'y' not in input('Are you sure to continue? [yN]> '):
319-
raise ResourceError('Command aborted.')
320302
if pull:
321303
raise NotImplementedError('Pull operation on requests for merge are not yet supported')
304+
305+
pb = ProgressBar()
306+
pb.setup(self.name)
307+
322308
try:
323-
repository = self.get_repository(user, repo)
324-
if self.repository.is_dirty():
325-
raise ResourceError('Please use this command after stashing your changes.')
326309
local_branch_name = 'requests/bitbucket/{}'.format(request)
327-
index = self.repository.index
328-
log.info('» Fetching pull request {}'.format(request))
329-
request = next(bb.repositoryPullRequestByPullRequestId(
310+
pr = next(self.bb.repositoryPullRequestByPullRequestId(
330311
owner=user,
331312
repository_name=repo,
332313
pullrequest_id=request
333314
))
334-
commit = self.repository.rev_parse(request['destination']['commit']['hash'])
335-
self.repository.head.reference = commit
336-
log.info('» Creation of requests branch {}'.format(local_branch_name))
337-
# create new branch
338-
head = self.repository.create_head(local_branch_name)
339-
head.checkout()
340-
# fetch and apply patch
341-
log.info('» Fetching and writing the patch in current directory')
342-
patch = bb.client.session.get(request['links']['diff']['href']).content.decode('utf-8')
343-
with open('.tmp.patch', 'w') as f:
344-
f.write(patch)
345-
log.info('» Applying the patch')
346-
git.cmd.Git().apply('.tmp.patch', stat=True)
347-
os.unlink('.tmp.patch')
348-
log.info('» Going back to original branch')
349-
index.checkout() # back to former branch
315+
source_branch = pr.source['branch']['name']
316+
source_slug = pr.source['repository']['full_name']
317+
source_url = pr.source['repository']['links']['html']['href']
318+
remote_name = 'requests/bitbucket/{}'.format(source_slug).replace('/', '-')
319+
try:
320+
remote = self.repository.remote(name=remote_name)
321+
except ValueError:
322+
remote = self.repository.create_remote(name=remote_name, url=source_url)
323+
refspec = '{}:{}'.format(source_branch, local_branch_name)
324+
refs = remote.fetch(refspec, progress=pb)
325+
for branch in self.repository.branches:
326+
if branch.name == local_branch_name:
327+
branch.set_tracking_branch(remote.refs[0])
350328
return local_branch_name
351329
except HTTPError as err:
352330
if '404' in err.args[0].split(' '):
353-
raise ResourceNotFoundError("Could not find snippet {}.".format(gist_id)) from err
331+
raise ResourceNotFoundError('Could not find opened request #{}'.format(request)) from err
354332
raise ResourceError("Couldn't delete snippet: {}".format(err)) from err
355333
except GitCommandError as err:
356334
if 'Error when fetching: fatal: ' in err.command[0]:

tests/helpers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -415,11 +415,11 @@ def teardown_method(self, method):
415415

416416
'''cassette name helper'''
417417

418-
def _make_cassette_name(self):
418+
def _make_cassette_name(self, frame_level=2):
419419
# returns the name of the function calling the function calling this one
420420
# in other words, when used in an helper function, returns the name of
421421
# the test function calling the helper function, to make a cassette name.
422-
test_function_name = sys._getframe(2).f_code.co_name
422+
test_function_name = sys._getframe(frame_level).f_code.co_name
423423
if test_function_name.startswith('test'):
424424
return '_'.join(['test', self.service.name, test_function_name])
425425
raise Exception("Helpers functions shall be used only within test functions!")
@@ -683,7 +683,7 @@ def action_request_fetch(self, namespace, repository, request, pull=False, fail=
683683
' * [new branch] master -> {1}/{0}'.format(request, local_branch)]).encode('utf-8'),
684684
0)
685685
])
686-
self.service.request_fetch(repository, namespace, request)
686+
self.service.request_fetch(repo=repository, user=namespace, request=request)
687687

688688
def action_request_create(self,
689689
namespace, repository,

tests/integration/cassettes/test_bitbucket_test_16_snippet_clone_with_gist.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,4 @@
9898
}
9999
],
100100
"recorded_with": "betamax/0.5.1"
101-
}
101+
}

tests/integration/cassettes/test_bitbucket_test_17_snippet_fetch_with_gist.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,4 @@
198198
}
199199
],
200200
"recorded_with": "betamax/0.5.1"
201-
}
201+
}

tests/integration/cassettes/test_bitbucket_test_19_snippet_fetch_with_gist_file.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,4 @@
198198
}
199199
],
200200
"recorded_with": "betamax/0.5.1"
201-
}
201+
}

tests/integration/cassettes/test_bitbucket_test_27_snippet_delete.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,4 @@
147147
}
148148
],
149149
"recorded_with": "betamax/0.5.1"
150-
}
150+
}

tests/integration/cassettes/test_bitbucket_test_31_request_fetch.json

Lines changed: 0 additions & 53 deletions
This file was deleted.

tests/integration/cassettes/test_bitbucket_test_31_request_fetch__bad_request.json

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"http_interactions": [
33
{
4-
"recorded_at": "2016-12-24T15:25:31",
4+
"recorded_at": "2016-12-29T00:47:59",
55
"request": {
66
"body": {
77
"encoding": "utf-8",
@@ -10,34 +10,35 @@
1010
"headers": {
1111
"Accept": "application/json",
1212
"Accept-Encoding": "identity",
13-
"Authorization": "Basic Z3V5em1vOkpKNHRsNHNzMTRuLmMwbQ==",
13+
"Authorization": "Basic Z3V5em1vOlF0WmpzZVFMaGE4NFBMZnRwaGJF",
1414
"Connection": "keep-alive",
1515
"From": "[email protected]",
16-
"User-Agent": "pybitbucket/0.11.2 python-requests/2.12.4"
16+
"User-Agent": "pybitbucket/0.12.0 python-requests/2.12.4"
1717
},
1818
"method": "GET",
1919
"uri": "https://api.bitbucket.org/2.0/user"
2020
},
2121
"response": {
2222
"body": {
2323
"encoding": "utf-8",
24-
"string": "{\"username\": \"<BITBUCKET_NAMESPACE>\", \"website\": \"http://i.got.nothing.to/blog\", \"display_name\": \"Guyzmo\", \"uuid\": \"{abf29c83-e77e-4c9a-b5c7-38db801ef79e}\", \"links\": {\"hooks\": {\"href\": \"https://api.bitbucket.org/2.0/users/<BITBUCKET_NAMESPACE>/hooks\"}, \"self\": {\"href\": \"https://api.bitbucket.org/2.0/users/<BITBUCKET_NAMESPACE>\"}, \"repositories\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/<BITBUCKET_NAMESPACE>\"}, \"html\": {\"href\": \"https://bitbucket.org/<BITBUCKET_NAMESPACE>/\"}, \"followers\": {\"href\": \"https://api.bitbucket.org/2.0/users/<BITBUCKET_NAMESPACE>/followers\"}, \"avatar\": {\"href\": \"https://bitbucket.org/account/<BITBUCKET_NAMESPACE>/avatar/32/\"}, \"following\": {\"href\": \"https://api.bitbucket.org/2.0/users/<BITBUCKET_NAMESPACE>/following\"}, \"snippets\": {\"href\": \"https://api.bitbucket.org/2.0/snippets/<BITBUCKET_NAMESPACE>\"}}, \"created_on\": \"2008-09-20T15:08:53.695316+00:00\", \"location\": null, \"type\": \"user\"}"
24+
"string": "{\"username\": \"guyzmo\", \"website\": \"http://i.got.nothing.to/blog\", \"display_name\": \"Guyzmo\", \"uuid\": \"{abf29c83-e77e-4c9a-b5c7-38db801ef79e}\", \"links\": {\"hooks\": {\"href\": \"https://api.bitbucket.org/2.0/users/guyzmo/hooks\"}, \"self\": {\"href\": \"https://api.bitbucket.org/2.0/users/guyzmo\"}, \"repositories\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/guyzmo\"}, \"html\": {\"href\": \"https://bitbucket.org/guyzmo/\"}, \"followers\": {\"href\": \"https://api.bitbucket.org/2.0/users/guyzmo/followers\"}, \"avatar\": {\"href\": \"https://bitbucket.org/account/guyzmo/avatar/32/\"}, \"following\": {\"href\": \"https://api.bitbucket.org/2.0/users/guyzmo/following\"}, \"snippets\": {\"href\": \"https://api.bitbucket.org/2.0/snippets/guyzmo\"}}, \"created_on\": \"2008-09-20T15:08:53.695316+00:00\", \"location\": null, \"type\": \"user\"}"
2525
},
2626
"headers": {
2727
"Connection": "keep-alive",
2828
"Content-Length": "801",
2929
"Content-Type": "application/json; charset=utf-8",
30-
"Date": "Sat, 24 Dec 2016 15:25:32 GMT",
30+
"Date": "Thu, 29 Dec 2016 00:47:59 GMT",
3131
"ETag": "\"321b3bdea810843db96c90951905a5e3\"",
3232
"Server": "nginx",
3333
"Strict-Transport-Security": "max-age=31536000",
3434
"Vary": "Authorization",
3535
"X-Accepted-OAuth-Scopes": "account",
3636
"X-Content-Type-Options": "nosniff",
3737
"X-Frame-Options": "SAMEORIGIN",
38-
"X-Render-Time": "0.0141069889069",
39-
"X-Request-Count": "707",
40-
"X-Served-By": "app-139",
38+
"X-OAuth-Scopes": "snippet:write, snippet, repository:delete, repository:admin, pullrequest:write, repository, project:write, project, team, account",
39+
"X-Render-Time": "0.0356111526489",
40+
"X-Request-Count": "583",
41+
"X-Served-By": "app-140",
4142
"X-Static-Version": "e72177d765d6",
4243
"X-Version": "e72177d765d6"
4344
},
@@ -47,6 +48,54 @@
4748
},
4849
"url": "https://api.bitbucket.org/2.0/user"
4950
}
51+
},
52+
{
53+
"recorded_at": "2016-12-29T00:47:59",
54+
"request": {
55+
"body": {
56+
"encoding": "utf-8",
57+
"string": ""
58+
},
59+
"headers": {
60+
"Accept": "application/json",
61+
"Accept-Encoding": "identity",
62+
"Authorization": "Basic Z3V5em1vOlF0WmpzZVFMaGE4NFBMZnRwaGJF",
63+
"Connection": "keep-alive",
64+
"From": "[email protected]",
65+
"User-Agent": "pybitbucket/0.12.0 python-requests/2.12.4"
66+
},
67+
"method": "GET",
68+
"uri": "https://api.bitbucket.org/2.0/repositories/atlassian/python-bitbucket/pullrequests/42"
69+
},
70+
"response": {
71+
"body": {
72+
"encoding": "utf-8",
73+
"string": "{\"type\": \"error\", \"error\": {\"message\": \"No PullRequest matches the given query.\"}}"
74+
},
75+
"headers": {
76+
"Connection": "keep-alive",
77+
"Content-Length": "82",
78+
"Content-Type": "application/json; charset=utf-8",
79+
"Date": "Thu, 29 Dec 2016 00:47:59 GMT",
80+
"ETag": "\"c4601e458b69ec3b0ba0e466c8c8df86\"",
81+
"Server": "nginx",
82+
"Strict-Transport-Security": "max-age=31536000",
83+
"Vary": "Authorization",
84+
"X-Accepted-OAuth-Scopes": "pullrequest",
85+
"X-Frame-Options": "SAMEORIGIN",
86+
"X-OAuth-Scopes": "snippet:write, snippet, repository:delete, repository:admin, pullrequest:write, repository, project:write, project, team, account",
87+
"X-Render-Time": "0.0433068275452",
88+
"X-Request-Count": "425",
89+
"X-Served-By": "app-142",
90+
"X-Static-Version": "e72177d765d6",
91+
"X-Version": "e72177d765d6"
92+
},
93+
"status": {
94+
"code": 404,
95+
"message": "NOT FOUND"
96+
},
97+
"url": "https://api.bitbucket.org/2.0/repositories/atlassian/python-bitbucket/pullrequests/42"
98+
}
5099
}
51100
],
52101
"recorded_with": "betamax/0.5.1"

0 commit comments

Comments
 (0)