Skip to content

Commit

Permalink
👍 Merge branch devel ; 🔖 Version bump to 1.9.0
Browse files Browse the repository at this point in the history
🚧 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]>
  • Loading branch information
guyzmo committed Feb 6, 2017
2 parents ee6384d + a6f09ff commit ea4fc21
Show file tree
Hide file tree
Showing 58 changed files with 2,135 additions and 354 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ matrix:
- os: linux
python: "3.5"
- os: linux
python: "3.5-dev"
python: "3.6"
- os: linux
python: "3.6-dev"
- os: linux
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ very simple. To clone a new project, out of GitHub, just issue:

% git hub clone guyzmo/git-repo

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

% git lab clone guyzmo/git-repo
% git bb clone guyzmo/git-repo
% git myprecious clone guyzmo/git-repo
% git gg clone guyzmo/git-repo

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

Expand Down Expand Up @@ -151,6 +152,10 @@ section in the gitconfig:
[gitrepo "bitbucket"]
token = username:password

[gitrepo "gogs"]
fqdn = UrlOfYourGogs
token = YourVerySecretKey

Here, we're setting the basics: just the private token. You'll notice that for bitbucket
the private token is your username and password seperated by a column. That's because
bitbucket does not offer throw away private tokens for tools (I might implement BB's OAuth
Expand Down Expand Up @@ -253,9 +258,11 @@ To use your own credentials, you can setup the following environment variables:
* `GITHUB_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on GitHub
* `GITLAB_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on GitLab
* `BITBUCKET_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on Bitbucket
* `GOGS_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on Gogs
* `PRIVATE_KEY_GITHUB` your private token you've setup on GitHub for your account
* `PRIVATE_KEY_GITLAB` your private token you've setup on GitLab for your account
* `PRIVATE_KEY_BITBUCKET` your private token you've setup on Bitbucket for your account
* `PRIVATE_KEY_GOGS` your private token you've setup on Gogs for your account

### TODO

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

With code contributions coming from:

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

### License

Copyright ©2016 Bernard `Guyzmo` Pratz <[email protected]>
Copyright ©2016,2017 Bernard `Guyzmo` Pratz <[email protected]>

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
Expand Down
1 change: 0 additions & 1 deletion VERSION

This file was deleted.

179 changes: 115 additions & 64 deletions git_repo/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
{self} [--path=<path>] [-v...] <target> clone <user>/<repo> [<repo> [<branch>]]
{self} [--path=<path>] [-v...] <target> add <user>/<repo> [<name>] [--tracking=<branch>] [-a]
{self} [--path=<path>] [-v...] <target> request (list|ls)
{self} [--path=<path>] [-v...] <target> request fetch <request>
{self} [--path=<path>] [-v...] <target> request create <title> [--message=<message>]
{self} [--path=<path>] [-v...] <target> request create <local_branch> <title> [--message=<message>]
{self} [--path=<path>] [-v...] <target> request create <remote_branch> <local_branch> <title> [--message=<message>]
{self} [--path=<path>] [-v...] <target> request fetch <request> [-f]
{self} [--path=<path>] [-v...] <target> request create [--title=<title>] [--message=<message>]
{self} [--path=<path>] [-v...] <target> request create <local_branch> [--title=<title>] [--message=<message>]
{self} [--path=<path>] [-v...] <target> request create <remote_branch> <local_branch> [--title=<title>] [--message=<message>]
{self} [--path=<path>] [-v...] <target> request <user>/<repo> (list|ls)
{self} [--path=<path>] [-v...] <target> request <user>/<repo> fetch <request>
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <title> [--branch=<remote>] [--message=<message>]
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <local_branch> <title> [--branch=<remote>] [--message=<message>]
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <remote_branch> <local_branch> <title> [--branch=<remote>] [--message=<message>]
{self} [--path=<path>] [-v...] <target> request <user>/<repo> fetch <request> [-f]
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create [--title=<title>] [--branch=<remote>] [--message=<message>]
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <local_branch> [--title=<title>] [--branch=<remote>] [--message=<message>]
{self} [--path=<path>] [-v...] <target> request <user>/<repo> create <remote_branch> <local_branch> [--title=<title>] [--branch=<remote>] [--message=<message>]
{self} [--path=<path>] [-v...] <target> (gist|snippet) (list|ls) [<gist>]
{self} [--path=<path>] [-v...] <target> (gist|snippet) clone <gist>
{self} [--path=<path>] [-v...] <target> (gist|snippet) fetch <gist> [<gist_file>]
Expand Down Expand Up @@ -89,7 +89,7 @@
--secret Do not publicize gist when pushing
Options for request:
<title> Title to give to the request for merge
-t,--title=<title> Title to give to the request for merge
-m,--message=<message> Description for the request for merge
Configuration options:
Expand Down Expand Up @@ -137,58 +137,46 @@
from .exceptions import ArgumentError, ResourceNotFoundError
from .services.service import RepositoryService

from .tools import print_tty, print_iter, loop_input, confirm
from .kwargparse import KeywordArgumentParser, store_parameter, register_action

from git import Repo, Git
from git.exc import InvalidGitRepositoryError, NoSuchPathError
from git.exc import InvalidGitRepositoryError, NoSuchPathError, BadName

import re

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

def confirm(what, where):
'''
Method to show a CLI based confirmation message, waiting for a yes/no answer.
"what" and "where" are used to better define the message.
'''
ans = input('Are you sure you want to delete the '
'{} {} from the service?\n[yN]> '.format(what, where))
if 'y' in ans:
ans = input('Are you really sure? there\'s no coming back!\n'
'[type \'burn!\' to proceed]> ')
if 'burn!' != ans:
return False
else:
return False
return True


class GitRepoRunner(KeywordArgumentParser):

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

def _guess_repo_slug(self, repository, service):
def _guess_repo_slug(self, repository, service, resolve_targets=None):
config = repository.config_reader()
target = service.name
if resolve_targets:
targets = [target.format(service=service.name) for target in resolve_targets]
else:
targets = (service.name, 'upstream', 'origin')
for remote in repository.remotes:
if remote.name in (target, 'upstream', 'origin'):
if remote.name in targets:
for url in remote.urls:
if url.startswith('https'):
if url.endswith('.git'):
url = url[:-4]
*_, user, name = url.split('/')
self.set_repo_slug('/'.join([user, name]))
break
return
elif url.startswith('git@'):
if url.endswith('.git'):
url = url[:-4]
_, repo_slug = url.split(':')
self.set_repo_slug(repo_slug)
break
return

def get_service(self, lookup_repository=True):
def get_service(self, lookup_repository=True, resolve_targets=None):
if not lookup_repository:
service = RepositoryService.get_service(None, self.target)
service.connect()
Expand All @@ -203,7 +191,7 @@ def get_service(self, lookup_repository=True):
raise FileNotFoundError('Cannot find path to the repository.')
service = RepositoryService.get_service(repository, self.target)
if not self.repo_name:
self._guess_repo_slug(repository, service)
self._guess_repo_slug(repository, service, resolve_targets)
return service

'''Argument storage'''
Expand Down Expand Up @@ -273,15 +261,14 @@ def set_gist_ref(self, gist):

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

'''Actions'''

@register_action('ls')
@register_action('list')
def do_list(self):
service = self.get_service(False)
service.list(self.user, self.long)
print_iter(self.get_service(False).list(self.user, self.long))
return 0

@register_action('add')
Expand Down Expand Up @@ -399,32 +386,77 @@ def do_open(self):
@register_action('request', 'ls')
@register_action('request', 'list')
def do_request_list(self):
service = self.get_service()
log.info('List of open requests to merge:')
log.info(" {}\t{}\t{}".format('id', 'title'.ljust(60), 'URL'))
for pr in service.request_list(self.user_name, self.repo_name):
print("{}\t{}\t{}".format(pr[0].rjust(3), pr[1][:60].ljust(60), pr[2]))
service = self.get_service(lookup_repository=self.repo_slug == None)
print_tty('List of open requests to merge:')
print_iter(service.request_list(self.user_name, self.repo_name))
return 0

@register_action('request', 'create')
def do_request_create(self):
service = self.get_service()
def request_edition(repository, from_branch):
try:
commit = repository.commit(from_branch)
title, *body = commit.message.split('\n')
except BadName:
log.error('Couldn\'t find local source branch {}'.format(from_branch))
return None
from tempfile import NamedTemporaryFile
from subprocess import call
with NamedTemporaryFile(
prefix='git-repo-issue-',
suffix='.md',
mode='w+b') as request_file:
request_file.write((
'# Request for Merge Title ##########################\n'
'{}\n'
'\n'
'# Request for Merge Body ###########################\n'
'{}\n'
'####################################################\n'
'## Filled with commit:\n'
'## {}\n'
'####################################################\n'
'## * All lines starting with # will be ignored.\n'
'## * First non-ignored line is the title of the request.\n'
).format(title, '\n'.join(body), commit.name_rev).encode('utf-8'))
request_file.flush()
rv = call("{} {}".format(os.environ['EDITOR'], request_file.name), shell=True)
if rv != 0:
raise ArgumentError("Aborting request, as editor exited abnormally.")
request_file.seek(0)
request_message = map(lambda l: l.decode('utf-8'),
filter(lambda l: not l.strip().startswith(b'#'), request_file.readlines()))
try:
title = next(request_message)
body = ''.join(request_message)
except Exception:
raise ResourceError("Format of the request message cannot be parsed.")

return title, body


service = self.get_service(resolve_targets=('upstream', '{service}', 'origin'))

new_request = service.request_create(self.user_name,
self.repo_name,
self.local_branch,
self.remote_branch,
self.title,
self.message)
self.message,
self.repo_slug != None,
request_edition)
log.info('Successfully created request of `{local}` onto `{}:{remote}`, with id `{ref}`!'.format(
'/'.join([self.user_name, self.repo_name]),
**new_request)
)
if 'url' in new_request:
log.info('available at: {url}'.format(**new_request))
return 0

@register_action('request', 'fetch')
def do_request_fetch(self):
service = self.get_service()
new_branch = service.request_fetch(self.user_name, self.repo_name, self.request)
new_branch = service.request_fetch(self.user_name, self.repo_name, self.request, force=self.force)
log.info('Successfully fetched request id `{}` of `{}` into `{}`!'.format(
self.request,
self.repo_slug,
Expand All @@ -438,16 +470,7 @@ def do_request_fetch(self):
@register_action('snippet', 'list')
def do_gist_list(self):
service = self.get_service(lookup_repository=False)
if 'github' == service.name and self.gist_ref:
log.info("{:15}\t{:>7}\t{}".format('language', 'size', 'name'))
else:
log.info("{:56}\t{}".format('id', 'title'.ljust(60)))
if self.gist_ref:
for gist_file in service.gist_list(self.gist_ref):
print("{:15}\t{:7}\t{}".format(*gist_file))
else:
for gist in service.gist_list():
print( "{:56}\t{}".format(gist[0], gist[1]))
print_iter(service.gist_list(self.gist_ref or None))
return 0

@register_action('gist', 'clone')
Expand Down Expand Up @@ -492,28 +515,55 @@ def do_gist_delete(self):
def do_config(self):
from getpass import getpass

def loop_input(*args, method=input, **kwarg):
out = ''
while len(out) == 0:
out = method(*args, **kwarg)
return out

def setup_service(service):
new_conf = dict(
fqdn=None,
remote=None,
)
conf = service.get_config(self.config)
if 'token' in conf:
raise Exception('A token has been generated for this service. Please revoke and delete before proceeding.')

print('Is your service self-hosted?')
if 'y' in input(' [yN]> ').lower():
new_conf['type'] = service.name
print('What name do you want to give this service?')
new_conf['name'] = input('[{}]> '.format(service.name))
new_conf['command'] = new_conf['name']
service.name, service.command = new_conf['name'], new_conf['command']
print('Enter the service\'s domain name:')
new_conf['fqdn'] = input('[{}]> '.format(service.fqdn))
print('Enter the service\'s port:')
new_conf['port'] = input('[443]> ') or '443'
print('Are you connecting using HTTPS? (you should):')
if 'n' in input(' [Yn]> ').lower():
new_conf['scheme'] = 'http'
else:
new_conf['scheme'] = 'https'
print('Do you need to use an insecure connection? (you shouldn\'t):')
new_conf['insecure'] = 'y' in input(' [yN]> ').lower()
service.session_insecure = new_conf['insecure']
if not new_conf['insecure']:
print('Do you want to setup the path to custom certificate?:')
if 'y' in input(' [yN]> ').lower():
new_conf['server-cert'] = loop_input('/path/to/certbundle.pem []> ')
service.session_certificate = new_conf['server-cert']

service.fqdn = new_conf['fqdn']
service.port = new_conf['port']
service.scheme = new_conf['scheme']

print('Please enter your credentials to connect to the service:')
username = loop_input('username> ')
password = loop_input('password> ', method=getpass)

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

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

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

service.store_config(self.config, token=token)
service.store_config(self.config, **new_conf)
if set_alias:
service.set_alias(self.config)

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

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

0 comments on commit ea4fc21

Please sign in to comment.