summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gitlab/base.py7
-rw-r--r--gitlab/cli.py41
-rw-r--r--gitlab/v4/cli.py296
-rw-r--r--gitlab/v4/objects.py69
4 files changed, 407 insertions, 6 deletions
diff --git a/gitlab/base.py b/gitlab/base.py
index df25a36..a9521eb 100644
--- a/gitlab/base.py
+++ b/gitlab/base.py
@@ -607,6 +607,13 @@ class RESTObject(object):
return None
return getattr(self, self._id_attr)
+ @property
+ def attributes(self):
+ d = self.__dict__['_updated_attrs'].copy()
+ d.update(self.__dict__['_attrs'])
+ d.update(self.__dict__['_parent_attrs'])
+ return d
+
class RESTObjectList(object):
"""Generator object representing a list of RESTObject's.
diff --git a/gitlab/cli.py b/gitlab/cli.py
index f23fff9..d803eb5 100644
--- a/gitlab/cli.py
+++ b/gitlab/cli.py
@@ -17,8 +17,8 @@
# 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 functools
import importlib
import re
import sys
@@ -27,6 +27,36 @@ import gitlab.config
camel_re = re.compile('(.)([A-Z])')
+# custom_actions = {
+# cls: {
+# action: (mandatory_args, optional_args, in_obj),
+# },
+# }
+custom_actions = {}
+
+
+def register_custom_action(cls_name, mandatory=tuple(), optional=tuple()):
+ def wrap(f):
+ @functools.wraps(f)
+ def wrapped_f(*args, **kwargs):
+ return f(*args, **kwargs)
+
+ # in_obj defines whether the method belongs to the obj or the manager
+ in_obj = True
+ final_name = cls_name
+ if cls_name.endswith('Manager'):
+ final_name = cls_name.replace('Manager', '')
+ in_obj = False
+ if final_name not in custom_actions:
+ custom_actions[final_name] = {}
+
+ action = f.__name__
+
+ custom_actions[final_name][action] = (mandatory, optional, in_obj)
+
+ return wrapped_f
+ return wrap
+
def die(msg, e=None):
if e:
@@ -51,6 +81,9 @@ def _get_base_parser():
parser.add_argument("-v", "--verbose", "--fancy",
help="Verbose mode",
action="store_true")
+ parser.add_argument("-d", "--debug",
+ help="Debug mode (display HTTP requests",
+ action="store_true")
parser.add_argument("-c", "--config-file", action='append',
help=("Configuration file to use. Can be used "
"multiple times."))
@@ -84,12 +117,13 @@ def main():
config_files = args.config_file
gitlab_id = args.gitlab
verbose = args.verbose
+ debug = args.debug
action = args.action
what = args.what
args = args.__dict__
# Remove CLI behavior-related args
- for item in ('gitlab', 'config_file', 'verbose', 'what', 'action',
+ for item in ('gitlab', 'config_file', 'verbose', 'debug', 'what', 'action',
'version'):
args.pop(item)
args = {k: v for k, v in args.items() if v is not None}
@@ -100,6 +134,9 @@ def main():
except Exception as e:
die(str(e))
+ if debug:
+ gl.enable_debug()
+
cli_module.run(gl, what, action, args, verbose)
sys.exit(0)
diff --git a/gitlab/v4/cli.py b/gitlab/v4/cli.py
new file mode 100644
index 0000000..821a27d
--- /dev/null
+++ b/gitlab/v4/cli.py
@@ -0,0 +1,296 @@
+#!/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
+import inspect
+import operator
+
+import six
+
+import gitlab
+import gitlab.base
+from gitlab import cli
+import gitlab.v4.objects
+
+
+class GitlabCLI(object):
+ def __init__(self, gl, what, action, args):
+ self.cls_name = cli.what_to_cls(what)
+ self.cls = gitlab.v4.objects.__dict__[self.cls_name]
+ self.what = what.replace('-', '_')
+ self.action = action.lower().replace('-', '')
+ self.gl = gl
+ self.args = args
+ self.mgr_cls = getattr(gitlab.v4.objects,
+ self.cls.__name__ + 'Manager')
+ # We could do something smart, like splitting the manager name to find
+ # parents, build the chain of managers to get to the final object.
+ # Instead we do something ugly and efficient: interpolate variables in
+ # the class _path attribute, and replace the value with the result.
+ self.mgr_cls._path = self.mgr_cls._path % self.args
+ self.mgr = self.mgr_cls(gl)
+
+ def __call__(self):
+ method = 'do_%s' % self.action
+ if hasattr(self, method):
+ return getattr(self, method)()
+ else:
+ return self.do_custom()
+
+ def do_custom(self):
+ in_obj = cli.custom_actions[self.cls_name][self.action][2]
+
+ # Get the object (lazy), then act
+ if in_obj:
+ data = {}
+ if hasattr(self.mgr, '_from_parent_attrs'):
+ for k in self.mgr._from_parent_attrs:
+ data[k] = self.args[k]
+ if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.cls):
+ data[self.cls._id_attr] = self.args.pop(self.cls._id_attr)
+ o = self.cls(self.mgr, data)
+ return getattr(o, self.action)(**self.args)
+ else:
+ return getattr(self.mgr, self.action)(**self.args)
+
+ def do_create(self):
+ try:
+ return self.mgr.create(self.args)
+ except Exception as e:
+ cli.die("Impossible to create object", e)
+
+ def do_list(self):
+ try:
+ return self.mgr.list(**self.args)
+ except Exception as e:
+ cli.die("Impossible to list objects", e)
+
+ def do_get(self):
+ id = None
+ if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.cls):
+ id = self.args.pop(self.cls._id_attr)
+
+ try:
+ return self.mgr.get(id, **self.args)
+ except Exception as e:
+ cli.die("Impossible to get object", e)
+
+ def do_delete(self):
+ id = self.args.pop(self.cls._id_attr)
+ try:
+ self.mgr.delete(id, **self.args)
+ except Exception as e:
+ cli.die("Impossible to destroy object", e)
+
+ def do_update(self):
+ id = self.args.pop(self.cls._id_attr)
+ try:
+ return self.mgr.update(id, self.args)
+ except Exception as e:
+ cli.die("Impossible to update object", e)
+
+
+def _populate_sub_parser_by_class(cls, sub_parser):
+ mgr_cls_name = cls.__name__ + 'Manager'
+ mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name)
+
+ for action_name in ['list', 'get', 'create', 'update', 'delete']:
+ if not hasattr(mgr_cls, action_name):
+ continue
+
+ sub_parser_action = sub_parser.add_parser(action_name)
+ if hasattr(mgr_cls, '_from_parent_attrs'):
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in mgr_cls._from_parent_attrs]
+ sub_parser_action.add_argument("--sudo", required=False)
+
+ if action_name == "list":
+ if hasattr(mgr_cls, '_list_filters'):
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=False)
+ for x in mgr_cls._list_filters]
+
+ 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 == 'delete':
+ id_attr = cls._id_attr.replace('_', '-')
+ sub_parser_action.add_argument("--%s" % id_attr, required=True)
+
+ if action_name == "get":
+ if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(cls):
+ if cls._id_attr is not None:
+ id_attr = cls._id_attr.replace('_', '-')
+ sub_parser_action.add_argument("--%s" % id_attr,
+ required=True)
+
+ if hasattr(mgr_cls, '_optional_get_attrs'):
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=False)
+ for x in mgr_cls._optional_get_attrs]
+
+ if action_name == "create":
+ if hasattr(mgr_cls, '_create_attrs'):
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in mgr_cls._create_attrs[0] if x != cls._id_attr]
+
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=False)
+ for x in mgr_cls._create_attrs[1] if x != cls._id_attr]
+
+ if action_name == "update":
+ if cls._id_attr is not None:
+ id_attr = cls._id_attr.replace('_', '-')
+ sub_parser_action.add_argument("--%s" % id_attr,
+ required=True)
+
+ if hasattr(mgr_cls, '_update_attrs'):
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in mgr_cls._update_attrs[0] if x != cls._id_attr]
+
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=False)
+ for x in mgr_cls._update_attrs[1] if x != cls._id_attr]
+
+ if cls.__name__ in cli.custom_actions:
+ name = cls.__name__
+ for action_name in cli.custom_actions[name]:
+ sub_parser_action = sub_parser.add_parser(action_name)
+ # Get the attributes for URL/path construction
+ if hasattr(mgr_cls, '_from_parent_attrs'):
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in mgr_cls._from_parent_attrs]
+ sub_parser_action.add_argument("--sudo", required=False)
+
+ # We need to get the object somehow
+ if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(cls):
+ if cls._id_attr is not None:
+ id_attr = cls._id_attr.replace('_', '-')
+ sub_parser_action.add_argument("--%s" % id_attr,
+ required=True)
+
+ required, optional, dummy = cli.custom_actions[name][action_name]
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in required if x != cls._id_attr]
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=False)
+ for x in optional if x != cls._id_attr]
+
+ if mgr_cls.__name__ in cli.custom_actions:
+ name = mgr_cls.__name__
+ for action_name in cli.custom_actions[name]:
+ sub_parser_action = sub_parser.add_parser(action_name)
+ if hasattr(mgr_cls, '_from_parent_attrs'):
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in mgr_cls._from_parent_attrs]
+ sub_parser_action.add_argument("--sudo", required=False)
+
+ required, optional, dummy = cli.custom_actions[name][action_name]
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in required if x != cls._id_attr]
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=False)
+ for x in optional if x != cls._id_attr]
+
+
+def extend_parser(parser):
+ 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.v4.objects.__dict__.values():
+ try:
+ if gitlab.base.RESTManager in inspect.getmro(cls):
+ if cls._obj_cls is not None:
+ classes.append(cls._obj_cls)
+ except AttributeError:
+ pass
+ classes.sort(key=operator.attrgetter("__name__"))
+
+ for cls in classes:
+ arg_name = cli.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
+
+
+class LegacyPrinter(object):
+ def display(self, obj, verbose=False, padding=0):
+ def display_dict(d):
+ for k in sorted(d.keys()):
+ v = d[k]
+ if isinstance(v, dict):
+ print('%s%s:' % (' ' * padding, k))
+ new_padding = padding + 2
+ self.display(v, True, new_padding)
+ continue
+ print('%s%s: %s' % (' ' * padding, k, v))
+
+ if verbose:
+ if isinstance(obj, dict):
+ display_dict(obj)
+ return
+
+ # not a dict, we assume it's a RESTObject
+ id = getattr(obj, obj._id_attr)
+ print('%s: %s' % (obj._id_attr, id))
+ attrs = obj.attributes
+ attrs.pop(obj._id_attr)
+ display_dict(attrs)
+ print('')
+
+ else:
+ id = getattr(obj, obj._id_attr)
+ print('%s: %s' % (obj._id_attr, id))
+ if hasattr(obj, '_short_print_attr'):
+ value = getattr(obj, obj._short_print_attr)
+ print('%s: %s' % (obj._short_print_attr, value))
+
+
+def run(gl, what, action, args, verbose):
+ g_cli = GitlabCLI(gl, what, action, args)
+ ret_val = g_cli()
+
+ printer = LegacyPrinter()
+
+ if isinstance(ret_val, list):
+ for o in ret_val:
+ if isinstance(o, gitlab.base.RESTObject):
+ printer.display(o, verbose)
+ else:
+ print(o)
+ elif isinstance(ret_val, gitlab.base.RESTObject):
+ printer.display(ret_val, verbose)
+ elif isinstance(ret_val, six.string_types):
+ print(ret_val)
diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py
index 03d75bf..641db82 100644
--- a/gitlab/v4/objects.py
+++ b/gitlab/v4/objects.py
@@ -22,6 +22,7 @@ import base64
import six
from gitlab.base import * # noqa
+from gitlab import cli
from gitlab.exceptions import * # noqa
from gitlab.mixins import * # noqa
from gitlab import utils
@@ -44,6 +45,7 @@ class SidekiqManager(RESTManager):
for the sidekiq metrics API.
"""
+ @cli.register_custom_action('SidekiqManager')
@exc.on_http_error(exc.GitlabGetError)
def queue_metrics(self, **kwargs):
"""Return the registred queues information.
@@ -60,6 +62,7 @@ class SidekiqManager(RESTManager):
"""
return self.gitlab.http_get('/sidekiq/queue_metrics', **kwargs)
+ @cli.register_custom_action('SidekiqManager')
@exc.on_http_error(exc.GitlabGetError)
def process_metrics(self, **kwargs):
"""Return the registred sidekiq workers.
@@ -76,6 +79,7 @@ class SidekiqManager(RESTManager):
"""
return self.gitlab.http_get('/sidekiq/process_metrics', **kwargs)
+ @cli.register_custom_action('SidekiqManager')
@exc.on_http_error(exc.GitlabGetError)
def job_stats(self, **kwargs):
"""Return statistics about the jobs performed.
@@ -92,6 +96,7 @@ class SidekiqManager(RESTManager):
"""
return self.gitlab.http_get('/sidekiq/job_stats', **kwargs)
+ @cli.register_custom_action('SidekiqManager')
@exc.on_http_error(exc.GitlabGetError)
def compound_metrics(self, **kwargs):
"""Return all available metrics and statistics.
@@ -156,6 +161,7 @@ class User(SaveMixin, ObjectDeleteMixin, RESTObject):
('projects', 'UserProjectManager'),
)
+ @cli.register_custom_action('User')
@exc.on_http_error(exc.GitlabBlockError)
def block(self, **kwargs):
"""Block the user.
@@ -176,6 +182,7 @@ class User(SaveMixin, ObjectDeleteMixin, RESTObject):
self._attrs['state'] = 'blocked'
return server_data
+ @cli.register_custom_action('User')
@exc.on_http_error(exc.GitlabUnblockError)
def unblock(self, **kwargs):
"""Unblock the user.
@@ -440,6 +447,7 @@ class Snippet(SaveMixin, ObjectDeleteMixin, RESTObject):
_constructor_types = {'author': 'User'}
_short_print_attr = 'title'
+ @cli.register_custom_action('Snippet')
@exc.on_http_error(exc.GitlabGetError)
def content(self, streamed=False, action=None, chunk_size=1024, **kwargs):
"""Return the content of a snippet.
@@ -474,6 +482,7 @@ class SnippetManager(CRUDMixin, RESTManager):
_update_attrs = (tuple(),
('title', 'file_name', 'content', 'visibility'))
+ @cli.register_custom_action('SnippetManager')
def public(self, **kwargs):
"""List all the public snippets.
@@ -528,6 +537,9 @@ class ProjectBranch(ObjectDeleteMixin, RESTObject):
_constructor_types = {'author': 'User', "committer": "User"}
_id_attr = 'name'
+ @cli.register_custom_action('ProjectBranch', tuple(),
+ ('developers_can_push',
+ 'developers_can_merge'))
@exc.on_http_error(exc.GitlabProtectError)
def protect(self, developers_can_push=False, developers_can_merge=False,
**kwargs):
@@ -550,6 +562,7 @@ class ProjectBranch(ObjectDeleteMixin, RESTObject):
self.manager.gitlab.http_put(path, post_data=post_data, **kwargs)
self._attrs['protected'] = True
+ @cli.register_custom_action('ProjectBranch')
@exc.on_http_error(exc.GitlabProtectError)
def unprotect(self, **kwargs):
"""Unprotect the branch.
@@ -578,6 +591,7 @@ class ProjectJob(RESTObject):
'commit': 'ProjectCommit',
'runner': 'Runner'}
+ @cli.register_custom_action('ProjectJob')
@exc.on_http_error(exc.GitlabJobCancelError)
def cancel(self, **kwargs):
"""Cancel the job.
@@ -592,6 +606,7 @@ class ProjectJob(RESTObject):
path = '%s/%s/cancel' % (self.manager.path, self.get_id())
self.manager.gitlab.http_post(path)
+ @cli.register_custom_action('ProjectJob')
@exc.on_http_error(exc.GitlabJobRetryError)
def retry(self, **kwargs):
"""Retry the job.
@@ -606,6 +621,7 @@ class ProjectJob(RESTObject):
path = '%s/%s/retry' % (self.manager.path, self.get_id())
self.manager.gitlab.http_post(path)
+ @cli.register_custom_action('ProjectJob')
@exc.on_http_error(exc.GitlabJobPlayError)
def play(self, **kwargs):
"""Trigger a job explicitly.
@@ -620,6 +636,7 @@ class ProjectJob(RESTObject):
path = '%s/%s/play' % (self.manager.path, self.get_id())
self.manager.gitlab.http_post(path)
+ @cli.register_custom_action('ProjectJob')
@exc.on_http_error(exc.GitlabJobEraseError)
def erase(self, **kwargs):
"""Erase the job (remove job artifacts and trace).
@@ -634,6 +651,7 @@ class ProjectJob(RESTObject):
path = '%s/%s/erase' % (self.manager.path, self.get_id())
self.manager.gitlab.http_post(path)
+ @cli.register_custom_action('ProjectJob')
@exc.on_http_error(exc.GitlabCreateError)
def keep_artifacts(self, **kwargs):
"""Prevent artifacts from being deleted when expiration is set.
@@ -648,6 +666,7 @@ class ProjectJob(RESTObject):
path = '%s/%s/artifacts/keep' % (self.manager.path, self.get_id())
self.manager.gitlab.http_post(path)
+ @cli.register_custom_action('ProjectJob')
@exc.on_http_error(exc.GitlabGetError)
def artifacts(self, streamed=False, action=None, chunk_size=1024,
**kwargs):
@@ -674,6 +693,7 @@ class ProjectJob(RESTObject):
**kwargs)
return utils.response_content(result, streamed, action, chunk_size)
+ @cli.register_custom_action('ProjectJob')
@exc.on_http_error(exc.GitlabGetError)
def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs):
"""Get the job trace.
@@ -759,6 +779,7 @@ class ProjectCommit(RESTObject):
('statuses', 'ProjectCommitStatusManager'),
)
+ @cli.register_custom_action('ProjectCommit')
@exc.on_http_error(exc.GitlabGetError)
def diff(self, **kwargs):
"""Generate the commit diff.
@@ -776,6 +797,7 @@ class ProjectCommit(RESTObject):
path = '%s/%s/diff' % (self.manager.path, self.get_id())
return self.manager.gitlab.http_get(path, **kwargs)
+ @cli.register_custom_action('ProjectCommit', ('branch',))
@exc.on_http_error(exc.GitlabCherryPickError)
def cherry_pick(self, branch, **kwargs):
"""Cherry-pick a commit into a branch.
@@ -824,6 +846,7 @@ class ProjectKeyManager(NoUpdateMixin, RESTManager):
_from_parent_attrs = {'project_id': 'id'}
_create_attrs = (('title', 'key'), tuple())
+ @cli.register_custom_action('ProjectKeyManager', ('key_id',))
@exc.on_http_error(exc.GitlabProjectDeployKeyError)
def enable(self, key_id, **kwargs):
"""Enable a deploy key for a project.
@@ -891,7 +914,7 @@ class ProjectIssueNoteManager(CRUDMixin, RESTManager):
_path = '/projects/%(project_id)s/issues/%(issue_iid)s/notes'
_obj_cls = ProjectIssueNote
_from_parent_attrs = {'project_id': 'project_id', 'issue_iid': 'iid'}
- _create_attrs = (('body', ), ('created_at'))
+ _create_attrs = (('body', ), ('created_at', ))
_update_attrs = (('body', ), tuple())
@@ -903,6 +926,7 @@ class ProjectIssue(SubscribableMixin, TodoMixin, TimeTrackingMixin, SaveMixin,
_id_attr = 'iid'
_managers = (('notes', 'ProjectIssueNoteManager'), )
+ @cli.register_custom_action('ProjectIssue', ('to_project_id',))
@exc.on_http_error(exc.GitlabUpdateError)
def move(self, to_project_id, **kwargs):
"""Move the issue to another project.
@@ -974,6 +998,7 @@ class ProjectTag(ObjectDeleteMixin, RESTObject):
_id_attr = 'name'
_short_print_attr = 'name'
+ @cli.register_custom_action('ProjectTag', ('description', ))
def set_release_description(self, description, **kwargs):
"""Set the release notes on the tag.
@@ -1048,6 +1073,7 @@ class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin,
('diffs', 'ProjectMergeRequestDiffManager')
)
+ @cli.register_custom_action('ProjectMergeRequest')
@exc.on_http_error(exc.GitlabMROnBuildSuccessError)
def cancel_merge_when_pipeline_succeeds(self, **kwargs):
"""Cancel merge when the pipeline succeeds.
@@ -1066,6 +1092,7 @@ class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin,
server_data = self.manager.gitlab.http_put(path, **kwargs)
self._update_attrs(server_data)
+ @cli.register_custom_action('ProjectMergeRequest')
@exc.on_http_error(exc.GitlabListError)
def closes_issues(self, **kwargs):
"""List issues that will close on merge."
@@ -1087,6 +1114,7 @@ class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin,
parent=self.manager._parent)
return RESTObjectList(manager, ProjectIssue, data_list)
+ @cli.register_custom_action('ProjectMergeRequest')
@exc.on_http_error(exc.GitlabListError)
def commits(self, **kwargs):
"""List the merge request commits.
@@ -1109,6 +1137,7 @@ class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin,
parent=self.manager._parent)
return RESTObjectList(manager, ProjectCommit, data_list)
+ @cli.register_custom_action('ProjectMergeRequest')
@exc.on_http_error(exc.GitlabListError)
def changes(self, **kwargs):
"""List the merge request changes.
@@ -1126,6 +1155,10 @@ class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin,
path = '%s/%s/changes' % (self.manager.path, self.get_id())
return self.manager.gitlab.http_get(path, **kwargs)
+ @cli.register_custom_action('ProjectMergeRequest', tuple(),
+ ('merge_commit_message',
+ 'should_remove_source_branch',
+ 'merge_when_pipeline_succeeds'))
@exc.on_http_error(exc.GitlabMRClosedError)
def merge(self, merge_commit_message=None,
should_remove_source_branch=False,
@@ -1177,6 +1210,7 @@ class ProjectMergeRequestManager(CRUDMixin, RESTManager):
class ProjectMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
_short_print_attr = 'title'
+ @cli.register_custom_action('ProjectMilestone')
@exc.on_http_error(exc.GitlabListError)
def issues(self, **kwargs):
"""List issues related to this milestone.
@@ -1200,6 +1234,7 @@ class ProjectMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
# FIXME(gpocentek): the computed manager path is not correct
return RESTObjectList(manager, ProjectIssue, data_list)
+ @cli.register_custom_action('ProjectMilestone')
@exc.on_http_error(exc.GitlabListError)
def merge_requests(self, **kwargs):
"""List the merge requests related to this milestone.
@@ -1399,6 +1434,7 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin,
data = {'branch': branch, 'commit_message': commit_message}
self.gitlab.http_delete(path, query_data=data, **kwargs)
+ @cli.register_custom_action('ProjectFileManager', ('file_path', 'ref'))
@exc.on_http_error(exc.GitlabGetError)
def raw(self, file_path, ref, streamed=False, action=None, chunk_size=1024,
**kwargs):
@@ -1431,6 +1467,7 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin,
class ProjectPipeline(RESTObject):
+ @cli.register_custom_action('ProjectPipeline')
@exc.on_http_error(exc.GitlabPipelineCancelError)
def cancel(self, **kwargs):
"""Cancel the job.
@@ -1445,6 +1482,7 @@ class ProjectPipeline(RESTObject):
path = '%s/%s/cancel' % (self.manager.path, self.get_id())
self.manager.gitlab.http_post(path)
+ @cli.register_custom_action('ProjectPipeline')
@exc.on_http_error(exc.GitlabPipelineRetryError)
def retry(self, **kwargs):
"""Retry the job.
@@ -1504,6 +1542,7 @@ class ProjectSnippet(SaveMixin, ObjectDeleteMixin, RESTObject):
_short_print_attr = 'title'
_managers = (('notes', 'ProjectSnippetNoteManager'), )
+ @cli.register_custom_action('ProjectSnippet')
@exc.on_http_error(exc.GitlabGetError)
def content(self, streamed=False, action=None, chunk_size=1024, **kwargs):
"""Return the content of a snippet.
@@ -1540,6 +1579,7 @@ class ProjectSnippetManager(CRUDMixin, RESTManager):
class ProjectTrigger(SaveMixin, ObjectDeleteMixin, RESTObject):
+ @cli.register_custom_action('ProjectTrigger')
def take_ownership(self, **kwargs):
"""Update the owner of a trigger."""
path = '%s/%s/take_ownership' % (self.manager.path, self.get_id())
@@ -1652,6 +1692,7 @@ class ProjectServiceManager(GetMixin, UpdateMixin, DeleteMixin, RESTManager):
super(ProjectServiceManager, self).update(id, new_data, **kwargs)
self.id = id
+ @cli.register_custom_action('ProjectServiceManager')
def available(self, **kwargs):
"""List the services known by python-gitlab.
@@ -1725,6 +1766,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
('variables', 'ProjectVariableManager'),
)
+ @cli.register_custom_action('Project', tuple(), ('path', 'ref'))
@exc.on_http_error(exc.GitlabGetError)
def repository_tree(self, path='', ref='', **kwargs):
"""Return a list of files in the repository.
@@ -1750,6 +1792,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
return self.manager.gitlab.http_get(gl_path, query_data=query_data,
**kwargs)
+ @cli.register_custom_action('Project', ('sha', ))
@exc.on_http_error(exc.GitlabGetError)
def repository_blob(self, sha, **kwargs):
"""Return a blob by blob SHA.
@@ -1769,6 +1812,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
path = '/projects/%s/repository/blobs/%s' % (self.get_id(), sha)
return self.manager.gitlab.http_get(path, **kwargs)
+ @cli.register_custom_action('Project', ('sha', ))
@exc.on_http_error(exc.GitlabGetError)
def repository_raw_blob(self, sha, streamed=False, action=None,
chunk_size=1024, **kwargs):
@@ -1796,6 +1840,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
**kwargs)
return utils.response_content(result, streamed, action, chunk_size)
+ @cli.register_custom_action('Project', ('from_', 'to'))
@exc.on_http_error(exc.GitlabGetError)
def repository_compare(self, from_, to, **kwargs):
"""Return a diff between two branches/commits.
@@ -1817,6 +1862,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
return self.manager.gitlab.http_get(path, query_data=query_data,
**kwargs)
+ @cli.register_custom_action('Project')
@exc.on_http_error(exc.GitlabGetError)
def repository_contributors(self, **kwargs):
"""Return a list of contributors for the project.
@@ -1834,6 +1880,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
path = '/projects/%s/repository/contributors' % self.get_id()
return self.manager.gitlab.http_get(path, **kwargs)
+ @cli.register_custom_action('Project', tuple(), ('sha', ))
@exc.on_http_error(exc.GitlabListError)
def repository_archive(self, sha=None, streamed=False, action=None,
chunk_size=1024, **kwargs):
@@ -1864,6 +1911,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
streamed=streamed, **kwargs)
return utils.response_content(result, streamed, action, chunk_size)
+ @cli.register_custom_action('Project', ('forked_from_id', ))
@exc.on_http_error(exc.GitlabCreateError)
def create_fork_relation(self, forked_from_id, **kwargs):
"""Create a forked from/to relation between existing projects.
@@ -1879,6 +1927,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
path = '/projects/%s/fork/%s' % (self.get_id(), forked_from_id)
self.manager.gitlab.http_post(path, **kwargs)
+ @cli.register_custom_action('Project')
@exc.on_http_error(exc.GitlabDeleteError)
def delete_fork_relation(self, **kwargs):
"""Delete a forked relation between existing projects.
@@ -1893,6 +1942,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
path = '/projects/%s/fork' % self.get_id()
self.manager.gitlab.http_delete(path, **kwargs)
+ @cli.register_custom_action('Project')
@exc.on_http_error(exc.GitlabCreateError)
def star(self, **kwargs):
"""Star a project.
@@ -1908,6 +1958,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
server_data = self.manager.gitlab.http_post(path, **kwargs)
self._update_attrs(server_data)
+ @cli.register_custom_action('Project')
@exc.on_http_error(exc.GitlabDeleteError)
def unstar(self, **kwargs):
"""Unstar a project.
@@ -1923,6 +1974,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
server_data = self.manager.gitlab.http_post(path, **kwargs)
self._update_attrs(server_data)
+ @cli.register_custom_action('Project')
@exc.on_http_error(exc.GitlabCreateError)
def archive(self, **kwargs):
"""Archive a project.
@@ -1938,6 +1990,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
server_data = self.manager.gitlab.http_post(path, **kwargs)
self._update_attrs(server_data)
+ @cli.register_custom_action('Project')
@exc.on_http_error(exc.GitlabDeleteError)
def unarchive(self, **kwargs):
"""Unarchive a project.
@@ -1953,6 +2006,8 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
server_data = self.manager.gitlab.http_post(path, **kwargs)
self._update_attrs(server_data)
+ @cli.register_custom_action('Project', ('group_id', 'group_access'),
+ ('expires_at', ))
@exc.on_http_error(exc.GitlabCreateError)
def share(self, group_id, group_access, expires_at=None, **kwargs):
"""Share the project with a group.
@@ -1972,6 +2027,8 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
'expires_at': expires_at}
self.manager.gitlab.http_post(path, post_data=data, **kwargs)
+ # variables not supported in CLI
+ @cli.register_custom_action('Project', ('ref', 'token'))
@exc.on_http_error(exc.GitlabCreateError)
def trigger_pipeline(self, ref, token, variables={}, **kwargs):
"""Trigger a CI build.
@@ -2005,6 +2062,7 @@ class RunnerManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager):
_update_attrs = (tuple(), ('description', 'active', 'tag_list'))
_list_filters = ('scope', )
+ @cli.register_custom_action('RunnerManager', tuple(), ('scope', ))
@exc.on_http_error(exc.GitlabListError)
def all(self, scope=None, **kwargs):
"""List all the runners.
@@ -2034,6 +2092,7 @@ class RunnerManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager):
class Todo(ObjectDeleteMixin, RESTObject):
+ @cli.register_custom_action('Todo')
@exc.on_http_error(exc.GitlabTodoError)
def mark_as_done(self, **kwargs):
"""Mark the todo as done.
@@ -2055,6 +2114,7 @@ class TodoManager(GetFromListMixin, DeleteMixin, RESTManager):
_obj_cls = Todo
_list_filters = ('action', 'author_id', 'project_id', 'state', 'type')
+ @cli.register_custom_action('TodoManager')
@exc.on_http_error(exc.GitlabTodoError)
def mark_all_as_done(self, **kwargs):
"""Mark all the todos as done.
@@ -2126,19 +2186,20 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
('issues', 'GroupIssueManager'),
)
+ @cli.register_custom_action('Group', ('to_project_id', ))
@exc.on_http_error(exc.GitlabTransferProjectError)
- def transfer_project(self, id, **kwargs):
+ def transfer_project(self, to_project_id, **kwargs):
"""Transfer a project to this group.
Args:
- id (int): ID of the project to transfer
+ to_project_id (int): ID of the project to transfer
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabTransferProjectError: If the project could not be transfered
"""
- path = '/groups/%d/projects/%d' % (self.id, id)
+ path = '/groups/%d/projects/%d' % (self.id, project_id)
self.manager.gitlab.http_post(path, **kwargs)