summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/api-usage.rst4
-rw-r--r--docs/gl_objects/commits.py4
-rw-r--r--docs/gl_objects/commits.rst6
-rw-r--r--docs/gl_objects/projects.py6
-rw-r--r--docs/gl_objects/projects.rst6
-rw-r--r--gitlab/__init__.py30
-rw-r--r--gitlab/cli.py11
-rw-r--r--gitlab/exceptions.py4
-rw-r--r--gitlab/objects.py33
-rw-r--r--gitlab/tests/test_gitlab.py70
10 files changed, 156 insertions, 18 deletions
diff --git a/docs/api-usage.rst b/docs/api-usage.rst
index a15aecb..7b7ab78 100644
--- a/docs/api-usage.rst
+++ b/docs/api-usage.rst
@@ -142,7 +142,9 @@ parameter to get all the items when using listing methods:
python-gitlab will iterate over the list by calling the correspnding API
multiple times. This might take some time if you have a lot of items to
retrieve. This might also consume a lot of memory as all the items will be
- stored in RAM.
+ stored in RAM. If you're encountering the python recursion limit exception,
+ use ``safe_all=True`` instead to stop pagination automatically if the
+ recursion limit is hit.
Sudo
====
diff --git a/docs/gl_objects/commits.py b/docs/gl_objects/commits.py
index 2ed66f5..0d47edb 100644
--- a/docs/gl_objects/commits.py
+++ b/docs/gl_objects/commits.py
@@ -39,6 +39,10 @@ commit = project.commits.get('e3d5a71b')
diff = commit.diff()
# end diff
+# cherry
+commit.cherry_pick(branch='target_branch')
+# end cherry
+
# comments list
comments = gl.project_commit_comments.list(project_id=1, commit_id='master')
# or
diff --git a/docs/gl_objects/commits.rst b/docs/gl_objects/commits.rst
index 8be1b86..6fef8bf 100644
--- a/docs/gl_objects/commits.rst
+++ b/docs/gl_objects/commits.rst
@@ -43,6 +43,12 @@ Get the diff for a commit:
:start-after: # diff
:end-before: # end diff
+Cherry-pick a commit into another branch:
+
+.. literalinclude:: commits.py
+ :start-after: # cherry
+ :end-before: # end cherry
+
Commit comments
===============
diff --git a/docs/gl_objects/projects.py b/docs/gl_objects/projects.py
index 54bde84..4412f22 100644
--- a/docs/gl_objects/projects.py
+++ b/docs/gl_objects/projects.py
@@ -400,6 +400,12 @@ pipeline = gl.project_pipelines.get(pipeline_id, project_id=1)
pipeline = project.pipelines.get(pipeline_id)
# end pipeline get
+# pipeline create
+pipeline = gl.project_pipelines.create({'project_id': 1, 'ref': 'master'})
+# or
+pipeline = project.pipelines.create({'ref': 'master'})
+# end pipeline create
+
# pipeline retry
pipeline.retry()
# end pipeline retry
diff --git a/docs/gl_objects/projects.rst b/docs/gl_objects/projects.rst
index dc6c48b..300b848 100644
--- a/docs/gl_objects/projects.rst
+++ b/docs/gl_objects/projects.rst
@@ -438,6 +438,12 @@ Cancel builds in a pipeline:
:start-after: # pipeline cancel
:end-before: # end pipeline cancel
+Create a pipeline for a particular reference:
+
+.. literalinclude:: projects.py
+ :start-after: # pipeline create
+ :end-before: # end pipeline create
+
Services
--------
diff --git a/gitlab/__init__.py b/gitlab/__init__.py
index 721106c..421b9eb 100644
--- a/gitlab/__init__.py
+++ b/gitlab/__init__.py
@@ -112,8 +112,8 @@ class Gitlab(object):
# build the "submanagers"
for parent_cls in six.itervalues(globals()):
if (not inspect.isclass(parent_cls)
- or not issubclass(parent_cls, GitlabObject)
- or parent_cls == CurrentUser):
+ or not issubclass(parent_cls, GitlabObject)
+ or parent_cls == CurrentUser):
continue
if not parent_cls.managers:
@@ -312,11 +312,13 @@ class Gitlab(object):
params = extra_attrs.copy()
params.update(kwargs.copy())
- get_all_results = kwargs.get('all', False)
+ catch_recursion_limit = kwargs.get('safe_all', False)
+ get_all_results = (kwargs.get('all', False) is True
+ or catch_recursion_limit)
# Remove these keys to avoid breaking the listing (urls will get too
# long otherwise)
- for key in ['all', 'next_url']:
+ for key in ['all', 'next_url', 'safe_all']:
if key in params:
del params[key]
@@ -334,12 +336,20 @@ class Gitlab(object):
results = [cls(self, item, **params) for item in r.json()
if item is not None]
- if ('next' in r.links and 'url' in r.links['next']
- and get_all_results is True):
- args = kwargs.copy()
- args.update(extra_attrs)
- args['next_url'] = r.links['next']['url']
- results.extend(self.list(cls, **args))
+ try:
+ if ('next' in r.links and 'url' in r.links['next']
+ and get_all_results):
+ args = kwargs.copy()
+ args.update(extra_attrs)
+ args['next_url'] = r.links['next']['url']
+ results.extend(self.list(cls, **args))
+ except Exception as e:
+ # Catch the recursion limit exception if the 'safe_all'
+ # kwarg was provided
+ if not (catch_recursion_limit and
+ "maximum recursion depth exceeded" in str(e)):
+ raise e
+
return results
def _raw_post(self, path_, data=None, content_type=None, **kwargs):
diff --git a/gitlab/cli.py b/gitlab/cli.py
index 32b3ec8..2a41907 100644
--- a/gitlab/cli.py
+++ b/gitlab/cli.py
@@ -42,7 +42,9 @@ EXTRA_ACTIONS = {
gitlab.ProjectCommit: {'diff': {'required': ['id', 'project-id']},
'blob': {'required': ['id', 'project-id',
'filepath']},
- 'builds': {'required': ['id', 'project-id']}},
+ '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',
@@ -267,6 +269,13 @@ class GitlabCLI(object):
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)
diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py
index 11bbe26..fc901d1 100644
--- a/gitlab/exceptions.py
+++ b/gitlab/exceptions.py
@@ -147,6 +147,10 @@ class GitlabTimeTrackingError(GitlabOperationError):
pass
+class GitlabCherryPickError(GitlabOperationError):
+ pass
+
+
def raise_error_from_response(response, error, expected_code=200):
"""Tries to parse gitlab error message from response and raises error.
diff --git a/gitlab/objects.py b/gitlab/objects.py
index e83e618..4a84a71 100644
--- a/gitlab/objects.py
+++ b/gitlab/objects.py
@@ -1086,7 +1086,7 @@ class ProjectBranch(GitlabObject):
requiredCreateAttrs = ['branch_name', 'ref']
def protect(self, protect=True, **kwargs):
- """Protects the project."""
+ """Protects the branch."""
url = self._url % {'project_id': self.project_id}
action = 'protect' if protect else 'unprotect'
url = "%s/%s/%s" % (url, self.name, action)
@@ -1099,7 +1099,7 @@ class ProjectBranch(GitlabObject):
del self.protected
def unprotect(self, **kwargs):
- """Unprotects the project."""
+ """Unprotects the branch."""
self.protect(False, **kwargs)
@@ -1302,6 +1302,23 @@ class ProjectCommit(GitlabObject):
{'project_id': self.project_id},
**kwargs)
+ def cherry_pick(self, branch, **kwargs):
+ """Cherry-pick a commit into a branch.
+
+ Args:
+ branch (str): Name of target branch.
+
+ Raises:
+ GitlabCherryPickError: If the cherry pick could not be applied.
+ """
+ url = ('/projects/%s/repository/commits/%s/cherry_pick' %
+ (self.project_id, self.id))
+
+ r = self.gitlab._raw_post(url, data={'project_id': self.project_id,
+ 'branch': branch}, **kwargs)
+ errors = {400: GitlabCherryPickError}
+ raise_error_from_response(r, errors, expected_code=201)
+
class ProjectCommitManager(BaseManager):
obj_cls = ProjectCommit
@@ -1516,7 +1533,7 @@ class ProjectIssue(GitlabObject):
GitlabConnectionError: If the server cannot be reached.
"""
url = ('/projects/%(project_id)s/issues/%(issue_id)s/'
- 'reset_spent_time' %
+ 'add_spent_time' %
{'project_id': self.project_id, 'issue_id': self.id})
r = self.gitlab._raw_post(url, **kwargs)
raise_error_from_response(r, GitlabTimeTrackingError, 200)
@@ -1529,7 +1546,7 @@ class ProjectIssue(GitlabObject):
GitlabConnectionError: If the server cannot be reached.
"""
url = ('/projects/%(project_id)s/issues/%(issue_id)s/'
- 'add_spent_time' %
+ 'reset_spent_time' %
{'project_id': self.project_id, 'issue_id': self.id})
r = self.gitlab._raw_post(url, **kwargs)
raise_error_from_response(r, GitlabTimeTrackingError, 200)
@@ -1656,7 +1673,7 @@ class ProjectMergeRequest(GitlabObject):
requiredUrlAttrs = ['project_id']
requiredCreateAttrs = ['source_branch', 'target_branch', 'title']
optionalCreateAttrs = ['assignee_id', 'description', 'target_project_id',
- 'labels', 'milestone_id']
+ 'labels', 'milestone_id', 'remove_source_branch']
optionalUpdateAttrs = ['target_branch', 'assignee_id', 'title',
'description', 'state_event', 'labels',
'milestone_id']
@@ -1919,10 +1936,14 @@ class ProjectFileManager(BaseManager):
class ProjectPipeline(GitlabObject):
_url = '/projects/%(project_id)s/pipelines'
- canCreate = False
+ _create_url = '/projects/%(project_id)s/pipeline'
+
canUpdate = False
canDelete = False
+ requiredUrlAttrs = ['project_id']
+ requiredCreateAttrs = ['ref']
+
def retry(self, **kwargs):
"""Retries failed builds in a pipeline.
diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py
index 4adf07f..4670def 100644
--- a/gitlab/tests/test_gitlab.py
+++ b/gitlab/tests/test_gitlab.py
@@ -26,6 +26,7 @@ except ImportError:
from httmock import HTTMock # noqa
from httmock import response # noqa
from httmock import urlmatch # noqa
+import six
import gitlab
from gitlab import * # noqa
@@ -243,6 +244,75 @@ class TestGitlabMethods(unittest.TestCase):
self.assertEqual(data[0].ref, "b")
self.assertEqual(len(data), 2)
+ def test_list_recursion_limit_caught(self):
+ @urlmatch(scheme="http", netloc="localhost",
+ path='/api/v3/projects/1/repository/branches', method="get")
+ def resp_one(url, request):
+ """First request:
+
+ http://localhost/api/v3/projects/1/repository/branches?per_page=1
+ """
+ headers = {
+ 'content-type': 'application/json',
+ 'link': '<http://localhost/api/v3/projects/1/repository/branc'
+ 'hes?page=2&per_page=0>; rel="next", <http://localhost/api/v3'
+ '/projects/1/repository/branches?page=2&per_page=0>; rel="las'
+ 't", <http://localhost/api/v3/projects/1/repository/branches?'
+ 'page=1&per_page=0>; rel="first"'
+ }
+ content = ('[{"branch_name": "otherbranch", '
+ '"project_id": 1, "ref": "b"}]').encode("utf-8")
+ resp = response(200, content, headers, None, 5, request)
+ return resp
+
+ @urlmatch(scheme="http", netloc="localhost",
+ path='/api/v3/projects/1/repository/branches', method="get",
+ query=r'.*page=2.*')
+ def resp_two(url, request):
+ # Mock a runtime error
+ raise RuntimeError("maximum recursion depth exceeded")
+
+ with HTTMock(resp_two, resp_one):
+ data = self.gl.list(ProjectBranch, project_id=1, per_page=1,
+ safe_all=True)
+ self.assertEqual(data[0].branch_name, "otherbranch")
+ self.assertEqual(data[0].project_id, 1)
+ self.assertEqual(data[0].ref, "b")
+ self.assertEqual(len(data), 1)
+
+ def test_list_recursion_limit_not_caught(self):
+ @urlmatch(scheme="http", netloc="localhost",
+ path='/api/v3/projects/1/repository/branches', method="get")
+ def resp_one(url, request):
+ """First request:
+
+ http://localhost/api/v3/projects/1/repository/branches?per_page=1
+ """
+ headers = {
+ 'content-type': 'application/json',
+ 'link': '<http://localhost/api/v3/projects/1/repository/branc'
+ 'hes?page=2&per_page=0>; rel="next", <http://localhost/api/v3'
+ '/projects/1/repository/branches?page=2&per_page=0>; rel="las'
+ 't", <http://localhost/api/v3/projects/1/repository/branches?'
+ 'page=1&per_page=0>; rel="first"'
+ }
+ content = ('[{"branch_name": "otherbranch", '
+ '"project_id": 1, "ref": "b"}]').encode("utf-8")
+ resp = response(200, content, headers, None, 5, request)
+ return resp
+
+ @urlmatch(scheme="http", netloc="localhost",
+ path='/api/v3/projects/1/repository/branches', method="get",
+ query=r'.*page=2.*')
+ def resp_two(url, request):
+ # Mock a runtime error
+ raise RuntimeError("maximum recursion depth exceeded")
+
+ with HTTMock(resp_two, resp_one):
+ with six.assertRaisesRegex(self, GitlabError,
+ "(maximum recursion depth exceeded)"):
+ self.gl.list(ProjectBranch, project_id=1, per_page=1, all=True)
+
def test_list_401(self):
@urlmatch(scheme="http", netloc="localhost",
path="/api/v3/projects/1/repository/branches", method="get")