# HG changeset patch
# User Georges Racinet <georges.racinet@octobus.net>
# Date 1670951254 -3600
#      Tue Dec 13 18:07:34 2022 +0100
# Branch stable
# Node ID 311ed039b41193d393c0f5440f31caa6cd3b7ad8
# Parent  137433550a869cf67ef25036f4c87786ff8c1efb
RepositoryService: implement FullPath

Closes hgitaly#114

diff --git a/hgitaly/repository.py b/hgitaly/repository.py
--- a/hgitaly/repository.py
+++ b/hgitaly/repository.py
@@ -28,6 +28,19 @@
         repo.vfs.write(GITLAB_PROJECT_FULL_PATH_FILENAME, full_path)
 
 
+def get_gitlab_project_full_path(repo):
+    """Get the full path of GitLab Project.
+
+    See also :func:`set_gitlab_project_full_path`.
+
+    :return: the full path, or ``None`` if not set.
+    """
+    try:
+        return repo.vfs.read(GITLAB_PROJECT_FULL_PATH_FILENAME)
+    except FileNotFoundError:
+        return None
+
+
 def unbundle(repo, bundle_path: str, rails_sync=False):
     """Call unbundle with proper options and conversions.
 
diff --git a/hgitaly/service/repository.py b/hgitaly/service/repository.py
--- a/hgitaly/service/repository.py
+++ b/hgitaly/service/repository.py
@@ -51,6 +51,7 @@
     validate_relative_path,
 )
 from ..repository import (
+    get_gitlab_project_full_path,
     set_gitlab_project_full_path,
     unbundle,
 )
@@ -80,6 +81,8 @@
     FetchBundleResponse,
     FindMergeBaseRequest,
     FindMergeBaseResponse,
+    FullPathRequest,
+    FullPathResponse,
     GetRawChangesRequest,
     GetRawChangesResponse,
     RepositoryExistsRequest,
@@ -416,6 +419,32 @@
         set_gitlab_project_full_path(repo, request.path.encode('utf-8'))
         return SetFullPathResponse()
 
+    def FullPath(self, request: FullPathRequest,
+                 context) -> FullPathResponse:
+        try:
+            repo = self.load_repo(request.repository, context)
+        except KeyError as exc:
+            kind, what = exc.args
+            if kind == 'storage':
+                context.abort(StatusCode.INVALID_ARGUMENT,
+                              'fetch config: 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)
+                context.abort(StatusCode.NOT_FOUND,
+                              'fetch config: GetRepoPath: not a Mercurial '
+                              'repository: "%s"' % os.fsdecode(what))
+
+        path = get_gitlab_project_full_path(repo)
+        if not path:  # None or (not probable) empty string
+            # Gitaly simply returns the `git config` stderr output for now.
+            # Pretty ridiculous to mimick it, we can hope not much to
+            # depend on it.
+            context.abort(StatusCode.INTERNAL, "Full path not set")
+        return FullPathResponse(path=path)
+
     def CreateBundle(self, request: CreateBundleRequest,
                      context) -> CreateBundleResponse:
         repo = self.load_repo(request.repository, context).unfiltered()
diff --git a/hgitaly/service/tests/test_repository_service.py b/hgitaly/service/tests/test_repository_service.py
--- a/hgitaly/service/tests/test_repository_service.py
+++ b/hgitaly/service/tests/test_repository_service.py
@@ -43,6 +43,7 @@
     CreateBundleFromRefListRequest,
     FetchBundleRequest,
     FindMergeBaseRequest,
+    FullPathRequest,
     GetArchiveRequest,
     HasLocalBranchesRequest,
     RemoveRepositoryRequest,
@@ -145,6 +146,12 @@
         return self.stub.SetFullPath(SetFullPathRequest(repository=grpc_repo,
                                                         path=path))
 
+    def full_path(self, grpc_repo=None):
+        if grpc_repo is None:
+            grpc_repo = self.grpc_repo
+
+        return self.stub.FullPath(FullPathRequest(repository=grpc_repo)).path
+
     def create_bundle(self, save_path):
         with open(save_path, 'wb') as fobj:
             for chunk in self.stub.CreateBundle(CreateBundleRequest(
@@ -558,6 +565,7 @@
         assert (wrapper.path / '.hg'
                 / GITLAB_PROJECT_FULL_PATH_FILENAME.decode('ascii')
                 ).read_text(encoding='utf-8') == expected
+        assert fixture.full_path() == expected
 
     def call_then_assert(full_path):
         fixture.set_full_path(full_path)
@@ -583,6 +591,28 @@
                                storage_name='unknown'))
 
 
+def test_full_path_errors(fixture_with_repo):
+    fixture = fixture_with_repo
+    grpc_repo = fixture.grpc_repo
+
+    def call_then_assert_error(error_code, grpc_repo=None):
+        with pytest.raises(grpc.RpcError) as exc_info:
+            fixture.full_path(grpc_repo=grpc_repo)
+        assert exc_info.value.code() == error_code
+
+    # existing repo, but full path not set
+    call_then_assert_error(grpc.StatusCode.INTERNAL)
+
+    call_then_assert_error(grpc.StatusCode.NOT_FOUND,
+                           grpc_repo=Repository(
+                               storage_name=grpc_repo.storage_name,
+                               relative_path='no/such/repo'))
+    call_then_assert_error(grpc.StatusCode.INVALID_ARGUMENT,
+                           grpc_repo=Repository(
+                               relative_path=grpc_repo.relative_path,
+                               storage_name='unknown'))
+
+
 def test_fetch_bundle(fixture_with_repo, tmpdir):
     fixture = fixture_with_repo
     wrapper = fixture.repo_wrapper
diff --git a/tests_with_gitaly/comparison.py b/tests_with_gitaly/comparison.py
--- a/tests_with_gitaly/comparison.py
+++ b/tests_with_gitaly/comparison.py
@@ -243,7 +243,7 @@
             norm = self.error_details_normalizer
             hg_details = exc_hg.details()
             git_details = exc_git.details()
-            if norm is not None:  # pragma no cover (temporary)
+            if norm is not None:
                 hg_details = norm(hg_details, vcs='hg')
                 git_details = norm(git_details, vcs='git')
             assert hg_details == git_details
diff --git a/tests_with_gitaly/test_repository_service.py b/tests_with_gitaly/test_repository_service.py
--- a/tests_with_gitaly/test_repository_service.py
+++ b/tests_with_gitaly/test_repository_service.py
@@ -20,6 +20,7 @@
     CreateBundleFromRefListRequest,
     CreateRepositoryFromBundleRequest,
     FindMergeBaseRequest,
+    FullPathRequest,
     RemoveRepositoryRequest,
     SetFullPathRequest,
 )
@@ -187,6 +188,33 @@
                               relative_path='no/such/path'))
 
 
+def test_full_path(gitaly_comparison, server_repos_root):
+    fixture = gitaly_comparison
+    grpc_repo = fixture.gitaly_repo
+
+    def normalizer(msg, **kw):
+        return msg.replace('Mercurial', 'git')
+
+    rpc_helper = fixture.rpc_helper(
+        stub_cls=RepositoryServiceStub,
+        method_name='FullPath',
+        request_cls=FullPathRequest,
+        error_details_normalizer=normalizer,
+    )
+    assert_compare_errors = rpc_helper.assert_compare_errors
+
+    # path not set on existing repo
+    assert_compare_errors(same_details=False)
+
+    # unknown storage and missing repo
+    assert_compare_errors(same_details=False,
+                          repository=Repository(storage_name='unknown',
+                                                relative_path='/some/path'))
+    assert_compare_errors(
+        repository=Repository(storage_name=grpc_repo.storage_name,
+                              relative_path='no/such/path'))
+
+
 def test_set_full_path(gitaly_comparison, server_repos_root):
     fixture = gitaly_comparison