summaryrefslogtreecommitdiff
path: root/lorrycontroller/gitlab.py
diff options
context:
space:
mode:
Diffstat (limited to 'lorrycontroller/gitlab.py')
-rw-r--r--lorrycontroller/gitlab.py183
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