diff options
Diffstat (limited to 'gitlab/v3/cli.py')
-rw-r--r-- | gitlab/v3/cli.py | 564 |
1 files changed, 564 insertions, 0 deletions
diff --git a/gitlab/v3/cli.py b/gitlab/v3/cli.py new file mode 100644 index 0000000..142ccfa --- /dev/null +++ b/gitlab/v3/cli.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2017 Gauvain Pocentek <gauvain@pocentek.net> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import print_function +from __future__ import absolute_import +import argparse +import inspect +import operator +import re +import sys + +import six + +import gitlab + +camel_re = re.compile('(.)([A-Z])') + +EXTRA_ACTIONS = { + gitlab.Group: {'search': {'required': ['query']}}, + gitlab.ProjectBranch: {'protect': {'required': ['id', 'project-id']}, + 'unprotect': {'required': ['id', 'project-id']}}, + gitlab.ProjectBuild: {'cancel': {'required': ['id', 'project-id']}, + 'retry': {'required': ['id', 'project-id']}, + 'artifacts': {'required': ['id', 'project-id']}, + 'trace': {'required': ['id', 'project-id']}}, + gitlab.ProjectCommit: {'diff': {'required': ['id', 'project-id']}, + 'blob': {'required': ['id', 'project-id', + 'filepath']}, + 'builds': {'required': ['id', 'project-id']}, + 'cherrypick': {'required': ['id', 'project-id', + 'branch']}}, + gitlab.ProjectIssue: {'subscribe': {'required': ['id', 'project-id']}, + 'unsubscribe': {'required': ['id', 'project-id']}, + 'move': {'required': ['id', 'project-id', + 'to-project-id']}}, + gitlab.ProjectMergeRequest: { + 'closes-issues': {'required': ['id', 'project-id']}, + 'cancel': {'required': ['id', 'project-id']}, + 'merge': {'required': ['id', 'project-id'], + 'optional': ['merge-commit-message', + 'should-remove-source-branch', + 'merged-when-build-succeeds']} + }, + gitlab.ProjectMilestone: {'issues': {'required': ['id', 'project-id']}}, + gitlab.Project: {'search': {'required': ['query']}, + 'owned': {}, + 'all': {'optional': [('all', bool)]}, + 'starred': {}, + 'star': {'required': ['id']}, + 'unstar': {'required': ['id']}, + 'archive': {'required': ['id']}, + 'unarchive': {'required': ['id']}, + 'share': {'required': ['id', 'group-id', + 'group-access']}}, + gitlab.User: {'block': {'required': ['id']}, + 'unblock': {'required': ['id']}, + 'search': {'required': ['query']}, + 'get-by-username': {'required': ['query']}}, +} + + +def _die(msg, e=None): + if e: + msg = "%s (%s)" % (msg, e) + sys.stderr.write(msg + "\n") + sys.exit(1) + + +def _what_to_cls(what): + return "".join([s.capitalize() for s in what.split("-")]) + + +def _cls_to_what(cls): + return camel_re.sub(r'\1-\2', cls.__name__).lower() + + +def do_auth(gitlab_id, config_files): + try: + gl = gitlab.Gitlab.from_config(gitlab_id, config_files) + gl.auth() + return gl + except Exception as e: + _die(str(e)) + + +class GitlabCLI(object): + def _get_id(self, cls, args): + try: + id = args.pop(cls.idAttr) + except Exception: + _die("Missing --%s argument" % cls.idAttr.replace('_', '-')) + + return id + + def do_create(self, cls, gl, what, args): + if not cls.canCreate: + _die("%s objects can't be created" % what) + + try: + o = cls.create(gl, args) + except Exception as e: + _die("Impossible to create object", e) + + return o + + def do_list(self, cls, gl, what, args): + if not cls.canList: + _die("%s objects can't be listed" % what) + + try: + l = cls.list(gl, **args) + except Exception as e: + _die("Impossible to list objects", e) + + return l + + def do_get(self, cls, gl, what, args): + if cls.canGet is False: + _die("%s objects can't be retrieved" % what) + + id = None + if cls not in [gitlab.CurrentUser] and cls.getRequiresId: + id = self._get_id(cls, args) + + try: + o = cls.get(gl, id, **args) + except Exception as e: + _die("Impossible to get object", e) + + return o + + def do_delete(self, cls, gl, what, args): + if not cls.canDelete: + _die("%s objects can't be deleted" % what) + + id = args.pop(cls.idAttr) + try: + gl.delete(cls, id, **args) + except Exception as e: + _die("Impossible to destroy object", e) + + def do_update(self, cls, gl, what, args): + if not cls.canUpdate: + _die("%s objects can't be updated" % what) + + o = self.do_get(cls, gl, what, args) + try: + for k, v in args.items(): + o.__dict__[k] = v + o.save() + except Exception as e: + _die("Impossible to update object", e) + + return o + + def do_group_search(self, cls, gl, what, args): + try: + return gl.groups.search(args['query']) + except Exception as e: + _die("Impossible to search projects", e) + + def do_project_search(self, cls, gl, what, args): + try: + return gl.projects.search(args['query']) + except Exception as e: + _die("Impossible to search projects", e) + + def do_project_all(self, cls, gl, what, args): + try: + return gl.projects.all(all=args.get('all', False)) + except Exception as e: + _die("Impossible to list all projects", e) + + def do_project_starred(self, cls, gl, what, args): + try: + return gl.projects.starred() + except Exception as e: + _die("Impossible to list starred projects", e) + + def do_project_owned(self, cls, gl, what, args): + try: + return gl.projects.owned() + except Exception as e: + _die("Impossible to list owned projects", e) + + def do_project_star(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.star() + except Exception as e: + _die("Impossible to star project", e) + + def do_project_unstar(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.unstar() + except Exception as e: + _die("Impossible to unstar project", e) + + def do_project_archive(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.archive_() + except Exception as e: + _die("Impossible to archive project", e) + + def do_project_unarchive(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.unarchive_() + except Exception as e: + _die("Impossible to unarchive project", e) + + def do_project_share(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.share(args['group_id'], args['group_access']) + except Exception as e: + _die("Impossible to share project", e) + + def do_user_block(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.block() + except Exception as e: + _die("Impossible to block user", e) + + def do_user_unblock(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.unblock() + except Exception as e: + _die("Impossible to block user", e) + + def do_project_commit_diff(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return [x['diff'] for x in o.diff()] + except Exception as e: + _die("Impossible to get commit diff", e) + + def do_project_commit_blob(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.blob(args['filepath']) + except Exception as e: + _die("Impossible to get commit blob", e) + + def do_project_commit_builds(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.builds() + except Exception as e: + _die("Impossible to get commit builds", e) + + def do_project_commit_cherrypick(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.cherry_pick(branch=args['branch']) + except Exception as e: + _die("Impossible to cherry-pick commit", e) + + def do_project_build_cancel(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.cancel() + except Exception as e: + _die("Impossible to cancel project build", e) + + def do_project_build_retry(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.retry() + except Exception as e: + _die("Impossible to retry project build", e) + + def do_project_build_artifacts(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.artifacts() + except Exception as e: + _die("Impossible to get project build artifacts", e) + + def do_project_build_trace(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.trace() + except Exception as e: + _die("Impossible to get project build trace", e) + + def do_project_issue_subscribe(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.subscribe() + except Exception as e: + _die("Impossible to subscribe to issue", e) + + def do_project_issue_unsubscribe(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.unsubscribe() + except Exception as e: + _die("Impossible to subscribe to issue", e) + + def do_project_issue_move(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + o.move(args['to_project_id']) + except Exception as e: + _die("Impossible to move issue", e) + + def do_project_merge_request_closesissues(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.closes_issues() + except Exception as e: + _die("Impossible to list issues closed by merge request", e) + + def do_project_merge_request_cancel(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.cancel_merge_when_build_succeeds() + except Exception as e: + _die("Impossible to cancel merge request", e) + + def do_project_merge_request_merge(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + should_remove = args.get('should_remove_source_branch', False) + build_succeeds = args.get('merged_when_build_succeeds', False) + return o.merge( + merge_commit_message=args.get('merge_commit_message', ''), + should_remove_source_branch=should_remove, + merged_when_build_succeeds=build_succeeds) + except Exception as e: + _die("Impossible to validate merge request", e) + + def do_project_milestone_issues(self, cls, gl, what, args): + try: + o = self.do_get(cls, gl, what, args) + return o.issues() + except Exception as e: + _die("Impossible to get milestone issues", e) + + def do_user_search(self, cls, gl, what, args): + try: + return gl.users.search(args['query']) + except Exception as e: + _die("Impossible to search users", e) + + def do_user_getbyusername(self, cls, gl, what, args): + try: + return gl.users.search(args['query']) + except Exception as e: + _die("Impossible to get user %s" % args['query'], e) + + +def _populate_sub_parser_by_class(cls, sub_parser): + for action_name in ['list', 'get', 'create', 'update', 'delete']: + attr = 'can' + action_name.capitalize() + if not getattr(cls, attr): + continue + sub_parser_action = sub_parser.add_parser(action_name) + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=True) + for x in cls.requiredUrlAttrs] + sub_parser_action.add_argument("--sudo", required=False) + + if action_name == "list": + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=True) + for x in cls.requiredListAttrs] + sub_parser_action.add_argument("--page", required=False) + sub_parser_action.add_argument("--per-page", required=False) + sub_parser_action.add_argument("--all", required=False, + action='store_true') + + if action_name in ["get", "delete"]: + if cls not in [gitlab.CurrentUser]: + if cls.getRequiresId: + id_attr = cls.idAttr.replace('_', '-') + sub_parser_action.add_argument("--%s" % id_attr, + required=True) + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=True) + for x in cls.requiredGetAttrs if x != cls.idAttr] + + if action_name == "get": + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=False) + for x in cls.optionalGetAttrs] + + if action_name == "list": + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=False) + for x in cls.optionalListAttrs] + + if action_name == "create": + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=True) + for x in cls.requiredCreateAttrs] + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=False) + for x in cls.optionalCreateAttrs] + + if action_name == "update": + id_attr = cls.idAttr.replace('_', '-') + sub_parser_action.add_argument("--%s" % id_attr, + required=True) + + attrs = (cls.requiredUpdateAttrs + if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs) + else cls.requiredCreateAttrs) + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=True) + for x in attrs if x != cls.idAttr] + + attrs = (cls.optionalUpdateAttrs + if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs) + else cls.optionalCreateAttrs) + [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), + required=False) + for x in attrs] + + if cls in EXTRA_ACTIONS: + def _add_arg(parser, required, data): + extra_args = {} + if isinstance(data, tuple): + if data[1] is bool: + extra_args = {'action': 'store_true'} + data = data[0] + + parser.add_argument("--%s" % data, required=required, **extra_args) + + for action_name in sorted(EXTRA_ACTIONS[cls]): + sub_parser_action = sub_parser.add_parser(action_name) + d = EXTRA_ACTIONS[cls][action_name] + [_add_arg(sub_parser_action, True, arg) + for arg in d.get('required', [])] + [_add_arg(sub_parser_action, False, arg) + for arg in d.get('optional', [])] + + +def _build_parser(args=sys.argv[1:]): + parser = argparse.ArgumentParser( + description="GitLab API Command Line Interface") + parser.add_argument("--version", help="Display the version.", + action="store_true") + parser.add_argument("-v", "--verbose", "--fancy", + help="Verbose mode", + action="store_true") + parser.add_argument("-c", "--config-file", action='append', + help=("Configuration file to use. Can be used " + "multiple times.")) + parser.add_argument("-g", "--gitlab", + help=("Which configuration section should " + "be used. If not defined, the default selection " + "will be used."), + required=False) + + subparsers = parser.add_subparsers(title='object', dest='what', + help="Object to manipulate.") + subparsers.required = True + + # populate argparse for all Gitlab Object + classes = [] + for cls in gitlab.__dict__.values(): + try: + if gitlab.GitlabObject in inspect.getmro(cls): + classes.append(cls) + except AttributeError: + pass + classes.sort(key=operator.attrgetter("__name__")) + + for cls in classes: + arg_name = _cls_to_what(cls) + object_group = subparsers.add_parser(arg_name) + + object_subparsers = object_group.add_subparsers( + dest='action', help="Action to execute.") + _populate_sub_parser_by_class(cls, object_subparsers) + object_subparsers.required = True + + return parser + + +def _parse_args(args=sys.argv[1:]): + parser = _build_parser() + return parser.parse_args(args) + + +def main(): + if "--version" in sys.argv: + print(gitlab.__version__) + exit(0) + + arg = _parse_args() + args = arg.__dict__ + + config_files = arg.config_file + gitlab_id = arg.gitlab + verbose = arg.verbose + action = arg.action + what = arg.what + + # Remove CLI behavior-related args + for item in ("gitlab", "config_file", "verbose", "what", "action", + "version"): + args.pop(item) + + args = {k: v for k, v in args.items() if v is not None} + + cls = None + try: + cls = gitlab.__dict__[_what_to_cls(what)] + except Exception: + _die("Unknown object: %s" % what) + + gl = do_auth(gitlab_id, config_files) + + cli = GitlabCLI() + method = None + what = what.replace('-', '_') + action = action.lower().replace('-', '') + for test in ["do_%s_%s" % (what, action), + "do_%s" % action]: + if hasattr(cli, test): + method = test + break + + if method is None: + sys.stderr.write("Don't know how to deal with this!\n") + sys.exit(1) + + ret_val = getattr(cli, method)(cls, gl, what, args) + + if isinstance(ret_val, list): + for o in ret_val: + if isinstance(o, gitlab.GitlabObject): + o.display(verbose) + print("") + else: + print(o) + elif isinstance(ret_val, gitlab.GitlabObject): + ret_val.display(verbose) + elif isinstance(ret_val, six.string_types): + print(ret_val) + + sys.exit(0) |