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'))