diff --git a/hgitaly/repository.py b/hgitaly/repository.py new file mode 100644 index 0000000000000000000000000000000000000000..3ec209c1a65689c63517be6888ae08ede311161c_aGdpdGFseS9yZXBvc2l0b3J5LnB5 --- /dev/null +++ b/hgitaly/repository.py @@ -0,0 +1,23 @@ +# Copyright 2021 Georges Racinet <georges.racinet@octobus.net> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +# +# SPDX-License-Identifier: GPL-2.0-or-later + +GITLAB_PROJECT_FULL_PATH_FILENAME = b'gitlab.project_full_path' + + +def set_gitlab_project_full_path(repo, full_path: bytes): + """Store information about the full path of GitLab Project. + + In GitLab terminology, ``full_path`` is the URI path, while ``path`` + its the last segment of ``full_path``. + + In Git repositories, this is stored in config. We could as well use + the repo-local hgrcs, but it is simpler to use a dedicated file, and + it makes sense to consider it not part of ``store`` (``svfs``), same + as ``hgrc``. + """ + with repo.wlock(): + repo.vfs.write(GITLAB_PROJECT_FULL_PATH_FILENAME, full_path) diff --git a/hgitaly/service/repository.py b/hgitaly/service/repository.py index 175010d29cd5c4bd509bacebdf62f2a965ff97d1_aGdpdGFseS9zZXJ2aWNlL3JlcG9zaXRvcnkucHk=..3ec209c1a65689c63517be6888ae08ede311161c_aGdpdGFseS9zZXJ2aWNlL3JlcG9zaXRvcnkucHk= 100644 --- a/hgitaly/service/repository.py +++ b/hgitaly/service/repository.py @@ -6,6 +6,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later from grpc import StatusCode import logging +import os import tempfile from mercurial import ( @@ -35,6 +36,10 @@ parse_special_ref, ensure_special_refs, ) +from ..repository import ( + set_gitlab_project_full_path, +) + from ..revision import ( CHANGESET_HASH_BYTES_REGEXP, gitlab_revision_changeset, @@ -56,6 +61,8 @@ SearchFilesByContentResponse, SearchFilesByNameRequest, SearchFilesByNameResponse, + SetFullPathRequest, + SetFullPathResponse, WriteRefRequest, WriteRefResponse, ApplyGitattributesRequest, @@ -300,3 +307,32 @@ context) -> SearchFilesByContentResponse: return not_implemented(context, SearchFilesByContentResponse, issue=80) # pragma no cover + + def SetFullPath(self, request: SetFullPathRequest, + context) -> SetFullPathResponse: + try: + repo = self.load_repo(request.repository, context) + except KeyError as exc: + kind, what = exc.args + if kind == 'storage': + message = ('writing config: rpc error: ' + 'code = InvalidArgument desc = GetStorageByName: ' + 'no such storage: "%s"' % what) + else: + # (H)Gitaly relative paths are always ASCII, but the + # root might not be (Gitaly does disclose the full expected + # path in the error message) + message = ('writing config: rpc error: code = NotFound ' + 'desc = GetRepoPath: not a Mercurial ' + 'repository: "%s"' % os.fsdecode(what)) + + # perhaps subject to change in future Gitaly reference impl + return internal_error(context, SetFullPathResponse, + message=message) + + if not request.path: + return invalid_argument(context, SetFullPathResponse, + message='no path provided') + + set_gitlab_project_full_path(repo, request.path.encode('utf-8')) + return SetFullPathResponse() diff --git a/hgitaly/service/tests/test_repository_service.py b/hgitaly/service/tests/test_repository_service.py index 175010d29cd5c4bd509bacebdf62f2a965ff97d1_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfcmVwb3NpdG9yeV9zZXJ2aWNlLnB5..3ec209c1a65689c63517be6888ae08ede311161c_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfcmVwb3NpdG9yeV9zZXJ2aWNlLnB5 100644 --- a/hgitaly/service/tests/test_repository_service.py +++ b/hgitaly/service/tests/test_repository_service.py @@ -19,6 +19,9 @@ iter_keep_arounds, ) +from hgitaly.repository import ( + GITLAB_PROJECT_FULL_PATH_FILENAME, +) from hgitaly.tests.common import make_empty_repo from hgitaly.stub.shared_pb2 import Repository @@ -28,6 +31,7 @@ GetArchiveRequest, HasLocalBranchesRequest, RepositoryExistsRequest, + SetFullPathRequest, WriteRefRequest, ) from hgitaly.stub.repository_service_pb2_grpc import RepositoryServiceStub @@ -332,3 +336,40 @@ do_rpc(brepo_name) assert exc_info.value.code() == grpc.StatusCode.INTERNAL assert 'Too many levels of symbolic links' in exc_info.value.details() + + +def test_set_full_path(grpc_channel, server_repos_root): + repo_stub = RepositoryServiceStub(grpc_channel) + wrapper, grpc_repo = make_empty_repo(server_repos_root) + + def do_rpc(full_path, grpc_repo=grpc_repo): + return repo_stub.SetFullPath(SetFullPathRequest(repository=grpc_repo, + path=full_path)) + + def assert_full_path(expected): + assert (wrapper.path / '.hg' + / GITLAB_PROJECT_FULL_PATH_FILENAME.decode('ascii') + ).read_text(encoding='utf-8') == expected + + def call_then_assert(full_path): + do_rpc(full_path) + assert_full_path(full_path) + + def call_then_assert_error(full_path, error_code, grpc_repo=grpc_repo): + with pytest.raises(grpc.RpcError) as exc_info: + do_rpc(full_path, grpc_repo=grpc_repo) + assert exc_info.value.code() == error_code + + call_then_assert('group/proj') + call_then_assert('accent-lovers/projé') + call_then_assert('group/subgroup/proj') + + call_then_assert_error('', grpc.StatusCode.INVALID_ARGUMENT) + call_then_assert_error('some/path', grpc.StatusCode.INTERNAL, + grpc_repo=Repository( + storage_name=grpc_repo.storage_name, + relative_path='no/such/repo')) + call_then_assert_error('some/path', grpc.StatusCode.INTERNAL, + grpc_repo=Repository( + relative_path=grpc_repo.relative_path, + storage_name='unknown')) diff --git a/tests_with_gitaly/test_repository_service.py b/tests_with_gitaly/test_repository_service.py index 175010d29cd5c4bd509bacebdf62f2a965ff97d1_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9yZXBvc2l0b3J5X3NlcnZpY2UucHk=..3ec209c1a65689c63517be6888ae08ede311161c_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9yZXBvc2l0b3J5X3NlcnZpY2UucHk= 100644 --- a/tests_with_gitaly/test_repository_service.py +++ b/tests_with_gitaly/test_repository_service.py @@ -16,6 +16,7 @@ from hgitaly.stub.repository_service_pb2 import ( CreateRepositoryRequest, FindMergeBaseRequest, + SetFullPathRequest, ) from hgitaly.stub.repository_service_pb2_grpc import RepositoryServiceStub @@ -138,3 +139,53 @@ assert exc_info_hg.value.code() == exc_info_git.value.code() assert 'Too many levels of symbolic links' in exc_info_hg.value.details() assert 'file exists' in exc_info_git.value.details() + + +def test_set_full_path(gitaly_comparison, server_repos_root): + fixture = gitaly_comparison + + repo_stubs = dict( + hg=RepositoryServiceStub(fixture.hgitaly_channel), + git=RepositoryServiceStub(fixture.gitaly_channel) + ) + + def do_rpc(vcs, path, grpc_repo=fixture.gitaly_repo): + return repo_stubs[vcs].SetFullPath( + SetFullPathRequest(repository=grpc_repo, path=path)) + + def assert_compare(path): + assert do_rpc('hg', path) == do_rpc('git', path) + + def assert_error_compare(path, + grpc_repo=None, + msg_normalizer=lambda m: m): + kwargs = dict(grpc_repo=grpc_repo) if grpc_repo is not None else {} + with pytest.raises(Exception) as hg_exc_info: + do_rpc('hg', path, **kwargs) + with pytest.raises(Exception) as git_exc_info: + do_rpc('git', path, **kwargs) + hg_exc, git_exc = hg_exc_info.value, git_exc_info.value + assert hg_exc.code() == git_exc.code() + hg_msg, git_msg = hg_exc.details(), git_exc.details() + assert msg_normalizer(hg_msg) == msg_normalizer(git_msg) + + # success case + assert_compare('group/project') + + # error cases + assert_error_compare('') + + def normalize_repo_not_found(msg): + return msg.replace('git repo', 'Mercurial repo') + + assert_error_compare('some/full/path', + grpc_repo=Repository( + storage_name=fixture.gitaly_repo.storage_name, + relative_path='no/such/repo'), + msg_normalizer=normalize_repo_not_found, + ) + + assert_error_compare('some/full/path', + grpc_repo=Repository( + relative_path=fixture.gitaly_repo.relative_path, + storage_name='unknown'))