diff --git a/gitlab/v4/objects/files.py b/gitlab/v4/objects/files.py index 43086b7fe..78476f5ad 100644 --- a/gitlab/v4/objects/files.py +++ b/gitlab/v4/objects/files.py @@ -2,11 +2,11 @@ from typing import ( Any, Callable, - cast, Dict, Iterator, List, Optional, + Tuple, TYPE_CHECKING, Union, ) @@ -20,7 +20,6 @@ from gitlab.mixins import ( CreateMixin, DeleteMixin, - GetMixin, ObjectDeleteMixin, SaveMixin, UpdateMixin, @@ -96,10 +95,11 @@ def delete( # type: ignore self.manager.delete(file_path, branch, commit_message, **kwargs) -class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager): +class ProjectFileManager(CreateMixin, UpdateMixin, DeleteMixin, RESTManager): _path = "/projects/{project_id}/repository/files" _obj_cls = ProjectFile _from_parent_attrs = {"project_id": "id"} + _optional_get_attrs: Tuple[str, ...] = () _create_attrs = RequiredOptional( required=("file_path", "branch", "content", "commit_message"), optional=("encoding", "author_email", "author_name"), @@ -112,11 +112,7 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa @cli.register_custom_action( cls_names="ProjectFileManager", required=("file_path", "ref") ) - # NOTE(jlvillal): Signature doesn't match UpdateMixin.update() so ignore - # type error - def get( # type: ignore - self, file_path: str, ref: str, **kwargs: Any - ) -> ProjectFile: + def get(self, file_path: str, ref: str, **kwargs: Any) -> ProjectFile: """Retrieve a single file. Args: @@ -131,7 +127,37 @@ def get( # type: ignore Returns: The generated RESTObject """ - return cast(ProjectFile, GetMixin.get(self, file_path, ref=ref, **kwargs)) + if TYPE_CHECKING: + assert file_path is not None + file_path = utils.EncodedId(file_path) + path = f"{self.path}/{file_path}" + server_data = self.gitlab.http_get(path, ref=ref, **kwargs) + if TYPE_CHECKING: + assert isinstance(server_data, dict) + return self._obj_cls(self, server_data) + + def head( + self, file_path: str, ref: str, **kwargs: Any + ) -> "requests.structures.CaseInsensitiveDict[Any]": + """Retrieve just metadata for a single file. + + Args: + file_path: Path of the file to retrieve + ref: Name of the branch, tag or commit + **kwargs: Extra options to send to the server (e.g. sudo) + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabGetError: If the file could not be retrieved + + Returns: + The response headers as a dictionary + """ + if TYPE_CHECKING: + assert file_path is not None + file_path = utils.EncodedId(file_path) + path = f"{self.path}/{file_path}" + return self.gitlab.http_head(path, ref=ref, **kwargs) @cli.register_custom_action( cls_names="ProjectFileManager", diff --git a/tests/unit/objects/test_repositories.py b/tests/unit/objects/test_repositories.py index f11bd64e4..f891d4d09 100644 --- a/tests/unit/objects/test_repositories.py +++ b/tests/unit/objects/test_repositories.py @@ -8,6 +8,7 @@ import pytest import responses +from requests.structures import CaseInsensitiveDict from gitlab.v4.objects import ProjectFile @@ -15,6 +16,52 @@ ref = "main" +@pytest.fixture +def resp_head_repository_file(): + header_response = { + "Cache-Control": "no-cache", + "Content-Length": "0", + "Content-Type": "application/json", + "Date": "Thu, 12 Sep 2024 14:27:49 GMT", + "Referrer-Policy": "strict-origin-when-cross-origin", + "Server": "nginx", + "Strict-Transport-Security": "max-age=63072000", + "Vary": "Origin", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "SAMEORIGIN", + "X-Gitlab-Blob-Id": "79f7bbd25901e8334750839545a9bd021f0e4c83", + "X-Gitlab-Commit-Id": "d5a3ff139356ce33e37e73add446f16869741b50", + "X-Gitlab-Content-Sha256": "4c294617b60715c1d218e61164a3abd4808a4284cbc30e6728a01ad9aada4481", + "X-Gitlab-Encoding": "base64", + "X-Gitlab-Execute-Filemode": "false", + "X-Gitlab-File-Name": "key.rb", + "X-Gitlab-File-Path": file_path, + "X-Gitlab-Last-Commit-Id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", + "X-Gitlab-Meta": '{"correlation_id":"01J7KFRPXBX65Y04HEH7MFX4GD","version":"1"}', + "X-Gitlab-Ref": ref, + "X-Gitlab-Size": "1476", + "X-Request-Id": "01J7KFRPXBX65Y04HEH7MFX4GD", + "X-Runtime": "0.083199", + "Connection": "keep-alive", + } + encoded_path = quote(file_path, safe="") + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.HEAD, + url=f"http://localhost/api/v4/projects/1/repository/files/{encoded_path}", + headers=header_response, + status=200, + ) + yield rsps + + +def test_head_repository_file(project, resp_head_repository_file): + headers = project.files.head(file_path, ref=ref) + assert isinstance(headers, CaseInsensitiveDict) + assert headers["X-Gitlab-File-Path"] == file_path + + @pytest.fixture def resp_get_repository_file(): file_response = {