diff options
Diffstat (limited to 'lorrycontroller/gitlab.py')
-rw-r--r-- | lorrycontroller/gitlab.py | 183 |
1 files changed, 106 insertions, 77 deletions
diff --git a/lorrycontroller/gitlab.py b/lorrycontroller/gitlab.py index 6938cae..4f70f0a 100644 --- a/lorrycontroller/gitlab.py +++ b/lorrycontroller/gitlab.py @@ -13,113 +13,133 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +''' +Run commands on a GitLab instance. +This uses the python wrapper around the GitLab API. +Use of the API requires the private token of a user with master access +to the targetted group. +''' + +import logging import re import urllib.parse -import itertools + try: import gitlab except ImportError: gitlab = None +from . import hosts + class MissingGitlabModuleError(Exception): pass -class Gitlab(object): +def _init_gitlab(host, token): + if gitlab: + url = "http://" + host + return gitlab.Gitlab(url, token) + else: + raise MissingGitlabModuleError('gitlab module missing\n' + '\tpython-gitlab is required with GitLab as the git server') - '''Run commands on a GitLab instance. - This uses the python wrapper around the GitLab API. - Use of the API requires the private token of a user with master access - to the targetted group. +class GitlabDownstream(hosts.DownstreamHost): + @staticmethod + def add_app_settings(app_settings): + app_settings.string( + ['gitlab-private-token'], + 'private token for GitLab API access') - ''' + @staticmethod + def check_app_settings(app_settings): + if not app_settings['gitlab-private-token']: + logging.error('A private token must be provided to create ' + 'repositories on a GitLab instance.') + app_settings.require('gitlab-private-token') - def __init__(self, host, token): - if gitlab: - url = "http://" + host - self.gl = gitlab.Gitlab(url, token) - else: - raise MissingGitlabModuleError('gitlab module missing\n' - '\tpython-gitlab is required with GitLab as the git server') + def __init__(self, app_settings): + # XXX This needs to be configurable + host = 'localhost' - def first(self, predicate, iterable): - return next(filter(predicate, iterable)) + self.gl = _init_gitlab(host, app_settings['gitlab-private-token']) - def split_and_unslashify_path(self, path): - group, project = path.split('/', 1) - return group, project.replace('/', '_') + def prepare_repo(self, repo_path, metadata): - def find_project(self, repo_path): - group, project = self.split_and_unslashify_path(repo_path) - predicate = lambda x: x.namespace.name == group and x.name == project - - return self.first(predicate, self.gl.projects.search(project)) - - def has_project(self, repo_path): - try: - return bool(self.find_project(repo_path)) - except StopIteration: - return False - - def create_project(self, repo_path): - # GitLab only supports one level of namespacing. - group_name, project_name = self.split_and_unslashify_path(repo_path) - group = None try: - group = self.gl.groups.get(group_name) - except gitlab.GitlabGetError as e: - if e.response_code == 404: - group = self.gl.groups.create( - {'name': group_name, 'path': group_name}) + project = self.gl.projects.get(repo_path) + except gitlab.GitlabGetError: + pass + else: + logging.info('Project %s exists in local GitLab already.', + repo_path) + if 'head' in metadata \ + and project.default_branch != metadata['head']: + project.default_branch = metadata['head'] + if 'description' in metadata \ + and project.description != metadata['description']: + project.description = metadata['description'] + project.save() + return + + path_comps = repo_path.split('/') + + if len(path_comps) < 2: + raise ValueError('cannot create GitLab project outside a group') + + # Create hierarchy of groups as necessary + parent_group = None + for group_name in path_comps[:-1]: + if parent_group is None: + group_path = group_name else: - raise + group_path = parent_group.full_path + '/' + group_name + try: + group = self.gl.groups.get(group_path) + except gitlab.GitlabGetError as e: + if e.response_code != 404: + raise + data = {'name': group_name, 'path': group_name} + if parent_group is not None: + data['parent_id'] = parent_group.id + group = self.gl.groups.create(data) + parent_group = group project = { - 'name': project_name, + 'name': path_comps[-1], 'public': True, 'merge_requests_enabled': False, 'namespace_id': group.id, - # Set the original path in the description. We will use this to - # work around lack of multi-level namespacing. - 'description': 'original_path: %s' % repo_path + 'default_branch': metadata.get('head'), + 'description': metadata.get('description'), } self.gl.projects.create(project) - def try_get_original_path(self, project_description): - match = re.search('original_path:\s(.*)', str(project_description)) - if match: - return match.groups()[0] - - def suitable_path(self, project): - '''Return a path for a downstream Lorry Controller instance to consume. - - Should the path that was lorried have contained more than one level of - namespacing (more than one '/' within the repository path), then for - GitLab to handle this, we replace any '/'s (remaining in the project - name after extracting the group name) with underscores (_). To preserve - the original path, we set the 'original_path' within the project - description. - This method will attempt to return 'original_path' if it was set, - otherwise it will return the 'path_with_namespace', being of the format - 'group_name/project_name', rather than 'group_name/project/name'. - ''' - return (self.try_get_original_path(project.description) or - project.path_with_namespace) + logging.info('Created %s project in local GitLab.', repo_path) - def list_projects(self): - '''List projects on a GitLab instance. - In attempt to handle GitLab's current lack of multi-level namespacing - (see: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772), return - the 'original_path' stored in a project's description, if it exists. - ''' +class GitlabUpstream(hosts.UpstreamHost): + @staticmethod + def check_host_type_params(validator, section): + validator.check_has_required_fields(section, ['private-token']) + + @staticmethod + def get_host_type_params(section): + return {'private-token': section['private-token']} + + def __init__(self, host_info): + self._protocol = host_info['protocol'] + self.gl = _init_gitlab(host_info['host'], + host_info['type_params']['private-token']) + + def list_repos(self): + '''List projects on a GitLab instance.''' - return [self.suitable_path(x) for x in self.gl.projects.list()] + return [x.path_with_namespace for x in self.gl.projects.list()] - def get_project_url(self, protocol, project_path): + def get_repo_url(self, repo_path): '''Return the clone url for a GitLab project. Depending on the protocol specified, will return a suitable clone url. @@ -132,11 +152,20 @@ class Gitlab(object): format matching 'http(s)://host/group/project.git'. ''' - project = self.find_project(project_path) + project = self.gl.projects.get(repo_path) - if protocol == 'ssh': + if self._protocol == 'ssh': return project.ssh_url_to_repo - elif protocol in ('http', 'https'): + elif self._protocol in ('http', 'https'): split = urllib.parse.urlsplit(project.http_url_to_repo) return urllib.parse.urlunsplit(( - protocol, split.netloc, split.path, '', '')) + self._protocol, split.netloc, split.path, '', '')) + + def get_repo_metadata(self, repo_path): + project = self.gl.projects.get(repo_path) + metadata = {} + if project.default_branch is not None: + metadata['head'] = project.default_branch + if project.description is not None: + metadata['description'] = project.description + return metadata |