Skip to content

Commit 9542185

Browse files
committed
Merge branches requests/github/82 and fixes into devel
Signed-off-by: Guyzmo <[email protected]>
2 parents 43999e6 + 336564e commit 9542185

28 files changed

+1711
-33
lines changed

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ very simple. To clone a new project, out of GitHub, just issue:
1818

1919
% git hub clone guyzmo/git-repo
2020

21-
But that works also with a project from GitLab, Bitbucket, or your own GitLab:
21+
But that works also with a project from GitLab, Bitbucket, your own GitLab or Gogs:
2222

2323
% git lab clone guyzmo/git-repo
2424
% git bb clone guyzmo/git-repo
2525
% git myprecious clone guyzmo/git-repo
26+
% git gg clone guyzmo/git-repo
2627

2728
If you want to can choose the default branch to clone:
2829

@@ -151,6 +152,10 @@ section in the gitconfig:
151152
[gitrepo "bitbucket"]
152153
token = username:password
153154

155+
[gitrepo "gogs"]
156+
fqdn = UrlOfYourGogs
157+
token = YourVerySecretKey
158+
154159
Here, we're setting the basics: just the private token. You'll notice that for bitbucket
155160
the private token is your username and password seperated by a column. That's because
156161
bitbucket does not offer throw away private tokens for tools (I might implement BB's OAuth
@@ -253,9 +258,11 @@ To use your own credentials, you can setup the following environment variables:
253258
* `GITHUB_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on GitHub
254259
* `GITLAB_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on GitLab
255260
* `BITBUCKET_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on Bitbucket
261+
* `GOGS_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on Gogs
256262
* `PRIVATE_KEY_GITHUB` your private token you've setup on GitHub for your account
257263
* `PRIVATE_KEY_GITLAB` your private token you've setup on GitLab for your account
258264
* `PRIVATE_KEY_BITBUCKET` your private token you've setup on Bitbucket for your account
265+
* `PRIVATE_KEY_GOGS` your private token you've setup on Gogs for your account
259266

260267
### TODO
261268

@@ -267,6 +274,7 @@ To use your own credentials, you can setup the following environment variables:
267274
* [x] add regression tests (and actually find a smart way to implement them…)
268275
* [x] add travis build
269276
* [x] show a nice progress bar, while it's fetching (cf [#15](https://github.com/guyzmo/git-repo/issues/15))
277+
* [x] add support for gogs (cf [#18](https://github.com/guyzmo/git-repo/issues/18))
270278
* [ ] add support for handling gists
271279
* [x] github support
272280
* [x] gitlab support (cf [#12](https://github.com/guyzmo/git-repo/issues/12))
@@ -278,7 +286,6 @@ To use your own credentials, you can setup the following environment variables:
278286
* [ ] add application token support for bitbucket (cf [#14](https://github.com/guyzmo/git-repo/issues/14))
279287
* [ ] add support for managing SSH keys (cf [#22](https://github.com/guyzmo/git-repo/issues/22))
280288
* [ ] add support for issues?
281-
* [ ] add support for gogs (cf [#18](https://github.com/guyzmo/git-repo/issues/18))
282289
* [ ] add support for gerrit (cf [#19](https://github.com/guyzmo/git-repo/issues/19))
283290
* [ ] do what's needed to make a nice documentation — if possible in markdown !@#$
284291
* for more features, write an issue or, even better, a PR!
@@ -291,6 +298,7 @@ The project and original idea has been brought and is maintained by:
291298

292299
With code contributions coming from:
293300

301+
* [@PyHedgehog](https://github.com/pyhedgehog)[commits](https://github.com/guyzmo/git-repo/commits?author=pyhedgehog)
294302
* [@guyhughes](https://github.com/guyhughes)[commits](https://github.com/guyzmo/git-repo/commits?author=guyhughes)
295303
* [@buaazp](https://github.com/buaazp)[commits](https://github.com/guyzmo/git-repo/commits?author=buaazp)
296304
* [@peterazmanov](https://github.com/peterazmanov)[commits](https://github.com/guyzmo/git-repo/commits?author=peterazmanov)
@@ -299,7 +307,7 @@ With code contributions coming from:
299307

300308
### License
301309

302-
Copyright ©2016 Bernard `Guyzmo` Pratz <[email protected]>
310+
Copyright ©2016,2017 Bernard `Guyzmo` Pratz <[email protected]>
303311

304312
This program is free software; you can redistribute it and/or
305313
modify it under the terms of the GNU General Public License

git_repo/repo.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def set_gist_ref(self, gist):
276276

277277
@store_parameter('--config')
278278
def store_gitconfig(self, val):
279-
self.config = val or os.path.join(os.environ['HOME'], '.gitconfig')
279+
self.config = val or RepositoryService.get_config_path()
280280

281281
'''Actions'''
282282

@@ -503,21 +503,54 @@ def loop_input(*args, method=input, **kwarg):
503503
return out
504504

505505
def setup_service(service):
506+
new_conf = dict(
507+
fqdn=None,
508+
remote=None,
509+
)
506510
conf = service.get_config(self.config)
507511
if 'token' in conf:
508512
raise Exception('A token has been generated for this service. Please revoke and delete before proceeding.')
509513

514+
print('Is your service self-hosted?')
515+
if 'y' in input(' [yN]> ').lower():
516+
new_conf['type'] = service.name
517+
print('What name do you want to give this service?')
518+
new_conf['name'] = input('[{}]> '.format(service.name))
519+
new_conf['command'] = new_conf['name']
520+
service.name, service.command = new_conf['name'], new_conf['command']
521+
print('Enter the service\'s domain name:')
522+
new_conf['fqdn'] = input('[{}]> '.format(service.fqdn))
523+
print('Enter the service\'s port:')
524+
new_conf['port'] = input('[443]> ') or 443
525+
print('Are you connecting using HTTPS? (you should):')
526+
if 'n' in input(' [Yn]> ').lower():
527+
new_conf['scheme'] = 'http'
528+
else:
529+
new_conf['scheme'] = 'https'
530+
print('Do you need to use an insecure connection? (you shouldn\'t):')
531+
new_conf['insecure'] = 'y' in input(' [yN]> ').lower()
532+
service.session_insecure = new_conf['insecure']
533+
if not new_conf['insecure']:
534+
print('Do you want to setup the path to custom certificate?:')
535+
if 'y' in input(' [yN]> ').lower():
536+
new_conf['server-cert'] = loop_input('/path/to/certbundle.pem []> ')
537+
service.session_certificate = new_conf['server-cert']
538+
539+
service.fqdn = new_conf['fqdn']
540+
service.port = new_conf['port']
541+
service.scheme = new_conf['scheme']
542+
510543
print('Please enter your credentials to connect to the service:')
511544
username = loop_input('username> ')
512545
password = loop_input('password> ', method=getpass)
513546

514-
token = service.get_auth_token(username, password, prompt=loop_input)
547+
new_conf['token'] = service.get_auth_token(username, password, prompt=loop_input)
515548
print('Great! You\'ve been identified 🍻')
516549

517550
print('Do you want to give a custom name for this service\'s remote?')
518551
if 'y' in input(' [yN]> ').lower():
519552
print('Enter the remote\'s name:')
520-
loop_input('[{}]> '.format(service.name))
553+
new_conf['remote'] = loop_input('[{}]> '.format(service.name))
521554

522555
print('Do you want to configure a git alias?')
523556
print('N.B.: instead of typing `git repo {0}` you\'ll be able to type `git {0}`'.format(service.command))
@@ -526,7 +559,7 @@ def setup_service(service):
526559
else:
527560
set_alias = True
528561

529-
service.store_config(self.config, token=token)
562+
service.store_config(self.config, **new_conf)
530563
if set_alias:
531564
service.set_alias(self.config)
532565

git_repo/services/ext/gitlab.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ def __init__(self, *args, **kwarg):
2323
super().__init__(*args, **kwarg)
2424

2525
def connect(self):
26-
self.gl.ssl_verify = not self.insecure
26+
self.gl.ssl_verify = self.session_certificate or not self.session_insecure
27+
if self.session_proxy:
28+
self.gl.session.proxies.update(self.session_proxy)
29+
2730
self.gl.set_url(self.url_ro)
2831
self.gl.set_token(self._privatekey)
2932
self.gl.token_auth()

git_repo/services/ext/gogs.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
#!/usr/bin/env python
2+
import sys
3+
import logging
4+
log = logging.getLogger('git_repo.gogs')
5+
6+
from ..service import register_target, RepositoryService, os
7+
from ...exceptions import ResourceError, ResourceExistsError, ResourceNotFoundError
8+
9+
from gogs_client import GogsApi, GogsRepo, Token, UsernamePassword, ApiFailure
10+
from requests import Session, HTTPError
11+
from urllib.parse import urlparse, urlunparse
12+
import functools
13+
14+
from git import config as git_config
15+
from git.exc import GitCommandError
16+
17+
class GogsClient(GogsApi):
18+
def __init__(self):
19+
self.session = Session()
20+
21+
def setup(self, *args, **kwarg):
22+
super().__init__(*args, session=self.session, **kwarg)
23+
24+
def set_token(self, token):
25+
self.auth = Token(token)
26+
27+
def set_default_private(self, p):
28+
self.default_private = p
29+
30+
def setup_session(self, ssl_config, proxy=dict()):
31+
self.session.verify = ssl_config
32+
self.session.proxies.update(proxy)
33+
34+
@property
35+
def username(self):
36+
if not hasattr(self, '_username'):
37+
self._username = self.authenticated_user(self.auth).username
38+
return self._username
39+
40+
def orgs(self):
41+
orgs = self._check_ok(self._get('/user/orgs', auth=self.auth)).json()
42+
#return [gogs_client.GogsUser.from_json(org) for org in orgs]
43+
return [org['username'] for org in orgs]
44+
45+
def create_repository(self, user, repo):
46+
if user == self.username:
47+
repository = self.create_repo(self.auth, name=repo, private=self.default_private)
48+
elif user in self.orgs():
49+
data = dict(name=repo, private=self.default_private)
50+
response = self._post('/org/{}/repos'.format(user), auth=self.auth, data=data)
51+
repository = GogsRepo.from_json(self._check_ok(response).json())
52+
else:
53+
data = dict(name=repo, private=self.default_private)
54+
response = self._post('/admin/users/{}/repos'.format(user), auth=self.auth, data=data)
55+
repository = GogsRepo.from_json(self._check_ok(response).json())
56+
57+
def delete_repository(self, user, repo):
58+
return self.delete_repo(self.auth, user, repo)
59+
60+
def repository(self, user, repo):
61+
return self.get_repo(self.auth, user, repo)
62+
63+
def repositories(self, user):
64+
r = self._get('/user/repos', auth=self.auth)
65+
repositories = self._check_ok(r).json()
66+
repositories = [repo for repo in repositories if repo['owner']['username'] == user]
67+
return repositories
68+
69+
@register_target('gg', 'gogs')
70+
class GogsService(RepositoryService):
71+
fqdn = 'try.gogs.io'
72+
73+
def __init__(self, *args, **kwargs):
74+
self.gg = GogsClient()
75+
76+
super().__init__(*args, **kwargs)
77+
78+
self.gg.setup(self.url_ro)
79+
self.gg.set_token(self._privatekey)
80+
self.gg.set_default_private(self.default_create_private)
81+
self.gg.setup_session(
82+
self.session_certificate or not self.session_insecure,
83+
self.session_proxy)
84+
85+
def connect(self):
86+
try:
87+
self.username = self.user # Call to self.gg.authenticated_user()
88+
except HTTPError as err:
89+
if err.response is not None and err.response.status_code == 401:
90+
if not self._privatekey:
91+
raise ConnectionError('Could not connect to GoGS. '
92+
'Please configure .gitconfig '
93+
'with your gogs private key.') from err
94+
else:
95+
raise ConnectionError('Could not connect to GoGS. '
96+
'Check your configuration and try again.') from err
97+
else:
98+
raise err
99+
100+
@classmethod
101+
def get_auth_token(cls, login, password, prompt=None):
102+
import platform
103+
name = 'git-repo token used on {}'.format(platform.node())
104+
gg = GogsApi(cls.build_url())
105+
auth = UsernamePassword(login, password)
106+
tokens = gg.get_tokens(auth, login)
107+
tokens = dict((token.name, token.token) for token in tokens)
108+
if name in tokens:
109+
return tokens[name]
110+
if 'git-repo token' in tokens:
111+
return tokens['git-repo token']
112+
token = gg.create_token(auth, name, login)
113+
return token.token
114+
115+
@property
116+
def user(self):
117+
return self.gg.username
118+
119+
def create(self, user, repo, add=False):
120+
try:
121+
self.gg.create_repository(user, repo)
122+
except ApiFailure as err:
123+
if err.status_code == 422:
124+
raise ResourceExistsError("Project already exists.") from err
125+
else:
126+
raise ResourceError("Unhandled error.") from err
127+
except Exception as err:
128+
raise ResourceError("Unhandled exception: {}".format(err)) from err
129+
if add:
130+
self.add(user=self.username, repo=repo, tracking=self.name)
131+
132+
def fork(self, user, repo):
133+
raise NotImplementedError
134+
135+
def delete(self, repo, user=None):
136+
if not user:
137+
user = self.username
138+
try:
139+
self.gg.delete_repository(user, repo)
140+
except ApiFailure as err:
141+
if err.status_code == 404:
142+
raise ResourceNotFoundError("Cannot delete: repository {}/{} does not exists.".format(user, repo)) from err
143+
elif err.status_code == 403:
144+
raise ResourcePermissionError("You don't have enough permissions for deleting the repository. Check the namespace or the private token's privileges") from err
145+
elif err.status_code == 422:
146+
raise ResourceNotFoundError("Cannot delete repository {}/{}: user {} does not exists.".format(user, repo, user)) from err
147+
raise ResourceError("Unhandled error: {}".format(err)) from err
148+
except Exception as err:
149+
raise ResourceError("Unhandled exception: {}".format(err)) from err
150+
151+
def list(self, user, _long=False):
152+
import shutil, sys
153+
from datetime import datetime
154+
term_width = shutil.get_terminal_size((80, 20)).columns
155+
def col_print(lines, indent=0, pad=2):
156+
# prints a list of items in a fashion similar to the dir command
157+
# borrowed from https://gist.github.com/critiqjo/2ca84db26daaeb1715e1
158+
n_lines = len(lines)
159+
if n_lines == 0:
160+
return
161+
col_width = max(len(line) for line in lines)
162+
n_cols = int((term_width + pad - indent)/(col_width + pad))
163+
n_cols = min(n_lines, max(1, n_cols))
164+
col_len = int(n_lines/n_cols) + (0 if n_lines % n_cols == 0 else 1)
165+
if (n_cols - 1) * col_len >= n_lines:
166+
n_cols -= 1
167+
cols = [lines[i*col_len : i*col_len + col_len] for i in range(n_cols)]
168+
rows = list(zip(*cols))
169+
rows_missed = zip(*[col[len(rows):] for col in cols[:-1]])
170+
rows.extend(rows_missed)
171+
for row in rows:
172+
print(" "*indent + (" "*pad).join(line.ljust(col_width) for line in row))
173+
174+
repositories = self.gg.repositories(user)
175+
if user != self.username and not repositories and user not in self.orgs:
176+
raise ResourceNotFoundError("Unable to list namespace {} - only authenticated user and orgs available for listing.".format(user))
177+
if not _long:
178+
col_print([repo['full_name'] for repo in repositories])
179+
else:
180+
print('Status\tCommits\tReqs\tIssues\tForks\tCoders\tWatch\tLikes\tLang\tModif\t\t\t\tName', file=sys.stderr)
181+
for repo in repositories:
182+
status = ''.join([
183+
'F' if repo['fork'] else ' ', # is a fork?
184+
'P' if repo['private'] else ' ', # is private?
185+
])
186+
try:
187+
issues = self.gg._check_ok(self.gg._get('/repos/{}/issues'.format(repo['full_name']), auth=self.auth)).json()
188+
except Exception:
189+
issues = []
190+
print('\t'.join([
191+
# status
192+
status,
193+
# stats
194+
str(len(list(()))), # number of commits
195+
str(len(list(()))), # number of pulls
196+
str(len(list(issues))), # number of issues
197+
str(repo.get('forks_count') or 0), # number of forks
198+
str(len(list(()))), # number of contributors
199+
str(repo.get('watchers_count') or 0), # number of subscribers
200+
str(repo.get('stars_count') or 0), # number of ♥
201+
# info
202+
repo.get('language') or '?', # language
203+
repo['updated_at'], # date
204+
repo['full_name'], # name
205+
]))
206+
207+
def get_repository(self, user, repo):
208+
try:
209+
return self.gg.repository(user, repo)
210+
except ApiFailure as err:
211+
if err.status_code == 404:
212+
raise ResourceNotFoundError("Cannot get: repository {}/{} does not exists.".format(user, repo)) from err
213+
raise ResourceError("Unhandled error: {}".format(err)) from err
214+
except Exception as err:
215+
raise ResourceError("Unhandled exception: {}".format(err)) from err
216+
217+
def gist_list(self, gist=None):
218+
raise NotImplementedError
219+
220+
def gist_fetch(self, gist, fname=None):
221+
raise NotImplementedError
222+
223+
def gist_clone(self, gist):
224+
raise NotImplementedError
225+
226+
def gist_create(self, gist_pathes, description, secret=False):
227+
raise NotImplementedError
228+
229+
def gist_delete(self, gist_id):
230+
raise NotImplementedError
231+
232+
def request_create(self, user, repo, local_branch, remote_branch, title, description=None):
233+
raise NotImplementedError
234+
235+
def request_list(self, user, repo):
236+
raise NotImplementedError
237+
238+
def request_fetch(self, user, repo, request, pull=False):
239+
raise NotImplementedError

0 commit comments

Comments
 (0)