Skip to content

Commit

Permalink
馃憤 Merge branch 'devel' ; 馃敄 Version bump to 1.7.4
Browse files Browse the repository at this point in the history
Features

* Added group support for the command: git lab create (fixes #52)
* Added organization support for the command: git hub create
* Added github two factor authentication support (fixes #39)

Bugfixes

* Fix open command check on OSX (fixes #47)
* Fix of the tests configuration (fixes #53)
* Made the git config keep the same order (fixes #48)

Signed-off-by: Guyzmo <guyzmo+github@m0g.net>
  • Loading branch information
guyzmo committed Oct 16, 2016
2 parents f9653cb + 985a4ee commit 2d1dd1f
Show file tree
Hide file tree
Showing 46 changed files with 1,275 additions and 472 deletions.
36 changes: 32 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
language: python
python:
- "3.4"
- "3.5"
- "3.5-dev" # 3.5 development branch
matrix:
include:
- os: linux
python: "3.4"
- os: linux
python: "3.5"
- os: linux
python: "3.5-dev"
- os: linux
python: "3.6-dev"
- os: linux
python: "nightly"
- os: linux
python: "pypy3"

- os: osx
sudo: required
language: generic

allow_failures:
- python: "3.6-dev"
- python: "nightly"
- python: "pypy3"
- os: "osx"
addons:
apt:
packages:
- pandoc
before_install: |
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
brew update;
brew install python3 pandoc;
python3 -m venv venv;
source venv/bin/activate;
pip install . test
fi
# command to install dependencies
install:
- "pip install --upgrade pip" # upgrade to latest pip (needed on py3.4)
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ so that you can push your code to all your remote repositories in one command:

### Installation

You can get the tool using pypi:
You can get the tool using pypi (use `pip3` if you have both Python2 and Python3 installed):

% pip install git-repo

Expand Down Expand Up @@ -276,10 +276,11 @@ With code contributions coming from:

* [@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)

### License

Copyright 漏
Copyright 漏2016 Bernard `Guyzmo` Pratz <guyzmo+git-repo+pub@m0g.net>

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.7.3
1.7.4
12 changes: 2 additions & 10 deletions git_repo/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,14 +242,6 @@ def set_repo_slug(self, repo_slug):
self.user_name = None
self.repo_name = self.repo_slug

@store_parameter('<branch>')
def set_branch(self, branch):
# FIXME workaround for default value that is not correctly parsed in docopt
if branch == None:
branch = 'master'

self.branch = branch

@store_parameter('<branch>')
@store_parameter('--branch')
def set_branch(self, branch):
Expand Down Expand Up @@ -491,7 +483,7 @@ def setup_service(service):
username = loop_input('username> ')
password = loop_input('password> ', method=getpass)

token = service.get_auth_token(username, password)
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?')
Expand All @@ -515,7 +507,7 @@ def setup_service(service):
else:
services = RepositoryService.service_map.values()

for service in services:
for service in sorted(services, key=lambda s: s.name):
print('Do you want to configure the {} service?'.format(service.name))
if 'n' in input(' [Yn]> ').lower():
continue
Expand Down
2 changes: 1 addition & 1 deletion git_repo/services/ext/bitbucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def get_repository(self, user, repo):
#raise ResourceNotFoundError('Cannot retrieve repository: {}/{} does not exists.'.format(user, repo))

@classmethod
def get_auth_token(cls, login, password):
def get_auth_token(cls, login, password, prompt=None):
log.warn("/!\\ Due to API limitations, the bitbucket login/password is stored as plaintext in configuration.")
return "{}:{}".format(login, password)

Expand Down
31 changes: 22 additions & 9 deletions git_repo/services/ext/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,15 @@ def connect(self):
'Check your configuration and try again.') from err

def create(self, user, repo, add=False):
if user != self.username:
raise NotImplementedError("Project creation supported for authentified user only!")
try:
self.gh.create_repo(repo)
if user != self.username:
org = self.gh.organization(user)
if org:
org.create_repo(repo)
else:
raise ResourceNotFoundError("Namespace {} neither an organization or current user.".format(user))
else:
self.gh.create_repo(repo)
except github3.models.GitHubError as err:
if err.code == 422 or err.message == 'name already exists on this account':
raise ResourceExistsError("Project already exists.") from err
Expand Down Expand Up @@ -194,13 +199,21 @@ def request_fetch(self, user, repo, request, pull=False):
raise err

@classmethod
def get_auth_token(cls, login, password):
def get_auth_token(cls, login, password, prompt=None):
import platform
auth = github3.GitHub().authorize(login, password,
scopes=[ 'repo', 'delete_repo', 'gist' ],
note='git-repo token used on {}'.format(platform.node()),
note_url='https://github.com/guyzmo/git-repo')
return auth.token
gh = github3.GitHub()
gh.login(login, password, two_factor_callback=lambda: prompt('2FA code> '))
try:
auth = gh.authorize(login, password,
scopes=[ 'repo', 'delete_repo', 'gist' ],
note='git-repo2 token used on {}'.format(platform.node()),
note_url='https://github.com/guyzmo/git-repo')
return auth.token
except github3.models.GitHubError as err:
if len(err.args) > 0 and 422 == err.args[0].status_code:
raise ResourceExistsError("A token already exist for this machine on your github account.")
else:
raise err

@property
def user(self):
Expand Down
12 changes: 6 additions & 6 deletions git_repo/services/ext/gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ def connect(self):

def create(self, user, repo, add=False):
try:
self.gl.projects.create(data={
'name': repo,
# 'namespace_id': user, # TODO does not work, cannot create on
# another namespace yet
})
group = self.gl.groups.search(user)
data = {'name': repo}
if group:
data['namespace_id'] = group[0].id
self.gl.projects.create(data=data)
except GitlabCreateError as err:
if json.loads(err.response_body.decode('utf-8'))['message']['name'][0] == 'has already been taken':
raise ResourceExistsError("Project already exists.") from err
Expand Down Expand Up @@ -74,7 +74,7 @@ def get_repository(self, user, repo):
raise ResourceNotFoundError("Cannot delete: repository {}/{} does not exists.".format(user, repo)) from err

@classmethod
def get_auth_token(cls, login, password):
def get_auth_token(cls, login, password, prompt=None):
gl = gitlab.Gitlab(url='https://{}'.format(cls.fqdn), email=login, password=password)
gl.auth()
return gl.user.private_token
Expand Down
4 changes: 2 additions & 2 deletions git_repo/services/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

'''select open command'''

if 'mac' in sys.platform: #pragma: no cover
if 'darwin' in sys.platform: #pragma: no cover
OPEN_COMMAND = 'open'
else: #pragma: no cover
OPEN_COMMAND = 'xdg-open'
Expand Down Expand Up @@ -122,7 +122,7 @@ def get_service(cls, repository, command):
return cls._current

@classmethod
def get_auth_token(cls, login, password):
def get_auth_token(cls, login, password, prompt=None):
raise NotImplementedError

def __init__(self, r=None, c=None):
Expand Down
57 changes: 31 additions & 26 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,54 @@

record_mode = 'once'

from git_repo.services.service import RepositoryService
services = list(RepositoryService.service_map.keys())

if os.environ.get('TRAVIS_GH3'):
# create default bogus values for tokens and namespaces if missing for pytest
# to run without environment values
# also if an environment variable is not set, then we don't want to record cassettes
record_mode = 'never'
for service_name in ('github', 'gitlab', 'bitbucket'):
token_name = 'PRIVATE_KEY_{}'.format(service_name.upper())
namespace_name = '{}_NAMESPACE'.format(service_name.upper())
for service in services:
token_name = 'PRIVATE_KEY_{}'.format(service.upper())
namespace_name = '{}_NAMESPACE'.format(service.upper())
if token_name not in os.environ:
os.environ[token_name] = 'not_configured:test' # using a : for bitbucket's case
os.environ[token_name] = '_namespace_{}_:_private_'.format(service) # using a : for bitbucket's case
if namespace_name not in os.environ:
os.environ[namespace_name] = 'not_configured'
os.environ[namespace_name] = '_namespace_{}_'.format(service)
else:
# if running tests "locally" and not in travis, let's try to extract the keys from
# the local configuration if there is some local configuration. And exposes them as
# environment variables.
import git, getpass
config = git.config.GitConfigParser(os.path.join(os.environ['HOME'], '.gitconfig'))
conf_section = list(filter(lambda n: 'gitrepo' in n, config.sections()))
key_dict = {section.split('"')[1] :config.get_value(section, 'privatekey') for section in conf_section}
for service, key in key_dict.items():

# handle the different forms of token configuration item (yup, technical debt bites here)
get_section = lambda s: 'gitrepo "{}"'.format(s)
get_token = lambda s: config.get_value(get_section(s), 'token',
config.get_value(get_section(s), 'private_token',
config.get_value(get_section(s), 'privatekey',
'_namespace_{}_:_private_'.format(s) # using a : for bitbucket's case
)))
# XXX temporary fix that should not be necessary when refactoring with pybitbucket
get_default_namespace = lambda s: os.environ[token_name].split(':')[0] if s == 'bitbucket' else '_namespace_{}_'.format(s)

for service in services:
token_name = 'PRIVATE_KEY_{}'.format(service.upper())
namespace_name = '{}_NAMESPACE'.format(service.upper())
os.environ[token_name] = key
os.environ[namespace_name] = os.environ.get('GITREPO_NAMESPACE', getpass.getuser())

api_token_github = os.environ['PRIVATE_KEY_GITHUB']
api_token_gitlab = os.environ['PRIVATE_KEY_GITLAB']
api_token_bitbucket = os.environ['PRIVATE_KEY_BITBUCKET']

github_namespace = os.environ['GITHUB_NAMESPACE']
gitlab_namespace = os.environ['GITLAB_NAMESPACE']
bitbucket_namespace = os.environ['BITBUCKET_NAMESPACE']
if token_name not in os.environ:
os.environ[token_name] = get_token(service)
if namespace_name not in os.environ:
os.environ[namespace_name] = os.environ.get('GITREPO_NAMESPACE', get_default_namespace(service))

betamax.Betamax.register_serializer(pretty_json.PrettyJSONSerializer)

with betamax.Betamax.configure() as config:
config.default_cassette_options['record_mode'] = record_mode
config.cassette_library_dir = 'tests/integration/cassettes'
config.default_cassette_options['serialize_with'] = 'prettyjson'
config.define_cassette_placeholder('<PRIVATE_KEY_GITHUB>', api_token_github)
config.define_cassette_placeholder('<PRIVATE_KEY_GITLAB>', api_token_gitlab)
config.define_cassette_placeholder('<PRIVATE_KEY_BITBUCKET>', api_token_bitbucket)
config.define_cassette_placeholder('<GITHUB_NAMESPACE>', github_namespace)
config.define_cassette_placeholder('<GITLAB_NAMESPACE>', gitlab_namespace)
config.define_cassette_placeholder('<BITBUCKET_NAMESPACE>', bitbucket_namespace)


# generating placeholders in betamax configuration for each service's key and default namespace
for service in services:
config.define_cassette_placeholder('<PRIVATE_KEY_{}>'.format(service.upper()), os.environ.get('PRIVATE_KEY_{}'.format(service.upper())))
config.define_cassette_placeholder('<{}_NAMESPACE>'.format(service.upper()), os.environ.get('{}_NAMESPACE'.format(service.upper())))

0 comments on commit 2d1dd1f

Please sign in to comment.