Skip to content

Commit ac1c619

Browse files
fix(cli): url-encode path components of the URL
In the CLI we need to make sure the components put into the path portion of the URL are url-encoded. Otherwise they will be interpreted as part of the path. For example can specify the project ID as a path, but in the URL it must be url-encoded or it doesn't work. Also stop adding the components of the path as query parameters in the URL. Closes: #783 Closes: #1498
1 parent c9ed3dd commit ac1c619

File tree

4 files changed

+67
-13
lines changed

4 files changed

+67
-13
lines changed

gitlab/v4/cli.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(
3939
self.action = action.lower()
4040
self.gl = gl
4141
self.args = args
42+
self.parent_args: Dict[str, Any] = {}
4243
self.mgr_cls: Union[
4344
Type[gitlab.mixins.CreateMixin],
4445
Type[gitlab.mixins.DeleteMixin],
@@ -53,7 +54,10 @@ def __init__(
5354
# the class _path attribute, and replace the value with the result.
5455
if TYPE_CHECKING:
5556
assert self.mgr_cls._path is not None
56-
self.mgr_cls._path = self.mgr_cls._path.format(**self.args)
57+
58+
self._process_from_parent_attrs()
59+
60+
self.mgr_cls._path = self.mgr_cls._path.format(**self.parent_args)
5761
self.mgr = self.mgr_cls(gl)
5862

5963
if self.mgr_cls._types:
@@ -63,6 +67,18 @@ def __init__(
6367
obj.set_from_cli(self.args[attr_name])
6468
self.args[attr_name] = obj.get()
6569

70+
def _process_from_parent_attrs(self) -> None:
71+
"""Items in the path need to be url-encoded. There is a 1:1 mapping from
72+
mgr_cls._from_parent_attrs <--> mgr_cls._path. Those values must be url-encoded
73+
as they may contain a slash '/'."""
74+
for key in self.mgr_cls._from_parent_attrs:
75+
if key not in self.args:
76+
continue
77+
78+
self.parent_args[key] = gitlab.utils.clean_str_id(self.args[key])
79+
# If we don't delete it then it will be added to the URL as a query-string
80+
del self.args[key]
81+
6682
def __call__(self) -> Any:
6783
# Check for a method that matches object + action
6884
method = f"do_{self.what}_{self.action}"
@@ -85,7 +101,7 @@ def do_custom(self) -> Any:
85101
data = {}
86102
if self.mgr._from_parent_attrs:
87103
for k in self.mgr._from_parent_attrs:
88-
data[k] = self.args[k]
104+
data[k] = self.parent_args[k]
89105
if not issubclass(self.cls, gitlab.mixins.GetWithoutIdMixin):
90106
if TYPE_CHECKING:
91107
assert isinstance(self.cls._id_attr, str)

tests/functional/cli/conftest.py

+14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import pytest
2+
import responses
3+
4+
from gitlab.const import DEFAULT_URL
25

36

47
@pytest.fixture
@@ -19,3 +22,14 @@ def _gitlab_cli(subcommands):
1922
return script_runner.run(*command)
2023

2124
return _gitlab_cli
25+
26+
27+
@pytest.fixture
28+
def resp_get_project():
29+
return {
30+
"method": responses.GET,
31+
"url": f"{DEFAULT_URL}/api/v4/projects/1",
32+
"json": {"name": "name", "path": "test-path", "id": 1},
33+
"content_type": "application/json",
34+
"status": 200,
35+
}

tests/functional/cli/test_cli.py

-11
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,6 @@
1717
CI_SERVER_URL = "https://gitlab.example.com"
1818

1919

20-
@pytest.fixture
21-
def resp_get_project():
22-
return {
23-
"method": responses.GET,
24-
"url": f"{DEFAULT_URL}/api/v4/projects/1",
25-
"json": {"name": "name", "path": "test-path", "id": 1},
26-
"content_type": "application/json",
27-
"status": 200,
28-
}
29-
30-
3120
def test_main_entrypoint(script_runner, gitlab_config):
3221
ret = script_runner.run("python", "-m", "gitlab", "--config-file", gitlab_config)
3322
assert ret.returncode == 2

tests/functional/cli/test_cli_variables.py

+35
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import copy
2+
3+
import pytest
4+
import responses
5+
6+
from gitlab import config
7+
from gitlab.const import DEFAULT_URL
8+
9+
110
def test_list_instance_variables(gitlab_cli, gl):
211
cmd = ["variable", "list"]
312
ret = gitlab_cli(cmd)
@@ -17,3 +26,29 @@ def test_list_project_variables(gitlab_cli, project):
1726
ret = gitlab_cli(cmd)
1827

1928
assert ret.success
29+
30+
31+
def test_list_project_variables_with_path(gitlab_cli, project):
32+
cmd = ["project-variable", "list", "--project-id", project.path_with_namespace]
33+
ret = gitlab_cli(cmd)
34+
35+
assert ret.success
36+
37+
38+
@pytest.mark.script_launch_mode("inprocess")
39+
@responses.activate
40+
def test_list_project_variables_with_path_url_check(
41+
monkeypatch, script_runner, resp_get_project
42+
):
43+
monkeypatch.setattr(config, "_DEFAULT_FILES", [])
44+
resp_get_project_variables = copy.deepcopy(resp_get_project)
45+
resp_get_project_variables.update(
46+
url=f"{DEFAULT_URL}/api/v4/projects/project%2Fwith%2Fa%2Fnamespace/variables"
47+
)
48+
resp_get_project_variables.update(json=[])
49+
50+
responses.add(**resp_get_project_variables)
51+
ret = script_runner.run(
52+
"gitlab", "project-variable", "list", "--project-id", "project/with/a/namespace"
53+
)
54+
assert ret.success

0 commit comments

Comments
 (0)