diff options
-rw-r--r-- | README.rst | 2 | ||||
-rw-r--r-- | docs/api-objects.rst | 1 | ||||
-rw-r--r-- | docs/api-usage.rst | 22 | ||||
-rw-r--r-- | docs/gl_objects/access_requests.rst | 4 | ||||
-rw-r--r-- | docs/gl_objects/applications.rst | 31 | ||||
-rw-r--r-- | docs/gl_objects/commits.rst | 4 | ||||
-rw-r--r-- | docs/gl_objects/features.rst | 2 | ||||
-rw-r--r-- | docs/gl_objects/projects.rst | 4 | ||||
-rw-r--r-- | docs/gl_objects/users.rst | 27 | ||||
-rw-r--r-- | gitlab/__init__.py | 13 | ||||
-rw-r--r-- | gitlab/exceptions.py | 4 | ||||
-rw-r--r-- | gitlab/mixins.py | 5 | ||||
-rw-r--r-- | gitlab/tests/test_gitlab.py | 120 | ||||
-rw-r--r-- | gitlab/utils.py | 4 | ||||
-rw-r--r-- | gitlab/v4/objects.py | 70 | ||||
-rwxr-xr-x | tools/build_test_env.sh | 17 | ||||
-rwxr-xr-x | tools/cli_test_v4.sh | 13 | ||||
-rw-r--r-- | tools/python_test_v4.py | 46 |
18 files changed, 364 insertions, 25 deletions
@@ -31,7 +31,7 @@ Requirements python-gitlab depends on: -* `python-requests <http://docs.python-requests.org/en/latest/>`_ +* `python-requests <https://2.python-requests.org/en/latest/>`_ Install with pip ---------------- diff --git a/docs/api-objects.rst b/docs/api-objects.rst index 569435c..32f0d0c 100644 --- a/docs/api-objects.rst +++ b/docs/api-objects.rst @@ -7,6 +7,7 @@ API examples gl_objects/access_requests gl_objects/appearance + gl_objects/applications gl_objects/emojis gl_objects/badges gl_objects/branches diff --git a/docs/api-usage.rst b/docs/api-usage.rst index dc88684..dac3997 100644 --- a/docs/api-usage.rst +++ b/docs/api-usage.rst @@ -204,6 +204,11 @@ listing methods support the ``page`` and ``per_page`` parameters: By default GitLab does not return the complete list of items. Use the ``all`` parameter to get all the items when using listing methods: +.. warning:: + + The all=True option uses keyset pagination by default if order_by is not supplied, + or if order_by="id". + .. code-block:: python all_groups = gl.groups.list(all=True) @@ -302,7 +307,19 @@ python-gitlab: gl = gitlab.gitlab(url, token, api_version=4, session=session) Reference: -http://docs.python-requests.org/en/master/user/advanced/#proxies +https://2.python-requests.org/en/master/user/advanced/#proxies + +SSL certificate verification +---------------------------- + +python-gitlab relies on the CA certificate bundle in the `certifi` package +that comes with the requests library. + +If you need python-gitlab to use your system CA store instead, you can provide +the path to the CA bundle in the `REQUESTS_CA_BUNDLE` environment variable. + +Reference: +https://2.python-requests.org/en/master/user/advanced/#ssl-cert-verification Client side certificate ----------------------- @@ -319,7 +336,7 @@ The following sample illustrates how to use a client-side certificate: gl = gitlab.gitlab(url, token, api_version=4, session=session) Reference: -http://docs.python-requests.org/en/master/user/advanced/#client-side-certificates +https://2.python-requests.org/en/master/user/advanced/#client-side-certificates Rate limits ----------- @@ -391,4 +408,3 @@ parameter to that API invocation: gl = gitlab.gitlab(url, token, api_version=4) gl.projects.import_github(ACCESS_TOKEN, 123456, "root", timeout=120.0) - diff --git a/docs/gl_objects/access_requests.rst b/docs/gl_objects/access_requests.rst index e890ce0..467c3e5 100644 --- a/docs/gl_objects/access_requests.rst +++ b/docs/gl_objects/access_requests.rst @@ -37,8 +37,8 @@ List access requests from projects and groups:: Create an access request:: - p_ar = project.accessrequests.create({}) - g_ar = group.accessrequests.create({}) + p_ar = project.accessrequests.create() + g_ar = group.accessrequests.create() Approve an access request:: diff --git a/docs/gl_objects/applications.rst b/docs/gl_objects/applications.rst new file mode 100644 index 0000000..146b6e8 --- /dev/null +++ b/docs/gl_objects/applications.rst @@ -0,0 +1,31 @@ +############ +Applications +############ + +Reference +--------- + +* v4 API: + + + :class:`gitlab.v4.objects.Applications` + + :class:`gitlab.v4.objects.ApplicationManager` + + :attr:`gitlab.Gitlab.applications` + +* GitLab API: https://docs.gitlab.com/ce/api/applications.html + +Examples +-------- + +List all OAuth applications:: + + applications = gl.applications.list() + +Create an application:: + + gl.applications.create({'name': 'your_app', 'redirect_uri': 'http://application.url', 'scopes': ['api']}) + +Delete an applications:: + + gl.applications.delete(app_id) + # or + application.delete() diff --git a/docs/gl_objects/commits.rst b/docs/gl_objects/commits.rst index 97cd1c4..abfedc8 100644 --- a/docs/gl_objects/commits.rst +++ b/docs/gl_objects/commits.rst @@ -72,6 +72,10 @@ Cherry-pick a commit into another branch:: commit.cherry_pick(branch='target_branch') +Revert a commit on a given branch:: + + commit.revert(branch='target_branch') + Get the references the commit has been pushed to (branches and tags):: commit.refs() # all references diff --git a/docs/gl_objects/features.rst b/docs/gl_objects/features.rst index 9f5e685..2344895 100644 --- a/docs/gl_objects/features.rst +++ b/docs/gl_objects/features.rst @@ -24,6 +24,8 @@ Create or set a feature:: feature = gl.features.set(feature_name, True) feature = gl.features.set(feature_name, 30) + feature = gl.features.set(feature_name, True, user=filipowm) + feature = gl.features.set(feature_name, 40, group=mygroup) Delete a feature:: diff --git a/docs/gl_objects/projects.rst b/docs/gl_objects/projects.rst index 8c3526c..1878757 100644 --- a/docs/gl_objects/projects.rst +++ b/docs/gl_objects/projects.rst @@ -103,7 +103,7 @@ Delete a project:: Fork a project:: - fork = project.forks.create({}) + fork = project.forks.create() # fork to a specific namespace fork = project.forks.create({'namespace': 'myteam'}) @@ -255,7 +255,7 @@ generated by GitLab you need to: # Create the export p = gl.projects.get(my_project) - export = p.exports.create({}) + export = p.exports.create() # Wait for the 'finished' status export.refresh() diff --git a/docs/gl_objects/users.rst b/docs/gl_objects/users.rst index 3e71ac4..5b1cf3d 100644 --- a/docs/gl_objects/users.rst +++ b/docs/gl_objects/users.rst @@ -153,6 +153,33 @@ Revoke (delete) an impersonation token for a user:: i_t.delete() + +User memberships +========================= + +References +---------- + +* v4 API: + + + :class:`gitlab.v4.objects.UserMembership` + + :class:`gitlab.v4.objects.UserMembershipManager` + + :attr:`gitlab.v4.objects.User.memberships` + +* GitLab API: https://docs.gitlab.com/ee/api/users.html#user-memberships-admin-only + +List direct memberships for a user:: + + memberships = user.memberships.list() + +List only direct project memberships:: + + memberships = user.memberships.list(type='Project') + +List only direct group memberships:: + + memberships = user.memberships.list(type='Namespace') + Current User ============ diff --git a/gitlab/__init__.py b/gitlab/__init__.py index c9716c2..f924372 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -30,7 +30,7 @@ from gitlab.exceptions import * # noqa from gitlab import utils # noqa __title__ = "python-gitlab" -__version__ = "2.0.0" +__version__ = "2.1.0" __author__ = "Gauvain Pocentek" __email__ = "gauvainpocentek@gmail.com" __license__ = "LGPL3" @@ -90,8 +90,8 @@ class Gitlab(object): self._api_version = str(api_version) self._server_version = self._server_revision = None - self._base_url = url - self._url = "%s/api/v%s" % (url, api_version) + self._base_url = url.rstrip("/") + self._url = "%s/api/v%s" % (self._base_url, api_version) #: Timeout to use for requests to gitlab server self.timeout = timeout #: Headers that will be used in request to GitLab @@ -144,6 +144,7 @@ class Gitlab(object): self.features = objects.FeatureManager(self) self.pagesdomains = objects.PagesDomainManager(self) self.user_activities = objects.UserActivitiesManager(self) + self.applications = objects.ApplicationManager(self) def __enter__(self): return self @@ -640,6 +641,12 @@ class Gitlab(object): get_all = kwargs.pop("all", False) url = self._build_url(path) + # use keyset pagination automatically, if all=True + order_by = kwargs.get("order_by") + if get_all and (not order_by or order_by == "id"): + kwargs["pagination"] = "keyset" + kwargs["order_by"] = "id" + if get_all is True and as_list is True: return list(GitlabList(self, url, query_data, **kwargs)) diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index aff3c87..d6791f2 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -245,6 +245,10 @@ class GitlabRepairError(GitlabOperationError): pass +class GitlabRevertError(GitlabOperationError): + pass + + class GitlabLicenseError(GitlabOperationError): pass diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 8544499..dde11d0 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -170,7 +170,7 @@ class CreateMixin(object): return getattr(self, "_create_attrs", (tuple(), tuple())) @exc.on_http_error(exc.GitlabCreateError) - def create(self, data, **kwargs): + def create(self, data=None, **kwargs): """Create a new object. Args: @@ -186,6 +186,9 @@ class CreateMixin(object): GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server cannot perform the request """ + if data is None: + data = {} + self._check_missing_create_attrs(data) files = {} diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index 3eccf6e..249d0c5 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -376,6 +376,23 @@ class TestGitlabHttpMethods(unittest.TestCase): self.assertRaises(GitlabHttpError, self.gl.http_delete, "/not_there") +class TestGitlabStripBaseUrl(unittest.TestCase): + def setUp(self): + self.gl = Gitlab( + "http://localhost/", private_token="private_token", api_version=4 + ) + + def test_strip_base_url(self): + self.assertEqual(self.gl.url, "http://localhost") + + def test_strip_api_url(self): + self.assertEqual(self.gl.api_url, "http://localhost/api/v4") + + def test_build_url(self): + r = self.gl._build_url("/projects") + self.assertEqual(r, "http://localhost/api/v4/projects") + + class TestGitlabAuth(unittest.TestCase): def test_invalid_auth_args(self): self.assertRaises( @@ -658,6 +675,38 @@ class TestGitlab(unittest.TestCase): self.assertEqual(user.name, "name") self.assertEqual(user.id, 1) + def test_user_memberships(self): + @urlmatch( + scheme="http", + netloc="localhost", + path="/api/v4/users/1/memberships", + method="get", + ) + def resp_get_user_memberships(url, request): + headers = {"content-type": "application/json"} + content = """[ + { + "source_id": 1, + "source_name": "Project one", + "source_type": "Project", + "access_level": "20" + }, + { + "source_id": 3, + "source_name": "Group three", + "source_type": "Namespace", + "access_level": "20" + } + ]""" + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get_user_memberships): + user = self.gl.users.get(1, lazy=True) + memberships = user.memberships.list() + self.assertIsInstance(memberships[0], UserMembership) + self.assertEqual(memberships[0].source_type, "Project") + def test_user_status(self): @urlmatch( scheme="http", @@ -794,6 +843,50 @@ class TestGitlab(unittest.TestCase): self.gl.users.get(1, lazy=True).activate() self.gl.users.get(1, lazy=True).deactivate() + def test_commit_revert(self): + @urlmatch( + scheme="http", + netloc="localhost", + path="/api/v4/projects/1/repository/commits/6b2257ea", + method="get", + ) + def resp_get_commit(url, request): + headers = {"content-type": "application/json"} + content = """{ + "id": "6b2257eabcec3db1f59dafbd84935e3caea04235", + "short_id": "6b2257ea", + "title": "Initial commit" + }""" + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + @urlmatch( + scheme="http", + netloc="localhost", + path="/api/v4/projects/1/repository/commits/6b2257ea", + method="post", + ) + def resp_revert_commit(url, request): + headers = {"content-type": "application/json"} + content = """{ + "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad", + "short_id": "8b090c1b", + "title":"Revert \\"Initial commit\\"" + }""" + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get_commit): + project = self.gl.projects.get(1, lazy=True) + commit = project.commits.get("6b2257ea") + self.assertEqual(commit.short_id, "6b2257ea") + self.assertEqual(commit.title, "Initial commit") + + with HTTMock(resp_revert_commit): + revert_commit = commit.revert(branch="master") + self.assertEqual(revert_commit["short_id"], "8b090c1b") + self.assertEqual(revert_commit["title"], 'Revert "Initial commit"') + def test_update_submodule(self): @urlmatch( scheme="http", netloc="localhost", path="/api/v4/projects/1$", method="get" @@ -871,6 +964,33 @@ class TestGitlab(unittest.TestCase): self.assertEqual(ret["full_path"], "/".join((base_path, name))) self.assertTrue(ret["full_name"].endswith(name)) + def test_applications(self): + content = '{"name": "test_app", "redirect_uri": "http://localhost:8080", "scopes": ["api", "email"]}' + json_content = json.loads(content) + + @urlmatch( + scheme="http", + netloc="localhost", + path="/api/v4/applications", + method="post", + ) + def resp_application_create(url, request): + headers = {"content-type": "application/json"} + return response(200, json_content, headers, None, 5, request) + + with HTTMock(resp_application_create): + application = self.gl.applications.create( + { + "name": "test_app", + "redirect_uri": "http://localhost:8080", + "scopes": ["api", "email"], + "confidential": False, + } + ) + self.assertEqual(application.name, "test_app") + self.assertEqual(application.redirect_uri, "http://localhost:8080") + self.assertEqual(application.scopes, ["api", "email"]) + def _default_config(self): fd, temp_path = tempfile.mkstemp() os.write(fd, valid_config) diff --git a/gitlab/utils.py b/gitlab/utils.py index 0992ed7..4241787 100644 --- a/gitlab/utils.py +++ b/gitlab/utils.py @@ -55,3 +55,7 @@ def sanitized_url(url): parsed = urlparse(url) new_path = parsed.path.replace(".", "%2E") return parsed._replace(path=new_path).geturl() + + +def remove_none_from_dict(data): + return {k: v for k, v in data.items() if v is not None} diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index f22229c..13fbb53 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -229,6 +229,17 @@ class UserImpersonationTokenManager(NoUpdateMixin, RESTManager): _list_filters = ("state",) +class UserMembership(RESTObject): + _id_attr = "source_id" + + +class UserMembershipManager(RetrieveMixin, RESTManager): + _path = "/users/%(user_id)s/memberships" + _obj_cls = UserMembership + _from_parent_attrs = {"user_id": "id"} + _list_filters = ("type",) + + class UserProject(RESTObject): pass @@ -311,6 +322,7 @@ class User(SaveMixin, ObjectDeleteMixin, RESTObject): ("gpgkeys", "UserGPGKeyManager"), ("impersonationtokens", "UserImpersonationTokenManager"), ("keys", "UserKeyManager"), + ("memberships", "UserMembershipManager"), ("projects", "UserProjectManager"), ("status", "UserStatusManager"), ) @@ -414,6 +426,7 @@ class UserManager(CRUDMixin, RESTManager): "search", "custom_attributes", "status", + "two_factor", ) _create_attrs = ( tuple(), @@ -438,6 +451,8 @@ class UserManager(CRUDMixin, RESTManager): "organization", "location", "avatar", + "public_email", + "private_profile", ), ) _update_attrs = ( @@ -459,6 +474,8 @@ class UserManager(CRUDMixin, RESTManager): "organization", "location", "avatar", + "public_email", + "private_profile", ), ) _types = {"confirm": types.LowercaseStringAttribute, "avatar": types.ImageAttribute} @@ -719,7 +736,16 @@ class FeatureManager(ListMixin, DeleteMixin, RESTManager): _obj_cls = Feature @exc.on_http_error(exc.GitlabSetError) - def set(self, name, value, feature_group=None, user=None, **kwargs): + def set( + self, + name, + value, + feature_group=None, + user=None, + group=None, + project=None, + **kwargs + ): """Create or update the object. Args: @@ -727,6 +753,8 @@ class FeatureManager(ListMixin, DeleteMixin, RESTManager): value (bool/int): The value to set for the object feature_group (str): A feature group name user (str): A GitLab username + group (str): A GitLab group + project (str): A GitLab project in form group/project **kwargs: Extra options to send to the server (e.g. sudo) Raises: @@ -737,7 +765,14 @@ class FeatureManager(ListMixin, DeleteMixin, RESTManager): obj: The created/updated attribute """ path = "%s/%s" % (self.path, name.replace("/", "%2F")) - data = {"value": value, "feature_group": feature_group, "user": user} + data = { + "value": value, + "feature_group": feature_group, + "user": user, + "group": group, + "project": project, + } + data = utils.remove_none_from_dict(data) server_data = self.gitlab.http_post(path, post_data=data, **kwargs) return self._obj_cls(self, server_data) @@ -2113,6 +2148,26 @@ class ProjectCommit(RESTObject): path = "%s/%s/merge_requests" % (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.GitlabRevertError) + def revert(self, branch, **kwargs): + """Revert a commit on a given branch. + + Args: + branch (str): Name of target branch + **kwargs: Extra options to send to the server (e.g. sudo) + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabRevertError: If the revert could not be performed + + Returns: + dict: The new commit data (*not* a RESTObject) + """ + path = "%s/%s/revert" % (self.manager.path, self.get_id()) + post_data = {"branch": branch} + return self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) + class ProjectCommitManager(RetrieveMixin, CreateMixin, RESTManager): _path = "/projects/%(project_id)s/repository/commits" @@ -5087,3 +5142,14 @@ class GeoNodeManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager): list: The list of failures """ return self.gitlab.http_list("/geo_nodes/current/failures", **kwargs) + + +class Application(ObjectDeleteMixin, RESTObject): + _url = "/applications" + _short_print_attr = "name" + + +class ApplicationManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): + _path = "/applications" + _obj_cls = Application + _create_attrs = (("name", "redirect_uri", "scopes"), ("confidential",)) diff --git a/tools/build_test_env.sh b/tools/build_test_env.sh index f5feebf..7468a9a 100755 --- a/tools/build_test_env.sh +++ b/tools/build_test_env.sh @@ -29,12 +29,15 @@ REUSE_CONTAINER= NOVENV= PY_VER=3 API_VER=4 +GITLAB_IMAGE="gitlab/gitlab-ce" +GITLAB_TAG="latest" while getopts :knp:a: opt "$@"; do case $opt in k) REUSE_CONTAINER=1;; n) NOVENV=1;; p) PY_VER=$OPTARG;; a) API_VER=$OPTARG;; + t) GITLAB_TAG=$OPTARG;; :) fatal "Option -${OPTARG} requires a value";; '?') fatal "Unknown option: -${OPTARG}";; *) fatal "Internal error: opt=${opt}";; @@ -81,6 +84,7 @@ cleanup() { } if [ -z "$REUSE_CONTAINER" ] || ! docker top gitlab-test >/dev/null 2>&1; then + try docker pull "$GITLAB_IMAGE:$GITLAB_TAG" GITLAB_OMNIBUS_CONFIG="external_url 'http://gitlab.test' gitlab_rails['initial_root_password'] = '5iveL!fe' gitlab_rails['initial_shared_runners_registration_token'] = 'sTPNtWLEuSrHzoHP8oCU' @@ -103,7 +107,7 @@ letsencrypt['enable'] = false " try docker run --name gitlab-test --detach --publish 8080:80 \ --publish 2222:22 --env "GITLAB_OMNIBUS_CONFIG=$GITLAB_OMNIBUS_CONFIG" \ - gitlab/gitlab-ce:latest >/dev/null + "$GITLAB_IMAGE:$GITLAB_TAG" >/dev/null fi LOGIN='root' @@ -141,20 +145,13 @@ while :; do sleep 1 docker top gitlab-test >/dev/null 2>&1 || fatal "docker failed to start" sleep 4 - # last command started by the container is "gitlab-ctl tail" - docker exec gitlab-test pgrep -f 'gitlab-ctl tail' &>/dev/null \ - && docker exec gitlab-test curl http://localhost/-/health 2>/dev/null \ - | grep -q 'GitLab OK' \ - && curl -s http://localhost:8080/users/sign_in 2>/dev/null \ - | grep -q "GitLab Community Edition" \ + docker logs gitlab-test 2>&1 | grep "gitlab Reconfigured!" \ && break I=$((I+5)) + log "Waiting for GitLab to reconfigure.. (${I}s)" [ "$I" -lt 180 ] || fatal "timed out" done -log "Pausing to give GitLab some time to finish starting up..." -sleep 200 - # Get the token TOKEN=$($(dirname $0)/generate_token.py) diff --git a/tools/cli_test_v4.sh b/tools/cli_test_v4.sh index dc6e0b2..cf157f4 100755 --- a/tools/cli_test_v4.sh +++ b/tools/cli_test_v4.sh @@ -61,6 +61,10 @@ testcase "adding member to a project" ' --user-id "$USER_ID" --access-level 40 >/dev/null 2>&1 ' +testcase "listing user memberships" ' + GITLAB user-membership list --user-id "$USER_ID" >/dev/null 2>&1 +' + testcase "file creation" ' GITLAB project-file create --project-id "$PROJECT_ID" \ --file-path README --branch master --content "CONTENT" \ @@ -100,6 +104,15 @@ testcase "merge request validation" ' --iid "$MR_ID" >/dev/null 2>&1 ' +# Test revert commit +COMMITS=$(GITLAB -v project-commit list --project-id "${PROJECT_ID}") +COMMIT_ID=$(pecho "${COMMITS}" | grep -m1 '^id:' | cut -d' ' -f2) + +testcase "revert commit" ' + GITLAB project-commit revert --project-id "$PROJECT_ID" \ + --id "$COMMIT_ID" --branch master +' + # Test project labels testcase "create project label" ' OUTPUT=$(GITLAB -v project-label create --project-id $PROJECT_ID \ diff --git a/tools/python_test_v4.py b/tools/python_test_v4.py index bffdd2a..90aa7f1 100644 --- a/tools/python_test_v4.py +++ b/tools/python_test_v4.py @@ -266,6 +266,35 @@ group1.members.create({"access_level": gitlab.const.GUEST_ACCESS, "user_id": use group2.members.create({"access_level": gitlab.const.OWNER_ACCESS, "user_id": user2.id}) +# User memberships (admin only) +memberships1 = user1.memberships.list() +assert len(memberships1) == 1 + +memberships2 = user2.memberships.list() +assert len(memberships2) == 2 + +membership = memberships1[0] +assert membership.source_type == "Namespace" +assert membership.access_level == gitlab.const.OWNER_ACCESS + +project_memberships = user1.memberships.list(type="Project") +assert len(project_memberships) == 0 + +group_memberships = user1.memberships.list(type="Namespace") +assert len(group_memberships) == 1 + +try: + membership = user1.memberships.list(type="Invalid") +except gitlab.GitlabListError as e: + error_message = e.error_message +assert error_message == "type does not have a valid value" + +try: + user1.memberships.list(sudo=user1.name) +except gitlab.GitlabListError as e: + error_message = e.error_message +assert error_message == "403 Forbidden" + # Administrator belongs to the groups assert len(group1.members.list()) == 3 assert len(group2.members.list()) == 2 @@ -462,6 +491,21 @@ d_note_from_get.delete() discussion = commit.discussions.get(discussion.id) # assert len(discussion.attributes["notes"]) == 1 +# Revert commit +revert_commit = commit.revert(branch="master") + +expected_message = 'Revert "{}"\n\nThis reverts commit {}'.format( + commit.message, commit.id +) +assert revert_commit["message"] == expected_message + +try: + commit.revert(branch="master") + # Only here to really ensure expected error without a full test framework + raise AssertionError("Two revert attempts should raise GitlabRevertError") +except gitlab.GitlabRevertError: + pass + # housekeeping admin_project.housekeeping() @@ -905,7 +949,7 @@ settings.save() [current_project.delete() for current_project in projects] # project import/export -ex = admin_project.exports.create({}) +ex = admin_project.exports.create() ex.refresh() count = 0 while ex.export_status != "finished": |