# Copyright (C) 2016 Codethink Limited # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from __future__ import absolute_import import re import urlparse import itertools try: import gitlab except ImportError: gitlab = None class MissingGitlabModuleError(Exception): pass class Gitlab(object): '''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. ''' 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 first(self, predicate, iterable): return next(itertools.ifilter(predicate, iterable)) def split_and_unslashify_path(self, path): group, project = path.split('/', 1) return group, project.replace('/', '_') 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.error_message == '404 Not found': group = self.gl.groups.create( {'name': group_name, 'path': group_name}) else: raise project = { 'name': project_name, '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 } 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) 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. ''' return [self.suitable_path(x) for x in self.gl.projects.list()] def get_project_url(self, protocol, project_path): '''Return the clone url for a GitLab project. Depending on the protocol specified, will return a suitable clone url. If 'ssh', a url in the format 'git@host:group/project.git' will be returned. If 'http' or 'https', the http_url_to_repo from the GitLab API is split with urlparse into its constituent parts: the protocol (http by default), the host, and the path ('group/project.git'). This is then rejoined, replacing the protocol with what is specified. The resulting format matching 'http(s)://host/group/project.git'. ''' project = self.find_project(project_path) if protocol == 'ssh': return project.ssh_url_to_repo elif protocol in ('http', 'https'): split = urlparse.urlsplit(project.http_url_to_repo) return urlparse.urlunsplit(( protocol, split.netloc, split.path, '', ''))