Skip to content

Commit 0ffa4b5

Browse files
committed
👍 Merge branch 'devel' ; 🔖 Version bump to 1.7.5
🚧 Feature: * Added the repository list feature support, which lists all repositories for an user. (fixes #50) 🚒 Bugfixes: * Allow cloning in empty directories (fixes #58) * Allow URLs that contain .git in the name (fixes #55) * Show a nice error when missing credentials on create (fixes #74) * Made remote add show the proper name on success (fixes #72) 💄 Cosmetics: * Removed distutil's usage of setup function in setup.py (cf #71) * Improved versions used in requirements (cf #61) * Fixed link in TODO list (cf #70) Signed-off-by: Guyzmo <[email protected]>
2 parents 2d1dd1f + 808af79 commit 0ffa4b5

13 files changed

+853
-101
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
language: python
2+
# Don't use the Travis Container-Based Infrastructure
3+
sudo: true
24
matrix:
35
include:
46
- os: linux
@@ -26,6 +28,7 @@ matrix:
2628
addons:
2729
apt:
2830
packages:
31+
- git
2932
- pandoc
3033
before_install: |
3134
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* https://gitlab.com/guyzmo/git-repo
66
* https://bitbucket.org/guyzmo/git-repo
77
* Issues: https://github.com/guyzmo/git-repo/issues
8+
* Chat on IRC: [#git-repo @freenode](irc://irc.freenode.org/git-repo)
89
* [![Issues in Ready](https://badge.waffle.io/guyzmo/git-repo.png?label=ready&title=Ready)](https://waffle.io/guyzmo/git-repo) [![Issues in Progress](https://badge.waffle.io/guyzmo/git-repo.png?label=in%20progress&title=Progress)](https://waffle.io/guyzmo/git-repo) [![Show Travis Build Status](https://travis-ci.org/guyzmo/git-repo.svg)](https://travis-ci.org/guyzmo/git-repo)
910
* [![Pypi Version](https://img.shields.io/pypi/v/git-repo.svg) ![Pypi Downloads](https://img.shields.io/pypi/dm/git-repo.svg)](https://pypi.python.org/pypi/git-repo)
1011

@@ -259,7 +260,7 @@ To use your own credentials, you can setup the following environment variables:
259260
* [ ] gitlab support (cf [#10](https://github.com/guyzmo/git-repo/issues/10))
260261
* [ ] bitbucket support (cf [#11](https://github.com/guyzmo/git-repo/issues/11))
261262
* [ ] add OAuth support for bitbucket (cf [#14](https://github.com/guyzmo/git-repo/issues/14))
262-
* [ ] add support for managing SSH keys (cf [#22](https://github.com/guyzmo/git-repo/issues/15))
263+
* [ ] add support for managing SSH keys (cf [#22](https://github.com/guyzmo/git-repo/issues/22))
263264
* [ ] add support for issues?
264265
* [ ] add support for gogs (cf [#18](https://github.com/guyzmo/git-repo/issues/18))
265266
* [ ] add support for gerrit (cf [#19](https://github.com/guyzmo/git-repo/issues/19))
@@ -270,13 +271,14 @@ To use your own credentials, you can setup the following environment variables:
270271

271272
The project and original idea has been brought and is maintained by:
272273

273-
* Bernard [@guyzmo](https://github.com/guyzmo) Pratz [commits](https://github.com/guyzmo/git-repo/commits?author=guyzmo)
274+
* Bernard [@guyzmo](https://github.com/guyzmo) Pratz [commits](https://github.com/guyzmo/git-repo/commits?author=guyzmo)
274275

275276
With code contributions coming from:
276277

277-
* [@guyhughes](https://github.com/guyhughes) [commits](https://github.com/guyzmo/git-repo/commits?author=guyhughes)
278-
* [@buaazp](https://github.com/buaazp) [commits](https://github.com/guyzmo/git-repo/commits?author=buaazp)
279-
* [@peterazmanov](https://github.com/peterazmanov) [commits](https://github.com/guyzmo/git-repo/commits?author=peterazmanov)
278+
* [@guyhughes](https://github.com/guyhughes)[commits](https://github.com/guyzmo/git-repo/commits?author=guyhughes)
279+
* [@buaazp](https://github.com/buaazp)[commits](https://github.com/guyzmo/git-repo/commits?author=buaazp)
280+
* [@peterazmanov](https://github.com/peterazmanov)[commits](https://github.com/guyzmo/git-repo/commits?author=peterazmanov)
281+
* [@Crazybus](https://github.com/Crazybus)[commits](https://github.com/guyzmo/git-repo/commits?author=Crazybus)
280282

281283
### License
282284

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.7.4
1+
1.7.5

git_repo/repo.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
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> (list|ls) [-l] <user>
910
{self} [--path=<path>] [-v...] <target> fork <user>/<repo> [--branch=<branch>]
1011
{self} [--path=<path>] [-v...] <target> fork <user>/<repo> <repo> [--branch=<branch>]
1112
{self} [--path=<path>] [-v...] <target> create <user>/<repo> [--add]
@@ -23,11 +24,11 @@
2324
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <title> [--branch=<remote>] [--message=<message>]
2425
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <local_branch> <title> [--branch=<remote>] [--message=<message>]
2526
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <remote_branch> <local_branch> <title> [--branch=<remote>] [--message=<message>]
26-
{self} [--path=<path>] [-v...] <target> gist (list|ls) [<gist>]
27-
{self} [--path=<path>] [-v...] <target> gist clone <gist>
28-
{self} [--path=<path>] [-v...] <target> gist fetch <gist> [<gist_file>]
29-
{self} [--path=<path>] [-v...] <target> gist create [--secret] <description> [<gist_path> <gist_path>...]
30-
{self} [--path=<path>] [-v...] <target> gist delete <gist> [-f]
27+
{self} [--path=<path>] [-v...] <target> (gist|snippet) (list|ls) [<gist>]
28+
{self} [--path=<path>] [-v...] <target> (gist|snippet) clone <gist>
29+
{self} [--path=<path>] [-v...] <target> (gist|snippet) fetch <gist> [<gist_file>]
30+
{self} [--path=<path>] [-v...] <target> (gist|snippet) create [--secret] <description> [<gist_path> <gist_path>...]
31+
{self} [--path=<path>] [-v...] <target> (gist|snippet) delete <gist> [-f]
3132
{self} [--path=<path>] [-v...] <target> config [--config=<gitconfig>]
3233
{self} [-v...] config [--config=<gitconfig>]
3334
{self} --help
@@ -40,6 +41,7 @@
4041
fork Fork (and clone) the repository from the service
4142
create Make this repository a new remote on the service
4243
delete Delete the remote repository
44+
list Lists the repositories for a given user
4345
gist Manages gist files
4446
request Handles requests for merge
4547
open Open the given or current repository in a browser
@@ -51,6 +53,12 @@
5153
-v,--verbose Makes it more chatty (repeat twice to see git commands)
5254
-h,--help Shows this message
5355
56+
Options for list:
57+
<user> Name of the user whose repositories will be listed
58+
-l,--long Show one repository per line, when set show the results
59+
with the following columns:
60+
STATUS, COMMITS, REQUESTS, ISSUES, FORKS, CONTRIBUTORS, WATCHERS, LIKES, LANGUAGE, MODIF, NAME
61+
5462
Options for add:
5563
<name> Name to use for the remote (defaults to name of repo)
5664
-t,--tracking=<branch> Makes this remote tracking for the current branch
@@ -86,7 +94,6 @@
8694
8795
Configuration options:
8896
alias Name to use for the git remote
89-
url URL of the repository
9097
fqdn URL of the repository
9198
type Name of the service to use (github, gitlab, bitbucket)
9299
@@ -101,7 +108,7 @@
101108
token = yourapitoken
102109
fqdn = custom.org
103110
104-
{self} version {version}, Copyright ⓒ2016 Bernard `Guyzmo` Pratz
111+
{self} version {version}, Copyright ©2016 Bernard `Guyzmo` Pratz
105112
{self} comes with ABSOLUTELY NO WARRANTY; for more informations
106113
read the LICENSE file available in the sources, or check
107114
out: http://www.gnu.org/licenses/gpl-2.0.txt
@@ -112,6 +119,7 @@
112119
import os
113120
import sys
114121
import json
122+
import shutil
115123
import logging
116124
import pkg_resources
117125

@@ -168,13 +176,13 @@ def _guess_repo_slug(self, repository, service):
168176
if remote.name in (target, 'upstream', 'origin'):
169177
for url in remote.urls:
170178
if url.startswith('https'):
171-
if '.git' in url:
179+
if url.endswith('.git'):
172180
url = url[:-4]
173181
*_, user, name = url.split('/')
174182
self.set_repo_slug('/'.join([user, name]))
175183
break
176184
elif url.startswith('git@'):
177-
if '.git' in url:
185+
if url.endswith('.git'):
178186
url = url[:-4]
179187
_, repo_slug = url.split(':')
180188
self.set_repo_slug(repo_slug)
@@ -269,6 +277,13 @@ def store_gitconfig(self, val):
269277

270278
'''Actions'''
271279

280+
@register_action('ls')
281+
@register_action('list')
282+
def do_list(self):
283+
service = self.get_service(False)
284+
service.list(self.user, self.long)
285+
return 0
286+
272287
@register_action('add')
273288
def do_remote_add(self):
274289
service = self.get_service()
@@ -278,7 +293,7 @@ def do_remote_add(self):
278293
alone=self.alone)
279294
log.info('Successfully added `{}` as remote named `{}`'.format(
280295
self.repo_slug,
281-
service.name)
296+
self.remote_name or service.name)
282297
)
283298
return 0
284299

@@ -323,9 +338,10 @@ def do_fork(self):
323338
def do_clone(self, service=None, repo_path=None):
324339
service = service or self.get_service(lookup_repository=False)
325340
repo_path = repo_path or os.path.join(self.path, self.target_repo or self.repo_name)
326-
if os.path.exists(repo_path):
341+
if os.path.exists(repo_path) and os.listdir(repo_path) != []:
327342
raise FileExistsError('Cannot clone repository, '
328-
'a folder named {} already exists!'.format(repo_path))
343+
'a folder named {} already exists and '
344+
'is not an empty directory!'.format(repo_path))
329345
try:
330346
repository = Repo.init(repo_path)
331347
service = RepositoryService.get_service(repository, self.target)
@@ -337,8 +353,8 @@ def do_clone(self, service=None, repo_path=None):
337353
return 0
338354
except Exception as err:
339355
if os.path.exists(repo_path):
340-
os.removedirs(repo_path)
341-
raise err from err
356+
shutil.rmtree(repo_path)
357+
raise ResourceNotFoundError(err.args[2].decode('utf-8')) from err
342358

343359
@register_action('create')
344360
def do_create(self):
@@ -418,6 +434,8 @@ def do_request_fetch(self):
418434

419435
@register_action('gist', 'ls')
420436
@register_action('gist', 'list')
437+
@register_action('snippet', 'ls')
438+
@register_action('snippet', 'list')
421439
def do_gist_list(self):
422440
service = self.get_service(lookup_repository=False)
423441
if self.gist_ref:
@@ -431,6 +449,7 @@ def do_gist_list(self):
431449
return 0
432450

433451
@register_action('gist', 'clone')
452+
@register_action('snippet', 'clone')
434453
def do_gist_clone(self):
435454
service = self.get_service(lookup_repository=False)
436455
repo_path = os.path.join(self.path, self.gist_ref.split('/')[-1])
@@ -440,24 +459,27 @@ def do_gist_clone(self):
440459
return 0
441460

442461
@register_action('gist', 'fetch')
462+
@register_action('snippet', 'fetch')
443463
def do_gist_fetch(self):
444464
service = self.get_service(lookup_repository=False)
445465
# send gist to stdout, not using log.info on purpose here!
446466
print(service.gist_fetch(self.gist_ref, self.gist_file))
447467
return 0
448468

449469
@register_action('gist', 'create')
470+
@register_action('snippet', 'create')
450471
def do_gist_create(self):
451472
service = self.get_service(lookup_repository=False)
452473
url = service.gist_create(self.gist_path, self.description, self.secret)
453474
log.info('Successfully created gist `{}`!'.format(url))
454475
return 0
455476

456477
@register_action('gist', 'delete')
478+
@register_action('snippet', 'delete')
457479
def do_gist_delete(self):
458480
service = self.get_service(lookup_repository=False)
459481
if not self.force: # pragma: no cover
460-
if not confirm('gist', self.gist_ref):
482+
if not confirm('snippet', self.gist_ref):
461483
return 0
462484

463485
service.gist_delete(self.gist_ref)

git_repo/services/ext/github.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def connect(self):
2323
self.gh.login(token=self._privatekey)
2424
self.username = self.gh.user().login
2525
except github3.models.GitHubError as err:
26-
if err.code is 401:
26+
if 401 == err.code:
2727
if not self._privatekey:
2828
raise ConnectionError('Could not connect to Github. '
2929
'Please configure .gitconfig '
@@ -74,6 +74,69 @@ def delete(self, repo, user=None):
7474
'Check the namespace or the private token\'s privileges') from err
7575
raise ResourceError('Unhandled exception: {}'.format(err)) from err
7676

77+
def list(self, user, _long=False):
78+
import shutil, sys
79+
from datetime import datetime
80+
term_width = shutil.get_terminal_size((80, 20)).columns
81+
def col_print(lines, indent=0, pad=2):
82+
# prints a list of items in a fashion similar to the dir command
83+
# borrowed from https://gist.github.com/critiqjo/2ca84db26daaeb1715e1
84+
n_lines = len(lines)
85+
if n_lines == 0:
86+
return
87+
col_width = max(len(line) for line in lines)
88+
n_cols = int((term_width + pad - indent)/(col_width + pad))
89+
n_cols = min(n_lines, max(1, n_cols))
90+
col_len = int(n_lines/n_cols) + (0 if n_lines % n_cols == 0 else 1)
91+
if (n_cols - 1) * col_len >= n_lines:
92+
n_cols -= 1
93+
cols = [lines[i*col_len : i*col_len + col_len] for i in range(n_cols)]
94+
rows = list(zip(*cols))
95+
rows_missed = zip(*[col[len(rows):] for col in cols[:-1]])
96+
rows.extend(rows_missed)
97+
for row in rows:
98+
print(" "*indent + (" "*pad).join(line.ljust(col_width) for line in row))
99+
100+
if not self.gh.user(user):
101+
raise ResourceNotFoundError("User {} does not exists.".format(user))
102+
103+
repositories = self.gh.iter_user_repos(user)
104+
if not _long:
105+
repositories = list(repositories)
106+
col_print(["/".join([user, repo.name]) for repo in repositories])
107+
else:
108+
print('Status\tCommits\tReqs\tIssues\tForks\tCoders\tWatch\tLikes\tLang\tModif\t\tName', file=sys.stderr)
109+
for repo in repositories:
110+
if repo.updated_at.year < datetime.now().year:
111+
date_fmt = "%b %d %Y"
112+
else:
113+
date_fmt = "%b %d %H:%M"
114+
115+
status = ''.join([
116+
'F' if repo.fork else ' ', # is a fork?
117+
'P' if repo.private else ' ', # is private?
118+
])
119+
print('\t'.join([
120+
# status
121+
status,
122+
# stats
123+
str(len(list(repo.iter_commits()))), # number of commits
124+
str(len(list(repo.iter_pulls()))), # number of pulls
125+
str(len(list(repo.iter_issues()))), # number of issues
126+
str(repo.forks), # number of forks
127+
str(len(list(repo.iter_contributors()))), # number of contributors
128+
str(repo.watchers), # number of subscribers
129+
str(repo.stargazers or 0), # number of ♥
130+
# info
131+
repo.language or '?', # language
132+
repo.updated_at.strftime(date_fmt), # date
133+
'/'.join([user, repo.name]), # name
134+
]))
135+
136+
137+
138+
139+
77140
def get_repository(self, user, repo):
78141
repository = self.gh.repository(user, repo)
79142
if not repository:
@@ -101,7 +164,9 @@ def gist_fetch(self, gist, fname=None):
101164
try:
102165
gist = self.gh.gist(self._format_gist(gist))
103166
except Exception as err:
104-
raise ResourceNotFoundError('Could not find gist') from err
167+
raise ResourceNotFoundError('Error while fetching gist') from err
168+
if not gist:
169+
raise ResourceNotFoundError('Could not find gist')
105170
if gist.files == 1 and not fname:
106171
gist_file = list(gist.iter_files())[0]
107172
else:

requirements.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
docopt
2-
GitPython>=2.0.6
3-
progress==1.2
4-
python-gitlab==0.13
2+
progress
3+
GitPython>=2.1.0
54
uritemplate.py==2.0.0
65
github3.py==0.9.5
7-
bitbucket-api==0.5.0
6+
python-gitlab>=0.13
7+
bitbucket-api

0 commit comments

Comments
 (0)