diff --git a/bloom/commands/release.py b/bloom/commands/release.py index 9bade4b2..61cd0660 100644 --- a/bloom/commands/release.py +++ b/bloom/commands/release.py @@ -82,6 +82,10 @@ from bloom.github import GithubException from bloom.github import GitHubAuthException +from bloom.gitlab import Gitlab +from bloom.gitlab import GitlabException +from bloom.gitlab import GitlabAuthException + from bloom.logging import debug from bloom.logging import error from bloom.logging import fmt @@ -683,13 +687,30 @@ def get_gh_info(url): 'path': '/'.join(url_paths[4:])} +def get_gl_info(url): + o = urlparse(url) + if 'gitlab' not in o.netloc: + return None + url_paths = o.path.split('/') + if len(url_paths) < 6: + return None + return {'server': '{}://{}'.format(o.scheme, o.netloc), + 'org': url_paths[1], + 'repo': url_paths[2], + 'branch': url_paths[4], + 'path': '/'.join(url_paths[5:])} + + def get_repo_info(distro_url): gh_info = get_gh_info(distro_url) if gh_info: return gh_info + else: + return get_gl_info(distro_url) _gh = None +_gl = None def get_github_interface(quiet=False): @@ -769,6 +790,52 @@ def mfa_prompt(oauth_config_path, username): return gh +def get_gitlab_interface(server, quiet=False): + global _gl + if _gl is not None: + return _gl + + config, oauth_config_path = get_bloom_config_and_path() + if 'gitlab' in config: + _gl = Gitlab(server, token=config['gitlab']) + return _gl + + if quiet: + return None + + info("") + warning("Looks like bloom doesn't have a gitlab token for you yet.") + warning("Go to http://{}/profile/personal_access_tokens to create one.".format(server)) + warning("Make sure you give it API access.") + warning("The token will be stored in `~/.config/bloom`.") + warning("You can delete the token from that file to have a new token generated.") + warning("Guard this token like a password, because it allows someone/something to act on your behalf.") + info("") + if not maybe_continue('y', "Would you like to input a token now"): + return None + token = None + while token is None: + try: + token = safe_input("Gitlab Token: ") + except (KeyboardInterrupt, EOFError): + return None + try: + gl = Gitlab(server, token=token) + gl.auth() + with open(oauth_config_path, 'w') as f: + config.update({'gitlab': token}) + f.write(json.dumps(config)) + info("The token was stored in the bloom config file") + _gl = gl + break + except GitlabAuthException: + error("Failed to authenticate your token.") + if not maybe_continue(): + return None + + return _gl + + def get_changelog_summary(release_tag): summary = u"" packages = dict([(p.name, p) for p in get_packages().values()]) @@ -948,6 +1015,27 @@ def _my_run(cmd, msg=None): # Open the pull request return gh.create_pull_request(base_info['org'], base_info['repo'], base_info['branch'], head_org, new_branch, title, body) + else: # if base_info['server'] == 'github.com': + gl = get_gitlab_interface(server) + if gl is None: + return None + + repo_obj = gl.get_repo(base_org, base_repo) + + # Determine New Branch Name + branches = gl.list_branches(repo_obj) + new_branch = 'bloom-{repository}-{count}' + count = 0 + while new_branch.format(repository=repository, count=count) in branches: + count += 1 + new_branch = new_branch.format(repository=repository, count=count) + + gl.create_branch(repo_obj, new_branch, base_branch) + gl.update_file(repo_obj, new_branch, title, base_path, updated_distro_file_yaml) + + mr = gl.create_pull_request(repo_obj, new_branch, base_branch, title, body) + return mr['web_url'] + _original_version = None diff --git a/bloom/gitlab.py b/bloom/gitlab.py new file mode 100644 index 00000000..ac1be5cb --- /dev/null +++ b/bloom/gitlab.py @@ -0,0 +1,90 @@ +import requests + + +class GitlabException(Exception): + def __init__(self, msg, code=None): + if code: + msg = "{msg}: {code}".format(**locals()) + super(GitlabException, self).__init__(msg) + + +class GitlabAuthException(GitlabException): + def __init__(self, msg, code=None): + super(GitlabAuthException, self).__init__(msg, code) + + +class Gitlab(object): + def __init__(self, server, token=None): + self.server = server + self.token = token + self.api_version = 4 + self.base_api_url = 'http://{server}/api/v{api_version}'.format(server=server, api_version=self.api_version) + + def update_params(self, params): + if self.token: + if params is None: + params = {} + params['private_token'] = self.token + return params + + def api_get(self, query, params=None): + r = requests.get(self.base_api_url + query, params=self.update_params(params)) + if r.status_code == 401: + raise GitlabAuthException('Authentication Failed', r.status_code) + elif r.status_code == 200: + return r.json() + else: + raise GitlabException('Query {} failed.'.format(query), r.status_code) + + def api_post(self, query, params=None): + r = requests.post(self.base_api_url + query, params=self.update_params(params)) + if r.status_code == 401: + raise GitlabAuthException('Authentication Failed', r.status_code) + elif r.status_code == 201: + return r.json() + else: + raise GitlabException('Query {} failed.'.format(query), r.status_code) + + def auth(self): + """ Authenticate by trying to get the projects """ + self.api_get('/projects') + + def get_repo(self, owner, repo): + path = '{}/{}'.format(owner, repo) + for repo_d in self.api_get('/projects', {'search': repo}): + if repo_d.get('path_with_namespace', '') == path: + return repo_d + + def list_branches(self, repo): + res = self.api_get('/projects/{}/repository/branches'.format(repo['id'])) + return [d['name'] for d in res] + + def create_branch(self, repo, new_branch, base_branch): + params = {'branch': new_branch, 'ref': base_branch} + return self.api_post('/projects/{}/repository/branches'.format(repo['id']), params) + + def create_commit(self, repo, branch, commit_message, actions): + params = { + 'branch': branch, + 'commit_message': commit_message, + 'actions': actions + } + return self.api_post('/projects/{}/repository/commits'.format(repo['id']), params) + + def update_file(self, repo, branch, commit_message, file_path, new_contents): + actions = [{ + 'action': 'update', + 'file_path': file_path, + 'content': new_contents + } + ] + return self.create_commit(repo, branch, commit_message, actions) + + def create_pull_request(self, repo, source_branch, target_branch, title, body=''): + params = { + 'source_branch': source_branch, + 'target_branch': target_branch, + 'title': title, + 'description': body + } + return self.api_post('/projects/{}/merge_requests'.format(repo['id']), params) diff --git a/stdeb.cfg b/stdeb.cfg index 2ed6add0..f15ae4d0 100755 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,6 +1,7 @@ [DEFAULT] -Depends: python-yaml, python-empy, python-argparse, python-rosdep (>= 0.10.25), python-rosdistro (>= 0.7.0), python-vcstools (>= 0.1.22), python-setuptools, python-catkin-pkg (>= 0.4.3) -Depends3: python3-yaml, python3-empy, python3-rosdep (>= 0.10.25), python3-rosdistro (>= 0.7.0), python3-vcstools (>= 0.1.22), python3-setuptools, python3-catkin-pkg (>= 0.4.3) + +Depends: python-yaml, python-empy, python-argparse, python-rosdep (>= 0.10.25), python-rosdistro (>= 0.7.0), python-vcstools (>= 0.1.22), python-setuptools, python-catkin-pkg (>= 0.4.3), python-requests (>= 2.2) +Depends3: python3-yaml, python3-empy, python3-rosdep (>= 0.10.25), python3-rosdistro (>= 0.7.0), python3-vcstools (>= 0.1.22), python3-setuptools, python3-catkin-pkg (>= 0.4.3), python3-requests (>= 2.2) Conflicts: python3-bloom Conflicts3: python-bloom Copyright-File: LICENSE.txt