diff options
-rw-r--r-- | docs/api-usage.rst | 4 | ||||
-rw-r--r-- | docs/gl_objects/commits.py | 4 | ||||
-rw-r--r-- | docs/gl_objects/commits.rst | 6 | ||||
-rw-r--r-- | docs/gl_objects/projects.py | 6 | ||||
-rw-r--r-- | docs/gl_objects/projects.rst | 6 | ||||
-rw-r--r-- | gitlab/__init__.py | 30 | ||||
-rw-r--r-- | gitlab/cli.py | 11 | ||||
-rw-r--r-- | gitlab/exceptions.py | 4 | ||||
-rw-r--r-- | gitlab/objects.py | 33 | ||||
-rw-r--r-- | gitlab/tests/test_gitlab.py | 70 |
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") |