diff --git a/.hgtags b/.hgtags
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_LmhndGFncw==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_LmhndGFncw== 100644
--- a/.hgtags
+++ b/.hgtags
@@ -62,3 +62,14 @@
 e5a02e0abd62fe3b9f6bd33ac1882ecbc2cd545c 0.33.2
 7e815fe93113f89496846fc87498ee246caab4e5 0.33.3
 9f5a999855c4f54e656a7cb27dd931b5149bf4bc 0.33.4
+d33263b080e179fcc0636b567ab2a952cd1e5405 0.34.0
+4066eb34a329cd8a91b2880a6fa873fc1609feba 0.35.0
+e29ee359b3c2b7f68ea9af072ee4723fde0238c8 0.36.0
+28e88c3e1f8375c13ec90b9d1aaca586b6e8a8cd 0.37.0
+1015845afe27043b6dab5e05a868c093cc4eb01c 0.38.0
+d106c9a3eb27c1854a5d7cd78c1f6253d0df2185 0.39.0
+e4a01ac4d5902136f4e683efe71a8b9ee87be7b5 0.39.1
+6341000c08fe7160fdba4106a83e2e331558d094 0.40.0
+d924b81496e212c7898c47269aff3afe10cfcfda 0.40.1
+6ce174b1c7593857e4e65b9f03981d581d075b95 0.40.2
+c31dbf702896e10dc0c33caf48a357bc8cfecbf1 0.40.3
diff --git a/conftest.py b/conftest.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_Y29uZnRlc3QucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_Y29uZnRlc3QucHk= 100644
--- a/conftest.py
+++ b/conftest.py
@@ -8,6 +8,7 @@
 from hgitaly.service.ref import RefServicer
 from hgitaly.service.diff import DiffServicer
 from hgitaly.service.mercurial_changeset import MercurialChangesetServicer
+from hgitaly.service.mercurial_operations import MercurialOperationsServicer
 from hgitaly.service.mercurial_repository import MercurialRepositoryServicer
 from hgitaly.service.repository import RepositoryServicer
 from hgitaly.service.server import ServerServicer
@@ -21,6 +22,9 @@
 from hgitaly.stub.mercurial_changeset_pb2_grpc import (
     add_MercurialChangesetServiceServicer_to_server
 )
+from hgitaly.stub.mercurial_operations_pb2_grpc import (
+    add_MercurialOperationsServiceServicer_to_server
+)
 from hgitaly.stub.mercurial_repository_pb2_grpc import (
     add_MercurialRepositoryServiceServicer_to_server
 )
@@ -51,6 +55,9 @@
     add_MercurialChangesetServiceServicer_to_server(
         MercurialChangesetServicer(storages),
         _grpc_server)
+    add_MercurialOperationsServiceServicer_to_server(
+        MercurialOperationsServicer(storages),
+        _grpc_server)
     add_MercurialRepositoryServiceServicer_to_server(
         MercurialRepositoryServicer(storages),
         _grpc_server)
diff --git a/generate-stubs b/generate-stubs
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_Z2VuZXJhdGUtc3R1YnM=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_Z2VuZXJhdGUtc3R1YnM= 100755
--- a/generate-stubs
+++ b/generate-stubs
@@ -5,7 +5,7 @@
 PROTOS_DIR=./protos
 STUBS_DIR=hgitaly/stub
 
-for PROTO in lint shared errors blob commit diff ref repository server mercurial-repository mercurial-changeset; do
+for PROTO in lint shared errors blob commit diff ref repository server mercurial-repository mercurial-changeset mercurial-operations; do
     python -m grpc_tools.protoc \
       -I${PROTOS_DIR} \
       --python_out=${STUBS_DIR} \
diff --git a/hgext3rd/hgitaly/__init__.py b/hgext3rd/hgitaly/__init__.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdleHQzcmQvaGdpdGFseS9fX2luaXRfXy5weQ==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdleHQzcmQvaGdpdGFseS9fX2luaXRfXy5weQ== 100644
--- a/hgext3rd/hgitaly/__init__.py
+++ b/hgext3rd/hgitaly/__init__.py
@@ -10,6 +10,7 @@
     error,
     pycompat,
     registrar,
+    util,
 )
 
 from hgitaly.server import (
@@ -20,6 +21,13 @@
 )
 from . import revset
 
+if util.safehasattr(registrar, 'configitem'):
+
+    configtable = {}
+    configitem = registrar.configitem(configtable)
+    configitem(b'hgitaly', b'tokei-executable', b'tokei')
+
+
 cmdtable = {}
 command = registrar.command(cmdtable)
 
diff --git a/hgitaly/VERSION b/hgitaly/VERSION
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9WRVJTSU9O..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9WRVJTSU9O 100644
--- a/hgitaly/VERSION
+++ b/hgitaly/VERSION
@@ -1,1 +1,1 @@
-0.33.5dev0
+0.40.4dev0
diff --git a/hgitaly/branch.py b/hgitaly/branch.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9icmFuY2gucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9icmFuY2gucHk= 100644
--- a/hgitaly/branch.py
+++ b/hgitaly/branch.py
@@ -29,6 +29,7 @@
     ensure_gitlab_branches as hg_git_ensure_gitlab_branches,
 )
 from .gitlab_ref import (
+    path_match_any,
     sort_key_ref_changeset_date_asc,
     sort_key_ref_changeset_date_desc,
 )
@@ -288,7 +289,7 @@
         yield key, repo[node]
 
 
-def iter_gitlab_branches_as_refs(repo, **kwargs):
+def iter_gitlab_branches_as_refs(repo, patterns=None, **kwargs):
     """Same as :func:`iter_gitlab_branches`, yielding full Git ref paths.
     """
     for branch, target in iter_gitlab_branches(repo, **kwargs):
@@ -292,6 +293,10 @@
     """Same as :func:`iter_gitlab_branches`, yielding full Git ref paths.
     """
     for branch, target in iter_gitlab_branches(repo, **kwargs):
+        ref_path = gitlab_branch_ref(branch)
+        if patterns and not path_match_any(ref_path, patterns):
+            continue
+
         yield gitlab_branch_ref(branch), target
 
 
diff --git a/hgitaly/changelog.py b/hgitaly/changelog.py
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9jaGFuZ2Vsb2cucHk=
--- /dev/null
+++ b/hgitaly/changelog.py
@@ -0,0 +1,23 @@
+# Copyright 2023 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
+"""Various changelog operations
+
+These are utilities involving several changesets, either in the arguments or
+in the response.
+"""
+
+
+def ancestor(cs1, cs2):
+    """Greatest common ancestor of two changesets."""
+    return cs1.repo().revs("ancestor(%s, %s)", cs1, cs2).first()
+
+
+def merge_content(source, target):
+    """Iterator of changeset contexts for the merge of source into target."""
+    repo = source.repo()
+    for rev in repo.revs("only(%s, %s)", source, target):
+        yield repo[rev]
diff --git a/hgitaly/gitlab_ref.py b/hgitaly/gitlab_ref.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9naXRsYWJfcmVmLnB5..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9naXRsYWJfcmVmLnB5 100644
--- a/hgitaly/gitlab_ref.py
+++ b/hgitaly/gitlab_ref.py
@@ -12,6 +12,15 @@
 `refs/some/thing` and not yet refined to a given type, such as branches
 or tags.
 """
+from fnmatch import fnmatch
+
+from heptapod.gitlab.branch import gitlab_branch_ref
+from heptapod.gitlab.tag import gitlab_tag_ref
+from hgext3rd.heptapod.branch import (
+    gitlab_branches,
+    get_default_gitlab_branch,
+)
+from hgext3rd.heptapod.tag import gitlab_tags
 from hgext3rd.heptapod import ensure_gitlab_special_refs
 from hgext3rd.heptapod import ensure_gitlab_keep_arounds
 from hgext3rd.heptapod.special_ref import (
@@ -46,5 +55,42 @@
     return repo.unfiltered()[sha]
 
 
-def iter_gitlab_special_refs_as_refs(repo, deref=True):
+def path_match(path_segments, pattern_segments):
+    # special case for prefix_matching
+    if pattern_segments[-1] == b'':
+        # direct equality could have issues comparing lists to tuples
+        return all(
+            path_segment == pat_segment
+            for path_segment, pat_segment in zip(path_segments,
+                                                 pattern_segments[:-1]))
+
+    return (len(pattern_segments) == len(path_segments)
+            and all(
+                fnmatch(path_segment, pat_segment)
+                for path_segment, pat_segment in zip(path_segments,
+                                                     pattern_segments)
+                ))
+
+
+def path_match_any(path, patterns):
+    """Tell if path matches at least one of the given glob-like patterns.
+
+    The matching is done as in `git-for-each-ref`, that is either as in
+    `fnmatch(3)` (C stdlib function, matches segment by segment of the path)
+    or as a direct prefix if pattern ends
+    with a slash.
+
+    Because Python's :func:`fnmatch` does not match per slash-separated
+    segment, but on the whole string, considering the slash to be an ordinary
+    character, we implement the segmentation, and hence need patterns to
+    come pre-split at slashes.
+
+    :param patterns: iterable of patterns, each pattern being an iterable
+      of segment level patterns, e.g., ``[b'refs', b'heads', b'branch', b'*']``
+    """
+    path_segments = path.split(b'/')
+    return any(path_match(path_segments, pat) for pat in patterns)
+
+
+def iter_gitlab_special_refs_as_refs(repo, deref=True, patterns=None):
     """Iterate on special refs, yielding full ref paths and target hashes.
@@ -50,4 +96,6 @@
     """Iterate on special refs, yielding full ref paths and target hashes.
+
+    :param patterns: as in :func:`path_match_any` (presplit)
     """
     all_special_refs = special_refs(repo)
     if all_special_refs is GITLAB_TYPED_REFS_MISSING:
@@ -56,7 +104,10 @@
         all_special_refs = ensure_special_refs(repo)
 
     for sref, sha in all_special_refs.items():
-        yield b'refs/' + sref, (repo[sha] if deref else sha.decode('ascii'))
+        ref_path = b'refs/' + sref
+        if patterns and not path_match_any(ref_path, patterns):
+            continue
+        yield ref_path, (repo[sha] if deref else sha.decode('ascii'))
 
 
 def ensure_special_refs(repo):
@@ -82,9 +133,9 @@
     return b'refs/keep-around/' + sha
 
 
-def iter_keep_arounds_as_refs(repo, deref=True):
+def iter_keep_arounds_as_refs(repo, deref=True, patterns=None):
     for sha in iter_keep_arounds(repo):
         if sha is GITLAB_TYPED_REFS_MISSING:
             ensure_gitlab_keep_arounds(repo.ui, repo)
             yield from iter_keep_arounds_as_refs(repo, deref=deref)
             return
@@ -86,10 +137,13 @@
     for sha in iter_keep_arounds(repo):
         if sha is GITLAB_TYPED_REFS_MISSING:
             ensure_gitlab_keep_arounds(repo.ui, repo)
             yield from iter_keep_arounds_as_refs(repo, deref=deref)
             return
-        yield (keep_around_ref_path(sha),
-               (repo[sha] if deref else sha.decode('ascii')))
+        ref_path = keep_around_ref_path(sha)
+        if patterns and not path_match_any(ref_path, patterns):
+            continue
+
+        yield (ref_path, (repo[sha] if deref else sha.decode('ascii')))
 
 
 def sort_key_ref_changeset_date_asc(ref_chgset):
@@ -106,3 +160,36 @@
     :param ref_chgset: a pair made of ref name and changeset (changectx)
     """
     return -ref_chgset[1].date()[0]
+
+
+def reverse_refs(repo):
+    reverse_refs = getattr(repo, '_hgitaly_reverse_refs', None)
+    if reverse_refs is not None:
+        return reverse_refs
+
+    reverse_refs = {}
+    tags = gitlab_tags(repo)
+    if tags is not GITLAB_TYPED_REFS_MISSING:
+        for tag_name, target in tags.items():
+            reverse_refs.setdefault(target, []).append(
+                gitlab_tag_ref(tag_name))
+    special = special_refs(repo)
+    branches = gitlab_branches(repo)
+    if branches is not GITLAB_TYPED_REFS_MISSING:
+        for branch_name, target in branches.items():
+            reverse_refs.setdefault(target, []).append(
+                gitlab_branch_ref(branch_name))
+    if special is not GITLAB_TYPED_REFS_MISSING:
+        for ref_name, target in special.items():
+            reverse_refs.setdefault(target, []).append(b'refs/' + ref_name)
+    default_branch = get_default_gitlab_branch(repo)
+    if (
+            default_branch is not None
+            and branches is not GITLAB_TYPED_REFS_MISSING
+    ):
+        target = branches.get(default_branch)
+        if target is not None:
+            reverse_refs.setdefault(target, []).append(b'HEAD')
+
+    repo._hgitaly_reverse_refs = reverse_refs
+    return reverse_refs
diff --git a/hgitaly/linguist/__init__.py b/hgitaly/linguist/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9saW5ndWlzdC9fX2luaXRfXy5weQ==
--- /dev/null
+++ b/hgitaly/linguist/__init__.py
@@ -0,0 +1,196 @@
+# Copyright 2023 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
+from hashlib import sha256
+import json
+import re
+import subprocess
+
+from grpc import StatusCode
+from importlib_resources import files
+
+
+def read_linguist_languages(path):
+    res = {}
+    with path.open() as langf:
+        for name, details in json.load(langf).items():
+            color = details.get('color')
+            if color is not None:
+                res[name] = color
+    return res
+
+
+LANGUAGE_COLORS = read_linguist_languages(
+    files(__name__).joinpath('languages.json'))
+
+TOKEI_VERSION_RX = re.compile(r'tokei (\d+)\.(\d+)\.(\d+)(\s|$)')
+TOKEI_VERSIONS = {}
+
+
+def language_color(lang_name):
+    """Perform the same defaulting as in Gitaly.
+
+    This is not thread-safe, because it actually updates
+    :data:`LANGUAGE_COLORS` due to defaulting in case the detected language
+    does not have a color there. That is not a problem: HGitaly is not
+    multithreaded (or rather uses a unique service thread at a given time,
+    out of the pool still created by grpcio).
+
+    As of this writing, go-enry v2.8.3 (already used by Gitaly to return the
+    color) defaults to `#cccccc`:
+
+      // GetColor returns a HTML color code of a given language.
+      func GetColor(language string) string {
+        if color, ok := data.LanguagesColor[language]; ok {
+            return color
+        }
+
+        if color, ok := data.LanguagesColor[GetLanguageGroup(language)]; ok {
+            return color
+        }
+
+        return "#cccccc"
+      }
+
+    Gitaly then concludes with (from internal/gitaly/linguist/linguist.go) ::
+
+      // Color returns the color Linguist has assigned to language.
+      func Color(language string) string {
+        if color := enry.GetColor(language); color != "#cccccc" {
+            return color
+        }
+
+        colorSha := sha256.Sum256([]byte(language))
+        return fmt.Sprintf("#%x", colorSha[0:3])
+      }
+    """
+    color = LANGUAGE_COLORS.get(lang_name)
+    if color is not None:
+        return color
+
+    color_sha = sha256(lang_name.encode('utf-8')).hexdigest()
+    color = '#' + color_sha[:6]
+    LANGUAGE_COLORS[lang_name] = color
+    return color
+
+
+class TokeiVersionParseError(RuntimeError):
+    """Indicates that `tokei --version` could not be parsed.
+
+    It is possible that the executable is just not Tokei
+    """
+
+
+def parse_tokei_version(out):
+    """Parse the given ``out``, expected to be output of ``tokei --version``
+
+    :returns: a triple of integers
+    :raises: :class:TokeiVersionParseError
+
+    Currently tested versions::
+
+      >>> parse_tokei_version("tokei 12.0.4")
+      (12, 0, 4)
+      >>> parse_tokei_version("tokei 12.1.2")
+      (12, 1, 2)
+
+    Errors::
+
+      >>> try: parse_tokei_version("Unrelated")
+      ... except Exception as exc: print(repr(exc))
+      TokeiVersionParseError('Unrelated')
+    """
+    match = TOKEI_VERSION_RX.search(out)
+    if match is None:
+        raise TokeiVersionParseError(out)
+    return tuple(int(match.group(i)) for i in (1, 2, 3))
+
+
+def tokei_version(bin_path):
+    version = TOKEI_VERSIONS.get(bin_path)
+    if version is not None:
+        return version
+
+    tokei = subprocess.run((bin_path, '--version'),
+                           encoding='utf-8',
+                           check=True,
+                           capture_output=True
+                           )
+    version = parse_tokei_version(tokei.stdout.strip())
+    TOKEI_VERSIONS[bin_path] = version
+    return version
+
+
+def language_stats(bin_path, path, context):
+    """Return the full language stats working directory at given path.
+
+    :param str bin_path: path to the Tokei executable
+    :param path: the directory to analyze as a :class:`pathlib.Path` instance.
+    """
+    try:
+        version = tokei_version(bin_path)  # may invoke Tokei
+        options = []
+        if version >= (12, 1):
+            options.append('--compact')
+
+        cmd = [bin_path]
+        cmd.extend(options)
+        cmd.extend(('-o', 'json', path))
+        tokei = subprocess.run(
+            cmd,
+            encoding='utf-8',
+            check=True,
+            capture_output=True,
+        )
+    except subprocess.CalledProcessError as exc:
+        # in Gitaly 15.5, the resulting error code is 'INTERNAL', with
+        # details 'language stats: waiting for linguist: exit status %d'
+        context.abort(
+            StatusCode.INTERNAL,
+            "CommitLanguages: tokei error status code=%d stderr=%r" % (
+                exc.returncode, exc.stderr))
+    except FileNotFoundError:
+        # in Gitaly 15.5, if `gitaly-linguist` cannot be found, the resulting
+        # error code is `INTERNAL`, with details 'language stats: waiting
+        # for linguist: exit status 127' (same as rewrapping in a shell)
+        context.abort(
+            StatusCode.INTERNAL,
+            "language stats: tokei executable '%s' not found" % bin_path)
+    except PermissionError:
+        # Gitaly 15.5 behaves similarly in this case as in the not found case,
+        # only with exit status 126 in details (same as in a shell, again)
+        context.abort(
+            StatusCode.INTERNAL,
+            "language stats: permission denied to run "
+            "tokei executable '%s'" % bin_path)
+    except TokeiVersionParseError as exc:
+        context.abort(
+            StatusCode.INTERNAL,
+            "language stats: could not parse Tokei version for executable "
+            "'%s' (got %r). Is it even Tokei?" % (bin_path, exc.args[0]))
+
+    # overriding variable also to free buffer immediately, as it can be
+    # fairly big, e.g. 10MB for heptapod-rails
+    tokei = json.loads(tokei.stdout)
+
+    stats = {}
+    total = 0
+
+    for lang_name, details in tokei.items():
+        # We can't use the `Total` entry because the ratios we have
+        # to produce are expressed in bytes, not lines.
+        if lang_name == 'Total':
+            continue
+
+        per_file = details['reports']
+        total_bytes = 0
+        for report in per_file:
+            total_bytes += (path / report['name']).stat().st_size
+        stats[lang_name] = dict(files=len(per_file),
+                                total_bytes=total_bytes,
+                                )
+        total += total_bytes
+    return stats, total
diff --git a/hgitaly/linguist/languages.json b/hgitaly/linguist/languages.json
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9saW5ndWlzdC9sYW5ndWFnZXMuanNvbg==
--- /dev/null
+++ b/hgitaly/linguist/languages.json
@@ -0,0 +1,1 @@
+{"1C Enterprise":{"type":"programming","color":"#814CCC","extensions":[".bsl",".os"],"tm_scope":"source.bsl","ace_mode":"text","language_id":0},"2-Dimensional Array":{"type":"data","color":"#38761D","extensions":[".2da"],"tm_scope":"source.2da","ace_mode":"text","language_id":387204628},"4D":{"type":"programming","color":"#004289","extensions":[".4dm"],"tm_scope":"source.4dm","ace_mode":"text","language_id":577529595},"ABAP":{"type":"programming","color":"#E8274B","extensions":[".abap"],"tm_scope":"source.abap","ace_mode":"abap","language_id":1},"ABAP CDS":{"type":"programming","color":"#555e25","extensions":[".asddls"],"tm_scope":"source.abapcds","language_id":452681853,"ace_mode":"text"},"ABNF":{"type":"data","ace_mode":"text","extensions":[".abnf"],"tm_scope":"source.abnf","language_id":429},"AGS Script":{"type":"programming","color":"#B9D9FF","aliases":["ags"],"extensions":[".asc",".ash"],"tm_scope":"source.c++","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-c++src","language_id":2},"AIDL":{"type":"programming","color":"#34EB6B","tm_scope":"source.aidl","extensions":[".aidl"],"ace_mode":"text","interpreters":["aidl"],"language_id":451700185},"AL":{"type":"programming","color":"#3AA2B5","extensions":[".al"],"tm_scope":"source.al","ace_mode":"text","language_id":658971832},"AMPL":{"type":"programming","color":"#E6EFBB","extensions":[".ampl",".mod"],"tm_scope":"source.ampl","ace_mode":"text","language_id":3},"ANTLR":{"type":"programming","color":"#9DC3FF","extensions":[".g4"],"tm_scope":"source.antlr","ace_mode":"text","language_id":4},"API Blueprint":{"type":"markup","color":"#2ACCA8","ace_mode":"markdown","extensions":[".apib"],"tm_scope":"text.html.markdown.source.gfm.apib","language_id":5},"APL":{"type":"programming","color":"#5A8164","extensions":[".apl",".dyalog"],"interpreters":["apl","aplx","dyalog"],"tm_scope":"source.apl","ace_mode":"text","codemirror_mode":"apl","codemirror_mime_type":"text/apl","language_id":6},"ASL":{"type":"programming","ace_mode":"text","extensions":[".asl",".dsl"],"tm_scope":"source.asl","language_id":124996147},"ASN.1":{"type":"data","extensions":[".asn",".asn1"],"tm_scope":"source.asn","ace_mode":"text","codemirror_mode":"asn.1","codemirror_mime_type":"text/x-ttcn-asn","language_id":7},"ASP.NET":{"type":"programming","tm_scope":"text.html.asp","color":"#9400ff","aliases":["aspx","aspx-vb"],"extensions":[".asax",".ascx",".ashx",".asmx",".aspx",".axd"],"ace_mode":"text","codemirror_mode":"htmlembedded","codemirror_mime_type":"application/x-aspx","language_id":564186416},"ATS":{"type":"programming","color":"#1ac620","aliases":["ats2"],"extensions":[".dats",".hats",".sats"],"tm_scope":"source.ats","ace_mode":"ocaml","language_id":9},"ActionScript":{"type":"programming","tm_scope":"source.actionscript.3","color":"#882B0F","aliases":["actionscript 3","actionscript3","as3"],"extensions":[".as"],"ace_mode":"actionscript","language_id":10},"Ada":{"type":"programming","color":"#02f88c","extensions":[".adb",".ada",".ads"],"aliases":["ada95","ada2005"],"tm_scope":"source.ada","ace_mode":"ada","language_id":11},"Adobe Font Metrics":{"type":"data","color":"#fa0f00","tm_scope":"source.afm","extensions":[".afm"],"aliases":["acfm","adobe composite font metrics","adobe multiple font metrics","amfm"],"ace_mode":"text","language_id":147198098},"Agda":{"type":"programming","color":"#315665","extensions":[".agda"],"tm_scope":"source.agda","ace_mode":"text","language_id":12},"Alloy":{"type":"programming","color":"#64C800","extensions":[".als"],"tm_scope":"source.alloy","ace_mode":"text","language_id":13},"Alpine Abuild":{"type":"programming","color":"#0D597F","group":"Shell","aliases":["abuild","apkbuild"],"filenames":["APKBUILD"],"tm_scope":"source.shell","ace_mode":"sh","codemirror_mode":"shell","codemirror_mime_type":"text/x-sh","language_id":14},"Altium Designer":{"type":"data","color":"#A89663","aliases":["altium"],"extensions":[".OutJob",".PcbDoc",".PrjPCB",".SchDoc"],"tm_scope":"source.ini","ace_mode":"ini","language_id":187772328},"AngelScript":{"type":"programming","color":"#C7D7DC","extensions":[".as",".angelscript"],"tm_scope":"source.angelscript","ace_mode":"text","codemirror_mode":"clike","codemirror_mime_type":"text/x-c++src","language_id":389477596},"Ant Build System":{"type":"data","color":"#A9157E","tm_scope":"text.xml.ant","filenames":["ant.xml","build.xml"],"ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"application/xml","language_id":15},"ApacheConf":{"type":"data","color":"#d12127","aliases":["aconf","apache"],"extensions":[".apacheconf",".vhost"],"filenames":[".htaccess","apache2.conf","httpd.conf"],"tm_scope":"source.apache-config","ace_mode":"apache_conf","language_id":16},"Apex":{"type":"programming","color":"#1797c0","extensions":[".cls"],"tm_scope":"source.java","ace_mode":"java","codemirror_mode":"clike","codemirror_mime_type":"text/x-java","language_id":17},"Apollo Guidance Computer":{"type":"programming","color":"#0B3D91","group":"Assembly","extensions":[".agc"],"tm_scope":"source.agc","ace_mode":"assembly_x86","language_id":18},"AppleScript":{"type":"programming","aliases":["osascript"],"extensions":[".applescript",".scpt"],"interpreters":["osascript"],"tm_scope":"source.applescript","ace_mode":"applescript","color":"#101F1F","language_id":19},"Arc":{"type":"programming","color":"#aa2afe","extensions":[".arc"],"tm_scope":"none","ace_mode":"text","language_id":20},"AsciiDoc":{"type":"prose","color":"#73a0c5","ace_mode":"asciidoc","wrap":true,"extensions":[".asciidoc",".adoc",".asc"],"tm_scope":"text.html.asciidoc","language_id":22},"AspectJ":{"type":"programming","color":"#a957b0","extensions":[".aj"],"tm_scope":"source.aspectj","ace_mode":"text","language_id":23},"Assembly":{"type":"programming","color":"#6E4C13","aliases":["asm","nasm"],"extensions":[".asm",".a51",".i",".inc",".nasm"],"tm_scope":"source.assembly","ace_mode":"assembly_x86","language_id":24},"Astro":{"type":"markup","color":"#ff5a03","extensions":[".astro"],"tm_scope":"source.astro","ace_mode":"html","codemirror_mode":"jsx","codemirror_mime_type":"text/jsx","language_id":578209015},"Asymptote":{"type":"programming","color":"#ff0000","extensions":[".asy"],"interpreters":["asy"],"tm_scope":"source.c++","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-kotlin","language_id":591605007},"Augeas":{"type":"programming","color":"#9CC134","extensions":[".aug"],"tm_scope":"none","ace_mode":"text","language_id":25},"AutoHotkey":{"type":"programming","color":"#6594b9","aliases":["ahk"],"extensions":[".ahk",".ahkl"],"tm_scope":"source.ahk","ace_mode":"autohotkey","language_id":26},"AutoIt":{"type":"programming","color":"#1C3552","aliases":["au3","AutoIt3","AutoItScript"],"extensions":[".au3"],"tm_scope":"source.autoit","ace_mode":"autohotkey","language_id":27},"Avro IDL":{"type":"data","color":"#0040FF","extensions":[".avdl"],"tm_scope":"source.avro","ace_mode":"text","language_id":785497837},"Awk":{"type":"programming","color":"#c30e9b","extensions":[".awk",".auk",".gawk",".mawk",".nawk"],"interpreters":["awk","gawk","mawk","nawk"],"tm_scope":"source.awk","ace_mode":"text","language_id":28},"BASIC":{"type":"programming","extensions":[".bas"],"tm_scope":"source.basic","ace_mode":"text","color":"#ff0000","language_id":28923963},"Ballerina":{"type":"programming","extensions":[".bal"],"tm_scope":"source.ballerina","ace_mode":"text","color":"#FF5000","language_id":720859680},"Batchfile":{"type":"programming","aliases":["bat","batch","dosbatch","winbatch"],"extensions":[".bat",".cmd"],"tm_scope":"source.batchfile","ace_mode":"batchfile","color":"#C1F12E","language_id":29},"Beef":{"type":"programming","color":"#a52f4e","extensions":[".bf"],"tm_scope":"source.cs","ace_mode":"csharp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csharp","language_id":545626333},"Befunge":{"type":"programming","extensions":[".befunge"],"tm_scope":"source.befunge","ace_mode":"text","language_id":30},"Berry":{"type":"programming","extensions":[".be"],"tm_scope":"source.berry","ace_mode":"text","color":"#15A13C","aliases":["be"],"language_id":121855308},"BibTeX":{"type":"markup","color":"#778899","group":"TeX","extensions":[".bib",".bibtex"],"tm_scope":"text.bibtex","ace_mode":"tex","codemirror_mode":"stex","codemirror_mime_type":"text/x-stex","language_id":982188347},"Bicep":{"type":"programming","color":"#519aba","extensions":[".bicep"],"tm_scope":"source.bicep","ace_mode":"text","language_id":321200902},"Bison":{"type":"programming","color":"#6A463F","group":"Yacc","tm_scope":"source.yacc","extensions":[".bison"],"ace_mode":"text","language_id":31},"BitBake":{"type":"programming","color":"#00bce4","tm_scope":"none","extensions":[".bb"],"ace_mode":"text","language_id":32},"Blade":{"type":"markup","color":"#f7523f","extensions":[".blade",".blade.php"],"tm_scope":"text.html.php.blade","ace_mode":"text","language_id":33},"BlitzBasic":{"type":"programming","color":"#00FFAE","aliases":["b3d","blitz3d","blitzplus","bplus"],"extensions":[".bb",".decls"],"tm_scope":"source.blitzmax","ace_mode":"text","language_id":34},"BlitzMax":{"type":"programming","color":"#cd6400","extensions":[".bmx"],"aliases":["bmax"],"tm_scope":"source.blitzmax","ace_mode":"text","language_id":35},"Bluespec":{"type":"programming","color":"#12223c","extensions":[".bsv"],"tm_scope":"source.bsv","ace_mode":"verilog","language_id":36},"Boo":{"type":"programming","color":"#d4bec1","extensions":[".boo"],"ace_mode":"text","tm_scope":"source.boo","language_id":37},"Boogie":{"type":"programming","color":"#c80fa0","extensions":[".bpl"],"interpreters":["boogie"],"tm_scope":"source.boogie","ace_mode":"text","language_id":955017407},"Brainfuck":{"type":"programming","color":"#2F2530","extensions":[".b",".bf"],"tm_scope":"source.bf","ace_mode":"text","codemirror_mode":"brainfuck","codemirror_mime_type":"text/x-brainfuck","language_id":38},"Brightscript":{"type":"programming","color":"#662D91","extensions":[".brs"],"tm_scope":"source.brightscript","ace_mode":"text","language_id":39},"Browserslist":{"type":"data","color":"#ffd539","filenames":[".browserslistrc","browserslist"],"tm_scope":"text.browserslist","ace_mode":"text","language_id":153503348},"C":{"type":"programming","color":"#555555","extensions":[".c",".cats",".h",".idc"],"interpreters":["tcc"],"tm_scope":"source.c","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","language_id":41},"C#":{"type":"programming","ace_mode":"csharp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csharp","tm_scope":"source.cs","color":"#178600","aliases":["csharp","cake","cakescript"],"extensions":[".cs",".cake",".csx",".linq"],"language_id":42},"C++":{"type":"programming","tm_scope":"source.c++","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-c++src","color":"#f34b7d","aliases":["cpp"],"extensions":[".cpp",".c++",".cc",".cp",".cxx",".h",".h++",".hh",".hpp",".hxx",".inc",".inl",".ino",".ipp",".ixx",".re",".tcc",".tpp"],"language_id":43},"C-ObjDump":{"type":"data","extensions":[".c-objdump"],"tm_scope":"objdump.x86asm","ace_mode":"assembly_x86","language_id":44},"C2hs Haskell":{"type":"programming","group":"Haskell","aliases":["c2hs"],"extensions":[".chs"],"tm_scope":"source.haskell","ace_mode":"haskell","codemirror_mode":"haskell","codemirror_mime_type":"text/x-haskell","language_id":45},"CIL":{"type":"data","tm_scope":"source.cil","extensions":[".cil"],"ace_mode":"text","language_id":29176339},"CLIPS":{"type":"programming","color":"#00A300","extensions":[".clp"],"tm_scope":"source.clips","ace_mode":"text","language_id":46},"CMake":{"type":"programming","color":"#DA3434","extensions":[".cmake",".cmake.in"],"filenames":["CMakeLists.txt"],"tm_scope":"source.cmake","ace_mode":"text","codemirror_mode":"cmake","codemirror_mime_type":"text/x-cmake","language_id":47},"COBOL":{"type":"programming","extensions":[".cob",".cbl",".ccp",".cobol",".cpy"],"tm_scope":"source.cobol","ace_mode":"cobol","codemirror_mode":"cobol","codemirror_mime_type":"text/x-cobol","language_id":48},"CODEOWNERS":{"type":"data","filenames":["CODEOWNERS"],"tm_scope":"text.codeowners","ace_mode":"gitignore","language_id":321684729},"COLLADA":{"type":"data","color":"#F1A42B","extensions":[".dae"],"tm_scope":"text.xml","ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"text/xml","language_id":49},"CSON":{"type":"data","color":"#244776","tm_scope":"source.coffee","ace_mode":"coffee","codemirror_mode":"coffeescript","codemirror_mime_type":"text/x-coffeescript","extensions":[".cson"],"language_id":424},"CSS":{"type":"markup","tm_scope":"source.css","ace_mode":"css","codemirror_mode":"css","codemirror_mime_type":"text/css","color":"#563d7c","extensions":[".css"],"language_id":50},"CSV":{"type":"data","color":"#237346","ace_mode":"text","tm_scope":"none","extensions":[".csv"],"language_id":51},"CUE":{"type":"programming","extensions":[".cue"],"tm_scope":"source.cue","ace_mode":"text","color":"#5886E1","language_id":356063509},"CWeb":{"type":"programming","color":"#00007a","extensions":[".w"],"tm_scope":"none","ace_mode":"text","language_id":657332628},"Cabal Config":{"type":"data","color":"#483465","aliases":["Cabal"],"extensions":[".cabal"],"filenames":["cabal.config","cabal.project"],"ace_mode":"haskell","codemirror_mode":"haskell","codemirror_mime_type":"text/x-haskell","tm_scope":"source.cabal","language_id":677095381},"Cadence":{"type":"programming","color":"#00ef8b","ace_mode":"text","tm_scope":"source.cadence","extensions":[".cdc"],"language_id":270184138},"Cairo":{"type":"programming","color":"#ff4a48","ace_mode":"text","tm_scope":"source.cairo","extensions":[".cairo"],"language_id":620599567},"CameLIGO":{"type":"programming","color":"#3be133","extensions":[".mligo"],"tm_scope":"source.mligo","ace_mode":"ocaml","codemirror_mode":"mllike","codemirror_mime_type":"text/x-ocaml","group":"LigoLANG","language_id":829207807},"Cap'n Proto":{"type":"programming","color":"#c42727","tm_scope":"source.capnp","extensions":[".capnp"],"ace_mode":"text","language_id":52},"CartoCSS":{"type":"programming","aliases":["Carto"],"extensions":[".mss"],"ace_mode":"text","tm_scope":"source.css.mss","language_id":53},"Ceylon":{"type":"programming","color":"#dfa535","extensions":[".ceylon"],"tm_scope":"source.ceylon","ace_mode":"text","language_id":54},"Chapel":{"type":"programming","color":"#8dc63f","aliases":["chpl"],"extensions":[".chpl"],"tm_scope":"source.chapel","ace_mode":"text","language_id":55},"Charity":{"type":"programming","extensions":[".ch"],"tm_scope":"none","ace_mode":"text","language_id":56},"ChucK":{"type":"programming","color":"#3f8000","extensions":[".ck"],"tm_scope":"source.java","ace_mode":"java","codemirror_mode":"clike","codemirror_mime_type":"text/x-java","language_id":57},"Cirru":{"type":"programming","color":"#ccccff","tm_scope":"source.cirru","ace_mode":"cirru","extensions":[".cirru"],"language_id":58},"Clarion":{"type":"programming","color":"#db901e","ace_mode":"text","extensions":[".clw"],"tm_scope":"source.clarion","language_id":59},"Clarity":{"type":"programming","color":"#5546ff","ace_mode":"lisp","extensions":[".clar"],"tm_scope":"source.clar","language_id":91493841},"Classic ASP":{"type":"programming","color":"#6a40fd","tm_scope":"text.html.asp","aliases":["asp"],"extensions":[".asp"],"ace_mode":"text","language_id":8},"Clean":{"type":"programming","color":"#3F85AF","extensions":[".icl",".dcl"],"tm_scope":"source.clean","ace_mode":"text","language_id":60},"Click":{"type":"programming","color":"#E4E6F3","extensions":[".click"],"tm_scope":"source.click","ace_mode":"text","language_id":61},"Clojure":{"type":"programming","tm_scope":"source.clojure","ace_mode":"clojure","codemirror_mode":"clojure","codemirror_mime_type":"text/x-clojure","color":"#db5855","extensions":[".clj",".boot",".cl2",".cljc",".cljs",".cljs.hl",".cljscm",".cljx",".hic"],"filenames":["riemann.config"],"language_id":62},"Closure Templates":{"type":"markup","color":"#0d948f","ace_mode":"soy_template","codemirror_mode":"soy","codemirror_mime_type":"text/x-soy","aliases":["soy"],"extensions":[".soy"],"tm_scope":"text.html.soy","language_id":357046146},"Cloud Firestore Security Rules":{"type":"data","color":"#FFA000","ace_mode":"less","codemirror_mode":"css","codemirror_mime_type":"text/css","tm_scope":"source.firestore","filenames":["firestore.rules"],"language_id":407996372},"CoNLL-U":{"type":"data","extensions":[".conllu",".conll"],"tm_scope":"text.conllu","ace_mode":"text","aliases":["CoNLL","CoNLL-X"],"language_id":421026389},"CodeQL":{"type":"programming","color":"#140f46","extensions":[".ql",".qll"],"tm_scope":"source.ql","ace_mode":"text","language_id":424259634,"aliases":["ql"]},"CoffeeScript":{"type":"programming","tm_scope":"source.coffee","ace_mode":"coffee","codemirror_mode":"coffeescript","codemirror_mime_type":"text/x-coffeescript","color":"#244776","aliases":["coffee","coffee-script"],"extensions":[".coffee","._coffee",".cake",".cjsx",".iced"],"filenames":["Cakefile"],"interpreters":["coffee"],"language_id":63},"ColdFusion":{"type":"programming","ace_mode":"coldfusion","color":"#ed2cd6","aliases":["cfm","cfml","coldfusion html"],"extensions":[".cfm",".cfml"],"tm_scope":"text.html.cfm","language_id":64},"ColdFusion CFC":{"type":"programming","color":"#ed2cd6","group":"ColdFusion","ace_mode":"coldfusion","aliases":["cfc"],"extensions":[".cfc"],"tm_scope":"source.cfscript","language_id":65},"Common Lisp":{"type":"programming","tm_scope":"source.lisp","color":"#3fb68b","aliases":["lisp"],"extensions":[".lisp",".asd",".cl",".l",".lsp",".ny",".podsl",".sexp"],"interpreters":["lisp","sbcl","ccl","clisp","ecl"],"ace_mode":"lisp","codemirror_mode":"commonlisp","codemirror_mime_type":"text/x-common-lisp","language_id":66},"Common Workflow Language":{"aliases":["cwl"],"type":"programming","ace_mode":"yaml","codemirror_mode":"yaml","codemirror_mime_type":"text/x-yaml","extensions":[".cwl"],"interpreters":["cwl-runner"],"color":"#B5314C","tm_scope":"source.cwl","language_id":988547172},"Component Pascal":{"type":"programming","color":"#B0CE4E","extensions":[".cp",".cps"],"tm_scope":"source.pascal","ace_mode":"pascal","codemirror_mode":"pascal","codemirror_mime_type":"text/x-pascal","language_id":67},"Cool":{"type":"programming","extensions":[".cl"],"tm_scope":"source.cool","ace_mode":"text","language_id":68},"Coq":{"type":"programming","color":"#d0b68c","extensions":[".coq",".v"],"tm_scope":"source.coq","ace_mode":"text","language_id":69},"Cpp-ObjDump":{"type":"data","extensions":[".cppobjdump",".c++-objdump",".c++objdump",".cpp-objdump",".cxx-objdump"],"tm_scope":"objdump.x86asm","aliases":["c++-objdump"],"ace_mode":"assembly_x86","language_id":70},"Creole":{"type":"prose","wrap":true,"extensions":[".creole"],"tm_scope":"text.html.creole","ace_mode":"text","language_id":71},"Crystal":{"type":"programming","color":"#000100","extensions":[".cr"],"ace_mode":"ruby","codemirror_mode":"crystal","codemirror_mime_type":"text/x-crystal","tm_scope":"source.crystal","interpreters":["crystal"],"language_id":72},"Csound":{"type":"programming","color":"#1a1a1a","aliases":["csound-orc"],"extensions":[".orc",".udo"],"tm_scope":"source.csound","ace_mode":"csound_orchestra","language_id":73},"Csound Document":{"type":"programming","color":"#1a1a1a","aliases":["csound-csd"],"extensions":[".csd"],"tm_scope":"source.csound-document","ace_mode":"csound_document","language_id":74},"Csound Score":{"type":"programming","color":"#1a1a1a","aliases":["csound-sco"],"extensions":[".sco"],"tm_scope":"source.csound-score","ace_mode":"csound_score","language_id":75},"Cuda":{"type":"programming","extensions":[".cu",".cuh"],"tm_scope":"source.cuda-c++","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-c++src","color":"#3A4E3A","language_id":77},"Cue Sheet":{"type":"data","extensions":[".cue"],"tm_scope":"source.cuesheet","ace_mode":"text","language_id":942714150},"Curry":{"type":"programming","color":"#531242","extensions":[".curry"],"tm_scope":"source.curry","ace_mode":"haskell","language_id":439829048},"Cycript":{"type":"programming","extensions":[".cy"],"tm_scope":"source.js","ace_mode":"javascript","codemirror_mode":"javascript","codemirror_mime_type":"text/javascript","language_id":78},"Cython":{"type":"programming","color":"#fedf5b","extensions":[".pyx",".pxd",".pxi"],"aliases":["pyrex"],"tm_scope":"source.cython","ace_mode":"text","codemirror_mode":"python","codemirror_mime_type":"text/x-cython","language_id":79},"D":{"type":"programming","color":"#ba595e","aliases":["Dlang"],"extensions":[".d",".di"],"tm_scope":"source.d","ace_mode":"d","codemirror_mode":"d","codemirror_mime_type":"text/x-d","language_id":80},"D-ObjDump":{"type":"data","extensions":[".d-objdump"],"tm_scope":"objdump.x86asm","ace_mode":"assembly_x86","language_id":81},"DIGITAL Command Language":{"type":"programming","aliases":["dcl"],"extensions":[".com"],"tm_scope":"none","ace_mode":"text","language_id":82},"DM":{"type":"programming","color":"#447265","extensions":[".dm"],"aliases":["byond"],"tm_scope":"source.dm","ace_mode":"c_cpp","language_id":83},"DNS Zone":{"type":"data","extensions":[".zone",".arpa"],"tm_scope":"text.zone_file","ace_mode":"text","language_id":84},"DTrace":{"type":"programming","aliases":["dtrace-script"],"extensions":[".d"],"interpreters":["dtrace"],"tm_scope":"source.c","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","language_id":85},"Dafny":{"type":"programming","color":"#FFEC25","extensions":[".dfy"],"interpreters":["dafny"],"tm_scope":"text.dfy.dafny","ace_mode":"text","language_id":969323346},"Darcs Patch":{"type":"data","color":"#8eff23","aliases":["dpatch"],"extensions":[".darcspatch",".dpatch"],"tm_scope":"none","ace_mode":"text","language_id":86},"Dart":{"type":"programming","color":"#00B4AB","extensions":[".dart"],"interpreters":["dart"],"tm_scope":"source.dart","ace_mode":"dart","codemirror_mode":"dart","codemirror_mime_type":"application/dart","language_id":87},"DataWeave":{"type":"programming","color":"#003a52","extensions":[".dwl"],"ace_mode":"text","tm_scope":"source.data-weave","language_id":974514097},"Debian Package Control File":{"type":"data","color":"#D70751","extensions":[".dsc"],"tm_scope":"source.deb-control","ace_mode":"text","language_id":527438264},"DenizenScript":{"type":"programming","color":"#FBEE96","ace_mode":"yaml","codemirror_mode":"yaml","codemirror_mime_type":"text/x-yaml","extensions":[".dsc"],"tm_scope":"source.denizenscript","language_id":435000929},"Dhall":{"type":"programming","color":"#dfafff","extensions":[".dhall"],"tm_scope":"source.haskell","ace_mode":"haskell","codemirror_mode":"haskell","codemirror_mime_type":"text/x-haskell","language_id":793969321},"Diff":{"type":"data","extensions":[".diff",".patch"],"aliases":["udiff"],"tm_scope":"source.diff","ace_mode":"diff","codemirror_mode":"diff","codemirror_mime_type":"text/x-diff","language_id":88},"DirectX 3D File":{"type":"data","color":"#aace60","extensions":[".x"],"ace_mode":"text","tm_scope":"none","language_id":201049282},"Dockerfile":{"type":"programming","aliases":["Containerfile"],"color":"#384d54","tm_scope":"source.dockerfile","extensions":[".dockerfile"],"filenames":["Containerfile","Dockerfile"],"ace_mode":"dockerfile","codemirror_mode":"dockerfile","codemirror_mime_type":"text/x-dockerfile","language_id":89},"Dogescript":{"type":"programming","color":"#cca760","extensions":[".djs"],"tm_scope":"none","ace_mode":"text","language_id":90},"Dylan":{"type":"programming","color":"#6c616e","extensions":[".dylan",".dyl",".intr",".lid"],"tm_scope":"source.dylan","ace_mode":"text","codemirror_mode":"dylan","codemirror_mime_type":"text/x-dylan","language_id":91},"E":{"type":"programming","color":"#ccce35","extensions":[".e"],"interpreters":["rune"],"tm_scope":"none","ace_mode":"text","language_id":92},"E-mail":{"type":"data","aliases":["email","eml","mail","mbox"],"extensions":[".eml",".mbox"],"tm_scope":"text.eml.basic","ace_mode":"text","codemirror_mode":"mbox","codemirror_mime_type":"application/mbox","language_id":529653389},"EBNF":{"type":"data","extensions":[".ebnf"],"tm_scope":"source.ebnf","ace_mode":"text","codemirror_mode":"ebnf","codemirror_mime_type":"text/x-ebnf","language_id":430},"ECL":{"type":"programming","color":"#8a1267","extensions":[".ecl",".eclxml"],"tm_scope":"source.ecl","ace_mode":"text","codemirror_mode":"ecl","codemirror_mime_type":"text/x-ecl","language_id":93},"ECLiPSe":{"type":"programming","color":"#001d9d","group":"prolog","extensions":[".ecl"],"tm_scope":"source.prolog.eclipse","ace_mode":"prolog","language_id":94},"EJS":{"type":"markup","color":"#a91e50","extensions":[".ejs",".ect",".ejs.t",".jst"],"tm_scope":"text.html.js","ace_mode":"ejs","language_id":95},"EQ":{"type":"programming","color":"#a78649","extensions":[".eq"],"tm_scope":"source.cs","ace_mode":"csharp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csharp","language_id":96},"Eagle":{"type":"data","extensions":[".sch",".brd"],"tm_scope":"text.xml","ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"text/xml","language_id":97},"Earthly":{"type":"programming","aliases":["Earthfile"],"color":"#2af0ff","tm_scope":"source.earthfile","ace_mode":"text","filenames":["Earthfile"],"language_id":963512632},"Easybuild":{"type":"data","color":"#069406","group":"Python","ace_mode":"python","codemirror_mode":"python","codemirror_mime_type":"text/x-python","tm_scope":"source.python","extensions":[".eb"],"language_id":342840477},"Ecere Projects":{"type":"data","color":"#913960","group":"JavaScript","extensions":[".epj"],"tm_scope":"source.json","ace_mode":"json","codemirror_mode":"javascript","codemirror_mime_type":"application/json","language_id":98},"EditorConfig":{"type":"data","color":"#fff1f2","group":"INI","filenames":[".editorconfig"],"aliases":["editor-config"],"ace_mode":"ini","codemirror_mode":"properties","codemirror_mime_type":"text/x-properties","tm_scope":"source.editorconfig","language_id":96139566},"Edje Data Collection":{"type":"data","extensions":[".edc"],"tm_scope":"source.c++","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-c++src","language_id":342840478},"Eiffel":{"type":"programming","color":"#4d6977","extensions":[".e"],"tm_scope":"source.eiffel","ace_mode":"eiffel","codemirror_mode":"eiffel","codemirror_mime_type":"text/x-eiffel","language_id":99},"Elixir":{"type":"programming","color":"#6e4a7e","extensions":[".ex",".exs"],"tm_scope":"source.elixir","ace_mode":"elixir","filenames":["mix.lock"],"interpreters":["elixir"],"language_id":100},"Elm":{"type":"programming","color":"#60B5CC","extensions":[".elm"],"tm_scope":"source.elm","ace_mode":"elm","codemirror_mode":"elm","codemirror_mime_type":"text/x-elm","language_id":101},"Emacs Lisp":{"type":"programming","tm_scope":"source.emacs.lisp","color":"#c065db","aliases":["elisp","emacs"],"filenames":[".abbrev_defs",".emacs",".emacs.desktop",".gnus",".spacemacs",".viper","Cask","Project.ede","_emacs","abbrev_defs"],"extensions":[".el",".emacs",".emacs.desktop"],"ace_mode":"lisp","codemirror_mode":"commonlisp","codemirror_mime_type":"text/x-common-lisp","language_id":102},"EmberScript":{"type":"programming","color":"#FFF4F3","extensions":[".em",".emberscript"],"tm_scope":"source.coffee","ace_mode":"coffee","codemirror_mode":"coffeescript","codemirror_mime_type":"text/x-coffeescript","language_id":103},"Erlang":{"type":"programming","color":"#B83998","extensions":[".erl",".app.src",".es",".escript",".hrl",".xrl",".yrl"],"filenames":["Emakefile","rebar.config","rebar.config.lock","rebar.lock"],"tm_scope":"source.erlang","ace_mode":"erlang","codemirror_mode":"erlang","codemirror_mime_type":"text/x-erlang","interpreters":["escript"],"language_id":104},"Euphoria":{"type":"programming","color":"#FF790B","extensions":[".e",".ex"],"interpreters":["eui","euiw"],"ace_mode":"text","tm_scope":"source.euphoria","language_id":880693982},"F#":{"type":"programming","color":"#b845fc","aliases":["fsharp"],"extensions":[".fs",".fsi",".fsx"],"tm_scope":"source.fsharp","ace_mode":"text","codemirror_mode":"mllike","codemirror_mime_type":"text/x-fsharp","language_id":105},"F*":{"fs_name":"Fstar","type":"programming","color":"#572e30","aliases":["fstar"],"extensions":[".fst"],"tm_scope":"source.fstar","ace_mode":"text","language_id":336943375},"FIGlet Font":{"type":"data","color":"#FFDDBB","aliases":["FIGfont"],"extensions":[".flf"],"tm_scope":"source.figfont","ace_mode":"text","language_id":686129783},"FLUX":{"type":"programming","color":"#88ccff","extensions":[".fx",".flux"],"tm_scope":"none","ace_mode":"text","language_id":106},"Factor":{"type":"programming","color":"#636746","extensions":[".factor"],"filenames":[".factor-boot-rc",".factor-rc"],"tm_scope":"source.factor","ace_mode":"text","codemirror_mode":"factor","codemirror_mime_type":"text/x-factor","language_id":108},"Fancy":{"type":"programming","color":"#7b9db4","extensions":[".fy",".fancypack"],"filenames":["Fakefile"],"tm_scope":"source.fancy","ace_mode":"text","language_id":109},"Fantom":{"type":"programming","color":"#14253c","extensions":[".fan"],"tm_scope":"source.fan","ace_mode":"text","language_id":110},"Faust":{"type":"programming","color":"#c37240","extensions":[".dsp"],"tm_scope":"source.faust","ace_mode":"text","language_id":622529198},"Fennel":{"type":"programming","tm_scope":"source.fnl","ace_mode":"text","color":"#fff3d7","interpreters":["fennel"],"extensions":[".fnl"],"language_id":239946126},"Filebench WML":{"type":"programming","color":"#F6B900","extensions":[".f"],"tm_scope":"none","ace_mode":"text","language_id":111},"Filterscript":{"type":"programming","group":"RenderScript","extensions":[".fs"],"tm_scope":"none","ace_mode":"text","language_id":112},"Fluent":{"type":"programming","color":"#ffcc33","extensions":[".ftl"],"tm_scope":"source.ftl","ace_mode":"text","language_id":206353404},"Formatted":{"type":"data","extensions":[".for",".eam.fs"],"tm_scope":"none","ace_mode":"text","language_id":113},"Forth":{"type":"programming","color":"#341708","extensions":[".fth",".4th",".f",".for",".forth",".fr",".frt",".fs"],"tm_scope":"source.forth","ace_mode":"forth","codemirror_mode":"forth","codemirror_mime_type":"text/x-forth","language_id":114},"Fortran":{"group":"Fortran","type":"programming","color":"#4d41b1","extensions":[".f",".f77",".for",".fpp"],"tm_scope":"source.fortran","ace_mode":"text","codemirror_mode":"fortran","codemirror_mime_type":"text/x-fortran","language_id":107},"Fortran Free Form":{"group":"Fortran","color":"#4d41b1","type":"programming","extensions":[".f90",".f03",".f08",".f95"],"tm_scope":"source.fortran.modern","ace_mode":"text","codemirror_mode":"fortran","codemirror_mime_type":"text/x-fortran","language_id":761352333},"FreeBasic":{"type":"programming","color":"#867db1","extensions":[".bi",".bas"],"tm_scope":"source.vbnet","aliases":["fb"],"ace_mode":"text","codemirror_mode":"vb","codemirror_mime_type":"text/x-vb","language_id":472896659},"FreeMarker":{"type":"programming","color":"#0050b2","aliases":["ftl"],"extensions":[".ftl"],"tm_scope":"text.html.ftl","ace_mode":"ftl","language_id":115},"Frege":{"type":"programming","color":"#00cafe","extensions":[".fr"],"tm_scope":"source.haskell","ace_mode":"haskell","language_id":116},"Futhark":{"type":"programming","color":"#5f021f","extensions":[".fut"],"tm_scope":"source.futhark","ace_mode":"text","language_id":97358117},"G-code":{"type":"programming","color":"#D08CF2","extensions":[".g",".cnc",".gco",".gcode"],"tm_scope":"source.gcode","ace_mode":"gcode","language_id":117},"GAML":{"type":"programming","color":"#FFC766","extensions":[".gaml"],"tm_scope":"none","ace_mode":"text","language_id":290345951},"GAMS":{"type":"programming","color":"#f49a22","extensions":[".gms"],"tm_scope":"none","ace_mode":"text","language_id":118},"GAP":{"type":"programming","color":"#0000cc","extensions":[".g",".gap",".gd",".gi",".tst"],"tm_scope":"source.gap","ace_mode":"text","language_id":119},"GCC Machine Description":{"type":"programming","color":"#FFCFAB","extensions":[".md"],"tm_scope":"source.lisp","ace_mode":"lisp","codemirror_mode":"commonlisp","codemirror_mime_type":"text/x-common-lisp","language_id":121},"GDB":{"type":"programming","extensions":[".gdb",".gdbinit"],"tm_scope":"source.gdb","ace_mode":"text","language_id":122},"GDScript":{"type":"programming","color":"#355570","extensions":[".gd"],"tm_scope":"source.gdscript","ace_mode":"text","language_id":123},"GEDCOM":{"type":"data","color":"#003058","ace_mode":"text","extensions":[".ged"],"tm_scope":"source.gedcom","language_id":459577965},"GLSL":{"type":"programming","color":"#5686a5","extensions":[".glsl",".fp",".frag",".frg",".fs",".fsh",".fshader",".geo",".geom",".glslf",".glslv",".gs",".gshader",".rchit",".rmiss",".shader",".tesc",".tese",".vert",".vrx",".vsh",".vshader"],"tm_scope":"source.glsl","ace_mode":"glsl","language_id":124},"GN":{"type":"data","extensions":[".gn",".gni"],"interpreters":["gn"],"filenames":[".gn"],"tm_scope":"source.gn","ace_mode":"python","codemirror_mode":"python","codemirror_mime_type":"text/x-python","language_id":302957008},"GSC":{"type":"programming","color":"#FF6800","extensions":[".gsc",".csc",".gsh"],"tm_scope":"source.gsc","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","language_id":257856279},"Game Maker Language":{"type":"programming","color":"#71b417","extensions":[".gml"],"tm_scope":"source.c++","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-c++src","language_id":125},"Gemfile.lock":{"type":"data","color":"#701516","searchable":false,"tm_scope":"source.gemfile-lock","ace_mode":"text","filenames":["Gemfile.lock"],"language_id":907065713},"Genero":{"type":"programming","color":"#63408e","extensions":[".4gl"],"tm_scope":"source.genero","ace_mode":"text","language_id":986054050},"Genero Forms":{"type":"markup","color":"#d8df39","extensions":[".per"],"tm_scope":"source.genero-forms","ace_mode":"text","language_id":902995658},"Genie":{"type":"programming","ace_mode":"text","extensions":[".gs"],"color":"#fb855d","tm_scope":"none","language_id":792408528},"Genshi":{"type":"programming","color":"#951531","extensions":[".kid"],"tm_scope":"text.xml.genshi","aliases":["xml+genshi","xml+kid"],"ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"text/xml","language_id":126},"Gentoo Ebuild":{"type":"programming","color":"#9400ff","group":"Shell","extensions":[".ebuild"],"tm_scope":"source.shell","ace_mode":"sh","codemirror_mode":"shell","codemirror_mime_type":"text/x-sh","language_id":127},"Gentoo Eclass":{"type":"programming","color":"#9400ff","group":"Shell","extensions":[".eclass"],"tm_scope":"source.shell","ace_mode":"sh","codemirror_mode":"shell","codemirror_mime_type":"text/x-sh","language_id":128},"Gerber Image":{"type":"data","color":"#d20b00","aliases":["rs-274x"],"extensions":[".gbr",".cmp",".gbl",".gbo",".gbp",".gbs",".gko",".gml",".gpb",".gpt",".gtl",".gto",".gtp",".gts",".ncl",".sol"],"interpreters":["gerbv","gerbview"],"tm_scope":"source.gerber","ace_mode":"text","language_id":404627610},"Gettext Catalog":{"type":"prose","aliases":["pot"],"extensions":[".po",".pot"],"tm_scope":"source.po","ace_mode":"text","language_id":129},"Gherkin":{"type":"programming","extensions":[".feature",".story"],"tm_scope":"text.gherkin.feature","aliases":["cucumber"],"ace_mode":"text","color":"#5B2063","language_id":76},"Git Attributes":{"type":"data","color":"#F44D27","group":"INI","aliases":["gitattributes"],"filenames":[".gitattributes"],"tm_scope":"source.gitattributes","ace_mode":"gitignore","codemirror_mode":"shell","codemirror_mime_type":"text/x-sh","language_id":956324166},"Git Config":{"type":"data","color":"#F44D27","group":"INI","aliases":["gitconfig","gitmodules"],"extensions":[".gitconfig"],"filenames":[".gitconfig",".gitmodules"],"ace_mode":"ini","codemirror_mode":"properties","codemirror_mime_type":"text/x-properties","tm_scope":"source.gitconfig","language_id":807968997},"Gleam":{"type":"programming","color":"#ffaff3","ace_mode":"text","extensions":[".gleam"],"tm_scope":"source.gleam","language_id":1054258749},"Glyph":{"type":"programming","color":"#c1ac7f","extensions":[".glf"],"tm_scope":"source.tcl","ace_mode":"tcl","codemirror_mode":"tcl","codemirror_mime_type":"text/x-tcl","language_id":130},"Glyph Bitmap Distribution Format":{"type":"data","extensions":[".bdf"],"tm_scope":"source.bdf","ace_mode":"text","language_id":997665271},"Gnuplot":{"type":"programming","color":"#f0a9f0","extensions":[".gp",".gnu",".gnuplot",".p",".plot",".plt"],"interpreters":["gnuplot"],"tm_scope":"source.gnuplot","ace_mode":"text","language_id":131},"Go":{"type":"programming","color":"#00ADD8","aliases":["golang"],"extensions":[".go"],"tm_scope":"source.go","ace_mode":"golang","codemirror_mode":"go","codemirror_mime_type":"text/x-go","language_id":132},"Go Checksums":{"type":"data","color":"#00ADD8","aliases":["go.sum","go sum"],"filenames":["go.sum"],"tm_scope":"go.sum","ace_mode":"text","language_id":1054391671},"Go Module":{"type":"data","color":"#00ADD8","aliases":["go.mod","go mod"],"filenames":["go.mod"],"tm_scope":"go.mod","ace_mode":"text","language_id":947461016},"Golo":{"type":"programming","color":"#88562A","extensions":[".golo"],"tm_scope":"source.golo","ace_mode":"text","language_id":133},"Gosu":{"type":"programming","color":"#82937f","extensions":[".gs",".gst",".gsx",".vark"],"tm_scope":"source.gosu.2","ace_mode":"text","language_id":134},"Grace":{"type":"programming","color":"#615f8b","extensions":[".grace"],"tm_scope":"source.grace","ace_mode":"text","language_id":135},"Gradle":{"type":"data","color":"#02303a","extensions":[".gradle"],"tm_scope":"source.groovy.gradle","ace_mode":"text","language_id":136},"Grammatical Framework":{"type":"programming","aliases":["gf"],"extensions":[".gf"],"color":"#ff0000","tm_scope":"source.gf","ace_mode":"haskell","codemirror_mode":"haskell","codemirror_mime_type":"text/x-haskell","language_id":137},"Graph Modeling Language":{"type":"data","extensions":[".gml"],"tm_scope":"none","ace_mode":"text","language_id":138},"GraphQL":{"type":"data","color":"#e10098","extensions":[".graphql",".gql",".graphqls"],"tm_scope":"source.graphql","ace_mode":"text","language_id":139},"Graphviz (DOT)":{"type":"data","color":"#2596be","tm_scope":"source.dot","extensions":[".dot",".gv"],"ace_mode":"text","language_id":140},"Groovy":{"type":"programming","tm_scope":"source.groovy","ace_mode":"groovy","codemirror_mode":"groovy","codemirror_mime_type":"text/x-groovy","color":"#4298b8","extensions":[".groovy",".grt",".gtpl",".gvy"],"interpreters":["groovy"],"filenames":["Jenkinsfile"],"language_id":142},"Groovy Server Pages":{"type":"programming","color":"#4298b8","group":"Groovy","aliases":["gsp","java server page"],"extensions":[".gsp"],"tm_scope":"text.html.jsp","ace_mode":"jsp","codemirror_mode":"htmlembedded","codemirror_mime_type":"application/x-jsp","language_id":143},"HAProxy":{"type":"data","color":"#106da9","extensions":[".cfg"],"filenames":["haproxy.cfg"],"tm_scope":"source.haproxy-config","ace_mode":"text","language_id":366607477},"HCL":{"type":"programming","extensions":[".hcl",".nomad",".tf",".tfvars",".workflow"],"aliases":["HashiCorp Configuration Language","terraform"],"ace_mode":"ruby","codemirror_mode":"ruby","codemirror_mime_type":"text/x-ruby","tm_scope":"source.terraform","language_id":144},"HLSL":{"type":"programming","color":"#aace60","extensions":[".hlsl",".cginc",".fx",".fxh",".hlsli"],"ace_mode":"text","tm_scope":"source.hlsl","language_id":145},"HTML":{"type":"markup","tm_scope":"text.html.basic","ace_mode":"html","codemirror_mode":"htmlmixed","codemirror_mime_type":"text/html","color":"#e34c26","aliases":["xhtml"],"extensions":[".html",".hta",".htm",".html.hl",".inc",".xht",".xhtml"],"language_id":146},"HTML+ECR":{"type":"markup","color":"#2e1052","tm_scope":"text.html.ecr","group":"HTML","aliases":["ecr"],"extensions":[".ecr"],"ace_mode":"text","codemirror_mode":"htmlmixed","codemirror_mime_type":"text/html","language_id":148},"HTML+EEX":{"type":"markup","color":"#6e4a7e","tm_scope":"text.html.elixir","group":"HTML","aliases":["eex","heex","leex"],"extensions":[".eex",".html.heex",".html.leex"],"ace_mode":"text","codemirror_mode":"htmlmixed","codemirror_mime_type":"text/html","language_id":149},"HTML+ERB":{"type":"markup","color":"#701516","tm_scope":"text.html.erb","group":"HTML","aliases":["erb","rhtml","html+ruby"],"extensions":[".erb",".erb.deface",".rhtml"],"ace_mode":"text","codemirror_mode":"htmlembedded","codemirror_mime_type":"application/x-erb","language_id":150},"HTML+PHP":{"type":"markup","color":"#4f5d95","tm_scope":"text.html.php","group":"HTML","extensions":[".phtml"],"ace_mode":"php","codemirror_mode":"php","codemirror_mime_type":"application/x-httpd-php","language_id":151},"HTML+Razor":{"type":"markup","color":"#512be4","tm_scope":"text.html.cshtml","group":"HTML","aliases":["razor"],"extensions":[".cshtml",".razor"],"ace_mode":"razor","codemirror_mode":"htmlmixed","codemirror_mime_type":"text/html","language_id":479039817},"HTTP":{"type":"data","color":"#005C9C","extensions":[".http"],"tm_scope":"source.httpspec","ace_mode":"text","codemirror_mode":"http","codemirror_mime_type":"message/http","language_id":152},"HXML":{"type":"data","color":"#f68712","ace_mode":"text","extensions":[".hxml"],"tm_scope":"source.hxml","language_id":786683730},"Hack":{"type":"programming","ace_mode":"php","codemirror_mode":"php","codemirror_mime_type":"application/x-httpd-php","extensions":[".hack",".hh",".hhi",".php"],"tm_scope":"source.hack","color":"#878787","language_id":153},"Haml":{"type":"markup","color":"#ece2a9","extensions":[".haml",".haml.deface"],"tm_scope":"text.haml","ace_mode":"haml","codemirror_mode":"haml","codemirror_mime_type":"text/x-haml","language_id":154},"Handlebars":{"type":"markup","color":"#f7931e","aliases":["hbs","htmlbars"],"extensions":[".handlebars",".hbs"],"tm_scope":"text.html.handlebars","ace_mode":"handlebars","language_id":155},"Harbour":{"type":"programming","color":"#0e60e3","extensions":[".hb"],"tm_scope":"source.harbour","ace_mode":"text","language_id":156},"Haskell":{"type":"programming","color":"#5e5086","extensions":[".hs",".hs-boot",".hsc"],"interpreters":["runghc","runhaskell","runhugs"],"tm_scope":"source.haskell","ace_mode":"haskell","codemirror_mode":"haskell","codemirror_mime_type":"text/x-haskell","language_id":157},"Haxe":{"type":"programming","ace_mode":"haxe","codemirror_mode":"haxe","codemirror_mime_type":"text/x-haxe","color":"#df7900","extensions":[".hx",".hxsl"],"tm_scope":"source.hx","language_id":158},"HiveQL":{"type":"programming","extensions":[".q",".hql"],"color":"#dce200","tm_scope":"source.hql","ace_mode":"sql","language_id":931814087},"HolyC":{"type":"programming","color":"#ffefaf","extensions":[".hc"],"tm_scope":"source.hc","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","language_id":928121743},"Hy":{"type":"programming","ace_mode":"text","color":"#7790B2","extensions":[".hy"],"interpreters":["hy"],"aliases":["hylang"],"tm_scope":"source.hy","language_id":159},"HyPhy":{"type":"programming","ace_mode":"text","extensions":[".bf"],"tm_scope":"none","language_id":160},"IDL":{"type":"programming","color":"#a3522f","extensions":[".pro",".dlm"],"tm_scope":"source.idl","ace_mode":"text","codemirror_mode":"idl","codemirror_mime_type":"text/x-idl","language_id":161},"IGOR Pro":{"type":"programming","color":"#0000cc","extensions":[".ipf"],"aliases":["igor","igorpro"],"tm_scope":"source.igor","ace_mode":"text","language_id":162},"INI":{"type":"data","color":"#d1dbe0","extensions":[".ini",".cfg",".dof",".lektorproject",".prefs",".pro",".properties",".url"],"filenames":[".flake8","buildozer.spec"],"tm_scope":"source.ini","aliases":["dosini"],"ace_mode":"ini","codemirror_mode":"properties","codemirror_mime_type":"text/x-properties","language_id":163},"IRC log":{"type":"data","aliases":["irc","irc logs"],"extensions":[".irclog",".weechatlog"],"tm_scope":"none","ace_mode":"text","codemirror_mode":"mirc","codemirror_mime_type":"text/mirc","language_id":164},"Idris":{"type":"programming","color":"#b30000","extensions":[".idr",".lidr"],"ace_mode":"text","tm_scope":"source.idris","language_id":165},"Ignore List":{"type":"data","color":"#000000","group":"INI","aliases":["ignore","gitignore","git-ignore"],"extensions":[".gitignore"],"filenames":[".atomignore",".babelignore",".bzrignore",".coffeelintignore",".cvsignore",".dockerignore",".eleventyignore",".eslintignore",".gitignore",".markdownlintignore",".nodemonignore",".npmignore",".prettierignore",".stylelintignore",".vercelignore",".vscodeignore","gitignore-global","gitignore_global"],"ace_mode":"gitignore","tm_scope":"source.gitignore","codemirror_mode":"shell","codemirror_mime_type":"text/x-sh","language_id":74444240},"ImageJ Macro":{"type":"programming","color":"#99AAFF","aliases":["ijm"],"extensions":[".ijm"],"ace_mode":"text","tm_scope":"none","language_id":575143428},"Inform 7":{"type":"programming","wrap":true,"extensions":[".ni",".i7x"],"tm_scope":"source.inform7","aliases":["i7","inform7"],"ace_mode":"text","language_id":166},"Inno Setup":{"type":"programming","color":"#264b99","extensions":[".iss",".isl"],"tm_scope":"source.inno","ace_mode":"text","language_id":167},"Io":{"type":"programming","color":"#a9188d","extensions":[".io"],"interpreters":["io"],"tm_scope":"source.io","ace_mode":"io","language_id":168},"Ioke":{"type":"programming","color":"#078193","extensions":[".ik"],"interpreters":["ioke"],"tm_scope":"source.ioke","ace_mode":"text","language_id":169},"Isabelle":{"type":"programming","color":"#FEFE00","extensions":[".thy"],"tm_scope":"source.isabelle.theory","ace_mode":"text","language_id":170},"Isabelle ROOT":{"type":"programming","color":"#FEFE00","group":"Isabelle","filenames":["ROOT"],"tm_scope":"source.isabelle.root","ace_mode":"text","language_id":171},"J":{"type":"programming","color":"#9EEDFF","extensions":[".ijs"],"interpreters":["jconsole"],"tm_scope":"source.j","ace_mode":"text","language_id":172},"JAR Manifest":{"type":"data","color":"#b07219","filenames":["MANIFEST.MF"],"tm_scope":"source.yaml","ace_mode":"text","language_id":447261135},"JFlex":{"type":"programming","color":"#DBCA00","group":"Lex","extensions":[".flex",".jflex"],"tm_scope":"source.jflex","ace_mode":"text","language_id":173},"JSON":{"type":"data","color":"#292929","tm_scope":"source.json","ace_mode":"json","codemirror_mode":"javascript","codemirror_mime_type":"application/json","extensions":[".json",".avsc",".geojson",".gltf",".har",".ice",".JSON-tmLanguage",".jsonl",".mcmeta",".tfstate",".tfstate.backup",".topojson",".webapp",".webmanifest",".yy",".yyp"],"filenames":[".arcconfig",".auto-changelog",".c8rc",".htmlhintrc",".imgbotconfig",".nycrc",".tern-config",".tern-project",".watchmanconfig","Pipfile.lock","composer.lock","mcmod.info"],"language_id":174},"JSON with Comments":{"type":"data","color":"#292929","group":"JSON","tm_scope":"source.js","ace_mode":"javascript","codemirror_mode":"javascript","codemirror_mime_type":"text/javascript","aliases":["jsonc"],"extensions":[".jsonc",".code-snippets",".sublime-build",".sublime-commands",".sublime-completions",".sublime-keymap",".sublime-macro",".sublime-menu",".sublime-mousemap",".sublime-project",".sublime-settings",".sublime-theme",".sublime-workspace",".sublime_metrics",".sublime_session"],"filenames":[".babelrc",".eslintrc.json",".jscsrc",".jshintrc",".jslintrc","api-extractor.json","devcontainer.json","jsconfig.json","language-configuration.json","tsconfig.json","tslint.json"],"language_id":423},"JSON5":{"type":"data","color":"#267CB9","extensions":[".json5"],"tm_scope":"source.js","ace_mode":"javascript","codemirror_mode":"javascript","codemirror_mime_type":"application/json","language_id":175},"JSONLD":{"type":"data","color":"#0c479c","extensions":[".jsonld"],"tm_scope":"source.js","ace_mode":"javascript","codemirror_mode":"javascript","codemirror_mime_type":"application/json","language_id":176},"JSONiq":{"color":"#40d47e","type":"programming","ace_mode":"jsoniq","codemirror_mode":"javascript","codemirror_mime_type":"application/json","extensions":[".jq"],"tm_scope":"source.jsoniq","language_id":177},"Janet":{"type":"programming","color":"#0886a5","extensions":[".janet"],"tm_scope":"source.janet","ace_mode":"scheme","codemirror_mode":"scheme","codemirror_mime_type":"text/x-scheme","interpreters":["janet"],"language_id":1028705371},"Jasmin":{"type":"programming","color":"#d03600","ace_mode":"java","extensions":[".j"],"tm_scope":"source.jasmin","language_id":180},"Java":{"type":"programming","tm_scope":"source.java","ace_mode":"java","codemirror_mode":"clike","codemirror_mime_type":"text/x-java","color":"#b07219","extensions":[".java",".jav"],"language_id":181},"Java Properties":{"type":"data","color":"#2A6277","extensions":[".properties"],"tm_scope":"source.java-properties","ace_mode":"properties","codemirror_mode":"properties","codemirror_mime_type":"text/x-properties","language_id":519377561},"Java Server Pages":{"type":"programming","color":"#2A6277","group":"Java","aliases":["jsp"],"extensions":[".jsp"],"tm_scope":"text.html.jsp","ace_mode":"jsp","codemirror_mode":"htmlembedded","codemirror_mime_type":"application/x-jsp","language_id":182},"JavaScript":{"type":"programming","tm_scope":"source.js","ace_mode":"javascript","codemirror_mode":"javascript","codemirror_mime_type":"text/javascript","color":"#f1e05a","aliases":["js","node"],"extensions":[".js","._js",".bones",".cjs",".es",".es6",".frag",".gs",".jake",".javascript",".jsb",".jscad",".jsfl",".jslib",".jsm",".jspre",".jss",".jsx",".mjs",".njs",".pac",".sjs",".ssjs",".xsjs",".xsjslib"],"filenames":["Jakefile"],"interpreters":["chakra","d8","gjs","js","node","nodejs","qjs","rhino","v8","v8-shell"],"language_id":183},"JavaScript+ERB":{"type":"programming","color":"#f1e05a","tm_scope":"source.js","group":"JavaScript","extensions":[".js.erb"],"ace_mode":"javascript","codemirror_mode":"javascript","codemirror_mime_type":"application/javascript","language_id":914318960},"Jest Snapshot":{"type":"data","color":"#15c213","tm_scope":"source.jest.snap","extensions":[".snap"],"ace_mode":"javascript","codemirror_mode":"javascript","codemirror_mime_type":"application/javascript","language_id":774635084},"Jinja":{"type":"markup","color":"#a52a22","aliases":["django","html+django","html+jinja","htmldjango"],"extensions":[".jinja",".j2",".jinja2"],"tm_scope":"text.html.django","ace_mode":"django","codemirror_mode":"django","codemirror_mime_type":"text/x-django","language_id":147},"Jison":{"type":"programming","color":"#56b3cb","group":"Yacc","extensions":[".jison"],"tm_scope":"source.jison","ace_mode":"text","language_id":284531423},"Jison Lex":{"type":"programming","color":"#56b3cb","group":"Lex","extensions":[".jisonlex"],"tm_scope":"source.jisonlex","ace_mode":"text","language_id":406395330},"Jolie":{"type":"programming","extensions":[".ol",".iol"],"interpreters":["jolie"],"color":"#843179","ace_mode":"text","tm_scope":"source.jolie","language_id":998078858},"Jsonnet":{"color":"#0064bd","type":"programming","ace_mode":"text","extensions":[".jsonnet",".libsonnet"],"tm_scope":"source.jsonnet","language_id":664885656},"Julia":{"type":"programming","extensions":[".jl"],"interpreters":["julia"],"color":"#a270ba","tm_scope":"source.julia","ace_mode":"julia","codemirror_mode":"julia","codemirror_mime_type":"text/x-julia","language_id":184},"Jupyter Notebook":{"type":"markup","ace_mode":"json","codemirror_mode":"javascript","codemirror_mime_type":"application/json","tm_scope":"source.json","color":"#DA5B0B","extensions":[".ipynb"],"filenames":["Notebook"],"aliases":["IPython Notebook"],"language_id":185},"KRL":{"type":"programming","color":"#28430A","extensions":[".krl"],"tm_scope":"none","ace_mode":"text","language_id":186},"Kaitai Struct":{"type":"programming","aliases":["ksy"],"ace_mode":"yaml","codemirror_mode":"yaml","codemirror_mime_type":"text/x-yaml","color":"#773b37","extensions":[".ksy"],"tm_scope":"source.yaml","language_id":818804755},"KakouneScript":{"type":"programming","color":"#6f8042","tm_scope":"source.kakscript","aliases":["kak","kakscript"],"extensions":[".kak"],"filenames":["kakrc"],"ace_mode":"text","language_id":603336474},"KiCad Layout":{"type":"data","color":"#2f4aab","aliases":["pcbnew"],"extensions":[".kicad_pcb",".kicad_mod",".kicad_wks"],"filenames":["fp-lib-table"],"tm_scope":"source.pcb.sexp","ace_mode":"lisp","codemirror_mode":"commonlisp","codemirror_mime_type":"text/x-common-lisp","language_id":187},"KiCad Legacy Layout":{"type":"data","color":"#2f4aab","extensions":[".brd"],"tm_scope":"source.pcb.board","ace_mode":"text","language_id":140848857},"KiCad Schematic":{"type":"data","color":"#2f4aab","aliases":["eeschema schematic"],"extensions":[".sch"],"tm_scope":"source.pcb.schematic","ace_mode":"text","language_id":622447435},"Kit":{"type":"markup","ace_mode":"html","codemirror_mode":"htmlmixed","codemirror_mime_type":"text/html","extensions":[".kit"],"tm_scope":"text.html.basic","language_id":188},"Kotlin":{"type":"programming","color":"#A97BFF","extensions":[".kt",".ktm",".kts"],"tm_scope":"source.kotlin","ace_mode":"text","codemirror_mode":"clike","codemirror_mime_type":"text/x-kotlin","language_id":189},"Kusto":{"type":"data","extensions":[".csl"],"tm_scope":"source.kusto","ace_mode":"text","language_id":225697190},"LFE":{"type":"programming","color":"#4C3023","extensions":[".lfe"],"tm_scope":"source.lisp","ace_mode":"lisp","codemirror_mode":"commonlisp","codemirror_mime_type":"text/x-common-lisp","language_id":190},"LLVM":{"type":"programming","extensions":[".ll"],"tm_scope":"source.llvm","ace_mode":"text","color":"#185619","language_id":191},"LOLCODE":{"type":"programming","extensions":[".lol"],"color":"#cc9900","tm_scope":"none","ace_mode":"text","language_id":192},"LSL":{"type":"programming","tm_scope":"source.lsl","ace_mode":"lsl","extensions":[".lsl",".lslp"],"interpreters":["lsl"],"color":"#3d9970","language_id":193},"LTspice Symbol":{"type":"data","extensions":[".asy"],"tm_scope":"source.ltspice.symbol","ace_mode":"text","codemirror_mode":"spreadsheet","codemirror_mime_type":"text/x-spreadsheet","language_id":1013566805},"LabVIEW":{"type":"programming","color":"#fede06","extensions":[".lvproj",".lvlib"],"tm_scope":"text.xml","ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"text/xml","language_id":194},"Lark":{"type":"data","color":"#2980B9","extensions":[".lark"],"tm_scope":"source.lark","ace_mode":"text","codemirror_mode":"ebnf","codemirror_mime_type":"text/x-ebnf","language_id":758480799},"Lasso":{"type":"programming","color":"#999999","extensions":[".lasso",".las",".lasso8",".lasso9"],"tm_scope":"file.lasso","aliases":["lassoscript"],"ace_mode":"text","language_id":195},"Latte":{"type":"markup","color":"#f2a542","extensions":[".latte"],"tm_scope":"text.html.smarty","ace_mode":"smarty","codemirror_mode":"smarty","codemirror_mime_type":"text/x-smarty","language_id":196},"Lean":{"type":"programming","extensions":[".lean",".hlean"],"tm_scope":"source.lean","ace_mode":"text","language_id":197},"Less":{"type":"markup","color":"#1d365d","aliases":["less-css"],"extensions":[".less"],"tm_scope":"source.css.less","ace_mode":"less","codemirror_mode":"css","codemirror_mime_type":"text/css","language_id":198},"Lex":{"type":"programming","color":"#DBCA00","aliases":["flex"],"extensions":[".l",".lex"],"filenames":["Lexer.x","lexer.x"],"tm_scope":"source.lex","ace_mode":"text","language_id":199},"LigoLANG":{"type":"programming","color":"#0e74ff","extensions":[".ligo"],"tm_scope":"source.ligo","ace_mode":"pascal","codemirror_mode":"pascal","codemirror_mime_type":"text/x-pascal","group":"LigoLANG","language_id":1040646257},"LilyPond":{"type":"programming","color":"#9ccc7c","extensions":[".ly",".ily"],"tm_scope":"source.lilypond","ace_mode":"text","language_id":200},"Limbo":{"type":"programming","extensions":[".b",".m"],"tm_scope":"none","ace_mode":"text","language_id":201},"Linker Script":{"type":"data","extensions":[".ld",".lds",".x"],"filenames":["ld.script"],"tm_scope":"none","ace_mode":"text","language_id":202},"Linux Kernel Module":{"type":"data","extensions":[".mod"],"tm_scope":"none","ace_mode":"text","language_id":203},"Liquid":{"type":"markup","color":"#67b8de","extensions":[".liquid"],"tm_scope":"text.html.liquid","ace_mode":"liquid","language_id":204},"Literate Agda":{"type":"programming","color":"#315665","group":"Agda","extensions":[".lagda"],"tm_scope":"none","ace_mode":"text","language_id":205},"Literate CoffeeScript":{"type":"programming","color":"#244776","tm_scope":"source.litcoffee","group":"CoffeeScript","ace_mode":"text","wrap":true,"aliases":["litcoffee"],"extensions":[".litcoffee",".coffee.md"],"language_id":206},"Literate Haskell":{"type":"programming","color":"#5e5086","group":"Haskell","aliases":["lhaskell","lhs"],"extensions":[".lhs"],"tm_scope":"text.tex.latex.haskell","ace_mode":"text","codemirror_mode":"haskell-literate","codemirror_mime_type":"text/x-literate-haskell","language_id":207},"LiveScript":{"type":"programming","color":"#499886","aliases":["live-script","ls"],"extensions":[".ls","._ls"],"filenames":["Slakefile"],"tm_scope":"source.livescript","ace_mode":"livescript","codemirror_mode":"livescript","codemirror_mime_type":"text/x-livescript","language_id":208},"Logos":{"type":"programming","extensions":[".xm",".x",".xi"],"ace_mode":"text","tm_scope":"source.logos","language_id":209},"Logtalk":{"type":"programming","color":"#295b9a","extensions":[".lgt",".logtalk"],"tm_scope":"source.logtalk","ace_mode":"text","language_id":210},"LookML":{"type":"programming","ace_mode":"yaml","codemirror_mode":"yaml","codemirror_mime_type":"text/x-yaml","color":"#652B81","extensions":[".lookml",".model.lkml",".view.lkml"],"tm_scope":"source.yaml","language_id":211},"LoomScript":{"type":"programming","extensions":[".ls"],"tm_scope":"source.loomscript","ace_mode":"text","language_id":212},"Lua":{"type":"programming","tm_scope":"source.lua","ace_mode":"lua","codemirror_mode":"lua","codemirror_mime_type":"text/x-lua","color":"#000080","extensions":[".lua",".fcgi",".nse",".p8",".pd_lua",".rbxs",".rockspec",".wlua"],"filenames":[".luacheckrc"],"interpreters":["lua"],"language_id":213},"M":{"type":"programming","aliases":["mumps"],"extensions":[".mumps",".m"],"ace_mode":"text","codemirror_mode":"mumps","codemirror_mime_type":"text/x-mumps","language_id":214,"tm_scope":"none"},"M4":{"type":"programming","extensions":[".m4",".mc"],"tm_scope":"source.m4","ace_mode":"text","language_id":215},"M4Sugar":{"type":"programming","group":"M4","aliases":["autoconf"],"extensions":[".m4"],"filenames":["configure.ac"],"tm_scope":"source.m4","ace_mode":"text","language_id":216},"MATLAB":{"type":"programming","color":"#e16737","aliases":["octave"],"extensions":[".matlab",".m"],"tm_scope":"source.matlab","ace_mode":"matlab","codemirror_mode":"octave","codemirror_mime_type":"text/x-octave","language_id":225},"MAXScript":{"type":"programming","color":"#00a6a6","extensions":[".ms",".mcr"],"tm_scope":"source.maxscript","ace_mode":"text","language_id":217},"MLIR":{"type":"programming","color":"#5EC8DB","extensions":[".mlir"],"tm_scope":"source.mlir","ace_mode":"text","language_id":448253929},"MQL4":{"type":"programming","color":"#62A8D6","extensions":[".mq4",".mqh"],"tm_scope":"source.mql5","ace_mode":"c_cpp","language_id":426},"MQL5":{"type":"programming","color":"#4A76B8","extensions":[".mq5",".mqh"],"tm_scope":"source.mql5","ace_mode":"c_cpp","language_id":427},"MTML":{"type":"markup","color":"#b7e1f4","extensions":[".mtml"],"tm_scope":"text.html.basic","ace_mode":"html","codemirror_mode":"htmlmixed","codemirror_mime_type":"text/html","language_id":218},"MUF":{"type":"programming","group":"Forth","extensions":[".muf",".m"],"tm_scope":"none","ace_mode":"forth","codemirror_mode":"forth","codemirror_mime_type":"text/x-forth","language_id":219},"Macaulay2":{"type":"programming","extensions":[".m2"],"aliases":["m2"],"interpreters":["M2"],"ace_mode":"text","tm_scope":"source.m2","color":"#d8ffff","language_id":34167825},"Makefile":{"type":"programming","color":"#427819","aliases":["bsdmake","make","mf"],"extensions":[".mak",".d",".make",".makefile",".mk",".mkfile"],"filenames":["BSDmakefile","GNUmakefile","Kbuild","Makefile","Makefile.am","Makefile.boot","Makefile.frag","Makefile.in","Makefile.inc","Makefile.wat","makefile","makefile.sco","mkfile"],"interpreters":["make"],"tm_scope":"source.makefile","ace_mode":"makefile","codemirror_mode":"cmake","codemirror_mime_type":"text/x-cmake","language_id":220},"Mako":{"type":"programming","color":"#7e858d","extensions":[".mako",".mao"],"tm_scope":"text.html.mako","ace_mode":"text","language_id":221},"Markdown":{"type":"prose","color":"#083fa1","aliases":["pandoc"],"ace_mode":"markdown","codemirror_mode":"gfm","codemirror_mime_type":"text/x-gfm","wrap":true,"extensions":[".md",".markdown",".mdown",".mdwn",".mdx",".mkd",".mkdn",".mkdown",".ronn",".scd",".workbook"],"filenames":["contents.lr"],"tm_scope":"source.gfm","language_id":222},"Marko":{"type":"markup","color":"#42bff2","tm_scope":"text.marko","extensions":[".marko"],"aliases":["markojs"],"ace_mode":"text","codemirror_mode":"htmlmixed","codemirror_mime_type":"text/html","language_id":932782397},"Mask":{"type":"markup","color":"#f97732","ace_mode":"mask","extensions":[".mask"],"tm_scope":"source.mask","language_id":223},"Mathematica":{"type":"programming","color":"#dd1100","extensions":[".mathematica",".cdf",".m",".ma",".mt",".nb",".nbp",".wl",".wlt"],"aliases":["mma","wolfram","wolfram language","wolfram lang","wl"],"tm_scope":"source.mathematica","ace_mode":"text","codemirror_mode":"mathematica","codemirror_mime_type":"text/x-mathematica","language_id":224},"Maven POM":{"type":"data","group":"XML","tm_scope":"text.xml.pom","filenames":["pom.xml"],"ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"text/xml","language_id":226},"Max":{"type":"programming","color":"#c4a79c","aliases":["max/msp","maxmsp"],"extensions":[".maxpat",".maxhelp",".maxproj",".mxt",".pat"],"tm_scope":"source.json","ace_mode":"json","codemirror_mode":"javascript","codemirror_mime_type":"application/json","language_id":227},"Mercury":{"type":"programming","color":"#ff2b2b","ace_mode":"prolog","interpreters":["mmi"],"extensions":[".m",".moo"],"tm_scope":"source.mercury","language_id":229},"Meson":{"type":"programming","color":"#007800","filenames":["meson.build","meson_options.txt"],"tm_scope":"source.meson","ace_mode":"text","language_id":799141244},"Metal":{"type":"programming","color":"#8f14e9","extensions":[".metal"],"tm_scope":"source.c++","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-c++src","language_id":230},"Microsoft Developer Studio Project":{"type":"data","extensions":[".dsp"],"tm_scope":"none","ace_mode":"text","language_id":800983837},"Microsoft Visual Studio Solution":{"type":"data","extensions":[".sln"],"tm_scope":"source.solution","ace_mode":"text","language_id":849523096},"MiniD":{"type":"programming","extensions":[".minid"],"tm_scope":"none","ace_mode":"text","language_id":231},"MiniYAML":{"type":"data","color":"#ff1111","tm_scope":"source.miniyaml","extensions":[".yaml"],"ace_mode":"yaml","codemirror_mode":"yaml","codemirror_mime_type":"text/x-yaml","language_id":4896465},"Mint":{"type":"programming","extensions":[".mint"],"ace_mode":"text","color":"#02b046","tm_scope":"source.mint","language_id":968740319},"Mirah":{"type":"programming","color":"#c7a938","extensions":[".druby",".duby",".mirah"],"tm_scope":"source.ruby","ace_mode":"ruby","codemirror_mode":"ruby","codemirror_mime_type":"text/x-ruby","language_id":232},"Modelica":{"type":"programming","color":"#de1d31","extensions":[".mo"],"tm_scope":"source.modelica","ace_mode":"text","codemirror_mode":"modelica","codemirror_mime_type":"text/x-modelica","language_id":233},"Modula-2":{"type":"programming","color":"#10253f","extensions":[".mod"],"tm_scope":"source.modula2","ace_mode":"text","language_id":234},"Modula-3":{"type":"programming","extensions":[".i3",".ig",".m3",".mg"],"color":"#223388","ace_mode":"text","tm_scope":"source.modula-3","language_id":564743864},"Module Management System":{"type":"programming","extensions":[".mms",".mmk"],"filenames":["descrip.mmk","descrip.mms"],"tm_scope":"none","ace_mode":"text","language_id":235},"Monkey":{"type":"programming","extensions":[".monkey",".monkey2"],"ace_mode":"text","tm_scope":"source.monkey","language_id":236},"Monkey C":{"type":"programming","color":"#8D6747","extensions":[".mc"],"tm_scope":"source.mc","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","language_id":231751931},"Moocode":{"type":"programming","extensions":[".moo"],"tm_scope":"none","ace_mode":"text","language_id":237},"MoonScript":{"type":"programming","color":"#ff4585","extensions":[".moon"],"interpreters":["moon"],"tm_scope":"source.moonscript","ace_mode":"text","language_id":238},"Motoko":{"type":"programming","color":"#fbb03b","extensions":[".mo"],"tm_scope":"source.mo","ace_mode":"text","language_id":202937027},"Motorola 68K Assembly":{"type":"programming","color":"#005daa","group":"Assembly","aliases":["m68k"],"extensions":[".asm",".i",".inc",".s",".x68"],"tm_scope":"source.m68k","ace_mode":"assembly_x86","language_id":477582706},"Muse":{"type":"prose","extensions":[".muse"],"tm_scope":"text.muse","ace_mode":"text","wrap":true,"language_id":474864066,"aliases":["amusewiki","emacs muse"]},"Mustache":{"type":"markup","color":"#724b3b","extensions":[".mustache"],"tm_scope":"text.html.smarty","ace_mode":"smarty","codemirror_mode":"smarty","codemirror_mime_type":"text/x-smarty","language_id":638334590},"Myghty":{"type":"programming","extensions":[".myt"],"tm_scope":"none","ace_mode":"text","language_id":239},"NASL":{"type":"programming","extensions":[".nasl",".inc"],"tm_scope":"source.nasl","ace_mode":"text","language_id":171666519},"NCL":{"type":"programming","color":"#28431f","extensions":[".ncl"],"tm_scope":"source.ncl","ace_mode":"text","language_id":240},"NEON":{"type":"data","extensions":[".neon"],"tm_scope":"source.neon","ace_mode":"text","aliases":["nette object notation","ne-on"],"language_id":481192983},"NL":{"type":"data","extensions":[".nl"],"tm_scope":"none","ace_mode":"text","language_id":241},"NPM Config":{"type":"data","color":"#cb3837","group":"INI","aliases":["npmrc"],"filenames":[".npmrc"],"tm_scope":"source.ini.npmrc","ace_mode":"text","language_id":685022663},"NSIS":{"type":"programming","extensions":[".nsi",".nsh"],"tm_scope":"source.nsis","ace_mode":"text","codemirror_mode":"nsis","codemirror_mime_type":"text/x-nsis","language_id":242},"NWScript":{"type":"programming","color":"#111522","extensions":[".nss"],"tm_scope":"source.c.nwscript","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","language_id":731233819},"Nearley":{"type":"programming","ace_mode":"text","color":"#990000","extensions":[".ne",".nearley"],"tm_scope":"source.ne","language_id":521429430},"Nemerle":{"type":"programming","color":"#3d3c6e","extensions":[".n"],"tm_scope":"source.nemerle","ace_mode":"text","language_id":243},"NetLinx":{"type":"programming","color":"#0aa0ff","extensions":[".axs",".axi"],"tm_scope":"source.netlinx","ace_mode":"text","language_id":244},"NetLinx+ERB":{"type":"programming","color":"#747faa","extensions":[".axs.erb",".axi.erb"],"tm_scope":"source.netlinx.erb","ace_mode":"text","language_id":245},"NetLogo":{"type":"programming","color":"#ff6375","extensions":[".nlogo"],"tm_scope":"source.lisp","ace_mode":"lisp","codemirror_mode":"commonlisp","codemirror_mime_type":"text/x-common-lisp","language_id":246},"NewLisp":{"type":"programming","color":"#87AED7","extensions":[".nl",".lisp",".lsp"],"interpreters":["newlisp"],"tm_scope":"source.lisp","ace_mode":"lisp","codemirror_mode":"commonlisp","codemirror_mime_type":"text/x-common-lisp","language_id":247},"Nextflow":{"type":"programming","ace_mode":"groovy","tm_scope":"source.nextflow","color":"#3ac486","extensions":[".nf"],"filenames":["nextflow.config"],"interpreters":["nextflow"],"language_id":506780613},"Nginx":{"type":"data","color":"#009639","extensions":[".nginx",".nginxconf",".vhost"],"filenames":["nginx.conf"],"tm_scope":"source.nginx","aliases":["nginx configuration file"],"ace_mode":"text","codemirror_mode":"nginx","codemirror_mime_type":"text/x-nginx-conf","language_id":248},"Nim":{"type":"programming","color":"#ffc200","extensions":[".nim",".nim.cfg",".nimble",".nimrod",".nims"],"filenames":["nim.cfg"],"ace_mode":"text","tm_scope":"source.nim","language_id":249},"Ninja":{"type":"data","tm_scope":"source.ninja","extensions":[".ninja"],"ace_mode":"text","language_id":250},"Nit":{"type":"programming","color":"#009917","extensions":[".nit"],"tm_scope":"source.nit","ace_mode":"text","language_id":251},"Nix":{"type":"programming","color":"#7e7eff","extensions":[".nix"],"aliases":["nixos"],"tm_scope":"source.nix","ace_mode":"nix","language_id":252},"Nu":{"type":"programming","color":"#c9df40","aliases":["nush"],"extensions":[".nu"],"filenames":["Nukefile"],"tm_scope":"source.nu","ace_mode":"scheme","codemirror_mode":"scheme","codemirror_mime_type":"text/x-scheme","interpreters":["nush"],"language_id":253},"NumPy":{"type":"programming","color":"#9C8AF9","group":"Python","extensions":[".numpy",".numpyw",".numsc"],"tm_scope":"none","ace_mode":"text","codemirror_mode":"python","codemirror_mime_type":"text/x-python","language_id":254},"Nunjucks":{"type":"markup","color":"#3d8137","extensions":[".njk"],"aliases":["njk"],"tm_scope":"text.html.nunjucks","ace_mode":"nunjucks","language_id":461856962},"OCaml":{"type":"programming","ace_mode":"ocaml","codemirror_mode":"mllike","codemirror_mime_type":"text/x-ocaml","color":"#3be133","extensions":[".ml",".eliom",".eliomi",".ml4",".mli",".mll",".mly"],"interpreters":["ocaml","ocamlrun","ocamlscript"],"tm_scope":"source.ocaml","language_id":255},"ObjDump":{"type":"data","extensions":[".objdump"],"tm_scope":"objdump.x86asm","ace_mode":"assembly_x86","language_id":256},"Object Data Instance Notation":{"type":"data","extensions":[".odin"],"tm_scope":"source.odin-ehr","ace_mode":"text","language_id":985227236},"ObjectScript":{"type":"programming","extensions":[".cls"],"language_id":202735509,"tm_scope":"source.objectscript","color":"#424893","ace_mode":"text"},"Objective-C":{"type":"programming","tm_scope":"source.objc","color":"#438eff","aliases":["obj-c","objc","objectivec"],"extensions":[".m",".h"],"ace_mode":"objectivec","codemirror_mode":"clike","codemirror_mime_type":"text/x-objectivec","language_id":257},"Objective-C++":{"type":"programming","tm_scope":"source.objc++","color":"#6866fb","aliases":["obj-c++","objc++","objectivec++"],"extensions":[".mm"],"ace_mode":"objectivec","codemirror_mode":"clike","codemirror_mime_type":"text/x-objectivec","language_id":258},"Objective-J":{"type":"programming","color":"#ff0c5a","aliases":["obj-j","objectivej","objj"],"extensions":[".j",".sj"],"tm_scope":"source.js.objj","ace_mode":"text","language_id":259},"Odin":{"type":"programming","color":"#60AFFE","aliases":["odinlang","odin-lang"],"extensions":[".odin"],"tm_scope":"source.odin","ace_mode":"text","language_id":889244082},"Omgrofl":{"type":"programming","extensions":[".omgrofl"],"color":"#cabbff","tm_scope":"none","ace_mode":"text","language_id":260},"Opa":{"type":"programming","extensions":[".opa"],"tm_scope":"source.opa","ace_mode":"text","language_id":261},"Opal":{"type":"programming","color":"#f7ede0","extensions":[".opal"],"tm_scope":"source.opal","ace_mode":"text","language_id":262},"Open Policy Agent":{"type":"programming","color":"#7d9199","ace_mode":"text","extensions":[".rego"],"language_id":840483232,"tm_scope":"source.rego"},"OpenCL":{"type":"programming","color":"#ed2e2d","group":"C","extensions":[".cl",".opencl"],"tm_scope":"source.c","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","language_id":263},"OpenEdge ABL":{"type":"programming","color":"#5ce600","aliases":["progress","openedge","abl"],"extensions":[".p",".cls",".w"],"tm_scope":"source.abl","ace_mode":"text","language_id":264},"OpenQASM":{"type":"programming","extensions":[".qasm"],"color":"#AA70FF","tm_scope":"source.qasm","ace_mode":"text","language_id":153739399},"OpenRC runscript":{"type":"programming","group":"Shell","aliases":["openrc"],"interpreters":["openrc-run"],"tm_scope":"source.shell","ace_mode":"sh","codemirror_mode":"shell","codemirror_mime_type":"text/x-sh","language_id":265},"OpenSCAD":{"type":"programming","color":"#e5cd45","extensions":[".scad"],"tm_scope":"source.scad","ace_mode":"scad","language_id":266},"OpenStep Property List":{"type":"data","extensions":[".plist",".glyphs"],"tm_scope":"source.plist","ace_mode":"text","language_id":598917541},"OpenType Feature File":{"type":"data","aliases":["AFDKO"],"extensions":[".fea"],"tm_scope":"source.opentype","ace_mode":"text","language_id":374317347},"Org":{"type":"prose","color":"#77aa99","wrap":true,"extensions":[".org"],"tm_scope":"none","ace_mode":"text","language_id":267},"Ox":{"type":"programming","extensions":[".ox",".oxh",".oxo"],"tm_scope":"source.ox","ace_mode":"text","language_id":268},"Oxygene":{"type":"programming","color":"#cdd0e3","extensions":[".oxygene"],"tm_scope":"none","ace_mode":"text","language_id":269},"Oz":{"type":"programming","color":"#fab738","extensions":[".oz"],"tm_scope":"source.oz","ace_mode":"text","codemirror_mode":"oz","codemirror_mime_type":"text/x-oz","language_id":270},"P4":{"type":"programming","color":"#7055b5","extensions":[".p4"],"tm_scope":"source.p4","ace_mode":"text","language_id":348895984},"PEG.js":{"type":"programming","color":"#234d6b","extensions":[".pegjs"],"tm_scope":"source.pegjs","ace_mode":"javascript","codemirror_mode":"javascript","codemirror_mime_type":"text/javascript","language_id":81442128},"PHP":{"type":"programming","tm_scope":"text.html.php","ace_mode":"php","codemirror_mode":"php","codemirror_mime_type":"application/x-httpd-php","color":"#4F5D95","extensions":[".php",".aw",".ctp",".fcgi",".inc",".php3",".php4",".php5",".phps",".phpt"],"filenames":[".php",".php_cs",".php_cs.dist","Phakefile"],"interpreters":["php"],"aliases":["inc"],"language_id":272},"PLSQL":{"type":"programming","ace_mode":"sql","codemirror_mode":"sql","codemirror_mime_type":"text/x-plsql","tm_scope":"none","color":"#dad8d8","extensions":[".pls",".bdy",".ddl",".fnc",".pck",".pkb",".pks",".plb",".plsql",".prc",".spc",".sql",".tpb",".tps",".trg",".vw"],"language_id":273},"PLpgSQL":{"type":"programming","color":"#336790","ace_mode":"pgsql","codemirror_mode":"sql","codemirror_mime_type":"text/x-sql","tm_scope":"source.sql","extensions":[".pgsql",".sql"],"language_id":274},"POV-Ray SDL":{"type":"programming","color":"#6bac65","aliases":["pov-ray","povray"],"extensions":[".pov",".inc"],"tm_scope":"source.pov-ray sdl","ace_mode":"text","language_id":275},"Pan":{"type":"programming","color":"#cc0000","extensions":[".pan"],"tm_scope":"source.pan","ace_mode":"text","language_id":276},"Papyrus":{"type":"programming","color":"#6600cc","extensions":[".psc"],"tm_scope":"source.papyrus.skyrim","ace_mode":"text","language_id":277},"Parrot":{"type":"programming","color":"#f3ca0a","extensions":[".parrot"],"tm_scope":"none","ace_mode":"text","language_id":278},"Parrot Assembly":{"group":"Parrot","type":"programming","aliases":["pasm"],"extensions":[".pasm"],"interpreters":["parrot"],"tm_scope":"none","ace_mode":"text","language_id":279},"Parrot Internal Representation":{"group":"Parrot","tm_scope":"source.parrot.pir","type":"programming","aliases":["pir"],"extensions":[".pir"],"interpreters":["parrot"],"ace_mode":"text","language_id":280},"Pascal":{"type":"programming","color":"#E3F171","aliases":["delphi","objectpascal"],"extensions":[".pas",".dfm",".dpr",".inc",".lpr",".pascal",".pp"],"interpreters":["instantfpc"],"tm_scope":"source.pascal","ace_mode":"pascal","codemirror_mode":"pascal","codemirror_mime_type":"text/x-pascal","language_id":281},"Pawn":{"type":"programming","color":"#dbb284","extensions":[".pwn",".inc",".sma"],"tm_scope":"source.pawn","ace_mode":"text","language_id":271},"Pep8":{"type":"programming","color":"#C76F5B","extensions":[".pep"],"ace_mode":"text","tm_scope":"source.pep8","language_id":840372442},"Perl":{"type":"programming","tm_scope":"source.perl","ace_mode":"perl","codemirror_mode":"perl","codemirror_mime_type":"text/x-perl","color":"#0298c3","extensions":[".pl",".al",".cgi",".fcgi",".perl",".ph",".plx",".pm",".psgi",".t"],"filenames":["Makefile.PL","Rexfile","ack","cpanfile"],"interpreters":["cperl","perl"],"aliases":["cperl"],"language_id":282},"Pic":{"type":"markup","group":"Roff","tm_scope":"source.pic","extensions":[".pic",".chem"],"ace_mode":"text","codemirror_mode":"troff","codemirror_mime_type":"text/troff","language_id":425},"Pickle":{"type":"data","extensions":[".pkl"],"tm_scope":"none","ace_mode":"text","language_id":284},"PicoLisp":{"type":"programming","color":"#6067af","extensions":[".l"],"interpreters":["picolisp","pil"],"tm_scope":"source.lisp","ace_mode":"lisp","language_id":285},"PigLatin":{"type":"programming","color":"#fcd7de","extensions":[".pig"],"tm_scope":"source.pig_latin","ace_mode":"text","language_id":286},"Pike":{"type":"programming","color":"#005390","extensions":[".pike",".pmod"],"interpreters":["pike"],"tm_scope":"source.pike","ace_mode":"text","language_id":287},"PlantUML":{"type":"data","extensions":[".puml",".iuml",".plantuml"],"tm_scope":"source.wsd","ace_mode":"text","language_id":833504686},"Pod":{"type":"prose","ace_mode":"perl","codemirror_mode":"perl","codemirror_mime_type":"text/x-perl","wrap":true,"extensions":[".pod"],"interpreters":["perl"],"tm_scope":"none","language_id":288},"Pod 6":{"type":"prose","ace_mode":"perl","tm_scope":"source.raku","wrap":true,"extensions":[".pod",".pod6"],"interpreters":["perl6"],"language_id":155357471},"PogoScript":{"type":"programming","color":"#d80074","extensions":[".pogo"],"tm_scope":"source.pogoscript","ace_mode":"text","language_id":289},"Pony":{"type":"programming","extensions":[".pony"],"tm_scope":"source.pony","ace_mode":"text","language_id":290},"PostCSS":{"type":"markup","color":"#dc3a0c","tm_scope":"source.postcss","group":"CSS","extensions":[".pcss",".postcss"],"ace_mode":"text","language_id":262764437},"PostScript":{"type":"markup","color":"#da291c","extensions":[".ps",".eps",".epsi",".pfa"],"tm_scope":"source.postscript","aliases":["postscr"],"ace_mode":"text","language_id":291},"PowerBuilder":{"type":"programming","color":"#8f0f8d","extensions":[".pbt",".sra",".sru",".srw"],"tm_scope":"none","ace_mode":"text","language_id":292},"PowerShell":{"type":"programming","color":"#012456","tm_scope":"source.powershell","ace_mode":"powershell","codemirror_mode":"powershell","codemirror_mime_type":"application/x-powershell","aliases":["posh","pwsh"],"extensions":[".ps1",".psd1",".psm1"],"interpreters":["pwsh"],"language_id":293},"Prisma":{"type":"data","color":"#0c344b","extensions":[".prisma"],"tm_scope":"source.prisma","ace_mode":"text","language_id":499933428},"Processing":{"type":"programming","color":"#0096D8","extensions":[".pde"],"tm_scope":"source.processing","ace_mode":"text","language_id":294},"Procfile":{"type":"programming","color":"#3B2F63","filenames":["Procfile"],"tm_scope":"source.procfile","ace_mode":"batchfile","language_id":305313959},"Proguard":{"type":"data","extensions":[".pro"],"tm_scope":"none","ace_mode":"text","language_id":716513858},"Prolog":{"type":"programming","color":"#74283c","extensions":[".pl",".pro",".prolog",".yap"],"interpreters":["swipl","yap"],"tm_scope":"source.prolog","ace_mode":"prolog","language_id":295},"Promela":{"type":"programming","color":"#de0000","tm_scope":"source.promela","ace_mode":"text","extensions":[".pml"],"language_id":441858312},"Propeller Spin":{"type":"programming","color":"#7fa2a7","extensions":[".spin"],"tm_scope":"source.spin","ace_mode":"text","language_id":296},"Protocol Buffer":{"type":"data","aliases":["protobuf","Protocol Buffers"],"extensions":[".proto"],"tm_scope":"source.proto","ace_mode":"protobuf","codemirror_mode":"protobuf","codemirror_mime_type":"text/x-protobuf","language_id":297},"Protocol Buffer Text Format":{"type":"data","aliases":["text proto","protobuf text format"],"extensions":[".textproto",".pbt",".pbtxt"],"tm_scope":"source.textproto","ace_mode":"text","language_id":436568854},"Public Key":{"type":"data","extensions":[".asc",".pub"],"tm_scope":"none","ace_mode":"text","codemirror_mode":"asciiarmor","codemirror_mime_type":"application/pgp","language_id":298},"Pug":{"type":"markup","color":"#a86454","extensions":[".jade",".pug"],"tm_scope":"text.jade","ace_mode":"jade","codemirror_mode":"pug","codemirror_mime_type":"text/x-pug","language_id":179},"Puppet":{"type":"programming","color":"#302B6D","extensions":[".pp"],"filenames":["Modulefile"],"ace_mode":"text","codemirror_mode":"puppet","codemirror_mime_type":"text/x-puppet","tm_scope":"source.puppet","language_id":299},"Pure Data":{"type":"data","extensions":[".pd"],"tm_scope":"none","ace_mode":"text","language_id":300},"PureBasic":{"type":"programming","color":"#5a6986","extensions":[".pb",".pbi"],"tm_scope":"none","ace_mode":"text","language_id":301},"PureScript":{"type":"programming","color":"#1D222D","extensions":[".purs"],"tm_scope":"source.purescript","ace_mode":"haskell","codemirror_mode":"haskell","codemirror_mime_type":"text/x-haskell","language_id":302},"Python":{"type":"programming","tm_scope":"source.python","ace_mode":"python","codemirror_mode":"python","codemirror_mime_type":"text/x-python","color":"#3572A5","extensions":[".py",".cgi",".fcgi",".gyp",".gypi",".lmi",".py3",".pyde",".pyi",".pyp",".pyt",".pyw",".rpy",".smk",".spec",".tac",".wsgi",".xpy"],"filenames":[".gclient","DEPS","SConscript","SConstruct","Snakefile","wscript"],"interpreters":["python","python2","python3"],"aliases":["python3","rusthon"],"language_id":303},"Python console":{"type":"programming","color":"#3572A5","group":"Python","aliases":["pycon"],"tm_scope":"text.python.console","ace_mode":"text","language_id":428},"Python traceback":{"type":"data","color":"#3572A5","group":"Python","extensions":[".pytb"],"tm_scope":"text.python.traceback","ace_mode":"text","language_id":304},"Q#":{"type":"programming","extensions":[".qs"],"aliases":["qsharp"],"color":"#fed659","ace_mode":"text","tm_scope":"source.qsharp","language_id":697448245},"QML":{"type":"programming","color":"#44a51c","extensions":[".qml",".qbs"],"tm_scope":"source.qml","ace_mode":"text","language_id":305},"QMake":{"type":"programming","extensions":[".pro",".pri"],"interpreters":["qmake"],"tm_scope":"source.qmake","ace_mode":"text","language_id":306},"Qt Script":{"type":"programming","ace_mode":"javascript","codemirror_mode":"javascript","codemirror_mime_type":"text/javascript","extensions":[".qs"],"filenames":["installscript.qs","toolchain_installscript.qs"],"color":"#00b841","tm_scope":"source.js","language_id":558193693},"Quake":{"type":"programming","filenames":["m3makefile","m3overrides"],"color":"#882233","ace_mode":"text","tm_scope":"source.quake","language_id":375265331},"R":{"type":"programming","color":"#198CE7","aliases":["R","Rscript","splus"],"extensions":[".r",".rd",".rsx"],"filenames":[".Rprofile","expr-dist"],"interpreters":["Rscript"],"tm_scope":"source.r","ace_mode":"r","codemirror_mode":"r","codemirror_mime_type":"text/x-rsrc","language_id":307},"RAML":{"type":"markup","ace_mode":"yaml","codemirror_mode":"yaml","codemirror_mime_type":"text/x-yaml","tm_scope":"source.yaml","color":"#77d9fb","extensions":[".raml"],"language_id":308},"RDoc":{"type":"prose","color":"#701516","ace_mode":"rdoc","wrap":true,"extensions":[".rdoc"],"tm_scope":"text.rdoc","language_id":309},"REALbasic":{"type":"programming","extensions":[".rbbas",".rbfrm",".rbmnu",".rbres",".rbtbar",".rbuistate"],"tm_scope":"source.vbnet","ace_mode":"text","language_id":310},"REXX":{"type":"programming","color":"#d90e09","aliases":["arexx"],"extensions":[".rexx",".pprx",".rex"],"interpreters":["regina","rexx"],"tm_scope":"source.rexx","ace_mode":"text","language_id":311},"RMarkdown":{"type":"prose","color":"#198ce7","wrap":true,"ace_mode":"markdown","codemirror_mode":"gfm","codemirror_mime_type":"text/x-gfm","extensions":[".qmd",".rmd"],"tm_scope":"source.gfm","language_id":313},"RPC":{"type":"programming","aliases":["rpcgen","oncrpc","xdr"],"ace_mode":"c_cpp","extensions":[".x"],"tm_scope":"source.c","language_id":1031374237},"RPGLE":{"type":"programming","ace_mode":"text","color":"#2BDE21","aliases":["ile rpg","sqlrpgle"],"extensions":[".rpgle",".sqlrpgle"],"tm_scope":"source.rpgle","language_id":609977990},"RPM Spec":{"type":"data","tm_scope":"source.rpm-spec","extensions":[".spec"],"aliases":["specfile"],"ace_mode":"text","codemirror_mode":"rpm","codemirror_mime_type":"text/x-rpm-spec","language_id":314},"RUNOFF":{"type":"markup","color":"#665a4e","extensions":[".rnh",".rno"],"wrap":true,"tm_scope":"text.runoff","ace_mode":"text","language_id":315},"Racket":{"type":"programming","color":"#3c5caa","extensions":[".rkt",".rktd",".rktl",".scrbl"],"interpreters":["racket"],"tm_scope":"source.racket","ace_mode":"lisp","language_id":316},"Ragel":{"type":"programming","color":"#9d5200","extensions":[".rl"],"aliases":["ragel-rb","ragel-ruby"],"tm_scope":"none","ace_mode":"text","language_id":317},"Raku":{"type":"programming","color":"#0000fb","extensions":[".6pl",".6pm",".nqp",".p6",".p6l",".p6m",".pl",".pl6",".pm",".pm6",".raku",".rakumod",".t"],"interpreters":["perl6","raku","rakudo"],"aliases":["perl6","perl-6"],"tm_scope":"source.raku","ace_mode":"perl","codemirror_mode":"perl","codemirror_mime_type":"text/x-perl","language_id":283},"Rascal":{"type":"programming","color":"#fffaa0","extensions":[".rsc"],"tm_scope":"source.rascal","ace_mode":"text","language_id":173616037},"Raw token data":{"type":"data","aliases":["raw"],"extensions":[".raw"],"tm_scope":"none","ace_mode":"text","language_id":318},"ReScript":{"type":"programming","color":"#ed5051","ace_mode":"rust","codemirror_mode":"rust","codemirror_mime_type":"text/x-rustsrc","extensions":[".res"],"interpreters":["ocaml"],"tm_scope":"source.rescript","language_id":501875647},"Readline Config":{"type":"data","group":"INI","aliases":["inputrc","readline"],"filenames":[".inputrc","inputrc"],"tm_scope":"source.inputrc","ace_mode":"text","language_id":538732839},"Reason":{"type":"programming","color":"#ff5847","ace_mode":"rust","codemirror_mode":"rust","codemirror_mime_type":"text/x-rustsrc","extensions":[".re",".rei"],"tm_scope":"source.reason","language_id":869538413},"ReasonLIGO":{"type":"programming","color":"#ff5847","ace_mode":"rust","codemirror_mode":"rust","codemirror_mime_type":"text/x-rustsrc","group":"LigoLANG","extensions":[".religo"],"tm_scope":"source.religo","language_id":319002153},"Rebol":{"type":"programming","color":"#358a5b","extensions":[".reb",".r",".r2",".r3",".rebol"],"ace_mode":"text","tm_scope":"source.rebol","language_id":319},"Record Jar":{"type":"data","filenames":["language-subtag-registry.txt"],"tm_scope":"source.record-jar","codemirror_mode":"properties","codemirror_mime_type":"text/x-properties","ace_mode":"text","color":"#0673ba","language_id":865765202},"Red":{"type":"programming","color":"#f50000","extensions":[".red",".reds"],"aliases":["red/system"],"tm_scope":"source.red","ace_mode":"text","language_id":320},"Redcode":{"type":"programming","extensions":[".cw"],"tm_scope":"none","ace_mode":"text","language_id":321},"Redirect Rules":{"type":"data","aliases":["redirects"],"filenames":["_redirects"],"tm_scope":"source.redirects","ace_mode":"text","language_id":1020148948},"Regular Expression":{"type":"data","color":"#009a00","extensions":[".regexp",".regex"],"aliases":["regexp","regex"],"ace_mode":"text","tm_scope":"source.regexp","language_id":363378884},"Ren'Py":{"type":"programming","aliases":["renpy"],"color":"#ff7f7f","extensions":[".rpy"],"tm_scope":"source.renpy","ace_mode":"python","language_id":322},"RenderScript":{"type":"programming","extensions":[".rs",".rsh"],"tm_scope":"none","ace_mode":"text","language_id":323},"Rich Text Format":{"type":"markup","extensions":[".rtf"],"tm_scope":"text.rtf","ace_mode":"text","language_id":51601661},"Ring":{"type":"programming","color":"#2D54CB","extensions":[".ring"],"tm_scope":"source.ring","ace_mode":"text","language_id":431},"Riot":{"type":"markup","color":"#A71E49","ace_mode":"html","extensions":[".riot"],"tm_scope":"text.html.riot","language_id":878396783},"RobotFramework":{"type":"programming","color":"#00c0b5","extensions":[".robot"],"tm_scope":"text.robot","ace_mode":"text","language_id":324},"Roff":{"type":"markup","color":"#ecdebe","extensions":[".roff",".1",".1in",".1m",".1x",".2",".3",".3in",".3m",".3p",".3pm",".3qt",".3x",".4",".5",".6",".7",".8",".9",".l",".man",".mdoc",".me",".ms",".n",".nr",".rno",".tmac"],"filenames":["eqnrc","mmn","mmt","troffrc","troffrc-end"],"tm_scope":"text.roff","aliases":["groff","man","manpage","man page","man-page","mdoc","nroff","troff"],"wrap":true,"ace_mode":"text","codemirror_mode":"troff","codemirror_mime_type":"text/troff","language_id":141},"Roff Manpage":{"type":"markup","color":"#ecdebe","group":"Roff","extensions":[".1",".1in",".1m",".1x",".2",".3",".3in",".3m",".3p",".3pm",".3qt",".3x",".4",".5",".6",".7",".8",".9",".man",".mdoc"],"wrap":true,"tm_scope":"text.roff","ace_mode":"text","codemirror_mode":"troff","codemirror_mime_type":"text/troff","language_id":612669833},"Rouge":{"type":"programming","ace_mode":"clojure","codemirror_mode":"clojure","codemirror_mime_type":"text/x-clojure","color":"#cc0088","extensions":[".rg"],"tm_scope":"source.clojure","language_id":325},"Ruby":{"type":"programming","tm_scope":"source.ruby","ace_mode":"ruby","codemirror_mode":"ruby","codemirror_mime_type":"text/x-ruby","color":"#701516","aliases":["jruby","macruby","rake","rb","rbx"],"extensions":[".rb",".builder",".eye",".fcgi",".gemspec",".god",".jbuilder",".mspec",".pluginspec",".podspec",".prawn",".rabl",".rake",".rbi",".rbuild",".rbw",".rbx",".ru",".ruby",".spec",".thor",".watchr"],"interpreters":["ruby","macruby","rake","jruby","rbx"],"filenames":[".irbrc",".pryrc",".simplecov","Appraisals","Berksfile","Brewfile","Buildfile","Capfile","Dangerfile","Deliverfile","Fastfile","Gemfile","Guardfile","Jarfile","Mavenfile","Podfile","Puppetfile","Rakefile","Snapfile","Steepfile","Thorfile","Vagrantfile","buildfile"],"language_id":326},"Rust":{"type":"programming","aliases":["rs"],"color":"#dea584","extensions":[".rs",".rs.in"],"tm_scope":"source.rust","ace_mode":"rust","codemirror_mode":"rust","codemirror_mime_type":"text/x-rustsrc","language_id":327},"SAS":{"type":"programming","color":"#B34936","extensions":[".sas"],"tm_scope":"source.sas","ace_mode":"text","codemirror_mode":"sas","codemirror_mime_type":"text/x-sas","language_id":328},"SCSS":{"type":"markup","color":"#c6538c","tm_scope":"source.css.scss","ace_mode":"scss","codemirror_mode":"css","codemirror_mime_type":"text/x-scss","extensions":[".scss"],"language_id":329},"SELinux Policy":{"aliases":["SELinux Kernel Policy Language","sepolicy"],"type":"data","tm_scope":"source.sepolicy","extensions":[".te"],"filenames":["file_contexts","genfs_contexts","initial_sids","port_contexts","security_classes"],"ace_mode":"text","language_id":880010326},"SMT":{"type":"programming","extensions":[".smt2",".smt"],"interpreters":["boolector","cvc4","mathsat5","opensmt","smtinterpol","smt-rat","stp","verit","yices2","z3"],"tm_scope":"source.smt","ace_mode":"text","language_id":330},"SPARQL":{"type":"data","color":"#0C4597","tm_scope":"source.sparql","ace_mode":"text","codemirror_mode":"sparql","codemirror_mime_type":"application/sparql-query","extensions":[".sparql",".rq"],"language_id":331},"SQF":{"type":"programming","color":"#3F3F3F","extensions":[".sqf",".hqf"],"tm_scope":"source.sqf","ace_mode":"text","language_id":332},"SQL":{"type":"data","color":"#e38c00","tm_scope":"source.sql","ace_mode":"sql","codemirror_mode":"sql","codemirror_mime_type":"text/x-sql","extensions":[".sql",".cql",".ddl",".inc",".mysql",".prc",".tab",".udf",".viw"],"language_id":333},"SQLPL":{"type":"programming","color":"#e38c00","ace_mode":"sql","codemirror_mode":"sql","codemirror_mime_type":"text/x-sql","tm_scope":"source.sql","extensions":[".sql",".db2"],"language_id":334},"SRecode Template":{"type":"markup","color":"#348a34","tm_scope":"source.lisp","ace_mode":"lisp","codemirror_mode":"commonlisp","codemirror_mime_type":"text/x-common-lisp","extensions":[".srt"],"language_id":335},"SSH Config":{"type":"data","group":"INI","filenames":["ssh-config","ssh_config","sshconfig","sshconfig.snip","sshd-config","sshd_config"],"ace_mode":"text","tm_scope":"source.ssh-config","language_id":554920715},"STON":{"type":"data","group":"Smalltalk","extensions":[".ston"],"tm_scope":"source.smalltalk","ace_mode":"text","language_id":336},"SVG":{"type":"data","color":"#ff9900","extensions":[".svg"],"tm_scope":"text.xml.svg","ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"text/xml","language_id":337},"SWIG":{"type":"programming","extensions":[".i"],"tm_scope":"source.c++","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-c++src","language_id":1066250075},"Sage":{"type":"programming","extensions":[".sage",".sagews"],"tm_scope":"source.python","ace_mode":"python","codemirror_mode":"python","codemirror_mime_type":"text/x-python","language_id":338},"SaltStack":{"type":"programming","color":"#646464","aliases":["saltstate","salt"],"extensions":[".sls"],"tm_scope":"source.yaml.salt","ace_mode":"yaml","codemirror_mode":"yaml","codemirror_mime_type":"text/x-yaml","language_id":339},"Sass":{"type":"markup","color":"#a53b70","tm_scope":"source.sass","extensions":[".sass"],"ace_mode":"sass","codemirror_mode":"sass","codemirror_mime_type":"text/x-sass","language_id":340},"Scala":{"type":"programming","tm_scope":"source.scala","ace_mode":"scala","codemirror_mode":"clike","codemirror_mime_type":"text/x-scala","color":"#c22d40","extensions":[".scala",".kojo",".sbt",".sc"],"interpreters":["scala"],"language_id":341},"Scaml":{"type":"markup","color":"#bd181a","extensions":[".scaml"],"tm_scope":"source.scaml","ace_mode":"text","language_id":342},"Scheme":{"type":"programming","color":"#1e4aec","extensions":[".scm",".sch",".sld",".sls",".sps",".ss"],"interpreters":["scheme","guile","bigloo","chicken","csi","gosh","r6rs"],"tm_scope":"source.scheme","ace_mode":"scheme","codemirror_mode":"scheme","codemirror_mime_type":"text/x-scheme","language_id":343},"Scilab":{"type":"programming","color":"#ca0f21","extensions":[".sci",".sce",".tst"],"tm_scope":"source.scilab","ace_mode":"text","language_id":344},"Self":{"type":"programming","color":"#0579aa","extensions":[".self"],"tm_scope":"none","ace_mode":"text","language_id":345},"ShaderLab":{"type":"programming","color":"#222c37","extensions":[".shader"],"ace_mode":"text","tm_scope":"source.shaderlab","language_id":664257356},"Shell":{"type":"programming","color":"#89e051","aliases":["sh","shell-script","bash","zsh"],"extensions":[".sh",".bash",".bats",".cgi",".command",".env",".fcgi",".ksh",".sh.in",".tmux",".tool",".zsh",".zsh-theme"],"filenames":[".bash_aliases",".bash_history",".bash_logout",".bash_profile",".bashrc",".cshrc",".env",".env.example",".flaskenv",".kshrc",".login",".profile",".zlogin",".zlogout",".zprofile",".zshenv",".zshrc","9fs","PKGBUILD","bash_aliases","bash_logout","bash_profile","bashrc","cshrc","gradlew","kshrc","login","man","profile","zlogin","zlogout","zprofile","zshenv","zshrc"],"interpreters":["ash","bash","dash","ksh","mksh","pdksh","rc","sh","zsh"],"tm_scope":"source.shell","ace_mode":"sh","codemirror_mode":"shell","codemirror_mime_type":"text/x-sh","language_id":346},"ShellCheck Config":{"type":"data","color":"#cecfcb","filenames":[".shellcheckrc"],"aliases":["shellcheckrc"],"tm_scope":"source.shellcheckrc","ace_mode":"ini","codemirror_mode":"properties","codemirror_mime_type":"text/x-properties","language_id":687511714},"ShellSession":{"type":"programming","extensions":[".sh-session"],"aliases":["bash session","console"],"tm_scope":"text.shell-session","ace_mode":"sh","codemirror_mode":"shell","codemirror_mime_type":"text/x-sh","language_id":347},"Shen":{"type":"programming","color":"#120F14","extensions":[".shen"],"tm_scope":"source.shen","ace_mode":"text","language_id":348},"Sieve":{"type":"programming","tm_scope":"source.sieve","ace_mode":"text","extensions":[".sieve"],"codemirror_mode":"sieve","codemirror_mime_type":"application/sieve","language_id":208976687},"Singularity":{"type":"programming","color":"#64E6AD","tm_scope":"source.singularity","filenames":["Singularity"],"ace_mode":"text","language_id":987024632},"Slash":{"type":"programming","color":"#007eff","extensions":[".sl"],"tm_scope":"text.html.slash","ace_mode":"text","language_id":349},"Slice":{"type":"programming","color":"#003fa2","tm_scope":"source.slice","ace_mode":"text","extensions":[".ice"],"language_id":894641667},"Slim":{"type":"markup","color":"#2b2b2b","extensions":[".slim"],"tm_scope":"text.slim","ace_mode":"text","codemirror_mode":"slim","codemirror_mime_type":"text/x-slim","language_id":350},"SmPL":{"type":"programming","extensions":[".cocci"],"aliases":["coccinelle"],"ace_mode":"text","tm_scope":"source.smpl","color":"#c94949","language_id":164123055},"Smali":{"type":"programming","extensions":[".smali"],"ace_mode":"text","tm_scope":"source.smali","language_id":351},"Smalltalk":{"type":"programming","color":"#596706","extensions":[".st",".cs"],"aliases":["squeak"],"tm_scope":"source.smalltalk","ace_mode":"text","codemirror_mode":"smalltalk","codemirror_mime_type":"text/x-stsrc","language_id":352},"Smarty":{"type":"programming","color":"#f0c040","extensions":[".tpl"],"ace_mode":"smarty","codemirror_mode":"smarty","codemirror_mime_type":"text/x-smarty","tm_scope":"text.html.smarty","language_id":353},"Solidity":{"type":"programming","color":"#AA6746","ace_mode":"text","tm_scope":"source.solidity","extensions":[".sol"],"language_id":237469032},"Soong":{"type":"data","tm_scope":"source.bp","ace_mode":"text","filenames":["Android.bp"],"language_id":222900098},"SourcePawn":{"type":"programming","color":"#f69e1d","aliases":["sourcemod"],"extensions":[".sp",".inc"],"tm_scope":"source.sourcepawn","ace_mode":"text","language_id":354},"Spline Font Database":{"type":"data","extensions":[".sfd"],"tm_scope":"text.sfd","ace_mode":"yaml","language_id":767169629},"Squirrel":{"type":"programming","color":"#800000","extensions":[".nut"],"tm_scope":"source.nut","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-c++src","language_id":355},"Stan":{"type":"programming","color":"#b2011d","extensions":[".stan"],"ace_mode":"text","tm_scope":"source.stan","language_id":356},"Standard ML":{"type":"programming","color":"#dc566d","aliases":["sml"],"extensions":[".ml",".fun",".sig",".sml"],"tm_scope":"source.ml","ace_mode":"text","codemirror_mode":"mllike","codemirror_mime_type":"text/x-ocaml","language_id":357},"Starlark":{"type":"programming","tm_scope":"source.python","ace_mode":"python","codemirror_mode":"python","codemirror_mime_type":"text/x-python","color":"#76d275","extensions":[".bzl"],"filenames":["BUCK","BUILD","BUILD.bazel","Tiltfile","WORKSPACE"],"aliases":["bazel","bzl"],"language_id":960266174},"Stata":{"type":"programming","color":"#1a5f91","extensions":[".do",".ado",".doh",".ihlp",".mata",".matah",".sthlp"],"tm_scope":"source.stata","ace_mode":"text","language_id":358},"StringTemplate":{"type":"markup","color":"#3fb34f","extensions":[".st"],"tm_scope":"source.string-template","ace_mode":"html","codemirror_mode":"htmlmixed","codemirror_mime_type":"text/html","language_id":89855901},"Stylus":{"type":"markup","color":"#ff6347","extensions":[".styl"],"tm_scope":"source.stylus","ace_mode":"stylus","language_id":359},"SubRip Text":{"type":"data","color":"#9e0101","extensions":[".srt"],"ace_mode":"text","tm_scope":"text.srt","language_id":360},"SugarSS":{"type":"markup","color":"#2fcc9f","tm_scope":"source.css.postcss.sugarss","group":"CSS","extensions":[".sss"],"ace_mode":"text","language_id":826404698},"SuperCollider":{"type":"programming","color":"#46390b","extensions":[".sc",".scd"],"interpreters":["sclang","scsynth"],"tm_scope":"source.supercollider","ace_mode":"text","language_id":361},"Svelte":{"type":"markup","color":"#ff3e00","tm_scope":"source.svelte","ace_mode":"html","codemirror_mode":"htmlmixed","codemirror_mime_type":"text/html","extensions":[".svelte"],"language_id":928734530},"Swift":{"type":"programming","color":"#F05138","extensions":[".swift"],"tm_scope":"source.swift","ace_mode":"text","codemirror_mode":"swift","codemirror_mime_type":"text/x-swift","language_id":362},"SystemVerilog":{"type":"programming","color":"#DAE1C2","extensions":[".sv",".svh",".vh"],"tm_scope":"source.systemverilog","ace_mode":"verilog","codemirror_mode":"verilog","codemirror_mime_type":"text/x-systemverilog","language_id":363},"TI Program":{"type":"programming","ace_mode":"text","color":"#A0AA87","extensions":[".8xp",".8xk",".8xk.txt",".8xp.txt"],"language_id":422,"tm_scope":"none"},"TLA":{"type":"programming","color":"#4b0079","extensions":[".tla"],"tm_scope":"source.tla","ace_mode":"text","language_id":364},"TOML":{"type":"data","color":"#9c4221","extensions":[".toml"],"filenames":["Cargo.lock","Gopkg.lock","Pipfile","poetry.lock"],"tm_scope":"source.toml","ace_mode":"toml","codemirror_mode":"toml","codemirror_mime_type":"text/x-toml","language_id":365},"TSQL":{"type":"programming","color":"#e38c00","extensions":[".sql"],"ace_mode":"sql","tm_scope":"source.tsql","language_id":918334941},"TSV":{"type":"data","color":"#237346","ace_mode":"text","tm_scope":"source.generic-db","extensions":[".tsv"],"language_id":1035892117},"TSX":{"type":"programming","color":"#2b7489","group":"TypeScript","extensions":[".tsx"],"tm_scope":"source.tsx","ace_mode":"javascript","codemirror_mode":"jsx","codemirror_mime_type":"text/jsx","language_id":94901924},"TXL":{"type":"programming","color":"#0178b8","extensions":[".txl"],"tm_scope":"source.txl","ace_mode":"text","language_id":366},"Talon":{"type":"programming","ace_mode":"text","color":"#333333","extensions":[".talon"],"tm_scope":"source.talon","language_id":959889508},"Tcl":{"type":"programming","color":"#e4cc98","extensions":[".tcl",".adp",".tcl.in",".tm"],"filenames":["owh","starfield"],"interpreters":["tclsh","wish"],"tm_scope":"source.tcl","ace_mode":"tcl","codemirror_mode":"tcl","codemirror_mime_type":"text/x-tcl","language_id":367},"Tcsh":{"type":"programming","group":"Shell","extensions":[".tcsh",".csh"],"interpreters":["tcsh","csh"],"tm_scope":"source.shell","ace_mode":"sh","codemirror_mode":"shell","codemirror_mime_type":"text/x-sh","language_id":368},"TeX":{"type":"markup","color":"#3D6117","ace_mode":"tex","codemirror_mode":"stex","codemirror_mime_type":"text/x-stex","tm_scope":"text.tex.latex","wrap":true,"aliases":["latex"],"extensions":[".tex",".aux",".bbx",".cbx",".cls",".dtx",".ins",".lbx",".ltx",".mkii",".mkiv",".mkvi",".sty",".toc"],"language_id":369},"Tea":{"type":"markup","extensions":[".tea"],"tm_scope":"source.tea","ace_mode":"text","language_id":370},"Terra":{"type":"programming","extensions":[".t"],"color":"#00004c","tm_scope":"source.terra","ace_mode":"lua","codemirror_mode":"lua","codemirror_mime_type":"text/x-lua","interpreters":["lua"],"language_id":371},"Texinfo":{"type":"prose","wrap":true,"extensions":[".texinfo",".texi",".txi"],"ace_mode":"text","tm_scope":"text.texinfo","interpreters":["makeinfo"],"language_id":988020015},"Text":{"type":"prose","wrap":true,"aliases":["fundamental","plain text"],"extensions":[".txt",".fr",".nb",".ncl",".no"],"filenames":["CITATION","CITATIONS","COPYING","COPYING.regex","COPYRIGHT.regex","FONTLOG","INSTALL","INSTALL.mysql","LICENSE","LICENSE.mysql","NEWS","README.me","README.mysql","README.nss","click.me","delete.me","keep.me","package.mask","package.use.mask","package.use.stable.mask","read.me","readme.1st","test.me","use.mask","use.stable.mask"],"tm_scope":"none","ace_mode":"text","language_id":372},"TextMate Properties":{"type":"data","color":"#df66e4","aliases":["tm-properties"],"filenames":[".tm_properties"],"ace_mode":"properties","codemirror_mode":"properties","codemirror_mime_type":"text/x-properties","tm_scope":"source.tm-properties","language_id":981795023},"Textile":{"type":"prose","color":"#ffe7ac","ace_mode":"textile","codemirror_mode":"textile","codemirror_mime_type":"text/x-textile","wrap":true,"extensions":[".textile"],"tm_scope":"none","language_id":373},"Thrift":{"type":"programming","color":"#D12127","tm_scope":"source.thrift","extensions":[".thrift"],"ace_mode":"text","language_id":374},"Turing":{"type":"programming","color":"#cf142b","extensions":[".t",".tu"],"tm_scope":"source.turing","ace_mode":"text","language_id":375},"Turtle":{"type":"data","extensions":[".ttl"],"tm_scope":"source.turtle","ace_mode":"text","codemirror_mode":"turtle","codemirror_mime_type":"text/turtle","language_id":376},"Twig":{"type":"markup","color":"#c1d026","extensions":[".twig"],"tm_scope":"text.html.twig","ace_mode":"twig","codemirror_mode":"twig","codemirror_mime_type":"text/x-twig","language_id":377},"Type Language":{"type":"data","aliases":["tl"],"extensions":[".tl"],"tm_scope":"source.tl","ace_mode":"text","language_id":632765617},"TypeScript":{"type":"programming","color":"#2b7489","aliases":["ts"],"interpreters":["deno","ts-node"],"extensions":[".ts"],"tm_scope":"source.ts","ace_mode":"typescript","codemirror_mode":"javascript","codemirror_mime_type":"application/typescript","language_id":378},"Unified Parallel C":{"type":"programming","color":"#4e3617","group":"C","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","extensions":[".upc"],"tm_scope":"source.c","language_id":379},"Unity3D Asset":{"type":"data","color":"#222c37","ace_mode":"yaml","codemirror_mode":"yaml","codemirror_mime_type":"text/x-yaml","extensions":[".anim",".asset",".mask",".mat",".meta",".prefab",".unity"],"tm_scope":"source.yaml","language_id":380},"Unix Assembly":{"type":"programming","group":"Assembly","extensions":[".s",".ms"],"tm_scope":"source.x86","ace_mode":"assembly_x86","language_id":120},"Uno":{"type":"programming","color":"#9933cc","extensions":[".uno"],"ace_mode":"csharp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csharp","tm_scope":"source.cs","language_id":381},"UnrealScript":{"type":"programming","color":"#a54c4d","extensions":[".uc"],"tm_scope":"source.java","ace_mode":"java","codemirror_mode":"clike","codemirror_mime_type":"text/x-java","language_id":382},"UrWeb":{"type":"programming","color":"#ccccee","aliases":["Ur/Web","Ur"],"extensions":[".ur",".urs"],"tm_scope":"source.ur","ace_mode":"text","language_id":383},"V":{"type":"programming","color":"#4f87c4","aliases":["vlang"],"extensions":[".v"],"tm_scope":"source.v","ace_mode":"golang","codemirror_mode":"go","codemirror_mime_type":"text/x-go","language_id":603371597},"VBA":{"type":"programming","color":"#867db1","extensions":[".bas",".cls",".frm",".frx",".vba"],"tm_scope":"source.vbnet","aliases":["vb6","visual basic 6","visual basic for applications"],"ace_mode":"text","codemirror_mode":"vb","codemirror_mime_type":"text/x-vb","language_id":399230729},"VBScript":{"type":"programming","color":"#15dcdc","extensions":[".vbs"],"tm_scope":"source.vbnet","ace_mode":"text","codemirror_mode":"vbscript","codemirror_mime_type":"text/vbscript","language_id":408016005},"VCL":{"type":"programming","color":"#148AA8","extensions":[".vcl"],"tm_scope":"source.varnish.vcl","ace_mode":"text","language_id":384},"VHDL":{"type":"programming","color":"#adb2cb","extensions":[".vhdl",".vhd",".vhf",".vhi",".vho",".vhs",".vht",".vhw"],"tm_scope":"source.vhdl","ace_mode":"vhdl","codemirror_mode":"vhdl","codemirror_mime_type":"text/x-vhdl","language_id":385},"Vala":{"type":"programming","color":"#a56de2","extensions":[".vala",".vapi"],"tm_scope":"source.vala","ace_mode":"vala","language_id":386},"Valve Data Format":{"type":"data","color":"#f26025","aliases":["keyvalues","vdf"],"extensions":[".vdf"],"ace_mode":"text","tm_scope":"source.keyvalues","language_id":544060961},"Verilog":{"type":"programming","color":"#b2b7f8","extensions":[".v",".veo"],"tm_scope":"source.verilog","ace_mode":"verilog","codemirror_mode":"verilog","codemirror_mime_type":"text/x-verilog","language_id":387},"Vim Help File":{"type":"prose","color":"#199f4b","aliases":["help","vimhelp"],"extensions":[".txt"],"tm_scope":"text.vim-help","ace_mode":"text","language_id":508563686},"Vim Script":{"type":"programming","color":"#199f4b","tm_scope":"source.viml","aliases":["vim","viml","nvim"],"extensions":[".vim",".vba",".vimrc",".vmb"],"filenames":[".exrc",".gvimrc",".nvimrc",".vimrc","_vimrc","gvimrc","nvimrc","vimrc"],"ace_mode":"text","language_id":388},"Vim Snippet":{"type":"markup","color":"#199f4b","aliases":["SnipMate","UltiSnip","UltiSnips","NeoSnippet"],"extensions":[".snip",".snippet",".snippets"],"tm_scope":"source.vim-snippet","ace_mode":"text","language_id":81265970},"Visual Basic .NET":{"type":"programming","color":"#945db7","extensions":[".vb",".vbhtml"],"aliases":["visual basic","vbnet","vb .net","vb.net"],"tm_scope":"source.vbnet","ace_mode":"text","codemirror_mode":"vb","codemirror_mime_type":"text/x-vb","language_id":389},"Volt":{"type":"programming","color":"#1F1F1F","extensions":[".volt"],"tm_scope":"source.d","ace_mode":"d","codemirror_mode":"d","codemirror_mime_type":"text/x-d","language_id":390},"Vue":{"type":"markup","color":"#41b883","extensions":[".vue"],"tm_scope":"text.html.vue","ace_mode":"html","language_id":391},"Vyper":{"type":"programming","extensions":[".vy"],"color":"#2980b9","ace_mode":"text","tm_scope":"source.vyper","language_id":1055641948},"Wavefront Material":{"type":"data","extensions":[".mtl"],"tm_scope":"source.wavefront.mtl","ace_mode":"text","language_id":392},"Wavefront Object":{"type":"data","extensions":[".obj"],"tm_scope":"source.wavefront.obj","ace_mode":"text","language_id":393},"Web Ontology Language":{"type":"data","color":"#5b70bd","extensions":[".owl"],"tm_scope":"text.xml","ace_mode":"xml","language_id":394},"WebAssembly":{"type":"programming","color":"#04133b","extensions":[".wast",".wat"],"aliases":["wast","wasm"],"tm_scope":"source.webassembly","ace_mode":"lisp","codemirror_mode":"commonlisp","codemirror_mime_type":"text/x-common-lisp","language_id":956556503},"WebIDL":{"type":"programming","extensions":[".webidl"],"tm_scope":"source.webidl","ace_mode":"text","codemirror_mode":"webidl","codemirror_mime_type":"text/x-webidl","language_id":395},"WebVTT":{"type":"data","wrap":true,"extensions":[".vtt"],"tm_scope":"source.vtt","ace_mode":"text","language_id":658679714},"Wget Config":{"type":"data","group":"INI","aliases":["wgetrc"],"filenames":[".wgetrc"],"tm_scope":"source.wgetrc","ace_mode":"text","language_id":668457123},"Wikitext":{"type":"prose","color":"#fc5757","wrap":true,"aliases":["mediawiki","wiki"],"extensions":[".mediawiki",".wiki",".wikitext"],"tm_scope":"text.html.mediawiki","ace_mode":"text","language_id":228},"Win32 Message File":{"type":"data","extensions":[".mc"],"tm_scope":"source.win32-messages","ace_mode":"ini","codemirror_mode":"properties","codemirror_mime_type":"text/x-properties","language_id":950967261},"Windows Registry Entries":{"type":"data","color":"#52d5ff","extensions":[".reg"],"tm_scope":"source.reg","ace_mode":"ini","codemirror_mode":"properties","codemirror_mime_type":"text/x-properties","language_id":969674868},"Witcher Script":{"type":"programming","color":"#ff0000","extensions":[".ws"],"ace_mode":"text","tm_scope":"source.witcherscript","language_id":686821385},"Wollok":{"type":"programming","color":"#a23738","extensions":[".wlk"],"ace_mode":"text","tm_scope":"source.wollok","language_id":632745969},"World of Warcraft Addon Data":{"type":"data","color":"#f7e43f","extensions":[".toc"],"tm_scope":"source.toc","ace_mode":"text","language_id":396},"X BitMap":{"type":"data","group":"C","aliases":["xbm"],"extensions":[".xbm"],"ace_mode":"c_cpp","tm_scope":"source.c","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","language_id":782911107},"X Font Directory Index":{"type":"data","filenames":["encodings.dir","fonts.alias","fonts.dir","fonts.scale"],"tm_scope":"source.fontdir","ace_mode":"text","language_id":208700028},"X PixMap":{"type":"data","group":"C","aliases":["xpm"],"extensions":[".xpm",".pm"],"ace_mode":"c_cpp","tm_scope":"source.c","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","language_id":781846279},"X10":{"type":"programming","aliases":["xten"],"ace_mode":"text","extensions":[".x10"],"color":"#4B6BEF","tm_scope":"source.x10","language_id":397},"XC":{"type":"programming","color":"#99DA07","extensions":[".xc"],"tm_scope":"source.xc","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","language_id":398},"XCompose":{"type":"data","filenames":[".XCompose","XCompose","xcompose"],"tm_scope":"config.xcompose","ace_mode":"text","language_id":225167241},"XML":{"type":"data","color":"#0060ac","tm_scope":"text.xml","ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"text/xml","aliases":["rss","xsd","wsdl"],"extensions":[".xml",".adml",".admx",".ant",".axaml",".axml",".builds",".ccproj",".ccxml",".clixml",".cproject",".cscfg",".csdef",".csl",".csproj",".ct",".depproj",".dita",".ditamap",".ditaval",".dll.config",".dotsettings",".filters",".fsproj",".fxml",".glade",".gml",".gmx",".grxml",".gst",".hzp",".iml",".ivy",".jelly",".jsproj",".kml",".launch",".mdpolicy",".mjml",".mm",".mod",".mxml",".natvis",".ncl",".ndproj",".nproj",".nuspec",".odd",".osm",".pkgproj",".pluginspec",".proj",".props",".ps1xml",".psc1",".pt",".rdf",".res",".resx",".rs",".rss",".sch",".scxml",".sfproj",".shproj",".srdf",".storyboard",".sublime-snippet",".targets",".tml",".ts",".tsx",".ui",".urdf",".ux",".vbproj",".vcxproj",".vsixmanifest",".vssettings",".vstemplate",".vxml",".wixproj",".workflow",".wsdl",".wsf",".wxi",".wxl",".wxs",".x3d",".xacro",".xaml",".xib",".xlf",".xliff",".xmi",".xml.dist",".xmp",".xproj",".xsd",".xspec",".xul",".zcml"],"filenames":[".classpath",".cproject",".project","App.config","NuGet.config","Settings.StyleCop","Web.Debug.config","Web.Release.config","Web.config","packages.config"],"language_id":399},"XML Property List":{"type":"data","color":"#0060ac","group":"XML","extensions":[".plist",".stTheme",".tmCommand",".tmLanguage",".tmPreferences",".tmSnippet",".tmTheme"],"tm_scope":"text.xml.plist","ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"text/xml","language_id":75622871},"XPages":{"type":"data","extensions":[".xsp-config",".xsp.metadata"],"tm_scope":"text.xml","ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"text/xml","language_id":400},"XProc":{"type":"programming","extensions":[".xpl",".xproc"],"tm_scope":"text.xml","ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"text/xml","language_id":401},"XQuery":{"type":"programming","color":"#5232e7","extensions":[".xquery",".xq",".xql",".xqm",".xqy"],"ace_mode":"xquery","codemirror_mode":"xquery","codemirror_mime_type":"application/xquery","tm_scope":"source.xq","language_id":402},"XS":{"type":"programming","extensions":[".xs"],"tm_scope":"source.c","ace_mode":"c_cpp","codemirror_mode":"clike","codemirror_mime_type":"text/x-csrc","language_id":403},"XSLT":{"type":"programming","aliases":["xsl"],"extensions":[".xslt",".xsl"],"tm_scope":"text.xml.xsl","ace_mode":"xml","codemirror_mode":"xml","codemirror_mime_type":"text/xml","color":"#EB8CEB","language_id":404},"Xojo":{"type":"programming","color":"#81bd41","extensions":[".xojo_code",".xojo_menu",".xojo_report",".xojo_script",".xojo_toolbar",".xojo_window"],"tm_scope":"source.xojo","ace_mode":"text","language_id":405},"Xonsh":{"type":"programming","color":"#285EEF","extensions":[".xsh"],"tm_scope":"source.python","ace_mode":"text","codemirror_mode":"python","codemirror_mime_type":"text/x-python","language_id":614078284},"Xtend":{"type":"programming","color":"#24255d","extensions":[".xtend"],"tm_scope":"source.xtend","ace_mode":"text","language_id":406},"YAML":{"type":"data","color":"#cb171e","tm_scope":"source.yaml","aliases":["yml"],"extensions":[".yml",".mir",".reek",".rviz",".sublime-syntax",".syntax",".yaml",".yaml-tmlanguage",".yaml.sed",".yml.mysql"],"filenames":[".clang-format",".clang-tidy",".gemrc","CITATION.cff","glide.lock","yarn.lock"],"ace_mode":"yaml","codemirror_mode":"yaml","codemirror_mime_type":"text/x-yaml","language_id":407},"YANG":{"type":"data","extensions":[".yang"],"tm_scope":"source.yang","ace_mode":"text","language_id":408},"YARA":{"type":"programming","color":"#220000","ace_mode":"text","extensions":[".yar",".yara"],"tm_scope":"source.yara","language_id":805122868},"YASnippet":{"type":"markup","aliases":["snippet","yas"],"color":"#32AB90","extensions":[".yasnippet"],"tm_scope":"source.yasnippet","ace_mode":"text","language_id":378760102},"Yacc":{"type":"programming","extensions":[".y",".yacc",".yy"],"tm_scope":"source.yacc","ace_mode":"text","color":"#4B6C4B","language_id":409},"ZAP":{"type":"programming","color":"#0d665e","extensions":[".zap",".xzap"],"tm_scope":"source.zap","ace_mode":"text","language_id":952972794},"ZIL":{"type":"programming","color":"#dc75e5","extensions":[".zil",".mud"],"tm_scope":"source.zil","ace_mode":"text","language_id":973483626},"Zeek":{"type":"programming","aliases":["bro"],"extensions":[".zeek",".bro"],"tm_scope":"source.zeek","ace_mode":"text","language_id":40},"ZenScript":{"type":"programming","color":"#00BCD1","extensions":[".zs"],"tm_scope":"source.zenscript","ace_mode":"text","language_id":494938890},"Zephir":{"type":"programming","color":"#118f9e","extensions":[".zep"],"tm_scope":"source.php.zephir","ace_mode":"php","language_id":410},"Zig":{"type":"programming","color":"#ec915c","extensions":[".zig"],"tm_scope":"source.zig","ace_mode":"text","language_id":646424281},"Zimpl":{"type":"programming","color":"#d67711","extensions":[".zimpl",".zmpl",".zpl"],"tm_scope":"none","ace_mode":"text","language_id":411},"cURL Config":{"type":"data","group":"INI","aliases":["curlrc"],"filenames":[".curlrc","_curlrc"],"tm_scope":"source.curlrc","ace_mode":"text","language_id":992375436},"desktop":{"type":"data","extensions":[".desktop",".desktop.in",".service"],"tm_scope":"source.desktop","ace_mode":"text","language_id":412},"dircolors":{"type":"data","extensions":[".dircolors"],"filenames":[".dir_colors",".dircolors","DIR_COLORS","_dir_colors","_dircolors","dir_colors"],"tm_scope":"source.dircolors","ace_mode":"text","language_id":691605112},"eC":{"type":"programming","color":"#913960","extensions":[".ec",".eh"],"tm_scope":"source.c.ec","ace_mode":"text","language_id":413},"edn":{"type":"data","ace_mode":"clojure","codemirror_mode":"clojure","codemirror_mime_type":"text/x-clojure","extensions":[".edn"],"tm_scope":"source.clojure","language_id":414},"fish":{"type":"programming","color":"#4aae47","group":"Shell","interpreters":["fish"],"extensions":[".fish"],"tm_scope":"source.fish","ace_mode":"text","language_id":415},"hoon":{"type":"programming","color":"#00b171","tm_scope":"source.hoon","ace_mode":"text","extensions":[".hoon"],"language_id":560883276},"jq":{"color":"#c7254e","ace_mode":"text","type":"programming","extensions":[".jq"],"tm_scope":"source.jq","language_id":905371884},"kvlang":{"type":"markup","ace_mode":"text","extensions":[".kv"],"color":"#1da6e0","tm_scope":"source.python.kivy","language_id":970675279},"mIRC Script":{"type":"programming","color":"#3d57c3","extensions":[".mrc"],"tm_scope":"source.msl","ace_mode":"text","language_id":517654727},"mcfunction":{"type":"programming","color":"#E22837","extensions":[".mcfunction"],"tm_scope":"source.mcfunction","ace_mode":"text","language_id":462488745},"mupad":{"type":"programming","color":"#244963","extensions":[".mu"],"tm_scope":"source.mupad","ace_mode":"text","language_id":416},"nanorc":{"type":"data","color":"#2d004d","group":"INI","extensions":[".nanorc"],"filenames":[".nanorc","nanorc"],"tm_scope":"source.nanorc","ace_mode":"text","language_id":775996197},"nesC":{"type":"programming","color":"#94B0C7","extensions":[".nc"],"ace_mode":"text","tm_scope":"source.nesc","language_id":417},"ooc":{"type":"programming","color":"#b0b77e","extensions":[".ooc"],"tm_scope":"source.ooc","ace_mode":"text","language_id":418},"q":{"type":"programming","extensions":[".q"],"tm_scope":"source.q","ace_mode":"text","color":"#0040cd","language_id":970539067},"reStructuredText":{"type":"prose","color":"#141414","wrap":true,"aliases":["rst"],"extensions":[".rst",".rest",".rest.txt",".rst.txt"],"tm_scope":"text.restructuredtext","ace_mode":"text","codemirror_mode":"rst","codemirror_mime_type":"text/x-rst","language_id":419},"robots.txt":{"type":"data","aliases":["robots","robots txt"],"filenames":["robots.txt"],"ace_mode":"text","tm_scope":"text.robots-txt","language_id":674736065},"sed":{"type":"programming","color":"#64b970","extensions":[".sed"],"interpreters":["gsed","minised","sed","ssed"],"ace_mode":"text","tm_scope":"source.sed","language_id":847830017},"wdl":{"type":"programming","color":"#42f1f4","extensions":[".wdl"],"tm_scope":"source.wdl","ace_mode":"text","language_id":374521672},"wisp":{"type":"programming","ace_mode":"clojure","codemirror_mode":"clojure","codemirror_mime_type":"text/x-clojure","color":"#7582D1","extensions":[".wisp"],"tm_scope":"source.clojure","language_id":420},"xBase":{"type":"programming","color":"#403a40","aliases":["advpl","clipper","foxpro"],"extensions":[".prg",".ch",".prw"],"tm_scope":"source.harbour","ace_mode":"text","language_id":421}}
diff --git a/hgitaly/manifest.py b/hgitaly/manifest.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9tYW5pZmVzdC5weQ==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9tYW5pZmVzdC5weQ== 100644
--- a/hgitaly/manifest.py
+++ b/hgitaly/manifest.py
@@ -20,6 +20,8 @@
 of the core manifest object it works on.
 """
 import attr
+from mercurial.context import filectx
+from mercurial.utils.stringutil import binary as is_binary
 
 
 @attr.s
@@ -146,9 +148,47 @@
         to be very different, since it lists full paths to leaf
         (non-directory) files. In particular, there are no empty directories.
 
-        The interpretation (would have to be formally proven) is that
-        the "flat path" is the longest common directory ancestor of all file
-        entries that are inside the considered directory entry.
+        The interpretation  is that the "flat path" is the longest
+        common directory ancestor of all file entries that are inside the
+        considered directory entry. Here is a proof (can certainly be much
+        simplified in a direct reasoning not involving reductio at absurdum,
+        but writing it down was enough to convince ourselves).
+
+        0. Assumptions and conventions:
+
+           + there is no empty directory (guaranteed by the manifest data
+             structure in the Mercurial case).
+           + by X≤Y, we mean that Y is a path ancestor of X (inclusive)
+           + let E be the considered directory entry and Fp its flat path
+             as computed by the Golang implementation
+
+        1. Fp is a common directory ancestor of all file entries f such that
+           f≤E
+
+           Let G be the greatest common ancestor of Fp and f.
+           The crawling performed by the implementation went through G,
+           since Fp≤G, so let's see what happened then.
+           If f is a direct child of G, then Fp=G. If not, then let h be the
+           direct child of G such that f≤h. h cannot be the only directory
+           child of g: otherwise we would have Fp≤h, and h would be a greater
+           common ancestor of Fp and f. Therefore the crawling stopped at G,
+           hence Fp=G.
+
+        2. Let G be the greatest common directory ancestor of all file entries
+           f such that f≤E are ancestors of Fp.
+
+           By derinition of "greatest", G≤Fp because Fp is a common directory
+           ancestor.
+           Let's see what the crawling implementation did at G if G≠Fp:
+
+           Because of the halting condition of the implementation,
+           either Fp has a file child and is therefore the greatest common
+           directory ancestor, or Fp has at least two distinct directory
+           children. Obviously, Only one of those can be an ancestor of G.
+           So let h be a child of Fp such that G≤h is false. There is at least
+           a file entry k such that k≤h, because directory entries cannot be
+           empty. G is not an ancestor of k, contradicting its definition,
+           so G must be equal to Fp.
 
         This implementation relies on manifest to emit paths in sorted manner.
         """
@@ -207,6 +247,17 @@
 
             yield file_path
 
+    def iter_files_with_content(self, exclude_binary=False):
+        manifest = self.changeset.manifest()
+        repo = self.changeset.repo().unfiltered()
+        for file_path, file_node, flags in manifest.iterentries():
+            # filectx has a `isbinary` method, but it would actually
+            # read the data once more (not cached on self)
+            content = filectx(repo, file_path, fileid=file_node).data()
+            if exclude_binary and is_binary(content):
+                continue
+            yield file_path, content, flags
+
 
 def miner(changeset):
     """Return an appropriate manifest extractor for the given changeset.
diff --git a/hgitaly/message.py b/hgitaly/message.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9tZXNzYWdlLnB5..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9tZXNzYWdlLnB5 100644
--- a/hgitaly/message.py
+++ b/hgitaly/message.py
@@ -38,6 +38,7 @@
     diff_opts,
     chunk_stats,
 )
+from .gitlab_ref import reverse_refs
 
 
 LONG_BYTES_TRUNCATION = 50
@@ -148,7 +149,28 @@
     return CommitStatInfo(additions=adds, deletions=dels, changed_files=files)
 
 
-def commit(ctx, with_short_stats=False):
+def ref_segments_match(ref, pattern):
+    """Tell if given ref first segments are exactly as in pattern."""
+    pat_split = pattern.rstrip(b'/').split(b'/')
+    ref_split = ref.split(b'/', len(pat_split))
+    for r, p in zip(ref_split, pat_split):
+        if r != p:
+            return False
+    return True
+
+
+def commit_referenced_by(ctx, patterns):
+    """Return references to the given commit, as full ref paths."""
+    if not patterns:
+        return
+
+    repo = ctx.repo()
+    refs = reverse_refs(repo).get(ctx.hex(), ())
+    return [r for r in refs
+            if any(ref_segments_match(r, pat) for pat in patterns)]
+
+
+def commit(ctx, with_short_stats=False, include_referenced_by=()):
     """Return :class:`GitCommit` object from Mercurial :class:`changectx`.
 
     :param bool with_short_stats: if ``True``, fill-in the ``short_stats``
@@ -185,6 +207,7 @@
     descr = ctx.description()
     author = commit_author(ctx)
     short_stats = commit_stats(ctx) if with_short_stats else None
+    referenced_by = commit_referenced_by(ctx, include_referenced_by)
     return GitCommit(id=ctx.hex(),
                      subject=descr.split(b'\n', 1)[0].rstrip(b'\r\n'),
                      body=descr,
@@ -195,6 +218,7 @@
                      author=author,
                      committer=author,
                      short_stats=short_stats,
+                     referenced_by=referenced_by,
                      )
 
 
diff --git a/hgitaly/server/mono.py b/hgitaly/server/mono.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zZXJ2ZXIvbW9uby5weQ==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2ZXIvbW9uby5weQ== 100644
--- a/hgitaly/server/mono.py
+++ b/hgitaly/server/mono.py
@@ -32,6 +32,7 @@
 from ..service.ref import RefServicer
 from ..service.diff import DiffServicer
 from ..service.mercurial_changeset import MercurialChangesetServicer
+from ..service.mercurial_operations import MercurialOperationsServicer
 from ..service.mercurial_repository import MercurialRepositoryServicer
 from ..service.repository import RepositoryServicer
 from ..service.server import ServerServicer
@@ -46,6 +47,9 @@
 from ..stub.mercurial_changeset_pb2_grpc import (
     add_MercurialChangesetServiceServicer_to_server
 )
+from ..stub.mercurial_operations_pb2_grpc import (
+    add_MercurialOperationsServiceServicer_to_server
+)
 from ..stub.mercurial_repository_pb2_grpc import (
     add_MercurialRepositoryServiceServicer_to_server
 )
@@ -91,6 +95,8 @@
     add_DiffServiceServicer_to_server(DiffServicer(storages), server)
     add_MercurialChangesetServiceServicer_to_server(
         MercurialChangesetServicer(storages), server)
+    add_MercurialOperationsServiceServicer_to_server(
+        MercurialOperationsServicer(storages), server)
     add_MercurialRepositoryServiceServicer_to_server(
         MercurialRepositoryServicer(storages), server)
     add_RepositoryServiceServicer_to_server(
diff --git a/hgitaly/service/commit.py b/hgitaly/service/commit.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zZXJ2aWNlL2NvbW1pdC5weQ==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2aWNlL2NvbW1pdC5weQ== 100644
--- a/hgitaly/service/commit.py
+++ b/hgitaly/service/commit.py
@@ -34,6 +34,10 @@
 from ..git import (
     OBJECT_MODE_TREE,
 )
+from ..linguist import (
+    language_color,
+    language_stats,
+)
 from ..logging import LoggerAdapter
 from ..oid import (
     tree_oid,
@@ -163,10 +167,8 @@
         repo = self.load_repo(request.repository, context).unfiltered()
         changeset = gitlab_revision_changeset(repo, request.revision)
         if changeset is None:
-            # in case of unknown revision, Gitaly error details are
-            # still about the path only (as of v15.4.6)
-            context.abort(StatusCode.NOT_FOUND,
-                          'not found: ' + os.fsdecode(request.path))
+            # As of v15.8.0, Gitaly error details don't give up anything
+            context.abort(StatusCode.NOT_FOUND, "tree entry not found")
 
         sha = changeset.hex().decode('ascii')
         # early testing shows that even for leaf files, Gitaly ignores
@@ -193,7 +195,7 @@
             if max_size != 0 and size > request.max_size:
                 context.abort(
                     StatusCode.FAILED_PRECONDITION,
-                    "TreeEntry: object size (%d) is bigger than the maximum "
+                    "object size (%d) is bigger than the maximum "
                     "allowed size (%d)" % (size, max_size))
 
             data = filectx.data()
@@ -221,8 +223,7 @@
 
         subtrees, file_paths = manifest.miner(changeset).ls_dir(path)
         if not subtrees and not file_paths:
-            context.abort(StatusCode.NOT_FOUND,
-                          'not found: ' + os.fsdecode(path))
+            context.abort(StatusCode.NOT_FOUND, "tree entry not found")
 
         # path is an actual directory
 
@@ -417,7 +418,7 @@
                 if entry_oid(path, is_dir) == page_token:
                     break
             else:
-                context.abort(StatusCode.UNKNOWN,
+                context.abort(StatusCode.INTERNAL,
                               "could not find starting OID: %s" % page_token)
 
             entries = entries[offset+1:]
@@ -572,5 +573,7 @@
             if request.all:
                 revs = repo.revs(b"reverse(%ld)", revs)
 
+        incl_ref_by = request.include_referenced_by
+
         with_short_stats = request.include_shortstat
         for chunk in chunked(revs):
@@ -575,6 +578,4 @@
         with_short_stats = request.include_shortstat
         for chunk in chunked(revs):
-            if not chunk:
-                continue
             yield FindCommitsResponse(
                 commits=(message.commit(repo[rev],
@@ -579,7 +580,8 @@
             yield FindCommitsResponse(
                 commits=(message.commit(repo[rev],
+                                        include_referenced_by=incl_ref_by,
                                         with_short_stats=with_short_stats)
                          for rev in chunk))
 
     def CommitLanguages(self, request: CommitLanguagesRequest,
                         context) -> CommitLanguagesResponse:
@@ -581,12 +583,33 @@
                                         with_short_stats=with_short_stats)
                          for rev in chunk))
 
     def CommitLanguages(self, request: CommitLanguagesRequest,
                         context) -> CommitLanguagesResponse:
-        not_implemented(context, issue=12)  # pragma no cover
+        repo = self.load_repo(request.repository, context)
+        changeset = gitlab_revision_changeset(repo, request.revision)
+        tokei_bin_path = os.fsdecode(
+            repo.ui.config(b'hgitaly', b'tokei-executable')
+        )
+        with self.working_dir(gl_repo=request.repository,
+                              repo=repo,
+                              context=context,
+                              changeset=changeset) as wd:
+
+            stats, total = language_stats(tokei_bin_path, wd.path, context)
+            Language = CommitLanguagesResponse.Language
+            resp_langs = []
+            for lang_name, lang_stats in stats.items():
+                resp_langs.append(Language(
+                    name=lang_name,
+                    share=100 * lang_stats['total_bytes'] / total,
+                    bytes=lang_stats['total_bytes'],
+                    color=language_color(lang_name),
+                ))
+            resp_langs.sort(key=lambda lang: -lang.share)
+            return CommitLanguagesResponse(languages=resp_langs)
 
     def RawBlame(self, request: RawBlameRequest,
                  context) -> RawBlameResponse:
         repo = self.load_repo(request.repository, context)
         filepath = request.path
         if not filepath:
@@ -587,10 +610,10 @@
 
     def RawBlame(self, request: RawBlameRequest,
                  context) -> RawBlameResponse:
         repo = self.load_repo(request.repository, context)
         filepath = request.path
         if not filepath:
-            context.abort(StatusCode.INVALID_ARGUMENT, "RawBlame: empty Path")
+            context.abort(StatusCode.INVALID_ARGUMENT, "empty Path")
 
         revision = pycompat.sysbytes(request.revision)
         ctx = gitlab_revision_changeset(repo, revision)
@@ -606,4 +629,7 @@
         repo = self.load_repo(request.repository, context)
         revision, path = request.revision, request.path
         ctx = gitlab_revision_changeset(repo, revision)
+        if ctx is None:
+            return LastCommitForPathResponse()
+
         changeset = latest_changeset_for_path(path, ctx)
@@ -609,4 +635,5 @@
         changeset = latest_changeset_for_path(path, ctx)
+        # TODO global_options.literal_pathspecs
         return LastCommitForPathResponse(commit=message.commit(changeset))
 
     def ListLastCommitsForTree(self, request: ListLastCommitsForTreeRequest,
@@ -642,5 +669,16 @@
 
         # subtrees first, then regular files, each one in lexicographical order
         subtrees, file_paths = manifest.miner(from_ctx).ls_dir(req_path)
+        end = offset + limit
+        nb_subtrees = len(subtrees)
+        if nb_subtrees > end:
+            subtrees = subtrees[offset:end]
+            file_paths = ()
+        else:
+            subtrees = subtrees[offset:end]
+            file_paths = file_paths[max(offset-nb_subtrees, 0):end-nb_subtrees]
+
+        changesets = latest_changesets_for_paths(subtrees, file_paths,
+                                                 from_ctx)
         all_paths = subtrees
         all_paths.extend(file_paths)
@@ -645,7 +683,6 @@
         all_paths = subtrees
         all_paths.extend(file_paths)
-
-        for chunk in chunked(all_paths[offset:offset + limit]):
+        for chunk in chunked(all_paths):
             yield ListLastCommitsForTreeResponse(
                 commits=[
                     message.commit_for_tree(
@@ -649,7 +686,7 @@
             yield ListLastCommitsForTreeResponse(
                 commits=[
                     message.commit_for_tree(
-                        latest_changeset_for_path(path, from_ctx),
+                        changesets[path],
                         path
                     )
                     for path in chunk
@@ -991,9 +1028,14 @@
     return dt.isoformat()
 
 
+def ancestors_inclusive(changeset):
+    yield changeset
+    yield from changeset.ancestors()
+
+
 def latest_changeset_for_path(path, seen_from):
     """Return latest ancestor of ``seen_from`` that touched the given path.
 
     :param bytes path: subdir or file
     :param seen_from: changectx
     """
@@ -994,20 +1036,66 @@
 def latest_changeset_for_path(path, seen_from):
     """Return latest ancestor of ``seen_from`` that touched the given path.
 
     :param bytes path: subdir or file
     :param seen_from: changectx
     """
-    # gracinet: just hoping that performance wise, this does the right
-    # thing, i.e do any scanning from the end
-    # While we can be reasonably confident that the file exists
-    # in the given revision, there are cases where deduplication implies
-    # that the filelog() predicate would not see any new file revision
-    # in some subgraph, because it's identical to another one that's not
-    # in that subgraph. Hence using the slower `file` is the only way
-    # to go.
-    repo = seen_from.repo()
-    rev = repo.revs('file(%s) and ::%s', b'path:' + path, seen_from).last()
-    return None if rev is None else repo[rev]
+    if not path or path in (b'.', b'/', b'./'):
+        # interpeted as a directory, this would be the repository root, which
+        # all changesets change.
+        return seen_from
+
+    if path.endswith(b'/'):
+        dir_prefix = path
+    else:
+        dir_prefix = path + b'/'
+
+    for changeset in ancestors_inclusive(seen_from):
+        files = changeset.files()
+        # TODO some performance improvement possible with a binary
+        # search if changeset files are guaranteed to be sorted
+        # but it'd probably make more of a difference in a Rust impl
+        for fpath in files:
+            if fpath == path or fpath.startswith(dir_prefix):
+                return changeset
+
+
+def latest_changesets_for_paths(subdirs, file_paths, seen_from):
+    """Return latest ancestor of ``seen_from`` that touched the given path.
+
+    :param bytes path: subdir or file
+    :param seen_from: changectx
+    """
+    dir_prefixes = set(d.rstrip(b'/') + b'/' for d in subdirs)
+    file_paths = set(file_paths)
+
+    latest_changesets = {}
+
+    for changeset in ancestors_inclusive(seen_from):
+        if not dir_prefixes and not file_paths:
+            break
+
+        for cfp in changeset.files():
+            # TODO some performance improvement possible with a binary
+            # search if changeset files are guaranteed to be sorted
+            # but it'd probably make more of a difference in a Rust impl
+            for dp in dir_prefixes:
+                if cfp.startswith(dp):
+                    latest_changesets[dp.rstrip(b'/')] = changeset
+                    dir_prefixes.discard(dp)
+                    break
+            else:
+                for fp in file_paths:
+                    if fp == cfp:
+                        latest_changesets[fp] = changeset
+                        break
+                else:
+                    continue
+                file_paths.discard(fp)
+                continue
+
+            dir_prefixes.discard(dp)
+
+    return latest_changesets
 
 
 def blamelines(repo, ctx, file):
diff --git a/hgitaly/service/diff.py b/hgitaly/service/diff.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zZXJ2aWNlL2RpZmYucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2aWNlL2RpZmYucHk= 100644
--- a/hgitaly/service/diff.py
+++ b/hgitaly/service/diff.py
@@ -25,6 +25,7 @@
     chunk_stats,
     diff_opts,
 )
+from ..errors import not_implemented
 from ..git import (
     NULL_BLOB_OID,
     EMPTY_TREE_OID,
@@ -51,6 +52,8 @@
     CommitDeltaResponse,
     FindChangedPathsRequest,
     FindChangedPathsResponse,
+    GetPatchIDRequest,
+    GetPatchIDResponse,
     RawDiffRequest,
     RawDiffResponse,
     RawPatchRequest,
@@ -266,8 +269,7 @@
         parsed_request = parse_diff_request(self, request, context)
         parsed, repo, ctx_from, ctx_to = parsed_request
         if not parsed:
-            context.abort(StatusCode.UNAVAILABLE,
-                          "CommitDiff: exit status 128")
+            context.abort(StatusCode.UNAVAILABLE, "exit status 128")
 
         m = None
         if request.paths:
@@ -338,8 +340,7 @@
         parsed_request = parse_diff_request(self, request, context)
         _parsed, repo, ctx_from, ctx_to = parsed_request
         if not _parsed:
-            context.abort(StatusCode.UNAVAILABLE,
-                          "CommitDelta: exit status 128")
+            context.abort(StatusCode.UNAVAILABLE, "exit status 128")
         m = None
         if request.paths:
             m = scmutil.matchfiles(repo, request.paths)
@@ -386,7 +387,7 @@
         parsed_request = parse_diff_request(self, request, context)
         _parsed, repo, ctx_from, ctx_to = parsed_request
         if not _parsed:
-            context.abort(StatusCode.UNKNOWN, "exit status 128")
+            context.abort(StatusCode.INTERNAL, "exit status 128")
         opts = {b'git': True}
         overrides = {
             (b'experimental', b'extendedheader.similarity'): True,
@@ -418,7 +419,7 @@
         parsed_request = parse_diff_request(self, request, context)
         _parsed, repo, ctx_from, ctx_to = parsed_request
         if not _parsed:
-            context.abort(StatusCode.UNKNOWN, "exit status 128")
+            context.abort(StatusCode.INTERNAL, "exit status 128")
         opts = {b'git': True}
         diffopts = diffutil.diffallopts(repo.ui, opts)
         ui = repo.ui
@@ -439,7 +440,7 @@
         parsed_request = parse_diff_request(self, request, context)
         _parsed, repo, ctx_from, ctx_to = parsed_request
         if not _parsed:
-            context.abort(StatusCode.UNAVAILABLE, "DiffStats: exit status 128")
+            context.abort(StatusCode.UNAVAILABLE, "exit status 128")
 
         diffchunks = ctx_to.diff(ctx_from, opts=diff_opts(repo))
 
@@ -508,6 +509,10 @@
                              for path in changed_paths(repo, *extr)):
             yield FindChangedPathsResponse(paths=paths)
 
+    def GetPatchID(self, request: GetPatchIDRequest,
+                   context) -> GetPatchIDResponse:
+        not_implemented(context, issue=131)  # pragma no cover
+
 
 def fcp_resolve_commit(context, repo, revision):
     ctx = gitlab_revision_changeset(
diff --git a/hgitaly/service/mercurial_operations.py b/hgitaly/service/mercurial_operations.py
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2aWNlL21lcmN1cmlhbF9vcGVyYXRpb25zLnB5
--- /dev/null
+++ b/hgitaly/service/mercurial_operations.py
@@ -0,0 +1,124 @@
+# Copyright 2023 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
+from grpc import StatusCode
+import logging
+
+from mercurial.merge import merge
+from mercurial.phases import (
+    public as PUBLIC,
+)
+
+from .. import message
+from ..changelog import (
+    ancestor,
+    merge_content,
+)
+from ..logging import LoggerAdapter
+from ..revision import (
+    gitlab_revision_changeset,
+)
+
+from ..stub.mercurial_operations_pb2 import (
+    MergeAnalysisRequest,
+    MergeAnalysisResponse,
+)
+from ..stub.mercurial_operations_pb2_grpc import (
+    MercurialOperationsServiceServicer,
+)
+from ..servicer import HGitalyServicer
+
+base_logger = logging.getLogger(__name__)
+
+MERGE_CONFLICTS_LABELS = [b'working copy', b'merge rev', b'common ancestor']
+
+
+class MercurialOperationsServicer(MercurialOperationsServiceServicer,
+                                  HGitalyServicer):
+    """MercurialOperationService implementation.
+
+    The ordering of methods in this source file is the same as in the proto
+    file.
+    """
+    def MergeAnalysis(self,
+                      request: MergeAnalysisRequest,
+                      context) -> MergeAnalysisResponse:
+        logger = LoggerAdapter(base_logger, context)
+        repo = self.load_repo(request.repository, context)
+        source_cs = gitlab_revision_changeset(repo, request.source_revision)
+        if source_cs is None:
+            context.abort(
+                StatusCode.INVALID_ARGUMENT,
+                "Source revision %r not found" % request.source_revision)
+        target_cs = gitlab_revision_changeset(repo, request.target_revision)
+        if target_cs is None:
+            context.abort(
+                StatusCode.INVALID_ARGUMENT,
+                "Target revision %r not found" % request.target_revision)
+
+        source_branch = source_cs.branch()
+        target_branch = target_cs.branch()
+        logger.info("Merge Analysis: source branch %r, target branch %r",
+                    source_branch, target_branch)
+        is_ff = (ancestor(source_cs, target_cs) == target_cs.rev()
+                 and source_branch == target_branch)
+
+        has_obsolete = has_unstable = False
+        for cs in merge_content(source_cs, target_cs):
+            if not has_obsolete:
+                has_obsolete = cs.obsolete()
+            if not has_unstable:
+                has_unstable = cs.isunstable()
+            if has_obsolete and has_unstable:
+                break
+
+        has_conflicts = False
+        if not (is_ff
+                or has_obsolete or has_unstable
+                or request.skip_conflicts_check):
+            with self.working_dir(gl_repo=request.repository,
+                                  repo=repo,
+                                  context=context,
+                                  changeset=target_cs) as wd:
+                has_conflicts = not wd_merge(wd, source_cs)
+
+        res = MergeAnalysisResponse(
+            is_fast_forward=is_ff,
+            has_obsolete_changesets=has_obsolete,
+            has_unstable_changesets=has_unstable,
+            has_conflicts=has_conflicts,
+            target_is_public=target_cs.phase() == PUBLIC,
+            target_node_id=target_cs.hex().decode('ascii'),
+            target_branch=target_cs.branch(),
+            target_topic=target_cs.topic(),
+            source_node_id=source_cs.hex().decode('ascii'),
+            source_branch=source_cs.branch(),
+            source_topic=source_cs.topic(),
+        )
+        logger.info("MergeAnalysis result %r", message.Logging(res))
+        return res
+
+
+def wd_merge(working_dir, source_cs):
+    """Merge source_cs in the given a working directory (repo share).
+
+    :source_cs: a :class:`changectx`, usually not tied to ``working_dir`` but
+      to its share source or a share sibling.
+    :return: whether it suceeded
+    """
+    # re-evalutate the changectx in the context of working_dir,
+    # as `merge()` will read the repo from it
+    repo = working_dir.repo
+    source_for_wd = repo[source_cs.rev()]
+
+    overrides = {(b'ui', b'forcemerge'): b'internal:merge3',
+                 (b'ui', b'interactive'): b'off'}
+    with repo.ui.configoverride(overrides, b'merge'):
+        # not sure labels are really necessary, but it is
+        # possible that the merge tools require them.
+        stats = merge(source_for_wd,
+                      labels=MERGE_CONFLICTS_LABELS)
+        return not stats.unresolvedcount
diff --git a/hgitaly/service/ref.py b/hgitaly/service/ref.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zZXJ2aWNlL3JlZi5weQ==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2aWNlL3JlZi5weQ== 100644
--- a/hgitaly/service/ref.py
+++ b/hgitaly/service/ref.py
@@ -8,6 +8,7 @@
 from grpc import StatusCode
 
 from heptapod.gitlab.branch import (
+    GITLAB_BRANCH_REF_PREFIX,
     gitlab_branch_from_ref,
     gitlab_branch_ref,
 )
@@ -11,6 +12,9 @@
     gitlab_branch_from_ref,
     gitlab_branch_ref,
 )
+from heptapod.gitlab.tag import (
+    GITLAB_TAG_REF_PREFIX,
+)
 from hgext3rd.heptapod.branch import get_default_gitlab_branch
 from hgext3rd.heptapod.typed_ref import GITLAB_TYPED_REFS_MISSING
 from hgext3rd.heptapod.special_ref import (
@@ -26,10 +30,9 @@
     not_implemented,
     structured_abort,
 )
-from .. import feature
 from ..logging import LoggerAdapter
 from ..pagination import (
     extract_limit,
 )
 from ..stub.shared_pb2 import (
     Branch,
@@ -30,10 +33,9 @@
 from ..logging import LoggerAdapter
 from ..pagination import (
     extract_limit,
 )
 from ..stub.shared_pb2 import (
     Branch,
-    GitCommit,
     SortDirection,
 )
 from ..stub.errors_pb2 import (
@@ -47,8 +49,6 @@
     FindAllTagNamesRequest,
     FindAllTagNamesResponse,
     FindLocalBranchesRequest,
-    FindLocalBranchCommitAuthor,
-    FindLocalBranchResponse,
     FindLocalBranchesResponse,
     FindAllBranchesRequest,
     FindAllBranchesResponse,
@@ -137,5 +137,4 @@
             # used in the hg_fixup_default_branch in PostReceive…
             # TODO now that we have richer notifications, make a clear one
             # for that case
-            branch = b''
             logger.warning("FindDefaultBranchName: no information stored "
@@ -141,6 +140,7 @@
             logger.warning("FindDefaultBranchName: no information stored "
-                           "for repo at %r, returning harcoded default %r",
-                           repo, branch)
+                           "for repo at %r, returning hempty response",
+                           repo)
+            return FindDefaultBranchNameResponse()
 
         return FindDefaultBranchNameResponse(name=gitlab_branch_ref(branch))
 
@@ -177,16 +177,8 @@
         after = after.encode('utf-8') if after else None
         sort_by = FIND_LOCAL_BRANCHES_SORT_BY_TO_INNER.get(request.sort_by)
 
-        if feature.is_enabled(context,
-                              'simplify-find-local-branches-response'):
-            resp_key = 'local_branches'
-            branch_resp = message.branch
-        else:
-            resp_key = 'branches'
-            branch_resp = find_local_branch_response
-
         for chunk in chunked(sorted_gitlab_branches_as_refs(repo,
                                                             sort_by=sort_by,
                                                             after=after),
                              limit=limit):
             yield FindLocalBranchesResponse(
@@ -188,10 +180,11 @@
         for chunk in chunked(sorted_gitlab_branches_as_refs(repo,
                                                             sort_by=sort_by,
                                                             after=after),
                              limit=limit):
             yield FindLocalBranchesResponse(
-                **{resp_key: (branch_resp(name, head) for name, head in chunk)
-                   })
+                local_branches=(message.branch(name, head)
+                                for name, head in chunk)
+            )
 
     def FindAllBranches(self,
                         request: FindAllBranchesRequest,
@@ -290,8 +283,7 @@
         refs = request.refs
         if refs and except_prefix:
             context.abort(StatusCode.INVALID_ARGUMENT,
-                          "DeleteRefs: ExceptWithPrefix and Refs are "
-                          "mutually exclusive")
+                          "ExceptWithPrefix and Refs are mutually exclusive")
 
         repo = self.load_repo(request.repository, context)
         srefs = special_refs(repo)
@@ -447,4 +439,5 @@
 
         Reference = ListRefsResponse.Reference
         repo = self.load_repo(request.repository, context)
+        patterns = request.patterns
 
@@ -450,9 +443,10 @@
 
-        refs = [(b'ALL', ZERO_SHA_STR)] if len(repo) else []
-        refs.extend(iter_gitlab_branches_as_refs(repo, deref=False))
+        refs = []
+        if patterns == [b'refs/'] and len(repo):
+            refs.append((b'ALL', ZERO_SHA_STR))
         if request.head:
             branch = get_default_gitlab_branch(repo)
             if branch is not None:
                 head = gitlab_branch_head(repo, branch)
                 if head is not None:
                     refs.append((b'HEAD', head.hex().decode('ascii')))
@@ -453,10 +447,26 @@
         if request.head:
             branch = get_default_gitlab_branch(repo)
             if branch is not None:
                 head = gitlab_branch_head(repo, branch)
                 if head is not None:
                     refs.append((b'HEAD', head.hex().decode('ascii')))
-        refs.extend(iter_gitlab_tags_as_refs(repo, deref=False))
-        refs.extend(iter_gitlab_special_refs_as_refs(repo, deref=False))
-        refs.extend(iter_keep_arounds_as_refs(repo, deref=False))
+        if patterns == [GITLAB_BRANCH_REF_PREFIX]:
+            refs.extend(iter_gitlab_branches_as_refs(repo, deref=False))
+        elif patterns == [GITLAB_TAG_REF_PREFIX]:
+            refs.extend(iter_gitlab_tags_as_refs(repo, deref=False))
+        else:
+            if patterns:
+                patterns = tuple(pat.split(b'/') for pat in patterns)
+            refs.extend(iter_gitlab_branches_as_refs(repo,
+                                                     deref=False,
+                                                     patterns=patterns))
+            refs.extend(iter_gitlab_tags_as_refs(repo,
+                                                 deref=False,
+                                                 patterns=patterns))
+            refs.extend(iter_gitlab_special_refs_as_refs(repo,
+                                                         deref=False,
+                                                         patterns=patterns))
+            refs.extend(iter_keep_arounds_as_refs(repo,
+                                                  deref=False,
+                                                  patterns=patterns))
 
@@ -462,5 +472,23 @@
 
-        if not refs:
+        pointing_at = request.pointing_at_oids
+        if pointing_at:
+            refs = [r for r in refs
+                    if r[1].encode('ascii') in pointing_at or r[0] == b'HEAD']
+
+        # ignore `peel_tags`:
+        # Mercurial has no real equivalent of Git's annotated tags. The
+        # closest notion would be the tagging changeset, but that is not
+        # well-defined. Hence Mercurial tags always point to the target commit.
+        # We could force the `peeled_target` attribute of the `Reference`
+        # messages, but that would not bring us to better compliance: the
+        # protocol comment states explicitely that it should stay empty if
+        # the object is not an annotated tag, and this is confirmed by
+        # Gitaly Comparison tests (tag created by hg-git in this case).
+        # On the contrary, filling-in `peeled_target` could very well make
+        # clients believe that the tag is annotated, leading to dedicated
+        # actions that can only go wrong.
+
+        if len(refs) < (2 if request.head else 1):
             # avoid yielding an empty response, as Gitaly does not.
             # TODO consider doing that in the `chunked()` helper,
             # this looks to be rather systematic.
@@ -464,6 +492,12 @@
             # avoid yielding an empty response, as Gitaly does not.
             # TODO consider doing that in the `chunked()` helper,
             # this looks to be rather systematic.
+            # also in this case, even if `head` is `True`, in the Gitaly
+            # impl, it looks like nothing being read from Git does not
+            # trigger any sending, even though the sender created by
+            # `newListRefsWriter` would emit HEAD first in any case… if it
+            # emitted anything. See Gitaly Comparison test for validation of
+            # this.
             return
 
         refs.sort()
@@ -478,29 +512,4 @@
         not_implemented(context, issue=89)  # pragma no cover
 
 
-def flbr_author(commit: GitCommit):
-    """Extract commit intro specific fields of FindLocalBranchCommitAuthor."""
-    return FindLocalBranchCommitAuthor(
-        name=commit.name,
-        email=commit.email,
-        date=commit.date,
-        timezone=commit.timezone,
-    )
-
-
-def find_local_branch_response(name, head):
-    commit = message.commit(head)
-    return FindLocalBranchResponse(
-        name=name,
-        commit_id=commit.id,
-        commit_subject=commit.subject,
-        commit_author=flbr_author(commit.author),
-        commit_committer=flbr_author(commit.committer),
-        commit=commit,
-    )
-
-
-TAG_REF_PREFIX = b'refs/tags/'
-
-
 def gitlab_tag_from_ref(ref):
@@ -506,6 +515,6 @@
 def gitlab_tag_from_ref(ref):
-    if ref.startswith(TAG_REF_PREFIX):
-        return ref[len(TAG_REF_PREFIX):]
+    if ref.startswith(GITLAB_TAG_REF_PREFIX):
+        return ref[len(GITLAB_TAG_REF_PREFIX):]
 
 
 def remove_special_refs(special_refs, to_remove):
diff --git a/hgitaly/service/repository.py b/hgitaly/service/repository.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zZXJ2aWNlL3JlcG9zaXRvcnkucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2aWNlL3JlcG9zaXRvcnkucHk= 100644
--- a/hgitaly/service/repository.py
+++ b/hgitaly/service/repository.py
@@ -6,6 +6,7 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 import gc
 from grpc import StatusCode
+from io import BytesIO
 import itertools
 import logging
 import os
@@ -9,6 +10,8 @@
 import itertools
 import logging
 import os
+from pathlib import Path
+from collections import deque
 import re
 import shutil
 import tempfile
@@ -94,9 +97,11 @@
     FullPathResponse,
     GetRawChangesRequest,
     GetRawChangesResponse,
+    GetCustomHooksRequest,
+    GetCustomHooksResponse,
     RepositoryExistsRequest,
     RepositoryExistsResponse,
     GetArchiveRequest,
     GetArchiveResponse,
     HasLocalBranchesRequest,
     HasLocalBranchesResponse,
@@ -97,9 +102,11 @@
     RepositoryExistsRequest,
     RepositoryExistsResponse,
     GetArchiveRequest,
     GetArchiveResponse,
     HasLocalBranchesRequest,
     HasLocalBranchesResponse,
+    RemoveAllRequest,
+    RemoveAllResponse,
     RemoveRepositoryRequest,
     RemoveRepositoryResponse,
     RestoreCustomHooksRequest,
@@ -108,6 +115,8 @@
     SearchFilesByContentResponse,
     SearchFilesByNameRequest,
     SearchFilesByNameResponse,
+    SetCustomHooksRequest,
+    SetCustomHooksResponse,
     SetFullPathRequest,
     SetFullPathResponse,
     WriteRefRequest,
@@ -152,7 +161,7 @@
         repo = self.load_repo(request.repository, context)
         if len(request.revisions) < 2:
             context.abort(StatusCode.INVALID_ARGUMENT,
-                          'FindMergeBase: at least 2 revisions are required')
+                          'at least 2 revisions are required')
 
         ctxs = []
         for rev in request.revisions:
@@ -354,11 +363,10 @@
             rx = None
         else:
             if len(filt) > SEARCH_FILES_FILTER_MAX_LENGTH:
-                context.abort(
-                    StatusCode.INVALID_ARGUMENT,
-                    "SearchFilesByName: filter exceeds maximum length")
+                context.abort(StatusCode.INVALID_ARGUMENT,
+                              "filter exceeds maximum length")
             # TODO what to do for invalid Regexp? Especially with incoming
             # format being Golang's
             try:
                 rx = re.compile(filt.encode('utf8'))
             except re.error as exc:
@@ -360,12 +368,10 @@
             # TODO what to do for invalid Regexp? Especially with incoming
             # format being Golang's
             try:
                 rx = re.compile(filt.encode('utf8'))
             except re.error as exc:
-                context.abort(
-                    StatusCode.INVALID_ARGUMENT,
-                    f"SearchFilesByName: filter did not compile: {exc})"
-                )
+                context.abort(StatusCode.INVALID_ARGUMENT,
+                              f"filter did not compile: {exc})")
 
         start = request.offset
         end = None if request.limit == 0 else start + request.limit
@@ -373,9 +379,10 @@
         for paths in chunked(
                 itertools.islice(
                     miner.file_names_by_regexp(rx, subdir),
-                    start, end)
+                    start, end),
+                force_if_empty=True,
         ):
             yield SearchFilesByNameResponse(files=paths)
 
     def SearchFilesByContent(self, request: SearchFilesByContentRequest,
                              context) -> SearchFilesByContentResponse:
@@ -377,7 +384,29 @@
         ):
             yield SearchFilesByNameResponse(files=paths)
 
     def SearchFilesByContent(self, request: SearchFilesByContentRequest,
                              context) -> SearchFilesByContentResponse:
-        not_implemented(context, issue=80)  # pragma no cover
+        """Almost straight results from `git grep`
+
+        this part of the protocol is totally undocumented, but here is what
+        Gitaly does:
+
+        - each match is sent with two lines of context before and after
+        - in case of overlapping matches (including context), they are sent
+          as one
+        - sending a match means cutting it in chunkd of `WRITE_BUFFER_SIZE`.
+        - after the full sending of each match aa  with the end_of_match`
+          boolean set to true and no data is sent.
+
+        (see also Comparison Tests for validatation of this).
+        """
+        repo = self.load_repo(request.repository, context)
+        query = request.query
+        if not query:
+            context.abort(StatusCode.INVALID_ARGUMENT, "no query given")
+
+        ref = request.ref
+        changeset = gitlab_revision_changeset(repo, ref)
+        if changeset is None:
+            return
 
@@ -383,6 +412,42 @@
 
-    def RestoreCustomHooks(self, request: RestoreCustomHooksRequest,
-                           context) -> RestoreCustomHooksResponse:
+        # TODO filtermaxlen?
+        rx = re.compile(query.encode('utf-8'), flags=re.IGNORECASE)
+        # TODO chunked_response (not used by Rails app)
+        match_data = BytesIO()
+        miner = manifest.miner(changeset)
+        for path, content, _flags in miner.iter_files_with_content(
+                exclude_binary=True):
+            # TODO rx options. Here's Gitaly reference (Golang), arguments
+            # passed to `git grep`):
+            # (with const surroundContext = 2)
+            #
+            #   git.Flag{Name: "--ignore-case"},
+            #   git.Flag{Name: "-I"},
+            #      (ignore binary files)
+            #   git.Flag{Name: "--line-number"},
+            #   git.Flag{Name: "--null"},
+            #      (use null as delimiter between path, line number and
+            #       matching line in the output, hence avoiding the need
+            #       to escape)
+            #   git.ValueFlag{Name: "--before-context",
+            #                 Value: surroundContext},
+            #   git.ValueFlag{Name: "--after-context", Value: surroundContext},
+            #   git.Flag{Name: "--perl-regexp"},
+            #   git.Flag{Name: "-e"},
+            # Gitaly does not fill in `matches` field any more
+            # (surely deprecated) and the Rails client does not read it
+            # TODO cheaper iteration on a window of lines (note that
+            # splitlines() takes care of `\r\n` and even `\r` EOLs.)
+            for matching_lines in grep_file(rx, content):
+                render_git_grep_matches(
+                    match_data,
+                    ref, path,
+                    matching_lines)
+                yield from iter_sfbc_resps(match_data)
+                match_data.truncate(0)
+                match_data.seek(0)
+
+    def set_custom_hooks(self, request, context):
         def load_repo(req, context):
             return self.load_repo(req.repository, context)
 
@@ -386,4 +451,18 @@
         def load_repo(req, context):
             return self.load_repo(req.repository, context)
 
+        with streaming_request_tempfile_extract(
+                request, context,
+                first_request_handler=load_repo
+        ) as (repo, tmpf):
+            tmpf.flush()
+            try:
+                restore_additional(repo.ui, repo,
+                                   tmpf.name.encode('ascii'))
+            except Exception as exc:
+                context.abort(StatusCode.INTERNAL,
+                              "Error in tarball application: %r" % exc)
+
+    def RestoreCustomHooks(self, request: RestoreCustomHooksRequest,
+                           context) -> RestoreCustomHooksResponse:
         try:
@@ -389,15 +468,5 @@
         try:
-            with streaming_request_tempfile_extract(
-                    request, context,
-                    first_request_handler=load_repo
-            ) as (repo, tmpf):
-                tmpf.flush()
-                try:
-                    restore_additional(repo.ui, repo,
-                                       tmpf.name.encode('ascii'))
-                except Exception as exc:
-                    context.abort(StatusCode.INTERNAL,
-                                  "Error in tarball application: %r" % exc)
+            self.set_custom_hooks(request, context)
         finally:
             return RestoreCustomHooksResponse()
 
@@ -401,8 +470,14 @@
         finally:
             return RestoreCustomHooksResponse()
 
-    def BackupCustomHooks(self, request: BackupCustomHooksRequest,
-                          context) -> BackupCustomHooksResponse:
+    def SetCustomHooks(self, request: SetCustomHooksRequest,
+                       context) -> SetCustomHooksResponse:
+        try:
+            self.set_custom_hooks(request, context)
+        finally:
+            return SetCustomHooksResponse()
+
+    def get_custom_hooks(self, request, context, resp_cls):
         repo = self.load_repo(request.repository, context)
         with tempfile.NamedTemporaryFile(
                 mode='wb+',
@@ -417,7 +492,17 @@
                 data = tmpf.read(WRITE_BUFFER_SIZE)
                 if not data:
                     break
-                yield BackupCustomHooksResponse(data=data)
+                yield resp_cls(data=data)
+
+    def BackupCustomHooks(self, request: BackupCustomHooksRequest,
+                          context) -> BackupCustomHooksResponse:
+        yield from self.get_custom_hooks(
+            request, context, BackupCustomHooksResponse)
+
+    def GetCustomHooks(self, request: GetCustomHooksRequest,
+                       context) -> GetCustomHooksResponse:
+        yield from self.get_custom_hooks(
+            request, context, GetCustomHooksResponse)
 
     def RemoveRepository(self, request: RemoveRepositoryRequest,
                          context) -> RemoveRepositoryResponse:
@@ -605,6 +690,38 @@
                 gc.collect()
         return CreateRepositoryFromBundleResponse()
 
+    def RemoveAll(self, request: RemoveAllRequest,
+                  context) -> RemoveAllResponse:
+        """Remove everything in the given storage.
+
+        This is exactly what Gitaly v15.9 does, hence including all temporary
+        files etc.
+        """
+        storage = request.storage_name
+        root_dir = self.storages.get(storage)
+        if root_dir is None:
+            context.abort(
+                StatusCode.INVALID_ARGUMENT,
+                'remove all: GetStorageByName: '
+                'no such storage: "%s"' % storage
+            )
+
+        root_dir = Path(os.fsdecode(root_dir)).resolve()
+        # it would obviously be simpler to use shutil.rmtree() on root_dir
+        # and recreate the storage afterwards *but*
+        # - permission not guaranteed on the root dir
+        # - risky on error recovery
+        try:
+            for sub in root_dir.iterdir():
+                if sub.is_dir() and not sub.is_symlink():
+                    shutil.rmtree(sub)
+                else:
+                    # also works with broken symlinks (even loops)
+                    sub.unlink()
+        except Exception as exc:
+            context.abort(StatusCode.INTERNAL, "remove all: %s" % exc)
+        return RemoveAllResponse()
+
     def hg_init_repository(self, repository: Repository, context):
         """Initialize a mercurial repository from a request object.
 
@@ -637,3 +754,87 @@
             context.set_details("hg_init_repository(%r): %r" % (repo_path,
                                                                 exc))
             raise RepositoryCreationError(repository)
+
+
+def render_git_grep_matches(buf, ref, path, enum_lines):
+    """Render a slice of lines as git grep does.
+
+    :param enum_lines: iterable of pairs `(line_no, line)`
+    """
+    for lineno, line in enum_lines:
+        buf.write(b'%s:%s\x00%d\x00%s\n' % (ref, path, lineno, line))
+
+
+SPLITLINES_RX = re.compile(br'(.*?)(\r\n?|\n|\Z)', re.MULTILINE)
+
+
+def grep_file(rx, data, context_width=2):
+    """Iterator yielding matches the given regular expression with context
+
+    This implementation avoids duplicating the data in memory.
+
+    :param int context_width: number of lines before and after to include
+      if possible (same as `grep -C`)
+    :returns: pairs `(line_no, line)`
+    """
+    prev_match_line_no = None
+    current_window = deque()  # current line and up to 2 lines before
+    match_lines = []
+    # According the the best ranked answer for
+    # https://stackoverflow.com/questions/3054604,
+    # this regexp-based splitting for lines can be a bit slower than
+    # `iter(splitlines)` (things may have changes since then, though).
+    # Yet, our biggest concern is to avoid exhausting HGitaly's RAM budget
+    # if this happens to run on large files.
+    # Unfortunately, we already have to load the entire data in RAM because
+    # it is typically Mercurial file content, we don't want to do it once
+    # more in the form of lines. In both cases a bytes string
+    # is allocated for each line, but that is harder to prevent and they
+    # should be deallocated at each iteration (no cycles).
+    # Unless there is a bug in the current approach, it is not worth the
+    # effort to try and further improve memory efficiency: implementing
+    # in RHGitaly would be the way to go.
+    for line_idx, line_m in enumerate(SPLITLINES_RX.finditer(data)):
+        line = line_m.group(1)
+        if not line:
+            continue
+        line_no = line_idx + 1
+        current_window.append((line_no, line))
+        if rx.search(line):
+            if (
+                    prev_match_line_no is None
+                    or line_no - prev_match_line_no > 2 * context_width
+            ):
+                match_lines = list(current_window)
+            else:
+                match_lines.append((line_no, line))
+            prev_match_line_no = line_no
+        elif (
+                prev_match_line_no is not None
+                and line_no <= prev_match_line_no + context_width
+        ):
+            match_lines.append((line_no, line))
+        elif match_lines:
+            yield match_lines
+            match_lines = []
+
+        if len(current_window) > context_width:
+            current_window.popleft()
+    if match_lines:
+        yield match_lines
+
+
+def iter_sfbc_resps(match_data: BytesIO):
+    """Yield SearchFilesByContentResponse messages for the given match_data.
+    """
+    value = match_data.getvalue()
+    match_len = len(value)
+    sent = 0  # actually rounded to the upper WRITE_BUFFER_SIZE
+    while sent < match_len:
+        yield SearchFilesByContentResponse(
+            match_data=value[sent:sent + WRITE_BUFFER_SIZE])
+        sent += WRITE_BUFFER_SIZE
+
+    match_data.truncate(0)
+    match_data.seek(0)
+    yield SearchFilesByContentResponse(end_of_match=True)
diff --git a/hgitaly/service/tests/test_commit.py b/hgitaly/service/tests/test_commit.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfY29tbWl0LnB5..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfY29tbWl0LnB5 100644
--- a/hgitaly/service/tests/test_commit.py
+++ b/hgitaly/service/tests/test_commit.py
@@ -14,7 +14,16 @@
     as_bytes,
 )
 from google.protobuf.timestamp_pb2 import Timestamp
-from hgext3rd.heptapod.branch import set_default_gitlab_branch
+from hgext3rd.heptapod.branch import (
+    set_default_gitlab_branch,
+    write_gitlab_branches,
+)
+from hgext3rd.heptapod.branch import (
+    write_gitlab_tags,
+)
+from hgext3rd.heptapod.special_ref import (
+    write_special_refs,
+)
 
 from hgitaly.oid import (
     tree_oid,
@@ -29,6 +38,8 @@
 from hgitaly.stub.commit_pb2 import (
     CommitIsAncestorRequest,
     CommitsByMessageRequest,
+    CommitLanguagesRequest,
+    CommitLanguagesResponse,
     CommitStatsRequest,
     CountCommitsRequest,
     CountDivergingCommitsRequest,
@@ -132,6 +143,10 @@
         resp = self.stub.ListCommits(request)
         return [c.id.encode('ascii') for chunk in resp for c in chunk.commits]
 
+    def commit_languages(self, **kw):
+        request = CommitLanguagesRequest(repository=self.grpc_repo, **kw)
+        return list(self.stub.CommitLanguages(request).languages)
+
 
 @pytest.fixture
 def commit_fixture_empty_repo(grpc_channel, server_repos_root):
@@ -315,6 +330,8 @@
                                 utc_timestamp=timestamp+20)
     sha0, sha1, sha2, sha3, sha4 = (ctx0.hex(), ctx1.hex(), ctx2.hex(),
                                     ctx3.hex(), ctx4.hex())
+    # symbolic revision
+    assert find_commits_ids(revision=b'branch/stable') == [sha3, sha0]
     # with follow; test that it follows renames
     assert find_commits_ids(paths=[b'zoo'], follow=True) == [sha2, sha0]
     # with follow and Git Range Notation, it does not **currently** follow
@@ -377,6 +394,35 @@
     sha5 = ctx5.hex()
     assert find_commits_ids(all=True, order=FindCommitsRequest.Order.TOPO) == [
         sha0, sha2, sha3, sha4, sha1, sha5]
+    # with include_referenced_by
+    assert fixture.find_commits(  # without state files empty instead of crash
+        revision=sha3,
+        include_referenced_by=[b'refs']
+    )[0].referenced_by == []
+    write_gitlab_branches(wrapper.repo, {b'branch/stable': sha3})
+    assert fixture.find_commits(
+        revision=sha3,
+        include_referenced_by=[b'refs']
+    )[0].referenced_by == [b'refs/heads/branch/stable']
+    assert fixture.find_commits(
+        revision=sha3,
+        include_referenced_by=[b'refs/tags']
+    )[0].referenced_by == []
+    write_gitlab_tags(wrapper.repo, {b'release': sha3})
+    assert fixture.find_commits(
+        revision=sha3,
+        include_referenced_by=[b'refs/tags']
+    )[0].referenced_by == [b'refs/tags/release']
+    write_special_refs(wrapper.repo, {b'pipelines/12': sha3})
+    assert fixture.find_commits(
+        revision=sha3,
+        include_referenced_by=[b'refs/pipelines/']
+    )[0].referenced_by == [b'refs/pipelines/12']
+    set_default_gitlab_branch(wrapper.repo, b'branch/stable')
+    assert fixture.find_commits(
+        revision=sha3,
+        include_referenced_by=[b'HEAD', b'refs/heads']
+    )[0].referenced_by == [b'refs/heads/branch/stable', b'HEAD']
 
     # tests with obsolete changesets
     wrapper.amend_file('foo')
@@ -636,6 +682,10 @@
         return pycompat.sysbytes(
             grpc_stub.LastCommitForPath(request).commit.id)
 
+    # case with path resolving to the repo root
+    assert do_rpc(revision=b'branch/other', path=b'/') == ctx2.hex()
+    assert do_rpc(revision=b'branch/other', path=b'') == ctx2.hex()
+
     assert do_rpc(revision=b'branch/default', path=b'foo') == ctx1.hex()
     assert do_rpc(revision=b'branch/other', path=b'foo') == ctx.hex()
     assert do_rpc(revision=ctx2.hex(), path=b'foo') == ctx.hex()
@@ -644,9 +694,11 @@
     # recursive directory matching (see Rails tests for validation
     # that we must match on directories)
     assert do_rpc(revision=b'branch/other', path=b'sub') == ctx2.hex()
+    # with trailing slash already provided
+    assert do_rpc(revision=b'branch/other', path=b'sub/') == ctx2.hex()
 
     # with obsolescence
     wrapper.command('amend', message=b'amended')
     assert ctx2.obsolete()
     assert do_rpc(revision=ctx2.hex(), path=b'foo') == ctx.hex()
 
@@ -647,9 +699,19 @@
 
     # with obsolescence
     wrapper.command('amend', message=b'amended')
     assert ctx2.obsolete()
     assert do_rpc(revision=ctx2.hex(), path=b'foo') == ctx.hex()
 
+    # with a removal
+    (wrapper.path / 'sub/bar').unlink()
+    ctx_rm = wrapper.commit(rel_paths=['sub/bar'], message="removed",
+                            add_remove=True)
+    wrapper.commit_file('foo2')
+    assert do_rpc(revision=b'branch/other', path=b'sub') == ctx_rm.hex()
+
+    # empty response for unresolvable revision
+    assert do_rpc(revision=b'unknown', path=b'foo') == b''
+
 
 def test_list_commits_by_oid(grpc_channel, server_repos_root):
     grpc_stub = CommitServiceStub(grpc_channel)
@@ -952,7 +1014,7 @@
     invalid_cursor = 'this is not an OID'
     with pytest.raises(grpc.RpcError) as exc_info:
         get_tree_entries(b'.', revision=sha1, page_token=invalid_cursor)
-    assert exc_info.value.code() == grpc.StatusCode.UNKNOWN
+    assert exc_info.value.code() == grpc.StatusCode.INTERNAL
     assert invalid_cursor in exc_info.value.details()
 
     # with skip_flat_paths, even non recursive calls give empty flat paths
@@ -1078,6 +1140,27 @@
         (b'foo', sha3),
     ]
 
+    # case when we don't have to walk the entire changelog
+    (wrapper.path / 'more').mkdir()
+    sha5 = wrapper.commit_file('more/foo').hex().decode()
+    assert do_rpc(sha5, b'more/') == [
+        (b'more/foo', sha5),
+    ]
+
+    # case of limit smaller than number of subtrees, now that sha5 created
+    # a second one
+    assert do_rpc(sha5, b'', limit=1) == [
+        (b'more', sha5),
+    ]
+    # some more to double check offset by one potential errors
+    assert do_rpc(sha5, b'', offset=1, limit=1) == [
+        (b'sub', sha1),
+    ]
+    assert do_rpc(sha5, b'', offset=1, limit=2) == [
+        (b'sub', sha1),
+        (b'foo', sha3),
+    ]
+
 
 def test_raw_blame(grpc_channel, server_repos_root):
     grpc_stub = CommitServiceStub(grpc_channel)
@@ -1138,7 +1221,7 @@
     with pytest.raises(grpc.RpcError) as exc_info:
         do_rpc(sha0, b'')
     assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert 'RawBlame: empty Path' in exc_info.value.details()
+    assert 'empty Path' in exc_info.value.details()
 
     # with obsolescence
     wrapper.command('amend', message=b'amended')
@@ -1255,3 +1338,47 @@
     wrapper.command('amend', message=b'amended')
     assert ctx4.obsolete()
     assert list_commits(sha4, limit=2) == [sha4, sha3]
+
+
+def test_commit_languages(commit_fixture_empty_repo):
+    fixture = commit_fixture_empty_repo
+    wrapper = fixture.repo_wrapper
+
+    # a Python file with valid content, including a blank line
+    wrapper.commit_file('foo.py',
+                        content="import sys\n\nprint(sys.version)\n",
+                        message="Some Python",
+                        )
+    # a Ruby file with valid content, with a comment
+    wrapper.commit_file('bar.rb',
+                        content=("# frozen_string_literal: true\n"
+                                 "\n"
+                                 "module Bar\n"
+                                 "def f(x)\n"
+                                 "  x+1\n"
+                                 ),
+                        message="Some Ruby",
+                        )
+
+    Language = CommitLanguagesResponse.Language
+    assert fixture.commit_languages() == [
+        Language(name='Ruby',
+                 bytes=57,
+                 share=64.7727279663086,
+                 color="#701516"),
+        Language(name='Python',
+                 bytes=31,
+                 share=35.227272033691406,
+                 color="#3572A5"),
+    ]
+
+    # color defaulting: COBOL does not have a color in github-linguist's
+    # `languages.json`
+    wrapper.commit_file(
+        'truc.cob',
+        content="Not really pretending to be COBOL, "
+        "extension should be enough")
+    assert fixture.commit_languages()[0] == Language(name='COBOL',
+                                                     bytes=61,
+                                                     share=40.939598083496094,
+                                                     color="#d29900")
diff --git a/hgitaly/service/tests/test_default_branch.py b/hgitaly/service/tests/test_default_branch.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfZGVmYXVsdF9icmFuY2gucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfZGVmYXVsdF9icmFuY2gucHk= 100644
--- a/hgitaly/service/tests/test_default_branch.py
+++ b/hgitaly/service/tests/test_default_branch.py
@@ -10,7 +10,10 @@
 
 from hgitaly.tests.common import make_empty_repo
 
-from hgitaly.stub.ref_pb2 import FindDefaultBranchNameRequest
+from hgitaly.stub.ref_pb2 import (
+    FindDefaultBranchNameRequest,
+    FindDefaultBranchNameResponse
+)
 from hgitaly.stub.ref_pb2_grpc import RefServiceStub
 from hgitaly.stub.repository_pb2 import WriteRefRequest
 from hgitaly.stub.repository_pb2_grpc import RepositoryServiceStub
@@ -26,7 +29,7 @@
 
     request = FindDefaultBranchNameRequest(repository=grpc_repo)
     response = ref_stub.FindDefaultBranchName(request)
-    assert response.name == b'refs/heads/'
+    assert response == FindDefaultBranchNameResponse()
 
     wreq = WriteRefRequest(repository=grpc_repo,
                            ref=b'HEAD',
diff --git a/hgitaly/service/tests/test_diff.py b/hgitaly/service/tests/test_diff.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfZGlmZi5weQ==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfZGlmZi5weQ== 100644
--- a/hgitaly/service/tests/test_diff.py
+++ b/hgitaly/service/tests/test_diff.py
@@ -222,8 +222,8 @@
     # varient 1
     with pytest.raises(grpc.RpcError) as exc_info:
         raw_diff(sha0, sha_not_exists)
-    assert exc_info.value.code() == grpc.StatusCode.UNKNOWN
+    assert exc_info.value.code() == grpc.StatusCode.INTERNAL
     assert 'exit status 128' in exc_info.value.details()
     # varient 2
     with pytest.raises(grpc.RpcError) as exc_info:
         raw_diff(sha_not_exists, sha0)
@@ -226,8 +226,8 @@
     assert 'exit status 128' in exc_info.value.details()
     # varient 2
     with pytest.raises(grpc.RpcError) as exc_info:
         raw_diff(sha_not_exists, sha0)
-    assert exc_info.value.code() == grpc.StatusCode.UNKNOWN
+    assert exc_info.value.code() == grpc.StatusCode.INTERNAL
     assert 'exit status 128' in exc_info.value.details()
 
 
@@ -339,8 +339,8 @@
     # varient 1
     with pytest.raises(grpc.RpcError) as exc_info:
         raw_patch(sha0, sha_not_exists)
-    assert exc_info.value.code() == grpc.StatusCode.UNKNOWN
+    assert exc_info.value.code() == grpc.StatusCode.INTERNAL
     assert 'exit status 128' in exc_info.value.details()
     # varient 2
     with pytest.raises(grpc.RpcError) as exc_info:
         raw_patch(sha_not_exists, sha0)
@@ -343,8 +343,8 @@
     assert 'exit status 128' in exc_info.value.details()
     # varient 2
     with pytest.raises(grpc.RpcError) as exc_info:
         raw_patch(sha_not_exists, sha0)
-    assert exc_info.value.code() == grpc.StatusCode.UNKNOWN
+    assert exc_info.value.code() == grpc.StatusCode.INTERNAL
     assert 'exit status 128' in exc_info.value.details()
 
 
diff --git a/hgitaly/service/tests/test_mercurial_operations.py b/hgitaly/service/tests/test_mercurial_operations.py
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfbWVyY3VyaWFsX29wZXJhdGlvbnMucHk=
--- /dev/null
+++ b/hgitaly/service/tests/test_mercurial_operations.py
@@ -0,0 +1,169 @@
+# Copyright 2022 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
+from grpc import (
+    RpcError,
+    StatusCode,
+)
+import pytest
+
+from hgitaly.stub.mercurial_operations_pb2 import (
+    MergeAnalysisRequest,
+    MergeAnalysisResponse,
+)
+from hgitaly.stub.mercurial_operations_pb2_grpc import (
+    MercurialOperationsServiceStub,
+)
+
+from .fixture import ServiceFixture
+
+
+class OperationsFixture(ServiceFixture):
+
+    stub_cls = MercurialOperationsServiceStub
+
+    def merge_analysis(self, **kw):
+        return self.stub.MergeAnalysis(MergeAnalysisRequest(
+            repository=self.grpc_repo, **kw))
+
+
+@pytest.fixture
+def operations_fixture(grpc_channel, server_repos_root):
+    with OperationsFixture(grpc_channel, server_repos_root) as fixture:
+        yield fixture
+
+
+def test_merge_analysis_ff(operations_fixture):
+    repo_wrapper = operations_fixture.repo_wrapper
+    commit_file = repo_wrapper.commit_file
+    default_ctx = commit_file('foo')
+    default_sha = default_ctx.hex().decode()
+    topic_ctx = commit_file('foo', parent=default_ctx, topic='zetop')
+    topic_first_hex = topic_ctx.hex()
+    topic_first_sha = topic_first_hex.decode()
+
+    merge_analysis = operations_fixture.merge_analysis
+    assert (
+        merge_analysis(source_revision=b'topic/default/zetop',
+                       target_revision=b'branch/default')
+        ==
+        MergeAnalysisResponse(is_fast_forward=True,
+                              source_node_id=topic_first_sha,
+                              source_branch=b'default',
+                              source_topic=b'zetop',
+                              target_node_id=default_sha,
+                              target_branch=b'default',
+                              target_topic=b'',
+                              target_is_public=False,
+                              )
+    )
+    newtop_sha = commit_file('foo',
+                             parent=topic_ctx,
+                             topic='newtop').hex().decode()
+    repo_wrapper.update(topic_ctx)
+    repo_wrapper.amend_file('foo').hex()
+
+    assert (
+        merge_analysis(source_revision=b'topic/default/newtop',
+                       target_revision=b'branch/default')
+        ==
+        MergeAnalysisResponse(is_fast_forward=True,
+                              has_obsolete_changesets=True,
+                              has_unstable_changesets=True,
+                              source_node_id=newtop_sha,
+                              source_branch=b'default',
+                              source_topic=b'newtop',
+                              target_node_id=default_sha,
+                              target_branch=b'default',
+                              target_topic=b'',
+                              target_is_public=False,
+                              )
+    )
+    assert (
+        merge_analysis(source_revision=topic_first_hex,
+                       target_revision=b'branch/default')
+        ==
+        MergeAnalysisResponse(is_fast_forward=True,
+                              has_obsolete_changesets=True,
+                              has_unstable_changesets=False,
+                              source_node_id=topic_first_sha,
+                              source_branch=b'default',
+                              source_topic=b'zetop',
+                              target_node_id=default_sha,
+                              target_branch=b'default',
+                              target_topic=b'',
+                              target_is_public=False,
+                              )
+    )
+
+    # error cases
+    with pytest.raises(RpcError) as exc_info:
+        merge_analysis(source_revision=b'unknown',
+                       target_revision=b'branch/default')
+    assert exc_info.value.code() == StatusCode.INVALID_ARGUMENT
+    assert 'source revision' in exc_info.value.details().lower()
+
+    with pytest.raises(RpcError) as exc_info:
+        merge_analysis(source_revision=b'topic/default/zetop',
+                       target_revision=b'unknownn')
+    assert exc_info.value.code() == StatusCode.INVALID_ARGUMENT
+    assert 'target revision' in exc_info.value.details().lower()
+
+
+def test_merge_analysis_conflict(operations_fixture):
+    repo_wrapper = operations_fixture.repo_wrapper
+    commit_file = repo_wrapper.commit_file
+    ctx0 = commit_file('foo')
+    default_sha = commit_file('foo', content="default").hex().decode()
+    repo_wrapper.set_phase('public', ['.'])  # also testing `target_is_public`
+    topic_first_sha = commit_file('foo', parent=ctx0,
+                                  topic='zetop', content="top"
+                                  ).hex().decode()
+
+    merge_analysis = operations_fixture.merge_analysis
+    assert (
+        merge_analysis(source_revision=b'topic/default/zetop',
+                       target_revision=b'branch/default')
+        ==
+        MergeAnalysisResponse(is_fast_forward=False,
+                              has_conflicts=True,
+                              source_node_id=topic_first_sha,
+                              source_branch=b'default',
+                              source_topic=b'zetop',
+                              target_node_id=default_sha,
+                              target_branch=b'default',
+                              target_topic=b'',
+                              target_is_public=True,
+                              )
+    )
+
+    # same without the conflicts check
+    conflicts_check_skipped = merge_analysis(
+        skip_conflicts_check=True,
+        source_revision=b'topic/default/zetop',
+        target_revision=b'branch/default',
+    )
+    assert conflicts_check_skipped.is_fast_forward is False
+    assert conflicts_check_skipped.has_conflicts is False  # was really skipped
+
+    # solving the conflict
+    topic_fixed_sha = commit_file('foo', topic='zetop',
+                                  content="default").hex().decode()
+    assert (
+        merge_analysis(source_revision=b'topic/default/zetop',
+                       target_revision=b'branch/default')
+        ==
+        MergeAnalysisResponse(is_fast_forward=False,
+                              has_conflicts=False,
+                              source_node_id=topic_fixed_sha,
+                              source_branch=b'default',
+                              source_topic=b'zetop',
+                              target_node_id=default_sha,
+                              target_branch=b'default',
+                              target_topic=b'',
+                              target_is_public=True,
+                              )
+    )
diff --git a/hgitaly/service/tests/test_ref.py b/hgitaly/service/tests/test_ref.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfcmVmLnB5..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfcmVmLnB5 100644
--- a/hgitaly/service/tests/test_ref.py
+++ b/hgitaly/service/tests/test_ref.py
@@ -94,7 +94,9 @@
     def list_refs(self, patterns=(b"refs/", ), **kw):
         return [(ref.name, ref.target)
                 for resp in self.stub.ListRefs(
-                        ListRefsRequest(repository=self.grpc_repo, **kw))
+                        ListRefsRequest(repository=self.grpc_repo,
+                                        patterns=patterns,
+                                        **kw))
                 for ref in resp.references]
 
 
@@ -201,16 +203,6 @@
 def test_find_local_branches(ref_fixture):
     ctx = ref_fixture.commit_file('foo', message="Ze subject")
 
-    resp = ref_fixture.find_local_branches(
-                pagination_params=PaginationParameter(limit=-1))
-    branches = [br for chunk in resp for br in chunk.branches]
-    assert len(branches) == 1
-    assert branches[0].name == b'refs/heads/branch/default'
-    target = branches[0].commit
-    assert target.id == ctx.hex().decode()
-    assert branches[0].commit_subject == b"Ze subject"
-
-    # now with the new style
     ref_fixture.feature_flags = [
         ('simplify-find-local-branches-response', True),
     ]
@@ -219,7 +211,9 @@
     branches = [br for chunk in resp for br in chunk.local_branches]
     assert len(branches) == 1
     assert branches[0].name == b'refs/heads/branch/default'
-    assert branches[0].target_commit == target
+    target = branches[0].target_commit
+    assert target.id == ctx.hex().decode()
+    assert target.subject == b"Ze subject"
 
 
 def test_find_local_branches_pagination(ref_fixture):
@@ -233,8 +227,8 @@
     assert len(resp) == 0
 
     # but no pagination_params does mean unlimited
-    assert sum(len(resp.branches)
+    assert sum(len(resp.local_branches)
                for resp in ref_fixture.find_local_branches()) == 22
 
     resp = list(ref_fixture.find_local_branches(
         pagination_params=PaginationParameter(limit=21)))
@@ -237,6 +231,6 @@
                for resp in ref_fixture.find_local_branches()) == 22
 
     resp = list(ref_fixture.find_local_branches(
         pagination_params=PaginationParameter(limit=21)))
-    assert len(resp[0].branches) == 20
+    assert len(resp[0].local_branches) == 20
     # cursor aka page token is (not yet) returned
@@ -242,5 +236,6 @@
     # cursor aka page token is (not yet) returned
-    assert [br.name for br in resp[1].branches] == [b'refs/heads/branch/br20']
+    assert [br.name for br in resp[1].local_branches] == [
+        b'refs/heads/branch/br20']
 
     # but we can infer it
     resp = list(ref_fixture.find_local_branches(
@@ -248,7 +243,8 @@
             limit=6,
             page_token='refs/heads/branch/br20')))
     assert len(resp) == 1
-    assert [br.name for br in resp[0].branches] == [b'refs/heads/branch/br21']
+    assert [br.name for br in resp[0].local_branches] == [
+        b'refs/heads/branch/br21']
 
 
 def test_find_local_branches_sort_by(ref_fixture):
@@ -272,7 +268,7 @@
                         sort_by=by,
                         pagination_params=pagination_params
                 )
-                for br in resp.branches
+                for br in resp.local_branches
                 ] == [b'refs/heads/branch/' + ex for ex in expected]
 
     assert_branches_sorted_by(SortBy.NAME, [b'aaa', b'bbb', b'ccc'])
@@ -563,3 +559,57 @@
         (b'refs/keep-around/' + ctx1.hex(), sha1),
         (b'refs/tags/v1.4', sha1),
     ]
+
+    # pattern used by gitaly-backup
+    assert fixture.list_refs(patterns=[b'refs/']) == [
+        PSEUDO_REF_ALL,
+        (special_ref, sha2),
+        (b'refs/heads/branch/aaa', tagging_sha),
+        (b'refs/heads/branch/default', sha1),
+        (b'refs/keep-around/' + ctx1.hex(), sha1),
+        (b'refs/tags/v1.4', sha1),
+    ]
+
+    # pattern used by Rails app for queries about branches and tags
+    assert fixture.list_refs(patterns=[b'refs/heads/']) == [
+        (b'refs/heads/branch/aaa', tagging_sha),
+        (b'refs/heads/branch/default', sha1),
+    ]
+    assert fixture.list_refs(patterns=[b'refs/tags/']) == [
+        (b'refs/tags/v1.4', sha1),
+    ]
+
+    # more general patterns
+    assert fixture.list_refs(patterns=[b'refs/heads/branch/a*']) == [
+        (b'refs/heads/branch/aaa', tagging_sha),
+    ]
+    assert fixture.list_refs(patterns=[b'refs/t*/*']) == [
+        (b'refs/tags/v1.4', sha1),
+    ]
+    assert fixture.list_refs(patterns=[b'refs/e*/*']) == [
+        (special_ref, sha2),
+    ]
+    assert fixture.list_refs(patterns=[b'refs/heads/*/a*']) == [
+        (b'refs/heads/branch/aaa', tagging_sha),
+    ]
+    # number of segments has to match
+    assert fixture.list_refs(patterns=[b'*/a*']) == []
+    assert fixture.list_refs(patterns=[b'refs/heads/br*']) == []
+    assert fixture.list_refs(patterns=[b'refs/heads/br*/*']) == [
+        (b'refs/heads/branch/aaa', tagging_sha),
+        (b'refs/heads/branch/default', sha1),
+    ]
+    assert fixture.list_refs(
+        patterns=[b'refs/keep-around/%s*' % ctx1.hex()[0:2]]
+    ) == [
+        (b'refs/keep-around/' + ctx1.hex(), sha1),
+    ]
+
+    # pointing_at_oids
+    assert fixture.list_refs(pointing_at_oids=[ctx2.hex()]) == [
+        (b'refs/environments/17', sha2)
+    ]
+    assert fixture.list_refs(head=True, pointing_at_oids=[ctx2.hex()]) == [
+        (b'HEAD', sha1),
+        (b'refs/environments/17', sha2)
+    ]
diff --git a/hgitaly/service/tests/test_repository_service.py b/hgitaly/service/tests/test_repository_service.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfcmVwb3NpdG9yeV9zZXJ2aWNlLnB5..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfcmVwb3NpdG9yeV9zZXJ2aWNlLnB5 100644
--- a/hgitaly/service/tests/test_repository_service.py
+++ b/hgitaly/service/tests/test_repository_service.py
@@ -29,6 +29,7 @@
 from hgitaly.repository import (
     GITLAB_PROJECT_FULL_PATH_FILENAME,
 )
+from hgitaly.stream import WRITE_BUFFER_SIZE
 from hgitaly.testing import TEST_DATA_DIR
 from hgitaly.testing.bundle import list_bundle_contents
 
@@ -45,4 +46,5 @@
     FindMergeBaseRequest,
     FullPathRequest,
     GetArchiveRequest,
+    GetCustomHooksRequest,
     HasLocalBranchesRequest,
@@ -48,5 +50,6 @@
     HasLocalBranchesRequest,
+    RemoveAllRequest,
     RemoveRepositoryRequest,
     RepositoryExistsRequest,
     RestoreCustomHooksRequest,
     SearchFilesByNameRequest,
@@ -49,5 +52,7 @@
     RemoveRepositoryRequest,
     RepositoryExistsRequest,
     RestoreCustomHooksRequest,
     SearchFilesByNameRequest,
+    SearchFilesByContentRequest,
+    SearchFilesByContentResponse,
     SetFullPathRequest,
@@ -53,4 +58,5 @@
     SetFullPathRequest,
+    SetCustomHooksRequest,
     WriteRefRequest,
 )
 from hgitaly.stub.repository_pb2_grpc import RepositoryServiceStub
@@ -61,6 +67,10 @@
 )
 from .fixture import ServiceFixture
 
+parametrize = pytest.mark.parametrize
+
+END_OF_MATCH = SearchFilesByContentResponse(end_of_match=True)
+
 
 @attr.s
 class RepositoryFixture(ServiceFixture):
@@ -125,6 +135,28 @@
                         ))
                 for path in resp.files]
 
+    def search_files_by_content(self, ref=b'branch/default',
+                                aggregate_splitlines=True,
+                                **kwargs):
+        stream = self.stub.SearchFilesByContent(SearchFilesByContentRequest(
+            repository=self.grpc_repo,
+            ref=ref,
+            **kwargs,
+        ))
+        if not aggregate_splitlines:
+            return list(stream)
+
+        res = []
+        match_data = BytesIO()
+        for resp in stream:
+            if resp.end_of_match:
+                res.append(match_data.getvalue().split(b'\n'))
+                match_data = BytesIO()  # deliberately not as in tested code
+                res.append(resp)
+            else:
+                match_data.write(resp.match_data)
+        return res
+
     def restore_custom_hooks(self, tarball_path,
                              grpc_repo=None,
                              nb_chunks=2,
@@ -137,9 +169,21 @@
             for chunk in stream.slice_binary_file(tarball_path, nb_chunks)
         )
 
+    def set_custom_hooks(self, tarball_path,
+                         grpc_repo=None,
+                         nb_chunks=2,
+                         **kw):
+        if grpc_repo is None:
+            grpc_repo = self.grpc_repo
+
+        return self.stub.SetCustomHooks(
+            SetCustomHooksRequest(repository=grpc_repo, data=chunk)
+            for chunk in stream.slice_binary_file(tarball_path, nb_chunks)
+        )
+
     def backup_custom_hooks(self, save_path):
         with open(save_path, 'wb') as fobj:
             for chunk in self.stub.BackupCustomHooks(BackupCustomHooksRequest(
                     repository=self.grpc_repo)):
                 fobj.write(chunk.data)
 
@@ -140,9 +184,15 @@
     def backup_custom_hooks(self, save_path):
         with open(save_path, 'wb') as fobj:
             for chunk in self.stub.BackupCustomHooks(BackupCustomHooksRequest(
                     repository=self.grpc_repo)):
                 fobj.write(chunk.data)
 
+    def get_custom_hooks(self, save_path):
+        with open(save_path, 'wb') as fobj:
+            for chunk in self.stub.GetCustomHooks(GetCustomHooksRequest(
+                    repository=self.grpc_repo)):
+                fobj.write(chunk.data)
+
     def remove_repository(self, grpc_repo=None):
         if grpc_repo is None:
             grpc_repo = self.grpc_repo
@@ -207,6 +257,9 @@
                     get_request_iter(ref_patterns)):
                 fobj.write(chunk.data)
 
+    def remove_all(self, storage_name):
+        return self.stub.RemoveAll(RemoveAllRequest(storage_name=storage_name))
+
     def assert_compare_repos(self, target_repo_path):
         """Compare the fixture repo with another one.
         """
@@ -452,7 +505,7 @@
     assert (
         exc_info.value.details()
         ==
-        'FindMergeBase: at least 2 revisions are required'
+        'at least 2 revisions are required'
     )
 
     sha_not_exists = b'deadnode' * 5
@@ -581,7 +634,7 @@
     assert search(ref=b'unknown', query='.') == []
 
 
-def test_restore_custom_hooks(fixture_with_repo, tmpdir):
+def test_search_files_by_content(fixture_with_repo):
     fixture = fixture_with_repo
     wrapper = fixture.repo_wrapper
 
@@ -585,6 +638,120 @@
     fixture = fixture_with_repo
     wrapper = fixture.repo_wrapper
 
+    wrapper.write_commit('afoo', message="Some foo")
+    sub = (wrapper.path / 'sub')
+    sub.mkdir()
+    (sub / 'bar').write_text('line1\nline2\nbar content\nline4\nline5')
+    (sub / 'ba2').write_text('line1\nba2 content')
+    (sub / 'ba3').write_text('ba3 content\nline2')
+    # this one has Windows and MacOS Classic line endings:
+    (sub / 'ba4').write_text('l1\nl2\r\nl3\rba4 content\nl5')
+    (sub / 'ba5').write_text('m1\nm2\nm3\nm4\nm5\nm6\nba5 content\n')
+    long_content = 'very large content\n' * 3000
+    shutil.copy2(TEST_DATA_DIR / 'backup_additional_no_git.tar',
+                 wrapper.path / 'bin')
+    (wrapper.path / 'large').write_text(long_content)
+
+    wrapper.commit(rel_paths=['bin', 'sub/bar', 'sub/ba2', 'sub/ba3',
+                              'sub/ba4', 'sub/ba5', 'large'],
+                   message="zebar", add_remove=True)
+    search = fixture.search_files_by_content
+
+    # cases not matching
+    assert not search(query='no match for this one')
+    assert not search(query='PaxHeader')  # tarfile is excluded
+    assert not search(ref=b"no/such/fref", query='foo')
+
+    # simple case: match, with two lines of content before and after
+    assert search(query="^bar.c") == [
+        [b'branch/default:sub/bar\x001\x00line1',
+         b'branch/default:sub/bar\x002\x00line2',
+         b'branch/default:sub/bar\x003\x00bar content',
+         b'branch/default:sub/bar\x004\x00line4',
+         b'branch/default:sub/bar\x005\x00line5',
+         b'',
+         ],
+        END_OF_MATCH,
+    ]
+
+    # with only one line if file before and after match
+    assert search(query="^ba2.c") == [
+        [b'branch/default:sub/ba2\x001\x00line1',
+         b'branch/default:sub/ba2\x002\x00ba2 content',
+         b'',
+         ],
+        END_OF_MATCH,
+    ]
+    assert search(query="^ba3.c") == [
+        [b'branch/default:sub/ba3\x001\x00ba3 content',
+         b'branch/default:sub/ba3\x002\x00line2',
+         b'',
+         ],
+        END_OF_MATCH,
+    ]
+
+    # more than two lines before match
+    assert search(query="^ba4.c") == [
+        [b'branch/default:sub/ba4\x002\x00l2',
+         b'branch/default:sub/ba4\x003\x00l3',
+         b'branch/default:sub/ba4\x004\x00ba4 content',
+         b'branch/default:sub/ba4\x005\x00l5',
+         b'',
+         ],
+        END_OF_MATCH,
+    ]
+
+    # two matches, with overlapping context or not
+    assert search(query="^l1|ba4.c") == [
+        [b'branch/default:sub/ba4\x001\x00l1',
+         b'branch/default:sub/ba4\x002\x00l2',
+         b'branch/default:sub/ba4\x003\x00l3',
+         b'branch/default:sub/ba4\x004\x00ba4 content',
+         b'branch/default:sub/ba4\x005\x00l5',
+         b'',
+         ],
+        END_OF_MATCH,
+    ]
+    assert search(query="^m1|ba5.c") == [
+        [b'branch/default:sub/ba5\x001\x00m1',
+         b'branch/default:sub/ba5\x002\x00m2',
+         b'branch/default:sub/ba5\x003\x00m3',
+         b'',
+         ],
+        END_OF_MATCH,
+        [
+         b'branch/default:sub/ba5\x005\x00m5',
+         b'branch/default:sub/ba5\x006\x00m6',
+         b'branch/default:sub/ba5\x007\x00ba5 content',
+         b''
+        ],
+        END_OF_MATCH,
+    ]
+    # several matching files, with case insensitivity
+    assert len(search(query='ConTent')) == 6 * 2
+
+    # long match requiring several responses
+    long_match = search(query='^very', aggregate_splitlines=False)
+    assert len(long_match) == 3
+    assert long_match[-1] == END_OF_MATCH
+    assert len(long_match[0].match_data) == WRITE_BUFFER_SIZE
+
+    # error cases
+    with pytest.raises(grpc.RpcError) as exc_info:
+        search()
+    assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT
+
+
+@parametrize('rpc_name', ['restore', 'set'])
+def test_set_custom_hooks(fixture_with_repo, tmpdir, rpc_name):
+    fixture = fixture_with_repo
+    wrapper = fixture.repo_wrapper
+
+    if rpc_name == 'restore':
+        meth = fixture.restore_custom_hooks
+    else:
+        meth = fixture.set_custom_hooks
+
     # this is a valid tar file with the minimal `hgrc` used in these tests
     # the underlying restoration function from py-heptapod is itself tested,
     # and Heptapod backup tests will further exercise this.
@@ -593,7 +760,7 @@
     hgrc_path = wrapper.path / '.hg/hgrc'
 
     os.unlink(hgrc_path)
-    fixture.restore_custom_hooks(tarball_path)
+    meth(tarball_path)
     assert hgrc_path.read_text()
 
     # TODO test unknown storage and repo
@@ -603,7 +770,7 @@
     invalid_tarball_path = tmpdir / 'invalid.tgz'
     invalid_tarball_path.write("something else")
     with pytest.raises(grpc.RpcError) as exc_info:
-        fixture.restore_custom_hooks(invalid_tarball_path)
+        meth(invalid_tarball_path)
     assert exc_info.value.code() == grpc.StatusCode.INTERNAL
 
 
@@ -607,5 +774,6 @@
     assert exc_info.value.code() == grpc.StatusCode.INTERNAL
 
 
-def test_backup_custom_hooks(fixture_with_repo, tmpdir):
+@parametrize('rpc_name', ['backup', 'get'])
+def test_backup_custom_hooks(fixture_with_repo, tmpdir, rpc_name):
     fixture = fixture_with_repo
@@ -611,3 +779,7 @@
     fixture = fixture_with_repo
+    if rpc_name == 'backup':
+        meth = fixture.backup_custom_hooks
+    else:
+        meth = fixture.get_custom_hooks
 
     tarball_path = tmpdir / 'hg_aditional.tgz'
@@ -612,6 +784,7 @@
 
     tarball_path = tmpdir / 'hg_aditional.tgz'
-    fixture.backup_custom_hooks(tarball_path)
+    meth(tarball_path)
+    assert tarball_path.exists()
 
 
 def test_remove_repository(fixture_with_repo):
@@ -972,3 +1145,25 @@
     # is not in the incremental bundle
     # TODO do better and assert phases.public
     assert unbundled_repo[sha1].phase() == phases.draft
+
+
+def test_remove_all(fixture_with_repo):
+    fixture = fixture_with_repo
+    storage_path = fixture.storage_path('default')
+    (storage_path / 'foo').write_bytes(b'something')
+    (storage_path / 'bar').symlink_to('foo')
+    (storage_path / 'broken').symlink_to('missing')
+    fixture.remove_all(storage_name='default')
+
+    assert list(storage_path.iterdir()) == []
+
+    with pytest.raises(grpc.RpcError) as exc_info:
+        fixture.remove_all(storage_name='unknown')
+    assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT
+
+    storage_path.rmdir()  # create error condition by removing the root dir
+    with pytest.raises(grpc.RpcError) as exc_info:
+        fixture.remove_all(storage_name='default')
+    exc = exc_info.value
+    assert exc.code() == grpc.StatusCode.INTERNAL
+    assert "No such file or directory: '%s'" % storage_path in exc.details()
diff --git a/hgitaly/stub/commit_pb2.py b/hgitaly/stub/commit_pb2.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zdHViL2NvbW1pdF9wYjIucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zdHViL2NvbW1pdF9wYjIucHk= 100644
--- a/hgitaly/stub/commit_pb2.py
+++ b/hgitaly/stub/commit_pb2.py
@@ -22,7 +22,7 @@
   syntax='proto3',
   serialized_options=b'Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb',
   create_key=_descriptor._internal_create_key,
-  serialized_pb=b'\n\x0c\x63ommit.proto\x12\x06gitaly\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\nlint.proto\x1a\x0cshared.proto\"\xd4\x03\n\x12ListCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\trevisions\x18\x02 \x03(\t\x12\x36\n\x11pagination_params\x18\x03 \x01(\x0b\x32\x1b.gitaly.PaginationParameter\x12/\n\x05order\x18\x04 \x01(\x0e\x32 .gitaly.ListCommitsRequest.Order\x12\x0f\n\x07reverse\x18\x0b \x01(\x08\x12\x13\n\x0bmax_parents\x18\x05 \x01(\r\x12\x14\n\x0c\x64isable_walk\x18\x06 \x01(\x08\x12\x14\n\x0c\x66irst_parent\x18\x07 \x01(\x08\x12)\n\x05\x61\x66ter\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12*\n\x06\x62\x65\x66ore\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x61uthor\x18\n \x01(\x0c\x12\x13\n\x0bignore_case\x18\x0c \x01(\x08\x12\x1f\n\x17\x63ommit_message_patterns\x18\r \x03(\x0c\"%\n\x05Order\x12\x08\n\x04NONE\x10\x00\x12\x08\n\x04TOPO\x10\x01\x12\x08\n\x04\x44\x41TE\x10\x02\"9\n\x13ListCommitsResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"}\n\x15ListAllCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x36\n\x11pagination_params\x18\x02 \x01(\x0b\x32\x1b.gitaly.PaginationParameter\"<\n\x16ListAllCommitsResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"T\n\x12\x43ommitStatsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\"H\n\x13\x43ommitStatsResponse\x12\x0b\n\x03oid\x18\x01 \x01(\t\x12\x11\n\tadditions\x18\x02 \x01(\x05\x12\x11\n\tdeletions\x18\x03 \x01(\x05\"n\n\x17\x43ommitIsAncestorRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x13\n\x0b\x61ncestor_id\x18\x02 \x01(\t\x12\x10\n\x08\x63hild_id\x18\x03 \x01(\t\")\n\x18\x43ommitIsAncestorResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"\x81\x01\n\x10TreeEntryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12\r\n\x05limit\x18\x04 \x01(\x03\x12\x10\n\x08max_size\x18\x05 \x01(\x03\"\xb5\x01\n\x11TreeEntryResponse\x12\x32\n\x04type\x18\x01 \x01(\x0e\x32$.gitaly.TreeEntryResponse.ObjectType\x12\x0b\n\x03oid\x18\x02 \x01(\t\x12\x0c\n\x04size\x18\x03 \x01(\x03\x12\x0c\n\x04mode\x18\x04 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\"5\n\nObjectType\x12\n\n\x06\x43OMMIT\x10\x00\x12\x08\n\x04\x42LOB\x10\x01\x12\x08\n\x04TREE\x10\x02\x12\x07\n\x03TAG\x10\x03\"\x9f\x02\n\x13\x43ountCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12)\n\x05\x61\x66ter\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12*\n\x06\x62\x65\x66ore\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04path\x18\x05 \x01(\x0c\x12\x11\n\tmax_count\x18\x06 \x01(\x05\x12\x0b\n\x03\x61ll\x18\x07 \x01(\x08\x12\x14\n\x0c\x66irst_parent\x18\x08 \x01(\x08\x12-\n\x0eglobal_options\x18\t \x01(\x0b\x32\x15.gitaly.GlobalOptions\"%\n\x14\x43ountCommitsResponse\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\"\x8b\x01\n\x1c\x43ountDivergingCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04\x66rom\x18\x02 \x01(\x0c\x12\n\n\x02to\x18\x03 \x01(\x0c\x12\x11\n\tmax_count\x18\x07 \x01(\x05J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07\"H\n\x1d\x43ountDivergingCommitsResponse\x12\x12\n\nleft_count\x18\x01 \x01(\x05\x12\x13\n\x0bright_count\x18\x02 \x01(\x05\"\xc5\x01\n\tTreeEntry\x12\x0b\n\x03oid\x18\x01 \x01(\t\x12\x10\n\x08root_oid\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12)\n\x04type\x18\x04 \x01(\x0e\x32\x1b.gitaly.TreeEntry.EntryType\x12\x0c\n\x04mode\x18\x05 \x01(\x05\x12\x12\n\ncommit_oid\x18\x06 \x01(\t\x12\x11\n\tflat_path\x18\x07 \x01(\x0c\"+\n\tEntryType\x12\x08\n\x04\x42LOB\x10\x00\x12\x08\n\x04TREE\x10\x01\x12\n\n\x06\x43OMMIT\x10\x03\"\xa5\x02\n\x15GetTreeEntriesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12\x11\n\trecursive\x18\x04 \x01(\x08\x12\x32\n\x04sort\x18\x05 \x01(\x0e\x32$.gitaly.GetTreeEntriesRequest.SortBy\x12\x36\n\x11pagination_params\x18\x06 \x01(\x0b\x32\x1b.gitaly.PaginationParameter\x12\x17\n\x0fskip_flat_paths\x18\x07 \x01(\x08\"&\n\x06SortBy\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x0f\n\x0bTREES_FIRST\x10\x01\"q\n\x16GetTreeEntriesResponse\x12\"\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x11.gitaly.TreeEntry\x12\x33\n\x11pagination_cursor\x18\x02 \x01(\x0b\x32\x18.gitaly.PaginationCursor\"R\n\x10ListFilesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\"\"\n\x11ListFilesResponse\x12\r\n\x05paths\x18\x01 \x03(\x0c\"e\n\x11\x46indCommitRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x10\n\x08trailers\x18\x03 \x01(\x08\"7\n\x12\x46indCommitResponse\x12!\n\x06\x63ommit\x18\x01 \x01(\x0b\x32\x11.gitaly.GitCommit\"T\n\x17ListCommitsByOidRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0b\n\x03oid\x18\x02 \x03(\t\">\n\x18ListCommitsByOidResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"^\n\x1bListCommitsByRefNameRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\tref_names\x18\x02 \x03(\x0c\"\xb1\x01\n\x1cListCommitsByRefNameResponse\x12\x46\n\x0b\x63ommit_refs\x18\x02 \x03(\x0b\x32\x31.gitaly.ListCommitsByRefNameResponse.CommitForRef\x1a\x43\n\x0c\x43ommitForRef\x12!\n\x06\x63ommit\x18\x01 \x01(\x0b\x32\x11.gitaly.GitCommit\x12\x10\n\x08ref_name\x18\x02 \x01(\x0cJ\x04\x08\x01\x10\x02\"\xd3\x01\n\x15\x46indAllCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x11\n\tmax_count\x18\x03 \x01(\x05\x12\x0c\n\x04skip\x18\x04 \x01(\x05\x12\x32\n\x05order\x18\x05 \x01(\x0e\x32#.gitaly.FindAllCommitsRequest.Order\"%\n\x05Order\x12\x08\n\x04NONE\x10\x00\x12\x08\n\x04TOPO\x10\x01\x12\x08\n\x04\x44\x41TE\x10\x02\"<\n\x16\x46indAllCommitsResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"\xf1\x03\n\x12\x46indCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\r\n\x05limit\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\x12\r\n\x05paths\x18\x05 \x03(\x0c\x12\x0e\n\x06\x66ollow\x18\x06 \x01(\x08\x12\x13\n\x0bskip_merges\x18\x07 \x01(\x08\x12\x14\n\x0c\x64isable_walk\x18\x08 \x01(\x08\x12)\n\x05\x61\x66ter\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12*\n\x06\x62\x65\x66ore\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0b\n\x03\x61ll\x18\x0b \x01(\x08\x12\x14\n\x0c\x66irst_parent\x18\x0c \x01(\x08\x12\x0e\n\x06\x61uthor\x18\r \x01(\x0c\x12/\n\x05order\x18\x0e \x01(\x0e\x32 .gitaly.FindCommitsRequest.Order\x12-\n\x0eglobal_options\x18\x0f \x01(\x0b\x32\x15.gitaly.GlobalOptions\x12\x10\n\x08trailers\x18\x10 \x01(\x08\x12\x19\n\x11include_shortstat\x18\x11 \x01(\x08\"\x1b\n\x05Order\x12\x08\n\x04NONE\x10\x00\x12\x08\n\x04TOPO\x10\x01\"9\n\x13\x46indCommitsResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"X\n\x16\x43ommitLanguagesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\"\xb1\x01\n\x17\x43ommitLanguagesResponse\x12;\n\tlanguages\x18\x01 \x03(\x0b\x32(.gitaly.CommitLanguagesResponse.Language\x1aY\n\x08Language\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05share\x18\x02 \x01(\x02\x12\r\n\x05\x63olor\x18\x03 \x01(\t\x12\x12\n\nfile_count\x18\x04 \x01(\r\x12\r\n\x05\x62ytes\x18\x05 \x01(\x04\"n\n\x0fRawBlameRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12\r\n\x05range\x18\x04 \x01(\x0c\" \n\x10RawBlameResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\xb1\x01\n\x18LastCommitForPathRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12\x18\n\x10literal_pathspec\x18\x04 \x01(\x08\x12-\n\x0eglobal_options\x18\x05 \x01(\x0b\x32\x15.gitaly.GlobalOptions\">\n\x19LastCommitForPathResponse\x12!\n\x06\x63ommit\x18\x01 \x01(\x0b\x32\x11.gitaly.GitCommit\"\xd9\x01\n\x1dListLastCommitsForTreeRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12\r\n\x05limit\x18\x04 \x01(\x05\x12\x0e\n\x06offset\x18\x05 \x01(\x05\x12\x1c\n\x10literal_pathspec\x18\x06 \x01(\x08\x42\x02\x18\x01\x12-\n\x0eglobal_options\x18\x07 \x01(\x0b\x32\x15.gitaly.GlobalOptions\"\xbb\x01\n\x1eListLastCommitsForTreeResponse\x12\x45\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x34.gitaly.ListLastCommitsForTreeResponse.CommitForTree\x1aR\n\rCommitForTree\x12!\n\x06\x63ommit\x18\x02 \x01(\x0b\x32\x11.gitaly.GitCommit\x12\x12\n\npath_bytes\x18\x04 \x01(\x0cJ\x04\x08\x01\x10\x02J\x04\x08\x03\x10\x04\"\xc4\x01\n\x17\x43ommitsByMessageRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x0e\n\x06offset\x18\x03 \x01(\x05\x12\r\n\x05limit\x18\x04 \x01(\x05\x12\x0c\n\x04path\x18\x05 \x01(\x0c\x12\r\n\x05query\x18\x06 \x01(\t\x12-\n\x0eglobal_options\x18\x07 \x01(\x0b\x32\x15.gitaly.GlobalOptions\">\n\x18\x43ommitsByMessageResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"]\n\x1f\x46ilterShasWithSignaturesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04shas\x18\x02 \x03(\x0c\"0\n FilterShasWithSignaturesResponse\x12\x0c\n\x04shas\x18\x01 \x03(\x0c\"`\n\x1d\x45xtractCommitSignatureRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\tcommit_id\x18\x02 \x01(\t\"H\n\x1e\x45xtractCommitSignatureResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\x12\x13\n\x0bsigned_text\x18\x02 \x01(\x0c\"^\n\x1aGetCommitSignaturesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x12\n\ncommit_ids\x18\x02 \x03(\t\"X\n\x1bGetCommitSignaturesResponse\x12\x11\n\tcommit_id\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x13\n\x0bsigned_text\x18\x03 \x01(\x0c\"\\\n\x18GetCommitMessagesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x12\n\ncommit_ids\x18\x02 \x03(\t\"?\n\x19GetCommitMessagesResponse\x12\x11\n\tcommit_id\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\x0c\"[\n\x18\x43heckObjectsExistRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\trevisions\x18\x02 \x03(\x0c\"\x96\x01\n\x19\x43heckObjectsExistResponse\x12\x46\n\trevisions\x18\x01 \x03(\x0b\x32\x33.gitaly.CheckObjectsExistResponse.RevisionExistence\x1a\x31\n\x11RevisionExistence\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\x0e\n\x06\x65xists\x18\x02 \x01(\x08\x32\xf1\x10\n\rCommitService\x12P\n\x0bListCommits\x12\x1a.gitaly.ListCommitsRequest\x1a\x1b.gitaly.ListCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12Y\n\x0eListAllCommits\x12\x1d.gitaly.ListAllCommitsRequest\x1a\x1e.gitaly.ListAllCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12]\n\x10\x43ommitIsAncestor\x12\x1f.gitaly.CommitIsAncestorRequest\x1a .gitaly.CommitIsAncestorResponse\"\x06\xfa\x97(\x02\x08\x02\x12J\n\tTreeEntry\x12\x18.gitaly.TreeEntryRequest\x1a\x19.gitaly.TreeEntryResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12Q\n\x0c\x43ountCommits\x12\x1b.gitaly.CountCommitsRequest\x1a\x1c.gitaly.CountCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x12l\n\x15\x43ountDivergingCommits\x12$.gitaly.CountDivergingCommitsRequest\x1a%.gitaly.CountDivergingCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x12Y\n\x0eGetTreeEntries\x12\x1d.gitaly.GetTreeEntriesRequest\x1a\x1e.gitaly.GetTreeEntriesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12J\n\tListFiles\x12\x18.gitaly.ListFilesRequest\x1a\x19.gitaly.ListFilesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12K\n\nFindCommit\x12\x19.gitaly.FindCommitRequest\x1a\x1a.gitaly.FindCommitResponse\"\x06\xfa\x97(\x02\x08\x02\x12N\n\x0b\x43ommitStats\x12\x1a.gitaly.CommitStatsRequest\x1a\x1b.gitaly.CommitStatsResponse\"\x06\xfa\x97(\x02\x08\x02\x12Y\n\x0e\x46indAllCommits\x12\x1d.gitaly.FindAllCommitsRequest\x1a\x1e.gitaly.FindAllCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12P\n\x0b\x46indCommits\x12\x1a.gitaly.FindCommitsRequest\x1a\x1b.gitaly.FindCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12Z\n\x0f\x43ommitLanguages\x12\x1e.gitaly.CommitLanguagesRequest\x1a\x1f.gitaly.CommitLanguagesResponse\"\x06\xfa\x97(\x02\x08\x02\x12G\n\x08RawBlame\x12\x17.gitaly.RawBlameRequest\x1a\x18.gitaly.RawBlameResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12`\n\x11LastCommitForPath\x12 .gitaly.LastCommitForPathRequest\x1a!.gitaly.LastCommitForPathResponse\"\x06\xfa\x97(\x02\x08\x02\x12q\n\x16ListLastCommitsForTree\x12%.gitaly.ListLastCommitsForTreeRequest\x1a&.gitaly.ListLastCommitsForTreeResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12_\n\x10\x43ommitsByMessage\x12\x1f.gitaly.CommitsByMessageRequest\x1a .gitaly.CommitsByMessageResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12_\n\x10ListCommitsByOid\x12\x1f.gitaly.ListCommitsByOidRequest\x1a .gitaly.ListCommitsByOidResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12k\n\x14ListCommitsByRefName\x12#.gitaly.ListCommitsByRefNameRequest\x1a$.gitaly.ListCommitsByRefNameResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12y\n\x18\x46ilterShasWithSignatures\x12\'.gitaly.FilterShasWithSignaturesRequest\x1a(.gitaly.FilterShasWithSignaturesResponse\"\x06\xfa\x97(\x02\x08\x02(\x01\x30\x01\x12h\n\x13GetCommitSignatures\x12\".gitaly.GetCommitSignaturesRequest\x1a#.gitaly.GetCommitSignaturesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x62\n\x11GetCommitMessages\x12 .gitaly.GetCommitMessagesRequest\x1a!.gitaly.GetCommitMessagesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x64\n\x11\x43heckObjectsExist\x12 .gitaly.CheckObjectsExistRequest\x1a!.gitaly.CheckObjectsExistResponse\"\x06\xfa\x97(\x02\x08\x02(\x01\x30\x01\x42\x34Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypbb\x06proto3'
+  serialized_pb=b'\n\x0c\x63ommit.proto\x12\x06gitaly\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\nlint.proto\x1a\x0cshared.proto\"\xd4\x03\n\x12ListCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\trevisions\x18\x02 \x03(\t\x12\x36\n\x11pagination_params\x18\x03 \x01(\x0b\x32\x1b.gitaly.PaginationParameter\x12/\n\x05order\x18\x04 \x01(\x0e\x32 .gitaly.ListCommitsRequest.Order\x12\x0f\n\x07reverse\x18\x0b \x01(\x08\x12\x13\n\x0bmax_parents\x18\x05 \x01(\r\x12\x14\n\x0c\x64isable_walk\x18\x06 \x01(\x08\x12\x14\n\x0c\x66irst_parent\x18\x07 \x01(\x08\x12)\n\x05\x61\x66ter\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12*\n\x06\x62\x65\x66ore\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x61uthor\x18\n \x01(\x0c\x12\x13\n\x0bignore_case\x18\x0c \x01(\x08\x12\x1f\n\x17\x63ommit_message_patterns\x18\r \x03(\x0c\"%\n\x05Order\x12\x08\n\x04NONE\x10\x00\x12\x08\n\x04TOPO\x10\x01\x12\x08\n\x04\x44\x41TE\x10\x02\"9\n\x13ListCommitsResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"}\n\x15ListAllCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x36\n\x11pagination_params\x18\x02 \x01(\x0b\x32\x1b.gitaly.PaginationParameter\"<\n\x16ListAllCommitsResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"T\n\x12\x43ommitStatsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\"H\n\x13\x43ommitStatsResponse\x12\x0b\n\x03oid\x18\x01 \x01(\t\x12\x11\n\tadditions\x18\x02 \x01(\x05\x12\x11\n\tdeletions\x18\x03 \x01(\x05\"n\n\x17\x43ommitIsAncestorRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x13\n\x0b\x61ncestor_id\x18\x02 \x01(\t\x12\x10\n\x08\x63hild_id\x18\x03 \x01(\t\")\n\x18\x43ommitIsAncestorResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"\x81\x01\n\x10TreeEntryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12\r\n\x05limit\x18\x04 \x01(\x03\x12\x10\n\x08max_size\x18\x05 \x01(\x03\"\xb5\x01\n\x11TreeEntryResponse\x12\x32\n\x04type\x18\x01 \x01(\x0e\x32$.gitaly.TreeEntryResponse.ObjectType\x12\x0b\n\x03oid\x18\x02 \x01(\t\x12\x0c\n\x04size\x18\x03 \x01(\x03\x12\x0c\n\x04mode\x18\x04 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\"5\n\nObjectType\x12\n\n\x06\x43OMMIT\x10\x00\x12\x08\n\x04\x42LOB\x10\x01\x12\x08\n\x04TREE\x10\x02\x12\x07\n\x03TAG\x10\x03\"\x9f\x02\n\x13\x43ountCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12)\n\x05\x61\x66ter\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12*\n\x06\x62\x65\x66ore\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04path\x18\x05 \x01(\x0c\x12\x11\n\tmax_count\x18\x06 \x01(\x05\x12\x0b\n\x03\x61ll\x18\x07 \x01(\x08\x12\x14\n\x0c\x66irst_parent\x18\x08 \x01(\x08\x12-\n\x0eglobal_options\x18\t \x01(\x0b\x32\x15.gitaly.GlobalOptions\"%\n\x14\x43ountCommitsResponse\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\"\x8b\x01\n\x1c\x43ountDivergingCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04\x66rom\x18\x02 \x01(\x0c\x12\n\n\x02to\x18\x03 \x01(\x0c\x12\x11\n\tmax_count\x18\x07 \x01(\x05J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07\"H\n\x1d\x43ountDivergingCommitsResponse\x12\x12\n\nleft_count\x18\x01 \x01(\x05\x12\x13\n\x0bright_count\x18\x02 \x01(\x05\"\xc9\x01\n\tTreeEntry\x12\x0b\n\x03oid\x18\x01 \x01(\t\x12\x14\n\x08root_oid\x18\x02 \x01(\tB\x02\x18\x01\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12)\n\x04type\x18\x04 \x01(\x0e\x32\x1b.gitaly.TreeEntry.EntryType\x12\x0c\n\x04mode\x18\x05 \x01(\x05\x12\x12\n\ncommit_oid\x18\x06 \x01(\t\x12\x11\n\tflat_path\x18\x07 \x01(\x0c\"+\n\tEntryType\x12\x08\n\x04\x42LOB\x10\x00\x12\x08\n\x04TREE\x10\x01\x12\n\n\x06\x43OMMIT\x10\x03\"\xa5\x02\n\x15GetTreeEntriesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12\x11\n\trecursive\x18\x04 \x01(\x08\x12\x32\n\x04sort\x18\x05 \x01(\x0e\x32$.gitaly.GetTreeEntriesRequest.SortBy\x12\x36\n\x11pagination_params\x18\x06 \x01(\x0b\x32\x1b.gitaly.PaginationParameter\x12\x17\n\x0fskip_flat_paths\x18\x07 \x01(\x08\"&\n\x06SortBy\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x0f\n\x0bTREES_FIRST\x10\x01\"q\n\x16GetTreeEntriesResponse\x12\"\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x11.gitaly.TreeEntry\x12\x33\n\x11pagination_cursor\x18\x02 \x01(\x0b\x32\x18.gitaly.PaginationCursor\"R\n\x10ListFilesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\"\"\n\x11ListFilesResponse\x12\r\n\x05paths\x18\x01 \x03(\x0c\"e\n\x11\x46indCommitRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x10\n\x08trailers\x18\x03 \x01(\x08\"7\n\x12\x46indCommitResponse\x12!\n\x06\x63ommit\x18\x01 \x01(\x0b\x32\x11.gitaly.GitCommit\"T\n\x17ListCommitsByOidRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0b\n\x03oid\x18\x02 \x03(\t\">\n\x18ListCommitsByOidResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"^\n\x1bListCommitsByRefNameRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\tref_names\x18\x02 \x03(\x0c\"\xb1\x01\n\x1cListCommitsByRefNameResponse\x12\x46\n\x0b\x63ommit_refs\x18\x02 \x03(\x0b\x32\x31.gitaly.ListCommitsByRefNameResponse.CommitForRef\x1a\x43\n\x0c\x43ommitForRef\x12!\n\x06\x63ommit\x18\x01 \x01(\x0b\x32\x11.gitaly.GitCommit\x12\x10\n\x08ref_name\x18\x02 \x01(\x0cJ\x04\x08\x01\x10\x02\"\xd3\x01\n\x15\x46indAllCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x11\n\tmax_count\x18\x03 \x01(\x05\x12\x0c\n\x04skip\x18\x04 \x01(\x05\x12\x32\n\x05order\x18\x05 \x01(\x0e\x32#.gitaly.FindAllCommitsRequest.Order\"%\n\x05Order\x12\x08\n\x04NONE\x10\x00\x12\x08\n\x04TOPO\x10\x01\x12\x08\n\x04\x44\x41TE\x10\x02\"<\n\x16\x46indAllCommitsResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"\x90\x04\n\x12\x46indCommitsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\r\n\x05limit\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\x12\r\n\x05paths\x18\x05 \x03(\x0c\x12\x0e\n\x06\x66ollow\x18\x06 \x01(\x08\x12\x13\n\x0bskip_merges\x18\x07 \x01(\x08\x12\x14\n\x0c\x64isable_walk\x18\x08 \x01(\x08\x12)\n\x05\x61\x66ter\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12*\n\x06\x62\x65\x66ore\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0b\n\x03\x61ll\x18\x0b \x01(\x08\x12\x14\n\x0c\x66irst_parent\x18\x0c \x01(\x08\x12\x0e\n\x06\x61uthor\x18\r \x01(\x0c\x12/\n\x05order\x18\x0e \x01(\x0e\x32 .gitaly.FindCommitsRequest.Order\x12-\n\x0eglobal_options\x18\x0f \x01(\x0b\x32\x15.gitaly.GlobalOptions\x12\x10\n\x08trailers\x18\x10 \x01(\x08\x12\x19\n\x11include_shortstat\x18\x11 \x01(\x08\x12\x1d\n\x15include_referenced_by\x18\x12 \x03(\x0c\"\x1b\n\x05Order\x12\x08\n\x04NONE\x10\x00\x12\x08\n\x04TOPO\x10\x01\"9\n\x13\x46indCommitsResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"X\n\x16\x43ommitLanguagesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\"\xaf\x01\n\x17\x43ommitLanguagesResponse\x12;\n\tlanguages\x18\x01 \x03(\x0b\x32(.gitaly.CommitLanguagesResponse.Language\x1aW\n\x08Language\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05share\x18\x02 \x01(\x02\x12\r\n\x05\x63olor\x18\x03 \x01(\t\x12\r\n\x05\x62ytes\x18\x05 \x01(\x04J\x04\x08\x04\x10\x05R\nfile_count\"n\n\x0fRawBlameRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12\r\n\x05range\x18\x04 \x01(\x0c\" \n\x10RawBlameResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\xb1\x01\n\x18LastCommitForPathRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12\x18\n\x10literal_pathspec\x18\x04 \x01(\x08\x12-\n\x0eglobal_options\x18\x05 \x01(\x0b\x32\x15.gitaly.GlobalOptions\">\n\x19LastCommitForPathResponse\x12!\n\x06\x63ommit\x18\x01 \x01(\x0b\x32\x11.gitaly.GitCommit\"\xd9\x01\n\x1dListLastCommitsForTreeRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\x0c\x12\r\n\x05limit\x18\x04 \x01(\x05\x12\x0e\n\x06offset\x18\x05 \x01(\x05\x12\x1c\n\x10literal_pathspec\x18\x06 \x01(\x08\x42\x02\x18\x01\x12-\n\x0eglobal_options\x18\x07 \x01(\x0b\x32\x15.gitaly.GlobalOptions\"\xbb\x01\n\x1eListLastCommitsForTreeResponse\x12\x45\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x34.gitaly.ListLastCommitsForTreeResponse.CommitForTree\x1aR\n\rCommitForTree\x12!\n\x06\x63ommit\x18\x02 \x01(\x0b\x32\x11.gitaly.GitCommit\x12\x12\n\npath_bytes\x18\x04 \x01(\x0cJ\x04\x08\x01\x10\x02J\x04\x08\x03\x10\x04\"\xc4\x01\n\x17\x43ommitsByMessageRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\x12\x0e\n\x06offset\x18\x03 \x01(\x05\x12\r\n\x05limit\x18\x04 \x01(\x05\x12\x0c\n\x04path\x18\x05 \x01(\x0c\x12\r\n\x05query\x18\x06 \x01(\t\x12-\n\x0eglobal_options\x18\x07 \x01(\x0b\x32\x15.gitaly.GlobalOptions\">\n\x18\x43ommitsByMessageResponse\x12\"\n\x07\x63ommits\x18\x01 \x03(\x0b\x32\x11.gitaly.GitCommit\"]\n\x1f\x46ilterShasWithSignaturesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04shas\x18\x02 \x03(\x0c\"0\n FilterShasWithSignaturesResponse\x12\x0c\n\x04shas\x18\x01 \x03(\x0c\"`\n\x1d\x45xtractCommitSignatureRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\tcommit_id\x18\x02 \x01(\t\"H\n\x1e\x45xtractCommitSignatureResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\x12\x13\n\x0bsigned_text\x18\x02 \x01(\x0c\"^\n\x1aGetCommitSignaturesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x12\n\ncommit_ids\x18\x02 \x03(\t\"X\n\x1bGetCommitSignaturesResponse\x12\x11\n\tcommit_id\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x13\n\x0bsigned_text\x18\x03 \x01(\x0c\"\\\n\x18GetCommitMessagesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x12\n\ncommit_ids\x18\x02 \x03(\t\"?\n\x19GetCommitMessagesResponse\x12\x11\n\tcommit_id\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\x0c\"[\n\x18\x43heckObjectsExistRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\trevisions\x18\x02 \x03(\x0c\"\x96\x01\n\x19\x43heckObjectsExistResponse\x12\x46\n\trevisions\x18\x01 \x03(\x0b\x32\x33.gitaly.CheckObjectsExistResponse.RevisionExistence\x1a\x31\n\x11RevisionExistence\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\x0e\n\x06\x65xists\x18\x02 \x01(\x08\x32\xf1\x10\n\rCommitService\x12P\n\x0bListCommits\x12\x1a.gitaly.ListCommitsRequest\x1a\x1b.gitaly.ListCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12Y\n\x0eListAllCommits\x12\x1d.gitaly.ListAllCommitsRequest\x1a\x1e.gitaly.ListAllCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12]\n\x10\x43ommitIsAncestor\x12\x1f.gitaly.CommitIsAncestorRequest\x1a .gitaly.CommitIsAncestorResponse\"\x06\xfa\x97(\x02\x08\x02\x12J\n\tTreeEntry\x12\x18.gitaly.TreeEntryRequest\x1a\x19.gitaly.TreeEntryResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12Q\n\x0c\x43ountCommits\x12\x1b.gitaly.CountCommitsRequest\x1a\x1c.gitaly.CountCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x12l\n\x15\x43ountDivergingCommits\x12$.gitaly.CountDivergingCommitsRequest\x1a%.gitaly.CountDivergingCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x12Y\n\x0eGetTreeEntries\x12\x1d.gitaly.GetTreeEntriesRequest\x1a\x1e.gitaly.GetTreeEntriesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12J\n\tListFiles\x12\x18.gitaly.ListFilesRequest\x1a\x19.gitaly.ListFilesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12K\n\nFindCommit\x12\x19.gitaly.FindCommitRequest\x1a\x1a.gitaly.FindCommitResponse\"\x06\xfa\x97(\x02\x08\x02\x12N\n\x0b\x43ommitStats\x12\x1a.gitaly.CommitStatsRequest\x1a\x1b.gitaly.CommitStatsResponse\"\x06\xfa\x97(\x02\x08\x02\x12Y\n\x0e\x46indAllCommits\x12\x1d.gitaly.FindAllCommitsRequest\x1a\x1e.gitaly.FindAllCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12P\n\x0b\x46indCommits\x12\x1a.gitaly.FindCommitsRequest\x1a\x1b.gitaly.FindCommitsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12Z\n\x0f\x43ommitLanguages\x12\x1e.gitaly.CommitLanguagesRequest\x1a\x1f.gitaly.CommitLanguagesResponse\"\x06\xfa\x97(\x02\x08\x02\x12G\n\x08RawBlame\x12\x17.gitaly.RawBlameRequest\x1a\x18.gitaly.RawBlameResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12`\n\x11LastCommitForPath\x12 .gitaly.LastCommitForPathRequest\x1a!.gitaly.LastCommitForPathResponse\"\x06\xfa\x97(\x02\x08\x02\x12q\n\x16ListLastCommitsForTree\x12%.gitaly.ListLastCommitsForTreeRequest\x1a&.gitaly.ListLastCommitsForTreeResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12_\n\x10\x43ommitsByMessage\x12\x1f.gitaly.CommitsByMessageRequest\x1a .gitaly.CommitsByMessageResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12_\n\x10ListCommitsByOid\x12\x1f.gitaly.ListCommitsByOidRequest\x1a .gitaly.ListCommitsByOidResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12k\n\x14ListCommitsByRefName\x12#.gitaly.ListCommitsByRefNameRequest\x1a$.gitaly.ListCommitsByRefNameResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12y\n\x18\x46ilterShasWithSignatures\x12\'.gitaly.FilterShasWithSignaturesRequest\x1a(.gitaly.FilterShasWithSignaturesResponse\"\x06\xfa\x97(\x02\x08\x02(\x01\x30\x01\x12h\n\x13GetCommitSignatures\x12\".gitaly.GetCommitSignaturesRequest\x1a#.gitaly.GetCommitSignaturesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x62\n\x11GetCommitMessages\x12 .gitaly.GetCommitMessagesRequest\x1a!.gitaly.GetCommitMessagesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x64\n\x11\x43heckObjectsExist\x12 .gitaly.CheckObjectsExistRequest\x1a!.gitaly.CheckObjectsExistResponse\"\x06\xfa\x97(\x02\x08\x02(\x01\x30\x01\x42\x34Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypbb\x06proto3'
   ,
   dependencies=[google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,lint__pb2.DESCRIPTOR,shared__pb2.DESCRIPTOR,])
 
@@ -118,8 +118,8 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=2133,
-  serialized_end=2176,
+  serialized_start=2137,
+  serialized_end=2180,
 )
 _sym_db.RegisterEnumDescriptor(_TREEENTRY_ENTRYTYPE)
 
@@ -143,8 +143,8 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=2434,
-  serialized_end=2472,
+  serialized_start=2438,
+  serialized_end=2476,
 )
 _sym_db.RegisterEnumDescriptor(_GETTREEENTRIESREQUEST_SORTBY)
 
@@ -941,7 +941,7 @@
       has_default_value=False, default_value=b"".decode('utf-8'),
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
-      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+      serialized_options=b'\030\001', file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
     _descriptor.FieldDescriptor(
       name='path', full_name='gitaly.TreeEntry.path', index=2,
       number=3, type=12, cpp_type=9, label=1,
@@ -991,7 +991,7 @@
   oneofs=[
   ],
   serialized_start=1979,
-  serialized_end=2176,
+  serialized_end=2180,
 )
 
 
@@ -1065,8 +1065,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2179,
-  serialized_end=2472,
+  serialized_start=2183,
+  serialized_end=2476,
 )
 
 
@@ -1104,8 +1104,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2474,
-  serialized_end=2587,
+  serialized_start=2478,
+  serialized_end=2591,
 )
 
 
@@ -1143,8 +1143,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2589,
-  serialized_end=2671,
+  serialized_start=2593,
+  serialized_end=2675,
 )
 
 
@@ -1175,8 +1175,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2673,
-  serialized_end=2707,
+  serialized_start=2677,
+  serialized_end=2711,
 )
 
 
@@ -1221,8 +1221,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2709,
-  serialized_end=2810,
+  serialized_start=2713,
+  serialized_end=2814,
 )
 
 
@@ -1253,8 +1253,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2812,
-  serialized_end=2867,
+  serialized_start=2816,
+  serialized_end=2871,
 )
 
 
@@ -1292,8 +1292,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2869,
-  serialized_end=2953,
+  serialized_start=2873,
+  serialized_end=2957,
 )
 
 
@@ -1324,8 +1324,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2955,
-  serialized_end=3017,
+  serialized_start=2959,
+  serialized_end=3021,
 )
 
 
@@ -1363,8 +1363,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3019,
-  serialized_end=3113,
+  serialized_start=3023,
+  serialized_end=3117,
 )
 
 
@@ -1402,8 +1402,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3220,
-  serialized_end=3287,
+  serialized_start=3224,
+  serialized_end=3291,
 )
 
 _LISTCOMMITSBYREFNAMERESPONSE = _descriptor.Descriptor(
@@ -1433,8 +1433,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3116,
-  serialized_end=3293,
+  serialized_start=3120,
+  serialized_end=3297,
 )
 
 
@@ -1494,8 +1494,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3296,
-  serialized_end=3507,
+  serialized_start=3300,
+  serialized_end=3511,
 )
 
 
@@ -1526,8 +1526,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3509,
-  serialized_end=3569,
+  serialized_start=3513,
+  serialized_end=3573,
 )
 
 
@@ -1658,6 +1658,13 @@
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='include_referenced_by', full_name='gitaly.FindCommitsRequest.include_referenced_by', index=17,
+      number=18, type=12, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
   ],
   extensions=[
   ],
@@ -1671,8 +1678,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3572,
-  serialized_end=4069,
+  serialized_start=3576,
+  serialized_end=4104,
 )
 
 
@@ -1703,8 +1710,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4071,
-  serialized_end=4128,
+  serialized_start=4106,
+  serialized_end=4163,
 )
 
 
@@ -1742,8 +1749,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4130,
-  serialized_end=4218,
+  serialized_start=4165,
+  serialized_end=4253,
 )
 
 
@@ -1777,14 +1784,7 @@
       is_extension=False, extension_scope=None,
       serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
     _descriptor.FieldDescriptor(
-      name='file_count', full_name='gitaly.CommitLanguagesResponse.Language.file_count', index=3,
-      number=4, type=13, cpp_type=3, label=1,
-      has_default_value=False, default_value=0,
-      message_type=None, enum_type=None, containing_type=None,
-      is_extension=False, extension_scope=None,
-      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
-    _descriptor.FieldDescriptor(
-      name='bytes', full_name='gitaly.CommitLanguagesResponse.Language.bytes', index=4,
+      name='bytes', full_name='gitaly.CommitLanguagesResponse.Language.bytes', index=3,
       number=5, type=4, cpp_type=4, label=1,
       has_default_value=False, default_value=0,
       message_type=None, enum_type=None, containing_type=None,
@@ -1802,8 +1802,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4309,
-  serialized_end=4398,
+  serialized_start=4344,
+  serialized_end=4431,
 )
 
 _COMMITLANGUAGESRESPONSE = _descriptor.Descriptor(
@@ -1833,8 +1833,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4221,
-  serialized_end=4398,
+  serialized_start=4256,
+  serialized_end=4431,
 )
 
 
@@ -1886,8 +1886,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4400,
-  serialized_end=4510,
+  serialized_start=4433,
+  serialized_end=4543,
 )
 
 
@@ -1918,8 +1918,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4512,
-  serialized_end=4544,
+  serialized_start=4545,
+  serialized_end=4577,
 )
 
 
@@ -1978,8 +1978,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4547,
-  serialized_end=4724,
+  serialized_start=4580,
+  serialized_end=4757,
 )
 
 
@@ -2010,8 +2010,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4726,
-  serialized_end=4788,
+  serialized_start=4759,
+  serialized_end=4821,
 )
 
 
@@ -2084,8 +2084,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4791,
-  serialized_end=5008,
+  serialized_start=4824,
+  serialized_end=5041,
 )
 
 
@@ -2123,8 +2123,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5116,
-  serialized_end=5198,
+  serialized_start=5149,
+  serialized_end=5231,
 )
 
 _LISTLASTCOMMITSFORTREERESPONSE = _descriptor.Descriptor(
@@ -2154,8 +2154,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5011,
-  serialized_end=5198,
+  serialized_start=5044,
+  serialized_end=5231,
 )
 
 
@@ -2228,8 +2228,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5201,
-  serialized_end=5397,
+  serialized_start=5234,
+  serialized_end=5430,
 )
 
 
@@ -2260,8 +2260,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5399,
-  serialized_end=5461,
+  serialized_start=5432,
+  serialized_end=5494,
 )
 
 
@@ -2299,8 +2299,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5463,
-  serialized_end=5556,
+  serialized_start=5496,
+  serialized_end=5589,
 )
 
 
@@ -2331,8 +2331,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5558,
-  serialized_end=5606,
+  serialized_start=5591,
+  serialized_end=5639,
 )
 
 
@@ -2370,8 +2370,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5608,
-  serialized_end=5704,
+  serialized_start=5641,
+  serialized_end=5737,
 )
 
 
@@ -2409,8 +2409,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5706,
-  serialized_end=5778,
+  serialized_start=5739,
+  serialized_end=5811,
 )
 
 
@@ -2448,8 +2448,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5780,
-  serialized_end=5874,
+  serialized_start=5813,
+  serialized_end=5907,
 )
 
 
@@ -2494,8 +2494,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5876,
-  serialized_end=5964,
+  serialized_start=5909,
+  serialized_end=5997,
 )
 
 
@@ -2533,8 +2533,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5966,
-  serialized_end=6058,
+  serialized_start=5999,
+  serialized_end=6091,
 )
 
 
@@ -2572,8 +2572,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6060,
-  serialized_end=6123,
+  serialized_start=6093,
+  serialized_end=6156,
 )
 
 
@@ -2611,8 +2611,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6125,
-  serialized_end=6216,
+  serialized_start=6158,
+  serialized_end=6249,
 )
 
 
@@ -2650,8 +2650,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6320,
-  serialized_end=6369,
+  serialized_start=6353,
+  serialized_end=6402,
 )
 
 _CHECKOBJECTSEXISTRESPONSE = _descriptor.Descriptor(
@@ -2681,8 +2681,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6219,
-  serialized_end=6369,
+  serialized_start=6252,
+  serialized_end=6402,
 )
 
 _LISTCOMMITSREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
@@ -3190,6 +3190,7 @@
 _TREEENTRYREQUEST.fields_by_name['repository']._options = None
 _COUNTCOMMITSREQUEST.fields_by_name['repository']._options = None
 _COUNTDIVERGINGCOMMITSREQUEST.fields_by_name['repository']._options = None
+_TREEENTRY.fields_by_name['root_oid']._options = None
 _GETTREEENTRIESREQUEST.fields_by_name['repository']._options = None
 _LISTFILESREQUEST.fields_by_name['repository']._options = None
 _FINDCOMMITREQUEST.fields_by_name['repository']._options = None
@@ -3216,8 +3217,8 @@
   index=0,
   serialized_options=None,
   create_key=_descriptor._internal_create_key,
-  serialized_start=6372,
-  serialized_end=8533,
+  serialized_start=6405,
+  serialized_end=8566,
   methods=[
   _descriptor.MethodDescriptor(
     name='ListCommits',
diff --git a/hgitaly/stub/diff_pb2.py b/hgitaly/stub/diff_pb2.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zdHViL2RpZmZfcGIyLnB5..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zdHViL2RpZmZfcGIyLnB5 100644
--- a/hgitaly/stub/diff_pb2.py
+++ b/hgitaly/stub/diff_pb2.py
@@ -21,7 +21,7 @@
   syntax='proto3',
   serialized_options=b'Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb',
   create_key=_descriptor._internal_create_key,
-  serialized_pb=b'\n\ndiff.proto\x12\x06gitaly\x1a\nlint.proto\x1a\x0cshared.proto\"\xcb\x03\n\x11\x43ommitDiffRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0eleft_commit_id\x18\x02 \x01(\t\x12\x17\n\x0fright_commit_id\x18\x03 \x01(\t\x12 \n\x18ignore_whitespace_change\x18\x04 \x01(\x08\x12\r\n\x05paths\x18\x05 \x03(\x0c\x12\x16\n\x0e\x63ollapse_diffs\x18\x06 \x01(\x08\x12\x16\n\x0e\x65nforce_limits\x18\x07 \x01(\x08\x12\x11\n\tmax_files\x18\x08 \x01(\x05\x12\x11\n\tmax_lines\x18\t \x01(\x05\x12\x11\n\tmax_bytes\x18\n \x01(\x05\x12\x17\n\x0fmax_patch_bytes\x18\x0e \x01(\x05\x12\x16\n\x0esafe_max_files\x18\x0b \x01(\x05\x12\x16\n\x0esafe_max_lines\x18\x0c \x01(\x05\x12\x16\n\x0esafe_max_bytes\x18\r \x01(\x05\x12\x35\n\tdiff_mode\x18\x0f \x01(\x0e\x32\".gitaly.CommitDiffRequest.DiffMode\"%\n\x08\x44iffMode\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x0c\n\x08WORDDIFF\x10\x01\"\xff\x01\n\x12\x43ommitDiffResponse\x12\x11\n\tfrom_path\x18\x01 \x01(\x0c\x12\x0f\n\x07to_path\x18\x02 \x01(\x0c\x12\x0f\n\x07\x66rom_id\x18\x03 \x01(\t\x12\r\n\x05to_id\x18\x04 \x01(\t\x12\x10\n\x08old_mode\x18\x05 \x01(\x05\x12\x10\n\x08new_mode\x18\x06 \x01(\x05\x12\x0e\n\x06\x62inary\x18\x07 \x01(\x08\x12\x16\n\x0eraw_patch_data\x18\t \x01(\x0c\x12\x14\n\x0c\x65nd_of_patch\x18\n \x01(\x08\x12\x17\n\x0foverflow_marker\x18\x0b \x01(\x08\x12\x11\n\tcollapsed\x18\x0c \x01(\x08\x12\x11\n\ttoo_large\x18\r \x01(\x08J\x04\x08\x08\x10\t\"\x82\x01\n\x12\x43ommitDeltaRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0eleft_commit_id\x18\x02 \x01(\t\x12\x17\n\x0fright_commit_id\x18\x03 \x01(\t\x12\r\n\x05paths\x18\x04 \x03(\x0c\"u\n\x0b\x43ommitDelta\x12\x11\n\tfrom_path\x18\x01 \x01(\x0c\x12\x0f\n\x07to_path\x18\x02 \x01(\x0c\x12\x0f\n\x07\x66rom_id\x18\x03 \x01(\t\x12\r\n\x05to_id\x18\x04 \x01(\t\x12\x10\n\x08old_mode\x18\x05 \x01(\x05\x12\x10\n\x08new_mode\x18\x06 \x01(\x05\":\n\x13\x43ommitDeltaResponse\x12#\n\x06\x64\x65ltas\x18\x01 \x03(\x0b\x32\x13.gitaly.CommitDelta\"o\n\x0eRawDiffRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0eleft_commit_id\x18\x02 \x01(\t\x12\x17\n\x0fright_commit_id\x18\x03 \x01(\t\"\x1f\n\x0fRawDiffResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"p\n\x0fRawPatchRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0eleft_commit_id\x18\x02 \x01(\t\x12\x17\n\x0fright_commit_id\x18\x03 \x01(\t\" \n\x10RawPatchResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"q\n\x10\x44iffStatsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0eleft_commit_id\x18\x02 \x01(\t\x12\x17\n\x0fright_commit_id\x18\x03 \x01(\t\"Q\n\tDiffStats\x12\x0c\n\x04path\x18\x01 \x01(\x0c\x12\x11\n\tadditions\x18\x02 \x01(\x05\x12\x11\n\tdeletions\x18\x03 \x01(\x05\x12\x10\n\x08old_path\x18\x04 \x01(\x0c\"5\n\x11\x44iffStatsResponse\x12 \n\x05stats\x18\x01 \x03(\x0b\x32\x11.gitaly.DiffStats\"\xdc\x03\n\x17\x46indChangedPathsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x13\n\x07\x63ommits\x18\x02 \x03(\tB\x02\x18\x01\x12\x39\n\x08requests\x18\x03 \x03(\x0b\x32\'.gitaly.FindChangedPathsRequest.Request\x1a\xc2\x02\n\x07Request\x12K\n\x0ctree_request\x18\x01 \x01(\x0b\x32\x33.gitaly.FindChangedPathsRequest.Request.TreeRequestH\x00\x12O\n\x0e\x63ommit_request\x18\x02 \x01(\x0b\x32\x35.gitaly.FindChangedPathsRequest.Request.CommitRequestH\x00\x1a\x46\n\x0bTreeRequest\x12\x1a\n\x12left_tree_revision\x18\x01 \x01(\t\x12\x1b\n\x13right_tree_revision\x18\x02 \x01(\t\x1aI\n\rCommitRequest\x12\x17\n\x0f\x63ommit_revision\x18\x01 \x01(\t\x12\x1f\n\x17parent_commit_revisions\x18\x02 \x03(\tB\x06\n\x04type\"?\n\x18\x46indChangedPathsResponse\x12#\n\x05paths\x18\x01 \x03(\x0b\x32\x14.gitaly.ChangedPaths\"\xba\x01\n\x0c\x43hangedPaths\x12\x0c\n\x04path\x18\x01 \x01(\x0c\x12+\n\x06status\x18\x02 \x01(\x0e\x32\x1b.gitaly.ChangedPaths.Status\x12\x10\n\x08old_mode\x18\x03 \x01(\x05\x12\x10\n\x08new_mode\x18\x04 \x01(\x05\"K\n\x06Status\x12\t\n\x05\x41\x44\x44\x45\x44\x10\x00\x12\x0c\n\x08MODIFIED\x10\x01\x12\x0b\n\x07\x44\x45LETED\x10\x02\x12\x0f\n\x0bTYPE_CHANGE\x10\x03\x12\n\n\x06\x43OPIED\x10\x04\x32\xea\x03\n\x0b\x44iffService\x12M\n\nCommitDiff\x12\x19.gitaly.CommitDiffRequest\x1a\x1a.gitaly.CommitDiffResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12P\n\x0b\x43ommitDelta\x12\x1a.gitaly.CommitDeltaRequest\x1a\x1b.gitaly.CommitDeltaResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x44\n\x07RawDiff\x12\x16.gitaly.RawDiffRequest\x1a\x17.gitaly.RawDiffResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12G\n\x08RawPatch\x12\x17.gitaly.RawPatchRequest\x1a\x18.gitaly.RawPatchResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12J\n\tDiffStats\x12\x18.gitaly.DiffStatsRequest\x1a\x19.gitaly.DiffStatsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12_\n\x10\x46indChangedPaths\x12\x1f.gitaly.FindChangedPathsRequest\x1a .gitaly.FindChangedPathsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x42\x34Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypbb\x06proto3'
+  serialized_pb=b'\n\ndiff.proto\x12\x06gitaly\x1a\nlint.proto\x1a\x0cshared.proto\"\xc3\x06\n\x11\x43ommitDiffRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0eleft_commit_id\x18\x02 \x01(\t\x12\x17\n\x0fright_commit_id\x18\x03 \x01(\t\x12$\n\x18ignore_whitespace_change\x18\x04 \x01(\x08\x42\x02\x18\x01\x12\r\n\x05paths\x18\x05 \x03(\x0c\x12\x16\n\x0e\x63ollapse_diffs\x18\x06 \x01(\x08\x12\x16\n\x0e\x65nforce_limits\x18\x07 \x01(\x08\x12\x11\n\tmax_files\x18\x08 \x01(\x05\x12\x11\n\tmax_lines\x18\t \x01(\x05\x12\x11\n\tmax_bytes\x18\n \x01(\x05\x12\x17\n\x0fmax_patch_bytes\x18\x0e \x01(\x05\x12\x16\n\x0esafe_max_files\x18\x0b \x01(\x05\x12\x16\n\x0esafe_max_lines\x18\x0c \x01(\x05\x12\x16\n\x0esafe_max_bytes\x18\r \x01(\x05\x12\x35\n\tdiff_mode\x18\x0f \x01(\x0e\x32\".gitaly.CommitDiffRequest.DiffMode\x12h\n\"max_patch_bytes_for_file_extension\x18\x10 \x03(\x0b\x32<.gitaly.CommitDiffRequest.MaxPatchBytesForFileExtensionEntry\x12G\n\x12whitespace_changes\x18\x11 \x01(\x0e\x32+.gitaly.CommitDiffRequest.WhitespaceChanges\x1a\x44\n\"MaxPatchBytesForFileExtensionEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"%\n\x08\x44iffMode\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x0c\n\x08WORDDIFF\x10\x01\"y\n\x11WhitespaceChanges\x12\"\n\x1eWHITESPACE_CHANGES_UNSPECIFIED\x10\x00\x12\x1d\n\x19WHITESPACE_CHANGES_IGNORE\x10\x01\x12!\n\x1dWHITESPACE_CHANGES_IGNORE_ALL\x10\x02\"\xff\x01\n\x12\x43ommitDiffResponse\x12\x11\n\tfrom_path\x18\x01 \x01(\x0c\x12\x0f\n\x07to_path\x18\x02 \x01(\x0c\x12\x0f\n\x07\x66rom_id\x18\x03 \x01(\t\x12\r\n\x05to_id\x18\x04 \x01(\t\x12\x10\n\x08old_mode\x18\x05 \x01(\x05\x12\x10\n\x08new_mode\x18\x06 \x01(\x05\x12\x0e\n\x06\x62inary\x18\x07 \x01(\x08\x12\x16\n\x0eraw_patch_data\x18\t \x01(\x0c\x12\x14\n\x0c\x65nd_of_patch\x18\n \x01(\x08\x12\x17\n\x0foverflow_marker\x18\x0b \x01(\x08\x12\x11\n\tcollapsed\x18\x0c \x01(\x08\x12\x11\n\ttoo_large\x18\r \x01(\x08J\x04\x08\x08\x10\t\"\x82\x01\n\x12\x43ommitDeltaRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0eleft_commit_id\x18\x02 \x01(\t\x12\x17\n\x0fright_commit_id\x18\x03 \x01(\t\x12\r\n\x05paths\x18\x04 \x03(\x0c\"u\n\x0b\x43ommitDelta\x12\x11\n\tfrom_path\x18\x01 \x01(\x0c\x12\x0f\n\x07to_path\x18\x02 \x01(\x0c\x12\x0f\n\x07\x66rom_id\x18\x03 \x01(\t\x12\r\n\x05to_id\x18\x04 \x01(\t\x12\x10\n\x08old_mode\x18\x05 \x01(\x05\x12\x10\n\x08new_mode\x18\x06 \x01(\x05\":\n\x13\x43ommitDeltaResponse\x12#\n\x06\x64\x65ltas\x18\x01 \x03(\x0b\x32\x13.gitaly.CommitDelta\"o\n\x0eRawDiffRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0eleft_commit_id\x18\x02 \x01(\t\x12\x17\n\x0fright_commit_id\x18\x03 \x01(\t\"\x1f\n\x0fRawDiffResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"p\n\x0fRawPatchRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0eleft_commit_id\x18\x02 \x01(\t\x12\x17\n\x0fright_commit_id\x18\x03 \x01(\t\" \n\x10RawPatchResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"q\n\x10\x44iffStatsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0eleft_commit_id\x18\x02 \x01(\t\x12\x17\n\x0fright_commit_id\x18\x03 \x01(\t\"Q\n\tDiffStats\x12\x0c\n\x04path\x18\x01 \x01(\x0c\x12\x11\n\tadditions\x18\x02 \x01(\x05\x12\x11\n\tdeletions\x18\x03 \x01(\x05\x12\x10\n\x08old_path\x18\x04 \x01(\x0c\"5\n\x11\x44iffStatsResponse\x12 \n\x05stats\x18\x01 \x03(\x0b\x32\x11.gitaly.DiffStats\"\xdc\x03\n\x17\x46indChangedPathsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x13\n\x07\x63ommits\x18\x02 \x03(\tB\x02\x18\x01\x12\x39\n\x08requests\x18\x03 \x03(\x0b\x32\'.gitaly.FindChangedPathsRequest.Request\x1a\xc2\x02\n\x07Request\x12K\n\x0ctree_request\x18\x01 \x01(\x0b\x32\x33.gitaly.FindChangedPathsRequest.Request.TreeRequestH\x00\x12O\n\x0e\x63ommit_request\x18\x02 \x01(\x0b\x32\x35.gitaly.FindChangedPathsRequest.Request.CommitRequestH\x00\x1a\x46\n\x0bTreeRequest\x12\x1a\n\x12left_tree_revision\x18\x01 \x01(\t\x12\x1b\n\x13right_tree_revision\x18\x02 \x01(\t\x1aI\n\rCommitRequest\x12\x17\n\x0f\x63ommit_revision\x18\x01 \x01(\t\x12\x1f\n\x17parent_commit_revisions\x18\x02 \x03(\tB\x06\n\x04type\"?\n\x18\x46indChangedPathsResponse\x12#\n\x05paths\x18\x01 \x03(\x0b\x32\x14.gitaly.ChangedPaths\"\xba\x01\n\x0c\x43hangedPaths\x12\x0c\n\x04path\x18\x01 \x01(\x0c\x12+\n\x06status\x18\x02 \x01(\x0e\x32\x1b.gitaly.ChangedPaths.Status\x12\x10\n\x08old_mode\x18\x03 \x01(\x05\x12\x10\n\x08new_mode\x18\x04 \x01(\x05\"K\n\x06Status\x12\t\n\x05\x41\x44\x44\x45\x44\x10\x00\x12\x0c\n\x08MODIFIED\x10\x01\x12\x0b\n\x07\x44\x45LETED\x10\x02\x12\x0f\n\x0bTYPE_CHANGE\x10\x03\x12\n\n\x06\x43OPIED\x10\x04\"m\n\x11GetPatchIDRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x14\n\x0cold_revision\x18\x02 \x01(\x0c\x12\x14\n\x0cnew_revision\x18\x03 \x01(\x0c\"&\n\x12GetPatchIDResponse\x12\x10\n\x08patch_id\x18\x01 \x01(\t2\xb7\x04\n\x0b\x44iffService\x12M\n\nCommitDiff\x12\x19.gitaly.CommitDiffRequest\x1a\x1a.gitaly.CommitDiffResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12P\n\x0b\x43ommitDelta\x12\x1a.gitaly.CommitDeltaRequest\x1a\x1b.gitaly.CommitDeltaResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x44\n\x07RawDiff\x12\x16.gitaly.RawDiffRequest\x1a\x17.gitaly.RawDiffResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12G\n\x08RawPatch\x12\x17.gitaly.RawPatchRequest\x1a\x18.gitaly.RawPatchResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12J\n\tDiffStats\x12\x18.gitaly.DiffStatsRequest\x1a\x19.gitaly.DiffStatsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12_\n\x10\x46indChangedPaths\x12\x1f.gitaly.FindChangedPathsRequest\x1a .gitaly.FindChangedPathsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12K\n\nGetPatchID\x12\x19.gitaly.GetPatchIDRequest\x1a\x1a.gitaly.GetPatchIDResponse\"\x06\xfa\x97(\x02\x08\x02\x42\x34Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypbb\x06proto3'
   ,
   dependencies=[lint__pb2.DESCRIPTOR,shared__pb2.DESCRIPTOR,])
 
@@ -47,8 +47,8 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=471,
-  serialized_end=508,
+  serialized_start=724,
+  serialized_end=761,
 )
 _sym_db.RegisterEnumDescriptor(_COMMITDIFFREQUEST_DIFFMODE)
 
@@ -52,6 +52,36 @@
 )
 _sym_db.RegisterEnumDescriptor(_COMMITDIFFREQUEST_DIFFMODE)
 
+_COMMITDIFFREQUEST_WHITESPACECHANGES = _descriptor.EnumDescriptor(
+  name='WhitespaceChanges',
+  full_name='gitaly.CommitDiffRequest.WhitespaceChanges',
+  filename=None,
+  file=DESCRIPTOR,
+  create_key=_descriptor._internal_create_key,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='WHITESPACE_CHANGES_UNSPECIFIED', index=0, number=0,
+      serialized_options=None,
+      type=None,
+      create_key=_descriptor._internal_create_key),
+    _descriptor.EnumValueDescriptor(
+      name='WHITESPACE_CHANGES_IGNORE', index=1, number=1,
+      serialized_options=None,
+      type=None,
+      create_key=_descriptor._internal_create_key),
+    _descriptor.EnumValueDescriptor(
+      name='WHITESPACE_CHANGES_IGNORE_ALL', index=2, number=2,
+      serialized_options=None,
+      type=None,
+      create_key=_descriptor._internal_create_key),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=763,
+  serialized_end=884,
+)
+_sym_db.RegisterEnumDescriptor(_COMMITDIFFREQUEST_WHITESPACECHANGES)
+
 _CHANGEDPATHS_STATUS = _descriptor.EnumDescriptor(
   name='Status',
   full_name='gitaly.ChangedPaths.Status',
@@ -87,9 +117,9 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=2283,
-  serialized_end=2358,
+  serialized_start=2659,
+  serialized_end=2734,
 )
 _sym_db.RegisterEnumDescriptor(_CHANGEDPATHS_STATUS)
 
 
@@ -92,7 +122,45 @@
 )
 _sym_db.RegisterEnumDescriptor(_CHANGEDPATHS_STATUS)
 
 
+_COMMITDIFFREQUEST_MAXPATCHBYTESFORFILEEXTENSIONENTRY = _descriptor.Descriptor(
+  name='MaxPatchBytesForFileExtensionEntry',
+  full_name='gitaly.CommitDiffRequest.MaxPatchBytesForFileExtensionEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='gitaly.CommitDiffRequest.MaxPatchBytesForFileExtensionEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='gitaly.CommitDiffRequest.MaxPatchBytesForFileExtensionEntry.value', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=b'8\001',
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=654,
+  serialized_end=722,
+)
+
 _COMMITDIFFREQUEST = _descriptor.Descriptor(
   name='CommitDiffRequest',
   full_name='gitaly.CommitDiffRequest',
@@ -128,7 +196,7 @@
       has_default_value=False, default_value=False,
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
-      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+      serialized_options=b'\030\001', file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
     _descriptor.FieldDescriptor(
       name='paths', full_name='gitaly.CommitDiffRequest.paths', index=4,
       number=5, type=12, cpp_type=9, label=3,
@@ -206,6 +274,20 @@
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='max_patch_bytes_for_file_extension', full_name='gitaly.CommitDiffRequest.max_patch_bytes_for_file_extension', index=15,
+      number=16, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='whitespace_changes', full_name='gitaly.CommitDiffRequest.whitespace_changes', index=16,
+      number=17, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
   ],
   extensions=[
   ],
@@ -209,6 +291,6 @@
   ],
   extensions=[
   ],
-  nested_types=[],
+  nested_types=[_COMMITDIFFREQUEST_MAXPATCHBYTESFORFILEEXTENSIONENTRY, ],
   enum_types=[
     _COMMITDIFFREQUEST_DIFFMODE,
@@ -213,5 +295,6 @@
   enum_types=[
     _COMMITDIFFREQUEST_DIFFMODE,
+    _COMMITDIFFREQUEST_WHITESPACECHANGES,
   ],
   serialized_options=None,
   is_extendable=False,
@@ -220,7 +303,7 @@
   oneofs=[
   ],
   serialized_start=49,
-  serialized_end=508,
+  serialized_end=884,
 )
 
 
@@ -328,8 +411,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=511,
-  serialized_end=766,
+  serialized_start=887,
+  serialized_end=1142,
 )
 
 
@@ -381,8 +464,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=769,
-  serialized_end=899,
+  serialized_start=1145,
+  serialized_end=1275,
 )
 
 
@@ -448,8 +531,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=901,
-  serialized_end=1018,
+  serialized_start=1277,
+  serialized_end=1394,
 )
 
 
@@ -480,8 +563,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1020,
-  serialized_end=1078,
+  serialized_start=1396,
+  serialized_end=1454,
 )
 
 
@@ -526,8 +609,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1080,
-  serialized_end=1191,
+  serialized_start=1456,
+  serialized_end=1567,
 )
 
 
@@ -558,8 +641,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1193,
-  serialized_end=1224,
+  serialized_start=1569,
+  serialized_end=1600,
 )
 
 
@@ -604,8 +687,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1226,
-  serialized_end=1338,
+  serialized_start=1602,
+  serialized_end=1714,
 )
 
 
@@ -636,8 +719,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1340,
-  serialized_end=1372,
+  serialized_start=1716,
+  serialized_end=1748,
 )
 
 
@@ -682,8 +765,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1374,
-  serialized_end=1487,
+  serialized_start=1750,
+  serialized_end=1863,
 )
 
 
@@ -735,8 +818,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1489,
-  serialized_end=1570,
+  serialized_start=1865,
+  serialized_end=1946,
 )
 
 
@@ -767,8 +850,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1572,
-  serialized_end=1625,
+  serialized_start=1948,
+  serialized_end=2001,
 )
 
 
@@ -806,8 +889,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1951,
-  serialized_end=2021,
+  serialized_start=2327,
+  serialized_end=2397,
 )
 
 _FINDCHANGEDPATHSREQUEST_REQUEST_COMMITREQUEST = _descriptor.Descriptor(
@@ -844,8 +927,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2023,
-  serialized_end=2096,
+  serialized_start=2399,
+  serialized_end=2472,
 )
 
 _FINDCHANGEDPATHSREQUEST_REQUEST = _descriptor.Descriptor(
@@ -887,8 +970,8 @@
       create_key=_descriptor._internal_create_key,
     fields=[]),
   ],
-  serialized_start=1782,
-  serialized_end=2104,
+  serialized_start=2158,
+  serialized_end=2480,
 )
 
 _FINDCHANGEDPATHSREQUEST = _descriptor.Descriptor(
@@ -932,8 +1015,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1628,
-  serialized_end=2104,
+  serialized_start=2004,
+  serialized_end=2480,
 )
 
 
@@ -964,8 +1047,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2106,
-  serialized_end=2169,
+  serialized_start=2482,
+  serialized_end=2545,
 )
 
 
@@ -1018,7 +1101,7 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2172,
-  serialized_end=2358,
+  serialized_start=2548,
+  serialized_end=2734,
 )
 
@@ -1023,4 +1106,83 @@
 )
 
+
+_GETPATCHIDREQUEST = _descriptor.Descriptor(
+  name='GetPatchIDRequest',
+  full_name='gitaly.GetPatchIDRequest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='repository', full_name='gitaly.GetPatchIDRequest.repository', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=b'\230\306,\001', file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='old_revision', full_name='gitaly.GetPatchIDRequest.old_revision', index=1,
+      number=2, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='new_revision', full_name='gitaly.GetPatchIDRequest.new_revision', index=2,
+      number=3, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=2736,
+  serialized_end=2845,
+)
+
+
+_GETPATCHIDRESPONSE = _descriptor.Descriptor(
+  name='GetPatchIDResponse',
+  full_name='gitaly.GetPatchIDResponse',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='patch_id', full_name='gitaly.GetPatchIDResponse.patch_id', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=2847,
+  serialized_end=2885,
+)
+
+_COMMITDIFFREQUEST_MAXPATCHBYTESFORFILEEXTENSIONENTRY.containing_type = _COMMITDIFFREQUEST
 _COMMITDIFFREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _COMMITDIFFREQUEST.fields_by_name['diff_mode'].enum_type = _COMMITDIFFREQUEST_DIFFMODE
@@ -1025,3 +1187,5 @@
 _COMMITDIFFREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _COMMITDIFFREQUEST.fields_by_name['diff_mode'].enum_type = _COMMITDIFFREQUEST_DIFFMODE
+_COMMITDIFFREQUEST.fields_by_name['max_patch_bytes_for_file_extension'].message_type = _COMMITDIFFREQUEST_MAXPATCHBYTESFORFILEEXTENSIONENTRY
+_COMMITDIFFREQUEST.fields_by_name['whitespace_changes'].enum_type = _COMMITDIFFREQUEST_WHITESPACECHANGES
 _COMMITDIFFREQUEST_DIFFMODE.containing_type = _COMMITDIFFREQUEST
@@ -1027,4 +1191,5 @@
 _COMMITDIFFREQUEST_DIFFMODE.containing_type = _COMMITDIFFREQUEST
+_COMMITDIFFREQUEST_WHITESPACECHANGES.containing_type = _COMMITDIFFREQUEST
 _COMMITDELTAREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _COMMITDELTARESPONSE.fields_by_name['deltas'].message_type = _COMMITDELTA
 _RAWDIFFREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
@@ -1047,6 +1212,7 @@
 _FINDCHANGEDPATHSRESPONSE.fields_by_name['paths'].message_type = _CHANGEDPATHS
 _CHANGEDPATHS.fields_by_name['status'].enum_type = _CHANGEDPATHS_STATUS
 _CHANGEDPATHS_STATUS.containing_type = _CHANGEDPATHS
+_GETPATCHIDREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 DESCRIPTOR.message_types_by_name['CommitDiffRequest'] = _COMMITDIFFREQUEST
 DESCRIPTOR.message_types_by_name['CommitDiffResponse'] = _COMMITDIFFRESPONSE
 DESCRIPTOR.message_types_by_name['CommitDeltaRequest'] = _COMMITDELTAREQUEST
@@ -1062,6 +1228,8 @@
 DESCRIPTOR.message_types_by_name['FindChangedPathsRequest'] = _FINDCHANGEDPATHSREQUEST
 DESCRIPTOR.message_types_by_name['FindChangedPathsResponse'] = _FINDCHANGEDPATHSRESPONSE
 DESCRIPTOR.message_types_by_name['ChangedPaths'] = _CHANGEDPATHS
+DESCRIPTOR.message_types_by_name['GetPatchIDRequest'] = _GETPATCHIDREQUEST
+DESCRIPTOR.message_types_by_name['GetPatchIDResponse'] = _GETPATCHIDRESPONSE
 _sym_db.RegisterFileDescriptor(DESCRIPTOR)
 
 CommitDiffRequest = _reflection.GeneratedProtocolMessageType('CommitDiffRequest', (_message.Message,), {
@@ -1065,8 +1233,15 @@
 _sym_db.RegisterFileDescriptor(DESCRIPTOR)
 
 CommitDiffRequest = _reflection.GeneratedProtocolMessageType('CommitDiffRequest', (_message.Message,), {
+
+  'MaxPatchBytesForFileExtensionEntry' : _reflection.GeneratedProtocolMessageType('MaxPatchBytesForFileExtensionEntry', (_message.Message,), {
+    'DESCRIPTOR' : _COMMITDIFFREQUEST_MAXPATCHBYTESFORFILEEXTENSIONENTRY,
+    '__module__' : 'diff_pb2'
+    # @@protoc_insertion_point(class_scope:gitaly.CommitDiffRequest.MaxPatchBytesForFileExtensionEntry)
+    })
+  ,
   'DESCRIPTOR' : _COMMITDIFFREQUEST,
   '__module__' : 'diff_pb2'
   # @@protoc_insertion_point(class_scope:gitaly.CommitDiffRequest)
   })
 _sym_db.RegisterMessage(CommitDiffRequest)
@@ -1068,8 +1243,9 @@
   'DESCRIPTOR' : _COMMITDIFFREQUEST,
   '__module__' : 'diff_pb2'
   # @@protoc_insertion_point(class_scope:gitaly.CommitDiffRequest)
   })
 _sym_db.RegisterMessage(CommitDiffRequest)
+_sym_db.RegisterMessage(CommitDiffRequest.MaxPatchBytesForFileExtensionEntry)
 
 CommitDiffResponse = _reflection.GeneratedProtocolMessageType('CommitDiffResponse', (_message.Message,), {
   'DESCRIPTOR' : _COMMITDIFFRESPONSE,
@@ -1193,5 +1369,19 @@
   })
 _sym_db.RegisterMessage(ChangedPaths)
 
+GetPatchIDRequest = _reflection.GeneratedProtocolMessageType('GetPatchIDRequest', (_message.Message,), {
+  'DESCRIPTOR' : _GETPATCHIDREQUEST,
+  '__module__' : 'diff_pb2'
+  # @@protoc_insertion_point(class_scope:gitaly.GetPatchIDRequest)
+  })
+_sym_db.RegisterMessage(GetPatchIDRequest)
+
+GetPatchIDResponse = _reflection.GeneratedProtocolMessageType('GetPatchIDResponse', (_message.Message,), {
+  'DESCRIPTOR' : _GETPATCHIDRESPONSE,
+  '__module__' : 'diff_pb2'
+  # @@protoc_insertion_point(class_scope:gitaly.GetPatchIDResponse)
+  })
+_sym_db.RegisterMessage(GetPatchIDResponse)
+
 
 DESCRIPTOR._options = None
@@ -1196,3 +1386,4 @@
 
 DESCRIPTOR._options = None
+_COMMITDIFFREQUEST_MAXPATCHBYTESFORFILEEXTENSIONENTRY._options = None
 _COMMITDIFFREQUEST.fields_by_name['repository']._options = None
@@ -1198,7 +1389,8 @@
 _COMMITDIFFREQUEST.fields_by_name['repository']._options = None
+_COMMITDIFFREQUEST.fields_by_name['ignore_whitespace_change']._options = None
 _COMMITDELTAREQUEST.fields_by_name['repository']._options = None
 _RAWDIFFREQUEST.fields_by_name['repository']._options = None
 _RAWPATCHREQUEST.fields_by_name['repository']._options = None
 _DIFFSTATSREQUEST.fields_by_name['repository']._options = None
 _FINDCHANGEDPATHSREQUEST.fields_by_name['repository']._options = None
 _FINDCHANGEDPATHSREQUEST.fields_by_name['commits']._options = None
@@ -1199,9 +1391,10 @@
 _COMMITDELTAREQUEST.fields_by_name['repository']._options = None
 _RAWDIFFREQUEST.fields_by_name['repository']._options = None
 _RAWPATCHREQUEST.fields_by_name['repository']._options = None
 _DIFFSTATSREQUEST.fields_by_name['repository']._options = None
 _FINDCHANGEDPATHSREQUEST.fields_by_name['repository']._options = None
 _FINDCHANGEDPATHSREQUEST.fields_by_name['commits']._options = None
+_GETPATCHIDREQUEST.fields_by_name['repository']._options = None
 
 _DIFFSERVICE = _descriptor.ServiceDescriptor(
   name='DiffService',
@@ -1210,8 +1403,8 @@
   index=0,
   serialized_options=None,
   create_key=_descriptor._internal_create_key,
-  serialized_start=2361,
-  serialized_end=2851,
+  serialized_start=2888,
+  serialized_end=3455,
   methods=[
   _descriptor.MethodDescriptor(
     name='CommitDiff',
@@ -1273,6 +1466,16 @@
     serialized_options=b'\372\227(\002\010\002',
     create_key=_descriptor._internal_create_key,
   ),
+  _descriptor.MethodDescriptor(
+    name='GetPatchID',
+    full_name='gitaly.DiffService.GetPatchID',
+    index=6,
+    containing_service=None,
+    input_type=_GETPATCHIDREQUEST,
+    output_type=_GETPATCHIDRESPONSE,
+    serialized_options=b'\372\227(\002\010\002',
+    create_key=_descriptor._internal_create_key,
+  ),
 ])
 _sym_db.RegisterServiceDescriptor(_DIFFSERVICE)
 
diff --git a/hgitaly/stub/diff_pb2_grpc.py b/hgitaly/stub/diff_pb2_grpc.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zdHViL2RpZmZfcGIyX2dycGMucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zdHViL2RpZmZfcGIyX2dycGMucHk= 100644
--- a/hgitaly/stub/diff_pb2_grpc.py
+++ b/hgitaly/stub/diff_pb2_grpc.py
@@ -46,6 +46,11 @@
                 request_serializer=diff__pb2.FindChangedPathsRequest.SerializeToString,
                 response_deserializer=diff__pb2.FindChangedPathsResponse.FromString,
                 )
+        self.GetPatchID = channel.unary_unary(
+                '/gitaly.DiffService/GetPatchID',
+                request_serializer=diff__pb2.GetPatchIDRequest.SerializeToString,
+                response_deserializer=diff__pb2.GetPatchIDResponse.FromString,
+                )
 
 
 class DiffServiceServicer(object):
@@ -95,6 +100,16 @@
         context.set_details('Method not implemented!')
         raise NotImplementedError('Method not implemented!')
 
+    def GetPatchID(self, request, context):
+        """GetPatchID computes a patch ID for a patch. Patch IDs are a unique ID computed by hashing
+        a patch with some parameters like line numbers ignored. The patch ID can thus be used to compare
+        whether diffs make the same change. Please refer to git-patch-id(1) for further information.
+        If the difference between old and new change is empty then this RPC returns an error.
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
+
 
 def add_DiffServiceServicer_to_server(servicer, server):
     rpc_method_handlers = {
@@ -128,6 +143,11 @@
                     request_deserializer=diff__pb2.FindChangedPathsRequest.FromString,
                     response_serializer=diff__pb2.FindChangedPathsResponse.SerializeToString,
             ),
+            'GetPatchID': grpc.unary_unary_rpc_method_handler(
+                    servicer.GetPatchID,
+                    request_deserializer=diff__pb2.GetPatchIDRequest.FromString,
+                    response_serializer=diff__pb2.GetPatchIDResponse.SerializeToString,
+            ),
     }
     generic_handler = grpc.method_handlers_generic_handler(
             'gitaly.DiffService', rpc_method_handlers)
@@ -241,3 +261,20 @@
             diff__pb2.FindChangedPathsResponse.FromString,
             options, channel_credentials,
             insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
+    def GetPatchID(request,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.unary_unary(request, target, '/gitaly.DiffService/GetPatchID',
+            diff__pb2.GetPatchIDRequest.SerializeToString,
+            diff__pb2.GetPatchIDResponse.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
diff --git a/hgitaly/stub/mercurial_operations_pb2.py b/hgitaly/stub/mercurial_operations_pb2.py
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zdHViL21lcmN1cmlhbF9vcGVyYXRpb25zX3BiMi5weQ==
--- /dev/null
+++ b/hgitaly/stub/mercurial_operations_pb2.py
@@ -0,0 +1,232 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: mercurial-operations.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from . import lint_pb2 as lint__pb2
+from . import shared_pb2 as shared__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='mercurial-operations.proto',
+  package='hgitaly',
+  syntax='proto3',
+  serialized_options=None,
+  create_key=_descriptor._internal_create_key,
+  serialized_pb=b'\n\x1amercurial-operations.proto\x12\x07hgitaly\x1a\nlint.proto\x1a\x0cshared.proto\"\x94\x01\n\x14MergeAnalysisRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x17\n\x0fsource_revision\x18\x02 \x01(\x0c\x12\x17\n\x0ftarget_revision\x18\x03 \x01(\x0c\x12\x1c\n\x14skip_conflicts_check\x18\x04 \x01(\x08\"\xad\x02\n\x15MergeAnalysisResponse\x12\x17\n\x0fis_fast_forward\x18\x01 \x01(\x08\x12\x1f\n\x17has_obsolete_changesets\x18\x02 \x01(\x08\x12\x1f\n\x17has_unstable_changesets\x18\x03 \x01(\x08\x12\x15\n\rhas_conflicts\x18\x04 \x01(\x08\x12\x18\n\x10target_is_public\x18\x05 \x01(\x08\x12\x16\n\x0etarget_node_id\x18\x06 \x01(\t\x12\x15\n\rtarget_branch\x18\x07 \x01(\x0c\x12\x14\n\x0ctarget_topic\x18\x08 \x01(\x0c\x12\x16\n\x0esource_node_id\x18\t \x01(\t\x12\x15\n\rsource_branch\x18\n \x01(\x0c\x12\x14\n\x0csource_topic\x18\x0b \x01(\x0c\x32t\n\x1aMercurialOperationsService\x12V\n\rMergeAnalysis\x12\x1d.hgitaly.MergeAnalysisRequest\x1a\x1e.hgitaly.MergeAnalysisResponse\"\x06\xfa\x97(\x02\x08\x02\x62\x06proto3'
+  ,
+  dependencies=[lint__pb2.DESCRIPTOR,shared__pb2.DESCRIPTOR,])
+
+
+
+
+_MERGEANALYSISREQUEST = _descriptor.Descriptor(
+  name='MergeAnalysisRequest',
+  full_name='hgitaly.MergeAnalysisRequest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='repository', full_name='hgitaly.MergeAnalysisRequest.repository', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=b'\230\306,\001', file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='source_revision', full_name='hgitaly.MergeAnalysisRequest.source_revision', index=1,
+      number=2, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='target_revision', full_name='hgitaly.MergeAnalysisRequest.target_revision', index=2,
+      number=3, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='skip_conflicts_check', full_name='hgitaly.MergeAnalysisRequest.skip_conflicts_check', index=3,
+      number=4, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=66,
+  serialized_end=214,
+)
+
+
+_MERGEANALYSISRESPONSE = _descriptor.Descriptor(
+  name='MergeAnalysisResponse',
+  full_name='hgitaly.MergeAnalysisResponse',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='is_fast_forward', full_name='hgitaly.MergeAnalysisResponse.is_fast_forward', index=0,
+      number=1, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='has_obsolete_changesets', full_name='hgitaly.MergeAnalysisResponse.has_obsolete_changesets', index=1,
+      number=2, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='has_unstable_changesets', full_name='hgitaly.MergeAnalysisResponse.has_unstable_changesets', index=2,
+      number=3, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='has_conflicts', full_name='hgitaly.MergeAnalysisResponse.has_conflicts', index=3,
+      number=4, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='target_is_public', full_name='hgitaly.MergeAnalysisResponse.target_is_public', index=4,
+      number=5, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='target_node_id', full_name='hgitaly.MergeAnalysisResponse.target_node_id', index=5,
+      number=6, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='target_branch', full_name='hgitaly.MergeAnalysisResponse.target_branch', index=6,
+      number=7, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='target_topic', full_name='hgitaly.MergeAnalysisResponse.target_topic', index=7,
+      number=8, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='source_node_id', full_name='hgitaly.MergeAnalysisResponse.source_node_id', index=8,
+      number=9, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='source_branch', full_name='hgitaly.MergeAnalysisResponse.source_branch', index=9,
+      number=10, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='source_topic', full_name='hgitaly.MergeAnalysisResponse.source_topic', index=10,
+      number=11, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=217,
+  serialized_end=518,
+)
+
+_MERGEANALYSISREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
+DESCRIPTOR.message_types_by_name['MergeAnalysisRequest'] = _MERGEANALYSISREQUEST
+DESCRIPTOR.message_types_by_name['MergeAnalysisResponse'] = _MERGEANALYSISRESPONSE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+MergeAnalysisRequest = _reflection.GeneratedProtocolMessageType('MergeAnalysisRequest', (_message.Message,), {
+  'DESCRIPTOR' : _MERGEANALYSISREQUEST,
+  '__module__' : 'mercurial_operations_pb2'
+  # @@protoc_insertion_point(class_scope:hgitaly.MergeAnalysisRequest)
+  })
+_sym_db.RegisterMessage(MergeAnalysisRequest)
+
+MergeAnalysisResponse = _reflection.GeneratedProtocolMessageType('MergeAnalysisResponse', (_message.Message,), {
+  'DESCRIPTOR' : _MERGEANALYSISRESPONSE,
+  '__module__' : 'mercurial_operations_pb2'
+  # @@protoc_insertion_point(class_scope:hgitaly.MergeAnalysisResponse)
+  })
+_sym_db.RegisterMessage(MergeAnalysisResponse)
+
+
+_MERGEANALYSISREQUEST.fields_by_name['repository']._options = None
+
+_MERCURIALOPERATIONSSERVICE = _descriptor.ServiceDescriptor(
+  name='MercurialOperationsService',
+  full_name='hgitaly.MercurialOperationsService',
+  file=DESCRIPTOR,
+  index=0,
+  serialized_options=None,
+  create_key=_descriptor._internal_create_key,
+  serialized_start=520,
+  serialized_end=636,
+  methods=[
+  _descriptor.MethodDescriptor(
+    name='MergeAnalysis',
+    full_name='hgitaly.MercurialOperationsService.MergeAnalysis',
+    index=0,
+    containing_service=None,
+    input_type=_MERGEANALYSISREQUEST,
+    output_type=_MERGEANALYSISRESPONSE,
+    serialized_options=b'\372\227(\002\010\002',
+    create_key=_descriptor._internal_create_key,
+  ),
+])
+_sym_db.RegisterServiceDescriptor(_MERCURIALOPERATIONSSERVICE)
+
+DESCRIPTOR.services_by_name['MercurialOperationsService'] = _MERCURIALOPERATIONSSERVICE
+
+# @@protoc_insertion_point(module_scope)
diff --git a/hgitaly/stub/mercurial_operations_pb2_grpc.py b/hgitaly/stub/mercurial_operations_pb2_grpc.py
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zdHViL21lcmN1cmlhbF9vcGVyYXRpb25zX3BiMl9ncnBjLnB5
--- /dev/null
+++ b/hgitaly/stub/mercurial_operations_pb2_grpc.py
@@ -0,0 +1,69 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+
+from . import mercurial_operations_pb2 as mercurial__operations__pb2
+
+
+class MercurialOperationsServiceStub(object):
+    """Missing associated documentation comment in .proto file."""
+
+    def __init__(self, channel):
+        """Constructor.
+
+        Args:
+            channel: A grpc.Channel.
+        """
+        self.MergeAnalysis = channel.unary_unary(
+                '/hgitaly.MercurialOperationsService/MergeAnalysis',
+                request_serializer=mercurial__operations__pb2.MergeAnalysisRequest.SerializeToString,
+                response_deserializer=mercurial__operations__pb2.MergeAnalysisResponse.FromString,
+                )
+
+
+class MercurialOperationsServiceServicer(object):
+    """Missing associated documentation comment in .proto file."""
+
+    def MergeAnalysis(self, request, context):
+        """Provide all information for a possible merge
+
+        Will be used in mergeability checks and last-minute checks if needed.
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
+
+
+def add_MercurialOperationsServiceServicer_to_server(servicer, server):
+    rpc_method_handlers = {
+            'MergeAnalysis': grpc.unary_unary_rpc_method_handler(
+                    servicer.MergeAnalysis,
+                    request_deserializer=mercurial__operations__pb2.MergeAnalysisRequest.FromString,
+                    response_serializer=mercurial__operations__pb2.MergeAnalysisResponse.SerializeToString,
+            ),
+    }
+    generic_handler = grpc.method_handlers_generic_handler(
+            'hgitaly.MercurialOperationsService', rpc_method_handlers)
+    server.add_generic_rpc_handlers((generic_handler,))
+
+
+ # This class is part of an EXPERIMENTAL API.
+class MercurialOperationsService(object):
+    """Missing associated documentation comment in .proto file."""
+
+    @staticmethod
+    def MergeAnalysis(request,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.unary_unary(request, target, '/hgitaly.MercurialOperationsService/MergeAnalysis',
+            mercurial__operations__pb2.MergeAnalysisRequest.SerializeToString,
+            mercurial__operations__pb2.MergeAnalysisResponse.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
diff --git a/hgitaly/stub/ref_pb2.py b/hgitaly/stub/ref_pb2.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zdHViL3JlZl9wYjIucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zdHViL3JlZl9wYjIucHk= 100644
--- a/hgitaly/stub/ref_pb2.py
+++ b/hgitaly/stub/ref_pb2.py
@@ -23,7 +23,7 @@
   syntax='proto3',
   serialized_options=b'Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb',
   create_key=_descriptor._internal_create_key,
-  serialized_pb=b'\n\tref.proto\x12\x06gitaly\x1a\x0c\x65rrors.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\nlint.proto\x1a\x0cshared.proto\"L\n\x1c\x46indDefaultBranchNameRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"-\n\x1d\x46indDefaultBranchNameResponse\x12\x0c\n\x04name\x18\x01 \x01(\x0c\"I\n\x19\x46indAllBranchNamesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"+\n\x1a\x46indAllBranchNamesResponse\x12\r\n\x05names\x18\x01 \x03(\x0c\"F\n\x16\x46indAllTagNamesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"(\n\x17\x46indAllTagNamesResponse\x12\r\n\x05names\x18\x01 \x03(\x0c\"\xf1\x01\n\x18\x46indLocalBranchesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x38\n\x07sort_by\x18\x02 \x01(\x0e\x32\'.gitaly.FindLocalBranchesRequest.SortBy\x12\x36\n\x11pagination_params\x18\x03 \x01(\x0b\x32\x1b.gitaly.PaginationParameter\"5\n\x06SortBy\x12\x08\n\x04NAME\x10\x00\x12\x0f\n\x0bUPDATED_ASC\x10\x01\x12\x10\n\x0cUPDATED_DESC\x10\x02\"v\n\x19\x46indLocalBranchesResponse\x12\x31\n\x08\x62ranches\x18\x01 \x03(\x0b\x32\x1f.gitaly.FindLocalBranchResponse\x12&\n\x0elocal_branches\x18\x02 \x03(\x0b\x32\x0e.gitaly.Branch\"\xf0\x01\n\x17\x46indLocalBranchResponse\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\x11\n\tcommit_id\x18\x02 \x01(\t\x12\x16\n\x0e\x63ommit_subject\x18\x03 \x01(\x0c\x12:\n\rcommit_author\x18\x04 \x01(\x0b\x32#.gitaly.FindLocalBranchCommitAuthor\x12=\n\x10\x63ommit_committer\x18\x05 \x01(\x0b\x32#.gitaly.FindLocalBranchCommitAuthor\x12!\n\x06\x63ommit\x18\x06 \x01(\x0b\x32\x11.gitaly.GitCommit\"v\n\x1b\x46indLocalBranchCommitAuthor\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\r\n\x05\x65mail\x18\x02 \x01(\x0c\x12(\n\x04\x64\x61te\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x10\n\x08timezone\x18\x04 \x01(\x0c\"t\n\x16\x46indAllBranchesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x13\n\x0bmerged_only\x18\x02 \x01(\x08\x12\x17\n\x0fmerged_branches\x18\x03 \x03(\x0c\"\x8e\x01\n\x17\x46indAllBranchesResponse\x12\x38\n\x08\x62ranches\x18\x01 \x03(\x0b\x32&.gitaly.FindAllBranchesResponse.Branch\x1a\x39\n\x06\x42ranch\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12!\n\x06target\x18\x02 \x01(\x0b\x32\x11.gitaly.GitCommit\"P\n\x0e\x46indTagRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08tag_name\x18\x02 \x01(\x0c\"+\n\x0f\x46indTagResponse\x12\x18\n\x03tag\x18\x01 \x01(\x0b\x32\x0b.gitaly.Tag\"P\n\x0c\x46indTagError\x12\x37\n\rtag_not_found\x18\x01 \x01(\x0b\x32\x1e.gitaly.ReferenceNotFoundErrorH\x00\x42\x07\n\x05\x65rror\"\xd1\x02\n\x12\x46indAllTagsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x32\n\x07sort_by\x18\x02 \x01(\x0b\x32!.gitaly.FindAllTagsRequest.SortBy\x12\x36\n\x11pagination_params\x18\x03 \x01(\x0b\x32\x1b.gitaly.PaginationParameter\x1a\xa0\x01\n\x06SortBy\x12\x32\n\x03key\x18\x01 \x01(\x0e\x32%.gitaly.FindAllTagsRequest.SortBy.Key\x12(\n\tdirection\x18\x02 \x01(\x0e\x32\x15.gitaly.SortDirection\"8\n\x03Key\x12\x0b\n\x07REFNAME\x10\x00\x12\x0f\n\x0b\x43REATORDATE\x10\x01\x12\x13\n\x0fVERSION_REFNAME\x10\x02\"0\n\x13\x46indAllTagsResponse\x12\x19\n\x04tags\x18\x01 \x03(\x0b\x32\x0b.gitaly.Tag\"M\n\x10RefExistsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0b\n\x03ref\x18\x02 \x01(\x0c\"\"\n\x11RefExistsResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"f\n\x13\x43reateBranchRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04name\x18\x02 \x01(\x0c\x12\x13\n\x0bstart_point\x18\x03 \x01(\x0c\"\xbb\x01\n\x14\x43reateBranchResponse\x12\x33\n\x06status\x18\x01 \x01(\x0e\x32#.gitaly.CreateBranchResponse.Status\x12\x1e\n\x06\x62ranch\x18\x02 \x01(\x0b\x32\x0e.gitaly.Branch\"N\n\x06Status\x12\x06\n\x02OK\x10\x00\x12\x0e\n\nERR_EXISTS\x10\x01\x12\x0f\n\x0b\x45RR_INVALID\x10\x02\x12\x1b\n\x17\x45RR_INVALID_START_POINT\x10\x03\"Q\n\x13\x44\x65leteBranchRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04name\x18\x02 \x01(\x0c\"\x16\n\x14\x44\x65leteBranchResponse\"O\n\x11\x46indBranchRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04name\x18\x02 \x01(\x0c\"4\n\x12\x46indBranchResponse\x12\x1e\n\x06\x62ranch\x18\x01 \x01(\x0b\x32\x0e.gitaly.Branch\"k\n\x11\x44\x65leteRefsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x1a\n\x12\x65xcept_with_prefix\x18\x02 \x03(\x0c\x12\x0c\n\x04refs\x18\x03 \x03(\x0c\"\'\n\x12\x44\x65leteRefsResponse\x12\x11\n\tgit_error\x18\x01 \x01(\t\"\x8f\x01\n\x0f\x44\x65leteRefsError\x12\x37\n\x0einvalid_format\x18\x01 \x01(\x0b\x32\x1d.gitaly.InvalidRefFormatErrorH\x00\x12:\n\x11references_locked\x18\x02 \x01(\x0b\x32\x1d.gitaly.ReferencesLockedErrorH\x00\x42\x07\n\x05\x65rror\"x\n&ListBranchNamesContainingCommitRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\tcommit_id\x18\x02 \x01(\t\x12\r\n\x05limit\x18\x03 \x01(\r\"E\n\'ListBranchNamesContainingCommitResponse\x12\x14\n\x0c\x62ranch_names\x18\x02 \x03(\x0cJ\x04\x08\x01\x10\x02\"u\n#ListTagNamesContainingCommitRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\tcommit_id\x18\x02 \x01(\t\x12\r\n\x05limit\x18\x03 \x01(\r\"?\n$ListTagNamesContainingCommitResponse\x12\x11\n\ttag_names\x18\x02 \x03(\x0cJ\x04\x08\x01\x10\x02\"^\n\x17GetTagSignaturesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x15\n\rtag_revisions\x18\x02 \x03(\t\"\xa1\x01\n\x18GetTagSignaturesResponse\x12\x41\n\nsignatures\x18\x01 \x03(\x0b\x32-.gitaly.GetTagSignaturesResponse.TagSignature\x1a\x42\n\x0cTagSignature\x12\x0e\n\x06tag_id\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\x0c\"g\n\x15GetTagMessagesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0f\n\x07tag_ids\x18\x03 \x03(\tJ\x04\x08\x02\x10\x03R\ttag_names\"I\n\x16GetTagMessagesResponse\x12\x0f\n\x07message\x18\x02 \x01(\x0c\x12\x0e\n\x06tag_id\x18\x03 \x01(\tJ\x04\x08\x01\x10\x02R\x08tag_name\"a\n\x1c\x46indAllRemoteBranchesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x13\n\x0bremote_name\x18\x02 \x01(\t\"A\n\x1d\x46indAllRemoteBranchesResponse\x12 \n\x08\x62ranches\x18\x01 \x03(\x0b\x32\x0e.gitaly.Branch\"O\n\x0fPackRefsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01J\x04\x08\x02\x10\x03R\x08\x61ll_refs\"\x12\n\x10PackRefsResponse\"\xbe\x02\n\x0fListRefsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08patterns\x18\x02 \x03(\x0c\x12\x0c\n\x04head\x18\x03 \x01(\x08\x12/\n\x07sort_by\x18\x04 \x01(\x0b\x32\x1e.gitaly.ListRefsRequest.SortBy\x1a\xab\x01\n\x06SortBy\x12/\n\x03key\x18\x01 \x01(\x0e\x32\".gitaly.ListRefsRequest.SortBy.Key\x12(\n\tdirection\x18\x02 \x01(\x0e\x32\x15.gitaly.SortDirection\"F\n\x03Key\x12\x0b\n\x07REFNAME\x10\x00\x12\x0f\n\x0b\x43REATORDATE\x10\x01\x12\x0e\n\nAUTHORDATE\x10\x02\x12\x11\n\rCOMMITTERDATE\x10\x03\"u\n\x10ListRefsResponse\x12\x36\n\nreferences\x18\x01 \x03(\x0b\x32\".gitaly.ListRefsResponse.Reference\x1a)\n\tReference\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\x0e\n\x06target\x18\x02 \x01(\t\"\x8a\x01\n\x14\x46indRefsByOIDRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0b\n\x03oid\x18\x02 \x01(\t\x12\x14\n\x0cref_patterns\x18\x03 \x03(\t\x12\x12\n\nsort_field\x18\x04 \x01(\t\x12\r\n\x05limit\x18\x05 \x01(\r\"%\n\x15\x46indRefsByOIDResponse\x12\x0c\n\x04refs\x18\x01 \x03(\t2\xa5\r\n\nRefService\x12l\n\x15\x46indDefaultBranchName\x12$.gitaly.FindDefaultBranchNameRequest\x1a%.gitaly.FindDefaultBranchNameResponse\"\x06\xfa\x97(\x02\x08\x02\x12\x65\n\x12\x46indAllBranchNames\x12!.gitaly.FindAllBranchNamesRequest\x1a\".gitaly.FindAllBranchNamesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\\\n\x0f\x46indAllTagNames\x12\x1e.gitaly.FindAllTagNamesRequest\x1a\x1f.gitaly.FindAllTagNamesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x62\n\x11\x46indLocalBranches\x12 .gitaly.FindLocalBranchesRequest\x1a!.gitaly.FindLocalBranchesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\\\n\x0f\x46indAllBranches\x12\x1e.gitaly.FindAllBranchesRequest\x1a\x1f.gitaly.FindAllBranchesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12P\n\x0b\x46indAllTags\x12\x1a.gitaly.FindAllTagsRequest\x1a\x1b.gitaly.FindAllTagsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x42\n\x07\x46indTag\x12\x16.gitaly.FindTagRequest\x1a\x17.gitaly.FindTagResponse\"\x06\xfa\x97(\x02\x08\x02\x12n\n\x15\x46indAllRemoteBranches\x12$.gitaly.FindAllRemoteBranchesRequest\x1a%.gitaly.FindAllRemoteBranchesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12H\n\tRefExists\x12\x18.gitaly.RefExistsRequest\x1a\x19.gitaly.RefExistsResponse\"\x06\xfa\x97(\x02\x08\x02\x12K\n\nFindBranch\x12\x19.gitaly.FindBranchRequest\x1a\x1a.gitaly.FindBranchResponse\"\x06\xfa\x97(\x02\x08\x02\x12K\n\nDeleteRefs\x12\x19.gitaly.DeleteRefsRequest\x1a\x1a.gitaly.DeleteRefsResponse\"\x06\xfa\x97(\x02\x08\x01\x12\x8c\x01\n\x1fListBranchNamesContainingCommit\x12..gitaly.ListBranchNamesContainingCommitRequest\x1a/.gitaly.ListBranchNamesContainingCommitResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x83\x01\n\x1cListTagNamesContainingCommit\x12+.gitaly.ListTagNamesContainingCommitRequest\x1a,.gitaly.ListTagNamesContainingCommitResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12_\n\x10GetTagSignatures\x12\x1f.gitaly.GetTagSignaturesRequest\x1a .gitaly.GetTagSignaturesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12Y\n\x0eGetTagMessages\x12\x1d.gitaly.GetTagMessagesRequest\x1a\x1e.gitaly.GetTagMessagesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12H\n\x08PackRefs\x12\x17.gitaly.PackRefsRequest\x1a\x18.gitaly.PackRefsResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12G\n\x08ListRefs\x12\x17.gitaly.ListRefsRequest\x1a\x18.gitaly.ListRefsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12T\n\rFindRefsByOID\x12\x1c.gitaly.FindRefsByOIDRequest\x1a\x1d.gitaly.FindRefsByOIDResponse\"\x06\xfa\x97(\x02\x08\x02\x42\x34Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypbb\x06proto3'
+  serialized_pb=b'\n\tref.proto\x12\x06gitaly\x1a\x0c\x65rrors.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\nlint.proto\x1a\x0cshared.proto\"L\n\x1c\x46indDefaultBranchNameRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"-\n\x1d\x46indDefaultBranchNameResponse\x12\x0c\n\x04name\x18\x01 \x01(\x0c\"I\n\x19\x46indAllBranchNamesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"+\n\x1a\x46indAllBranchNamesResponse\x12\r\n\x05names\x18\x01 \x03(\x0c\"F\n\x16\x46indAllTagNamesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"(\n\x17\x46indAllTagNamesResponse\x12\r\n\x05names\x18\x01 \x03(\x0c\"\xf1\x01\n\x18\x46indLocalBranchesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x38\n\x07sort_by\x18\x02 \x01(\x0e\x32\'.gitaly.FindLocalBranchesRequest.SortBy\x12\x36\n\x11pagination_params\x18\x03 \x01(\x0b\x32\x1b.gitaly.PaginationParameter\"5\n\x06SortBy\x12\x08\n\x04NAME\x10\x00\x12\x0f\n\x0bUPDATED_ASC\x10\x01\x12\x10\n\x0cUPDATED_DESC\x10\x02\"v\n\x19\x46indLocalBranchesResponse\x12\x31\n\x08\x62ranches\x18\x01 \x03(\x0b\x32\x1f.gitaly.FindLocalBranchResponse\x12&\n\x0elocal_branches\x18\x02 \x03(\x0b\x32\x0e.gitaly.Branch\"\xf0\x01\n\x17\x46indLocalBranchResponse\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\x11\n\tcommit_id\x18\x02 \x01(\t\x12\x16\n\x0e\x63ommit_subject\x18\x03 \x01(\x0c\x12:\n\rcommit_author\x18\x04 \x01(\x0b\x32#.gitaly.FindLocalBranchCommitAuthor\x12=\n\x10\x63ommit_committer\x18\x05 \x01(\x0b\x32#.gitaly.FindLocalBranchCommitAuthor\x12!\n\x06\x63ommit\x18\x06 \x01(\x0b\x32\x11.gitaly.GitCommit\"v\n\x1b\x46indLocalBranchCommitAuthor\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\r\n\x05\x65mail\x18\x02 \x01(\x0c\x12(\n\x04\x64\x61te\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x10\n\x08timezone\x18\x04 \x01(\x0c\"t\n\x16\x46indAllBranchesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x13\n\x0bmerged_only\x18\x02 \x01(\x08\x12\x17\n\x0fmerged_branches\x18\x03 \x03(\x0c\"\x8e\x01\n\x17\x46indAllBranchesResponse\x12\x38\n\x08\x62ranches\x18\x01 \x03(\x0b\x32&.gitaly.FindAllBranchesResponse.Branch\x1a\x39\n\x06\x42ranch\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12!\n\x06target\x18\x02 \x01(\x0b\x32\x11.gitaly.GitCommit\"P\n\x0e\x46indTagRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08tag_name\x18\x02 \x01(\x0c\"+\n\x0f\x46indTagResponse\x12\x18\n\x03tag\x18\x01 \x01(\x0b\x32\x0b.gitaly.Tag\"P\n\x0c\x46indTagError\x12\x37\n\rtag_not_found\x18\x01 \x01(\x0b\x32\x1e.gitaly.ReferenceNotFoundErrorH\x00\x42\x07\n\x05\x65rror\"\xd1\x02\n\x12\x46indAllTagsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x32\n\x07sort_by\x18\x02 \x01(\x0b\x32!.gitaly.FindAllTagsRequest.SortBy\x12\x36\n\x11pagination_params\x18\x03 \x01(\x0b\x32\x1b.gitaly.PaginationParameter\x1a\xa0\x01\n\x06SortBy\x12\x32\n\x03key\x18\x01 \x01(\x0e\x32%.gitaly.FindAllTagsRequest.SortBy.Key\x12(\n\tdirection\x18\x02 \x01(\x0e\x32\x15.gitaly.SortDirection\"8\n\x03Key\x12\x0b\n\x07REFNAME\x10\x00\x12\x0f\n\x0b\x43REATORDATE\x10\x01\x12\x13\n\x0fVERSION_REFNAME\x10\x02\"0\n\x13\x46indAllTagsResponse\x12\x19\n\x04tags\x18\x01 \x03(\x0b\x32\x0b.gitaly.Tag\"M\n\x10RefExistsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0b\n\x03ref\x18\x02 \x01(\x0c\"\"\n\x11RefExistsResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"f\n\x13\x43reateBranchRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04name\x18\x02 \x01(\x0c\x12\x13\n\x0bstart_point\x18\x03 \x01(\x0c\"\xbb\x01\n\x14\x43reateBranchResponse\x12\x33\n\x06status\x18\x01 \x01(\x0e\x32#.gitaly.CreateBranchResponse.Status\x12\x1e\n\x06\x62ranch\x18\x02 \x01(\x0b\x32\x0e.gitaly.Branch\"N\n\x06Status\x12\x06\n\x02OK\x10\x00\x12\x0e\n\nERR_EXISTS\x10\x01\x12\x0f\n\x0b\x45RR_INVALID\x10\x02\x12\x1b\n\x17\x45RR_INVALID_START_POINT\x10\x03\"Q\n\x13\x44\x65leteBranchRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04name\x18\x02 \x01(\x0c\"\x16\n\x14\x44\x65leteBranchResponse\"O\n\x11\x46indBranchRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04name\x18\x02 \x01(\x0c\"4\n\x12\x46indBranchResponse\x12\x1e\n\x06\x62ranch\x18\x01 \x01(\x0b\x32\x0e.gitaly.Branch\"k\n\x11\x44\x65leteRefsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x1a\n\x12\x65xcept_with_prefix\x18\x02 \x03(\x0c\x12\x0c\n\x04refs\x18\x03 \x03(\x0c\"\'\n\x12\x44\x65leteRefsResponse\x12\x11\n\tgit_error\x18\x01 \x01(\t\"\x8f\x01\n\x0f\x44\x65leteRefsError\x12\x37\n\x0einvalid_format\x18\x01 \x01(\x0b\x32\x1d.gitaly.InvalidRefFormatErrorH\x00\x12:\n\x11references_locked\x18\x02 \x01(\x0b\x32\x1d.gitaly.ReferencesLockedErrorH\x00\x42\x07\n\x05\x65rror\"x\n&ListBranchNamesContainingCommitRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\tcommit_id\x18\x02 \x01(\t\x12\r\n\x05limit\x18\x03 \x01(\r\"E\n\'ListBranchNamesContainingCommitResponse\x12\x14\n\x0c\x62ranch_names\x18\x02 \x03(\x0cJ\x04\x08\x01\x10\x02\"u\n#ListTagNamesContainingCommitRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\tcommit_id\x18\x02 \x01(\t\x12\r\n\x05limit\x18\x03 \x01(\r\"?\n$ListTagNamesContainingCommitResponse\x12\x11\n\ttag_names\x18\x02 \x03(\x0cJ\x04\x08\x01\x10\x02\"^\n\x17GetTagSignaturesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x15\n\rtag_revisions\x18\x02 \x03(\t\"\xa1\x01\n\x18GetTagSignaturesResponse\x12\x41\n\nsignatures\x18\x01 \x03(\x0b\x32-.gitaly.GetTagSignaturesResponse.TagSignature\x1a\x42\n\x0cTagSignature\x12\x0e\n\x06tag_id\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\x0c\"g\n\x15GetTagMessagesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0f\n\x07tag_ids\x18\x03 \x03(\tJ\x04\x08\x02\x10\x03R\ttag_names\"I\n\x16GetTagMessagesResponse\x12\x0f\n\x07message\x18\x02 \x01(\x0c\x12\x0e\n\x06tag_id\x18\x03 \x01(\tJ\x04\x08\x01\x10\x02R\x08tag_name\"a\n\x1c\x46indAllRemoteBranchesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x13\n\x0bremote_name\x18\x02 \x01(\t\"A\n\x1d\x46indAllRemoteBranchesResponse\x12 \n\x08\x62ranches\x18\x01 \x03(\x0b\x32\x0e.gitaly.Branch\"O\n\x0fPackRefsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01J\x04\x08\x02\x10\x03R\x08\x61ll_refs\"\x12\n\x10PackRefsResponse\"\xeb\x02\n\x0fListRefsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08patterns\x18\x02 \x03(\x0c\x12\x0c\n\x04head\x18\x03 \x01(\x08\x12/\n\x07sort_by\x18\x04 \x01(\x0b\x32\x1e.gitaly.ListRefsRequest.SortBy\x12\x18\n\x10pointing_at_oids\x18\x05 \x03(\x0c\x12\x11\n\tpeel_tags\x18\x06 \x01(\x08\x1a\xab\x01\n\x06SortBy\x12/\n\x03key\x18\x01 \x01(\x0e\x32\".gitaly.ListRefsRequest.SortBy.Key\x12(\n\tdirection\x18\x02 \x01(\x0e\x32\x15.gitaly.SortDirection\"F\n\x03Key\x12\x0b\n\x07REFNAME\x10\x00\x12\x0f\n\x0b\x43REATORDATE\x10\x01\x12\x0e\n\nAUTHORDATE\x10\x02\x12\x11\n\rCOMMITTERDATE\x10\x03\"\x8c\x01\n\x10ListRefsResponse\x12\x36\n\nreferences\x18\x01 \x03(\x0b\x32\".gitaly.ListRefsResponse.Reference\x1a@\n\tReference\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\x0e\n\x06target\x18\x02 \x01(\t\x12\x15\n\rpeeled_target\x18\x03 \x01(\t\"\x8a\x01\n\x14\x46indRefsByOIDRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0b\n\x03oid\x18\x02 \x01(\t\x12\x14\n\x0cref_patterns\x18\x03 \x03(\t\x12\x12\n\nsort_field\x18\x04 \x01(\t\x12\r\n\x05limit\x18\x05 \x01(\r\"%\n\x15\x46indRefsByOIDResponse\x12\x0c\n\x04refs\x18\x01 \x03(\t2\xab\r\n\nRefService\x12l\n\x15\x46indDefaultBranchName\x12$.gitaly.FindDefaultBranchNameRequest\x1a%.gitaly.FindDefaultBranchNameResponse\"\x06\xfa\x97(\x02\x08\x02\x12h\n\x12\x46indAllBranchNames\x12!.gitaly.FindAllBranchNamesRequest\x1a\".gitaly.FindAllBranchNamesResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x02\x30\x01\x12_\n\x0f\x46indAllTagNames\x12\x1e.gitaly.FindAllTagNamesRequest\x1a\x1f.gitaly.FindAllTagNamesResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x02\x30\x01\x12\x62\n\x11\x46indLocalBranches\x12 .gitaly.FindLocalBranchesRequest\x1a!.gitaly.FindLocalBranchesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\\\n\x0f\x46indAllBranches\x12\x1e.gitaly.FindAllBranchesRequest\x1a\x1f.gitaly.FindAllBranchesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12P\n\x0b\x46indAllTags\x12\x1a.gitaly.FindAllTagsRequest\x1a\x1b.gitaly.FindAllTagsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x42\n\x07\x46indTag\x12\x16.gitaly.FindTagRequest\x1a\x17.gitaly.FindTagResponse\"\x06\xfa\x97(\x02\x08\x02\x12n\n\x15\x46indAllRemoteBranches\x12$.gitaly.FindAllRemoteBranchesRequest\x1a%.gitaly.FindAllRemoteBranchesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12H\n\tRefExists\x12\x18.gitaly.RefExistsRequest\x1a\x19.gitaly.RefExistsResponse\"\x06\xfa\x97(\x02\x08\x02\x12K\n\nFindBranch\x12\x19.gitaly.FindBranchRequest\x1a\x1a.gitaly.FindBranchResponse\"\x06\xfa\x97(\x02\x08\x02\x12K\n\nDeleteRefs\x12\x19.gitaly.DeleteRefsRequest\x1a\x1a.gitaly.DeleteRefsResponse\"\x06\xfa\x97(\x02\x08\x01\x12\x8c\x01\n\x1fListBranchNamesContainingCommit\x12..gitaly.ListBranchNamesContainingCommitRequest\x1a/.gitaly.ListBranchNamesContainingCommitResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x83\x01\n\x1cListTagNamesContainingCommit\x12+.gitaly.ListTagNamesContainingCommitRequest\x1a,.gitaly.ListTagNamesContainingCommitResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12_\n\x10GetTagSignatures\x12\x1f.gitaly.GetTagSignaturesRequest\x1a .gitaly.GetTagSignaturesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12Y\n\x0eGetTagMessages\x12\x1d.gitaly.GetTagMessagesRequest\x1a\x1e.gitaly.GetTagMessagesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12H\n\x08PackRefs\x12\x17.gitaly.PackRefsRequest\x1a\x18.gitaly.PackRefsResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12G\n\x08ListRefs\x12\x17.gitaly.ListRefsRequest\x1a\x18.gitaly.ListRefsResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12T\n\rFindRefsByOID\x12\x1c.gitaly.FindRefsByOIDRequest\x1a\x1d.gitaly.FindRefsByOIDResponse\"\x06\xfa\x97(\x02\x08\x02\x42\x34Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypbb\x06proto3'
   ,
   dependencies=[errors__pb2.DESCRIPTOR,google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,lint__pb2.DESCRIPTOR,shared__pb2.DESCRIPTOR,])
 
@@ -154,8 +154,8 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=4322,
-  serialized_end=4392,
+  serialized_start=4367,
+  serialized_end=4437,
 )
 _sym_db.RegisterEnumDescriptor(_LISTREFSREQUEST_SORTBY_KEY)
 
@@ -1826,8 +1826,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4221,
-  serialized_end=4392,
+  serialized_start=4266,
+  serialized_end=4437,
 )
 
 _LISTREFSREQUEST = _descriptor.Descriptor(
@@ -1866,6 +1866,20 @@
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='pointing_at_oids', full_name='gitaly.ListRefsRequest.pointing_at_oids', index=4,
+      number=5, type=12, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='peel_tags', full_name='gitaly.ListRefsRequest.peel_tags', index=5,
+      number=6, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
   ],
   extensions=[
   ],
@@ -1879,7 +1893,7 @@
   oneofs=[
   ],
   serialized_start=4074,
-  serialized_end=4392,
+  serialized_end=4437,
 )
 
 
@@ -1905,6 +1919,13 @@
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='peeled_target', full_name='gitaly.ListRefsResponse.Reference.peeled_target', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
   ],
   extensions=[
   ],
@@ -1917,8 +1938,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4470,
-  serialized_end=4511,
+  serialized_start=4516,
+  serialized_end=4580,
 )
 
 _LISTREFSRESPONSE = _descriptor.Descriptor(
@@ -1948,8 +1969,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4394,
-  serialized_end=4511,
+  serialized_start=4440,
+  serialized_end=4580,
 )
 
 
@@ -2008,8 +2029,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4514,
-  serialized_end=4652,
+  serialized_start=4583,
+  serialized_end=4721,
 )
 
 
@@ -2040,8 +2061,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4654,
-  serialized_end=4691,
+  serialized_start=4723,
+  serialized_end=4760,
 )
 
 _FINDDEFAULTBRANCHNAMEREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
@@ -2534,8 +2555,8 @@
   index=0,
   serialized_options=None,
   create_key=_descriptor._internal_create_key,
-  serialized_start=4694,
-  serialized_end=6395,
+  serialized_start=4763,
+  serialized_end=6470,
   methods=[
   _descriptor.MethodDescriptor(
     name='FindDefaultBranchName',
@@ -2554,7 +2575,7 @@
     containing_service=None,
     input_type=_FINDALLBRANCHNAMESREQUEST,
     output_type=_FINDALLBRANCHNAMESRESPONSE,
-    serialized_options=b'\372\227(\002\010\002',
+    serialized_options=b'\210\002\001\372\227(\002\010\002',
     create_key=_descriptor._internal_create_key,
   ),
   _descriptor.MethodDescriptor(
@@ -2564,7 +2585,7 @@
     containing_service=None,
     input_type=_FINDALLTAGNAMESREQUEST,
     output_type=_FINDALLTAGNAMESRESPONSE,
-    serialized_options=b'\372\227(\002\010\002',
+    serialized_options=b'\210\002\001\372\227(\002\010\002',
     create_key=_descriptor._internal_create_key,
   ),
   _descriptor.MethodDescriptor(
diff --git a/hgitaly/stub/ref_pb2_grpc.py b/hgitaly/stub/ref_pb2_grpc.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zdHViL3JlZl9wYjJfZ3JwYy5weQ==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zdHViL3JlZl9wYjJfZ3JwYy5weQ== 100644
--- a/hgitaly/stub/ref_pb2_grpc.py
+++ b/hgitaly/stub/ref_pb2_grpc.py
@@ -119,10 +119,12 @@
         raise NotImplementedError('Method not implemented!')
 
     def FindAllBranchNames(self, request, context):
-        """This comment is left unintentionally blank.
+        """FindAllBranchNames is deprecated in favor of ListRefs
+
+        https://gitlab.com/gitlab-org/gitaly/-/issues/3966
         """
         context.set_code(grpc.StatusCode.UNIMPLEMENTED)
         context.set_details('Method not implemented!')
         raise NotImplementedError('Method not implemented!')
 
     def FindAllTagNames(self, request, context):
@@ -123,10 +125,12 @@
         """
         context.set_code(grpc.StatusCode.UNIMPLEMENTED)
         context.set_details('Method not implemented!')
         raise NotImplementedError('Method not implemented!')
 
     def FindAllTagNames(self, request, context):
-        """This comment is left unintentionally blank.
+        """FindAllTagNames is deprecated in favor of ListRefs
+
+        https://gitlab.com/gitlab-org/gitaly/-/issues/3966
         """
         context.set_code(grpc.StatusCode.UNIMPLEMENTED)
         context.set_details('Method not implemented!')
diff --git a/hgitaly/stub/repository_pb2.py b/hgitaly/stub/repository_pb2.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zdHViL3JlcG9zaXRvcnlfcGIyLnB5..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zdHViL3JlcG9zaXRvcnlfcGIyLnB5 100644
--- a/hgitaly/stub/repository_pb2.py
+++ b/hgitaly/stub/repository_pb2.py
@@ -21,7 +21,7 @@
   syntax='proto3',
   serialized_options=b'Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb',
   create_key=_descriptor._internal_create_key,
-  serialized_pb=b'\n\x10repository.proto\x12\x06gitaly\x1a\nlint.proto\x1a\x0cshared.proto\"G\n\x17RepositoryExistsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"*\n\x18RepositoryExistsResponse\x12\x0e\n\x06\x65xists\x18\x01 \x01(\x08\"H\n\x18RepackIncrementalRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x1b\n\x19RepackIncrementalResponse\"X\n\x11RepackFullRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x15\n\rcreate_bitmap\x18\x02 \x01(\x08\"\x14\n\x12RepackFullResponse\"A\n\x11MidxRepackRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x14\n\x12MidxRepackResponse\"k\n\x15GarbageCollectRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x15\n\rcreate_bitmap\x18\x02 \x01(\x08\x12\r\n\x05prune\x18\x03 \x01(\x08\"\x18\n\x16GarbageCollectResponse\"\xb0\x01\n\x17WriteCommitGraphRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x44\n\rsplitStrategy\x18\x02 \x01(\x0e\x32-.gitaly.WriteCommitGraphRequest.SplitStrategy\"!\n\rSplitStrategy\x12\x10\n\x0cSizeMultiple\x10\x00\"\x1a\n\x18WriteCommitGraphResponse\">\n\x0e\x43leanupRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x11\n\x0f\x43leanupResponse\"E\n\x15RepositorySizeRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"&\n\x16RepositorySizeResponse\x12\x0c\n\x04size\x18\x01 \x01(\x03\"[\n\x19\x41pplyGitattributesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\"\x1c\n\x1a\x41pplyGitattributesResponse\"e\n\x12\x46\x65tchBundleRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x13\n\x0bupdate_head\x18\x03 \x01(\x08\"\x15\n\x13\x46\x65tchBundleResponse\"\x82\x02\n\x12\x46\x65tchRemoteRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\r\n\x05\x66orce\x18\x03 \x01(\x08\x12\x0f\n\x07no_tags\x18\x04 \x01(\x08\x12\x0f\n\x07timeout\x18\x05 \x01(\x05\x12\x0f\n\x07ssh_key\x18\x06 \x01(\t\x12\x13\n\x0bknown_hosts\x18\x07 \x01(\t\x12\x10\n\x08no_prune\x18\t \x01(\x08\x12%\n\rremote_params\x18\n \x01(\x0b\x32\x0e.gitaly.Remote\x12\x1a\n\x12\x63heck_tags_changed\x18\x0b \x01(\x08J\x04\x08\x08\x10\tJ\x04\x08\x02\x10\x03R\x06remote\"+\n\x13\x46\x65tchRemoteResponse\x12\x14\n\x0ctags_changed\x18\x01 \x01(\x08\"_\n\x17\x43reateRepositoryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0e\x64\x65\x66\x61ult_branch\x18\x02 \x01(\x0c\"\x1a\n\x18\x43reateRepositoryResponse\"\x99\x02\n\x11GetArchiveRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\tcommit_id\x18\x02 \x01(\t\x12\x0e\n\x06prefix\x18\x03 \x01(\t\x12\x30\n\x06\x66ormat\x18\x04 \x01(\x0e\x32 .gitaly.GetArchiveRequest.Format\x12\x0c\n\x04path\x18\x05 \x01(\x0c\x12\x0f\n\x07\x65xclude\x18\x06 \x03(\x0c\x12\x12\n\nelide_path\x18\x07 \x01(\x08\x12\x19\n\x11include_lfs_blobs\x18\x08 \x01(\x08\"3\n\x06\x46ormat\x12\x07\n\x03ZIP\x10\x00\x12\x07\n\x03TAR\x10\x01\x12\n\n\x06TAR_GZ\x10\x02\x12\x0b\n\x07TAR_BZ2\x10\x03\"\"\n\x12GetArchiveResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"G\n\x17HasLocalBranchesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\")\n\x18HasLocalBranchesResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"\xa2\x01\n\x18\x46\x65tchSourceBranchRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12-\n\x11source_repository\x18\x02 \x01(\x0b\x32\x12.gitaly.Repository\x12\x15\n\rsource_branch\x18\x03 \x01(\x0c\x12\x12\n\ntarget_ref\x18\x04 \x01(\x0c\"+\n\x19\x46\x65tchSourceBranchResponse\x12\x0e\n\x06result\x18\x01 \x01(\x08\";\n\x0b\x46sckRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x1d\n\x0c\x46sckResponse\x12\r\n\x05\x65rror\x18\x01 \x01(\x0c\"\x89\x01\n\x0fWriteRefRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0b\n\x03ref\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x0c\x12\x14\n\x0cold_revision\x18\x04 \x01(\x0c\x12\r\n\x05\x66orce\x18\x05 \x01(\x08J\x04\x08\x06\x10\x07\"\x18\n\x10WriteRefResponseJ\x04\x08\x01\x10\x02\"W\n\x14\x46indMergeBaseRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\trevisions\x18\x02 \x03(\x0c\"%\n\x15\x46indMergeBaseResponse\x12\x0c\n\x04\x62\x61se\x18\x01 \x01(\t\"p\n\x11\x43reateForkRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12-\n\x11source_repository\x18\x02 \x01(\x0b\x32\x12.gitaly.Repository\"\x14\n\x12\x43reateForkResponse\"\xbf\x01\n\x1e\x43reateRepositoryFromURLRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x15\n\thttp_host\x18\x03 \x01(\tB\x02\x18\x01\x12!\n\x19http_authorization_header\x18\x04 \x01(\t\x12\x0e\n\x06mirror\x18\x05 \x01(\x08\x12\x18\n\x10resolved_address\x18\x06 \x01(\t\"!\n\x1f\x43reateRepositoryFromURLResponse\"C\n\x13\x43reateBundleRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"$\n\x14\x43reateBundleResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"`\n\x1e\x43reateBundleFromRefListRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08patterns\x18\x02 \x03(\x0c\"/\n\x1f\x43reateBundleFromRefListResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"@\n\x10GetConfigRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"!\n\x11GetConfigResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"W\n\x19RestoreCustomHooksRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x1c\n\x1aRestoreCustomHooksResponse\"H\n\x18\x42\x61\x63kupCustomHooksRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\")\n\x19\x42\x61\x63kupCustomHooksResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"_\n!CreateRepositoryFromBundleRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"$\n\"CreateRepositoryFromBundleResponse\"B\n\x12\x46indLicenseRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x8c\x01\n\x13\x46indLicenseResponse\x12\x1a\n\x12license_short_name\x18\x01 \x01(\t\x12\x14\n\x0clicense_name\x18\x02 \x01(\t\x12\x13\n\x0blicense_url\x18\x03 \x01(\t\x12\x14\n\x0clicense_path\x18\x04 \x01(\t\x12\x18\n\x10license_nickname\x18\x05 \x01(\t\"H\n\x18GetInfoAttributesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"/\n\x19GetInfoAttributesResponse\x12\x12\n\nattributes\x18\x01 \x01(\x0c\"H\n\x18\x43\x61lculateChecksumRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"-\n\x19\x43\x61lculateChecksumResponse\x12\x10\n\x08\x63hecksum\x18\x01 \x01(\t\"B\n\x12GetSnapshotRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"#\n\x13GetSnapshotResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\xa9\x01\n#CreateRepositoryFromSnapshotRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08http_url\x18\x02 \x01(\t\x12\x11\n\thttp_auth\x18\x03 \x01(\t\x12\x15\n\thttp_host\x18\x04 \x01(\tB\x02\x18\x01\x12\x18\n\x10resolved_address\x18\x05 \x01(\t\"&\n$CreateRepositoryFromSnapshotResponse\"p\n\x14GetRawChangesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x15\n\rfrom_revision\x18\x02 \x01(\t\x12\x13\n\x0bto_revision\x18\x03 \x01(\t\"\xbe\x03\n\x15GetRawChangesResponse\x12<\n\x0braw_changes\x18\x01 \x03(\x0b\x32\'.gitaly.GetRawChangesResponse.RawChange\x1a\xe6\x02\n\tRawChange\x12\x0f\n\x07\x62lob_id\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\x03\x12\x44\n\toperation\x18\x05 \x01(\x0e\x32\x31.gitaly.GetRawChangesResponse.RawChange.Operation\x12\x15\n\rraw_operation\x18\x06 \x01(\t\x12\x10\n\x08old_mode\x18\x07 \x01(\x05\x12\x10\n\x08new_mode\x18\x08 \x01(\x05\x12\x16\n\x0enew_path_bytes\x18\t \x01(\x0c\x12\x16\n\x0eold_path_bytes\x18\n \x01(\x0c\"i\n\tOperation\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05\x41\x44\x44\x45\x44\x10\x01\x12\n\n\x06\x43OPIED\x10\x02\x12\x0b\n\x07\x44\x45LETED\x10\x03\x12\x0c\n\x08MODIFIED\x10\x04\x12\x0b\n\x07RENAMED\x10\x05\x12\x10\n\x0cTYPE_CHANGED\x10\x06J\x04\x08\x03\x10\x04J\x04\x08\x04\x10\x05R\x08new_pathR\x08old_path\"\x93\x01\n\x18SearchFilesByNameRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0b\n\x03ref\x18\x03 \x01(\x0c\x12\x0e\n\x06\x66ilter\x18\x04 \x01(\t\x12\r\n\x05limit\x18\x05 \x01(\r\x12\x0e\n\x06offset\x18\x06 \x01(\r\"*\n\x19SearchFilesByNameResponse\x12\r\n\x05\x66iles\x18\x01 \x03(\x0c\"\x81\x01\n\x1bSearchFilesByContentRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0b\n\x03ref\x18\x03 \x01(\x0c\x12\x18\n\x10\x63hunked_response\x18\x04 \x01(\x08\"Y\n\x1cSearchFilesByContentResponse\x12\x0f\n\x07matches\x18\x01 \x03(\x0c\x12\x12\n\nmatch_data\x18\x02 \x01(\x0c\x12\x14\n\x0c\x65nd_of_match\x18\x03 \x01(\x08\"\x8d\x01\n\x06Remote\x12\x0b\n\x03url\x18\x01 \x01(\t\x12!\n\x19http_authorization_header\x18\x03 \x01(\t\x12\x16\n\x0emirror_refmaps\x18\x04 \x03(\t\x12\x15\n\thttp_host\x18\x05 \x01(\tB\x02\x18\x01\x12\x18\n\x10resolved_address\x18\x06 \x01(\tJ\x04\x08\x02\x10\x03R\x04name\"M\n\x1dGetObjectDirectorySizeRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\".\n\x1eGetObjectDirectorySizeResponse\x12\x0c\n\x04size\x18\x01 \x01(\x03\"G\n\x17RemoveRepositoryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x1a\n\x18RemoveRepositoryResponse\"^\n\x17RenameRepositoryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x15\n\rrelative_path\x18\x02 \x01(\t\"\x1a\n\x18RenameRepositoryResponse\"n\n\x1aReplicateRepositoryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\"\n\x06source\x18\x02 \x01(\x0b\x32\x12.gitaly.Repository\"\x1d\n\x1bReplicateRepositoryResponse\"I\n\x19OptimizeRepositoryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x1c\n\x1aOptimizeRepositoryResponse\"N\n\x1ePruneUnreachableObjectsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"!\n\x1fPruneUnreachableObjectsResponse\"P\n\x12SetFullPathRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04path\x18\x02 \x01(\t\"\x15\n\x13SetFullPathResponse\"?\n\x0f\x46ullPathRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\" \n\x10\x46ullPathResponse\x12\x0c\n\x04path\x18\x01 \x01(\t2\xe3\x1e\n\x11RepositoryService\x12]\n\x10RepositoryExists\x12\x1f.gitaly.RepositoryExistsRequest\x1a .gitaly.RepositoryExistsResponse\"\x06\xfa\x97(\x02\x08\x02\x12\x63\n\x11RepackIncremental\x12 .gitaly.RepackIncrementalRequest\x1a!.gitaly.RepackIncrementalResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12N\n\nRepackFull\x12\x19.gitaly.RepackFullRequest\x1a\x1a.gitaly.RepackFullResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12N\n\nMidxRepack\x12\x19.gitaly.MidxRepackRequest\x1a\x1a.gitaly.MidxRepackResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12Z\n\x0eGarbageCollect\x12\x1d.gitaly.GarbageCollectRequest\x1a\x1e.gitaly.GarbageCollectResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12`\n\x10WriteCommitGraph\x12\x1f.gitaly.WriteCommitGraphRequest\x1a .gitaly.WriteCommitGraphResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12W\n\x0eRepositorySize\x12\x1d.gitaly.RepositorySizeRequest\x1a\x1e.gitaly.RepositorySizeResponse\"\x06\xfa\x97(\x02\x08\x02\x12\x63\n\x12\x41pplyGitattributes\x12!.gitaly.ApplyGitattributesRequest\x1a\".gitaly.ApplyGitattributesResponse\"\x06\xfa\x97(\x02\x08\x01\x12N\n\x0b\x46\x65tchRemote\x12\x1a.gitaly.FetchRemoteRequest\x1a\x1b.gitaly.FetchRemoteResponse\"\x06\xfa\x97(\x02\x08\x01\x12]\n\x10\x43reateRepository\x12\x1f.gitaly.CreateRepositoryRequest\x1a .gitaly.CreateRepositoryResponse\"\x06\xfa\x97(\x02\x08\x01\x12M\n\nGetArchive\x12\x19.gitaly.GetArchiveRequest\x1a\x1a.gitaly.GetArchiveResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12]\n\x10HasLocalBranches\x12\x1f.gitaly.HasLocalBranchesRequest\x1a .gitaly.HasLocalBranchesResponse\"\x06\xfa\x97(\x02\x08\x02\x12`\n\x11\x46\x65tchSourceBranch\x12 .gitaly.FetchSourceBranchRequest\x1a!.gitaly.FetchSourceBranchResponse\"\x06\xfa\x97(\x02\x08\x01\x12\x39\n\x04\x46sck\x12\x13.gitaly.FsckRequest\x1a\x14.gitaly.FsckResponse\"\x06\xfa\x97(\x02\x08\x02\x12\x45\n\x08WriteRef\x12\x17.gitaly.WriteRefRequest\x1a\x18.gitaly.WriteRefResponse\"\x06\xfa\x97(\x02\x08\x01\x12T\n\rFindMergeBase\x12\x1c.gitaly.FindMergeBaseRequest\x1a\x1d.gitaly.FindMergeBaseResponse\"\x06\xfa\x97(\x02\x08\x02\x12K\n\nCreateFork\x12\x19.gitaly.CreateForkRequest\x1a\x1a.gitaly.CreateForkResponse\"\x06\xfa\x97(\x02\x08\x01\x12r\n\x17\x43reateRepositoryFromURL\x12&.gitaly.CreateRepositoryFromURLRequest\x1a\'.gitaly.CreateRepositoryFromURLResponse\"\x06\xfa\x97(\x02\x08\x01\x12S\n\x0c\x43reateBundle\x12\x1b.gitaly.CreateBundleRequest\x1a\x1c.gitaly.CreateBundleResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12v\n\x17\x43reateBundleFromRefList\x12&.gitaly.CreateBundleFromRefListRequest\x1a\'.gitaly.CreateBundleFromRefListResponse\"\x06\xfa\x97(\x02\x08\x02(\x01\x30\x01\x12P\n\x0b\x46\x65tchBundle\x12\x1a.gitaly.FetchBundleRequest\x1a\x1b.gitaly.FetchBundleResponse\"\x06\xfa\x97(\x02\x08\x01(\x01\x12}\n\x1a\x43reateRepositoryFromBundle\x12).gitaly.CreateRepositoryFromBundleRequest\x1a*.gitaly.CreateRepositoryFromBundleResponse\"\x06\xfa\x97(\x02\x08\x01(\x01\x12J\n\tGetConfig\x12\x18.gitaly.GetConfigRequest\x1a\x19.gitaly.GetConfigResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12N\n\x0b\x46indLicense\x12\x1a.gitaly.FindLicenseRequest\x1a\x1b.gitaly.FindLicenseResponse\"\x06\xfa\x97(\x02\x08\x02\x12\x62\n\x11GetInfoAttributes\x12 .gitaly.GetInfoAttributesRequest\x1a!.gitaly.GetInfoAttributesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12`\n\x11\x43\x61lculateChecksum\x12 .gitaly.CalculateChecksumRequest\x1a!.gitaly.CalculateChecksumResponse\"\x06\xfa\x97(\x02\x08\x02\x12\x45\n\x07\x43leanup\x12\x16.gitaly.CleanupRequest\x1a\x17.gitaly.CleanupResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12P\n\x0bGetSnapshot\x12\x1a.gitaly.GetSnapshotRequest\x1a\x1b.gitaly.GetSnapshotResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x81\x01\n\x1c\x43reateRepositoryFromSnapshot\x12+.gitaly.CreateRepositoryFromSnapshotRequest\x1a,.gitaly.CreateRepositoryFromSnapshotResponse\"\x06\xfa\x97(\x02\x08\x01\x12V\n\rGetRawChanges\x12\x1c.gitaly.GetRawChangesRequest\x1a\x1d.gitaly.GetRawChangesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12k\n\x14SearchFilesByContent\x12#.gitaly.SearchFilesByContentRequest\x1a$.gitaly.SearchFilesByContentResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x62\n\x11SearchFilesByName\x12 .gitaly.SearchFilesByNameRequest\x1a!.gitaly.SearchFilesByNameResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x65\n\x12RestoreCustomHooks\x12!.gitaly.RestoreCustomHooksRequest\x1a\".gitaly.RestoreCustomHooksResponse\"\x06\xfa\x97(\x02\x08\x01(\x01\x12\x62\n\x11\x42\x61\x63kupCustomHooks\x12 .gitaly.BackupCustomHooksRequest\x1a!.gitaly.BackupCustomHooksResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12o\n\x16GetObjectDirectorySize\x12%.gitaly.GetObjectDirectorySizeRequest\x1a&.gitaly.GetObjectDirectorySizeResponse\"\x06\xfa\x97(\x02\x08\x02\x12]\n\x10RemoveRepository\x12\x1f.gitaly.RemoveRepositoryRequest\x1a .gitaly.RemoveRepositoryResponse\"\x06\xfa\x97(\x02\x08\x01\x12]\n\x10RenameRepository\x12\x1f.gitaly.RenameRepositoryRequest\x1a .gitaly.RenameRepositoryResponse\"\x06\xfa\x97(\x02\x08\x01\x12\x66\n\x13ReplicateRepository\x12\".gitaly.ReplicateRepositoryRequest\x1a#.gitaly.ReplicateRepositoryResponse\"\x06\xfa\x97(\x02\x08\x01\x12\x63\n\x12OptimizeRepository\x12!.gitaly.OptimizeRepositoryRequest\x1a\".gitaly.OptimizeRepositoryResponse\"\x06\xfa\x97(\x02\x08\x03\x12r\n\x17PruneUnreachableObjects\x12&.gitaly.PruneUnreachableObjectsRequest\x1a\'.gitaly.PruneUnreachableObjectsResponse\"\x06\xfa\x97(\x02\x08\x03\x12N\n\x0bSetFullPath\x12\x1a.gitaly.SetFullPathRequest\x1a\x1b.gitaly.SetFullPathResponse\"\x06\xfa\x97(\x02\x08\x01\x12\x45\n\x08\x46ullPath\x12\x17.gitaly.FullPathRequest\x1a\x18.gitaly.FullPathResponse\"\x06\xfa\x97(\x02\x08\x02\x42\x34Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypbb\x06proto3'
+  serialized_pb=b'\n\x10repository.proto\x12\x06gitaly\x1a\nlint.proto\x1a\x0cshared.proto\"G\n\x17RepositoryExistsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"*\n\x18RepositoryExistsResponse\x12\x0e\n\x06\x65xists\x18\x01 \x01(\x08\"H\n\x18RepackIncrementalRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x1b\n\x19RepackIncrementalResponse\"X\n\x11RepackFullRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x15\n\rcreate_bitmap\x18\x02 \x01(\x08\"\x14\n\x12RepackFullResponse\"A\n\x11MidxRepackRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x14\n\x12MidxRepackResponse\"k\n\x15GarbageCollectRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x15\n\rcreate_bitmap\x18\x02 \x01(\x08\x12\r\n\x05prune\x18\x03 \x01(\x08\"\x18\n\x16GarbageCollectResponse\"\xb0\x01\n\x17WriteCommitGraphRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x44\n\rsplitStrategy\x18\x02 \x01(\x0e\x32-.gitaly.WriteCommitGraphRequest.SplitStrategy\"!\n\rSplitStrategy\x12\x10\n\x0cSizeMultiple\x10\x00\"\x1a\n\x18WriteCommitGraphResponse\">\n\x0e\x43leanupRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x11\n\x0f\x43leanupResponse\"E\n\x15RepositorySizeRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"&\n\x16RepositorySizeResponse\x12\x0c\n\x04size\x18\x01 \x01(\x03\"C\n\x13ObjectFormatRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"<\n\x14ObjectFormatResponse\x12$\n\x06\x66ormat\x18\x01 \x01(\x0e\x32\x14.gitaly.ObjectFormat\"[\n\x19\x41pplyGitattributesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08revision\x18\x02 \x01(\x0c\"\x1c\n\x1a\x41pplyGitattributesResponse\"e\n\x12\x46\x65tchBundleRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x13\n\x0bupdate_head\x18\x03 \x01(\x08\"\x15\n\x13\x46\x65tchBundleResponse\"\x82\x02\n\x12\x46\x65tchRemoteRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\r\n\x05\x66orce\x18\x03 \x01(\x08\x12\x0f\n\x07no_tags\x18\x04 \x01(\x08\x12\x0f\n\x07timeout\x18\x05 \x01(\x05\x12\x0f\n\x07ssh_key\x18\x06 \x01(\t\x12\x13\n\x0bknown_hosts\x18\x07 \x01(\t\x12\x10\n\x08no_prune\x18\t \x01(\x08\x12%\n\rremote_params\x18\n \x01(\x0b\x32\x0e.gitaly.Remote\x12\x1a\n\x12\x63heck_tags_changed\x18\x0b \x01(\x08J\x04\x08\x08\x10\tJ\x04\x08\x02\x10\x03R\x06remote\"+\n\x13\x46\x65tchRemoteResponse\x12\x14\n\x0ctags_changed\x18\x01 \x01(\x08\"\x8c\x01\n\x17\x43reateRepositoryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x16\n\x0e\x64\x65\x66\x61ult_branch\x18\x02 \x01(\x0c\x12+\n\robject_format\x18\x03 \x01(\x0e\x32\x14.gitaly.ObjectFormat\"\x1a\n\x18\x43reateRepositoryResponse\"\x99\x02\n\x11GetArchiveRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\tcommit_id\x18\x02 \x01(\t\x12\x0e\n\x06prefix\x18\x03 \x01(\t\x12\x30\n\x06\x66ormat\x18\x04 \x01(\x0e\x32 .gitaly.GetArchiveRequest.Format\x12\x0c\n\x04path\x18\x05 \x01(\x0c\x12\x0f\n\x07\x65xclude\x18\x06 \x03(\x0c\x12\x12\n\nelide_path\x18\x07 \x01(\x08\x12\x19\n\x11include_lfs_blobs\x18\x08 \x01(\x08\"3\n\x06\x46ormat\x12\x07\n\x03ZIP\x10\x00\x12\x07\n\x03TAR\x10\x01\x12\n\n\x06TAR_GZ\x10\x02\x12\x0b\n\x07TAR_BZ2\x10\x03\"\"\n\x12GetArchiveResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"G\n\x17HasLocalBranchesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\")\n\x18HasLocalBranchesResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"\xa2\x01\n\x18\x46\x65tchSourceBranchRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12-\n\x11source_repository\x18\x02 \x01(\x0b\x32\x12.gitaly.Repository\x12\x15\n\rsource_branch\x18\x03 \x01(\x0c\x12\x12\n\ntarget_ref\x18\x04 \x01(\x0c\"+\n\x19\x46\x65tchSourceBranchResponse\x12\x0e\n\x06result\x18\x01 \x01(\x08\";\n\x0b\x46sckRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x1d\n\x0c\x46sckResponse\x12\r\n\x05\x65rror\x18\x01 \x01(\x0c\"\x89\x01\n\x0fWriteRefRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0b\n\x03ref\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x0c\x12\x14\n\x0cold_revision\x18\x04 \x01(\x0c\x12\r\n\x05\x66orce\x18\x05 \x01(\x08J\x04\x08\x06\x10\x07\"\x18\n\x10WriteRefResponseJ\x04\x08\x01\x10\x02\"W\n\x14\x46indMergeBaseRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x11\n\trevisions\x18\x02 \x03(\x0c\"%\n\x15\x46indMergeBaseResponse\x12\x0c\n\x04\x62\x61se\x18\x01 \x01(\t\"p\n\x11\x43reateForkRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12-\n\x11source_repository\x18\x02 \x01(\x0b\x32\x12.gitaly.Repository\"\x14\n\x12\x43reateForkResponse\"\xbf\x01\n\x1e\x43reateRepositoryFromURLRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x15\n\thttp_host\x18\x03 \x01(\tB\x02\x18\x01\x12!\n\x19http_authorization_header\x18\x04 \x01(\t\x12\x0e\n\x06mirror\x18\x05 \x01(\x08\x12\x18\n\x10resolved_address\x18\x06 \x01(\t\"!\n\x1f\x43reateRepositoryFromURLResponse\"C\n\x13\x43reateBundleRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"$\n\x14\x43reateBundleResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"`\n\x1e\x43reateBundleFromRefListRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08patterns\x18\x02 \x03(\x0c\"/\n\x1f\x43reateBundleFromRefListResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"@\n\x10GetConfigRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"!\n\x11GetConfigResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"W\n\x19RestoreCustomHooksRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"S\n\x15SetCustomHooksRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x1c\n\x1aRestoreCustomHooksResponse\"\x18\n\x16SetCustomHooksResponse\"H\n\x18\x42\x61\x63kupCustomHooksRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"E\n\x15GetCustomHooksRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\")\n\x19\x42\x61\x63kupCustomHooksResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"&\n\x16GetCustomHooksResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"_\n!CreateRepositoryFromBundleRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"$\n\"CreateRepositoryFromBundleResponse\"B\n\x12\x46indLicenseRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x8c\x01\n\x13\x46indLicenseResponse\x12\x1a\n\x12license_short_name\x18\x01 \x01(\t\x12\x14\n\x0clicense_name\x18\x02 \x01(\t\x12\x13\n\x0blicense_url\x18\x03 \x01(\t\x12\x14\n\x0clicense_path\x18\x04 \x01(\t\x12\x18\n\x10license_nickname\x18\x05 \x01(\t\"H\n\x18GetInfoAttributesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"/\n\x19GetInfoAttributesResponse\x12\x12\n\nattributes\x18\x01 \x01(\x0c\"H\n\x18\x43\x61lculateChecksumRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"-\n\x19\x43\x61lculateChecksumResponse\x12\x10\n\x08\x63hecksum\x18\x01 \x01(\t\"B\n\x12GetSnapshotRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"#\n\x13GetSnapshotResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\xa9\x01\n#CreateRepositoryFromSnapshotRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x10\n\x08http_url\x18\x02 \x01(\t\x12\x11\n\thttp_auth\x18\x03 \x01(\t\x12\x15\n\thttp_host\x18\x04 \x01(\tB\x02\x18\x01\x12\x18\n\x10resolved_address\x18\x05 \x01(\t\"&\n$CreateRepositoryFromSnapshotResponse\"p\n\x14GetRawChangesRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x15\n\rfrom_revision\x18\x02 \x01(\t\x12\x13\n\x0bto_revision\x18\x03 \x01(\t\"\xbe\x03\n\x15GetRawChangesResponse\x12<\n\x0braw_changes\x18\x01 \x03(\x0b\x32\'.gitaly.GetRawChangesResponse.RawChange\x1a\xe6\x02\n\tRawChange\x12\x0f\n\x07\x62lob_id\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\x03\x12\x44\n\toperation\x18\x05 \x01(\x0e\x32\x31.gitaly.GetRawChangesResponse.RawChange.Operation\x12\x15\n\rraw_operation\x18\x06 \x01(\t\x12\x10\n\x08old_mode\x18\x07 \x01(\x05\x12\x10\n\x08new_mode\x18\x08 \x01(\x05\x12\x16\n\x0enew_path_bytes\x18\t \x01(\x0c\x12\x16\n\x0eold_path_bytes\x18\n \x01(\x0c\"i\n\tOperation\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05\x41\x44\x44\x45\x44\x10\x01\x12\n\n\x06\x43OPIED\x10\x02\x12\x0b\n\x07\x44\x45LETED\x10\x03\x12\x0c\n\x08MODIFIED\x10\x04\x12\x0b\n\x07RENAMED\x10\x05\x12\x10\n\x0cTYPE_CHANGED\x10\x06J\x04\x08\x03\x10\x04J\x04\x08\x04\x10\x05R\x08new_pathR\x08old_path\"\x93\x01\n\x18SearchFilesByNameRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0b\n\x03ref\x18\x03 \x01(\x0c\x12\x0e\n\x06\x66ilter\x18\x04 \x01(\t\x12\r\n\x05limit\x18\x05 \x01(\r\x12\x0e\n\x06offset\x18\x06 \x01(\r\"*\n\x19SearchFilesByNameResponse\x12\r\n\x05\x66iles\x18\x01 \x03(\x0c\"\x81\x01\n\x1bSearchFilesByContentRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0b\n\x03ref\x18\x03 \x01(\x0c\x12\x18\n\x10\x63hunked_response\x18\x04 \x01(\x08\"Y\n\x1cSearchFilesByContentResponse\x12\x0f\n\x07matches\x18\x01 \x03(\x0c\x12\x12\n\nmatch_data\x18\x02 \x01(\x0c\x12\x14\n\x0c\x65nd_of_match\x18\x03 \x01(\x08\"\x8d\x01\n\x06Remote\x12\x0b\n\x03url\x18\x01 \x01(\t\x12!\n\x19http_authorization_header\x18\x03 \x01(\t\x12\x16\n\x0emirror_refmaps\x18\x04 \x03(\t\x12\x15\n\thttp_host\x18\x05 \x01(\tB\x02\x18\x01\x12\x18\n\x10resolved_address\x18\x06 \x01(\tJ\x04\x08\x02\x10\x03R\x04name\"M\n\x1dGetObjectDirectorySizeRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\".\n\x1eGetObjectDirectorySizeResponse\x12\x0c\n\x04size\x18\x01 \x01(\x03\"G\n\x17RemoveRepositoryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"\x1a\n\x18RemoveRepositoryResponse\"^\n\x17RenameRepositoryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x15\n\rrelative_path\x18\x02 \x01(\t\"\x1a\n\x18RenameRepositoryResponse\"n\n\x1aReplicateRepositoryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\"\n\x06source\x18\x02 \x01(\x0b\x32\x12.gitaly.Repository\"\x1d\n\x1bReplicateRepositoryResponse\"\xdb\x01\n\x19OptimizeRepositoryRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12<\n\x08strategy\x18\x02 \x01(\x0e\x32*.gitaly.OptimizeRepositoryRequest.Strategy\"R\n\x08Strategy\x12\x18\n\x14STRATEGY_UNSPECIFIED\x10\x00\x12\x18\n\x14STRATEGY_HEURISTICAL\x10\x01\x12\x12\n\x0eSTRATEGY_EAGER\x10\x02\"\x1c\n\x1aOptimizeRepositoryResponse\"N\n\x1ePruneUnreachableObjectsRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\"!\n\x1fPruneUnreachableObjectsResponse\"P\n\x12SetFullPathRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\x12\x0c\n\x04path\x18\x02 \x01(\t\"\x15\n\x13SetFullPathResponse\"?\n\x0f\x46ullPathRequest\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x98\xc6,\x01\" \n\x10\x46ullPathResponse\x12\x0c\n\x04path\x18\x01 \x01(\t\"(\n\x10RemoveAllRequest\x12\x14\n\x0cstorage_name\x18\x01 \x01(\t\"\x13\n\x11RemoveAllResponse2\xc0!\n\x11RepositoryService\x12]\n\x10RepositoryExists\x12\x1f.gitaly.RepositoryExistsRequest\x1a .gitaly.RepositoryExistsResponse\"\x06\xfa\x97(\x02\x08\x02\x12\x63\n\x11RepackIncremental\x12 .gitaly.RepackIncrementalRequest\x1a!.gitaly.RepackIncrementalResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12N\n\nRepackFull\x12\x19.gitaly.RepackFullRequest\x1a\x1a.gitaly.RepackFullResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12N\n\nMidxRepack\x12\x19.gitaly.MidxRepackRequest\x1a\x1a.gitaly.MidxRepackResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12Z\n\x0eGarbageCollect\x12\x1d.gitaly.GarbageCollectRequest\x1a\x1e.gitaly.GarbageCollectResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12`\n\x10WriteCommitGraph\x12\x1f.gitaly.WriteCommitGraphRequest\x1a .gitaly.WriteCommitGraphResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12W\n\x0eRepositorySize\x12\x1d.gitaly.RepositorySizeRequest\x1a\x1e.gitaly.RepositorySizeResponse\"\x06\xfa\x97(\x02\x08\x02\x12Q\n\x0cObjectFormat\x12\x1b.gitaly.ObjectFormatRequest\x1a\x1c.gitaly.ObjectFormatResponse\"\x06\xfa\x97(\x02\x08\x02\x12\x63\n\x12\x41pplyGitattributes\x12!.gitaly.ApplyGitattributesRequest\x1a\".gitaly.ApplyGitattributesResponse\"\x06\xfa\x97(\x02\x08\x01\x12N\n\x0b\x46\x65tchRemote\x12\x1a.gitaly.FetchRemoteRequest\x1a\x1b.gitaly.FetchRemoteResponse\"\x06\xfa\x97(\x02\x08\x01\x12]\n\x10\x43reateRepository\x12\x1f.gitaly.CreateRepositoryRequest\x1a .gitaly.CreateRepositoryResponse\"\x06\xfa\x97(\x02\x08\x01\x12M\n\nGetArchive\x12\x19.gitaly.GetArchiveRequest\x1a\x1a.gitaly.GetArchiveResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12]\n\x10HasLocalBranches\x12\x1f.gitaly.HasLocalBranchesRequest\x1a .gitaly.HasLocalBranchesResponse\"\x06\xfa\x97(\x02\x08\x02\x12`\n\x11\x46\x65tchSourceBranch\x12 .gitaly.FetchSourceBranchRequest\x1a!.gitaly.FetchSourceBranchResponse\"\x06\xfa\x97(\x02\x08\x01\x12\x39\n\x04\x46sck\x12\x13.gitaly.FsckRequest\x1a\x14.gitaly.FsckResponse\"\x06\xfa\x97(\x02\x08\x02\x12\x45\n\x08WriteRef\x12\x17.gitaly.WriteRefRequest\x1a\x18.gitaly.WriteRefResponse\"\x06\xfa\x97(\x02\x08\x01\x12T\n\rFindMergeBase\x12\x1c.gitaly.FindMergeBaseRequest\x1a\x1d.gitaly.FindMergeBaseResponse\"\x06\xfa\x97(\x02\x08\x02\x12K\n\nCreateFork\x12\x19.gitaly.CreateForkRequest\x1a\x1a.gitaly.CreateForkResponse\"\x06\xfa\x97(\x02\x08\x01\x12r\n\x17\x43reateRepositoryFromURL\x12&.gitaly.CreateRepositoryFromURLRequest\x1a\'.gitaly.CreateRepositoryFromURLResponse\"\x06\xfa\x97(\x02\x08\x01\x12S\n\x0c\x43reateBundle\x12\x1b.gitaly.CreateBundleRequest\x1a\x1c.gitaly.CreateBundleResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12v\n\x17\x43reateBundleFromRefList\x12&.gitaly.CreateBundleFromRefListRequest\x1a\'.gitaly.CreateBundleFromRefListResponse\"\x06\xfa\x97(\x02\x08\x02(\x01\x30\x01\x12P\n\x0b\x46\x65tchBundle\x12\x1a.gitaly.FetchBundleRequest\x1a\x1b.gitaly.FetchBundleResponse\"\x06\xfa\x97(\x02\x08\x01(\x01\x12}\n\x1a\x43reateRepositoryFromBundle\x12).gitaly.CreateRepositoryFromBundleRequest\x1a*.gitaly.CreateRepositoryFromBundleResponse\"\x06\xfa\x97(\x02\x08\x01(\x01\x12J\n\tGetConfig\x12\x18.gitaly.GetConfigRequest\x1a\x19.gitaly.GetConfigResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12N\n\x0b\x46indLicense\x12\x1a.gitaly.FindLicenseRequest\x1a\x1b.gitaly.FindLicenseResponse\"\x06\xfa\x97(\x02\x08\x02\x12\x62\n\x11GetInfoAttributes\x12 .gitaly.GetInfoAttributesRequest\x1a!.gitaly.GetInfoAttributesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12`\n\x11\x43\x61lculateChecksum\x12 .gitaly.CalculateChecksumRequest\x1a!.gitaly.CalculateChecksumResponse\"\x06\xfa\x97(\x02\x08\x02\x12\x45\n\x07\x43leanup\x12\x16.gitaly.CleanupRequest\x1a\x17.gitaly.CleanupResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x03\x12P\n\x0bGetSnapshot\x12\x1a.gitaly.GetSnapshotRequest\x1a\x1b.gitaly.GetSnapshotResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x81\x01\n\x1c\x43reateRepositoryFromSnapshot\x12+.gitaly.CreateRepositoryFromSnapshotRequest\x1a,.gitaly.CreateRepositoryFromSnapshotResponse\"\x06\xfa\x97(\x02\x08\x01\x12V\n\rGetRawChanges\x12\x1c.gitaly.GetRawChangesRequest\x1a\x1d.gitaly.GetRawChangesResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12k\n\x14SearchFilesByContent\x12#.gitaly.SearchFilesByContentRequest\x1a$.gitaly.SearchFilesByContentResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12\x62\n\x11SearchFilesByName\x12 .gitaly.SearchFilesByNameRequest\x1a!.gitaly.SearchFilesByNameResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12h\n\x12RestoreCustomHooks\x12!.gitaly.RestoreCustomHooksRequest\x1a\".gitaly.RestoreCustomHooksResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x01(\x01\x12Y\n\x0eSetCustomHooks\x12\x1d.gitaly.SetCustomHooksRequest\x1a\x1e.gitaly.SetCustomHooksResponse\"\x06\xfa\x97(\x02\x08\x01(\x01\x12\x65\n\x11\x42\x61\x63kupCustomHooks\x12 .gitaly.BackupCustomHooksRequest\x1a!.gitaly.BackupCustomHooksResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x02\x30\x01\x12Y\n\x0eGetCustomHooks\x12\x1d.gitaly.GetCustomHooksRequest\x1a\x1e.gitaly.GetCustomHooksResponse\"\x06\xfa\x97(\x02\x08\x02\x30\x01\x12o\n\x16GetObjectDirectorySize\x12%.gitaly.GetObjectDirectorySizeRequest\x1a&.gitaly.GetObjectDirectorySizeResponse\"\x06\xfa\x97(\x02\x08\x02\x12]\n\x10RemoveRepository\x12\x1f.gitaly.RemoveRepositoryRequest\x1a .gitaly.RemoveRepositoryResponse\"\x06\xfa\x97(\x02\x08\x01\x12]\n\x10RenameRepository\x12\x1f.gitaly.RenameRepositoryRequest\x1a .gitaly.RenameRepositoryResponse\"\x06\xfa\x97(\x02\x08\x01\x12\x66\n\x13ReplicateRepository\x12\".gitaly.ReplicateRepositoryRequest\x1a#.gitaly.ReplicateRepositoryResponse\"\x06\xfa\x97(\x02\x08\x01\x12\x63\n\x12OptimizeRepository\x12!.gitaly.OptimizeRepositoryRequest\x1a\".gitaly.OptimizeRepositoryResponse\"\x06\xfa\x97(\x02\x08\x03\x12r\n\x17PruneUnreachableObjects\x12&.gitaly.PruneUnreachableObjectsRequest\x1a\'.gitaly.PruneUnreachableObjectsResponse\"\x06\xfa\x97(\x02\x08\x03\x12Q\n\x0bSetFullPath\x12\x1a.gitaly.SetFullPathRequest\x1a\x1b.gitaly.SetFullPathResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x01\x12H\n\x08\x46ullPath\x12\x17.gitaly.FullPathRequest\x1a\x18.gitaly.FullPathResponse\"\t\x88\x02\x01\xfa\x97(\x02\x08\x02\x12\x46\n\tRemoveAll\x12\x18.gitaly.RemoveAllRequest\x1a\x19.gitaly.RemoveAllResponse\"\x04\x80\x98(\x01\x42\x34Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypbb\x06proto3'
   ,
   dependencies=[lint__pb2.DESCRIPTOR,shared__pb2.DESCRIPTOR,])
 
@@ -77,8 +77,8 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=1922,
-  serialized_end=1973,
+  serialized_start=2099,
+  serialized_end=2150,
 )
 _sym_db.RegisterEnumDescriptor(_GETARCHIVEREQUEST_FORMAT)
 
@@ -127,8 +127,8 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=5010,
-  serialized_end=5115,
+  serialized_start=5409,
+  serialized_end=5514,
 )
 _sym_db.RegisterEnumDescriptor(_GETRAWCHANGESRESPONSE_RAWCHANGE_OPERATION)
 
@@ -132,6 +132,36 @@
 )
 _sym_db.RegisterEnumDescriptor(_GETRAWCHANGESRESPONSE_RAWCHANGE_OPERATION)
 
+_OPTIMIZEREPOSITORYREQUEST_STRATEGY = _descriptor.EnumDescriptor(
+  name='Strategy',
+  full_name='gitaly.OptimizeRepositoryRequest.Strategy',
+  filename=None,
+  file=DESCRIPTOR,
+  create_key=_descriptor._internal_create_key,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='STRATEGY_UNSPECIFIED', index=0, number=0,
+      serialized_options=None,
+      type=None,
+      create_key=_descriptor._internal_create_key),
+    _descriptor.EnumValueDescriptor(
+      name='STRATEGY_HEURISTICAL', index=1, number=1,
+      serialized_options=None,
+      type=None,
+      create_key=_descriptor._internal_create_key),
+    _descriptor.EnumValueDescriptor(
+      name='STRATEGY_EAGER', index=2, number=2,
+      serialized_options=None,
+      type=None,
+      create_key=_descriptor._internal_create_key),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=6742,
+  serialized_end=6824,
+)
+_sym_db.RegisterEnumDescriptor(_OPTIMIZEREPOSITORYREQUEST_STRATEGY)
+
 
 _REPOSITORYEXISTSREQUEST = _descriptor.Descriptor(
   name='RepositoryExistsRequest',
@@ -632,6 +662,70 @@
 )
 
 
+_OBJECTFORMATREQUEST = _descriptor.Descriptor(
+  name='ObjectFormatRequest',
+  full_name='gitaly.ObjectFormatRequest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='repository', full_name='gitaly.ObjectFormatRequest.repository', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=b'\230\306,\001', file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1011,
+  serialized_end=1078,
+)
+
+
+_OBJECTFORMATRESPONSE = _descriptor.Descriptor(
+  name='ObjectFormatResponse',
+  full_name='gitaly.ObjectFormatResponse',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='format', full_name='gitaly.ObjectFormatResponse.format', index=0,
+      number=1, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1080,
+  serialized_end=1140,
+)
+
+
 _APPLYGITATTRIBUTESREQUEST = _descriptor.Descriptor(
   name='ApplyGitattributesRequest',
   full_name='gitaly.ApplyGitattributesRequest',
@@ -666,8 +760,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1011,
-  serialized_end=1102,
+  serialized_start=1142,
+  serialized_end=1233,
 )
 
 
@@ -691,8 +785,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1104,
-  serialized_end=1132,
+  serialized_start=1235,
+  serialized_end=1263,
 )
 
 
@@ -737,8 +831,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1134,
-  serialized_end=1235,
+  serialized_start=1265,
+  serialized_end=1366,
 )
 
 
@@ -762,8 +856,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1237,
-  serialized_end=1258,
+  serialized_start=1368,
+  serialized_end=1389,
 )
 
 
@@ -850,8 +944,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1261,
-  serialized_end=1519,
+  serialized_start=1392,
+  serialized_end=1650,
 )
 
 
@@ -882,8 +976,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1521,
-  serialized_end=1564,
+  serialized_start=1652,
+  serialized_end=1695,
 )
 
 
@@ -909,6 +1003,13 @@
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='object_format', full_name='gitaly.CreateRepositoryRequest.object_format', index=2,
+      number=3, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
   ],
   extensions=[
   ],
@@ -921,8 +1022,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1566,
-  serialized_end=1661,
+  serialized_start=1698,
+  serialized_end=1838,
 )
 
 
@@ -946,8 +1047,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1663,
-  serialized_end=1689,
+  serialized_start=1840,
+  serialized_end=1866,
 )
 
 
@@ -1028,8 +1129,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1692,
-  serialized_end=1973,
+  serialized_start=1869,
+  serialized_end=2150,
 )
 
 
@@ -1060,8 +1161,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1975,
-  serialized_end=2009,
+  serialized_start=2152,
+  serialized_end=2186,
 )
 
 
@@ -1092,8 +1193,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2011,
-  serialized_end=2082,
+  serialized_start=2188,
+  serialized_end=2259,
 )
 
 
@@ -1124,8 +1225,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2084,
-  serialized_end=2125,
+  serialized_start=2261,
+  serialized_end=2302,
 )
 
 
@@ -1177,8 +1278,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2128,
-  serialized_end=2290,
+  serialized_start=2305,
+  serialized_end=2467,
 )
 
 
@@ -1209,8 +1310,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2292,
-  serialized_end=2335,
+  serialized_start=2469,
+  serialized_end=2512,
 )
 
 
@@ -1241,8 +1342,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2337,
-  serialized_end=2396,
+  serialized_start=2514,
+  serialized_end=2573,
 )
 
 
@@ -1273,8 +1374,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2398,
-  serialized_end=2427,
+  serialized_start=2575,
+  serialized_end=2604,
 )
 
 
@@ -1333,8 +1434,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2430,
-  serialized_end=2567,
+  serialized_start=2607,
+  serialized_end=2744,
 )
 
 
@@ -1358,8 +1459,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2569,
-  serialized_end=2593,
+  serialized_start=2746,
+  serialized_end=2770,
 )
 
 
@@ -1397,8 +1498,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2595,
-  serialized_end=2682,
+  serialized_start=2772,
+  serialized_end=2859,
 )
 
 
@@ -1429,8 +1530,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2684,
-  serialized_end=2721,
+  serialized_start=2861,
+  serialized_end=2898,
 )
 
 
@@ -1468,8 +1569,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2723,
-  serialized_end=2835,
+  serialized_start=2900,
+  serialized_end=3012,
 )
 
 
@@ -1493,8 +1594,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2837,
-  serialized_end=2857,
+  serialized_start=3014,
+  serialized_end=3034,
 )
 
 
@@ -1560,8 +1661,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2860,
-  serialized_end=3051,
+  serialized_start=3037,
+  serialized_end=3228,
 )
 
 
@@ -1585,8 +1686,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3053,
-  serialized_end=3086,
+  serialized_start=3230,
+  serialized_end=3263,
 )
 
 
@@ -1617,8 +1718,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3088,
-  serialized_end=3155,
+  serialized_start=3265,
+  serialized_end=3332,
 )
 
 
@@ -1649,8 +1750,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3157,
-  serialized_end=3193,
+  serialized_start=3334,
+  serialized_end=3370,
 )
 
 
@@ -1688,8 +1789,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3195,
-  serialized_end=3291,
+  serialized_start=3372,
+  serialized_end=3468,
 )
 
 
@@ -1720,8 +1821,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3293,
-  serialized_end=3340,
+  serialized_start=3470,
+  serialized_end=3517,
 )
 
 
@@ -1752,8 +1853,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3342,
-  serialized_end=3406,
+  serialized_start=3519,
+  serialized_end=3583,
 )
 
 
@@ -1784,8 +1885,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3408,
-  serialized_end=3441,
+  serialized_start=3585,
+  serialized_end=3618,
 )
 
 
@@ -1823,8 +1924,47 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3443,
-  serialized_end=3530,
+  serialized_start=3620,
+  serialized_end=3707,
+)
+
+
+_SETCUSTOMHOOKSREQUEST = _descriptor.Descriptor(
+  name='SetCustomHooksRequest',
+  full_name='gitaly.SetCustomHooksRequest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='repository', full_name='gitaly.SetCustomHooksRequest.repository', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=b'\230\306,\001', file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='data', full_name='gitaly.SetCustomHooksRequest.data', index=1,
+      number=2, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=3709,
+  serialized_end=3792,
 )
 
 
@@ -1848,8 +1988,33 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3532,
-  serialized_end=3560,
+  serialized_start=3794,
+  serialized_end=3822,
+)
+
+
+_SETCUSTOMHOOKSRESPONSE = _descriptor.Descriptor(
+  name='SetCustomHooksResponse',
+  full_name='gitaly.SetCustomHooksResponse',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=3824,
+  serialized_end=3848,
 )
 
 
@@ -1880,8 +2045,40 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3562,
-  serialized_end=3634,
+  serialized_start=3850,
+  serialized_end=3922,
+)
+
+
+_GETCUSTOMHOOKSREQUEST = _descriptor.Descriptor(
+  name='GetCustomHooksRequest',
+  full_name='gitaly.GetCustomHooksRequest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='repository', full_name='gitaly.GetCustomHooksRequest.repository', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=b'\230\306,\001', file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=3924,
+  serialized_end=3993,
 )
 
 
@@ -1912,8 +2109,40 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3636,
-  serialized_end=3677,
+  serialized_start=3995,
+  serialized_end=4036,
+)
+
+
+_GETCUSTOMHOOKSRESPONSE = _descriptor.Descriptor(
+  name='GetCustomHooksResponse',
+  full_name='gitaly.GetCustomHooksResponse',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='data', full_name='gitaly.GetCustomHooksResponse.data', index=0,
+      number=1, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=4038,
+  serialized_end=4076,
 )
 
 
@@ -1951,8 +2180,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3679,
-  serialized_end=3774,
+  serialized_start=4078,
+  serialized_end=4173,
 )
 
 
@@ -1976,8 +2205,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3776,
-  serialized_end=3812,
+  serialized_start=4175,
+  serialized_end=4211,
 )
 
 
@@ -2008,8 +2237,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3814,
-  serialized_end=3880,
+  serialized_start=4213,
+  serialized_end=4279,
 )
 
 
@@ -2068,8 +2297,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3883,
-  serialized_end=4023,
+  serialized_start=4282,
+  serialized_end=4422,
 )
 
 
@@ -2100,8 +2329,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4025,
-  serialized_end=4097,
+  serialized_start=4424,
+  serialized_end=4496,
 )
 
 
@@ -2132,8 +2361,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4099,
-  serialized_end=4146,
+  serialized_start=4498,
+  serialized_end=4545,
 )
 
 
@@ -2164,8 +2393,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4148,
-  serialized_end=4220,
+  serialized_start=4547,
+  serialized_end=4619,
 )
 
 
@@ -2196,8 +2425,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4222,
-  serialized_end=4267,
+  serialized_start=4621,
+  serialized_end=4666,
 )
 
 
@@ -2228,8 +2457,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4269,
-  serialized_end=4335,
+  serialized_start=4668,
+  serialized_end=4734,
 )
 
 
@@ -2260,8 +2489,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4337,
-  serialized_end=4372,
+  serialized_start=4736,
+  serialized_end=4771,
 )
 
 
@@ -2320,8 +2549,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4375,
-  serialized_end=4544,
+  serialized_start=4774,
+  serialized_end=4943,
 )
 
 
@@ -2345,8 +2574,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4546,
-  serialized_end=4584,
+  serialized_start=4945,
+  serialized_end=4983,
 )
 
 
@@ -2391,8 +2620,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4586,
-  serialized_end=4698,
+  serialized_start=4985,
+  serialized_end=5097,
 )
 
 
@@ -2473,8 +2702,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4789,
-  serialized_end=5147,
+  serialized_start=5188,
+  serialized_end=5546,
 )
 
 _GETRAWCHANGESRESPONSE = _descriptor.Descriptor(
@@ -2504,8 +2733,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4701,
-  serialized_end=5147,
+  serialized_start=5100,
+  serialized_end=5546,
 )
 
 
@@ -2571,8 +2800,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5150,
-  serialized_end=5297,
+  serialized_start=5549,
+  serialized_end=5696,
 )
 
 
@@ -2603,8 +2832,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5299,
-  serialized_end=5341,
+  serialized_start=5698,
+  serialized_end=5740,
 )
 
 
@@ -2656,8 +2885,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5344,
-  serialized_end=5473,
+  serialized_start=5743,
+  serialized_end=5872,
 )
 
 
@@ -2702,8 +2931,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5475,
-  serialized_end=5564,
+  serialized_start=5874,
+  serialized_end=5963,
 )
 
 
@@ -2762,8 +2991,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5567,
-  serialized_end=5708,
+  serialized_start=5966,
+  serialized_end=6107,
 )
 
 
@@ -2794,8 +3023,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5710,
-  serialized_end=5787,
+  serialized_start=6109,
+  serialized_end=6186,
 )
 
 
@@ -2826,8 +3055,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5789,
-  serialized_end=5835,
+  serialized_start=6188,
+  serialized_end=6234,
 )
 
 
@@ -2858,8 +3087,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5837,
-  serialized_end=5908,
+  serialized_start=6236,
+  serialized_end=6307,
 )
 
 
@@ -2883,8 +3112,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5910,
-  serialized_end=5936,
+  serialized_start=6309,
+  serialized_end=6335,
 )
 
 
@@ -2922,8 +3151,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=5938,
-  serialized_end=6032,
+  serialized_start=6337,
+  serialized_end=6431,
 )
 
 
@@ -2947,8 +3176,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6034,
-  serialized_end=6060,
+  serialized_start=6433,
+  serialized_end=6459,
 )
 
 
@@ -2986,8 +3215,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6062,
-  serialized_end=6172,
+  serialized_start=6461,
+  serialized_end=6571,
 )
 
 
@@ -3011,8 +3240,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6174,
-  serialized_end=6203,
+  serialized_start=6573,
+  serialized_end=6602,
 )
 
 
@@ -3031,8 +3260,15 @@
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       serialized_options=b'\230\306,\001', file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='strategy', full_name='gitaly.OptimizeRepositoryRequest.strategy', index=1,
+      number=2, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
   ],
   extensions=[
   ],
   nested_types=[],
   enum_types=[
@@ -3034,8 +3270,9 @@
   ],
   extensions=[
   ],
   nested_types=[],
   enum_types=[
+    _OPTIMIZEREPOSITORYREQUEST_STRATEGY,
   ],
   serialized_options=None,
   is_extendable=False,
@@ -3043,8 +3280,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6205,
-  serialized_end=6278,
+  serialized_start=6605,
+  serialized_end=6824,
 )
 
 
@@ -3068,8 +3305,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6280,
-  serialized_end=6308,
+  serialized_start=6826,
+  serialized_end=6854,
 )
 
 
@@ -3100,8 +3337,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6310,
-  serialized_end=6388,
+  serialized_start=6856,
+  serialized_end=6934,
 )
 
 
@@ -3125,8 +3362,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6390,
-  serialized_end=6423,
+  serialized_start=6936,
+  serialized_end=6969,
 )
 
 
@@ -3164,8 +3401,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6425,
-  serialized_end=6505,
+  serialized_start=6971,
+  serialized_end=7051,
 )
 
 
@@ -3189,8 +3426,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6507,
-  serialized_end=6528,
+  serialized_start=7053,
+  serialized_end=7074,
 )
 
 
@@ -3221,8 +3458,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6530,
-  serialized_end=6593,
+  serialized_start=7076,
+  serialized_end=7139,
 )
 
 
@@ -3253,8 +3490,65 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=6595,
-  serialized_end=6627,
+  serialized_start=7141,
+  serialized_end=7173,
+)
+
+
+_REMOVEALLREQUEST = _descriptor.Descriptor(
+  name='RemoveAllRequest',
+  full_name='gitaly.RemoveAllRequest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='storage_name', full_name='gitaly.RemoveAllRequest.storage_name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=7175,
+  serialized_end=7215,
+)
+
+
+_REMOVEALLRESPONSE = _descriptor.Descriptor(
+  name='RemoveAllResponse',
+  full_name='gitaly.RemoveAllResponse',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=7217,
+  serialized_end=7236,
 )
 
 _REPOSITORYEXISTSREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
@@ -3267,8 +3561,10 @@
 _WRITECOMMITGRAPHREQUEST_SPLITSTRATEGY.containing_type = _WRITECOMMITGRAPHREQUEST
 _CLEANUPREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _REPOSITORYSIZEREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
+_OBJECTFORMATREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
+_OBJECTFORMATRESPONSE.fields_by_name['format'].enum_type = shared__pb2._OBJECTFORMAT
 _APPLYGITATTRIBUTESREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _FETCHBUNDLEREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _FETCHREMOTEREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _FETCHREMOTEREQUEST.fields_by_name['remote_params'].message_type = _REMOTE
 _CREATEREPOSITORYREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
@@ -3270,8 +3566,9 @@
 _APPLYGITATTRIBUTESREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _FETCHBUNDLEREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _FETCHREMOTEREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _FETCHREMOTEREQUEST.fields_by_name['remote_params'].message_type = _REMOTE
 _CREATEREPOSITORYREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
+_CREATEREPOSITORYREQUEST.fields_by_name['object_format'].enum_type = shared__pb2._OBJECTFORMAT
 _GETARCHIVEREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _GETARCHIVEREQUEST.fields_by_name['format'].enum_type = _GETARCHIVEREQUEST_FORMAT
 _GETARCHIVEREQUEST_FORMAT.containing_type = _GETARCHIVEREQUEST
@@ -3288,4 +3585,5 @@
 _CREATEBUNDLEFROMREFLISTREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _GETCONFIGREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _RESTORECUSTOMHOOKSREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
+_SETCUSTOMHOOKSREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _BACKUPCUSTOMHOOKSREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
@@ -3291,4 +3589,5 @@
 _BACKUPCUSTOMHOOKSREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
+_GETCUSTOMHOOKSREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _CREATEREPOSITORYFROMBUNDLEREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _FINDLICENSEREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _GETINFOATTRIBUTESREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
@@ -3308,6 +3607,8 @@
 _REPLICATEREPOSITORYREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _REPLICATEREPOSITORYREQUEST.fields_by_name['source'].message_type = shared__pb2._REPOSITORY
 _OPTIMIZEREPOSITORYREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
+_OPTIMIZEREPOSITORYREQUEST.fields_by_name['strategy'].enum_type = _OPTIMIZEREPOSITORYREQUEST_STRATEGY
+_OPTIMIZEREPOSITORYREQUEST_STRATEGY.containing_type = _OPTIMIZEREPOSITORYREQUEST
 _PRUNEUNREACHABLEOBJECTSREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _SETFULLPATHREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
 _FULLPATHREQUEST.fields_by_name['repository'].message_type = shared__pb2._REPOSITORY
@@ -3327,6 +3628,8 @@
 DESCRIPTOR.message_types_by_name['CleanupResponse'] = _CLEANUPRESPONSE
 DESCRIPTOR.message_types_by_name['RepositorySizeRequest'] = _REPOSITORYSIZEREQUEST
 DESCRIPTOR.message_types_by_name['RepositorySizeResponse'] = _REPOSITORYSIZERESPONSE
+DESCRIPTOR.message_types_by_name['ObjectFormatRequest'] = _OBJECTFORMATREQUEST
+DESCRIPTOR.message_types_by_name['ObjectFormatResponse'] = _OBJECTFORMATRESPONSE
 DESCRIPTOR.message_types_by_name['ApplyGitattributesRequest'] = _APPLYGITATTRIBUTESREQUEST
 DESCRIPTOR.message_types_by_name['ApplyGitattributesResponse'] = _APPLYGITATTRIBUTESRESPONSE
 DESCRIPTOR.message_types_by_name['FetchBundleRequest'] = _FETCHBUNDLEREQUEST
@@ -3358,4 +3661,5 @@
 DESCRIPTOR.message_types_by_name['GetConfigRequest'] = _GETCONFIGREQUEST
 DESCRIPTOR.message_types_by_name['GetConfigResponse'] = _GETCONFIGRESPONSE
 DESCRIPTOR.message_types_by_name['RestoreCustomHooksRequest'] = _RESTORECUSTOMHOOKSREQUEST
+DESCRIPTOR.message_types_by_name['SetCustomHooksRequest'] = _SETCUSTOMHOOKSREQUEST
 DESCRIPTOR.message_types_by_name['RestoreCustomHooksResponse'] = _RESTORECUSTOMHOOKSRESPONSE
@@ -3361,2 +3665,3 @@
 DESCRIPTOR.message_types_by_name['RestoreCustomHooksResponse'] = _RESTORECUSTOMHOOKSRESPONSE
+DESCRIPTOR.message_types_by_name['SetCustomHooksResponse'] = _SETCUSTOMHOOKSRESPONSE
 DESCRIPTOR.message_types_by_name['BackupCustomHooksRequest'] = _BACKUPCUSTOMHOOKSREQUEST
@@ -3362,2 +3667,3 @@
 DESCRIPTOR.message_types_by_name['BackupCustomHooksRequest'] = _BACKUPCUSTOMHOOKSREQUEST
+DESCRIPTOR.message_types_by_name['GetCustomHooksRequest'] = _GETCUSTOMHOOKSREQUEST
 DESCRIPTOR.message_types_by_name['BackupCustomHooksResponse'] = _BACKUPCUSTOMHOOKSRESPONSE
@@ -3363,4 +3669,5 @@
 DESCRIPTOR.message_types_by_name['BackupCustomHooksResponse'] = _BACKUPCUSTOMHOOKSRESPONSE
+DESCRIPTOR.message_types_by_name['GetCustomHooksResponse'] = _GETCUSTOMHOOKSRESPONSE
 DESCRIPTOR.message_types_by_name['CreateRepositoryFromBundleRequest'] = _CREATEREPOSITORYFROMBUNDLEREQUEST
 DESCRIPTOR.message_types_by_name['CreateRepositoryFromBundleResponse'] = _CREATEREPOSITORYFROMBUNDLERESPONSE
 DESCRIPTOR.message_types_by_name['FindLicenseRequest'] = _FINDLICENSEREQUEST
@@ -3396,6 +3703,8 @@
 DESCRIPTOR.message_types_by_name['SetFullPathResponse'] = _SETFULLPATHRESPONSE
 DESCRIPTOR.message_types_by_name['FullPathRequest'] = _FULLPATHREQUEST
 DESCRIPTOR.message_types_by_name['FullPathResponse'] = _FULLPATHRESPONSE
+DESCRIPTOR.message_types_by_name['RemoveAllRequest'] = _REMOVEALLREQUEST
+DESCRIPTOR.message_types_by_name['RemoveAllResponse'] = _REMOVEALLRESPONSE
 _sym_db.RegisterFileDescriptor(DESCRIPTOR)
 
 RepositoryExistsRequest = _reflection.GeneratedProtocolMessageType('RepositoryExistsRequest', (_message.Message,), {
@@ -3510,6 +3819,20 @@
   })
 _sym_db.RegisterMessage(RepositorySizeResponse)
 
+ObjectFormatRequest = _reflection.GeneratedProtocolMessageType('ObjectFormatRequest', (_message.Message,), {
+  'DESCRIPTOR' : _OBJECTFORMATREQUEST,
+  '__module__' : 'repository_pb2'
+  # @@protoc_insertion_point(class_scope:gitaly.ObjectFormatRequest)
+  })
+_sym_db.RegisterMessage(ObjectFormatRequest)
+
+ObjectFormatResponse = _reflection.GeneratedProtocolMessageType('ObjectFormatResponse', (_message.Message,), {
+  'DESCRIPTOR' : _OBJECTFORMATRESPONSE,
+  '__module__' : 'repository_pb2'
+  # @@protoc_insertion_point(class_scope:gitaly.ObjectFormatResponse)
+  })
+_sym_db.RegisterMessage(ObjectFormatResponse)
+
 ApplyGitattributesRequest = _reflection.GeneratedProtocolMessageType('ApplyGitattributesRequest', (_message.Message,), {
   'DESCRIPTOR' : _APPLYGITATTRIBUTESREQUEST,
   '__module__' : 'repository_pb2'
@@ -3727,6 +4050,13 @@
   })
 _sym_db.RegisterMessage(RestoreCustomHooksRequest)
 
+SetCustomHooksRequest = _reflection.GeneratedProtocolMessageType('SetCustomHooksRequest', (_message.Message,), {
+  'DESCRIPTOR' : _SETCUSTOMHOOKSREQUEST,
+  '__module__' : 'repository_pb2'
+  # @@protoc_insertion_point(class_scope:gitaly.SetCustomHooksRequest)
+  })
+_sym_db.RegisterMessage(SetCustomHooksRequest)
+
 RestoreCustomHooksResponse = _reflection.GeneratedProtocolMessageType('RestoreCustomHooksResponse', (_message.Message,), {
   'DESCRIPTOR' : _RESTORECUSTOMHOOKSRESPONSE,
   '__module__' : 'repository_pb2'
@@ -3734,6 +4064,13 @@
   })
 _sym_db.RegisterMessage(RestoreCustomHooksResponse)
 
+SetCustomHooksResponse = _reflection.GeneratedProtocolMessageType('SetCustomHooksResponse', (_message.Message,), {
+  'DESCRIPTOR' : _SETCUSTOMHOOKSRESPONSE,
+  '__module__' : 'repository_pb2'
+  # @@protoc_insertion_point(class_scope:gitaly.SetCustomHooksResponse)
+  })
+_sym_db.RegisterMessage(SetCustomHooksResponse)
+
 BackupCustomHooksRequest = _reflection.GeneratedProtocolMessageType('BackupCustomHooksRequest', (_message.Message,), {
   'DESCRIPTOR' : _BACKUPCUSTOMHOOKSREQUEST,
   '__module__' : 'repository_pb2'
@@ -3741,6 +4078,13 @@
   })
 _sym_db.RegisterMessage(BackupCustomHooksRequest)
 
+GetCustomHooksRequest = _reflection.GeneratedProtocolMessageType('GetCustomHooksRequest', (_message.Message,), {
+  'DESCRIPTOR' : _GETCUSTOMHOOKSREQUEST,
+  '__module__' : 'repository_pb2'
+  # @@protoc_insertion_point(class_scope:gitaly.GetCustomHooksRequest)
+  })
+_sym_db.RegisterMessage(GetCustomHooksRequest)
+
 BackupCustomHooksResponse = _reflection.GeneratedProtocolMessageType('BackupCustomHooksResponse', (_message.Message,), {
   'DESCRIPTOR' : _BACKUPCUSTOMHOOKSRESPONSE,
   '__module__' : 'repository_pb2'
@@ -3748,6 +4092,13 @@
   })
 _sym_db.RegisterMessage(BackupCustomHooksResponse)
 
+GetCustomHooksResponse = _reflection.GeneratedProtocolMessageType('GetCustomHooksResponse', (_message.Message,), {
+  'DESCRIPTOR' : _GETCUSTOMHOOKSRESPONSE,
+  '__module__' : 'repository_pb2'
+  # @@protoc_insertion_point(class_scope:gitaly.GetCustomHooksResponse)
+  })
+_sym_db.RegisterMessage(GetCustomHooksResponse)
+
 CreateRepositoryFromBundleRequest = _reflection.GeneratedProtocolMessageType('CreateRepositoryFromBundleRequest', (_message.Message,), {
   'DESCRIPTOR' : _CREATEREPOSITORYFROMBUNDLEREQUEST,
   '__module__' : 'repository_pb2'
@@ -4001,6 +4352,20 @@
   })
 _sym_db.RegisterMessage(FullPathResponse)
 
+RemoveAllRequest = _reflection.GeneratedProtocolMessageType('RemoveAllRequest', (_message.Message,), {
+  'DESCRIPTOR' : _REMOVEALLREQUEST,
+  '__module__' : 'repository_pb2'
+  # @@protoc_insertion_point(class_scope:gitaly.RemoveAllRequest)
+  })
+_sym_db.RegisterMessage(RemoveAllRequest)
+
+RemoveAllResponse = _reflection.GeneratedProtocolMessageType('RemoveAllResponse', (_message.Message,), {
+  'DESCRIPTOR' : _REMOVEALLRESPONSE,
+  '__module__' : 'repository_pb2'
+  # @@protoc_insertion_point(class_scope:gitaly.RemoveAllResponse)
+  })
+_sym_db.RegisterMessage(RemoveAllResponse)
+
 
 DESCRIPTOR._options = None
 _REPOSITORYEXISTSREQUEST.fields_by_name['repository']._options = None
@@ -4011,6 +4376,7 @@
 _WRITECOMMITGRAPHREQUEST.fields_by_name['repository']._options = None
 _CLEANUPREQUEST.fields_by_name['repository']._options = None
 _REPOSITORYSIZEREQUEST.fields_by_name['repository']._options = None
+_OBJECTFORMATREQUEST.fields_by_name['repository']._options = None
 _APPLYGITATTRIBUTESREQUEST.fields_by_name['repository']._options = None
 _FETCHBUNDLEREQUEST.fields_by_name['repository']._options = None
 _FETCHREMOTEREQUEST.fields_by_name['repository']._options = None
@@ -4028,4 +4394,5 @@
 _CREATEBUNDLEFROMREFLISTREQUEST.fields_by_name['repository']._options = None
 _GETCONFIGREQUEST.fields_by_name['repository']._options = None
 _RESTORECUSTOMHOOKSREQUEST.fields_by_name['repository']._options = None
+_SETCUSTOMHOOKSREQUEST.fields_by_name['repository']._options = None
 _BACKUPCUSTOMHOOKSREQUEST.fields_by_name['repository']._options = None
@@ -4031,4 +4398,5 @@
 _BACKUPCUSTOMHOOKSREQUEST.fields_by_name['repository']._options = None
+_GETCUSTOMHOOKSREQUEST.fields_by_name['repository']._options = None
 _CREATEREPOSITORYFROMBUNDLEREQUEST.fields_by_name['repository']._options = None
 _FINDLICENSEREQUEST.fields_by_name['repository']._options = None
 _GETINFOATTRIBUTESREQUEST.fields_by_name['repository']._options = None
@@ -4056,8 +4424,8 @@
   index=0,
   serialized_options=None,
   create_key=_descriptor._internal_create_key,
-  serialized_start=6630,
-  serialized_end=10569,
+  serialized_start=7239,
+  serialized_end=11527,
   methods=[
   _descriptor.MethodDescriptor(
     name='RepositoryExists',
@@ -4130,5 +4498,15 @@
     create_key=_descriptor._internal_create_key,
   ),
   _descriptor.MethodDescriptor(
+    name='ObjectFormat',
+    full_name='gitaly.RepositoryService.ObjectFormat',
+    index=7,
+    containing_service=None,
+    input_type=_OBJECTFORMATREQUEST,
+    output_type=_OBJECTFORMATRESPONSE,
+    serialized_options=b'\372\227(\002\010\002',
+    create_key=_descriptor._internal_create_key,
+  ),
+  _descriptor.MethodDescriptor(
     name='ApplyGitattributes',
     full_name='gitaly.RepositoryService.ApplyGitattributes',
@@ -4133,6 +4511,6 @@
     name='ApplyGitattributes',
     full_name='gitaly.RepositoryService.ApplyGitattributes',
-    index=7,
+    index=8,
     containing_service=None,
     input_type=_APPLYGITATTRIBUTESREQUEST,
     output_type=_APPLYGITATTRIBUTESRESPONSE,
@@ -4142,7 +4520,7 @@
   _descriptor.MethodDescriptor(
     name='FetchRemote',
     full_name='gitaly.RepositoryService.FetchRemote',
-    index=8,
+    index=9,
     containing_service=None,
     input_type=_FETCHREMOTEREQUEST,
     output_type=_FETCHREMOTERESPONSE,
@@ -4152,7 +4530,7 @@
   _descriptor.MethodDescriptor(
     name='CreateRepository',
     full_name='gitaly.RepositoryService.CreateRepository',
-    index=9,
+    index=10,
     containing_service=None,
     input_type=_CREATEREPOSITORYREQUEST,
     output_type=_CREATEREPOSITORYRESPONSE,
@@ -4162,7 +4540,7 @@
   _descriptor.MethodDescriptor(
     name='GetArchive',
     full_name='gitaly.RepositoryService.GetArchive',
-    index=10,
+    index=11,
     containing_service=None,
     input_type=_GETARCHIVEREQUEST,
     output_type=_GETARCHIVERESPONSE,
@@ -4172,7 +4550,7 @@
   _descriptor.MethodDescriptor(
     name='HasLocalBranches',
     full_name='gitaly.RepositoryService.HasLocalBranches',
-    index=11,
+    index=12,
     containing_service=None,
     input_type=_HASLOCALBRANCHESREQUEST,
     output_type=_HASLOCALBRANCHESRESPONSE,
@@ -4182,7 +4560,7 @@
   _descriptor.MethodDescriptor(
     name='FetchSourceBranch',
     full_name='gitaly.RepositoryService.FetchSourceBranch',
-    index=12,
+    index=13,
     containing_service=None,
     input_type=_FETCHSOURCEBRANCHREQUEST,
     output_type=_FETCHSOURCEBRANCHRESPONSE,
@@ -4192,7 +4570,7 @@
   _descriptor.MethodDescriptor(
     name='Fsck',
     full_name='gitaly.RepositoryService.Fsck',
-    index=13,
+    index=14,
     containing_service=None,
     input_type=_FSCKREQUEST,
     output_type=_FSCKRESPONSE,
@@ -4202,7 +4580,7 @@
   _descriptor.MethodDescriptor(
     name='WriteRef',
     full_name='gitaly.RepositoryService.WriteRef',
-    index=14,
+    index=15,
     containing_service=None,
     input_type=_WRITEREFREQUEST,
     output_type=_WRITEREFRESPONSE,
@@ -4212,7 +4590,7 @@
   _descriptor.MethodDescriptor(
     name='FindMergeBase',
     full_name='gitaly.RepositoryService.FindMergeBase',
-    index=15,
+    index=16,
     containing_service=None,
     input_type=_FINDMERGEBASEREQUEST,
     output_type=_FINDMERGEBASERESPONSE,
@@ -4222,7 +4600,7 @@
   _descriptor.MethodDescriptor(
     name='CreateFork',
     full_name='gitaly.RepositoryService.CreateFork',
-    index=16,
+    index=17,
     containing_service=None,
     input_type=_CREATEFORKREQUEST,
     output_type=_CREATEFORKRESPONSE,
@@ -4232,7 +4610,7 @@
   _descriptor.MethodDescriptor(
     name='CreateRepositoryFromURL',
     full_name='gitaly.RepositoryService.CreateRepositoryFromURL',
-    index=17,
+    index=18,
     containing_service=None,
     input_type=_CREATEREPOSITORYFROMURLREQUEST,
     output_type=_CREATEREPOSITORYFROMURLRESPONSE,
@@ -4242,7 +4620,7 @@
   _descriptor.MethodDescriptor(
     name='CreateBundle',
     full_name='gitaly.RepositoryService.CreateBundle',
-    index=18,
+    index=19,
     containing_service=None,
     input_type=_CREATEBUNDLEREQUEST,
     output_type=_CREATEBUNDLERESPONSE,
@@ -4252,7 +4630,7 @@
   _descriptor.MethodDescriptor(
     name='CreateBundleFromRefList',
     full_name='gitaly.RepositoryService.CreateBundleFromRefList',
-    index=19,
+    index=20,
     containing_service=None,
     input_type=_CREATEBUNDLEFROMREFLISTREQUEST,
     output_type=_CREATEBUNDLEFROMREFLISTRESPONSE,
@@ -4262,7 +4640,7 @@
   _descriptor.MethodDescriptor(
     name='FetchBundle',
     full_name='gitaly.RepositoryService.FetchBundle',
-    index=20,
+    index=21,
     containing_service=None,
     input_type=_FETCHBUNDLEREQUEST,
     output_type=_FETCHBUNDLERESPONSE,
@@ -4272,7 +4650,7 @@
   _descriptor.MethodDescriptor(
     name='CreateRepositoryFromBundle',
     full_name='gitaly.RepositoryService.CreateRepositoryFromBundle',
-    index=21,
+    index=22,
     containing_service=None,
     input_type=_CREATEREPOSITORYFROMBUNDLEREQUEST,
     output_type=_CREATEREPOSITORYFROMBUNDLERESPONSE,
@@ -4282,7 +4660,7 @@
   _descriptor.MethodDescriptor(
     name='GetConfig',
     full_name='gitaly.RepositoryService.GetConfig',
-    index=22,
+    index=23,
     containing_service=None,
     input_type=_GETCONFIGREQUEST,
     output_type=_GETCONFIGRESPONSE,
@@ -4292,7 +4670,7 @@
   _descriptor.MethodDescriptor(
     name='FindLicense',
     full_name='gitaly.RepositoryService.FindLicense',
-    index=23,
+    index=24,
     containing_service=None,
     input_type=_FINDLICENSEREQUEST,
     output_type=_FINDLICENSERESPONSE,
@@ -4302,7 +4680,7 @@
   _descriptor.MethodDescriptor(
     name='GetInfoAttributes',
     full_name='gitaly.RepositoryService.GetInfoAttributes',
-    index=24,
+    index=25,
     containing_service=None,
     input_type=_GETINFOATTRIBUTESREQUEST,
     output_type=_GETINFOATTRIBUTESRESPONSE,
@@ -4312,7 +4690,7 @@
   _descriptor.MethodDescriptor(
     name='CalculateChecksum',
     full_name='gitaly.RepositoryService.CalculateChecksum',
-    index=25,
+    index=26,
     containing_service=None,
     input_type=_CALCULATECHECKSUMREQUEST,
     output_type=_CALCULATECHECKSUMRESPONSE,
@@ -4322,7 +4700,7 @@
   _descriptor.MethodDescriptor(
     name='Cleanup',
     full_name='gitaly.RepositoryService.Cleanup',
-    index=26,
+    index=27,
     containing_service=None,
     input_type=_CLEANUPREQUEST,
     output_type=_CLEANUPRESPONSE,
@@ -4332,7 +4710,7 @@
   _descriptor.MethodDescriptor(
     name='GetSnapshot',
     full_name='gitaly.RepositoryService.GetSnapshot',
-    index=27,
+    index=28,
     containing_service=None,
     input_type=_GETSNAPSHOTREQUEST,
     output_type=_GETSNAPSHOTRESPONSE,
@@ -4342,7 +4720,7 @@
   _descriptor.MethodDescriptor(
     name='CreateRepositoryFromSnapshot',
     full_name='gitaly.RepositoryService.CreateRepositoryFromSnapshot',
-    index=28,
+    index=29,
     containing_service=None,
     input_type=_CREATEREPOSITORYFROMSNAPSHOTREQUEST,
     output_type=_CREATEREPOSITORYFROMSNAPSHOTRESPONSE,
@@ -4352,7 +4730,7 @@
   _descriptor.MethodDescriptor(
     name='GetRawChanges',
     full_name='gitaly.RepositoryService.GetRawChanges',
-    index=29,
+    index=30,
     containing_service=None,
     input_type=_GETRAWCHANGESREQUEST,
     output_type=_GETRAWCHANGESRESPONSE,
@@ -4362,7 +4740,7 @@
   _descriptor.MethodDescriptor(
     name='SearchFilesByContent',
     full_name='gitaly.RepositoryService.SearchFilesByContent',
-    index=30,
+    index=31,
     containing_service=None,
     input_type=_SEARCHFILESBYCONTENTREQUEST,
     output_type=_SEARCHFILESBYCONTENTRESPONSE,
@@ -4372,7 +4750,7 @@
   _descriptor.MethodDescriptor(
     name='SearchFilesByName',
     full_name='gitaly.RepositoryService.SearchFilesByName',
-    index=31,
+    index=32,
     containing_service=None,
     input_type=_SEARCHFILESBYNAMEREQUEST,
     output_type=_SEARCHFILESBYNAMERESPONSE,
@@ -4382,7 +4760,7 @@
   _descriptor.MethodDescriptor(
     name='RestoreCustomHooks',
     full_name='gitaly.RepositoryService.RestoreCustomHooks',
-    index=32,
+    index=33,
     containing_service=None,
     input_type=_RESTORECUSTOMHOOKSREQUEST,
     output_type=_RESTORECUSTOMHOOKSRESPONSE,
@@ -4386,9 +4764,19 @@
     containing_service=None,
     input_type=_RESTORECUSTOMHOOKSREQUEST,
     output_type=_RESTORECUSTOMHOOKSRESPONSE,
+    serialized_options=b'\210\002\001\372\227(\002\010\001',
+    create_key=_descriptor._internal_create_key,
+  ),
+  _descriptor.MethodDescriptor(
+    name='SetCustomHooks',
+    full_name='gitaly.RepositoryService.SetCustomHooks',
+    index=34,
+    containing_service=None,
+    input_type=_SETCUSTOMHOOKSREQUEST,
+    output_type=_SETCUSTOMHOOKSRESPONSE,
     serialized_options=b'\372\227(\002\010\001',
     create_key=_descriptor._internal_create_key,
   ),
   _descriptor.MethodDescriptor(
     name='BackupCustomHooks',
     full_name='gitaly.RepositoryService.BackupCustomHooks',
@@ -4389,10 +4777,10 @@
     serialized_options=b'\372\227(\002\010\001',
     create_key=_descriptor._internal_create_key,
   ),
   _descriptor.MethodDescriptor(
     name='BackupCustomHooks',
     full_name='gitaly.RepositoryService.BackupCustomHooks',
-    index=33,
+    index=35,
     containing_service=None,
     input_type=_BACKUPCUSTOMHOOKSREQUEST,
     output_type=_BACKUPCUSTOMHOOKSRESPONSE,
@@ -4396,9 +4784,19 @@
     containing_service=None,
     input_type=_BACKUPCUSTOMHOOKSREQUEST,
     output_type=_BACKUPCUSTOMHOOKSRESPONSE,
+    serialized_options=b'\210\002\001\372\227(\002\010\002',
+    create_key=_descriptor._internal_create_key,
+  ),
+  _descriptor.MethodDescriptor(
+    name='GetCustomHooks',
+    full_name='gitaly.RepositoryService.GetCustomHooks',
+    index=36,
+    containing_service=None,
+    input_type=_GETCUSTOMHOOKSREQUEST,
+    output_type=_GETCUSTOMHOOKSRESPONSE,
     serialized_options=b'\372\227(\002\010\002',
     create_key=_descriptor._internal_create_key,
   ),
   _descriptor.MethodDescriptor(
     name='GetObjectDirectorySize',
     full_name='gitaly.RepositoryService.GetObjectDirectorySize',
@@ -4399,10 +4797,10 @@
     serialized_options=b'\372\227(\002\010\002',
     create_key=_descriptor._internal_create_key,
   ),
   _descriptor.MethodDescriptor(
     name='GetObjectDirectorySize',
     full_name='gitaly.RepositoryService.GetObjectDirectorySize',
-    index=34,
+    index=37,
     containing_service=None,
     input_type=_GETOBJECTDIRECTORYSIZEREQUEST,
     output_type=_GETOBJECTDIRECTORYSIZERESPONSE,
@@ -4412,7 +4810,7 @@
   _descriptor.MethodDescriptor(
     name='RemoveRepository',
     full_name='gitaly.RepositoryService.RemoveRepository',
-    index=35,
+    index=38,
     containing_service=None,
     input_type=_REMOVEREPOSITORYREQUEST,
     output_type=_REMOVEREPOSITORYRESPONSE,
@@ -4422,7 +4820,7 @@
   _descriptor.MethodDescriptor(
     name='RenameRepository',
     full_name='gitaly.RepositoryService.RenameRepository',
-    index=36,
+    index=39,
     containing_service=None,
     input_type=_RENAMEREPOSITORYREQUEST,
     output_type=_RENAMEREPOSITORYRESPONSE,
@@ -4432,7 +4830,7 @@
   _descriptor.MethodDescriptor(
     name='ReplicateRepository',
     full_name='gitaly.RepositoryService.ReplicateRepository',
-    index=37,
+    index=40,
     containing_service=None,
     input_type=_REPLICATEREPOSITORYREQUEST,
     output_type=_REPLICATEREPOSITORYRESPONSE,
@@ -4442,7 +4840,7 @@
   _descriptor.MethodDescriptor(
     name='OptimizeRepository',
     full_name='gitaly.RepositoryService.OptimizeRepository',
-    index=38,
+    index=41,
     containing_service=None,
     input_type=_OPTIMIZEREPOSITORYREQUEST,
     output_type=_OPTIMIZEREPOSITORYRESPONSE,
@@ -4452,7 +4850,7 @@
   _descriptor.MethodDescriptor(
     name='PruneUnreachableObjects',
     full_name='gitaly.RepositoryService.PruneUnreachableObjects',
-    index=39,
+    index=42,
     containing_service=None,
     input_type=_PRUNEUNREACHABLEOBJECTSREQUEST,
     output_type=_PRUNEUNREACHABLEOBJECTSRESPONSE,
@@ -4462,7 +4860,7 @@
   _descriptor.MethodDescriptor(
     name='SetFullPath',
     full_name='gitaly.RepositoryService.SetFullPath',
-    index=40,
+    index=43,
     containing_service=None,
     input_type=_SETFULLPATHREQUEST,
     output_type=_SETFULLPATHRESPONSE,
@@ -4466,9 +4864,9 @@
     containing_service=None,
     input_type=_SETFULLPATHREQUEST,
     output_type=_SETFULLPATHRESPONSE,
-    serialized_options=b'\372\227(\002\010\001',
+    serialized_options=b'\210\002\001\372\227(\002\010\001',
     create_key=_descriptor._internal_create_key,
   ),
   _descriptor.MethodDescriptor(
     name='FullPath',
     full_name='gitaly.RepositoryService.FullPath',
@@ -4470,9 +4868,9 @@
     create_key=_descriptor._internal_create_key,
   ),
   _descriptor.MethodDescriptor(
     name='FullPath',
     full_name='gitaly.RepositoryService.FullPath',
-    index=41,
+    index=44,
     containing_service=None,
     input_type=_FULLPATHREQUEST,
     output_type=_FULLPATHRESPONSE,
@@ -4476,7 +4874,17 @@
     containing_service=None,
     input_type=_FULLPATHREQUEST,
     output_type=_FULLPATHRESPONSE,
-    serialized_options=b'\372\227(\002\010\002',
+    serialized_options=b'\210\002\001\372\227(\002\010\002',
+    create_key=_descriptor._internal_create_key,
+  ),
+  _descriptor.MethodDescriptor(
+    name='RemoveAll',
+    full_name='gitaly.RepositoryService.RemoveAll',
+    index=45,
+    containing_service=None,
+    input_type=_REMOVEALLREQUEST,
+    output_type=_REMOVEALLRESPONSE,
+    serialized_options=b'\200\230(\001',
     create_key=_descriptor._internal_create_key,
   ),
 ])
diff --git a/hgitaly/stub/repository_pb2_grpc.py b/hgitaly/stub/repository_pb2_grpc.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zdHViL3JlcG9zaXRvcnlfcGIyX2dycGMucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zdHViL3JlcG9zaXRvcnlfcGIyX2dycGMucHk= 100644
--- a/hgitaly/stub/repository_pb2_grpc.py
+++ b/hgitaly/stub/repository_pb2_grpc.py
@@ -50,6 +50,11 @@
                 request_serializer=repository__pb2.RepositorySizeRequest.SerializeToString,
                 response_deserializer=repository__pb2.RepositorySizeResponse.FromString,
                 )
+        self.ObjectFormat = channel.unary_unary(
+                '/gitaly.RepositoryService/ObjectFormat',
+                request_serializer=repository__pb2.ObjectFormatRequest.SerializeToString,
+                response_deserializer=repository__pb2.ObjectFormatResponse.FromString,
+                )
         self.ApplyGitattributes = channel.unary_unary(
                 '/gitaly.RepositoryService/ApplyGitattributes',
                 request_serializer=repository__pb2.ApplyGitattributesRequest.SerializeToString,
@@ -180,8 +185,13 @@
                 request_serializer=repository__pb2.RestoreCustomHooksRequest.SerializeToString,
                 response_deserializer=repository__pb2.RestoreCustomHooksResponse.FromString,
                 )
+        self.SetCustomHooks = channel.stream_unary(
+                '/gitaly.RepositoryService/SetCustomHooks',
+                request_serializer=repository__pb2.SetCustomHooksRequest.SerializeToString,
+                response_deserializer=repository__pb2.SetCustomHooksResponse.FromString,
+                )
         self.BackupCustomHooks = channel.unary_stream(
                 '/gitaly.RepositoryService/BackupCustomHooks',
                 request_serializer=repository__pb2.BackupCustomHooksRequest.SerializeToString,
                 response_deserializer=repository__pb2.BackupCustomHooksResponse.FromString,
                 )
@@ -183,8 +193,13 @@
         self.BackupCustomHooks = channel.unary_stream(
                 '/gitaly.RepositoryService/BackupCustomHooks',
                 request_serializer=repository__pb2.BackupCustomHooksRequest.SerializeToString,
                 response_deserializer=repository__pb2.BackupCustomHooksResponse.FromString,
                 )
+        self.GetCustomHooks = channel.unary_stream(
+                '/gitaly.RepositoryService/GetCustomHooks',
+                request_serializer=repository__pb2.GetCustomHooksRequest.SerializeToString,
+                response_deserializer=repository__pb2.GetCustomHooksResponse.FromString,
+                )
         self.GetObjectDirectorySize = channel.unary_unary(
                 '/gitaly.RepositoryService/GetObjectDirectorySize',
                 request_serializer=repository__pb2.GetObjectDirectorySizeRequest.SerializeToString,
@@ -225,6 +240,11 @@
                 request_serializer=repository__pb2.FullPathRequest.SerializeToString,
                 response_deserializer=repository__pb2.FullPathResponse.FromString,
                 )
+        self.RemoveAll = channel.unary_unary(
+                '/gitaly.RepositoryService/RemoveAll',
+                request_serializer=repository__pb2.RemoveAllRequest.SerializeToString,
+                response_deserializer=repository__pb2.RemoveAllResponse.FromString,
+                )
 
 
 class RepositoryServiceServicer(object):
@@ -280,6 +300,13 @@
         context.set_details('Method not implemented!')
         raise NotImplementedError('Method not implemented!')
 
+    def ObjectFormat(self, request, context):
+        """ObjectFormat determines the object format that is being used by the repository.
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
+
     def ApplyGitattributes(self, request, context):
         """This comment is left unintentionally blank.
         """
@@ -325,7 +352,8 @@
         raise NotImplementedError('Method not implemented!')
 
     def Fsck(self, request, context):
-        """This comment is left unintentionally blank.
+        """Fsck checks the repository for consistency via git-fsck(1). This can be used to check for
+        repository corruption.
         """
         context.set_code(grpc.StatusCode.UNIMPLEMENTED)
         context.set_details('Method not implemented!')
@@ -399,7 +427,8 @@
         raise NotImplementedError('Method not implemented!')
 
     def FindLicense(self, request, context):
-        """This comment is left unintentionally blank.
+        """FindLicense looks in the given repository and attempts to detect all the
+        details about the license used in the repository.
         """
         context.set_code(grpc.StatusCode.UNIMPLEMENTED)
         context.set_details('Method not implemented!')
@@ -462,10 +491,21 @@
         raise NotImplementedError('Method not implemented!')
 
     def RestoreCustomHooks(self, request_iterator, context):
-        """This comment is left unintentionally blank.
+        """RestoreCustomHooks sets the git hooks for a repository. The hooks are sent
+        in a tar archive containing a `custom_hooks` directory. This directory is
+        ultimately extracted to the repository.
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
+
+    def SetCustomHooks(self, request_iterator, context):
+        """SetCustomHooks sets the git hooks for a repository. The hooks are sent in a
+        tar archive containing a `custom_hooks` directory. This directory is 
+        ultimately extracted to the repository.
         """
         context.set_code(grpc.StatusCode.UNIMPLEMENTED)
         context.set_details('Method not implemented!')
         raise NotImplementedError('Method not implemented!')
 
     def BackupCustomHooks(self, request, context):
@@ -466,10 +506,21 @@
         """
         context.set_code(grpc.StatusCode.UNIMPLEMENTED)
         context.set_details('Method not implemented!')
         raise NotImplementedError('Method not implemented!')
 
     def BackupCustomHooks(self, request, context):
-        """This comment is left unintentionally blank.
+        """BackupCustomHooks fetches the git hooks for a repository. The hooks are
+        sent in a tar archive containing a `custom_hooks` directory. If no hooks
+        are present in the repository, the response will have no data.
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
+
+    def GetCustomHooks(self, request, context):
+        """GetCustomHooks fetches the git hooks for a repository. The hooks are sent
+        in a tar archive containing a `custom_hooks` directory. If no hooks are
+        present in the repository, the response will have no data.
         """
         context.set_code(grpc.StatusCode.UNIMPLEMENTED)
         context.set_details('Method not implemented!')
@@ -552,6 +603,13 @@
         context.set_details('Method not implemented!')
         raise NotImplementedError('Method not implemented!')
 
+    def RemoveAll(self, request, context):
+        """RemoveAll deletes all repositories on a specified storage.
+        """
+        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+        context.set_details('Method not implemented!')
+        raise NotImplementedError('Method not implemented!')
+
 
 def add_RepositoryServiceServicer_to_server(servicer, server):
     rpc_method_handlers = {
@@ -590,6 +648,11 @@
                     request_deserializer=repository__pb2.RepositorySizeRequest.FromString,
                     response_serializer=repository__pb2.RepositorySizeResponse.SerializeToString,
             ),
+            'ObjectFormat': grpc.unary_unary_rpc_method_handler(
+                    servicer.ObjectFormat,
+                    request_deserializer=repository__pb2.ObjectFormatRequest.FromString,
+                    response_serializer=repository__pb2.ObjectFormatResponse.SerializeToString,
+            ),
             'ApplyGitattributes': grpc.unary_unary_rpc_method_handler(
                     servicer.ApplyGitattributes,
                     request_deserializer=repository__pb2.ApplyGitattributesRequest.FromString,
@@ -720,8 +783,13 @@
                     request_deserializer=repository__pb2.RestoreCustomHooksRequest.FromString,
                     response_serializer=repository__pb2.RestoreCustomHooksResponse.SerializeToString,
             ),
+            'SetCustomHooks': grpc.stream_unary_rpc_method_handler(
+                    servicer.SetCustomHooks,
+                    request_deserializer=repository__pb2.SetCustomHooksRequest.FromString,
+                    response_serializer=repository__pb2.SetCustomHooksResponse.SerializeToString,
+            ),
             'BackupCustomHooks': grpc.unary_stream_rpc_method_handler(
                     servicer.BackupCustomHooks,
                     request_deserializer=repository__pb2.BackupCustomHooksRequest.FromString,
                     response_serializer=repository__pb2.BackupCustomHooksResponse.SerializeToString,
             ),
@@ -723,8 +791,13 @@
             'BackupCustomHooks': grpc.unary_stream_rpc_method_handler(
                     servicer.BackupCustomHooks,
                     request_deserializer=repository__pb2.BackupCustomHooksRequest.FromString,
                     response_serializer=repository__pb2.BackupCustomHooksResponse.SerializeToString,
             ),
+            'GetCustomHooks': grpc.unary_stream_rpc_method_handler(
+                    servicer.GetCustomHooks,
+                    request_deserializer=repository__pb2.GetCustomHooksRequest.FromString,
+                    response_serializer=repository__pb2.GetCustomHooksResponse.SerializeToString,
+            ),
             'GetObjectDirectorySize': grpc.unary_unary_rpc_method_handler(
                     servicer.GetObjectDirectorySize,
                     request_deserializer=repository__pb2.GetObjectDirectorySizeRequest.FromString,
@@ -765,6 +838,11 @@
                     request_deserializer=repository__pb2.FullPathRequest.FromString,
                     response_serializer=repository__pb2.FullPathResponse.SerializeToString,
             ),
+            'RemoveAll': grpc.unary_unary_rpc_method_handler(
+                    servicer.RemoveAll,
+                    request_deserializer=repository__pb2.RemoveAllRequest.FromString,
+                    response_serializer=repository__pb2.RemoveAllResponse.SerializeToString,
+            ),
     }
     generic_handler = grpc.method_handlers_generic_handler(
             'gitaly.RepositoryService', rpc_method_handlers)
@@ -896,6 +974,23 @@
             insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
 
     @staticmethod
+    def ObjectFormat(request,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.unary_unary(request, target, '/gitaly.RepositoryService/ObjectFormat',
+            repository__pb2.ObjectFormatRequest.SerializeToString,
+            repository__pb2.ObjectFormatResponse.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
     def ApplyGitattributes(request,
             target,
             options=(),
@@ -1338,6 +1433,23 @@
             insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
 
     @staticmethod
+    def SetCustomHooks(request_iterator,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.stream_unary(request_iterator, target, '/gitaly.RepositoryService/SetCustomHooks',
+            repository__pb2.SetCustomHooksRequest.SerializeToString,
+            repository__pb2.SetCustomHooksResponse.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
     def BackupCustomHooks(request,
             target,
             options=(),
@@ -1355,6 +1467,23 @@
             insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
 
     @staticmethod
+    def GetCustomHooks(request,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.unary_stream(request, target, '/gitaly.RepositoryService/GetCustomHooks',
+            repository__pb2.GetCustomHooksRequest.SerializeToString,
+            repository__pb2.GetCustomHooksResponse.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
     def GetObjectDirectorySize(request,
             target,
             options=(),
@@ -1489,3 +1618,20 @@
             repository__pb2.FullPathResponse.FromString,
             options, channel_credentials,
             insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+
+    @staticmethod
+    def RemoveAll(request,
+            target,
+            options=(),
+            channel_credentials=None,
+            call_credentials=None,
+            insecure=False,
+            compression=None,
+            wait_for_ready=None,
+            timeout=None,
+            metadata=None):
+        return grpc.experimental.unary_unary(request, target, '/gitaly.RepositoryService/RemoveAll',
+            repository__pb2.RemoveAllRequest.SerializeToString,
+            repository__pb2.RemoveAllResponse.FromString,
+            options, channel_credentials,
+            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
diff --git a/hgitaly/stub/shared_pb2.py b/hgitaly/stub/shared_pb2.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS9zdHViL3NoYXJlZF9wYjIucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS9zdHViL3NoYXJlZF9wYjIucHk= 100644
--- a/hgitaly/stub/shared_pb2.py
+++ b/hgitaly/stub/shared_pb2.py
@@ -22,7 +22,7 @@
   syntax='proto3',
   serialized_options=b'Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb',
   create_key=_descriptor._internal_create_key,
-  serialized_pb=b'\n\x0cshared.proto\x12\x06gitaly\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\nlint.proto\"\xc3\x01\n\nRepository\x12\x14\n\x0cstorage_name\x18\x02 \x01(\t\x12\x15\n\rrelative_path\x18\x03 \x01(\t\x12\x1c\n\x14git_object_directory\x18\x04 \x01(\t\x12(\n git_alternate_object_directories\x18\x05 \x03(\t\x12\x15\n\rgl_repository\x18\x06 \x01(\t\x12\x17\n\x0fgl_project_path\x18\x08 \x01(\tJ\x04\x08\x01\x10\x02J\x04\x08\x07\x10\x08R\x04path\"+\n\rCommitTrailer\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05value\x18\x02 \x01(\x0c\"M\n\x0e\x43ommitStatInfo\x12\x11\n\tadditions\x18\x01 \x01(\x05\x12\x11\n\tdeletions\x18\x02 \x01(\x05\x12\x15\n\rchanged_files\x18\x03 \x01(\x05\"\xc2\x02\n\tGitCommit\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07subject\x18\x02 \x01(\x0c\x12\x0c\n\x04\x62ody\x18\x03 \x01(\x0c\x12$\n\x06\x61uthor\x18\x04 \x01(\x0b\x32\x14.gitaly.CommitAuthor\x12\'\n\tcommitter\x18\x05 \x01(\x0b\x32\x14.gitaly.CommitAuthor\x12\x12\n\nparent_ids\x18\x06 \x03(\t\x12\x11\n\tbody_size\x18\x07 \x01(\x03\x12-\n\x0esignature_type\x18\x08 \x01(\x0e\x32\x15.gitaly.SignatureType\x12\x0f\n\x07tree_id\x18\t \x01(\t\x12\'\n\x08trailers\x18\n \x03(\x0b\x32\x15.gitaly.CommitTrailer\x12+\n\x0bshort_stats\x18\x0b \x01(\x0b\x32\x16.gitaly.CommitStatInfo\"g\n\x0c\x43ommitAuthor\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\r\n\x05\x65mail\x18\x02 \x01(\x0c\x12(\n\x04\x64\x61te\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x10\n\x08timezone\x18\x04 \x01(\x0c\"\x1b\n\nExitStatus\x12\r\n\x05value\x18\x01 \x01(\x05\"@\n\x06\x42ranch\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12(\n\rtarget_commit\x18\x02 \x01(\x0b\x32\x11.gitaly.GitCommit\"\xc5\x01\n\x03Tag\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\n\n\x02id\x18\x02 \x01(\t\x12(\n\rtarget_commit\x18\x03 \x01(\x0b\x32\x11.gitaly.GitCommit\x12\x0f\n\x07message\x18\x04 \x01(\x0c\x12\x14\n\x0cmessage_size\x18\x05 \x01(\x03\x12$\n\x06tagger\x18\x06 \x01(\x0b\x32\x14.gitaly.CommitAuthor\x12-\n\x0esignature_type\x18\x07 \x01(\x0e\x32\x15.gitaly.SignatureType\"Y\n\x04User\x12\r\n\x05gl_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\x0c\x12\r\n\x05\x65mail\x18\x03 \x01(\x0c\x12\x13\n\x0bgl_username\x18\x04 \x01(\t\x12\x10\n\x08timezone\x18\x05 \x01(\t\":\n\nObjectPool\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x90\xc6,\x01\"8\n\x13PaginationParameter\x12\x12\n\npage_token\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\"\'\n\x10PaginationCursor\x12\x13\n\x0bnext_cursor\x18\x01 \x01(\t\"*\n\rGlobalOptions\x12\x19\n\x11literal_pathspecs\x18\x01 \x01(\x08*B\n\nObjectType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\n\n\x06\x43OMMIT\x10\x01\x12\x08\n\x04\x42LOB\x10\x02\x12\x08\n\x04TREE\x10\x03\x12\x07\n\x03TAG\x10\x04*5\n\rSignatureType\x12\x08\n\x04NONE\x10\x00\x12\x07\n\x03PGP\x10\x01\x12\x08\n\x04X509\x10\x02\x12\x07\n\x03SSH\x10\x03*.\n\rSortDirection\x12\r\n\tASCENDING\x10\x00\x12\x0e\n\nDESCENDING\x10\x01\x42\x34Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypbb\x06proto3'
+  serialized_pb=b'\n\x0cshared.proto\x12\x06gitaly\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\nlint.proto\"\xc3\x01\n\nRepository\x12\x14\n\x0cstorage_name\x18\x02 \x01(\t\x12\x15\n\rrelative_path\x18\x03 \x01(\t\x12\x1c\n\x14git_object_directory\x18\x04 \x01(\t\x12(\n git_alternate_object_directories\x18\x05 \x03(\t\x12\x15\n\rgl_repository\x18\x06 \x01(\t\x12\x17\n\x0fgl_project_path\x18\x08 \x01(\tJ\x04\x08\x01\x10\x02J\x04\x08\x07\x10\x08R\x04path\"+\n\rCommitTrailer\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05value\x18\x02 \x01(\x0c\"M\n\x0e\x43ommitStatInfo\x12\x11\n\tadditions\x18\x01 \x01(\x05\x12\x11\n\tdeletions\x18\x02 \x01(\x05\x12\x15\n\rchanged_files\x18\x03 \x01(\x05\"\xeb\x02\n\tGitCommit\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07subject\x18\x02 \x01(\x0c\x12\x0c\n\x04\x62ody\x18\x03 \x01(\x0c\x12$\n\x06\x61uthor\x18\x04 \x01(\x0b\x32\x14.gitaly.CommitAuthor\x12\'\n\tcommitter\x18\x05 \x01(\x0b\x32\x14.gitaly.CommitAuthor\x12\x12\n\nparent_ids\x18\x06 \x03(\t\x12\x11\n\tbody_size\x18\x07 \x01(\x03\x12-\n\x0esignature_type\x18\x08 \x01(\x0e\x32\x15.gitaly.SignatureType\x12\x0f\n\x07tree_id\x18\t \x01(\t\x12\'\n\x08trailers\x18\n \x03(\x0b\x32\x15.gitaly.CommitTrailer\x12+\n\x0bshort_stats\x18\x0b \x01(\x0b\x32\x16.gitaly.CommitStatInfo\x12\x15\n\rreferenced_by\x18\x0c \x03(\x0c\x12\x10\n\x08\x65ncoding\x18\r \x01(\t\"g\n\x0c\x43ommitAuthor\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\r\n\x05\x65mail\x18\x02 \x01(\x0c\x12(\n\x04\x64\x61te\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x10\n\x08timezone\x18\x04 \x01(\x0c\"\x1b\n\nExitStatus\x12\r\n\x05value\x18\x01 \x01(\x05\"@\n\x06\x42ranch\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12(\n\rtarget_commit\x18\x02 \x01(\x0b\x32\x11.gitaly.GitCommit\"\xc5\x01\n\x03Tag\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\n\n\x02id\x18\x02 \x01(\t\x12(\n\rtarget_commit\x18\x03 \x01(\x0b\x32\x11.gitaly.GitCommit\x12\x0f\n\x07message\x18\x04 \x01(\x0c\x12\x14\n\x0cmessage_size\x18\x05 \x01(\x03\x12$\n\x06tagger\x18\x06 \x01(\x0b\x32\x14.gitaly.CommitAuthor\x12-\n\x0esignature_type\x18\x07 \x01(\x0e\x32\x15.gitaly.SignatureType\"Y\n\x04User\x12\r\n\x05gl_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\x0c\x12\r\n\x05\x65mail\x18\x03 \x01(\x0c\x12\x13\n\x0bgl_username\x18\x04 \x01(\t\x12\x10\n\x08timezone\x18\x05 \x01(\t\":\n\nObjectPool\x12,\n\nrepository\x18\x01 \x01(\x0b\x32\x12.gitaly.RepositoryB\x04\x90\xc6,\x01\"8\n\x13PaginationParameter\x12\x12\n\npage_token\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\"\'\n\x10PaginationCursor\x12\x13\n\x0bnext_cursor\x18\x01 \x01(\t\"*\n\rGlobalOptions\x12\x19\n\x11literal_pathspecs\x18\x01 \x01(\x08*B\n\nObjectType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\n\n\x06\x43OMMIT\x10\x01\x12\x08\n\x04\x42LOB\x10\x02\x12\x08\n\x04TREE\x10\x03\x12\x07\n\x03TAG\x10\x04*_\n\x0cObjectFormat\x12\x1d\n\x19OBJECT_FORMAT_UNSPECIFIED\x10\x00\x12\x16\n\x12OBJECT_FORMAT_SHA1\x10\x01\x12\x18\n\x14OBJECT_FORMAT_SHA256\x10\x02*5\n\rSignatureType\x12\x08\n\x04NONE\x10\x00\x12\x07\n\x03PGP\x10\x01\x12\x08\n\x04X509\x10\x02\x12\x07\n\x03SSH\x10\x03*.\n\rSortDirection\x12\r\n\tASCENDING\x10\x00\x12\x0e\n\nDESCENDING\x10\x01\x42\x34Z2gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypbb\x06proto3'
   ,
   dependencies=[google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,lint__pb2.DESCRIPTOR,])
 
@@ -61,9 +61,9 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=1410,
-  serialized_end=1476,
+  serialized_start=1451,
+  serialized_end=1517,
 )
 _sym_db.RegisterEnumDescriptor(_OBJECTTYPE)
 
 ObjectType = enum_type_wrapper.EnumTypeWrapper(_OBJECTTYPE)
@@ -66,7 +66,38 @@
 )
 _sym_db.RegisterEnumDescriptor(_OBJECTTYPE)
 
 ObjectType = enum_type_wrapper.EnumTypeWrapper(_OBJECTTYPE)
+_OBJECTFORMAT = _descriptor.EnumDescriptor(
+  name='ObjectFormat',
+  full_name='gitaly.ObjectFormat',
+  filename=None,
+  file=DESCRIPTOR,
+  create_key=_descriptor._internal_create_key,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='OBJECT_FORMAT_UNSPECIFIED', index=0, number=0,
+      serialized_options=None,
+      type=None,
+      create_key=_descriptor._internal_create_key),
+    _descriptor.EnumValueDescriptor(
+      name='OBJECT_FORMAT_SHA1', index=1, number=1,
+      serialized_options=None,
+      type=None,
+      create_key=_descriptor._internal_create_key),
+    _descriptor.EnumValueDescriptor(
+      name='OBJECT_FORMAT_SHA256', index=2, number=2,
+      serialized_options=None,
+      type=None,
+      create_key=_descriptor._internal_create_key),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=1519,
+  serialized_end=1614,
+)
+_sym_db.RegisterEnumDescriptor(_OBJECTFORMAT)
+
+ObjectFormat = enum_type_wrapper.EnumTypeWrapper(_OBJECTFORMAT)
 _SIGNATURETYPE = _descriptor.EnumDescriptor(
   name='SignatureType',
   full_name='gitaly.SignatureType',
@@ -97,8 +128,8 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=1478,
-  serialized_end=1531,
+  serialized_start=1616,
+  serialized_end=1669,
 )
 _sym_db.RegisterEnumDescriptor(_SIGNATURETYPE)
 
@@ -123,8 +154,8 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=1533,
-  serialized_end=1579,
+  serialized_start=1671,
+  serialized_end=1717,
 )
 _sym_db.RegisterEnumDescriptor(_SORTDIRECTION)
 
@@ -134,6 +165,9 @@
 BLOB = 2
 TREE = 3
 TAG = 4
+OBJECT_FORMAT_UNSPECIFIED = 0
+OBJECT_FORMAT_SHA1 = 1
+OBJECT_FORMAT_SHA256 = 2
 NONE = 0
 PGP = 1
 X509 = 2
@@ -380,6 +414,20 @@
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='referenced_by', full_name='gitaly.GitCommit.referenced_by', index=11,
+      number=12, type=12, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='encoding', full_name='gitaly.GitCommit.encoding', index=12,
+      number=13, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
   ],
   extensions=[
   ],
@@ -393,7 +441,7 @@
   oneofs=[
   ],
   serialized_start=392,
-  serialized_end=714,
+  serialized_end=755,
 )
 
 
@@ -445,8 +493,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=716,
-  serialized_end=819,
+  serialized_start=757,
+  serialized_end=860,
 )
 
 
@@ -477,8 +525,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=821,
-  serialized_end=848,
+  serialized_start=862,
+  serialized_end=889,
 )
 
 
@@ -516,8 +564,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=850,
-  serialized_end=914,
+  serialized_start=891,
+  serialized_end=955,
 )
 
 
@@ -590,8 +638,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=917,
-  serialized_end=1114,
+  serialized_start=958,
+  serialized_end=1155,
 )
 
 
@@ -650,8 +698,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1116,
-  serialized_end=1205,
+  serialized_start=1157,
+  serialized_end=1246,
 )
 
 
@@ -682,8 +730,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1207,
-  serialized_end=1265,
+  serialized_start=1248,
+  serialized_end=1306,
 )
 
 
@@ -721,8 +769,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1267,
-  serialized_end=1323,
+  serialized_start=1308,
+  serialized_end=1364,
 )
 
 
@@ -753,8 +801,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1325,
-  serialized_end=1364,
+  serialized_start=1366,
+  serialized_end=1405,
 )
 
 
@@ -785,8 +833,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1366,
-  serialized_end=1408,
+  serialized_start=1407,
+  serialized_end=1449,
 )
 
 _GITCOMMIT.fields_by_name['author'].message_type = _COMMITAUTHOR
@@ -814,6 +862,7 @@
 DESCRIPTOR.message_types_by_name['PaginationCursor'] = _PAGINATIONCURSOR
 DESCRIPTOR.message_types_by_name['GlobalOptions'] = _GLOBALOPTIONS
 DESCRIPTOR.enum_types_by_name['ObjectType'] = _OBJECTTYPE
+DESCRIPTOR.enum_types_by_name['ObjectFormat'] = _OBJECTFORMAT
 DESCRIPTOR.enum_types_by_name['SignatureType'] = _SIGNATURETYPE
 DESCRIPTOR.enum_types_by_name['SortDirection'] = _SORTDIRECTION
 _sym_db.RegisterFileDescriptor(DESCRIPTOR)
diff --git a/hgitaly/tag.py b/hgitaly/tag.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS90YWcucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS90YWcucHk= 100644
--- a/hgitaly/tag.py
+++ b/hgitaly/tag.py
@@ -11,6 +11,9 @@
 from heptapod.gitlab.tag import (
     gitlab_tag_ref,
 )
+from .gitlab_ref import (
+    path_match_any,
+)
 
 EXCLUDED_TAG_TYPES = (b'local', None)  # 'tip' has type None
 
@@ -30,6 +33,6 @@
         yield name, (repo[node] if deref else hex(node).decode('ascii'))
 
 
-def iter_gitlab_tags_as_refs(repo, **kwargs):
+def iter_gitlab_tags_as_refs(repo, patterns=None, **kwargs):
     """Iterate on the repo tags, yielding full Git refs and change contexts.
 
@@ -34,4 +37,5 @@
     """Iterate on the repo tags, yielding full Git refs and change contexts.
 
+    :param patterns: as in :func:`path_match_any` (presplit)
     """
     for name, target in iter_gitlab_tags(repo, **kwargs):
@@ -36,6 +40,10 @@
     """
     for name, target in iter_gitlab_tags(repo, **kwargs):
-        yield gitlab_tag_ref(name), target
+        ref_path = gitlab_tag_ref(name)
+        if patterns and not path_match_any(ref_path, patterns):
+            continue
+
+        yield ref_path, target
 
 
 def tagged_changeset(repo, tag_name):
diff --git a/hgitaly/tests/test_linguist.py b/hgitaly/tests/test_linguist.py
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS90ZXN0cy90ZXN0X2xpbmd1aXN0LnB5
--- /dev/null
+++ b/hgitaly/tests/test_linguist.py
@@ -0,0 +1,68 @@
+# Copyright 2023 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
+import pytest
+from grpc import StatusCode
+
+from ..linguist import (
+    language_stats,
+)
+from ..testing.context import FakeServicerContext
+
+
+class FakeContextAbort(RuntimeError):
+    pass
+
+
+class FakeContext(FakeServicerContext):
+
+    _trailing_metadata = _code = _details = None
+
+    def set_trailing_metadata(self, metadata):
+        self._trailing_metadata = metadata
+
+    def trailing_metadata(self):
+        return self._trailing_metadata
+
+    def code(self):
+        return self._code
+
+    def details(self):
+        return self._details
+
+    def abort(self, code, details):
+        self._code = code
+        self._details = details
+        raise FakeContextAbort()
+
+
+def test_tokei_subprocesss_errors(tmpdir):
+    bin_path = tmpdir / 'tokei'
+    context = FakeContext()
+    with pytest.raises(FakeContextAbort):
+        language_stats(bin_path, tmpdir / 'tree', context)
+    assert context.code() == StatusCode.INTERNAL
+    assert "not found" in context.details()
+
+    bin_path.write_binary(b'#!/bin/sh\n\n echo "oops" >&2; exit 3\n')
+    with pytest.raises(FakeContextAbort):
+        language_stats(bin_path, tmpdir / 'tree', context)
+    assert "permission denied" in context.details()
+
+    bin_path.chmod(0o755)
+
+    with pytest.raises(FakeContextAbort):
+        language_stats(bin_path, tmpdir / 'tree', context)
+    assert context.code() == StatusCode.INTERNAL
+    assert "status code=3" in context.details()
+    assert "oops" in context.details()
+
+    bin_path.write_binary(b'#!/bin/sh\n\n echo "tokei not-a-version"\n')
+    with pytest.raises(FakeContextAbort):
+        language_stats(bin_path, tmpdir / 'tree', context)
+    assert context.code() == StatusCode.INTERNAL
+    assert "Tokei version" in context.details()
+    assert "'tokei not-a-version'" in context.details()
diff --git a/hgitaly/tests/test_workdir.py b/hgitaly/tests/test_workdir.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS90ZXN0cy90ZXN0X3dvcmtkaXIucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS90ZXN0cy90ZXN0X3dvcmtkaXIucHk= 100644
--- a/hgitaly/tests/test_workdir.py
+++ b/hgitaly/tests/test_workdir.py
@@ -11,6 +11,8 @@
 import warnings
 
 from mercurial import error
+from mercurial.merge import merge
+from mercurial.node import nullrev as NULL_REVISION
 from hgext3rd.heptapod.branch import set_default_gitlab_branch
 
 import pytest
@@ -206,6 +208,35 @@
         assert parent_cs.obsolete()
 
 
+@parametrize('merge_result', ('conflict', 'mergeable'))
+def test_working_directory_merge(wd_fixture, merge_result):
+    wds_root, wrapper = wd_fixture
+    commit_file = wrapper.commit_file
+    conflict = merge_result == 'conflict'
+
+    # a simple merge setup with a conflicting topic and a non-conflicting one
+    ctx0 = commit_file('foo')
+    cs_default = commit_file('foo', content="default")
+    topic_file = 'foo' if conflict else 'bar'
+    cs_topic = commit_file(topic_file, parent=ctx0, topic='top', content="top")
+
+    # leaving merge result in a working directory
+    with working_directory(wds_root, wrapper.repo, changeset=cs_default) as wd:
+        default_wd_id = wd.id
+        stats = merge(wd.repo[cs_topic.rev()])
+        if conflict:
+            assert stats.unresolvedcount > 0
+
+    # re-acquisition cleans up remnants
+    with working_directory(wds_root, wrapper.repo, changeset=cs_default) as wd:
+        assert wd.id == default_wd_id
+
+        wctx = wd.repo[None]
+        assert wctx.p1() == cs_default
+        assert wctx.p2().rev() == NULL_REVISION
+        # TODO clean status (provide in mercurial_testhelpers)
+
+
 @pytest.fixture
 def purge_fixture(wd_fixture):
     wds_root, wrapper = wd_fixture
diff --git a/hgitaly/util.py b/hgitaly/util.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS91dGlsLnB5..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS91dGlsLnB5 100644
--- a/hgitaly/util.py
+++ b/hgitaly/util.py
@@ -14,7 +14,10 @@
 DEFAULT_CHUNK_SIZE = 20
 
 
-def chunked(iterable, size=DEFAULT_CHUNK_SIZE, limit=None):
+def chunked(iterable,
+            size=DEFAULT_CHUNK_SIZE,
+            limit=None,
+            force_if_empty=False):
     """Return a generator of chunks out of given iterable.
 
     >>> [chunk for chunk in chunked(range(3), size=2)]
@@ -37,7 +40,8 @@
             chunk = []
         chunk.append(val)
 
-    yield chunk
+    if chunk or force_if_empty:
+        yield chunk
 
 
 def chunked_with_cursor(message_class, iterable, next_cursor, builder,
diff --git a/hgitaly/workdir.py b/hgitaly/workdir.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aGdpdGFseS93b3JrZGlyLnB5..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aGdpdGFseS93b3JrZGlyLnB5 100644
--- a/hgitaly/workdir.py
+++ b/hgitaly/workdir.py
@@ -74,7 +74,7 @@
                                     update=False))
 
     def command(self, name, *args, hidden=False, **kwargs):
-        cmd = cmdutil.findcmd(b'update', commands.table)[1][0]
+        cmd = cmdutil.findcmd(name, commands.table)[1][0]
         repo = self.repo
         if hidden:
             repo = repo.unfiltered()
@@ -84,7 +84,7 @@
         self.command(b'update', target_rev, clean=True, hidden=True)
 
     def purge(self):
-        self.command(b'purge', all=True)
+        self.command(b'purge', all=True, hidden=True, confirm=False)
 
     def init_repo(self, repo):
         weakref.finalize(repo, clear_repo_class, repo.unfiltered().__class__)
diff --git a/install-requirements.txt b/install-requirements.txt
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_aW5zdGFsbC1yZXF1aXJlbWVudHMudHh0..0944dbc03457ecdaf8e5085639a35a25a2f16e62_aW5zdGFsbC1yZXF1aXJlbWVudHMudHh0 100644
--- a/install-requirements.txt
+++ b/install-requirements.txt
@@ -5,3 +5,4 @@
 grpc-interceptor
 hg-loggingmod
 psutil
+importlib_resources~=2.0.0
diff --git a/protos/commit.proto b/protos/commit.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cHJvdG9zL2NvbW1pdC5wcm90bw==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL2NvbW1pdC5wcm90bw== 100644
--- a/protos/commit.proto
+++ b/protos/commit.proto
@@ -415,8 +415,12 @@
 
   // OID of the object this tree entry points to
   string oid = 1;
-  // OID of the tree attached to commit_oid
-  string root_oid = 2;
+  // OID of the tree attached to commit_oid. This field is deprecated and will be removed in Gitaly
+  // v16.0 as it cannot be implemented efficiently without having to spawn another extra process
+  // that needs to get queried for every single TreeEntry. And at that point the caller can just
+  // manually resolve the root tree ID via a separate RPC call, especially so as there are not
+  // currently any known users of this feature.
+  string root_oid = 2 [deprecated=true];
   // Path relative to repository root
   bytes path = 3;
   // This comment is left unintentionally blank.
@@ -607,6 +611,13 @@
   // include_shortstat determines whether to include the number of lines and files
   // changed in the commits. Populates the `short_stats` field.
   bool include_shortstat = 17;
+  // include_referenced_by determines whether to include references that point to a
+  // commit in the response. The field contains patterns that are matched against
+  // references. Only matching references are included in the response. For 
+  // example, one can get tags pointing to a commit with `refs/tags`, or branches
+  // respectively with `refs/heads`. By sending both, the response will include all
+  // references that match either pattern. 
+  repeated bytes include_referenced_by = 18; // protolint:disable:this REPEATED_FIELD_NAMES_PLURALIZED
 }
 
 // A single 'page' of the result set
@@ -637,8 +648,11 @@
     float share = 2;
     // Color specifies the associated color for this language, for example #3fd5e0.
     string color = 3;
-    // FileCount tells how many files with this language are found.
-    uint32 file_count = 4;
+    // FileCount was a uint32 that stored how many files with this language were found.
+    // However, it was never used and there is no client demand for it.
+    // See: https://gitlab.com/gitlab-org/gitaly/-/issues/4293.
+    reserved 4;
+    reserved "file_count";
     // Bytes is the total amount of bytes written in this language
     uint64 bytes = 5;
   }
diff --git a/protos/diff.proto b/protos/diff.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cHJvdG9zL2RpZmYucHJvdG8=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL2RpZmYucHJvdG8= 100644
--- a/protos/diff.proto
+++ b/protos/diff.proto
@@ -53,6 +53,15 @@
     };
   }
 
+  // GetPatchID computes a patch ID for a patch. Patch IDs are a unique ID computed by hashing
+  // a patch with some parameters like line numbers ignored. The patch ID can thus be used to compare
+  // whether diffs make the same change. Please refer to git-patch-id(1) for further information.
+  // If the difference between old and new change is empty then this RPC returns an error.
+  rpc GetPatchID(GetPatchIDRequest) returns (GetPatchIDResponse) {
+    option (op_type) = {
+      op: ACCESSOR
+    };
+  }
 }
 
 // This comment is left unintentionally blank.
@@ -65,9 +74,23 @@
     WORDDIFF = 1; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
   }
 
+  // WhiteSpaceChanges states which whitespace changes we should ignore. These options correlate to
+  // the ones present in git-diff(1).
+  enum WhitespaceChanges {
+    // WHITESPACE_CHANGES_UNSPECIFIED is used to not ignore any whitespace changes. This also defaults
+    // to the value in the deprecated field `ignore_whitespace_change` to stay backward compatible.
+    WHITESPACE_CHANGES_UNSPECIFIED = 0;
+    // WHITESPACE_CHANGES_IGNORE specifies to use the `--ignore-space-change` flag of git-diff(1).
+    // Only changes in amount of whitespace are ignored.
+    WHITESPACE_CHANGES_IGNORE = 1;
+    // WHITESPACE_CHANGES_IGNORE_ALL specifies to use the `--ignore-all-space` flag of git-diff(1).
+    // All whitespace characters are ignored when comparing lines.
+    WHITESPACE_CHANGES_IGNORE_ALL = 2;
+  }
+
   // This comment is left unintentionally blank.
   Repository repository = 1 [(target_repository)=true];
   // This comment is left unintentionally blank.
   string left_commit_id = 2;
   // This comment is left unintentionally blank.
   string right_commit_id = 3;
@@ -68,11 +91,11 @@
   // This comment is left unintentionally blank.
   Repository repository = 1 [(target_repository)=true];
   // This comment is left unintentionally blank.
   string left_commit_id = 2;
   // This comment is left unintentionally blank.
   string right_commit_id = 3;
-  // This comment is left unintentionally blank.
-  bool ignore_whitespace_change = 4;
+  // This field is deprecated, use the `whitespace_changes` field instead.
+  bool ignore_whitespace_change = 4 [deprecated=true];
   // This comment is left unintentionally blank.
   repeated bytes paths = 5;
   // This comment is left unintentionally blank.
@@ -100,6 +123,15 @@
 
   // DiffMode is the mode used for generating the diff. Please refer to the enum declaration for supported modes.
   DiffMode diff_mode = 15;
+
+  // Overrides max patch bytes for file extension.
+  // If the file does not have an extension (eg Dockerfile), then the file name is used for matching.
+  // For files with more than on extension (eg file.html.tmpl) only the last extension is matched.
+  map<string, int32> max_patch_bytes_for_file_extension = 16;
+
+  // WhitespaceChanges states which whitespace changes should be included in the diff.
+  // Please refer to the enum declaration for supported modes.
+  WhitespaceChanges whitespace_changes = 17;
 }
 
 // A CommitDiffResponse corresponds to a single changed file in a commit.
@@ -312,3 +344,24 @@
   // new_mode is the mode of the changed path after the change. Please refer to `old_mode` for a list of potential values.
   int32 new_mode = 4;
 }
+
+// GetPatchIDRequest is a request for the GetPatchID RPC.
+message GetPatchIDRequest {
+  // Repository is the repository the patch ID shall be computed in.
+  Repository repository = 1 [(target_repository)=true];
+  // OldRevision is the old revision that shall be used to compute the patch
+  // from that will then be passed to git-patch-id(1). Accepts revisions as
+  // specified in gitrevisions(5).
+  bytes old_revision = 2;
+  // newRevision is the new revision that shall be used to compute the patch
+  // from that will then be passed to git-patch-id(1). Accepts revisions as
+  // specified in gitrevisions(5).
+  bytes new_revision = 3;
+}
+
+// GetPatchIDResponse is a response for the GetPatchID RPC.
+message GetPatchIDResponse {
+  // PatchId is the patch ID that was generated by hashing the diff of the
+  // given old and new revision.
+  string patch_id = 1;
+}
diff --git a/protos/log.proto b/protos/log.proto
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL2xvZy5wcm90bw==
--- /dev/null
+++ b/protos/log.proto
@@ -0,0 +1,47 @@
+syntax = "proto3";
+
+package gitaly;
+
+option go_package = "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb";
+
+// LogEntry is a single entry in a repository's write-ahead log.
+//
+// Schema for :
+// - `repository/<repository_id>/log/entry/<log_index>`.
+message LogEntry {
+  // ReferenceUpdate models a single reference update.
+  message ReferenceUpdate {
+    // reference_name is the fully qualified name of the reference
+    // to update.
+    bytes reference_name = 1;
+    // new_oid is the new oid to point the reference to. Deletions
+    // are denoted as the SHA1 or SHA256 zero OID depending on the
+    // hash type used in the repository.
+    bytes new_oid = 2;
+  }
+
+  // DefaultBranchUpdate models a default branch update.
+  message DefaultBranchUpdate {
+    // reference_name is the fully qualified name of the reference
+    // to update the default branch to.
+    bytes reference_name = 1;
+  }
+
+  // reference_updates contains the reference updates this log
+  // entry records. The logged reference updates have already passed
+  // through verification and are applied without any further checks.
+  repeated ReferenceUpdate reference_updates = 1;
+  // default_branch_update contains the information pertaining to updating
+  // the default branch of the repo.
+  DefaultBranchUpdate default_branch_update = 2;
+}
+
+// LogIndex serializes a log index. It's used for storing a repository's
+// applied log index in the database.
+//
+// Schema for:
+// - `repository/<repository_id>/log/index/applied`
+message LogIndex {
+  // log_index is an index pointing to a position in the log.
+  uint64 log_index = 1;
+}
diff --git a/protos/mercurial-operations.proto b/protos/mercurial-operations.proto
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL21lcmN1cmlhbC1vcGVyYXRpb25zLnByb3Rv
--- /dev/null
+++ b/protos/mercurial-operations.proto
@@ -0,0 +1,48 @@
+syntax = "proto3";
+
+package hgitaly;
+
+import "lint.proto";
+import "shared.proto";
+
+service MercurialOperationsService {
+
+  // Provide all information for a possible merge
+  //
+  // Will be used in mergeability checks and last-minute checks if needed.
+  rpc MergeAnalysis(MergeAnalysisRequest) returns (MergeAnalysisResponse) {
+    option (.gitaly.op_type) = {
+      op: ACCESSOR
+    };
+  }
+
+}
+
+message MergeAnalysisRequest {
+  .gitaly.Repository repository = 1 [(.gitaly.target_repository)=true];
+  bytes source_revision = 2;
+  bytes target_revision = 3;
+  // Conflicts checks are expensive (involve using a temporary working
+  // directory) and are always skipped in fast-forward situations.
+  // However it is worthwile to avoid them if the merge is not
+  // a fast-forward and that is all the caller wants to know.
+  bool skip_conflicts_check = 4;
+}
+
+message MergeAnalysisResponse {
+  bool is_fast_forward = 1;
+  bool has_obsolete_changesets = 2;
+  bool has_unstable_changesets = 3;
+  // `has_conflicts` is evaluated unless
+  // (is_fast_forward OR has_obsolete_changesets OR has_instable changesets)
+  bool has_conflicts = 4;
+  bool target_is_public = 5;
+  // topic information is retrieved only for non-public changesets
+  // (standard vanishing property as in the Mercurial CLI by default)
+  string target_node_id = 6;
+  bytes target_branch = 7;
+  bytes target_topic = 8;
+  string source_node_id = 9;
+  bytes source_branch = 10;
+  bytes source_topic = 11;
+}
diff --git a/protos/objectpool.proto b/protos/objectpool.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cHJvdG9zL29iamVjdHBvb2wucHJvdG8=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL29iamVjdHBvb2wucHJvdG8= 100644
--- a/protos/objectpool.proto
+++ b/protos/objectpool.proto
@@ -7,9 +7,26 @@
 
 option go_package = "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb";
 
-// ObjectPoolService is a service containing RPCs to manipulate object pools.
-// An object pool is a separate repository that can be linked to from multiple
-// satellite repositories in order to deduplicate common objects between them.
-// This is mostly used in the contexet of repository forks.
+// ObjectPoolService is a service that manages the lifetime of object pools.
+//
+// An object pool is a separate repository that can be linked to from multiple object pool members
+// in order to deduplicate common objects between them. This is mostly used in the context of
+// repository forks.
+//
+// The typical lifetime of an object pool is as follows:
+//
+// 1. An object pool is created via CreateObjectPool from its primary pool member. Typically this
+//    would be the repository that gets forked.
+// 2. One or more repositories are linked to the object pool via LinkRepositoryToObjectPool. Each
+//    object pool member linked to the repository will have its objects deduplicated when its
+//    objects get repacked the next time.
+// 3. The object pool is regularly updated via FetchIntoObjectPool. This is typically only done from
+//    the primary object pool member.
+// 4. Repositories may leave the object pool via DisconnectGitAlternates. There is not much of a
+//    reason to do this for any repositories except for the primary object pool member in case it
+//    for example becomes private.
+// 5. When the object pool does not have any members anymore it gets deleted via DeleteObjectPool.
+//    It is the responsibility of the caller to ensure that it really has no members left, else
+//    any existing member will become corrupt.
 service ObjectPoolService {
 
@@ -14,9 +31,13 @@
 service ObjectPoolService {
 
-  // This comment is left unintentionally blank.
+  // CreateObjectPool creates an object pool from a specific source repository. It will create the
+  // object pool by cloning all contents from that source repository. The source repository will not
+  // automatically be linked to the object pool, you need to call LinkRepositoryToObjectPool for
+  // this. If the object pool exists already this RPC returns an error with the FailedPrecondition
+  // gRPC error code.
   rpc CreateObjectPool(CreateObjectPoolRequest) returns (CreateObjectPoolResponse) {
     option (op_type) = {
       op: MUTATOR
     };
   }
 
@@ -17,13 +38,14 @@
   rpc CreateObjectPool(CreateObjectPoolRequest) returns (CreateObjectPoolResponse) {
     option (op_type) = {
       op: MUTATOR
     };
   }
 
-  // This comment is left unintentionally blank.
+  // DeleteObjectPool deletes the object pool. There are no safety checks in place, so if any
+  // repository is still using this object pool it will become corrupted.
   rpc DeleteObjectPool(DeleteObjectPoolRequest) returns (DeleteObjectPoolResponse) {
     option (op_type) = {
       op: MUTATOR
     };
   }
 
@@ -24,13 +46,14 @@
   rpc DeleteObjectPool(DeleteObjectPoolRequest) returns (DeleteObjectPoolResponse) {
     option (op_type) = {
       op: MUTATOR
     };
   }
 
-  // Repositories are assumed to be stored on the same disk
+  // LinkRepositoryToObjectPool links the specified repository to the object pool. Objects contained
+  // in the object pool will be deduplicated for this repository when repacking objects.
   rpc LinkRepositoryToObjectPool(LinkRepositoryToObjectPoolRequest) returns (LinkRepositoryToObjectPoolResponse) {
     option (op_type) = {
       op: MUTATOR
     };
   }
 
@@ -31,8 +54,13 @@
   rpc LinkRepositoryToObjectPool(LinkRepositoryToObjectPoolRequest) returns (LinkRepositoryToObjectPoolResponse) {
     option (op_type) = {
       op: MUTATOR
     };
   }
 
-  // This comment is left unintentionally blank.
+  // ReduplicateRepository will repack the objects in the object pool member so that the repository
+  // does not depend on the pool member anymore and can be removed from it. Note that this function
+  // is not safe for use.
+  //
+  // This RPC is deprecated. Please use DisconnectGitAlternates instead.  It will be removed in
+  // Gitaly v16.0, refer to https://gitlab.com/gitlab-org/gitaly/-/issues/4655.
   rpc ReduplicateRepository(ReduplicateRepositoryRequest) returns (ReduplicateRepositoryResponse) {
@@ -38,6 +66,7 @@
   rpc ReduplicateRepository(ReduplicateRepositoryRequest) returns (ReduplicateRepositoryResponse) {
+    option deprecated = true;
     option (op_type) = {
       op: MUTATOR
     };
   }
 
@@ -39,9 +68,22 @@
     option (op_type) = {
       op: MUTATOR
     };
   }
 
-  // This comment is left unintentionally blank.
+  // DisconnectGitAlternates will disconnect the object pool member from its object pool. It will:
+  //
+  // 1. Link all objects from the object pool into the member repository. This essenitally
+  //    reduplicates previously-duplicated objects so that the repository will continue to function
+  //    after being unlinked.
+  // 2. Remove the alternates link to the object pool.
+  // 3. Perform a consistency check to assert that the repository is indeed fully functional after
+  //    unlinking it from its pool. If the consistency check fails the alternates link is restored
+  //    an the RPC fails.
+  //
+  // If successful, the object pool member is disconnected from the object pool and does not depend
+  // on it anymore.
+  //
+  // This RPC does not return an error in case the repository is not linked to any object pool.
   rpc DisconnectGitAlternates(DisconnectGitAlternatesRequest) returns (DisconnectGitAlternatesResponse) {
     option (op_type) = {
       op: MUTATOR
@@ -58,7 +100,8 @@
     };
   }
 
-  // This comment is left unintentionally blank.
+  // GetObjectPool returns the object pool a repository is connected to. If the repository is not
+  // connected to a pool then this RPC returns successfully with an empty response.
   rpc GetObjectPool(GetObjectPoolRequest) returns (GetObjectPoolResponse) {
     option (op_type) = {
       op: ACCESSOR
@@ -67,6 +110,5 @@
 
 }
 
-// Creates an object pool from the repository. The client is responsible for
-// joining this pool later with this repository.
+// CreatesObjectPoolRequest is a request for the CreateObjectPool RPC.
 message CreateObjectPoolRequest {
@@ -72,3 +114,4 @@
 message CreateObjectPoolRequest {
-  // This comment is left unintentionally blank.
+  // ObjectPool is the object pool to create. This field controls where exactly the object pool will
+  // be created.
   ObjectPool object_pool = 1 [(target_repository)=true];
@@ -74,5 +117,5 @@
   ObjectPool object_pool = 1 [(target_repository)=true];
-  // This comment is left unintentionally blank.
+  // Origin is the repository from which the object pool shall be created.
   Repository origin = 2 [(additional_repository)=true];
 }
 
@@ -76,7 +119,7 @@
   Repository origin = 2 [(additional_repository)=true];
 }
 
-// This comment is left unintentionally blank.
+// CreateObjectPoolResponse is a response for the CreateObjectPool RPC.
 message CreateObjectPoolResponse {
 }
 
@@ -80,6 +123,5 @@
 message CreateObjectPoolResponse {
 }
 
-// Removes the directory from disk, caller is responsible for leaving the object
-// pool before calling this RPC
+// DeleteObjectPoolRequest is a request for the DeleteObjectPool RPC.
 message DeleteObjectPoolRequest {
@@ -85,5 +127,5 @@
 message DeleteObjectPoolRequest {
-  // This comment is left unintentionally blank.
+  // ObjectPool is the object pool that shall be deleted.
   ObjectPool object_pool = 1 [(target_repository)=true];
 }
 
@@ -87,7 +129,7 @@
   ObjectPool object_pool = 1 [(target_repository)=true];
 }
 
-// This comment is left unintentionally blank.
+// DeleteObjectPoolResponse is a response for the DeleteObjectPool RPC.
 message DeleteObjectPoolResponse {
 }
 
@@ -91,5 +133,5 @@
 message DeleteObjectPoolResponse {
 }
 
-// This comment is left unintentionally blank.
+// LinkRepositoryToObjectPoolRequest is a request for the LinkRepositoryToObjectPool RPC.
 message LinkRepositoryToObjectPoolRequest {
@@ -95,3 +137,3 @@
 message LinkRepositoryToObjectPoolRequest {
-  // This comment is left unintentionally blank.
+  // ObjectPool is the object pool to which the repository shall be linked to.
   ObjectPool object_pool = 1 [(additional_repository)=true];
@@ -97,5 +139,5 @@
   ObjectPool object_pool = 1 [(additional_repository)=true];
-  // This comment is left unintentionally blank.
+  // Repository is the repository that shall be linked to the object pool.
   Repository repository = 2 [(target_repository)=true];
 }
 
@@ -99,7 +141,7 @@
   Repository repository = 2 [(target_repository)=true];
 }
 
-// This comment is left unintentionally blank.
+// LinkRepositoryToObjectPoolResponse is a response for the LinkRepositoryToObjectPool RPC.
 message LinkRepositoryToObjectPoolResponse {
 }
 
@@ -103,5 +145,5 @@
 message LinkRepositoryToObjectPoolResponse {
 }
 
-// This comment is left unintentionally blank.
+// ReduplicateRepositoryRequest is a request for the ReduplicateRepository RPC.
 message ReduplicateRepositoryRequest {
@@ -107,5 +149,5 @@
 message ReduplicateRepositoryRequest {
-  // This comment is left unintentionally blank.
+  // Repository is the repository whose objects shall be reduplicated.
   Repository repository = 1 [(target_repository)=true];
 }
 
@@ -109,7 +151,7 @@
   Repository repository = 1 [(target_repository)=true];
 }
 
-// This comment is left unintentionally blank.
+// ReduplicateRepositoryResponse is a response for the ReduplicateRepository RPC.
 message ReduplicateRepositoryResponse {
 }
 
@@ -113,5 +155,5 @@
 message ReduplicateRepositoryResponse {
 }
 
-// This comment is left unintentionally blank.
+// DisconnectGitAlternatesRequest is a request for the DisconnectGitAlternates RPC.
 message DisconnectGitAlternatesRequest {
@@ -117,5 +159,5 @@
 message DisconnectGitAlternatesRequest {
-  // This comment is left unintentionally blank.
+  // Repository is th repository that shall be disconnected from its object pool.
   Repository repository = 1  [(target_repository)=true];
 }
 
@@ -119,7 +161,7 @@
   Repository repository = 1  [(target_repository)=true];
 }
 
-// This comment is left unintentionally blank.
+// DisconnectGitAlternatesResponse is a response for the DisconnectGitAlternates RPC.
 message DisconnectGitAlternatesResponse {
 }
 
@@ -140,5 +182,5 @@
 message FetchIntoObjectPoolResponse {
 }
 
-// This comment is left unintentionally blank.
+// GetObjectPoolRequest is a request for the GetObjectPool RPC.
 message GetObjectPoolRequest {
@@ -144,5 +186,5 @@
 message GetObjectPoolRequest {
-  // This comment is left unintentionally blank.
+  // Repository is the repository for which the object pool shall be retrieved.
   Repository repository = 1 [(target_repository)=true];
 }
 
@@ -146,5 +188,5 @@
   Repository repository = 1 [(target_repository)=true];
 }
 
-// This comment is left unintentionally blank.
+// GetObjectDirectorySizeResponse is a response for the GetObjectPool RPC.
 message GetObjectPoolResponse {
@@ -150,4 +192,5 @@
 message GetObjectPoolResponse {
-  // This comment is left unintentionally blank.
+  // ObjectPool is the object pool the repository is connected to. If the repository is not
+  // connected to any object pool, then this field will be empty.
   ObjectPool object_pool = 1;
 }
diff --git a/protos/operations.proto b/protos/operations.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cHJvdG9zL29wZXJhdGlvbnMucHJvdG8=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL29wZXJhdGlvbnMucHJvdG8= 100644
--- a/protos/operations.proto
+++ b/protos/operations.proto
@@ -228,6 +228,14 @@
   // perform access checks against the Rails `/internal/allowed` API. This user is also exposed to
   // any custom hooks executed as part of this RPC call.
   User user = 3;
+  // expected_old_oid is the object ID which branch is expected to point to.
+  // This is used as a safety guard to avoid races when branch has been
+  // updated meanwhile to point to a different object ID.
+  //
+  // If unset, the target branch will be deleted regardless of its current
+  // state. If set, it must either contain a valid, full object ID. Otherwise, this
+  // RPC will return an error.
+  string expected_old_oid = 4;
 }
 
 // UserDeleteBranchResponse is a response for the UserDeleteBranch RPC.
@@ -260,6 +268,14 @@
   bytes tag_name = 2;
   // This comment is left unintentionally blank.
   User user = 3;
+  // expected_old_oid is the object ID which tag is expected to point to.
+  // This is used as a safety guard to avoid races when tag has been
+  // updated meanwhile to point to a different object ID.
+  //
+  // If unset, the target tag will be deleted regardless of its current
+  // state. If set, it must either contain a valid, full object ID. Otherwise,
+  // this RPC will return an error.
+  string expected_old_oid = 4;
 }
 
 // This comment is left unintentionally blank.
@@ -341,6 +357,15 @@
   // timestamp is the optional timestamp to use for the merge commit. If it's
   // not set, the current time will be used.
   google.protobuf.Timestamp timestamp = 7;
+  // expected_old_oid is the object ID which branch is expected to point to.
+  // This is used as a safety guard to avoid races when branch has been
+  // updated meanwhile to point to a different object ID.
+  //
+  // If unset, the target branch will be overwritten regardless of its current
+  // state. If set, it must either contain a valid, full object ID  or the
+  // zero object ID in case the branch should be created. Otherwise, this RPC
+  // will return an error.
+  string expected_old_oid = 8;
 
   // apply must only be set in the second message. Only if this second message
   // is sent and if apply is set to true will the branch be updated to point to
@@ -449,6 +474,15 @@
   // branch name only and not a fully qualified reference, e.g. "master"
   // instead of "refs/heads/master".
   bytes branch = 4;
+  // expected_old_oid is the object ID which branch is expected to point to.
+  // This is used as a safety guard to avoid races when branch has been
+  // updated meanwhile to point to a different object ID.
+  //
+  // If unset, the target branch will be overwritten regardless of its current
+  // state. If set, it must either contain a valid, full object ID  or the
+  // zero object ID in case the branch should be created. Otherwise, this RPC
+  // will return an error.
+  string expected_old_oid = 5;
 }
 
 // This comment is left unintentionally blank.
@@ -486,6 +520,16 @@
   // timestamp is the optional timestamp to use for the created cherry-picked
   // commit's committer date. If it's not set, the current time will be used.
   google.protobuf.Timestamp timestamp = 9;
+  // expected_old_oid is the object ID which branch is expected to point to.
+  // This is used as a safety guard to avoid races when branch has been
+  // updated meanwhile to point to a different object ID. Only applicable when
+  // branch_name is set.
+  //
+  // If unset, the target branch will be overwritten regardless of its current
+  // state. If set, it must either contain a valid, full object ID  or the
+  // zero object ID in case the branch should be created. Otherwise, this RPC
+  // will return an error.
+  string expected_old_oid = 10;
 }
 
 // This comment is left unintentionally blank.
@@ -566,6 +610,16 @@
   // timestamp is the optional timestamp to use for the created cherry-picked
   // commit's committer date. If it's not set, the current time will be used.
   google.protobuf.Timestamp timestamp = 9;
+  // expected_old_oid is the object ID which branch is expected to point to.
+  // This is used as a safety guard to avoid races when branch has been
+  // updated meanwhile to point to a different object ID. Only applicable when
+  // branch_name is set.
+  //
+  // If unset, the target branch will be overwritten regardless of its current
+  // state. If set, it must either contain a valid, full object ID  or the
+  // zero object ID in case the branch should be created. Otherwise, this RPC
+  // will return an error.
+  string expected_old_oid = 10;
 }
 
 // This comment is left unintentionally blank.
@@ -687,6 +741,16 @@
   // timestamp is the optional timestamp to use for the commits as author and
   // committer date. If it's not set, the current time will be used.
   google.protobuf.Timestamp timestamp = 11;
+  // expected_old_oid is the object ID which branch is expected to point to.
+  // This is used as a safety guard to avoid races when branch has been
+  // updated meanwhile to point to a different object ID. Only applicable when
+  // branch_name is set.
+  //
+  // If unset, the target branch will be overwritten regardless of its current
+  // state. If set, it must either contain a valid, full object ID  or the
+  // zero object ID in case the branch should be created. Otherwise, this RPC
+  // will return an error.
+  string expected_old_oid = 12;
 }
 
 // UserCommitFiles is the request of UserCommitFiles.
@@ -866,6 +930,15 @@
     // timestamp is the optional timestamp to use for the squashed commit as
     // committer date. If it's not set, the current time will be used.
     google.protobuf.Timestamp timestamp = 4;
+    // expected_old_oid is the object ID which branch is expected to point to.
+    // This is used as a safety guard to avoid races when branch has been
+    // updated meanwhile to point to a different object ID.
+    //
+    // If unset, the target branch will be overwritten regardless of its current
+    // state. If set, it must either contain a valid, full object ID  or the
+    // zero object ID in case the branch should be created. Otherwise, this RPC
+    // will return an error.
+    string expected_old_oid = 5;
   }
 
   oneof user_apply_patch_request_payload {
@@ -903,6 +976,15 @@
   // submodule as committer date. If it's not set, the current time will be
   // used.
   google.protobuf.Timestamp timestamp = 7;
+  // expected_old_oid is the object ID which branch is expected to point to.
+  // This is used as a safety guard to avoid races when branch has been
+  // updated meanwhile to point to a different object ID.
+  //
+  // If unset, the target branch will be overwritten regardless of its current
+  // state. If set, it must either contain a valid, full object ID  or the
+  // zero object ID in case the branch should be created. Otherwise, this RPC
+  // will return an error.
+  string expected_old_oid = 8;
 }
 
 // This comment is left unintentionally blank.
diff --git a/protos/ref.proto b/protos/ref.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cHJvdG9zL3JlZi5wcm90bw==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL3JlZi5wcm90bw== 100644
--- a/protos/ref.proto
+++ b/protos/ref.proto
@@ -19,8 +19,10 @@
     };
   }
 
-  // This comment is left unintentionally blank.
+  // FindAllBranchNames is deprecated in favor of ListRefs
+  //
+  // https://gitlab.com/gitlab-org/gitaly/-/issues/3966
   rpc FindAllBranchNames(FindAllBranchNamesRequest) returns (stream FindAllBranchNamesResponse) {
     option (op_type) = {
       op: ACCESSOR
     };
@@ -23,7 +25,7 @@
   rpc FindAllBranchNames(FindAllBranchNamesRequest) returns (stream FindAllBranchNamesResponse) {
     option (op_type) = {
       op: ACCESSOR
     };
-
+    option deprecated = true;
   }
 
@@ -28,7 +30,9 @@
   }
 
-  // This comment is left unintentionally blank.
+  // FindAllTagNames is deprecated in favor of ListRefs
+  //
+  // https://gitlab.com/gitlab-org/gitaly/-/issues/3966
   rpc FindAllTagNames(FindAllTagNamesRequest) returns (stream FindAllTagNamesResponse) {
     option (op_type) = {
       op: ACCESSOR
     };
@@ -31,7 +35,8 @@
   rpc FindAllTagNames(FindAllTagNamesRequest) returns (stream FindAllTagNamesResponse) {
     option (op_type) = {
       op: ACCESSOR
     };
+    option deprecated = true;
   }
 
   // Return a stream so we can divide the response in chunks of branches
@@ -587,6 +592,13 @@
   bool head = 3;
   // SortBy allows to request SHAs in particular order.
   SortBy sort_by = 4;
+  // PointingAtOids is a list of OIDs that can optionally be passed to only return refs
+  // pointing at the given OIDs. This corresponds to the --points-at option of git-for-each-ref(1).
+  repeated bytes pointing_at_oids = 5;
+  // PeelTags controls whether annotated tags should be peeled to their target objects so that the
+  // `PeeledTarget` returned for the reference is the ID of the target object. Note that this
+  // will significantly slow down the request by a factor of 3 to 4.
+  bool peel_tags = 6;
 }
 
 // ListRefsResponse is a response for the ListRefs RPC. The RPC can return multiple responses
@@ -598,6 +610,9 @@
     bytes name = 1;
     // Target is the object ID the reference points to.
     string target = 2;
+    // PeeledTarget is the object ID an annotated tag points to. This field is only set when
+    // `PeelTags=true`. This field is empty in case the object is not an annotated tag.
+    string peeled_target = 3;
   }
 
   // References is the set of references returned by the RPC.
diff --git a/protos/repository.proto b/protos/repository.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cHJvdG9zL3JlcG9zaXRvcnkucHJvdG8=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL3JlcG9zaXRvcnkucHJvdG8= 100644
--- a/protos/repository.proto
+++ b/protos/repository.proto
@@ -64,6 +64,13 @@
     };
   }
 
+  // ObjectFormat determines the object format that is being used by the repository.
+  rpc ObjectFormat(ObjectFormatRequest) returns (ObjectFormatResponse) {
+    option (op_type) = {
+      op: ACCESSOR
+    };
+  }
+
   // This comment is left unintentionally blank.
   rpc ApplyGitattributes(ApplyGitattributesRequest) returns (ApplyGitattributesResponse) {
     option (op_type) = {
@@ -108,7 +115,8 @@
     };
   }
 
-  // This comment is left unintentionally blank.
+  // Fsck checks the repository for consistency via git-fsck(1). This can be used to check for
+  // repository corruption.
   rpc Fsck(FsckRequest) returns (FsckResponse) {
     option (op_type) = {
       op: ACCESSOR
@@ -182,7 +190,8 @@
     };
   }
 
-  // This comment is left unintentionally blank.
+  // FindLicense looks in the given repository and attempts to detect all the
+  // details about the license used in the repository.
   rpc FindLicense(FindLicenseRequest) returns (FindLicenseResponse) {
     option (op_type) = {
       op: ACCESSOR
@@ -246,8 +255,10 @@
     };
   }
 
-  // This comment is left unintentionally blank.
+  // RestoreCustomHooks sets the git hooks for a repository. The hooks are sent
+  // in a tar archive containing a `custom_hooks` directory. This directory is
+  // ultimately extracted to the repository.
   rpc RestoreCustomHooks(stream RestoreCustomHooksRequest) returns (RestoreCustomHooksResponse) {
     option (op_type) = {
       op: MUTATOR
     };
@@ -250,6 +261,16 @@
   rpc RestoreCustomHooks(stream RestoreCustomHooksRequest) returns (RestoreCustomHooksResponse) {
     option (op_type) = {
       op: MUTATOR
     };
+    option deprecated = true;
+  }
+
+  // SetCustomHooks sets the git hooks for a repository. The hooks are sent in a
+  // tar archive containing a `custom_hooks` directory. This directory is 
+  // ultimately extracted to the repository.
+  rpc SetCustomHooks(stream SetCustomHooksRequest) returns (SetCustomHooksResponse) {
+    option (op_type) = {
+      op: MUTATOR
+    };
   }
 
@@ -254,7 +275,9 @@
   }
 
-  // This comment is left unintentionally blank.
+  // BackupCustomHooks fetches the git hooks for a repository. The hooks are
+  // sent in a tar archive containing a `custom_hooks` directory. If no hooks
+  // are present in the repository, the response will have no data.
   rpc BackupCustomHooks(BackupCustomHooksRequest) returns (stream BackupCustomHooksResponse) {
     option (op_type) = {
       op: ACCESSOR
     };
@@ -257,7 +280,17 @@
   rpc BackupCustomHooks(BackupCustomHooksRequest) returns (stream BackupCustomHooksResponse) {
     option (op_type) = {
       op: ACCESSOR
     };
+    option deprecated = true;
+  }
+
+  // GetCustomHooks fetches the git hooks for a repository. The hooks are sent
+  // in a tar archive containing a `custom_hooks` directory. If no hooks are
+  // present in the repository, the response will have no data.
+  rpc GetCustomHooks(GetCustomHooksRequest) returns (stream GetCustomHooksResponse) {
+    option (op_type) = {
+      op: ACCESSOR
+    };
   }
 
   // This comment is left unintentionally blank.
@@ -324,6 +357,7 @@
   // an admin inspects the repository's gitconfig such that he can easily see
   // what the repository name is.
   rpc SetFullPath(SetFullPathRequest) returns (SetFullPathResponse) {
+    option deprecated = true;
     option (op_type)  = {
       op: MUTATOR
     };
@@ -332,7 +366,8 @@
   // FullPath reads the "gitlab.fullpath" configuration from the repository's
   // gitconfig. Returns an error in case the full path has not been configured.
   rpc FullPath(FullPathRequest) returns (FullPathResponse) {
+    option deprecated = true;
     option (op_type) = {
       op: ACCESSOR
     };
   }
@@ -335,7 +370,12 @@
     option (op_type) = {
       op: ACCESSOR
     };
   }
+
+  // RemoveAll deletes all repositories on a specified storage.
+  rpc RemoveAll(RemoveAllRequest) returns (RemoveAllResponse) {
+    option (intercepted_method) = true;
+  }
 }
 
 // This comment is left unintentionally blank.
@@ -442,6 +482,18 @@
   int64 size = 1;
 }
 
+// ObjectFormatRequest is a request for the ObjectFormat RPC.
+message ObjectFormatRequest {
+  // Repository is the repository for which to determine the object format.
+  Repository repository = 1 [(target_repository)=true];
+}
+
+// ObjectFormatResponse is a response for the ObjectFormat RPC.
+message ObjectFormatResponse {
+  // Format is the object format that the repository uses.
+  ObjectFormat format = 1;
+}
+
 // This comment is left unintentionally blank.
 message ApplyGitattributesRequest {
   // This comment is left unintentionally blank.
@@ -517,6 +569,10 @@
   // repository. Note, this will be treated as the branch name and not a
   // fully qualified reference.
   bytes default_branch = 2;
+  // ObjectFormat is the object format the repository should be created with. Note that this is
+  // experimental and should not be used by callers yet. It is mostly intended for internal testing
+  // purposes in Gitaly right now.
+  ObjectFormat object_format = 3;
 }
 
 // This comment is left unintentionally blank.
@@ -600,5 +656,5 @@
   bool result = 1;
 }
 
-// This comment is left unintentionally blank.
+// FsckRequest is a request for the Fsck RPC.
 message FsckRequest {
@@ -604,5 +660,5 @@
 message FsckRequest {
-  // This comment is left unintentionally blank.
+  // Repository is the repository that shall be checked for consistency.
   Repository repository = 1 [(target_repository)=true];
 }
 
@@ -606,5 +662,5 @@
   Repository repository = 1 [(target_repository)=true];
 }
 
-// This comment is left unintentionally blank.
+// FsckResponse is a response for the Fsck RPC.
 message FsckResponse {
@@ -610,5 +666,5 @@
 message FsckResponse {
-  // This comment is left unintentionally blank.
+  // Error contains both stdout and stderr of git-fsck(1) in case it returned an error.
   bytes error = 1;
 }
 
@@ -748,7 +804,15 @@
 }
 
 // This comment is left unintentionally blank.
+message  SetCustomHooksRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // This comment is left unintentionally blank.
+  bytes data = 2;
+}
+
+// This comment is left unintentionally blank.
 message RestoreCustomHooksResponse {
 }
 
 // This comment is left unintentionally blank.
@@ -751,10 +815,14 @@
 message RestoreCustomHooksResponse {
 }
 
 // This comment is left unintentionally blank.
+message SetCustomHooksResponse {
+}
+
+// This comment is left unintentionally blank.
 message BackupCustomHooksRequest {
   // This comment is left unintentionally blank.
   Repository repository = 1 [(target_repository)=true];
 }
 
 // This comment is left unintentionally blank.
@@ -755,12 +823,18 @@
 message BackupCustomHooksRequest {
   // This comment is left unintentionally blank.
   Repository repository = 1 [(target_repository)=true];
 }
 
 // This comment is left unintentionally blank.
+message GetCustomHooksRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+}
+
+// This comment is left unintentionally blank.
 message BackupCustomHooksResponse {
   // This comment is left unintentionally blank.
   bytes data = 1;
 }
 
 // This comment is left unintentionally blank.
@@ -761,9 +835,15 @@
 message BackupCustomHooksResponse {
   // This comment is left unintentionally blank.
   bytes data = 1;
 }
 
 // This comment is left unintentionally blank.
+message GetCustomHooksResponse {
+  // This comment is left unintentionally blank.
+  bytes data = 1;
+}
+
+// This comment is left unintentionally blank.
 message CreateRepositoryFromBundleRequest {
   // Only available on the first message
   Repository repository = 1 [(target_repository)=true];
@@ -775,5 +855,5 @@
 message CreateRepositoryFromBundleResponse {
 }
 
-// This comment is left unintentionally blank.
+// FindLicenseRequest asks to detect the license for the given repository.
 message FindLicenseRequest {
@@ -779,5 +859,5 @@
 message FindLicenseRequest {
-  // This comment is left unintentionally blank.
+  // Repository is repository where to detect the license.
   Repository repository = 1 [(target_repository)=true];
 }
 
@@ -794,6 +874,8 @@
   // LicenseUrl is a URL to the license on the internet.
   string license_url = 3;
   // LicensePath is a path to the file that contains the text of the license.
+  // When a LICENSE file is found containing the filename of another file,
+  // that filename will be returned, for example "mit.txt".
   string license_path = 4;
   // LicenseNickname is a shortened full name for better readability.
   // It exists only for a small set of licenses and an empty value is returned in most cases.
@@ -949,7 +1031,7 @@
 
 // This comment is left unintentionally blank.
 message SearchFilesByNameResponse {
-  // This comment is left unintentionally blank.
+  // Files contains the paths of files that have been found to match the query.
   repeated bytes files = 1;
 }
 
@@ -1062,5 +1144,5 @@
 message ReplicateRepositoryResponse{
 }
 
-// This comment is left unintentionally blank.
+// OptimizeRepositoryRequest is a request for the OptimizeRepository RPC.
 message OptimizeRepositoryRequest {
@@ -1066,3 +1148,17 @@
 message OptimizeRepositoryRequest {
-  // This comment is left unintentionally blank.
+  // Strategy determines how the repository shall be optimized.
+  enum Strategy {
+    // STRATEGY_UNSPECIFIED indicates that the strategy has not been explicitly set by the
+    // caller. The default will be STRATEGY_HEURISTICAL in that case.
+    STRATEGY_UNSPECIFIED = 0;
+    // STRATEGY_HEURISTICAL performs heuristical optimizations in the repository. The server will
+    // decide on a set of heuristics which parts need optimization and which ones don't to avoid
+    // performing unnecessary optimization tasks.
+    STRATEGY_HEURISTICAL = 1;
+    // STRATEGY_EAGER performs eager optimizations in the repository. The server will optimize all
+    // data structures regardless of whether they are well-optimized already.
+    STRATEGY_EAGER = 2;
+  }
+
+  // Repository is the repository that should be optimized.
   Repository repository = 1 [(target_repository)=true];
@@ -1068,3 +1164,5 @@
   Repository repository = 1 [(target_repository)=true];
+  // Strategy is the strategy that determines which parts of the repository shall be optimized.
+  Strategy strategy = 2;
 }
 
@@ -1069,7 +1167,7 @@
 }
 
-// This comment is left unintentionally blank.
-message OptimizeRepositoryResponse{
+// OptimizeRepositoryResponse is a response for the OptimizeRepository RPC.
+message OptimizeRepositoryResponse {
 }
 
 // PruneUnreachableObjectsRequest is a request for the PruneUnreachableObjects
@@ -1107,3 +1205,13 @@
   // Path read from the "gitlab.fullpath" config key.
   string path = 1;
 }
+
+// RemoveAll is a request for the RemoveAll RPC.
+message RemoveAllRequest {
+  // StorageName of the storage to have all repositories removed from.
+  string storage_name = 1;
+}
+
+// RemoveAllResponse is a response for the RemoveAll RPC.
+message RemoveAllResponse {
+}
diff --git a/protos/server.proto b/protos/server.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cHJvdG9zL3NlcnZlci5wcm90bw==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL3NlcnZlci5wcm90bw== 100644
--- a/protos/server.proto
+++ b/protos/server.proto
@@ -79,7 +79,7 @@
 // ClockSyncedRequest contains settings to be used for the system clock synchronisation check.
 message ClockSyncedRequest {
   // NtpHost is a URL to the external NTP service that should be used for clock sync check.
-  // Default is "ntp.pool.org"
+  // Default is "pool.ntp.org"
   string ntp_host = 1;
   reserved "drift_threshold_millis";
   reserved 2;
diff --git a/protos/service_config.proto b/protos/service_config.proto
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL3NlcnZpY2VfY29uZmlnLnByb3Rv
--- /dev/null
+++ b/protos/service_config.proto
@@ -0,0 +1,82 @@
+syntax = "proto3";
+
+package gitaly;
+
+import "google/protobuf/duration.proto";
+import "google/protobuf/wrappers.proto";
+
+option go_package = "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb";
+
+// ServiceConfig defines the configuration that allows service owners to publish parameters to be
+// automatically used by all clients of their service. gRPC doesn't export this protobuf. So, we
+// built a minimized version to for Gitaly use. The completed version is defined at
+// https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto
+message ServiceConfig {
+  // protolint:disable:next REPEATED_FIELD_NAMES_PLURALIZED
+  repeated LoadBalancingConfig load_balancing_config = 1;
+  // protolint:disable:next REPEATED_FIELD_NAMES_PLURALIZED
+  repeated MethodConfig method_config = 2;
+}
+
+// LoadBalancingConfig wraps around the round-robin strategies. Only one strategy can be selected.
+message LoadBalancingConfig {
+  oneof policy {
+    // PickFirst strategy
+    PickFirstConfig pick_first = 1 [json_name = "pick_first"];
+    // RoundRobin strategy
+    RoundRobinConfig round_robin = 2 [json_name = "round_robin"];
+  }
+}
+
+// PickFirstConfig signals the pick_first load-balancing strategy. This strategy is the default
+// strategy of grpc client libraries so that the connection has only one subchannel, which is the
+// first address after resolution
+message PickFirstConfig {
+}
+
+// RoundRobinConfig indicates the round_robin strategy. This strategy distributes the incoming
+// requests to active subchannels in a round-robin fashion.
+message RoundRobinConfig {
+}
+
+// Configuration for a method.
+message MethodConfig {
+  // Name is an object indicating which services/methods being affected by the config
+  message Name {
+    // service is name of the service,including its package. For example: gitaly.SmartHTTPService
+    string service = 1;
+    // method is the name of the method within the above service. Empty method name implies the
+    // method policy is effective for all methods of the service
+    string method = 2;
+  }
+
+  // RetryPolicy defines the configuration for exponential backoff when a request fails
+  message RetryPolicy {
+    // max_attempts is the total retry attempts client perform before bailing out
+    uint32 max_attempts = 1;
+    // initial_backoff is the minimum delay for the first retries
+    google.protobuf.Duration initial_backoff = 2;
+    // max_backoff is the minimum delay
+    google.protobuf.Duration max_backoff = 3;
+    // backoff_multiplier is the factor determining "how fast" the delay increases after each retry
+    float backoff_multiplier = 4;
+    // retryable_status_codes defines the list of eligible status codes. The status must be in
+    // capitalized snake_case form. For example, UNAVAILABLE, FAILED_PRECONDITION
+    repeated string retryable_status_codes = 5;
+  }
+
+  // name defines the list of affected services/methods
+  // The name should be  is the definition of grpc. Nothing we can do about it
+  // protolint:disable:next REPEATED_FIELD_NAMES_PLURALIZED
+  repeated Name name = 1;
+  // The following fields are unused by Gitaly at the moment. Please refer to the original
+  // documentation for more information
+  // protolint:disable FIELDS_HAVE_COMMENT
+  google.protobuf.BoolValue wait_for_ready = 2;
+  google.protobuf.Duration timeout = 3;
+  google.protobuf.UInt32Value max_request_message_bytes = 4;
+  google.protobuf.UInt32Value max_response_message_bytes = 5;
+  // protolint:enable FIELDS_HAVE_COMMENT
+  // retry_policy defines the exponential backoff configuration for the affected services/methods
+  RetryPolicy retry_policy = 6;
+}
diff --git a/protos/shared.proto b/protos/shared.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cHJvdG9zL3NoYXJlZC5wcm90bw==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL3NoYXJlZC5wcm90bw== 100644
--- a/protos/shared.proto
+++ b/protos/shared.proto
@@ -21,6 +21,17 @@
   TAG = 4; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
 }
 
+// ObjectFormat is the object format that a Git repository can use.
+enum ObjectFormat {
+  // OBJECT_FORMAT_UNSPECIFIED is the default object type when it has not been explicitly requested
+  // by the client. Defaults to OBJECT_FORMAT_SHA1.
+  OBJECT_FORMAT_UNSPECIFIED = 0;
+  // OBJECT_FORMAT_SHA1 is the object format based on the SHA1 hash.
+  OBJECT_FORMAT_SHA1 = 1;
+  // OBJECT_FORMAT_SHA256 is the object format based on the SHA256 hash. This is experimental.
+  OBJECT_FORMAT_SHA256 = 2;
+}
+
 // This comment is left unintentionally blank.
 enum SignatureType {
   // This comment is left unintentionally blank.
@@ -114,6 +125,14 @@
   // The stats include additions, deletions and changed_files,
   // they are only set when `include_shortstat == true`.
   CommitStatInfo  short_stats = 11;
+  // referenced_by contains fully-qualified reference names (e.g refs/heads/main)
+  // that point to the commit.
+  repeated bytes referenced_by = 12; // protolint:disable:this REPEATED_FIELD_NAMES_PLURALIZED
+  // The encoding of the commit message. This field will only be present if
+  // `i18n.commitEncoding` was set to a value other than "UTF-8" at the time
+  // this commit was made.
+  // See: https://git-scm.com/docs/git-commit#_discussion
+  string encoding = 13;
 }
 
 // This comment is left unintentionally blank.
diff --git a/protos/smarthttp.proto b/protos/smarthttp.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cHJvdG9zL3NtYXJ0aHR0cC5wcm90bw==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cHJvdG9zL3NtYXJ0aHR0cC5wcm90bw== 100644
--- a/protos/smarthttp.proto
+++ b/protos/smarthttp.proto
@@ -28,13 +28,6 @@
     };
   }
 
-  // Request and response body for POST /upload-pack
-  rpc PostUploadPack(stream PostUploadPackRequest) returns (stream PostUploadPackResponse) {
-    option (op_type) = {
-      op: ACCESSOR
-    };
-  }
-
   // Request and response body for POST /upload-pack using sidechannel protocol
   rpc PostUploadPackWithSidechannel(PostUploadPackWithSidechannelRequest) returns (PostUploadPackWithSidechannelResponse) {
     option (op_type) = {
@@ -68,25 +61,6 @@
 }
 
 // This comment is left unintentionally blank.
-message PostUploadPackRequest {
-  // repository should only be present in the first message of the stream
-  Repository repository = 1 [(target_repository)=true];
-  // Raw data to be copied to stdin of 'git upload-pack'
-  bytes data = 2;
-  // Parameters to use with git -c (key=value pairs)
-  repeated string git_config_options = 3;
-
-  // Git protocol version
-  string git_protocol = 4;
-}
-
-// This comment is left unintentionally blank.
-message PostUploadPackResponse {
-  // Raw data from stdout of 'git upload-pack'
-  bytes data = 1;
-}
-
-// This comment is left unintentionally blank.
 message PostUploadPackWithSidechannelRequest {
   // repository should only be present in the first message of the stream
   Repository repository = 1 [(target_repository)=true];
diff --git a/ruby/lib/hgitaly.rb b/ruby/lib/hgitaly.rb
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVieS9saWIvaGdpdGFseS5yYg==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVieS9saWIvaGdpdGFseS5yYg== 100644
--- a/ruby/lib/hgitaly.rb
+++ b/ruby/lib/hgitaly.rb
@@ -5,5 +5,7 @@
 
 require 'hgitaly/mercurial-changeset_services_pb'
 
+require 'hgitaly/mercurial-operations_services_pb'
+
 require 'hgitaly/mercurial-repository_services_pb'
 
diff --git a/ruby/lib/hgitaly/mercurial-operations_pb.rb b/ruby/lib/hgitaly/mercurial-operations_pb.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVieS9saWIvaGdpdGFseS9tZXJjdXJpYWwtb3BlcmF0aW9uc19wYi5yYg==
--- /dev/null
+++ b/ruby/lib/hgitaly/mercurial-operations_pb.rb
@@ -0,0 +1,35 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: mercurial-operations.proto
+
+require 'lint_pb'
+require 'shared_pb'
+require 'google/protobuf'
+
+Google::Protobuf::DescriptorPool.generated_pool.build do
+  add_file("mercurial-operations.proto", :syntax => :proto3) do
+    add_message "hgitaly.MergeAnalysisRequest" do
+      optional :repository, :message, 1, "gitaly.Repository"
+      optional :source_revision, :bytes, 2
+      optional :target_revision, :bytes, 3
+      optional :skip_conflicts_check, :bool, 4
+    end
+    add_message "hgitaly.MergeAnalysisResponse" do
+      optional :is_fast_forward, :bool, 1
+      optional :has_obsolete_changesets, :bool, 2
+      optional :has_unstable_changesets, :bool, 3
+      optional :has_conflicts, :bool, 4
+      optional :target_is_public, :bool, 5
+      optional :target_node_id, :string, 6
+      optional :target_branch, :bytes, 7
+      optional :target_topic, :bytes, 8
+      optional :source_node_id, :string, 9
+      optional :source_branch, :bytes, 10
+      optional :source_topic, :bytes, 11
+    end
+  end
+end
+
+module Hgitaly
+  MergeAnalysisRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hgitaly.MergeAnalysisRequest").msgclass
+  MergeAnalysisResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hgitaly.MergeAnalysisResponse").msgclass
+end
diff --git a/ruby/lib/hgitaly/mercurial-operations_services_pb.rb b/ruby/lib/hgitaly/mercurial-operations_services_pb.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVieS9saWIvaGdpdGFseS9tZXJjdXJpYWwtb3BlcmF0aW9uc19zZXJ2aWNlc19wYi5yYg==
--- /dev/null
+++ b/ruby/lib/hgitaly/mercurial-operations_services_pb.rb
@@ -0,0 +1,25 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# Source: mercurial-operations.proto for package 'hgitaly'
+
+require 'grpc'
+require 'mercurial-operations_pb'
+
+module Hgitaly
+  module MercurialOperationsService
+    class Service
+
+      include ::GRPC::GenericService
+
+      self.marshal_class_method = :encode
+      self.unmarshal_class_method = :decode
+      self.service_name = 'hgitaly.MercurialOperationsService'
+
+      # Provide all information for a possible merge
+      #
+      # Will be used in mergeability checks and last-minute checks if needed.
+      rpc :MergeAnalysis, ::Hgitaly::MergeAnalysisRequest, ::Hgitaly::MergeAnalysisResponse
+    end
+
+    Stub = Service.rpc_stub_class
+  end
+end
diff --git a/ruby/lib/hgitaly/version.rb b/ruby/lib/hgitaly/version.rb
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVieS9saWIvaGdpdGFseS92ZXJzaW9uLnJi..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVieS9saWIvaGdpdGFseS92ZXJzaW9uLnJi 100644
--- a/ruby/lib/hgitaly/version.rb
+++ b/ruby/lib/hgitaly/version.rb
@@ -1,4 +1,4 @@
 # This file is generated by generate-grpc-lib. Do not edit.
 module Hgitaly
-  VERSION = '0.31.0'
+  VERSION = '0.40.3'
 end
diff --git a/rust/rhgitaly/clippy.toml b/rust/rhgitaly/clippy.toml
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9jbGlwcHkudG9tbA==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9jbGlwcHkudG9tbA== 100644
--- a/rust/rhgitaly/clippy.toml
+++ b/rust/rhgitaly/clippy.toml
@@ -1,3 +1,3 @@
 # MSRV is as Mercurial's, defined as the version in Debian testing
 # (currrently bookworm)
-msrv = "1.61"
+msrv = "1.65"
diff --git a/rust/rhgitaly/proto/commit.proto b/rust/rhgitaly/proto/commit.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9wcm90by9jb21taXQucHJvdG8=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9wcm90by9jb21taXQucHJvdG8= 100644
--- a/rust/rhgitaly/proto/commit.proto
+++ b/rust/rhgitaly/proto/commit.proto
@@ -20,6 +20,13 @@
   }
 
   // This comment is left unintentionally blank.
+  rpc GetTreeEntries(GetTreeEntriesRequest) returns (stream GetTreeEntriesResponse) {
+    option (op_type) = {
+      op: ACCESSOR
+    };
+  }
+
+  // This comment is left unintentionally blank.
   rpc FindCommit(FindCommitRequest) returns (FindCommitResponse) {
     option (op_type) = {
       op: ACCESSOR
@@ -490,8 +497,11 @@
     float share = 2;
     // Color specifies the associated color for this language, for example #3fd5e0.
     string color = 3;
-    // FileCount tells how many files with this language are found.
-    uint32 file_count = 4;
+    // FileCount was a uint32 that stored how many files with this language were found.
+    // However, it was never used and there is no client demand for it.
+    // See: https://gitlab.com/gitlab-org/gitaly/-/issues/4293.
+    reserved 4;
+    reserved "file_count";
     // Bytes is the total amount of bytes written in this language
     uint64 bytes = 5;
   }
diff --git a/rust/rhgitaly/proto/ref.proto b/rust/rhgitaly/proto/ref.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9wcm90by9yZWYucHJvdG8=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9wcm90by9yZWYucHJvdG8= 100644
--- a/rust/rhgitaly/proto/ref.proto
+++ b/rust/rhgitaly/proto/ref.proto
@@ -13,6 +13,13 @@
 service RefService {
 
   // This comment is left unintentionally blank.
+  rpc FindDefaultBranchName(FindDefaultBranchNameRequest) returns (FindDefaultBranchNameResponse) {
+    option (op_type) = {
+      op: ACCESSOR
+    };
+  }
+
+  // This comment is left unintentionally blank.
   rpc RefExists(RefExistsRequest) returns (RefExistsResponse) {
     option (op_type) = {
       op: ACCESSOR
diff --git a/rust/rhgitaly/proto/repository.proto b/rust/rhgitaly/proto/repository.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9wcm90by9yZXBvc2l0b3J5LnByb3Rv..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9wcm90by9yZXBvc2l0b3J5LnByb3Rv 100644
--- a/rust/rhgitaly/proto/repository.proto
+++ b/rust/rhgitaly/proto/repository.proto
@@ -204,6 +204,10 @@
   // repository. Note, this will be treated as the branch name and not a
   // fully qualified reference.
   bytes default_branch = 2;
+  // ObjectFormat is the object format the repository should be created with. Note that this is
+  // experimental and should not be used by callers yet. It is mostly intended for internal testing
+  // purposes in Gitaly right now.
+  ObjectFormat object_format = 3;
 }
 
 // This comment is left unintentionally blank.
@@ -287,5 +291,5 @@
   bool result = 1;
 }
 
-// This comment is left unintentionally blank.
+// FsckRequest is a request for the Fsck RPC.
 message FsckRequest {
@@ -291,5 +295,5 @@
 message FsckRequest {
-  // This comment is left unintentionally blank.
+  // Repository is the repository that shall be checked for consistency.
   Repository repository = 1 [(target_repository)=true];
 }
 
@@ -293,5 +297,5 @@
   Repository repository = 1 [(target_repository)=true];
 }
 
-// This comment is left unintentionally blank.
+// FsckResponse is a response for the Fsck RPC.
 message FsckResponse {
@@ -297,5 +301,5 @@
 message FsckResponse {
-  // This comment is left unintentionally blank.
+  // Error contains both stdout and stderr of git-fsck(1) in case it returned an error.
   bytes error = 1;
 }
 
@@ -462,5 +466,5 @@
 message CreateRepositoryFromBundleResponse {
 }
 
-// This comment is left unintentionally blank.
+// FindLicenseRequest asks to detect the license for the given repository.
 message FindLicenseRequest {
@@ -466,5 +470,5 @@
 message FindLicenseRequest {
-  // This comment is left unintentionally blank.
+  // Repository is repository where to detect the license.
   Repository repository = 1 [(target_repository)=true];
 }
 
@@ -481,6 +485,8 @@
   // LicenseUrl is a URL to the license on the internet.
   string license_url = 3;
   // LicensePath is a path to the file that contains the text of the license.
+  // When a LICENSE file is found containing the filename of another file,
+  // that filename will be returned, for example "mit.txt".
   string license_path = 4;
   // LicenseNickname is a shortened full name for better readability.
   // It exists only for a small set of licenses and an empty value is returned in most cases.
@@ -749,5 +755,5 @@
 message ReplicateRepositoryResponse{
 }
 
-// This comment is left unintentionally blank.
+// OptimizeRepositoryRequest is a request for the OptimizeRepository RPC.
 message OptimizeRepositoryRequest {
@@ -753,3 +759,17 @@
 message OptimizeRepositoryRequest {
-  // This comment is left unintentionally blank.
+  // Strategy determines how the repository shall be optimized.
+  enum Strategy {
+    // STRATEGY_UNSPECIFIED indicates that the strategy has not been explicitly set by the
+    // caller. The default will be STRATEGY_HEURISTICAL in that case.
+    STRATEGY_UNSPECIFIED = 0;
+    // STRATEGY_HEURISTICAL performs heuristical optimizations in the repository. The server will
+    // decide on a set of heuristics which parts need optimization and which ones don't to avoid
+    // performing unnecessary optimization tasks.
+    STRATEGY_HEURISTICAL = 1;
+    // STRATEGY_EAGER performs eager optimizations in the repository. The server will optimize all
+    // data structures regardless of whether they are well-optimized already.
+    STRATEGY_EAGER = 2;
+  }
+
+  // Repository is the repository that should be optimized.
   Repository repository = 1 [(target_repository)=true];
@@ -755,3 +775,5 @@
   Repository repository = 1 [(target_repository)=true];
+  // Strategy is the strategy that determines which parts of the repository shall be optimized.
+  Strategy strategy = 2;
 }
 
@@ -756,7 +778,7 @@
 }
 
-// This comment is left unintentionally blank.
-message OptimizeRepositoryResponse{
+// OptimizeRepositoryResponse is a response for the OptimizeRepository RPC.
+message OptimizeRepositoryResponse {
 }
 
 // PruneUnreachableObjectsRequest is a request for the PruneUnreachableObjects
diff --git a/rust/rhgitaly/proto/server.proto b/rust/rhgitaly/proto/server.proto
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9wcm90by9zZXJ2ZXIucHJvdG8=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9wcm90by9zZXJ2ZXIucHJvdG8= 100644
--- a/rust/rhgitaly/proto/server.proto
+++ b/rust/rhgitaly/proto/server.proto
@@ -70,7 +70,7 @@
 // ClockSyncedRequest contains settings to be used for the system clock synchronisation check.
 message ClockSyncedRequest {
   // NtpHost is a URL to the external NTP service that should be used for clock sync check.
-  // Default is "ntp.pool.org"
+  // Default is "pool.ntp.org"
   string ntp_host = 1;
   reserved "drift_threshold_millis";
   reserved 2;
diff --git a/rust/rhgitaly/src/gitlab/mod.rs b/rust/rhgitaly/src/gitlab/mod.rs
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9zcmMvZ2l0bGFiL21vZC5ycw==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9zcmMvZ2l0bGFiL21vZC5ycw== 100644
--- a/rust/rhgitaly/src/gitlab/mod.rs
+++ b/rust/rhgitaly/src/gitlab/mod.rs
@@ -5,3 +5,12 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
 pub mod revision;
 pub mod state;
+
+const GITLAB_BRANCH_REF_PREFIX: &[u8] = b"refs/heads/";
+
+// Return the full GitLab ref path of a GitLab branch
+pub fn gitlab_branch_ref(name: &[u8]) -> Vec<u8> {
+    let mut res = GITLAB_BRANCH_REF_PREFIX.to_vec();
+    res.extend_from_slice(name);
+    res
+}
diff --git a/rust/rhgitaly/src/gitlab/state.rs b/rust/rhgitaly/src/gitlab/state.rs
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9zcmMvZ2l0bGFiL3N0YXRlLnJz..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9zcmMvZ2l0bGFiL3N0YXRlLnJz 100644
--- a/rust/rhgitaly/src/gitlab/state.rs
+++ b/rust/rhgitaly/src/gitlab/state.rs
@@ -60,9 +60,9 @@
 ///
 /// This means that it must be clear from context in code handling `TypedRef`s what
 /// their type is suppposed to be.
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct TypedRef {
     pub name: Vec<u8>,
     pub target_sha: Vec<u8>,
 }
 
@@ -64,9 +64,9 @@
 pub struct TypedRef {
     pub name: Vec<u8>,
     pub target_sha: Vec<u8>,
 }
 
-#[derive(Debug, Clone, PartialEq, derive_more::From)]
+#[derive(Debug, Clone, PartialEq, Eq, derive_more::From)]
 /// Express that a line does not follow the expected file format
 ///
 /// Absence of this error does not mean that the line components are themselves valid.
diff --git a/rust/rhgitaly/src/mercurial.rs b/rust/rhgitaly/src/mercurial.rs
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9zcmMvbWVyY3VyaWFsLnJz..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9zcmMvbWVyY3VyaWFsLnJz 100644
--- a/rust/rhgitaly/src/mercurial.rs
+++ b/rust/rhgitaly/src/mercurial.rs
@@ -5,6 +5,8 @@
 //
 // SPDX-License-Identifier: GPL-2.0-or-later
 //! Utilities for Mercurial content handling
+use std::collections::VecDeque;
+
 use hg::changelog::Changelog;
 use hg::errors::HgError;
 use hg::manifest::{Manifest, ManifestEntry, Manifestlog};
@@ -12,6 +14,7 @@
 use hg::revlog::{Node, NodePrefix, RevlogError, NULL_REVISION};
 use hg::utils::hg_path::HgPath;
 
-use crate::gitaly::ObjectType;
+use crate::gitaly::{tree_entry::EntryType, ObjectType, TreeEntry};
+use crate::util::common_subpath_split;
 
 use super::git;
@@ -16,6 +19,6 @@
 
 use super::git;
-use super::oid::blob_oid;
+use super::oid::{blob_oid, tree_oid};
 use tracing::debug;
 
 #[derive(Debug, Default)]
@@ -256,6 +259,498 @@
     })
 }
 
+/// An iterator over manifest, yielding only entries from a given sub directory
+///
+/// This could be upstreamed to hg-core, where it would benefit the (private) binary search
+/// method to avoid linear scanning for the directory starting point.
+struct ManifestDirIterator<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    /// Path of the directory, without trailing slash
+    path: &'a [u8],
+    in_dir: bool,
+    manifest_iter: IM,
+}
+
+impl<'a, 'm, IM> ManifestDirIterator<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    fn new(manifest_iter: IM, path: &'a [u8]) -> Self {
+        ManifestDirIterator {
+            path,
+            manifest_iter,
+            in_dir: false,
+        }
+    }
+
+    /// Return the length of the string prefix corresponding to path, hence with trailing slash.
+    ///
+    /// This is meant so that, if `self.path` is `"foo"`, then `"foo/bar"[self.path_len()..]` is
+    /// `"bar"`. It also handles the case where `self.path` is empty correctly, sparing the callers
+    /// to check for this special case.
+    fn prefix_len(&self) -> usize {
+        let pl = self.path.len();
+        if pl == 0 {
+            0
+        } else {
+            pl + 1
+        }
+    }
+
+    /// Derive from `self.path` a string prefix ready for concatenation with a relative path.
+    fn prefix(&self) -> Vec<u8> {
+        let mut v = self.path.to_vec();
+        if !v.is_empty() {
+            v.push(b'/');
+        }
+        v
+    }
+
+    fn is_path_inside(&self, other_path: &[u8]) -> bool {
+        let pl = self.path.len();
+        pl == 0
+            || (other_path.len() > pl
+                && other_path[pl] == b'/'
+                && other_path.starts_with(self.path))
+    }
+}
+
+impl<'a, 'm, IM> Iterator for ManifestDirIterator<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    type Item = Result<ManifestEntry<'m>, HgError>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        while let Some(entry_res) = self.manifest_iter.next() {
+            match entry_res {
+                Err(e) => {
+                    return Some(Err(e));
+                }
+                Ok(entry) => {
+                    if !self.is_path_inside(entry.path.as_bytes()) {
+                        if self.in_dir {
+                            // we're leaving the directory to list, job done
+                            return None;
+                        } else {
+                            // we have not entered the directory to list yet
+                            continue;
+                        }
+                    }
+                    return Some(Ok(entry));
+                }
+            }
+        }
+        None
+    }
+}
+
+/// Result of splitting a path for first and last segment
+struct PathWithTopLevelDir<'a> {
+    /// The full path, for flat_path computation
+    path: &'a [u8],
+    /// The top level directory in the path (first segment)
+    top_level: &'a [u8],
+}
+
+/// Analyze the given `sub_path`, splitting the directory part to get its top-level.
+///
+/// The given (full) `entry_path` is for error messages only.
+///
+/// If `sub_path` is a top-level file, `None` is returned. Otherwise, the returned
+/// [`PathWithTopLevelDir`] encloses the directory part of `sub_path`.
+///
+/// The computation relies on expectations for Mercurial manifeset entries, which are reflected
+/// by the returned errors. Using the full entry path here should help with error inspection.
+fn split_dir_and_top_level<'a>(
+    sub_path: &'a [u8],
+    entry_path: &[u8],
+) -> Result<Option<PathWithTopLevelDir<'a>>, HgError> {
+    let mut rsplit = sub_path.rsplitn(2, |c| *c == b'/');
+    rsplit.next().ok_or_else(|| {
+        HgError::corrupted(format!(
+            "Manifest entry with trailing slash: {:?}",
+            entry_path
+        ))
+    })?;
+    rsplit
+        .next()
+        .map(|dir_path| {
+            let top_level = dir_path.splitn(2, |c| *c == b'/').next().ok_or_else(|| {
+                HgError::corrupted(format!(
+                    "Manifest entry with double slash: {:?}",
+                    entry_path
+                ))
+            })?;
+            Ok(PathWithTopLevelDir {
+                top_level,
+                path: dir_path,
+            })
+        })
+        .transpose()
+}
+
+/// Iterator yielding a [`TreeEntry`] without flat path for each top-level file or directory in a
+/// manifest directory.
+pub struct DirIteratorWithoutFlatPaths<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    /// Is expected to be the repetition of an incoming request "oid" field,
+    /// often something like `branch/default` and not a real OID
+    commit_oid: String,
+    changeset_node: Node,
+    root_oid: String,
+    manifest_dir_iter: ManifestDirIterator<'a, 'm, IM>,
+    current_subdir: &'m [u8],
+}
+
+impl<'a, 'm, IM> DirIteratorWithoutFlatPaths<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    pub fn new(commit_oid: String, changeset_node: Node, manifest: IM, path: &'a [u8]) -> Self {
+        let root_oid = tree_oid(&changeset_node, b"");
+        Self {
+            commit_oid,
+            changeset_node,
+            root_oid,
+            manifest_dir_iter: ManifestDirIterator::new(manifest, path),
+            current_subdir: &[],
+        }
+    }
+
+    fn tree_entry(
+        &self,
+        path: &[u8],
+        oid: String,
+        obj_type: EntryType,
+        mode: i32,
+    ) -> Result<TreeEntry, HgError> {
+        Ok(TreeEntry {
+            oid,
+            mode,
+            commit_oid: self.commit_oid.clone(),
+            root_oid: self.root_oid.clone(),
+            path: path.to_vec(),
+            r#type: obj_type as i32,
+            flat_path: Vec::new(),
+        })
+    }
+}
+
+impl<'a, 'm, IM> Iterator for DirIteratorWithoutFlatPaths<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    type Item = Result<TreeEntry, HgError>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        while let Some(entry_res) = self.manifest_dir_iter.next() {
+            match entry_res {
+                Err(e) => {
+                    return Some(Err(e));
+                }
+                Ok(entry) => {
+                    let ep = entry.path.as_bytes();
+                    let sp = &ep[self.manifest_dir_iter.prefix_len()..];
+                    match split_dir_and_top_level(sp, ep) {
+                        Ok(None) => match git_perms(&entry) {
+                            Err(e) => return Some(Err(e)),
+                            Ok(mode) => {
+                                return Some(self.tree_entry(
+                                    ep,
+                                    blob_oid(&self.changeset_node, ep),
+                                    EntryType::Blob,
+                                    mode,
+                                ));
+                            }
+                        },
+                        Ok(Some(split)) => {
+                            if split.top_level != self.current_subdir {
+                                self.current_subdir = split.top_level;
+                                let full_path = &ep
+                                    [..self.manifest_dir_iter.prefix_len() + split.top_level.len()];
+                                return Some(self.tree_entry(
+                                    full_path,
+                                    tree_oid(&self.changeset_node, full_path),
+                                    EntryType::Tree,
+                                    git::OBJECT_MODE_TREE,
+                                ));
+                            }
+                        }
+                        Err(e) => return Some(Err(e)),
+                    }
+                }
+            }
+        }
+        None
+    }
+}
+
+pub struct RecursiveDirIterator<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    /// Is expected to be the repetition of an incoming request "oid" field,
+    /// often something like `branch/default` and not a real OID
+    commit_oid: String,
+    changeset_node: Node,
+    root_oid: String,
+    manifest_dir_iter: ManifestDirIterator<'a, 'm, IM>,
+    current_subdir: &'m [u8],
+    to_yield: VecDeque<Result<TreeEntry, HgError>>,
+}
+
+impl<'a, 'm, IM> RecursiveDirIterator<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    pub fn new(commit_oid: String, changeset_node: Node, manifest: IM, path: &'a [u8]) -> Self {
+        let root_oid = tree_oid(&changeset_node, b"");
+        Self {
+            commit_oid,
+            changeset_node,
+            root_oid,
+            manifest_dir_iter: ManifestDirIterator::new(manifest, path),
+            current_subdir: "".as_ref(),
+            /// Queue of `TreeEntry` messages that have to be emitted.
+            ///
+            /// Each manifest entry can give rise to several `TreeEntry` messages to yield,
+            /// because of intermediate directories. Hence we store them in this queue.
+            ///
+            /// Each call to `next()` pops either pops from the queue or consumes one or several
+            /// manifest entries, possibly pushing several more entries in the queue beside yielding
+            /// one.
+            to_yield: VecDeque::new(),
+        }
+    }
+
+    fn tree_entry(
+        &self,
+        path: &[u8],
+        oid: String,
+        obj_type: EntryType,
+        mode: i32,
+    ) -> Result<TreeEntry, HgError> {
+        Ok(TreeEntry {
+            oid,
+            mode,
+            commit_oid: self.commit_oid.clone(),
+            root_oid: self.root_oid.clone(),
+            path: path.to_vec(),
+            r#type: obj_type as i32,
+            flat_path: Vec::new(), // resulting flat_path is always default for recursive queries
+        })
+    }
+}
+
+impl<'a, 'm, IM> Iterator for RecursiveDirIterator<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    type Item = Result<TreeEntry, HgError>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(entry) = self.to_yield.pop_front() {
+            return Some(entry);
+        }
+
+        while let Some(entry_res) = self.manifest_dir_iter.next() {
+            match entry_res {
+                Err(e) => {
+                    return Some(Err(e));
+                }
+                Ok(entry) => {
+                    let ep = entry.path.as_bytes();
+                    let sp = &ep[self.manifest_dir_iter.prefix_len()..];
+
+                    let (mut subdir, rem_idx) = common_subpath_split(self.current_subdir, sp);
+                    for i in rem_idx..sp.len() {
+                        if sp[i] != b'/' {
+                            continue;
+                        }
+                        subdir = &sp[..i];
+                        let fulldir = &ep[..i + self.manifest_dir_iter.prefix_len()];
+                        self.to_yield.push_back(self.tree_entry(
+                            fulldir,
+                            tree_oid(&self.changeset_node, fulldir),
+                            EntryType::Tree,
+                            git::OBJECT_MODE_TREE,
+                        ));
+                    }
+                    self.current_subdir = subdir;
+
+                    match git_perms(&entry) {
+                        Err(e) => {
+                            self.to_yield.push_back(Err(e));
+                        }
+                        Ok(mode) => {
+                            self.to_yield.push_back(self.tree_entry(
+                                ep,
+                                blob_oid(&self.changeset_node, ep),
+                                EntryType::Blob,
+                                mode,
+                            ));
+                        }
+                    }
+                }
+            }
+        }
+        self.to_yield.pop_front()
+    }
+}
+
+/// Non recursive iterator over a directory, including "flat path" information
+pub struct DirIteratorWithFlatPaths<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    /// Is expected to be the repetition of an incoming request "oid" field,
+    /// often something like `branch/default` and not a real OID
+    commit_oid: String,
+    changeset_node: Node,
+    root_oid: String,
+    manifest_dir_iter: ManifestDirIterator<'a, 'm, IM>,
+    /// This iterator also has the potential to yield several elements for a single
+    /// read manifest entry, however this queue is overvill, because it can at most yield two
+    /// TreeEntries (a Tree and a Blob), so we should later on replace with something lighter.
+    to_yield: VecDeque<Result<TreeEntry, HgError>>,
+}
+
+impl<'a, 'm, IM> DirIteratorWithFlatPaths<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    pub fn new(commit_oid: String, changeset_node: Node, manifest: IM, path: &'a [u8]) -> Self {
+        let root_oid = tree_oid(&changeset_node, b"");
+        Self {
+            commit_oid,
+            changeset_node,
+            root_oid,
+            manifest_dir_iter: ManifestDirIterator::new(manifest, path),
+            /// Queue of `TreeEntry` messages that have to be emitted.
+            ///
+            /// Each manifest entry can give rise to several `TreeEntry` messages to yield,
+            /// because of intermediate directories. Hence we store them in this queue.
+            ///
+            /// each call to `next()` either pops an element from the queue or reads next manifest
+            /// line.
+            to_yield: VecDeque::new(),
+        }
+    }
+
+    fn dir_tree_entry(&self, rel_path: &[u8], rel_flat_path: &[u8]) -> Result<TreeEntry, HgError> {
+        let mut path = self.manifest_dir_iter.prefix();
+        path.extend_from_slice(rel_path);
+        let mut flat_path = self.manifest_dir_iter.prefix();
+        flat_path.extend_from_slice(rel_flat_path);
+        Ok(TreeEntry {
+            oid: tree_oid(&self.changeset_node, &path),
+            mode: git::OBJECT_MODE_TREE,
+            commit_oid: self.commit_oid.clone(),
+            root_oid: self.root_oid.clone(),
+            path,
+            r#type: EntryType::Tree as i32,
+            flat_path,
+        })
+    }
+
+    fn file_tree_entry(&self, path: &[u8], mode: i32) -> Result<TreeEntry, HgError> {
+        Ok(TreeEntry {
+            mode,
+            oid: blob_oid(&self.changeset_node, path),
+            commit_oid: self.commit_oid.clone(),
+            root_oid: self.root_oid.clone(),
+            path: path.to_vec(),
+            r#type: EntryType::Blob as i32,
+            flat_path: path.to_vec(),
+        })
+    }
+
+    fn enqueue_file_entry(&mut self, entry: &ManifestEntry) {
+        match git_perms(entry) {
+            Err(e) => {
+                self.to_yield.push_back(Err(e));
+            }
+            Ok(mode) => {
+                self.to_yield
+                    .push_back(self.file_tree_entry(entry.path.as_bytes(), mode));
+            }
+        }
+    }
+
+    /// Main scanning loop for [`Iterator`] implementation
+    ///
+    /// Returns `Result<()>` whereas `next()` returns instead of `Option<Result<T>>`
+    /// to make error treatment easier (question-mark operator). Therefore this method
+    /// only pushes to `self.to_yield`.
+    fn inner_next(&mut self) -> Result<(), HgError> {
+        let mut current_subdir: Option<&[u8]> = None;
+        let mut current_flat_path: &[u8] = &[];
+
+        while let Some(entry_res) = self.manifest_dir_iter.next() {
+            let entry = entry_res?;
+            let ep = entry.path.as_bytes();
+            let sp = &ep[self.manifest_dir_iter.prefix_len()..];
+
+            if let Some(subdir) = current_subdir {
+                let (common, rem_idx) = common_subpath_split(subdir, sp);
+                if rem_idx != 0 {
+                    current_flat_path = common;
+                } else {
+                    // we are leaving current_subdir, so schedule to yield it and clear it
+                    // so that the later check whether we are entering a new one or simply have
+                    // a top-level file runs.
+                    self.to_yield
+                        .push_back(self.dir_tree_entry(subdir, current_flat_path));
+                    current_subdir = None
+                }
+            }
+
+            if current_subdir.is_none() {
+                match split_dir_and_top_level(sp, ep)? {
+                    None => {
+                        self.enqueue_file_entry(&entry);
+                        break;
+                    }
+                    Some(split) => {
+                        current_subdir = Some(split.top_level);
+                        current_flat_path = split.path;
+                    }
+                }
+            }
+        }
+        // If the last entry is not a top-level file, then the loop ends without yielding
+        // `current_subdir`, hence we need to do it now.
+        if let Some(subdir) = current_subdir {
+            self.to_yield
+                .push_back(self.dir_tree_entry(subdir, current_flat_path));
+        }
+        Ok(())
+    }
+}
+
+impl<'a, 'm, IM> Iterator for DirIteratorWithFlatPaths<'a, 'm, IM>
+where
+    IM: Iterator<Item = Result<ManifestEntry<'m>, HgError>>,
+{
+    type Item = Result<TreeEntry, HgError>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(entry) = self.to_yield.pop_front() {
+            return Some(entry);
+        }
+
+        match self.inner_next() {
+            Ok(()) => self.to_yield.pop_front(),
+            Err(e) => Some(Err(e)),
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
 
@@ -385,4 +880,253 @@
             ])
         )
     }
+
+    #[test]
+    fn test_manifest_dir_iterator() {
+        let manifest = paths_manifest(vec!["foo/a", "foo/sub/a", "top-file"]);
+        let iter = ManifestDirIterator::new(manifest.into_iter(), b"");
+        let res: Vec<&[u8]> = iter.map(|r| r.unwrap().path.as_bytes()).collect();
+        assert_eq!(
+            res,
+            vec![
+                b"foo/a".as_ref(),
+                b"foo/sub/a".as_ref(),
+                b"top-file".as_ref(),
+            ]
+        );
+
+        let manifest = paths_manifest(vec![
+            "foo.",
+            "foo/a",
+            "foo/sub/a",
+            "foo/sub/b",
+            "foo/subb",
+            "foo0",
+            "other/file",
+        ]);
+        let iter = ManifestDirIterator::new(manifest.into_iter(), b"foo");
+        let res: Vec<&[u8]> = iter.map(|r| r.unwrap().path.as_bytes()).collect();
+        assert_eq!(
+            res,
+            vec![
+                b"foo/a".as_ref(),
+                b"foo/sub/a".as_ref(),
+                b"foo/sub/b".as_ref(),
+                b"foo/subb".as_ref(),
+            ]
+        );
+    }
+
+    fn testing_blob_flat_path(cs_node: &Node, path: &[u8], flat_path: &[u8]) -> TreeEntry {
+        TreeEntry {
+            path: path.to_vec(),
+            r#type: EntryType::Blob as i32,
+            mode: git::OBJECT_MODE_NON_EXECUTABLE,
+            commit_oid: "branch/test".to_owned(),
+            root_oid: tree_oid(cs_node, b""),
+            oid: blob_oid(cs_node, path),
+            flat_path: flat_path.to_vec(),
+        }
+    }
+
+    fn testing_blob(cs_node: &Node, path: &[u8]) -> TreeEntry {
+        testing_blob_flat_path(cs_node, path, &[])
+    }
+
+    fn testing_tree_flat_path(cs_node: &Node, path: &[u8], flat_path: &[u8]) -> TreeEntry {
+        TreeEntry {
+            path: path.to_vec(),
+            r#type: EntryType::Tree as i32,
+            mode: git::OBJECT_MODE_TREE,
+            commit_oid: "branch/test".to_owned(),
+            root_oid: tree_oid(cs_node, b""),
+            oid: tree_oid(cs_node, path),
+            flat_path: flat_path.to_vec(),
+        }
+    }
+
+    fn testing_tree(cs_node: &Node, path: &[u8]) -> TreeEntry {
+        testing_tree_flat_path(cs_node, path, &[])
+    }
+
+    #[test]
+    fn test_dir_iterator_without_flat_paths() {
+        let cs_node = Node::from_hex(b"1234567812345678123456781234567812345678").unwrap();
+
+        let manifest = paths_manifest(vec!["foo/a"]);
+        let iter = DirIteratorWithoutFlatPaths::new(
+            "branch/test".to_owned(),
+            cs_node,
+            manifest.into_iter(),
+            &[],
+        );
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(res, vec![testing_tree(&cs_node, b"foo"),]);
+
+        let manifest = paths_manifest(vec![
+            "foo.",
+            "foo/a",
+            "foo/sub/a",
+            "foo/sub/b",
+            "foo/subb",
+            "foo0",
+        ]);
+        let iter = DirIteratorWithoutFlatPaths::new(
+            "branch/test".to_owned(),
+            cs_node,
+            manifest.into_iter(),
+            b"foo",
+        );
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(
+            res,
+            vec![
+                testing_blob(&cs_node, b"foo/a"),
+                testing_tree(&cs_node, b"foo/sub"),
+                testing_blob(&cs_node, b"foo/subb"),
+            ]
+        );
+
+        let manifest = paths_manifest(vec!["foo.", "foo/a", "foo/sub/ssub/b", "foo/subb", "foo0"]);
+        let iter = DirIteratorWithoutFlatPaths::new(
+            "branch/test".to_owned(),
+            cs_node,
+            manifest.into_iter(),
+            b"foo",
+        );
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(
+            res,
+            vec![
+                testing_blob(&cs_node, b"foo/a"),
+                testing_tree(&cs_node, b"foo/sub"),
+                testing_blob(&cs_node, b"foo/subb"),
+            ]
+        );
+    }
+
+    #[test]
+    fn test_recursive_dir_iter() {
+        let cs_node = Node::from_hex(b"1234567812345678123456781234567812345678").unwrap();
+
+        let manifest = paths_manifest(vec!["foo/a"]);
+        let iter =
+            RecursiveDirIterator::new("branch/test".to_owned(), cs_node, manifest.into_iter(), &[]);
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(
+            res,
+            vec![
+                testing_tree(&cs_node, b"foo"),
+                testing_blob(&cs_node, b"foo/a"),
+            ]
+        );
+
+        let manifest = paths_manifest(vec![
+            "foo.",
+            "foo/a",
+            "foo/sub/a",
+            "foo/sub/b",
+            "foo/subb",
+            "foo0",
+        ]);
+        let iter = RecursiveDirIterator::new(
+            "branch/test".to_owned(),
+            cs_node,
+            manifest.into_iter(),
+            b"foo",
+        );
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(
+            res,
+            vec![
+                testing_blob(&cs_node, b"foo/a"),
+                testing_tree(&cs_node, b"foo/sub"),
+                testing_blob(&cs_node, b"foo/sub/a"),
+                testing_blob(&cs_node, b"foo/sub/b"),
+                testing_blob(&cs_node, b"foo/subb"),
+            ]
+        );
+
+        let manifest = paths_manifest(vec![
+            "foo.",
+            "foo/a",
+            "foo/sub/a",
+            "foo/sub/ssub/b",
+            "foo/subb",
+            "foo0",
+        ]);
+        let iter = RecursiveDirIterator::new(
+            "branch/test".to_owned(),
+            cs_node,
+            manifest.into_iter(),
+            b"foo",
+        );
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(
+            res,
+            vec![
+                testing_blob(&cs_node, b"foo/a"),
+                testing_tree(&cs_node, b"foo/sub"),
+                testing_blob(&cs_node, b"foo/sub/a"),
+                testing_tree(&cs_node, b"foo/sub/ssub"),
+                testing_blob(&cs_node, b"foo/sub/ssub/b"),
+                testing_blob(&cs_node, b"foo/subb"),
+            ]
+        );
+    }
+    #[test]
+    fn test_dir_iter_flat_paths() {
+        let cs_node = Node::from_hex(b"1234567812345678123456781234567812345678").unwrap();
+
+        let manifest = paths_manifest(vec!["foo/a"]);
+        let iter = DirIteratorWithFlatPaths::new(
+            "branch/test".to_owned(),
+            cs_node,
+            manifest.into_iter(),
+            &[],
+        );
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(res, vec![testing_tree_flat_path(&cs_node, b"foo", b"foo"),]);
+
+        let manifest = paths_manifest(vec![
+            "foo.",
+            "foo/a",
+            "foo/sub/a",
+            "foo/sub/b",
+            "foo/subb",
+            "foo0",
+        ]);
+        let iter = DirIteratorWithFlatPaths::new(
+            "branch/test".to_owned(),
+            cs_node,
+            manifest.into_iter(),
+            b"foo",
+        );
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(
+            res,
+            vec![
+                testing_blob_flat_path(&cs_node, b"foo/a", b"foo/a"),
+                testing_tree_flat_path(&cs_node, b"foo/sub", b"foo/sub"),
+                testing_blob_flat_path(&cs_node, b"foo/subb", b"foo/subb"),
+            ]
+        );
+
+        let manifest = paths_manifest(vec!["foo.", "foo/a", "foo/sub/ssub/b", "foo/subb", "foo0"]);
+        let iter = DirIteratorWithFlatPaths::new(
+            "branch/test".to_owned(),
+            cs_node,
+            manifest.into_iter(),
+            b"foo",
+        );
+        let res: Vec<TreeEntry> = iter.map(|r| r.unwrap()).collect();
+        assert_eq!(
+            res,
+            vec![
+                testing_blob_flat_path(&cs_node, b"foo/a", b"foo/a"),
+                testing_tree_flat_path(&cs_node, b"foo/sub", b"foo/sub/ssub"),
+                testing_blob_flat_path(&cs_node, b"foo/subb", b"foo/subb"),
+            ]
+        );
+    }
 }
diff --git a/rust/rhgitaly/src/message.rs b/rust/rhgitaly/src/message.rs
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9zcmMvbWVzc2FnZS5ycw==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9zcmMvbWVzc2FnZS5ycw== 100644
--- a/rust/rhgitaly/src/message.rs
+++ b/rust/rhgitaly/src/message.rs
@@ -18,7 +18,7 @@
 use prost_types::Timestamp;
 use std::str::FromStr;
 
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Eq)]
 pub enum TimeStampParseError {
     Empty,
     MissingTimeZone,
diff --git a/rust/rhgitaly/src/service/commit.rs b/rust/rhgitaly/src/service/commit.rs
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9zcmMvc2VydmljZS9jb21taXQucnM=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9zcmMvc2VydmljZS9jb21taXQucnM= 100644
--- a/rust/rhgitaly/src/service/commit.rs
+++ b/rust/rhgitaly/src/service/commit.rs
@@ -5,6 +5,7 @@
 use tracing::{field, info, instrument};
 
 use hg::changelog::Changelog;
+use hg::errors::HgError;
 use hg::repo::Repo;
 use hg::revlog::{NodePrefix, RevlogError, NULL_REVISION};
 use hg::utils::hg_path::HgPath;
@@ -14,8 +15,9 @@
 use crate::git;
 use crate::gitaly::commit_service_server::{CommitService, CommitServiceServer};
 use crate::gitaly::{
-    tree_entry_response::ObjectType, FindCommitRequest, FindCommitResponse,
-    ListCommitsByOidRequest, ListCommitsByOidResponse, Repository, TreeEntryRequest,
-    TreeEntryResponse,
+    get_tree_entries_request::SortBy, tree_entry::EntryType, tree_entry_response::ObjectType,
+    FindCommitRequest, FindCommitResponse, GetTreeEntriesRequest, GetTreeEntriesResponse,
+    ListCommitsByOidRequest, ListCommitsByOidResponse, PaginationParameter, Repository, TreeEntry,
+    TreeEntryRequest, TreeEntryResponse,
 };
 use crate::gitlab::revision::gitlab_revision_node_prefix;
@@ -20,6 +22,9 @@
 };
 use crate::gitlab::revision::gitlab_revision_node_prefix;
-use crate::mercurial::{changelog_entry_manifest, ls_path, read_blob, PathContent};
+use crate::mercurial::{
+    changelog_entry_manifest, ls_path, read_blob, DirIteratorWithFlatPaths,
+    DirIteratorWithoutFlatPaths, PathContent, RecursiveDirIterator,
+};
 use crate::message;
 use crate::metadata::correlation_id;
 use crate::oid::tree_oid;
@@ -27,7 +32,10 @@
     default_repo_spec_error_status, load_repo_and_stream, load_repo_and_then, repo_store_vfs,
     RequestWithRepo,
 };
-use crate::streaming::{BlockingResponseSender, ResultResponseStream};
+use crate::streaming::{
+    empty_response_stream, stream_with_pagination, BlockingResponseSender, PaginableMessageItem,
+    ResultResponseStream,
+};
 
 #[derive(Debug)]
 pub struct CommitServiceImpl {
@@ -71,8 +79,8 @@
     Ok(ListCommitsByOidResponse { commits })
 }
 
-fn not_found_for_path(path: &[u8]) -> Status {
-    Status::not_found(format!("not found: {}", &String::from_utf8_lossy(path)))
+fn not_found_for_path(_path: &[u8]) -> Status {
+    Status::not_found("tree entry not found")
 }
 
 fn stream_tree_entry(
@@ -106,8 +114,7 @@
                 .map_err(|e| Status::internal(format!("Error reading file {:?}", e)))?;
             if req.max_size != 0 && metadata.size > req.max_size {
                 tx.send(Err(Status::failed_precondition(format!(
-                    "TreeEntry: object size ({}) is bigger than the maximum \
-                                         allowed size ({})",
+                    "object size ({}) is bigger than the maximum allowed size ({})",
                     metadata.size, req.max_size
                 ))));
             } else {
@@ -144,9 +151,124 @@
     Ok(())
 }
 
+fn stream_get_tree_entries(
+    req: GetTreeEntriesRequest,
+    repo: &Repo,
+    node_prefix: NodePrefix,
+    tx: &BlockingResponseSender<GetTreeEntriesResponse>,
+) -> Result<(), Status> {
+    let changelog = repo
+        .changelog()
+        .map_err(|e| Status::internal(format!("Error getting changelog: {:?}", e)))?;
+    let manifestlog = repo
+        .manifestlog()
+        .map_err(|e| Status::internal(format!("Error getting manifestlog: {:?}", e)))?;
+    let (cs_node, manifest) = changelog_entry_manifest(&changelog, &manifestlog, node_prefix)
+        .map_err(|e| Status::internal(format!("Error obtaining manifest: {:?}", e)))?
+        .ok_or_else(|| not_found_for_path(&req.path))?;
+
+    let mut pl = req.path.len();
+    while pl > 0 && req.path[pl - 1] == b'/' {
+        pl -= 1;
+    }
+    if pl == 1 && req.path[0] == b'.' {
+        pl = 0
+    };
+    let trimmed = &req.path[0..pl];
+
+    let commit_oid = String::from_utf8_lossy(&req.revision).to_string();
+
+    if req.recursive {
+        stream_get_tree_entries_from_iterator(
+            tx,
+            &req,
+            RecursiveDirIterator::new(commit_oid, cs_node, manifest.iter(), trimmed),
+        );
+    } else if req.skip_flat_paths {
+        stream_get_tree_entries_from_iterator(
+            tx,
+            &req,
+            DirIteratorWithoutFlatPaths::new(commit_oid, cs_node, manifest.iter(), trimmed),
+        );
+    } else {
+        stream_get_tree_entries_from_iterator(
+            tx,
+            &req,
+            DirIteratorWithFlatPaths::new(commit_oid, cs_node, manifest.iter(), trimmed),
+        );
+    }
+    Ok(())
+}
+
+/// Stream `GetTreeEntries` responses, using the inner iterator.
+///
+/// Various options (e.g., `recursive`) give rise to different types of iterators on entries.
+/// This function applies the sort and pagination parameters, final chunking and streams responses
+/// to the channel.
+fn stream_get_tree_entries_from_iterator<It>(
+    tx: &BlockingResponseSender<GetTreeEntriesResponse>,
+    request: &GetTreeEntriesRequest,
+    iterator: It,
+) where
+    It: Iterator<Item = Result<TreeEntry, HgError>>,
+{
+    let pagination = &request.pagination_params;
+    if request.sort == SortBy::TreesFirst as i32 {
+        let mut trees = Vec::new();
+        let mut blobs = Vec::new();
+        for res in iterator {
+            match res {
+                Ok(ent) => {
+                    if ent.r#type == EntryType::Tree as i32 {
+                        trees.push(Ok(ent));
+                    } else if ent.r#type == EntryType::Blob as i32 {
+                        blobs.push(Ok(ent));
+                    }
+                }
+                // arbitrary choice, weakly motivated by errors being more likely with blobs
+                // than with trees at this point.
+                err => blobs.push(err),
+            }
+        }
+        stream_get_tree_entries_paginated(
+            tx,
+            pagination,
+            trees.into_iter().chain(blobs.into_iter()),
+        );
+    } else {
+        stream_get_tree_entries_paginated(tx, pagination, iterator);
+    }
+}
+
+/// Stream [`GetTreeEntriesResponse`] messages with pagination
+///
+/// This function takes care of the offset and limit logic enclosed into the given pagination
+/// parameter, regrouping the `TreeEntry` elements yielded by the iterator into suitable chunks,
+/// and providing the `next_cursor` value, but does not take care of ordering.
+/// Instead it expects the incoming iiterator to be already properly ordered.
+fn stream_get_tree_entries_paginated<It>(
+    tx: &BlockingResponseSender<GetTreeEntriesResponse>,
+    pagination: &Option<PaginationParameter>,
+    iterator: It,
+) where
+    It: Iterator<Item = Result<TreeEntry, HgError>>,
+{
+    stream_with_pagination(
+        tx,
+        pagination,
+        iterator,
+        |entries, pagination_cursor| GetTreeEntriesResponse {
+            entries,
+            pagination_cursor,
+        },
+        |err| Status::internal(format!("error reading manifest: #{:?}", err)),
+        |token| Status::internal(format!("could not find starting OID: {}", token)),
+    )
+}
+
 impl RequestWithRepo for TreeEntryRequest {
     fn repository_ref(&self) -> Option<&Repository> {
         self.repository.as_ref()
     }
 }
 
@@ -147,9 +269,15 @@
 impl RequestWithRepo for TreeEntryRequest {
     fn repository_ref(&self) -> Option<&Repository> {
         self.repository.as_ref()
     }
 }
 
+impl RequestWithRepo for GetTreeEntriesRequest {
+    fn repository_ref(&self) -> Option<&Repository> {
+        self.repository.as_ref()
+    }
+}
+
 impl RequestWithRepo for ListCommitsByOidRequest {
     fn repository_ref(&self) -> Option<&Repository> {
         self.repository.as_ref()
@@ -162,7 +290,17 @@
     }
 }
 
+impl PaginableMessageItem for TreeEntry {
+    fn match_token(&self, token: &str) -> bool {
+        self.oid == token
+    }
+
+    fn next_cursor(&self) -> String {
+        self.oid.clone()
+    }
+}
+
 #[tonic::async_trait]
 impl CommitService for CommitServiceImpl {
     type ListCommitsByOidStream = ReceiverStream<Result<ListCommitsByOidResponse, Status>>;
     type TreeEntryStream = ReceiverStream<Result<TreeEntryResponse, Status>>;
@@ -165,7 +303,8 @@
 #[tonic::async_trait]
 impl CommitService for CommitServiceImpl {
     type ListCommitsByOidStream = ReceiverStream<Result<ListCommitsByOidResponse, Status>>;
     type TreeEntryStream = ReceiverStream<Result<TreeEntryResponse, Status>>;
+    type GetTreeEntriesStream = ReceiverStream<Result<GetTreeEntriesResponse, Status>>;
 
     async fn list_commits_by_oid(
         &self,
@@ -196,6 +335,16 @@
         self.inner_tree_entry(inner, correlation_id(&metadata))
             .await
     }
+
+    async fn get_tree_entries(
+        &self,
+        request: Request<GetTreeEntriesRequest>,
+    ) -> Result<Response<Self::GetTreeEntriesStream>, Status> {
+        let (metadata, _ext, inner) = request.into_parts();
+
+        self.inner_get_tree_entries(inner, correlation_id(&metadata))
+            .await
+    }
 }
 
 struct TreeEntryTracingRequest<'a>(&'a TreeEntryRequest);
@@ -212,6 +361,22 @@
     }
 }
 
+struct GetTreeEntriesTracingRequest<'a>(&'a GetTreeEntriesRequest);
+
+impl Debug for GetTreeEntriesTracingRequest<'_> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("GetTreeEntriesRequest")
+            .field("repository", &self.0.repository)
+            .field("revision", &String::from_utf8_lossy(&self.0.revision))
+            .field("path", &String::from_utf8_lossy(&self.0.path))
+            .field("recursive", &self.0.recursive)
+            .field("sort", &self.0.sort)
+            .field("pagination_params", &self.0.pagination_params)
+            .field("skip_flat_paths", &self.0.skip_flat_paths)
+            .finish()
+    }
+}
+
 struct FindCommitTracingRequest<'a>(&'a FindCommitRequest);
 
 impl Debug for FindCommitTracingRequest<'_> {
@@ -268,6 +433,56 @@
         }
     }
 
+    #[instrument(name = "get_tree_entries",
+                 skip(self, request),
+                 fields(request=field::debug(GetTreeEntriesTracingRequest(&request))))]
+    async fn inner_get_tree_entries(
+        &self,
+        request: GetTreeEntriesRequest,
+        correlation_id: Option<&MetadataValue<Ascii>>,
+    ) -> ResultResponseStream<GetTreeEntriesResponse> {
+        info!("Processing");
+
+        if request.revision.is_empty() {
+            return Err(Status::invalid_argument("empty revision"));
+        }
+
+        let config = self.config.clone();
+        let store_vfs = repo_store_vfs(&config, &request.repository)
+            .await
+            .map_err(default_repo_spec_error_status)?;
+
+        match gitlab_revision_node_prefix(&store_vfs, &request.revision)
+            .await
+            .map_err(|e| Status::internal(format!("Error resolving revision: {:?}", e)))?
+        {
+            None => {
+                info!("Revision not resolved");
+                empty_response_stream()
+            }
+            Some(node_prefix) => {
+                info!("Revision resolved as {:x}", &node_prefix);
+
+                if request.pagination_params.as_ref().map(|p| p.limit) == Some(0) {
+                    // let's not even load the repo (see Gitaly Comparison tests,
+                    // no error case either)
+                    return empty_response_stream();
+                }
+
+                load_repo_and_stream(
+                    self.config.clone(),
+                    request,
+                    default_repo_spec_error_status,
+                    move |req, repo, tx| {
+                        if let Err(e) = stream_get_tree_entries(req, &repo, node_prefix, &tx) {
+                            tx.send(Err(e));
+                        }
+                    },
+                )
+            }
+        }
+    }
+
     #[instrument(name = "list_commits_by_oid", skip(self))]
     async fn inner_list_commits_by_oid(
         &self,
diff --git a/rust/rhgitaly/src/service/ref.rs b/rust/rhgitaly/src/service/ref.rs
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9zcmMvc2VydmljZS9yZWYucnM=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9zcmMvc2VydmljZS9yZWYucnM= 100644
--- a/rust/rhgitaly/src/service/ref.rs
+++ b/rust/rhgitaly/src/service/ref.rs
@@ -11,5 +11,9 @@
 
 use crate::config::Config;
 use crate::gitaly::ref_service_server::{RefService, RefServiceServer};
-use crate::gitaly::{RefExistsRequest, RefExistsResponse};
+use crate::gitaly::{
+    FindDefaultBranchNameRequest, FindDefaultBranchNameResponse, RefExistsRequest,
+    RefExistsResponse,
+};
+use crate::gitlab::gitlab_branch_ref;
 use crate::gitlab::revision::{map_full_ref, RefError};
@@ -15,4 +19,5 @@
 use crate::gitlab::revision::{map_full_ref, RefError};
+use crate::gitlab::state::get_gitlab_default_branch;
 use crate::metadata::correlation_id;
 use crate::repository::{default_repo_spec_error_status, repo_store_vfs};
 
@@ -23,6 +28,15 @@
 
 #[tonic::async_trait]
 impl RefService for RefServiceImpl {
+    async fn find_default_branch_name(
+        &self,
+        request: Request<FindDefaultBranchNameRequest>,
+    ) -> Result<Response<FindDefaultBranchNameResponse>, Status> {
+        let (metadata, _ext, inner) = request.into_parts();
+
+        self.inner_find_default_branch_name(inner, correlation_id(&metadata))
+            .await
+    }
     async fn ref_exists(
         &self,
         request: Request<RefExistsRequest>,
@@ -46,6 +60,29 @@
 }
 
 impl RefServiceImpl {
+    #[instrument(name = "find_default_branch_name", skip(self))]
+    async fn inner_find_default_branch_name(
+        &self,
+        request: FindDefaultBranchNameRequest,
+        correlation_id: Option<&MetadataValue<Ascii>>,
+    ) -> Result<Response<FindDefaultBranchNameResponse>, Status> {
+        info!("Processing");
+
+        let store_vfs = repo_store_vfs(&self.config, &request.repository)
+            .await
+            .map_err(default_repo_spec_error_status)?;
+
+        Ok(Response::new(FindDefaultBranchNameResponse {
+            name: get_gitlab_default_branch(&store_vfs)
+                .await
+                .map_err(|e| {
+                    Status::internal(format!("Error reading GitLab default branch: {:?}", e))
+                })?
+                .map(|ref name| gitlab_branch_ref(name))
+                .unwrap_or_else(Vec::new),
+        }))
+    }
+
     #[instrument(name="ref_exists",
                  skip(self, request),
                  fields(request=field::debug(RefExistsTracingRequest(&request))))]
diff --git a/rust/rhgitaly/src/streaming.rs b/rust/rhgitaly/src/streaming.rs
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9zcmMvc3RyZWFtaW5nLnJz..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9zcmMvc3RyZWFtaW5nLnJz 100644
--- a/rust/rhgitaly/src/streaming.rs
+++ b/rust/rhgitaly/src/streaming.rs
@@ -4,6 +4,7 @@
 // GNU General Public License version 2 or any later version.
 // SPDX-License-Identifier: GPL-2.0-or-later
 use std::env;
+use std::fmt::Debug;
 use std::str::FromStr;
 
 use lazy_static::lazy_static;
@@ -7,8 +8,8 @@
 use std::str::FromStr;
 
 use lazy_static::lazy_static;
-use tokio::sync::mpsc::Sender;
+use tokio::sync::mpsc::{self, Sender};
 use tokio_stream::wrappers::ReceiverStream;
 use tonic::{Response, Status};
 use tracing::error;
 
@@ -11,7 +12,8 @@
 use tokio_stream::wrappers::ReceiverStream;
 use tonic::{Response, Status};
 use tracing::error;
 
+use crate::gitaly::{PaginationCursor, PaginationParameter};
 const DEFAULT_WRITE_BUFFER_SIZE: usize = 131072;
 
 lazy_static! {
@@ -26,6 +28,12 @@
 pub type ResultResponseStream<Resp> =
     Result<Response<ReceiverStream<Result<Resp, Status>>>, Status>;
 
+/// Return a valid, yet empty response stream
+pub fn empty_response_stream<Resp>() -> ResultResponseStream<Resp> {
+    let (_tx, rx) = mpsc::channel(1);
+    Ok(Response::new(ReceiverStream::new(rx)))
+}
+
 /// A specialization of [`Sender`] allowing only blocking sends and logging sending errors
 ///
 /// Server streaming methods involving a repository typically spawn a new thread with
@@ -59,3 +67,241 @@
         });
     }
 }
+
+/// Wrap an inner iterator over Results to aggregate into chunks.
+///
+/// As soon as an error is encountered while iterating for a chunk, it is yielded in place
+/// of the chunk.
+///
+/// As of Rust 1.65, the stdlib  the [`Iterator`] trait has a `next_chunk` method but
+/// - it is unstable
+/// - it yields arrays
+/// - the remainder still needs to be collected, making it not so practical
+/// - it does not have the error handling, of course
+struct ChunkedIterator<T, IT, E>
+where
+    IT: Iterator<Item = Result<T, E>>,
+{
+    inner: IT,
+    chunk_size: usize,
+}
+
+impl<T, IT, E> Iterator for ChunkedIterator<T, IT, E>
+where
+    IT: Iterator<Item = Result<T, E>>,
+{
+    type Item = Result<Vec<T>, E>;
+
+    fn next(&mut self) -> Option<Result<Vec<T>, E>> {
+        let mut chunk: Vec<T> = Vec::new();
+        while chunk.len() < self.chunk_size {
+            match self.inner.next() {
+                None => {
+                    if chunk.is_empty() {
+                        return None;
+                    } else {
+                        break;
+                    }
+                }
+                Some(Ok(v)) => chunk.push(v),
+                Some(Err(e)) => {
+                    return Some(Err(e));
+                }
+            }
+        }
+        Some(Ok(chunk))
+    }
+}
+
+pub const DEFAULT_CHUNK_SIZE: usize = 50;
+
+/// Stream responses by aggregating an inner iterator into chunks
+///
+/// This is useful when streamed responses have a repeated field: the inner iterator
+/// would yield individual elements, and this generic function can be used to
+/// collect them in chunks, from which responses are built and sent to the channel.
+///
+/// The resulting chunks are guaranteed not to be empty.
+///
+/// In `resp_builder`, the boolean tells whether the response to build is the first one.
+/// It is indeed frequent in the Gitaly protocol for streamed responses with a repeated field
+/// and additional metadata to put the metadata on the first response only, the subsequent
+/// ones being only about chunking the repeated field.
+///
+/// This parallels Python `hgitaly.util.chunked` except that the chunk size is for now
+/// the constant [`DEFAULT_CHUNK_SIZE`].
+/// Note that this flexibility was introduced early in the Python implementation, but has
+/// not been actually used yet.
+/// Gitaly seems to apply a more subtle logic based on actual response size
+/// in some cases. It looks like a better avenue for improvement to mimick that rather than
+/// making the chunk size customizable.
+pub fn stream_chunks<Iter, Resp, Item, E>(
+    tx: &BlockingResponseSender<Resp>,
+    iter: Iter,
+    resp_builder: impl FnOnce(Vec<Item>, bool) -> Resp + Copy,
+    err_handler: impl FnOnce(E) -> Status + Copy,
+) where
+    Iter: Iterator<Item = Result<Item, E>>,
+{
+    let mut first = true;
+    for res in (ChunkedIterator {
+        inner: iter,
+        chunk_size: DEFAULT_CHUNK_SIZE,
+    }) {
+        tx.send(res.map_or_else(
+            |e| Err(err_handler(e)),
+            |chunk| Ok(resp_builder(chunk, first)),
+        ));
+        if first {
+            first = false;
+        }
+    }
+}
+
+/// This trait is for elements of responses which rely on the Pagination protocol
+///
+/// Typically, the response will include a repeated field, and the traait is to be
+/// implemented on inividual items of that field.
+///
+/// For an example, see [`crate::gitaly::GetTreeEntriesResponse`]
+pub trait PaginableMessageItem: Debug {
+    /// Provide the `next_cursor` field of `PaginationCursor`
+    ///
+    /// This is the value to tell clients that the next page of results would
+    /// start after the current item, which is typically the last one returned
+    /// by the current call
+    fn next_cursor(&self) -> String;
+
+    /// Tell whether this item is the one bearing the givn token.
+    ///
+    /// This means typically that the wanted results start right afterwards.
+    fn match_token(&self, token: &str) -> bool;
+}
+
+/// Stream responses by grouping them in chunks and abiding to pagination parameters.
+///
+/// This is meant to take into account incoming [`PaginationParameter`] and provide in turn
+/// the appropriate responses so that the client can request the next page. These together
+/// represent an effort in the Gitaly protocol to standardize  a client-driven second level of
+/// batching on top of the server-driven streaming responses.
+///
+/// This function takes severak closure arguments:
+///
+/// - `resp_builder` is a closure responsible to build a response from collected items and a
+/// [`PaginationCursor`] message.
+/// - `err_handler` is the generic transformation from the errors that `iterator` may yield.
+/// - `token_not_found` provides the error [`Status`] in case no item matches
+///    `pagination.page_token`. It is expected to differ among gRPC methods and possibly need
+///    some contextual information.
+pub fn stream_with_pagination<Iter, Resp, Item, E>(
+    tx: &BlockingResponseSender<Resp>,
+    pagination: &Option<PaginationParameter>,
+    mut iterator: Iter,
+    resp_builder: impl FnOnce(Vec<Item>, Option<PaginationCursor>) -> Resp + Copy,
+    err_handler: impl FnOnce(E) -> Status + Copy,
+    token_not_found: impl FnOnce(&str) -> Status + Copy,
+) where
+    Iter: Iterator<Item = Result<Item, E>>,
+    Item: PaginableMessageItem,
+{
+    match pagination {
+        None => {
+            // no pagination param: in particular, no limit
+            stream_chunks_with_cursor(tx, iterator, "", resp_builder, err_handler);
+        }
+        Some(pagination) => {
+            let token = &pagination.page_token;
+            if !token.is_empty() {
+                let mut found = false;
+                for res in iterator.by_ref() {
+                    match res {
+                        Ok(item) => {
+                            if item.match_token(token) {
+                                found = true;
+                                break;
+                            }
+                        }
+                        Err(e) => {
+                            tx.send(Err(err_handler(e)));
+                            return;
+                        }
+                    }
+                }
+                if !found {
+                    tx.send(Err(token_not_found(token)));
+                    return;
+                }
+            }
+
+            if pagination.limit < 0 {
+                return stream_chunks_with_cursor(tx, iterator, "", resp_builder, err_handler);
+            }
+
+            // No other choice than collecting to truncate and derive `next_cursor`
+            let limit = pagination.limit as usize;
+            let mut limited: Vec<Result<Item, E>> = Vec::new();
+            for (i, item) in iterator.enumerate() {
+                if i >= limit {
+                    break;
+                }
+                limited.push(item);
+            }
+            match limited.last() {
+                None => {}
+                // In case it is an error, we need to take ownership, since `err_handler` does
+                // not work on references. We cannot clone because some of the errors we have to
+                // deal with do not implement `Clone` (e.g., [`HgError`] does not)
+                Some(Err(_e)) => tx.send(Err(err_handler(
+                    limited
+                        .pop()
+                        .expect("Last element already known to exist and to be an error")
+                        .unwrap_err(),
+                ))),
+                Some(Ok(ref item)) => {
+                    // We need to take a ref for next_cursor, so that the closure used in the
+                    // underlying `stream_chunks()` is `Copy`, with leads us to clone in actual
+                    // `PaginationCursor` instantiation (this is bearable).
+                    // We also need to take the ref right now, to make the borrow checker accept its
+                    // use in the call below.
+                    let next_cursor = &item.next_cursor();
+                    stream_chunks_with_cursor(
+                        tx,
+                        limited.into_iter(),
+                        next_cursor,
+                        resp_builder,
+                        err_handler,
+                    );
+                }
+            }
+        }
+    }
+}
+
+/// Shortcut to reduce code dupliaction in `stream_chunks_with_pagination`
+fn stream_chunks_with_cursor<Iter, Resp, Item, E>(
+    tx: &BlockingResponseSender<Resp>,
+    iter: Iter,
+    next_cursor: &str,
+    resp_builder: impl FnOnce(Vec<Item>, Option<PaginationCursor>) -> Resp + Copy,
+    err_handler: impl FnOnce(E) -> Status + Copy,
+) where
+    Iter: Iterator<Item = Result<Item, E>>,
+{
+    stream_chunks(
+        tx,
+        iter,
+        |chunk, first| {
+            resp_builder(
+                chunk,
+                if first {
+                    Some(PaginationCursor {
+                        next_cursor: next_cursor.to_string(),
+                    })
+                } else {
+                    None
+                },
+            )
+        },
+        err_handler,
+    );
+}
diff --git a/rust/rhgitaly/src/util.rs b/rust/rhgitaly/src/util.rs
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_cnVzdC9yaGdpdGFseS9zcmMvdXRpbC5ycw==..0944dbc03457ecdaf8e5085639a35a25a2f16e62_cnVzdC9yaGdpdGFseS9zcmMvdXRpbC5ycw== 100644
--- a/rust/rhgitaly/src/util.rs
+++ b/rust/rhgitaly/src/util.rs
@@ -21,3 +21,90 @@
         }
     }
 }
+
+/// Return the common subpath as a subslice of `first` and the index after which the remainder
+/// starts in `second`.
+pub fn common_subpath_split<'a, 'b>(first: &'a [u8], second: &'b [u8]) -> (&'a [u8], usize) {
+    // position of the latest slash in `first` after the common subpath, if any.
+    let mut latest_slash: Option<usize> = None;
+
+    let mut fit = first.iter();
+    let mut sit = second.iter();
+    let mut i: usize = 0;
+
+    loop {
+        match fit.next() {
+            Some(fc) => match sit.next() {
+                None => {
+                    if *fc == b'/' {
+                        latest_slash = Some(i);
+                    } else {
+                        break;
+                    }
+                }
+                Some(sc) => {
+                    if *fc != *sc {
+                        break;
+                    }
+                    if *fc == b'/' {
+                        latest_slash = Some(i);
+                    }
+                    i += 1;
+                }
+            },
+            None => match sit.next() {
+                None => {
+                    return (first, first.len());
+                }
+                Some(sc) => {
+                    if *sc == b'/' {
+                        return (first, first.len() + 1);
+                    } else {
+                        break;
+                    }
+                }
+            },
+        }
+    }
+
+    match latest_slash {
+        None => (&[], 0),
+        Some(ls) => (&first[..ls], ls + 1),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+
+    #[test]
+    fn test_common_subpath_split() {
+        assert_eq!(common_subpath_split(b"foo/a", b"bar"), (b"".as_ref(), 0));
+        assert_eq!(common_subpath_split(b"bar", b"foo/a"), (b"".as_ref(), 0));
+        assert_eq!(
+            common_subpath_split(b"foo/a", b"foo/b"),
+            (b"foo".as_ref(), 4)
+        );
+        assert_eq!(common_subpath_split(b"foo/a", b"foox/a"), (b"".as_ref(), 0));
+        assert_eq!(common_subpath_split(b"foox/a", b"foo/a"), (b"".as_ref(), 0));
+        assert_eq!(
+            common_subpath_split(b"foo/a/", b"foo/a/b"),
+            (b"foo/a".as_ref(), 6)
+        );
+        assert_eq!(
+            common_subpath_split(b"foo/a", b"foo/a/b"),
+            (b"foo/a".as_ref(), 6)
+        );
+        assert_eq!(
+            common_subpath_split(b"foo/a/b", b"foo/a"),
+            (b"foo/a".as_ref(), 6)
+        );
+        assert_eq!(
+            common_subpath_split(b"foo/a/b/c", b"foo/a/d/e"),
+            (b"foo/a".as_ref(), 6)
+        );
+        assert_eq!(common_subpath_split(b"", b"foo/a"), (b"".as_ref(), 0));
+        assert_eq!(common_subpath_split(b"foo/a", b""), (b"".as_ref(), 0));
+    }
+}
diff --git a/setup.py b/setup.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_c2V0dXAucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_c2V0dXAucHk= 100644
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,9 @@
     keywords='hg mercurial heptapod gitlab',
     license='GPLv2+',
     packages=find_packages(),
-    package_data=dict(hgitaly=[VERSION_FILE]),
+    package_data={'hgitaly': [VERSION_FILE],
+                  'hgitaly.linguist': ['languages.json'],
+                  },
     install_requires=install_req,
     python_requires='>=3.8',
 )
diff --git a/tests_with_gitaly/test_blob_tree.py b/tests_with_gitaly/test_blob_tree.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9ibG9iX3RyZWUucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9ibG9iX3RyZWUucHk= 100644
--- a/tests_with_gitaly/test_blob_tree.py
+++ b/tests_with_gitaly/test_blob_tree.py
@@ -177,9 +177,6 @@
     # reusing content to test GetTreeEntries, hence with a new rpc_helper
     #
 
-    if hg_server == 'rhgitaly':  # TODO split with a module fixture
-        return
-
     def gte_normalizer(rpc_helper, responses, vcs='hg'):
         for resp in responses:
             for entry in resp.entries:
@@ -188,6 +185,7 @@
                 entry.root_oid = b''
 
     rpc_helper = fixture.rpc_helper(stub_cls=CommitServiceStub,
+                                    hg_server=hg_server,
                                     method_name='GetTreeEntries',
                                     request_cls=GetTreeEntriesRequest,
                                     request_defaults=dict(
@@ -207,6 +205,9 @@
 
     assert_compare(path=b'.', revision=b'unknown')
 
+    # case when path is actually a file
+    assert_compare(path=b'sub/bar')
+
     # sort parameter
     SortBy = GetTreeEntriesRequest.SortBy
     for recursive in (False, True):
@@ -237,8 +238,10 @@
                        sort=SortBy.TREES_FIRST)
 
 
-def test_compare_get_tree_entries_pagination(gitaly_comparison):
-    fixture = gitaly_comparison
+@parametrize('hg_server', ('hgitaly', 'rhgitaly'))
+def test_compare_get_tree_entries_pagination(gitaly_rhgitaly_comparison,
+                                             hg_server):
+    fixture = gitaly_rhgitaly_comparison
 
     wrapper = fixture.hg_repo_wrapper
     wrapper.write_commit('foo', message="Some foo")
@@ -271,6 +274,7 @@
     cursor2git = {}  # hg cursor -> git cursor
 
     rpc_helper = fixture.rpc_helper(stub_cls=CommitServiceStub,
+                                    hg_server=hg_server,
                                     method_name='GetTreeEntries',
                                     request_cls=GetTreeEntriesRequest,
                                     request_defaults=dict(
@@ -340,6 +344,10 @@
                    pagination_params=PaginationParameter(limit=0),
                    )
 
+    # case of unknown revision and limit=0
+    assert_compare(path=b'.', revision=b'unknown',
+                   pagination_params=PaginationParameter(limit=0))
+
     # case of no pagination params
     assert_compare_aggregated(path=b'sub',
                               recursive=True,
diff --git a/tests_with_gitaly/test_commit.py b/tests_with_gitaly/test_commit.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9jb21taXQucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9jb21taXQucHk= 100644
--- a/tests_with_gitaly/test_commit.py
+++ b/tests_with_gitaly/test_commit.py
@@ -14,5 +14,6 @@
     Repository,
 )
 from hgitaly.stub.commit_pb2 import (
+    CommitLanguagesRequest,
     FindCommitRequest,
     FindCommitsRequest,
@@ -17,5 +18,6 @@
     FindCommitRequest,
     FindCommitsRequest,
+    LastCommitForPathRequest,
     ListCommitsRequest,
     ListCommitsByOidRequest,
     ListFilesRequest,
@@ -35,7 +37,7 @@
 parametrize = pytest.mark.parametrize
 
 
-def test_compare_list_last_commits_for_tree(gitaly_comparison):
+def test_compare_last_commit_for(gitaly_comparison):
     fixture = gitaly_comparison
     git_repo = fixture.git_repo
 
@@ -102,7 +104,34 @@
 
     # error won't be due to invalidity as a SHA, but because commit doesn't
     # exist (let's not depend on Gitaly accepting symbolic revisions, here)
-    assert_compare_errors(revision=b'be0123ef' * 5, path=b'')
+    assert_compare_errors(revision=b'be0123ef' * 5, path=b'sub')
+
+    def lcfp_norm(rpc_helper, response, **kw):
+        normalize_commit_message(response.commit)
+
+    rpc_helper = fixture.rpc_helper(stub_cls=CommitServiceStub,
+                                    method_name='LastCommitForPath',
+                                    request_cls=LastCommitForPathRequest,
+                                    request_sha_attrs=['revision'],
+                                    response_sha_attrs=[
+                                        'commit.id',
+                                        'commit.parent_ids[]',
+                                        ],
+                                    normalizer=lcfp_norm,
+                                    )
+
+    assert_compare = rpc_helper.assert_compare
+    assert_compare_errors = rpc_helper.assert_compare_errors
+
+    for path in (b'sub/dir', b'sub/dir/', b'', b'.', b'/', b'./',
+                 b'sub', b'sub/', b'foo'):
+        for rev in (b'branch/default', b'branch/other',
+                    ctx2.hex(), ctx3.hex()):
+            assert_compare(revision=rev, path=path)
+
+    # case when revision does not exist
+    for path in (b'', b'foo'):
+        assert_compare(revision=b'be0123ef' * 5, path=path)
 
 
 def test_compare_raw_blame(gitaly_comparison):
@@ -289,7 +318,8 @@
     assert_compare_errors(repository=None,
                           revision=b'HEAD',
                           same_details=False)
-    assert_compare_errors(repository=None)  # and no revision
+    assert_compare_errors(repository=None,  # and no revision
+                          same_details=False)
     fixture.gitaly_repo.relative_path = 'unknown/repo'
     assert_compare_errors(revision=b'HEAD', same_details=False)
 
@@ -425,6 +455,42 @@
         assert_compare(before=date)
         assert_compare(before=date, after=date)
 
+    # with `include_referenced_by` option
+    mr_ref_path, _ = fixture.write_special_ref(b'merge-requests/2/train',
+                                               ctx2.hex())
+    wrapper.command('tag', b'release', rev=ctx3.hex())
+    all_revs.append(wrapper.repo[b'.'].hex())  # will be HEAD
+    wrapper.command('gitlab-mirror')
+    fixture.invalidate()  # will need to normalize the tagging changeset itself
+
+    for patterns in ([b'refs/heads'],
+                     [b'refs/heads/'],
+                     [b'refs/heads/branch/'],
+                     [b'refs/tags'],
+                     [b'refs/tags/'],
+                     [b'refs/tags/rel'],
+                     [b'refs/merge-requests'],
+                     [b'refs/heads', b'refs/tags'],  # matching is logical OR
+                     [b'HEAD'],
+                     ):
+        for rev in all_revs:
+            rpc_helper.assert_compare(revision=rev,
+                                      include_referenced_by=patterns)
+
+    # This looks to be simple matching by segment prefixes,
+    # but git-log(1) isn't very explicit about the type of matching it is
+    # performing (Gitaly uses `git log --decorate-ref` for this as of v15.9):
+    #        --decorate-refs=<pattern>, --decorate-refs-exclude=<pattern>
+    #            For each candidate reference, do not use it for decoration if
+    #            it matches any patterns given to --decorate-refs-exclude or if
+    #            it doesn’t match any of the patterns given to --decorate-refs.
+    for patterns in ([b'refs/h*ds/'],  # glob
+                     [b'refs/tags/rel'],  # general prefixing
+                     [b'refs/h..ds/'],  # regexp
+                     ):
+        rpc_helper.assert_compare(revision=b'branch/default',
+                                  include_referenced_by=patterns)
+
 
 def test_compare_list_commits(gitaly_comparison):
     fixture = gitaly_comparison
@@ -642,3 +708,47 @@
     # NULL_NODE gets ignored
     rpc_helper.assert_compare(oid=[NULL_HEX])
     rpc_helper.assert_compare(oid=[NULL_HEX, hexes[0]])
+
+
+def test_compare_commit_languages(gitaly_comparison):
+    fixture = gitaly_comparison
+    wrapper = fixture.hg_repo_wrapper
+
+    rpc_helper = fixture.rpc_helper(
+        stub_cls=CommitServiceStub,
+        method_name='CommitLanguages',
+        request_cls=CommitLanguagesRequest,
+        request_sha_attrs=['oid'],
+    )
+    assert_compare = rpc_helper.assert_compare
+
+    # a Python file with valid content, including a blank line
+    wrapper.commit_file('foo.py',
+                        content="import sys\n\nprint(sys.version)\n",
+                        message="Some Python",
+                        )
+    # a Ruby file with valid content, with a comment
+    # despite Ruby being after Python in lexicographical order,
+    # it should come first, being the one with the most bytes.
+    wrapper.commit_file('bar.rb',
+                        content=("# frozen_string_literal: true\n"
+                                 "\n"
+                                 "module Bar\n"
+                                 "def f(x)\n"
+                                 "  x+1\n"
+                                 ),
+                        message="Some Ruby",
+                        )
+    assert_compare()
+
+    # GitHub Linguist restricts itself to some language types, namely, from
+    # blob_helper.rb:
+    #
+    #     DETECTABLE_TYPES = [:programming, :markup].freeze
+    #
+    # it turns out that COBOL is a programming language without any associated
+    # color in the `languages.json` file.
+    wrapper.commit_file('truc.cob')
+    assert_compare()
+
+    # error cases are hard to reproduce permanently in Gitaly
diff --git a/tests_with_gitaly/test_diff.py b/tests_with_gitaly/test_diff.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9kaWZmLnB5..0944dbc03457ecdaf8e5085639a35a25a2f16e62_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9kaWZmLnB5 100644
--- a/tests_with_gitaly/test_diff.py
+++ b/tests_with_gitaly/test_diff.py
@@ -119,6 +119,6 @@
     sha_not_exists = b'deadnode' * 5
     with pytest.raises(grpc.RpcError) as exc_info:
         do_rpc('hg', sha_not_exists, ctx2.hex())
-    assert exc_info.value.code() == grpc.StatusCode.UNKNOWN
+    assert exc_info.value.code() == grpc.StatusCode.INTERNAL
     with pytest.raises(grpc.RpcError) as exc_info:
         do_rpc('git', sha_not_exists, ctx2.hex())
@@ -123,6 +123,6 @@
     with pytest.raises(grpc.RpcError) as exc_info:
         do_rpc('git', sha_not_exists, ctx2.hex())
-    assert exc_info.value.code() == grpc.StatusCode.UNKNOWN
+    assert exc_info.value.code() == grpc.StatusCode.INTERNAL
 
     # case 5: EMPTY_TREE_OID to represent null commit on the left
     hg_resp_lines = do_rpc('hg', EMPTY_TREE_OID, ctx0.hex()).split(b'\n')
diff --git a/tests_with_gitaly/test_ref.py b/tests_with_gitaly/test_ref.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9yZWYucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9yZWYucHk= 100644
--- a/tests_with_gitaly/test_ref.py
+++ b/tests_with_gitaly/test_ref.py
@@ -15,6 +15,7 @@
 )
 from hgitaly.stub.ref_pb2 import (
     FindBranchRequest,
+    FindDefaultBranchNameRequest,
     FindTagRequest,
     FindLocalBranchesRequest,
     ListRefsRequest,
@@ -62,6 +63,34 @@
 
 
 @parametrize('hg_server', ('hgitaly', 'rhgitaly'))
+def test_compare_find_default_branch_name(gitaly_rhgitaly_comparison,
+                                          hg_server):
+    fixture = gitaly_rhgitaly_comparison
+    git_repo = fixture.git_repo
+
+    rpc_helper = fixture.rpc_helper(
+        hg_server=hg_server,
+        stub_cls=RefServiceStub,
+        method_name='FindDefaultBranchName',
+        request_cls=FindDefaultBranchNameRequest,
+    )
+
+    # empty repo
+    rpc_helper.assert_compare()
+
+    fixture.hg_repo_wrapper.write_commit('foo', message="Some foo")
+    gl_branch = b'branch/default'
+
+    # mirror worked
+    assert git_repo.branch_titles() == {gl_branch: b"Some foo"}
+
+    rpc_helper.assert_compare()
+    # invalid case
+    rpc_helper.assert_compare_errors(repository=None,
+                                     same_details=False)
+
+
+@parametrize('hg_server', ('hgitaly', 'rhgitaly'))
 def test_compare_ref_exists(gitaly_rhgitaly_comparison, hg_server):
     fixture = gitaly_rhgitaly_comparison
     wrapper = fixture.hg_repo_wrapper
@@ -112,10 +141,7 @@
     assert_compare_errors(ref=b'notrefs/something')
 
 
-@parametrize('branch_style', ['new-style', 'old-style'])
-def test_compare_find_local_branches(gitaly_comparison, branch_style):
-    new_style = branch_style == 'new-style'
-
+def test_compare_find_local_branches(gitaly_comparison):
     fixture = gitaly_comparison
     wrapper = fixture.hg_repo_wrapper
 
@@ -129,12 +155,5 @@
     assert set(fixture.git_repo.branch_titles().keys()) == {
         b'branch/br%02d' % i for i in range(3)}
 
-    if new_style:
-        resp_branches = 'local_branches'
-        resp_target = 'target_commit'
-    else:
-        resp_branches = 'branches'
-        resp_target = 'commit'
-
     def normalize_response(rpc_helper, resp, **kw):
         for chunk in resp:
@@ -139,5 +158,5 @@
     def normalize_response(rpc_helper, resp, **kw):
         for chunk in resp:
-            for branch in getattr(chunk, resp_branches):
-                normalize_commit_message(getattr(branch, resp_target))
+            for branch in chunk.local_branches:
+                normalize_commit_message(branch.target_commit)
 
@@ -143,15 +162,6 @@
 
-    if new_style:
-        resp_sha_attrs = ['local_branches[].target_commit.id',
-                          'local_branches[].target_commit.parent_ids[]',
-                          ]
-    else:
-        resp_sha_attrs = ['branches[].commit_id',
-                          'branches[].commit.id',
-                          'branches[].commit.parent_ids[]',
-                          ]
     rpc_helper = fixture.rpc_helper(
         stub_cls=RefServiceStub,
         method_name='FindLocalBranches',
         request_cls=FindLocalBranchesRequest,
         streaming=True,
@@ -153,8 +163,10 @@
     rpc_helper = fixture.rpc_helper(
         stub_cls=RefServiceStub,
         method_name='FindLocalBranches',
         request_cls=FindLocalBranchesRequest,
         streaming=True,
-        response_sha_attrs=resp_sha_attrs,
+        response_sha_attrs=['local_branches[].target_commit.id',
+                            'local_branches[].target_commit.parent_ids[]',
+                            ],
         normalizer=normalize_response,
     )
@@ -159,8 +171,5 @@
         normalizer=normalize_response,
     )
-    if new_style:
-        rpc_helper.feature_flags.append(
-            ('simplify-find-local-branches-response', new_style))
 
     def assert_compare(limit=0, page_token='', pagination=True, **kw):
         if pagination:
@@ -283,7 +292,7 @@
         for resp in resps:
             all_pseudo_ref_idx = None
             for i, ref_msg in enumerate(resp.references):
-                if ref_msg.name == b'ALL':
+                if ref_msg.name == b'ALL' and rpc_helper.norm_all_pseudo_ref:
                     all_pseudo_ref_idx = i
                 if ref_msg.name.startswith(prefix):
                     ref_msg.name = prefix + rpc_helper.hg2git(
@@ -297,6 +306,7 @@
         request_cls=ListRefsRequest,
         request_defaults=dict(patterns=[b"refs/"], head=True),
         streaming=True,
+        request_sha_attrs=['pointing_at_oids'],
         response_sha_attrs=['references[].target'],
         normalizer=normalize_refs,
     )
@@ -300,6 +310,7 @@
         response_sha_attrs=['references[].target'],
         normalizer=normalize_refs,
     )
+    rpc_helper.norm_all_pseudo_ref = True
     # empty repo, in particular no default GitLab branch (HEAD)
     rpc_helper.assert_compare()
 
@@ -310,4 +321,5 @@
     hg_sha = hg_wrapper.commit_file('bar').hex()
 
     # branches
+    hg_shas = []
     for i in range(3):
@@ -313,4 +325,6 @@
     for i in range(3):
-        hg_wrapper.commit_file('foo', branch='br%02d' % i, return_ctx=False,
-                               utc_timestamp=now - commit_ages[i])
+        hg_shas.append(
+            hg_wrapper.commit_file('foo', branch='br%02d' % i,
+                                   utc_timestamp=now - commit_ages[i]).hex()
+        )
     rpc_helper.assert_compare()
@@ -316,7 +330,9 @@
     rpc_helper.assert_compare()
+    for head in False, True:
+        rpc_helper.assert_compare(patterns=[b'refs/heads/'], head=head)
 
     # with tags
     hg_wrapper.command('tag', b'v3.1', rev=b'br01')
     hg_wrapper.command('gitlab-mirror')
     fixture.invalidate()
     rpc_helper.assert_compare()
@@ -317,10 +333,13 @@
 
     # with tags
     hg_wrapper.command('tag', b'v3.1', rev=b'br01')
     hg_wrapper.command('gitlab-mirror')
     fixture.invalidate()
     rpc_helper.assert_compare()
+    for head in False, True:
+        rpc_helper.assert_compare(patterns=[b'refs/heads/'], head=head)
+        rpc_helper.assert_compare(patterns=[b'refs/tags/'], head=head)
 
     # now with special refs
     fixture.write_special_ref(b'pipeline/13', hg_sha)
     rpc_helper.assert_compare()
@@ -323,8 +342,11 @@
 
     # now with special refs
     fixture.write_special_ref(b'pipeline/13', hg_sha)
     rpc_helper.assert_compare()
+    for head in False, True:
+        rpc_helper.assert_compare(patterns=[b'refs/heads/'], head=head)
+        rpc_helper.assert_compare(patterns=[b'refs/tags/'], head=head)
 
     # with a keep around
     fixture.create_keep_around(hg_sha)
     rpc_helper.assert_compare()
@@ -327,6 +349,25 @@
 
     # with a keep around
     fixture.create_keep_around(hg_sha)
     rpc_helper.assert_compare()
+    for head in False, True:
+        rpc_helper.assert_compare(patterns=[b'refs/heads/'], head=head)
+        rpc_helper.assert_compare(patterns=[b'refs/tags/'], head=head)
+
+    # more general patterns
+    rpc_helper.norm_all_pseudo_ref = False
+    for head in False, True:
+        # notice how HEAD is ignored by pattern matching
+        rpc_helper.assert_compare(patterns=[b'refs/heads/branch/br*'],
+                                  head=head)
+
+        # 'branch/something' should not match `br*` and special case of
+        # HEAD not being part of response if nothing matches
+        rpc_helper.assert_compare(patterns=[b'refs/heads/br*'], head=head)
+
+    # logical conjunction of several patterns
+    rpc_helper.assert_compare(patterns=[b'refs/heads/branch/br*',
+                                        b'refs/heads/branch/d*'],
+                              head=head)
 
     # no error that we can think of would be specific of this gRPC method
@@ -331,2 +372,14 @@
 
     # no error that we can think of would be specific of this gRPC method
+
+    # pointing_at_oids
+    for head in False, True:
+        rpc_helper.assert_compare(pointing_at_oids=[hg_shas[0]], head=head)
+        rpc_helper.assert_compare(pointing_at_oids=[hg_shas[0], hg_shas[1]],
+                                  head=head)
+        # together with patterns
+        for i in range(3):
+            # empty result if `i == 0 and not head`
+            rpc_helper.assert_compare(pointing_at_oids=[hg_shas[i]],
+                                      patterns=[b'refs/tags/'],
+                                      head=head)
diff --git a/tests_with_gitaly/test_repository_service.py b/tests_with_gitaly/test_repository_service.py
index aadc12c12dc78f21d4b3ddb9e4f674b5f252ea0c_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9yZXBvc2l0b3J5X3NlcnZpY2UucHk=..0944dbc03457ecdaf8e5085639a35a25a2f16e62_dGVzdHNfd2l0aF9naXRhbHkvdGVzdF9yZXBvc2l0b3J5X3NlcnZpY2UucHk= 100644
--- a/tests_with_gitaly/test_repository_service.py
+++ b/tests_with_gitaly/test_repository_service.py
@@ -21,4 +21,5 @@
     CreateRepositoryFromBundleRequest,
     FindMergeBaseRequest,
     FullPathRequest,
+    RemoveAllRequest,
     RemoveRepositoryRequest,
@@ -24,4 +25,5 @@
     RemoveRepositoryRequest,
+    SearchFilesByContentRequest,
     SearchFilesByNameRequest,
     SetFullPathRequest,
 )
@@ -623,3 +625,81 @@
 
     # unknown ref gives back an empty list of files, not an error
     assert_compare(ref=b'unknown', query='.')
+
+
+def test_search_files_by_content(gitaly_comparison):
+    fixture = gitaly_comparison
+
+    wrapper = fixture.hg_repo_wrapper
+    wrapper.write_commit('afoo', message="Some foo")
+    sub = (wrapper.path / 'sub')
+    sub.mkdir()
+    (sub / 'bar').write_text('line1\nline2\nbar content\nline4\nline5')
+    (sub / 'ba2').write_text('line1\nba2 content')
+    (sub / 'ba3').write_text('ba3 content\nline2')
+    # This one has a Windows endings, and exhibits that `git grep` normalizes
+    # to `\n`. Also Git does not interpret the MacOS classic line ending
+    # '\r' and we do. In that case, we can claim our response to be more
+    # correct and we will not compare it.
+    (sub / 'ba4').write_text('l1\r\nl2\nl3\nba4 content\nline6')
+    (sub / 'ba5').write_text('m1\nm2\nm3\nm4\nm5\nm6\nba5 content\n')
+    (wrapper.path / 'large').write_text('very large content\n' * 3000)
+    # TODO OS indep for paths (actually TODO make wrapper.commit easier to
+    # use, e.g., check how to make it accept patterns)
+    wrapper.commit(rel_paths=['sub/bar', 'sub/ba2', 'sub/ba3',
+                              'sub/ba4', 'sub/ba5', 'large'],
+                   message="zebar", add_remove=True)
+
+    default_rev = b'branch/default'
+
+    rpc_helper = fixture.rpc_helper(
+        stub_cls=RepositoryServiceStub,
+        method_name='SearchFilesByContent',
+        request_cls=SearchFilesByContentRequest,
+        request_defaults=dict(
+            ref=default_rev,
+        ),
+        streaming=True,
+        error_details_normalizer=lambda s, vcs: s.lower(),
+    )
+    assert_compare = rpc_helper.assert_compare
+    assert_compare_errors = rpc_helper.assert_compare_errors
+
+    # precondition for the test: mirror worked
+    assert fixture.git_repo.branch_titles() == {
+        default_rev: b"zebar",
+    }
+    assert_compare(query='no match for this one')
+    assert_compare(query='^bar.c')
+    assert_compare(query='^ba2.c')   # only one line before match
+    assert_compare(query='^ba3.c')   # only one line after match
+    assert_compare(query='^ba4.c')   # more than two lines before match
+    assert_compare(query='^very')
+    assert_compare(query='^l1|ba4')  # two matches with overlapping context
+    assert_compare(query='^m1|ba5')  # two matches with non-overlapping context
+    assert_compare(query='ConTent')  # several files and case insensity
+
+    # errors if query is missing
+    assert_compare_errors()
+    assert_compare_errors(ref=b'topic/default/does-not-exist')
+    # unresolvable ref
+    assert_compare(ref=b'topic/default/does-not-exist', query='foo')
+    # missing repo, ref
+    assert_compare_errors(ref=b'')
+    assert_compare_errors(repository=None)
+    fixture.gitaly_repo.relative_path = 'no/such/repo'
+    assert_compare_errors(query='foo', same_details=False)
+
+
+def test_remove_all(gitaly_comparison, server_repos_root):
+    fixture = gitaly_comparison
+    rpc_helper = fixture.rpc_helper(
+        stub_cls=RepositoryServiceStub,
+        method_name='RemoveAll',
+        request_cls=RemoveAllRequest,
+        repository_arg=False,
+    )
+    assert_compare_errors = rpc_helper.assert_compare_errors
+
+    # unknown storage
+    assert_compare_errors(storage_name='unknown')