summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn L. Villalovos <john@sodarock.com>2022-01-09 22:11:47 -0800
committerJohn L. Villalovos <john@sodarock.com>2022-01-13 10:31:24 -0800
commit12435d74364ca881373d690eab89d2e2baa62a49 (patch)
tree7976389d86a50666458d3f45d2ca64fb6deb0e50
parent824151ce9238f97118ec21aa8b3267cc7a2cd649 (diff)
downloadgitlab-12435d74364ca881373d690eab89d2e2baa62a49.tar.gz
fix: use url-encoded ID in all paths
Make sure all usage of the ID in the URL path is encoded. Normally it isn't an issue as most IDs are integers or strings which don't contain a slash ('/'). But when the ID is a string with a slash character it will break things. Add a test case that shows this fixes wikis issue with subpages which use the slash character. Closes: #1079
-rw-r--r--gitlab/base.py9
-rw-r--r--gitlab/mixins.py37
-rw-r--r--gitlab/utils.py14
-rw-r--r--gitlab/v4/objects/commits.py12
-rw-r--r--gitlab/v4/objects/environments.py2
-rw-r--r--gitlab/v4/objects/epics.py2
-rw-r--r--gitlab/v4/objects/files.py2
-rw-r--r--gitlab/v4/objects/geo_nodes.py4
-rw-r--r--gitlab/v4/objects/groups.py12
-rw-r--r--gitlab/v4/objects/issues.py6
-rw-r--r--gitlab/v4/objects/jobs.py18
-rw-r--r--gitlab/v4/objects/merge_requests.py18
-rw-r--r--gitlab/v4/objects/milestones.py8
-rw-r--r--gitlab/v4/objects/pipelines.py8
-rw-r--r--gitlab/v4/objects/projects.py32
-rw-r--r--gitlab/v4/objects/repositories.py16
-rw-r--r--gitlab/v4/objects/snippets.py4
-rw-r--r--tests/functional/api/test_wikis.py15
-rw-r--r--tests/unit/test_base.py14
19 files changed, 141 insertions, 92 deletions
diff --git a/gitlab/base.py b/gitlab/base.py
index af32905..96e770c 100644
--- a/gitlab/base.py
+++ b/gitlab/base.py
@@ -218,6 +218,15 @@ class RESTObject(object):
return getattr(self, self._id_attr)
@property
+ def encoded_id(self) -> Any:
+ """Ensure that the ID is url-encoded so that it can be safely used in a URL
+ path"""
+ obj_id = self.get_id()
+ if isinstance(obj_id, str):
+ obj_id = gitlab.utils._url_encode(obj_id)
+ return obj_id
+
+ @property
def attributes(self) -> Dict[str, Any]:
d = self.__dict__["_updated_attrs"].copy()
d.update(self.__dict__["_attrs"])
diff --git a/gitlab/mixins.py b/gitlab/mixins.py
index c02f4c0..1832247 100644
--- a/gitlab/mixins.py
+++ b/gitlab/mixins.py
@@ -99,8 +99,7 @@ class GetMixin(_RestManagerBase):
GitlabAuthenticationError: If authentication is not correct
GitlabGetError: If the server cannot perform the request
"""
- if not isinstance(id, int):
- id = utils._url_encode(id)
+ id = utils._url_encode(id)
path = f"{self.path}/{id}"
if TYPE_CHECKING:
assert self._obj_cls is not None
@@ -173,7 +172,7 @@ class RefreshMixin(_RestObjectBase):
GitlabGetError: If the server cannot perform the request
"""
if self._id_attr:
- path = f"{self.manager.path}/{self.id}"
+ path = f"{self.manager.path}/{self.encoded_id}"
else:
if TYPE_CHECKING:
assert self.manager.path is not None
@@ -391,7 +390,7 @@ class UpdateMixin(_RestManagerBase):
if id is None:
path = self.path
else:
- path = f"{self.path}/{id}"
+ path = f"{self.path}/{utils._url_encode(id)}"
self._check_missing_update_attrs(new_data)
files = {}
@@ -477,9 +476,7 @@ class DeleteMixin(_RestManagerBase):
if id is None:
path = self.path
else:
- if not isinstance(id, int):
- id = utils._url_encode(id)
- path = f"{self.path}/{id}"
+ path = f"{self.path}/{utils._url_encode(id)}"
self.gitlab.http_delete(path, **kwargs)
@@ -545,6 +542,7 @@ class SaveMixin(_RestObjectBase):
return
# call the manager
+ # Don't use `self.encoded_id` here as `self.manager.update()` will encode it.
obj_id = self.get_id()
if TYPE_CHECKING:
assert isinstance(self.manager, UpdateMixin)
@@ -575,6 +573,7 @@ class ObjectDeleteMixin(_RestObjectBase):
"""
if TYPE_CHECKING:
assert isinstance(self.manager, DeleteMixin)
+ # Don't use `self.encoded_id` here as `self.manager.delete()` will encode it.
self.manager.delete(self.get_id(), **kwargs)
@@ -598,7 +597,7 @@ class UserAgentDetailMixin(_RestObjectBase):
GitlabAuthenticationError: If authentication is not correct
GitlabGetError: If the server cannot perform the request
"""
- path = f"{self.manager.path}/{self.get_id()}/user_agent_detail"
+ path = f"{self.manager.path}/{self.encoded_id}/user_agent_detail"
result = self.manager.gitlab.http_get(path, **kwargs)
if TYPE_CHECKING:
assert not isinstance(result, requests.Response)
@@ -631,7 +630,7 @@ class AccessRequestMixin(_RestObjectBase):
GitlabUpdateError: If the server fails to perform the request
"""
- path = f"{self.manager.path}/{self.id}/approve"
+ path = f"{self.manager.path}/{self.encoded_id}/approve"
data = {"access_level": access_level}
server_data = self.manager.gitlab.http_put(path, post_data=data, **kwargs)
if TYPE_CHECKING:
@@ -705,7 +704,7 @@ class SubscribableMixin(_RestObjectBase):
GitlabAuthenticationError: If authentication is not correct
GitlabSubscribeError: If the subscription cannot be done
"""
- path = f"{self.manager.path}/{self.get_id()}/subscribe"
+ path = f"{self.manager.path}/{self.encoded_id}/subscribe"
server_data = self.manager.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
assert not isinstance(server_data, requests.Response)
@@ -725,7 +724,7 @@ class SubscribableMixin(_RestObjectBase):
GitlabAuthenticationError: If authentication is not correct
GitlabUnsubscribeError: If the unsubscription cannot be done
"""
- path = f"{self.manager.path}/{self.get_id()}/unsubscribe"
+ path = f"{self.manager.path}/{self.encoded_id}/unsubscribe"
server_data = self.manager.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
assert not isinstance(server_data, requests.Response)
@@ -752,7 +751,7 @@ class TodoMixin(_RestObjectBase):
GitlabAuthenticationError: If authentication is not correct
GitlabTodoError: If the todo cannot be set
"""
- path = f"{self.manager.path}/{self.get_id()}/todo"
+ path = f"{self.manager.path}/{self.encoded_id}/todo"
self.manager.gitlab.http_post(path, **kwargs)
@@ -781,7 +780,7 @@ class TimeTrackingMixin(_RestObjectBase):
if "time_stats" in self.attributes:
return self.attributes["time_stats"]
- path = f"{self.manager.path}/{self.get_id()}/time_stats"
+ path = f"{self.manager.path}/{self.encoded_id}/time_stats"
result = self.manager.gitlab.http_get(path, **kwargs)
if TYPE_CHECKING:
assert not isinstance(result, requests.Response)
@@ -800,7 +799,7 @@ class TimeTrackingMixin(_RestObjectBase):
GitlabAuthenticationError: If authentication is not correct
GitlabTimeTrackingError: If the time tracking update cannot be done
"""
- path = f"{self.manager.path}/{self.get_id()}/time_estimate"
+ path = f"{self.manager.path}/{self.encoded_id}/time_estimate"
data = {"duration": duration}
result = self.manager.gitlab.http_post(path, post_data=data, **kwargs)
if TYPE_CHECKING:
@@ -819,7 +818,7 @@ class TimeTrackingMixin(_RestObjectBase):
GitlabAuthenticationError: If authentication is not correct
GitlabTimeTrackingError: If the time tracking update cannot be done
"""
- path = f"{self.manager.path}/{self.get_id()}/reset_time_estimate"
+ path = f"{self.manager.path}/{self.encoded_id}/reset_time_estimate"
result = self.manager.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
assert not isinstance(result, requests.Response)
@@ -838,7 +837,7 @@ class TimeTrackingMixin(_RestObjectBase):
GitlabAuthenticationError: If authentication is not correct
GitlabTimeTrackingError: If the time tracking update cannot be done
"""
- path = f"{self.manager.path}/{self.get_id()}/add_spent_time"
+ path = f"{self.manager.path}/{self.encoded_id}/add_spent_time"
data = {"duration": duration}
result = self.manager.gitlab.http_post(path, post_data=data, **kwargs)
if TYPE_CHECKING:
@@ -857,7 +856,7 @@ class TimeTrackingMixin(_RestObjectBase):
GitlabAuthenticationError: If authentication is not correct
GitlabTimeTrackingError: If the time tracking update cannot be done
"""
- path = f"{self.manager.path}/{self.get_id()}/reset_spent_time"
+ path = f"{self.manager.path}/{self.encoded_id}/reset_spent_time"
result = self.manager.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
assert not isinstance(result, requests.Response)
@@ -893,7 +892,7 @@ class ParticipantsMixin(_RestObjectBase):
The list of participants
"""
- path = f"{self.manager.path}/{self.get_id()}/participants"
+ path = f"{self.manager.path}/{self.encoded_id}/participants"
result = self.manager.gitlab.http_get(path, **kwargs)
if TYPE_CHECKING:
assert not isinstance(result, requests.Response)
@@ -967,7 +966,7 @@ class PromoteMixin(_RestObjectBase):
The updated object data (*not* a RESTObject)
"""
- path = f"{self.manager.path}/{self.id}/promote"
+ path = f"{self.manager.path}/{self.encoded_id}/promote"
http_method = self._get_update_method()
result = http_method(path, **kwargs)
if TYPE_CHECKING:
diff --git a/gitlab/utils.py b/gitlab/utils.py
index 1f29104..7914521 100644
--- a/gitlab/utils.py
+++ b/gitlab/utils.py
@@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urllib.parse
-from typing import Any, Callable, Dict, Optional
+from typing import Any, Callable, Dict, Optional, overload, Union
import requests
@@ -56,7 +56,17 @@ def copy_dict(dest: Dict[str, Any], src: Dict[str, Any]) -> None:
dest[k] = v
+@overload
+def _url_encode(id: int) -> int:
+ ...
+
+
+@overload
def _url_encode(id: str) -> str:
+ ...
+
+
+def _url_encode(id: Union[int, str]) -> Union[int, str]:
"""Encode/quote the characters in the string so that they can be used in a path.
Reference to documentation on why this is necessary.
@@ -74,6 +84,8 @@ def _url_encode(id: str) -> str:
parameters.
"""
+ if isinstance(id, int):
+ return id
return urllib.parse.quote(id, safe="")
diff --git a/gitlab/v4/objects/commits.py b/gitlab/v4/objects/commits.py
index 02a10dc..fa08ef0 100644
--- a/gitlab/v4/objects/commits.py
+++ b/gitlab/v4/objects/commits.py
@@ -42,7 +42,7 @@ class ProjectCommit(RESTObject):
Returns:
The changes done in this commit
"""
- path = f"{self.manager.path}/{self.get_id()}/diff"
+ path = f"{self.manager.path}/{self.encoded_id}/diff"
return self.manager.gitlab.http_list(path, **kwargs)
@cli.register_custom_action("ProjectCommit", ("branch",))
@@ -58,7 +58,7 @@ class ProjectCommit(RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabCherryPickError: If the cherry-pick could not be performed
"""
- path = f"{self.manager.path}/{self.get_id()}/cherry_pick"
+ path = f"{self.manager.path}/{self.encoded_id}/cherry_pick"
post_data = {"branch": branch}
self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
@@ -80,7 +80,7 @@ class ProjectCommit(RESTObject):
Returns:
The references the commit is pushed to.
"""
- path = f"{self.manager.path}/{self.get_id()}/refs"
+ path = f"{self.manager.path}/{self.encoded_id}/refs"
query_data = {"type": type}
return self.manager.gitlab.http_list(path, query_data=query_data, **kwargs)
@@ -101,7 +101,7 @@ class ProjectCommit(RESTObject):
Returns:
The merge requests related to the commit.
"""
- path = f"{self.manager.path}/{self.get_id()}/merge_requests"
+ path = f"{self.manager.path}/{self.encoded_id}/merge_requests"
return self.manager.gitlab.http_list(path, **kwargs)
@cli.register_custom_action("ProjectCommit", ("branch",))
@@ -122,7 +122,7 @@ class ProjectCommit(RESTObject):
Returns:
The new commit data (*not* a RESTObject)
"""
- path = f"{self.manager.path}/{self.get_id()}/revert"
+ path = f"{self.manager.path}/{self.encoded_id}/revert"
post_data = {"branch": branch}
return self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
@@ -141,7 +141,7 @@ class ProjectCommit(RESTObject):
Returns:
The commit's signature data
"""
- path = f"{self.manager.path}/{self.get_id()}/signature"
+ path = f"{self.manager.path}/{self.encoded_id}/signature"
return self.manager.gitlab.http_get(path, **kwargs)
diff --git a/gitlab/v4/objects/environments.py b/gitlab/v4/objects/environments.py
index 35f2fb2..1dbfe08 100644
--- a/gitlab/v4/objects/environments.py
+++ b/gitlab/v4/objects/environments.py
@@ -36,7 +36,7 @@ class ProjectEnvironment(SaveMixin, ObjectDeleteMixin, RESTObject):
Returns:
A dict of the result.
"""
- path = f"{self.manager.path}/{self.get_id()}/stop"
+ path = f"{self.manager.path}/{self.encoded_id}/stop"
return self.manager.gitlab.http_post(path, **kwargs)
diff --git a/gitlab/v4/objects/epics.py b/gitlab/v4/objects/epics.py
index 999c45f..bb0bb79 100644
--- a/gitlab/v4/objects/epics.py
+++ b/gitlab/v4/objects/epics.py
@@ -72,7 +72,7 @@ class GroupEpicIssue(ObjectDeleteMixin, SaveMixin, RESTObject):
return
# call the manager
- obj_id = self.get_id()
+ obj_id = self.encoded_id
self.manager.update(obj_id, updated_data, **kwargs)
diff --git a/gitlab/v4/objects/files.py b/gitlab/v4/objects/files.py
index 64046f9..644c017 100644
--- a/gitlab/v4/objects/files.py
+++ b/gitlab/v4/objects/files.py
@@ -76,7 +76,7 @@ class ProjectFile(SaveMixin, ObjectDeleteMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabDeleteError: If the server cannot perform the request
"""
- file_path = utils._url_encode(self.get_id())
+ file_path = self.encoded_id
self.manager.delete(file_path, branch, commit_message, **kwargs)
diff --git a/gitlab/v4/objects/geo_nodes.py b/gitlab/v4/objects/geo_nodes.py
index ebeb0d6..6633275 100644
--- a/gitlab/v4/objects/geo_nodes.py
+++ b/gitlab/v4/objects/geo_nodes.py
@@ -30,7 +30,7 @@ class GeoNode(SaveMixin, ObjectDeleteMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabRepairError: If the server failed to perform the request
"""
- path = f"/geo_nodes/{self.get_id()}/repair"
+ path = f"/geo_nodes/{self.encoded_id}/repair"
server_data = self.manager.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
assert isinstance(server_data, dict)
@@ -51,7 +51,7 @@ class GeoNode(SaveMixin, ObjectDeleteMixin, RESTObject):
Returns:
The status of the geo node
"""
- path = f"/geo_nodes/{self.get_id()}/status"
+ path = f"/geo_nodes/{self.encoded_id}/status"
result = self.manager.gitlab.http_get(path, **kwargs)
if TYPE_CHECKING:
assert isinstance(result, dict)
diff --git a/gitlab/v4/objects/groups.py b/gitlab/v4/objects/groups.py
index c2e252e..453548b 100644
--- a/gitlab/v4/objects/groups.py
+++ b/gitlab/v4/objects/groups.py
@@ -115,7 +115,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
A list of dicts describing the resources found.
"""
data = {"scope": scope, "search": search}
- path = f"/groups/{self.get_id()}/search"
+ path = f"/groups/{self.encoded_id}/search"
return self.manager.gitlab.http_list(path, query_data=data, **kwargs)
@cli.register_custom_action("Group", ("cn", "group_access", "provider"))
@@ -136,7 +136,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the server cannot perform the request
"""
- path = f"/groups/{self.get_id()}/ldap_group_links"
+ path = f"/groups/{self.encoded_id}/ldap_group_links"
data = {"cn": cn, "group_access": group_access, "provider": provider}
self.manager.gitlab.http_post(path, post_data=data, **kwargs)
@@ -156,7 +156,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabDeleteError: If the server cannot perform the request
"""
- path = f"/groups/{self.get_id()}/ldap_group_links"
+ path = f"/groups/{self.encoded_id}/ldap_group_links"
if provider is not None:
path += f"/{provider}"
path += f"/{cn}"
@@ -174,7 +174,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the server cannot perform the request
"""
- path = f"/groups/{self.get_id()}/ldap_sync"
+ path = f"/groups/{self.encoded_id}/ldap_sync"
self.manager.gitlab.http_post(path, **kwargs)
@cli.register_custom_action("Group", ("group_id", "group_access"), ("expires_at",))
@@ -200,7 +200,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
Returns:
Group
"""
- path = f"/groups/{self.get_id()}/share"
+ path = f"/groups/{self.encoded_id}/share"
data = {
"group_id": group_id,
"group_access": group_access,
@@ -224,7 +224,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabDeleteError: If the server failed to perform the request
"""
- path = f"/groups/{self.get_id()}/share/{group_id}"
+ path = f"/groups/{self.encoded_id}/share/{group_id}"
self.manager.gitlab.http_delete(path, **kwargs)
diff --git a/gitlab/v4/objects/issues.py b/gitlab/v4/objects/issues.py
index 5a99a09..585e02e 100644
--- a/gitlab/v4/objects/issues.py
+++ b/gitlab/v4/objects/issues.py
@@ -132,7 +132,7 @@ class ProjectIssue(
GitlabAuthenticationError: If authentication is not correct
GitlabUpdateError: If the issue could not be moved
"""
- path = f"{self.manager.path}/{self.get_id()}/move"
+ path = f"{self.manager.path}/{self.encoded_id}/move"
data = {"to_project_id": to_project_id}
server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs)
if TYPE_CHECKING:
@@ -154,7 +154,7 @@ class ProjectIssue(
Returns:
The list of merge requests.
"""
- path = f"{self.manager.path}/{self.get_id()}/related_merge_requests"
+ path = f"{self.manager.path}/{self.encoded_id}/related_merge_requests"
result = self.manager.gitlab.http_get(path, **kwargs)
if TYPE_CHECKING:
assert isinstance(result, dict)
@@ -175,7 +175,7 @@ class ProjectIssue(
Returns:
The list of merge requests.
"""
- path = f"{self.manager.path}/{self.get_id()}/closed_by"
+ path = f"{self.manager.path}/{self.encoded_id}/closed_by"
result = self.manager.gitlab.http_get(path, **kwargs)
if TYPE_CHECKING:
assert isinstance(result, dict)
diff --git a/gitlab/v4/objects/jobs.py b/gitlab/v4/objects/jobs.py
index be06f86..fbcb1fd 100644
--- a/gitlab/v4/objects/jobs.py
+++ b/gitlab/v4/objects/jobs.py
@@ -27,7 +27,7 @@ class ProjectJob(RefreshMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabJobCancelError: If the job could not be canceled
"""
- path = f"{self.manager.path}/{self.get_id()}/cancel"
+ path = f"{self.manager.path}/{self.encoded_id}/cancel"
result = self.manager.gitlab.http_post(path)
if TYPE_CHECKING:
assert isinstance(result, dict)
@@ -45,7 +45,7 @@ class ProjectJob(RefreshMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabJobRetryError: If the job could not be retried
"""
- path = f"{self.manager.path}/{self.get_id()}/retry"
+ path = f"{self.manager.path}/{self.encoded_id}/retry"
result = self.manager.gitlab.http_post(path)
if TYPE_CHECKING:
assert isinstance(result, dict)
@@ -63,7 +63,7 @@ class ProjectJob(RefreshMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabJobPlayError: If the job could not be triggered
"""
- path = f"{self.manager.path}/{self.get_id()}/play"
+ path = f"{self.manager.path}/{self.encoded_id}/play"
self.manager.gitlab.http_post(path)
@cli.register_custom_action("ProjectJob")
@@ -78,7 +78,7 @@ class ProjectJob(RefreshMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabJobEraseError: If the job could not be erased
"""
- path = f"{self.manager.path}/{self.get_id()}/erase"
+ path = f"{self.manager.path}/{self.encoded_id}/erase"
self.manager.gitlab.http_post(path)
@cli.register_custom_action("ProjectJob")
@@ -93,7 +93,7 @@ class ProjectJob(RefreshMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the request could not be performed
"""
- path = f"{self.manager.path}/{self.get_id()}/artifacts/keep"
+ path = f"{self.manager.path}/{self.encoded_id}/artifacts/keep"
self.manager.gitlab.http_post(path)
@cli.register_custom_action("ProjectJob")
@@ -108,7 +108,7 @@ class ProjectJob(RefreshMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabDeleteError: If the request could not be performed
"""
- path = f"{self.manager.path}/{self.get_id()}/artifacts"
+ path = f"{self.manager.path}/{self.encoded_id}/artifacts"
self.manager.gitlab.http_delete(path)
@cli.register_custom_action("ProjectJob")
@@ -138,7 +138,7 @@ class ProjectJob(RefreshMixin, RESTObject):
Returns:
The artifacts if `streamed` is False, None otherwise.
"""
- path = f"{self.manager.path}/{self.get_id()}/artifacts"
+ path = f"{self.manager.path}/{self.encoded_id}/artifacts"
result = self.manager.gitlab.http_get(
path, streamed=streamed, raw=True, **kwargs
)
@@ -175,7 +175,7 @@ class ProjectJob(RefreshMixin, RESTObject):
Returns:
The artifacts if `streamed` is False, None otherwise.
"""
- path = f"{self.manager.path}/{self.get_id()}/artifacts/{path}"
+ path = f"{self.manager.path}/{self.encoded_id}/artifacts/{path}"
result = self.manager.gitlab.http_get(
path, streamed=streamed, raw=True, **kwargs
)
@@ -210,7 +210,7 @@ class ProjectJob(RefreshMixin, RESTObject):
Returns:
The trace
"""
- path = f"{self.manager.path}/{self.get_id()}/trace"
+ path = f"{self.manager.path}/{self.encoded_id}/trace"
result = self.manager.gitlab.http_get(
path, streamed=streamed, raw=True, **kwargs
)
diff --git a/gitlab/v4/objects/merge_requests.py b/gitlab/v4/objects/merge_requests.py
index 0e81de1..9a4f8c8 100644
--- a/gitlab/v4/objects/merge_requests.py
+++ b/gitlab/v4/objects/merge_requests.py
@@ -182,7 +182,7 @@ class ProjectMergeRequest(
"""
path = (
- f"{self.manager.path}/{self.get_id()}/cancel_merge_when_pipeline_succeeds"
+ f"{self.manager.path}/{self.encoded_id}/cancel_merge_when_pipeline_succeeds"
)
server_data = self.manager.gitlab.http_put(path, **kwargs)
if TYPE_CHECKING:
@@ -210,7 +210,7 @@ class ProjectMergeRequest(
Returns:
List of issues
"""
- path = f"{self.manager.path}/{self.get_id()}/closes_issues"
+ path = f"{self.manager.path}/{self.encoded_id}/closes_issues"
data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs)
if TYPE_CHECKING:
assert isinstance(data_list, gitlab.GitlabList)
@@ -238,7 +238,7 @@ class ProjectMergeRequest(
The list of commits
"""
- path = f"{self.manager.path}/{self.get_id()}/commits"
+ path = f"{self.manager.path}/{self.encoded_id}/commits"
data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs)
if TYPE_CHECKING:
assert isinstance(data_list, gitlab.GitlabList)
@@ -260,7 +260,7 @@ class ProjectMergeRequest(
Returns:
List of changes
"""
- path = f"{self.manager.path}/{self.get_id()}/changes"
+ path = f"{self.manager.path}/{self.encoded_id}/changes"
return self.manager.gitlab.http_get(path, **kwargs)
@cli.register_custom_action("ProjectMergeRequest", tuple(), ("sha",))
@@ -281,7 +281,7 @@ class ProjectMergeRequest(
https://docs.gitlab.com/ee/api/merge_request_approvals.html#approve-merge-request
"""
- path = f"{self.manager.path}/{self.get_id()}/approve"
+ path = f"{self.manager.path}/{self.encoded_id}/approve"
data = {}
if sha:
data["sha"] = sha
@@ -306,7 +306,7 @@ class ProjectMergeRequest(
https://docs.gitlab.com/ee/api/merge_request_approvals.html#unapprove-merge-request
"""
- path = f"{self.manager.path}/{self.get_id()}/unapprove"
+ path = f"{self.manager.path}/{self.encoded_id}/unapprove"
data: Dict[str, Any] = {}
server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs)
@@ -326,7 +326,7 @@ class ProjectMergeRequest(
GitlabAuthenticationError: If authentication is not correct
GitlabMRRebaseError: If rebasing failed
"""
- path = f"{self.manager.path}/{self.get_id()}/rebase"
+ path = f"{self.manager.path}/{self.encoded_id}/rebase"
data: Dict[str, Any] = {}
return self.manager.gitlab.http_put(path, post_data=data, **kwargs)
@@ -342,7 +342,7 @@ class ProjectMergeRequest(
Raises:
GitlabGetError: If cannot be merged
"""
- path = f"{self.manager.path}/{self.get_id()}/merge_ref"
+ path = f"{self.manager.path}/{self.encoded_id}/merge_ref"
return self.manager.gitlab.http_get(path, **kwargs)
@cli.register_custom_action(
@@ -376,7 +376,7 @@ class ProjectMergeRequest(
GitlabAuthenticationError: If authentication is not correct
GitlabMRClosedError: If the merge failed
"""
- path = f"{self.manager.path}/{self.get_id()}/merge"
+ path = f"{self.manager.path}/{self.encoded_id}/merge"
data: Dict[str, Any] = {}
if merge_commit_message:
data["merge_commit_message"] = merge_commit_message
diff --git a/gitlab/v4/objects/milestones.py b/gitlab/v4/objects/milestones.py
index a1e48a5..6b1e28d 100644
--- a/gitlab/v4/objects/milestones.py
+++ b/gitlab/v4/objects/milestones.py
@@ -45,7 +45,7 @@ class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
The list of issues
"""
- path = f"{self.manager.path}/{self.get_id()}/issues"
+ path = f"{self.manager.path}/{self.encoded_id}/issues"
data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs)
if TYPE_CHECKING:
assert isinstance(data_list, RESTObjectList)
@@ -73,7 +73,7 @@ class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
Returns:
The list of merge requests
"""
- path = f"{self.manager.path}/{self.get_id()}/merge_requests"
+ path = f"{self.manager.path}/{self.encoded_id}/merge_requests"
data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs)
if TYPE_CHECKING:
assert isinstance(data_list, RESTObjectList)
@@ -126,7 +126,7 @@ class ProjectMilestone(PromoteMixin, SaveMixin, ObjectDeleteMixin, RESTObject):
The list of issues
"""
- path = f"{self.manager.path}/{self.get_id()}/issues"
+ path = f"{self.manager.path}/{self.encoded_id}/issues"
data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs)
if TYPE_CHECKING:
assert isinstance(data_list, RESTObjectList)
@@ -154,7 +154,7 @@ class ProjectMilestone(PromoteMixin, SaveMixin, ObjectDeleteMixin, RESTObject):
Returns:
The list of merge requests
"""
- path = f"{self.manager.path}/{self.get_id()}/merge_requests"
+ path = f"{self.manager.path}/{self.encoded_id}/merge_requests"
data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs)
if TYPE_CHECKING:
assert isinstance(data_list, RESTObjectList)
diff --git a/gitlab/v4/objects/pipelines.py b/gitlab/v4/objects/pipelines.py
index ac4290f..ec4e8e4 100644
--- a/gitlab/v4/objects/pipelines.py
+++ b/gitlab/v4/objects/pipelines.py
@@ -66,7 +66,7 @@ class ProjectPipeline(RefreshMixin, ObjectDeleteMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabPipelineCancelError: If the request failed
"""
- path = f"{self.manager.path}/{self.get_id()}/cancel"
+ path = f"{self.manager.path}/{self.encoded_id}/cancel"
return self.manager.gitlab.http_post(path)
@cli.register_custom_action("ProjectPipeline")
@@ -81,7 +81,7 @@ class ProjectPipeline(RefreshMixin, ObjectDeleteMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabPipelineRetryError: If the request failed
"""
- path = f"{self.manager.path}/{self.get_id()}/retry"
+ path = f"{self.manager.path}/{self.encoded_id}/retry"
return self.manager.gitlab.http_post(path)
@@ -194,7 +194,7 @@ class ProjectPipelineSchedule(SaveMixin, ObjectDeleteMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabOwnershipError: If the request failed
"""
- path = f"{self.manager.path}/{self.get_id()}/take_ownership"
+ path = f"{self.manager.path}/{self.encoded_id}/take_ownership"
server_data = self.manager.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
assert isinstance(server_data, dict)
@@ -213,7 +213,7 @@ class ProjectPipelineSchedule(SaveMixin, ObjectDeleteMixin, RESTObject):
GitlabAuthenticationError: If authentication is not correct
GitlabPipelinePlayError: If the request failed
"""
- path = f"{self.manager.path}/{self.get_id()}/play"
+ path = f"{self.manager.path}/{self.encoded_id}/play"
server_data = self.manager.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
assert isinstance(server_data, dict)
diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py
index 74671c8..58666ce 100644
--- a/gitlab/v4/objects/projects.py
+++ b/gitlab/v4/objects/projects.py
@@ -197,7 +197,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the relation could not be created
"""
- path = f"/projects/{self.get_id()}/fork/{forked_from_id}"
+ path = f"/projects/{self.encoded_id}/fork/{forked_from_id}"
self.manager.gitlab.http_post(path, **kwargs)
@cli.register_custom_action("Project")
@@ -212,7 +212,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabAuthenticationError: If authentication is not correct
GitlabDeleteError: If the server failed to perform the request
"""
- path = f"/projects/{self.get_id()}/fork"
+ path = f"/projects/{self.encoded_id}/fork"
self.manager.gitlab.http_delete(path, **kwargs)
@cli.register_custom_action("Project")
@@ -227,7 +227,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabAuthenticationError: If authentication is not correct
GitlabGetError: If the server failed to perform the request
"""
- path = f"/projects/{self.get_id()}/languages"
+ path = f"/projects/{self.encoded_id}/languages"
return self.manager.gitlab.http_get(path, **kwargs)
@cli.register_custom_action("Project")
@@ -242,7 +242,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the server failed to perform the request
"""
- path = f"/projects/{self.get_id()}/star"
+ path = f"/projects/{self.encoded_id}/star"
server_data = self.manager.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
assert isinstance(server_data, dict)
@@ -260,7 +260,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabAuthenticationError: If authentication is not correct
GitlabDeleteError: If the server failed to perform the request
"""
- path = f"/projects/{self.get_id()}/unstar"
+ path = f"/projects/{self.encoded_id}/unstar"
server_data = self.manager.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
assert isinstance(server_data, dict)
@@ -278,7 +278,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the server failed to perform the request
"""
- path = f"/projects/{self.get_id()}/archive"
+ path = f"/projects/{self.encoded_id}/archive"
server_data = self.manager.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
assert isinstance(server_data, dict)
@@ -296,7 +296,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabAuthenticationError: If authentication is not correct
GitlabDeleteError: If the server failed to perform the request
"""
- path = f"/projects/{self.get_id()}/unarchive"
+ path = f"/projects/{self.encoded_id}/unarchive"
server_data = self.manager.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
assert isinstance(server_data, dict)
@@ -324,7 +324,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the server failed to perform the request
"""
- path = f"/projects/{self.get_id()}/share"
+ path = f"/projects/{self.encoded_id}/share"
data = {
"group_id": group_id,
"group_access": group_access,
@@ -345,7 +345,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabAuthenticationError: If authentication is not correct
GitlabDeleteError: If the server failed to perform the request
"""
- path = f"/projects/{self.get_id()}/share/{group_id}"
+ path = f"/projects/{self.encoded_id}/share/{group_id}"
self.manager.gitlab.http_delete(path, **kwargs)
# variables not supported in CLI
@@ -373,7 +373,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabCreateError: If the server failed to perform the request
"""
variables = variables or {}
- path = f"/projects/{self.get_id()}/trigger/pipeline"
+ path = f"/projects/{self.encoded_id}/trigger/pipeline"
post_data = {"ref": ref, "token": token, "variables": variables}
attrs = self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
if TYPE_CHECKING:
@@ -393,7 +393,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabHousekeepingError: If the server failed to perform the
request
"""
- path = f"/projects/{self.get_id()}/housekeeping"
+ path = f"/projects/{self.encoded_id}/housekeeping"
self.manager.gitlab.http_post(path, **kwargs)
# see #56 - add file attachment features
@@ -478,7 +478,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
Returns:
The uncompressed tar archive of the repository
"""
- path = f"/projects/{self.get_id()}/snapshot"
+ path = f"/projects/{self.encoded_id}/snapshot"
result = self.manager.gitlab.http_get(
path, streamed=streamed, raw=True, **kwargs
)
@@ -506,7 +506,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
A list of dicts describing the resources found.
"""
data = {"scope": scope, "search": search}
- path = f"/projects/{self.get_id()}/search"
+ path = f"/projects/{self.encoded_id}/search"
return self.manager.gitlab.http_list(path, query_data=data, **kwargs)
@cli.register_custom_action("Project")
@@ -521,7 +521,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the server failed to perform the request
"""
- path = f"/projects/{self.get_id()}/mirror/pull"
+ path = f"/projects/{self.encoded_id}/mirror/pull"
self.manager.gitlab.http_post(path, **kwargs)
@cli.register_custom_action("Project", ("to_namespace",))
@@ -577,7 +577,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
Returns:
The artifacts if `streamed` is False, None otherwise.
"""
- path = f"/projects/{self.get_id()}/jobs/artifacts/{ref_name}/download"
+ path = f"/projects/{self.encoded_id}/jobs/artifacts/{ref_name}/download"
result = self.manager.gitlab.http_get(
path, job=job, streamed=streamed, raw=True, **kwargs
)
@@ -622,7 +622,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
"""
path = (
- f"/projects/{self.get_id()}/jobs/artifacts/{ref_name}/raw/"
+ f"/projects/{self.encoded_id}/jobs/artifacts/{ref_name}/raw/"
f"{artifact_path}?job={job}"
)
result = self.manager.gitlab.http_get(
diff --git a/gitlab/v4/objects/repositories.py b/gitlab/v4/objects/repositories.py
index b52add3..ca70b5b 100644
--- a/gitlab/v4/objects/repositories.py
+++ b/gitlab/v4/objects/repositories.py
@@ -40,7 +40,7 @@ class RepositoryMixin(_RestObjectBase):
"""
submodule = utils._url_encode(submodule)
- path = f"/projects/{self.get_id()}/repository/submodules/{submodule}"
+ path = f"/projects/{self.encoded_id}/repository/submodules/{submodule}"
data = {"branch": branch, "commit_sha": commit_sha}
if "commit_message" in kwargs:
data["commit_message"] = kwargs["commit_message"]
@@ -71,7 +71,7 @@ class RepositoryMixin(_RestObjectBase):
Returns:
The representation of the tree
"""
- gl_path = f"/projects/{self.get_id()}/repository/tree"
+ gl_path = f"/projects/{self.encoded_id}/repository/tree"
query_data: Dict[str, Any] = {"recursive": recursive}
if path:
query_data["path"] = path
@@ -98,7 +98,7 @@ class RepositoryMixin(_RestObjectBase):
The blob content and metadata
"""
- path = f"/projects/{self.get_id()}/repository/blobs/{sha}"
+ path = f"/projects/{self.encoded_id}/repository/blobs/{sha}"
return self.manager.gitlab.http_get(path, **kwargs)
@cli.register_custom_action("Project", ("sha",))
@@ -130,7 +130,7 @@ class RepositoryMixin(_RestObjectBase):
Returns:
The blob content if streamed is False, None otherwise
"""
- path = f"/projects/{self.get_id()}/repository/blobs/{sha}/raw"
+ path = f"/projects/{self.encoded_id}/repository/blobs/{sha}/raw"
result = self.manager.gitlab.http_get(
path, streamed=streamed, raw=True, **kwargs
)
@@ -157,7 +157,7 @@ class RepositoryMixin(_RestObjectBase):
Returns:
The diff
"""
- path = f"/projects/{self.get_id()}/repository/compare"
+ path = f"/projects/{self.encoded_id}/repository/compare"
query_data = {"from": from_, "to": to}
return self.manager.gitlab.http_get(path, query_data=query_data, **kwargs)
@@ -183,7 +183,7 @@ class RepositoryMixin(_RestObjectBase):
Returns:
The contributors
"""
- path = f"/projects/{self.get_id()}/repository/contributors"
+ path = f"/projects/{self.encoded_id}/repository/contributors"
return self.manager.gitlab.http_list(path, **kwargs)
@cli.register_custom_action("Project", tuple(), ("sha", "format"))
@@ -217,7 +217,7 @@ class RepositoryMixin(_RestObjectBase):
Returns:
The binary data of the archive
"""
- path = f"/projects/{self.get_id()}/repository/archive"
+ path = f"/projects/{self.encoded_id}/repository/archive"
if format:
path += "." + format
query_data = {}
@@ -242,5 +242,5 @@ class RepositoryMixin(_RestObjectBase):
GitlabAuthenticationError: If authentication is not correct
GitlabDeleteError: If the server failed to perform the request
"""
- path = f"/projects/{self.get_id()}/repository/merged_branches"
+ path = f"/projects/{self.encoded_id}/repository/merged_branches"
self.manager.gitlab.http_delete(path, **kwargs)
diff --git a/gitlab/v4/objects/snippets.py b/gitlab/v4/objects/snippets.py
index 66459c0..9d9dcc4 100644
--- a/gitlab/v4/objects/snippets.py
+++ b/gitlab/v4/objects/snippets.py
@@ -50,7 +50,7 @@ class Snippet(UserAgentDetailMixin, SaveMixin, ObjectDeleteMixin, RESTObject):
Returns:
The snippet content
"""
- path = f"/snippets/{self.get_id()}/raw"
+ path = f"/snippets/{self.encoded_id}/raw"
result = self.manager.gitlab.http_get(
path, streamed=streamed, raw=True, **kwargs
)
@@ -124,7 +124,7 @@ class ProjectSnippet(UserAgentDetailMixin, SaveMixin, ObjectDeleteMixin, RESTObj
Returns:
The snippet content
"""
- path = f"{self.manager.path}/{self.get_id()}/raw"
+ path = f"{self.manager.path}/{self.encoded_id}/raw"
result = self.manager.gitlab.http_get(
path, streamed=streamed, raw=True, **kwargs
)
diff --git a/tests/functional/api/test_wikis.py b/tests/functional/api/test_wikis.py
new file mode 100644
index 0000000..26ac244
--- /dev/null
+++ b/tests/functional/api/test_wikis.py
@@ -0,0 +1,15 @@
+"""
+GitLab API:
+https://docs.gitlab.com/ee/api/wikis.html
+"""
+
+
+def test_wikis(project):
+
+ page = project.wikis.create({"title": "title/subtitle", "content": "test content"})
+ page.content = "update content"
+ page.title = "subtitle"
+
+ page.save()
+
+ page.delete()
diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py
index fa9f6aa..1493799 100644
--- a/tests/unit/test_base.py
+++ b/tests/unit/test_base.py
@@ -144,6 +144,20 @@ class TestRESTObject:
obj.id = None
assert obj.get_id() is None
+ def test_encoded_id(self, fake_manager):
+ obj = FakeObject(fake_manager, {"foo": "bar"})
+ obj.id = 42
+ assert 42 == obj.encoded_id
+
+ obj.id = None
+ assert obj.encoded_id is None
+
+ obj.id = "plain"
+ assert "plain" == obj.encoded_id
+
+ obj.id = "a/path"
+ assert "a%2Fpath" == obj.encoded_id
+
def test_custom_id_attr(self, fake_manager):
class OtherFakeObject(FakeObject):
_id_attr = "foo"