Skip to content

Commit ea4fc21

Browse files
committed
👍 Merge branch devel ; 🔖 Version bump to 1.9.0
🚧 Features * Gogs (fixes #18) (kudos @pyhedgehog) * certificate pinning (fixes #88) * non-standard http ports (fixes #81) * possibility to use custom SSH address (fixes #107) * .gitconfig in XDG home directory (fixes #95) * .gitconfig's proxy definition support (fixes #105) * automatically grab PR title/body from last commit (fixes #73) 🚒 Bugfixes * refactoring of listings, improved list command (fixes #114) 💄 Cosmetics * Switch to a git tag scheme for versioning (using setuptools-scm) * Updated README with new informations * Added @pyhedgehog to contributors * Updated TODO list Signed-off-by: Guyzmo <[email protected]>
2 parents ee6384d + a6f09ff commit ea4fc21

File tree

58 files changed

+2135
-354
lines changed

Some content is hidden

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

58 files changed

+2135
-354
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ matrix:
88
- os: linux
99
python: "3.5"
1010
- os: linux
11-
python: "3.5-dev"
11+
python: "3.6"
1212
- os: linux
1313
python: "3.6-dev"
1414
- os: linux

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

VERSION

Lines changed: 0 additions & 1 deletion
This file was deleted.

git_repo/repo.py

Lines changed: 115 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515
{self} [--path=<path>] [-v...] <target> clone <user>/<repo> [<repo> [<branch>]]
1616
{self} [--path=<path>] [-v...] <target> add <user>/<repo> [<name>] [--tracking=<branch>] [-a]
1717
{self} [--path=<path>] [-v...] <target> request (list|ls)
18-
{self} [--path=<path>] [-v...] <target> request fetch <request>
19-
{self} [--path=<path>] [-v...] <target> request create <title> [--message=<message>]
20-
{self} [--path=<path>] [-v...] <target> request create <local_branch> <title> [--message=<message>]
21-
{self} [--path=<path>] [-v...] <target> request create <remote_branch> <local_branch> <title> [--message=<message>]
18+
{self} [--path=<path>] [-v...] <target> request fetch <request> [-f]
19+
{self} [--path=<path>] [-v...] <target> request create [--title=<title>] [--message=<message>]
20+
{self} [--path=<path>] [-v...] <target> request create <local_branch> [--title=<title>] [--message=<message>]
21+
{self} [--path=<path>] [-v...] <target> request create <remote_branch> <local_branch> [--title=<title>] [--message=<message>]
2222
{self} [--path=<path>] [-v...] <target> request <user>/<repo> (list|ls)
23-
{self} [--path=<path>] [-v...] <target> request <user>/<repo> fetch <request>
24-
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <title> [--branch=<remote>] [--message=<message>]
25-
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <local_branch> <title> [--branch=<remote>] [--message=<message>]
26-
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <remote_branch> <local_branch> <title> [--branch=<remote>] [--message=<message>]
23+
{self} [--path=<path>] [-v...] <target> request <user>/<repo> fetch <request> [-f]
24+
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create [--title=<title>] [--branch=<remote>] [--message=<message>]
25+
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <local_branch> [--title=<title>] [--branch=<remote>] [--message=<message>]
26+
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <remote_branch> <local_branch> [--title=<title>] [--branch=<remote>] [--message=<message>]
2727
{self} [--path=<path>] [-v...] <target> (gist|snippet) (list|ls) [<gist>]
2828
{self} [--path=<path>] [-v...] <target> (gist|snippet) clone <gist>
2929
{self} [--path=<path>] [-v...] <target> (gist|snippet) fetch <gist> [<gist_file>]
@@ -89,7 +89,7 @@
8989
--secret Do not publicize gist when pushing
9090
9191
Options for request:
92-
<title> Title to give to the request for merge
92+
-t,--title=<title> Title to give to the request for merge
9393
-m,--message=<message> Description for the request for merge
9494
9595
Configuration options:
@@ -137,58 +137,46 @@
137137
from .exceptions import ArgumentError, ResourceNotFoundError
138138
from .services.service import RepositoryService
139139

140+
from .tools import print_tty, print_iter, loop_input, confirm
140141
from .kwargparse import KeywordArgumentParser, store_parameter, register_action
141142

142143
from git import Repo, Git
143-
from git.exc import InvalidGitRepositoryError, NoSuchPathError
144+
from git.exc import InvalidGitRepositoryError, NoSuchPathError, BadName
144145

145146
import re
146147

147148
EXTRACT_URL_RE = re.compile('[^:]*(://|@)[^/]*/')
148149

149-
def confirm(what, where):
150-
'''
151-
Method to show a CLI based confirmation message, waiting for a yes/no answer.
152-
"what" and "where" are used to better define the message.
153-
'''
154-
ans = input('Are you sure you want to delete the '
155-
'{} {} from the service?\n[yN]> '.format(what, where))
156-
if 'y' in ans:
157-
ans = input('Are you really sure? there\'s no coming back!\n'
158-
'[type \'burn!\' to proceed]> ')
159-
if 'burn!' != ans:
160-
return False
161-
else:
162-
return False
163-
return True
164-
165150

166151
class GitRepoRunner(KeywordArgumentParser):
167152

168153
def init(self): # pragma: no cover
169154
if 'GIT_WORK_TREE' in os.environ.keys() or 'GIT_DIR' in os.environ.keys():
170155
del os.environ['GIT_WORK_TREE']
171156

172-
def _guess_repo_slug(self, repository, service):
157+
def _guess_repo_slug(self, repository, service, resolve_targets=None):
173158
config = repository.config_reader()
174-
target = service.name
159+
if resolve_targets:
160+
targets = [target.format(service=service.name) for target in resolve_targets]
161+
else:
162+
targets = (service.name, 'upstream', 'origin')
175163
for remote in repository.remotes:
176-
if remote.name in (target, 'upstream', 'origin'):
164+
if remote.name in targets:
177165
for url in remote.urls:
178166
if url.startswith('https'):
179167
if url.endswith('.git'):
180168
url = url[:-4]
181169
*_, user, name = url.split('/')
182170
self.set_repo_slug('/'.join([user, name]))
183-
break
171+
return
184172
elif url.startswith('git@'):
185173
if url.endswith('.git'):
186174
url = url[:-4]
187175
_, repo_slug = url.split(':')
188176
self.set_repo_slug(repo_slug)
189-
break
177+
return
190178

191-
def get_service(self, lookup_repository=True):
179+
def get_service(self, lookup_repository=True, resolve_targets=None):
192180
if not lookup_repository:
193181
service = RepositoryService.get_service(None, self.target)
194182
service.connect()
@@ -203,7 +191,7 @@ def get_service(self, lookup_repository=True):
203191
raise FileNotFoundError('Cannot find path to the repository.')
204192
service = RepositoryService.get_service(repository, self.target)
205193
if not self.repo_name:
206-
self._guess_repo_slug(repository, service)
194+
self._guess_repo_slug(repository, service, resolve_targets)
207195
return service
208196

209197
'''Argument storage'''
@@ -273,15 +261,14 @@ def set_gist_ref(self, gist):
273261

274262
@store_parameter('--config')
275263
def store_gitconfig(self, val):
276-
self.config = val or os.path.join(os.environ['HOME'], '.gitconfig')
264+
self.config = val or RepositoryService.get_config_path()
277265

278266
'''Actions'''
279267

280268
@register_action('ls')
281269
@register_action('list')
282270
def do_list(self):
283-
service = self.get_service(False)
284-
service.list(self.user, self.long)
271+
print_iter(self.get_service(False).list(self.user, self.long))
285272
return 0
286273

287274
@register_action('add')
@@ -399,32 +386,77 @@ def do_open(self):
399386
@register_action('request', 'ls')
400387
@register_action('request', 'list')
401388
def do_request_list(self):
402-
service = self.get_service()
403-
log.info('List of open requests to merge:')
404-
log.info(" {}\t{}\t{}".format('id', 'title'.ljust(60), 'URL'))
405-
for pr in service.request_list(self.user_name, self.repo_name):
406-
print("{}\t{}\t{}".format(pr[0].rjust(3), pr[1][:60].ljust(60), pr[2]))
389+
service = self.get_service(lookup_repository=self.repo_slug == None)
390+
print_tty('List of open requests to merge:')
391+
print_iter(service.request_list(self.user_name, self.repo_name))
407392
return 0
408393

409394
@register_action('request', 'create')
410395
def do_request_create(self):
411-
service = self.get_service()
396+
def request_edition(repository, from_branch):
397+
try:
398+
commit = repository.commit(from_branch)
399+
title, *body = commit.message.split('\n')
400+
except BadName:
401+
log.error('Couldn\'t find local source branch {}'.format(from_branch))
402+
return None
403+
from tempfile import NamedTemporaryFile
404+
from subprocess import call
405+
with NamedTemporaryFile(
406+
prefix='git-repo-issue-',
407+
suffix='.md',
408+
mode='w+b') as request_file:
409+
request_file.write((
410+
'# Request for Merge Title ##########################\n'
411+
'{}\n'
412+
'\n'
413+
'# Request for Merge Body ###########################\n'
414+
'{}\n'
415+
'####################################################\n'
416+
'## Filled with commit:\n'
417+
'## {}\n'
418+
'####################################################\n'
419+
'## * All lines starting with # will be ignored.\n'
420+
'## * First non-ignored line is the title of the request.\n'
421+
).format(title, '\n'.join(body), commit.name_rev).encode('utf-8'))
422+
request_file.flush()
423+
rv = call("{} {}".format(os.environ['EDITOR'], request_file.name), shell=True)
424+
if rv != 0:
425+
raise ArgumentError("Aborting request, as editor exited abnormally.")
426+
request_file.seek(0)
427+
request_message = map(lambda l: l.decode('utf-8'),
428+
filter(lambda l: not l.strip().startswith(b'#'), request_file.readlines()))
429+
try:
430+
title = next(request_message)
431+
body = ''.join(request_message)
432+
except Exception:
433+
raise ResourceError("Format of the request message cannot be parsed.")
434+
435+
return title, body
436+
437+
438+
service = self.get_service(resolve_targets=('upstream', '{service}', 'origin'))
439+
412440
new_request = service.request_create(self.user_name,
413441
self.repo_name,
414442
self.local_branch,
415443
self.remote_branch,
416444
self.title,
417-
self.message)
445+
self.message,
446+
self.repo_slug != None,
447+
request_edition)
418448
log.info('Successfully created request of `{local}` onto `{}:{remote}`, with id `{ref}`!'.format(
419449
'/'.join([self.user_name, self.repo_name]),
420450
**new_request)
421451
)
452+
if 'url' in new_request:
453+
log.info('available at: {url}'.format(**new_request))
422454
return 0
423455

424456
@register_action('request', 'fetch')
425457
def do_request_fetch(self):
426458
service = self.get_service()
427-
new_branch = service.request_fetch(self.user_name, self.repo_name, self.request)
459+
new_branch = service.request_fetch(self.user_name, self.repo_name, self.request, force=self.force)
428460
log.info('Successfully fetched request id `{}` of `{}` into `{}`!'.format(
429461
self.request,
430462
self.repo_slug,
@@ -438,16 +470,7 @@ def do_request_fetch(self):
438470
@register_action('snippet', 'list')
439471
def do_gist_list(self):
440472
service = self.get_service(lookup_repository=False)
441-
if 'github' == service.name and self.gist_ref:
442-
log.info("{:15}\t{:>7}\t{}".format('language', 'size', 'name'))
443-
else:
444-
log.info("{:56}\t{}".format('id', 'title'.ljust(60)))
445-
if self.gist_ref:
446-
for gist_file in service.gist_list(self.gist_ref):
447-
print("{:15}\t{:7}\t{}".format(*gist_file))
448-
else:
449-
for gist in service.gist_list():
450-
print( "{:56}\t{}".format(gist[0], gist[1]))
473+
print_iter(service.gist_list(self.gist_ref or None))
451474
return 0
452475

453476
@register_action('gist', 'clone')
@@ -492,28 +515,55 @@ def do_gist_delete(self):
492515
def do_config(self):
493516
from getpass import getpass
494517

495-
def loop_input(*args, method=input, **kwarg):
496-
out = ''
497-
while len(out) == 0:
498-
out = method(*args, **kwarg)
499-
return out
500-
501518
def setup_service(service):
519+
new_conf = dict(
520+
fqdn=None,
521+
remote=None,
522+
)
502523
conf = service.get_config(self.config)
503524
if 'token' in conf:
504525
raise Exception('A token has been generated for this service. Please revoke and delete before proceeding.')
505526

527+
print('Is your service self-hosted?')
528+
if 'y' in input(' [yN]> ').lower():
529+
new_conf['type'] = service.name
530+
print('What name do you want to give this service?')
531+
new_conf['name'] = input('[{}]> '.format(service.name))
532+
new_conf['command'] = new_conf['name']
533+
service.name, service.command = new_conf['name'], new_conf['command']
534+
print('Enter the service\'s domain name:')
535+
new_conf['fqdn'] = input('[{}]> '.format(service.fqdn))
536+
print('Enter the service\'s port:')
537+
new_conf['port'] = input('[443]> ') or '443'
538+
print('Are you connecting using HTTPS? (you should):')
539+
if 'n' in input(' [Yn]> ').lower():
540+
new_conf['scheme'] = 'http'
541+
else:
542+
new_conf['scheme'] = 'https'
543+
print('Do you need to use an insecure connection? (you shouldn\'t):')
544+
new_conf['insecure'] = 'y' in input(' [yN]> ').lower()
545+
service.session_insecure = new_conf['insecure']
546+
if not new_conf['insecure']:
547+
print('Do you want to setup the path to custom certificate?:')
548+
if 'y' in input(' [yN]> ').lower():
549+
new_conf['server-cert'] = loop_input('/path/to/certbundle.pem []> ')
550+
service.session_certificate = new_conf['server-cert']
551+
552+
service.fqdn = new_conf['fqdn']
553+
service.port = new_conf['port']
554+
service.scheme = new_conf['scheme']
555+
506556
print('Please enter your credentials to connect to the service:')
507557
username = loop_input('username> ')
508558
password = loop_input('password> ', method=getpass)
509559

510-
token = service.get_auth_token(username, password, prompt=loop_input)
560+
new_conf['token'] = service.get_auth_token(username, password, prompt=loop_input)
511561
print('Great! You\'ve been identified 🍻')
512562

513563
print('Do you want to give a custom name for this service\'s remote?')
514564
if 'y' in input(' [yN]> ').lower():
515565
print('Enter the remote\'s name:')
516-
loop_input('[{}]> '.format(service.name))
566+
new_conf['remote'] = loop_input('[{}]> '.format(service.name))
517567

518568
print('Do you want to configure a git alias?')
519569
print('N.B.: instead of typing `git repo {0}` you\'ll be able to type `git {0}`'.format(service.command))
@@ -522,7 +572,7 @@ def setup_service(service):
522572
else:
523573
set_alias = True
524574

525-
service.store_config(self.config, token=token)
575+
service.store_config(self.config, **new_conf)
526576
if set_alias:
527577
service.set_alias(self.config)
528578

@@ -555,7 +605,8 @@ def cli(): #pragma: no cover
555605
sys.exit(main(docopt(__doc__.format(self=sys.argv[0].split('/')[-1], version=__version__))))
556606
finally:
557607
# Whatever happens, make sure that the cursor reappears with some ANSI voodoo
558-
sys.stdout.write('\033[?25h')
608+
if sys.stdout.isatty():
609+
sys.stdout.write('\033[?25h')
559610

560611
if __name__ == '__main__': #pragma: no cover
561612
cli()

0 commit comments

Comments
 (0)