Skip to content

Commit 8493155

Browse files
committed
🚧 Refactoring of the fork command, for a more natural approach
* cf #34 * updated tests Signed-off-by: Guyzmo <[email protected]>
1 parent c0ff1a1 commit 8493155

File tree

10 files changed

+205
-243
lines changed

10 files changed

+205
-243
lines changed

git_repo/repo.py

Lines changed: 80 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22

33
'''
44
Usage:
5-
{self} [--path=<path>] [-v...] <target> fork [<branch>] [--clone]
5+
{self} [--path=<path>] [-v...] <target> fork [--branch=<branch>]
66
{self} [--path=<path>] [-v...] <target> create [--add]
77
{self} [--path=<path>] [-v...] <target> delete [-f]
88
{self} [--path=<path>] [-v...] <target> open
9-
{self} [--path=<path>] [-v...] <target> fork <user>/<repo> [<branch>] [--clone]
9+
{self} [--path=<path>] [-v...] <target> fork <user>/<repo> [--branch=<branch>]
10+
{self} [--path=<path>] [-v...] <target> fork <user>/<repo> <repo> [--branch=<branch>]
1011
{self} [--path=<path>] [-v...] <target> create <user>/<repo> [--add]
1112
{self} [--path=<path>] [-v...] <target> delete <user>/<repo> [-f]
1213
{self} [--path=<path>] [-v...] <target> open <user>/<repo>
13-
{self} [--path=<path>] [-v...] <target> clone <user>/<repo> [<branch>]
14+
{self} [--path=<path>] [-v...] <target> clone <user>/<repo> [<repo> [<branch>]]
1415
{self} [--path=<path>] [-v...] <target> add <user>/<repo> [<name>] [--tracking=<branch>] [-a]
1516
{self} [--path=<path>] [-v...] <target> request (list|ls)
1617
{self} [--path=<path>] [-v...] <target> request fetch <request>
@@ -55,9 +56,14 @@
5556
-t,--tracking=<branch> Makes this remote tracking for the current branch
5657
-a,--alone Does not add the remote to the 'all' remote
5758
58-
Options for fork and clone:
59-
<branch> Branch to pull (when cloning) [default: master]
60-
--clone Clone locally after fork
59+
Option for both clone and fork:
60+
<repo> Name of the local workspace directory
61+
62+
Options for clone:
63+
<branch> Branch to pull [default: master]
64+
65+
Options for fork:
66+
--branch=<branch> Branch to pull [default: master]
6167
6268
Options for create:
6369
--add Add to local repository after creation
@@ -159,19 +165,20 @@ def _guess_repo_slug(self, repository, service):
159165
config = repository.config_reader()
160166
target = service.name
161167
for remote in repository.remotes:
162-
for url in remote.urls:
163-
if url.startswith('https'):
164-
if '.git' in url:
165-
url = url[:-4]
166-
*_, user, name = url.split('/')
167-
self.set_repo_slug('/'.join([user, name]))
168-
break
169-
elif url.startswith('git@'):
170-
if '.git' in url:
171-
url = url[:-4]
172-
_, repo_slug = url.split(':')
173-
self.set_repo_slug(repo_slug)
174-
break
168+
if remote.name in (target, 'upstream', 'origin'):
169+
for url in remote.urls:
170+
if url.startswith('https'):
171+
if '.git' in url:
172+
url = url[:-4]
173+
*_, user, name = url.split('/')
174+
self.set_repo_slug('/'.join([user, name]))
175+
break
176+
elif url.startswith('git@'):
177+
if '.git' in url:
178+
url = url[:-4]
179+
_, repo_slug = url.split(':')
180+
self.set_repo_slug(repo_slug)
181+
break
175182

176183
def get_service(self, lookup_repository=True):
177184
if not lookup_repository:
@@ -243,6 +250,19 @@ def set_branch(self, branch):
243250

244251
self.branch = branch
245252

253+
@store_parameter('<branch>')
254+
@store_parameter('--branch')
255+
def set_branch(self, branch):
256+
# FIXME workaround for default value that is not correctly parsed in docopt
257+
if branch == None:
258+
branch = 'master'
259+
260+
self.branch = branch
261+
262+
@store_parameter('<repo>')
263+
def set_target_repo(self, repo):
264+
self.target_repo = repo
265+
246266
@store_parameter('<name>')
247267
def set_name(self, name):
248268
self.remote_name = name
@@ -272,69 +292,61 @@ def do_remote_add(self):
272292

273293
@register_action('fork')
274294
def do_fork(self):
275-
def clone_repo():
276-
try:
277-
repo_path = os.path.join(self.path, self.repo_name)
278-
repository = Repo(repo_path)
279-
except (InvalidGitRepositoryError, NoSuchPathError):
280-
repo_path = os.path.join(self.path, self.repo_name)
281-
repository = Repo.init(repo_path)
282-
return repository, repo_path
283-
284-
def lookup_repo():
295+
if not self.repo_slug:
296+
service = self.get_service(lookup_repository=True)
285297
try:
286298
repo_path = self.path
287299
repository = Repo(repo_path)
288300
except (InvalidGitRepositoryError, NoSuchPathError):
301+
raise ArgumentError('Path {} is not a git repository'.format(self.path))
302+
303+
else:
304+
# git <target> fork <user>/<repo>
305+
if not self.target_repo:
306+
if not self.user_name:
307+
raise ArgumentError('Cannot clone repository, '
308+
'you shall provide either a <user>/<repo> parameter '
309+
'or no parameters to fork current repository!')
310+
service = self.get_service(None)
311+
312+
# git <target> fork <user>/<repo> <path>
313+
else:
314+
repo_path = os.path.join(self.path, self.target_repo)
289315
try:
290-
repo_path = os.path.join(self.path, self.repo_name)
291-
repository = Repo(repo_path)
316+
service = RepositoryService.get_service(Repo(repo_path), self.target)
292317
except (InvalidGitRepositoryError, NoSuchPathError):
293-
return None, None
294-
return repository, repo_path
318+
service = self.get_service(lookup_repository=False)
319+
# if the repository does not exists at given path, clone upstream into that path
320+
self.do_clone(service, repo_path)
295321

296-
service = self.get_service(lookup_repository=self.repo_slug == None)
297-
if not self.repo_name and not self.user_name:
298-
raise ArgumentError('Cannot clone repository, '
299-
'you shall provide the <user>/<repo> parameter '
300-
'or run the command from a git repository!')
301-
if not self.clone:
302-
repository, repo_path = lookup_repo()
303-
if repository == None:
304-
repository, repo_path = clone_repo()
305-
self.clone = True
306-
else:
307-
repository, repo_path = clone_repo()
308-
service = RepositoryService.get_service(repository, self.target)
309-
service.fork(self.user_name, self.repo_name, branch=self.branch, clone=self.clone)
310-
if self.clone:
311-
log.info('Successfully cloned repository {} in {}'.format(
312-
self.repo_slug,
313-
repo_path)
314-
)
315-
else:
316-
log.info('Successfully added repository {} as a remote to {}'.format(
317-
self.repo_slug,
318-
repo_path)
319-
)
322+
service.run_fork(self.user_name, self.repo_name, branch=self.branch)
323+
324+
if not self.repo_slug or self.target_repo:
325+
log.info('Successfully forked {} as {} within {}.'.format(
326+
self.repo_slug, '/'.join([service.username, self.repo_name]), repo_path))
320327

321328
return 0
322329

323330
@register_action('clone')
324-
def do_clone(self):
325-
service = self.get_service(lookup_repository=False)
326-
repo_path = os.path.join(self.path, self.repo_name)
331+
def do_clone(self, service=None, repo_path=None):
332+
service = service or self.get_service(lookup_repository=False)
333+
repo_path = repo_path or os.path.join(self.path, self.target_repo or self.repo_name)
327334
if os.path.exists(repo_path):
328335
raise FileExistsError('Cannot clone repository, '
329336
'a folder named {} already exists!'.format(repo_path))
330-
repository = Repo.init(repo_path)
331-
service = RepositoryService.get_service(repository, self.target)
332-
service.clone(self.user_name, self.repo_name, self.branch)
333-
log.info('Successfully cloned `{}` into `{}`!'.format(
334-
service.format_path(self.repo_slug),
335-
repo_path)
336-
)
337-
return 0
337+
try:
338+
repository = Repo.init(repo_path)
339+
service = RepositoryService.get_service(repository, self.target)
340+
service.clone(self.user_name, self.repo_name, self.branch)
341+
log.info('Successfully cloned `{}` into `{}`!'.format(
342+
service.format_path(self.repo_slug),
343+
repo_path)
344+
)
345+
return 0
346+
except Exception as err:
347+
if os.path.exists(repo_path):
348+
os.removedirs(repo_path)
349+
raise err from err
338350

339351
@register_action('create')
340352
def do_create(self):

git_repo/services/ext/bitbucket.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def connect(self):
104104
if not ':' in self._privatekey:
105105
raise ConnectionError('Could not connect to BitBucket. Please setup your private key with login:password')
106106
self.bb.username, self.bb.password = self._privatekey.split(':')
107+
self.username = self.bb.username
107108
result, _ = self.bb.get_user()
108109
if not result:
109110
raise ConnectionError('Could not connect to BitBucket. Not authorized, wrong credentials.')
@@ -117,21 +118,11 @@ def create(self, user, repo, add=False):
117118
if add:
118119
self.add(user=user, repo=repo, tracking=self.name)
119120

120-
def fork(self, user, repo, branch='master', clone=False):
121-
log.info("Forking repository {}/{}…".format(user, repo))
122-
repositories = self.get_repository(user, repo)
123-
if repo in repositories:
124-
raise ResourceExistsError('Cannot fork repository as it already exists')
121+
def fork(self, user, repo):
125122
success, result = self.bb.fork(user, repo)
126123
if not success:
127124
raise ResourceError("Couldn't complete fork: {message} (error #{code}: {reason})".format(**result))
128-
fork = result
129-
self.add(repo=repo, user=user, name='upstream', alone=True)
130-
remote = self.add(repo=fork['slug'], user=fork['owner'], tracking=self.name)
131-
if clone:
132-
self.pull(remote, branch)
133-
log.info("New forked repository available at {}".format(self.format_path(repository=fork['slug'],
134-
namespace=fork['owner'])))
125+
return '/'.join([result['owner'], result['slug']])
135126

136127
def delete(self, repo, user=None):
137128
if not user:

git_repo/services/ext/github.py

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def connect(self):
2222
try:
2323
self.gh.login(token=self._privatekey)
2424
self.username = self.gh.user().name
25+
self.username = self.gh.user().name
2526
except github3.models.GitHubError as err:
2627
if err.code is 401:
2728
if not self._privatekey:
@@ -45,35 +46,14 @@ def create(self, user, repo, add=False):
4546
if add:
4647
self.add(user=self.username, repo=repo, tracking=self.name)
4748

48-
def fork(self, user, repo, branch='master', clone=False):
49-
log.info("Forking repository {}/{}…".format(user, repo))
50-
# checking for an 'upstream' remote.
51-
upstream_remotes = list(filter(lambda x: x.name == 'upstream', self.repository.remotes))
52-
if len(upstream_remotes) != 0:
53-
raise ResourceExistsError('A remote named `upstream` already exists. Has this repo already been forked?')
54-
# forking the repository on the service
49+
def fork(self, user, repo):
5550
try:
56-
fork = self.gh.repository(user, repo).create_fork()
51+
return self.gh.repository(user, repo).create_fork().full_name
5752
except github3.models.GitHubError as err:
5853
if err.message == 'name already exists on this account':
5954
raise ResourceExistsError("Project already exists.") from err
6055
else: # pragma: no cover
6156
raise ResourceError("Unhandled error: {}".format(err)) from err
62-
# checking if a remote with the service's name already exists
63-
service_remotes = list(filter(lambda x: x.name == self.name, self.repository.remotes))
64-
if len(service_remotes) != 0:
65-
# if it does, rename it to upstream
66-
repo.delete(service_remotes[0])
67-
repo.create_remote('upstream', service_remotes[0].url)
68-
else:
69-
# otherwise create an upstream remote with the source repository
70-
self.add(user=user, repo=repo, name='upstream', alone=True)
71-
# add the service named repository
72-
remote = self.add(repo=repo, user=self.username, tracking=self.name)
73-
if clone:
74-
self.pull(remote, branch)
75-
log.info("New forked repository available at {}/{}".format(self.url_ro,
76-
fork.full_name))
7757

7858
def delete(self, repo, user=None):
7959
if not user:

git_repo/services/ext/gitlab.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def __init__(self, *args, **kwarg):
2222
def connect(self):
2323
self.gl.set_token(self._privatekey)
2424
self.gl.token_auth()
25+
self.username = self.gl.user.username
2526

2627
def create(self, user, repo, add=False):
2728
try:
@@ -38,20 +39,14 @@ def create(self, user, repo, add=False):
3839
if add:
3940
self.add(user=user, repo=repo, tracking=self.name)
4041

41-
def fork(self, user, repo, branch='master', clone=False):
42+
def fork(self, user, repo):
4243
try:
43-
fork = self.gl.projects.get('{}/{}'.format(user, repo)).forks.create({})
44+
return self.gl.projects.get('{}/{}'.format(user, repo)).forks.create({}).path_with_namespace
4445
except GitlabCreateError as err:
4546
if json.loads(err.response_body.decode('utf-8'))['message']['name'][0] == 'has already been taken':
4647
raise ResourceExistsError("Project already exists.") from err
4748
else:
4849
raise ResourceError("Unhandled error: {}".format(err)) from err
49-
self.add(user=user, repo=repo, name='upstream', alone=True)
50-
remote = self.add(repo=fork.path, user=fork.namespace['path'], tracking=self.name)
51-
if clone:
52-
self.pull(remote, branch)
53-
log.info("New forked repository available at {}/{}".format(self.url_ro,
54-
fork.path_with_namespace))
5550

5651
def delete(self, repo, user=None):
5752
if not user:

git_repo/services/service.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ def add(self, repo, user=None, name=None, tracking=False, alone=False, rw=True):
277277
else:
278278
url = self.format_path(repo, user, rw=True)
279279
# check if url not already in remote all
280-
if url not in all_remote.list_urls:
281-
all_remote.set_url(url=self.format_path(repo, user, rw=rw), add=True)
280+
if url not in all_remote.urls:
281+
all_remote.set_url(new_url=self.format_path(repo, user, rw=rw), add=True)
282282

283283
# adding "self" as the tracking remote
284284
if tracking:
@@ -293,6 +293,33 @@ def add(self, repo, user=None, name=None, tracking=False, alone=False, rw=True):
293293
else:
294294
return self.repository.create_remote(name, self.format_path(repo, user, rw=rw))
295295

296+
297+
def run_fork(self, user, repo, branch):
298+
if user == self.user:
299+
# forking the repository on the service
300+
raise ResourceError("Cannot fork a project from yourself.")
301+
log.info("Forking repository {}/{}…".format(user, repo))
302+
# checking for an 'upstream' remote.
303+
if self.repository:
304+
upstream_remotes = list(filter(lambda x: x.name == 'upstream', self.repository.remotes))
305+
if len(upstream_remotes) != 0:
306+
raise ResourceExistsError('A remote named `upstream` already exists. Has this repo already been forked?')
307+
fork_name = self.fork(user, repo)
308+
# checking if a remote with the service's name already exists
309+
if self.repository:
310+
service_remotes = list(filter(lambda x: x.name == self.name, self.repository.remotes))
311+
if len(service_remotes) != 0:
312+
# if it does, rename it to upstream
313+
self.repository.create_remote('upstream', service_remotes[0].url)
314+
self.repository.delete_remote(service_remotes[0].name)
315+
else:
316+
# otherwise create an upstream remote with the source y
317+
self.add(user=user, repo=repo, name='upstream', alone=True)
318+
# add the service named repository
319+
remote = self.add(repo=repo, user=self.user, tracking=self.name)
320+
log.info("New forked repository available at {}/{}".format(self.url_ro,
321+
fork_name))
322+
296323
def open(self, user=None, repo=None):
297324
'''Open the URL of a repository in the user's browser'''
298325
call([OPEN_COMMAND, self.format_path(repo, namespace=user, rw=False)])
@@ -322,7 +349,7 @@ def create(self, user, repo, add=False): #pragma: no cover
322349
'''
323350
raise NotImplementedError
324351

325-
def fork(self, user, repo, clone=False): #pragma: no cover
352+
def fork(self, user, repo): #pragma: no covr
326353
'''Forks a new remote repository on the service
327354
and pulls commits from it
328355

tests/helpers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ def setup_args(self, d, args={}):
167167
'<name>': None,
168168
'<branch>': None,
169169
'<target>': self.target,
170+
'<target_repo>': None,
170171
'<user>/<repo>': '',
171172
'add': False,
172173
'clone': False,
@@ -459,7 +460,7 @@ def action_fork(self, cassette_name, local_namespace, remote_namespace, reposito
459460
])
460461
with self.recorder.use_cassette('_'.join(['test', self.service.name, cassette_name])):
461462
self.service.connect()
462-
self.service.fork(remote_namespace, repository, clone=True)
463+
self.service.fork(remote_namespace, repository)
463464
# emulate the outcome of the git actions
464465
self.service.repository.create_remote('upstream', url=remote_slug)
465466
self.service.repository.create_remote('all', url=local_slug)
@@ -490,7 +491,7 @@ def action_fork__no_clone(self, cassette_name, local_namespace, remote_namespace
490491
])
491492
with self.recorder.use_cassette('_'.join(['test', self.service.name, cassette_name])):
492493
self.service.connect()
493-
self.service.fork(remote_namespace, repository, clone=False)
494+
self.service.fork(remote_namespace, repository)
494495
# emulate the outcome of the git actions
495496
self.service.repository.create_remote('upstream', url=remote_slug)
496497
self.service.repository.create_remote('all', url=local_slug)

0 commit comments

Comments
 (0)