Commit 09dae559 authored by Georges Racinet's avatar Georges Racinet 🦑
Browse files

heptapod#519: Heptapod 0.23 is the new stable

--HG--
branch : stable
Pipeline #25489 passed with stage
in 1 minute and 3 seconds
......@@ -16,3 +16,9 @@ venv/
.coverage.*
htmlcov/
.mypy_cache/
# Ruby generated lib
# versions don't have to be pinned, because the gem
# dependencies are only to generate the stubs.
ruby/Gemfile.lock
ruby/.bundle
......@@ -18,3 +18,6 @@ df53b71c17a4bbd1a85ab72e9752fc0b70ea77ab 0.12.0
9058efa8be3929d711cd80806f22785d92ac345a 0.12.1
fd405714088c2746874f119e16e899ee3f1f30e2 0.12.2
ba3cda48dd083b47767fe4d22990ee10f750128e 0.13.0
06846432db85694d68d1e3a9a8146a4792de3212 0.14.0
fd9fadf22229290942e37ae8832e56bf9b321e68 0.14.1
10bcb5a1f54743079361810914dfd96021062e93 0.14.2
......@@ -189,7 +189,6 @@ The virtualenv has to be activated
Example in a HDK context:
```
rm protos/*.proto
cp ../gitaly/proto/*.proto protos/ # we dont want the `go` subdir
```
......@@ -199,6 +198,50 @@ The virtualenv has to be activated
5. perform necessary `hg add` after close inspection of `hg status`
## Updating the HGitaly specific gRPC protocol
This package defines and implements an additional gRPC protocol, with
gRPC services and methods that are specific to Mercurial, or more generally
Heptapod.
### Protocol specification
The sources are `proto` files in the `protos/` directory, same as for the
Gitaly protocol.
They distinguish themselves by this declaration:
```
package hgitaly;
```
Each time a change is made to the protocol, the libraries for all
provided programming languages have to be regenerated and committed, ideally
together with the protocol change.
### Python library
It has a special status, being versioned together with the protocol and the
server implementation. It is provided as the [hgitaly.stub](hgitaly/stub)
package.
The Python stubs are produced by the same script that takes care of Gitaly
`proto` files:
```
./generate-stubs
```
### Ruby library
See [the separate documentation](ruby/README.md)
### Other languages
A Go library will probably be necessary quite soon for Workhorse or perhaps
Heptapod Shell.
A Rust library would be nice to have
[mercurial-testhelpers]: https://pypi.org/project/mercurial-testhelpers/
[grpcio]: https://pypi.org/project/grpcio/
......
......@@ -4,7 +4,8 @@ from hgitaly.service.blob import BlobServicer
from hgitaly.service.commit import CommitServicer
from hgitaly.service.ref import RefServicer
from hgitaly.service.diff import DiffServicer
from hgitaly.service.repository_service import RepositoryServiceServicer
from hgitaly.service.mercurial_repository import MercurialRepositoryServicer
from hgitaly.service.repository import RepositoryServicer
from hgitaly.service.server import ServerServicer
from hgitaly.stub.blob_pb2_grpc import add_BlobServiceServicer_to_server
from hgitaly.stub.commit_pb2_grpc import add_CommitServiceServicer_to_server
......@@ -13,6 +14,9 @@ from hgitaly.stub.diff_pb2_grpc import add_DiffServiceServicer_to_server
from hgitaly.stub.repository_service_pb2_grpc import (
add_RepositoryServiceServicer_to_server
)
from hgitaly.stub.mercurial_repository_pb2_grpc import (
add_MercurialRepositoryServiceServicer_to_server
)
from hgitaly.stub.server_pb2_grpc import add_ServerServiceServicer_to_server
......@@ -29,7 +33,10 @@ def grpc_server(_grpc_server, grpc_addr, server_repos_root):
add_DiffServiceServicer_to_server(DiffServicer(storages),
_grpc_server)
add_RepositoryServiceServicer_to_server(
RepositoryServiceServicer(storages),
RepositoryServicer(storages),
_grpc_server)
add_MercurialRepositoryServiceServicer_to_server(
MercurialRepositoryServicer(storages),
_grpc_server)
add_ServerServiceServicer_to_server(ServerServicer(storages),
_grpc_server)
......
......@@ -5,7 +5,7 @@ set -eu
PROTOS_DIR=./protos
STUBS_DIR=hgitaly/stub
for PROTO in lint shared blob commit diff ref repository-service server; do
for PROTO in lint shared blob commit diff ref repository-service server mercurial-repository; do
python -m grpc_tools.protoc \
-I${PROTOS_DIR} \
--python_out=${STUBS_DIR} \
......
......@@ -50,6 +50,8 @@ def gitlab_revision_changeset(repo, revision):
"""
if revision == b'HEAD':
revision = get_default_gitlab_branch(repo)
if revision is None:
return None
# non ambigous ref forms: they should obviously take precedence
# TODO direct reference to tag here as well
......
......@@ -24,7 +24,8 @@ from .service.blob import BlobServicer
from .service.commit import CommitServicer
from .service.ref import RefServicer
from .service.diff import DiffServicer
from .service.repository_service import RepositoryServiceServicer
from .service.mercurial_repository import MercurialRepositoryServicer
from .service.repository import RepositoryServicer
from .service.server import ServerServicer
from .stub.blob_pb2_grpc import add_BlobServiceServicer_to_server
......@@ -34,6 +35,9 @@ from .stub.diff_pb2_grpc import add_DiffServiceServicer_to_server
from .stub.repository_service_pb2_grpc import (
add_RepositoryServiceServicer_to_server
)
from .stub.mercurial_repository_pb2_grpc import (
add_MercurialRepositoryServiceServicer_to_server
)
from .stub.server_pb2_grpc import add_ServerServiceServicer_to_server
logger = logging.getLogger(__name__)
......@@ -68,8 +72,10 @@ def init(listen_urls, storages):
add_CommitServiceServicer_to_server(CommitServicer(storages), server)
add_RefServiceServicer_to_server(RefServicer(storages), server)
add_DiffServiceServicer_to_server(DiffServicer(storages), server)
add_MercurialRepositoryServiceServicer_to_server(
MercurialRepositoryServicer(storages), server)
add_RepositoryServiceServicer_to_server(
RepositoryServiceServicer(storages), server)
RepositoryServicer(storages), server)
add_ServerServiceServicer_to_server(ServerServicer(storages), server)
for url in listen_urls:
......
......@@ -97,6 +97,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def CommitIsAncestor(self,
request: CommitIsAncestorRequest,
context) -> CommitIsAncestorResponse:
logger.debug("Processing %r", message.Logging(request))
# The question is legit for filtered changesets and that
# happens in MR rebase scenarios, before the Rails app realizes
# the MR has to be updated.
......@@ -126,7 +127,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
Actually, it always yields one response message, using the empty
response in case the given path does not resolve.
"""
logger.info("TreeEntry request=%r", message.Logging(request))
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context).unfiltered()
changeset = gitlab_revision_changeset(repo, request.revision)
if changeset is None:
......@@ -236,6 +237,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
TODO: find out if there are default values to apply for ``from`` and
``to``
"""
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
unfi = repo.unfiltered()
rev_from = gitlab_revision_changeset(repo, getattr(request, 'from'))
......@@ -261,6 +263,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def CountCommits(self,
request: CountCommitsRequest,
context) -> CountCommitsResponse:
logger.debug("Processing %r", message.Logging(request))
# TODO: yet to finish this method to support all lookups
repo = self.load_repo(request.repository, context)
revision = request.revision
......@@ -304,6 +307,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def CountDivergingCommits(self,
request: CountDivergingCommitsRequest,
context) -> CountDivergingCommitsResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
rev_from = gitlab_revision_changeset(repo, getattr(request, 'from'))
rev_to = gitlab_revision_changeset(repo, getattr(request, 'to'))
......@@ -336,7 +340,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def GetTreeEntries(self, request: GetTreeEntriesRequest,
context) -> GetTreeEntriesResponse:
logger.info("GetTreeEntries request=%r", message.Logging(request))
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context).unfiltered()
revision = request.revision
changeset = gitlab_revision_changeset(repo, revision)
......@@ -374,9 +378,9 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def ListFiles(self, request: ListFilesRequest,
context) -> ListFilesResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
revision = pycompat.sysbytes(request.revision)
logger.debug("ListFiles revision=%r", revision)
ctx = gitlab_revision_changeset(repo, revision)
if ctx is None:
return
......@@ -386,9 +390,9 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def CommitStats(self, request: CommitStatsRequest,
context) -> CommitStatsResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
revision = pycompat.sysbytes(request.revision)
logger.debug("CommitStats revision=%r", revision)
ctx = gitlab_revision_changeset(repo, revision)
if ctx is None:
return internal_error(
......@@ -412,9 +416,9 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def FindCommit(self,
request: FindCommitRequest, context) -> FindCommitResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
revision = request.revision
logger.debug("FindCommit revision=%r", revision)
ctx = gitlab_revision_changeset(repo, revision)
if ctx is None:
......@@ -428,12 +432,12 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def FindAllCommits(self, request: FindAllCommitsRequest,
context) -> FindAllCommitsResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
revision = request.revision
opts = {}
if revision:
# If false, return all commits reachable by any branch in the repo
logger.debug("FindAllCommits revision=%r", revision)
ctx = gitlab_revision_changeset(repo, revision)
if ctx is None:
logger.debug(
......@@ -471,6 +475,9 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def FindCommits(self, request: FindCommitsRequest,
context) -> FindCommitsResponse:
req_log = message.Logging(request)
logger.debug("Processing %r", message.Logging(request))
if request.limit == 0:
return
......@@ -484,8 +491,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
if request.revision and not opts[b'rev'][0]:
logger.debug(
"FindCommits revision %r could not be found",
request.revision)
"Request %r, revision could not be found", req_log)
return FindCommitsResponse()
if util.versiontuple(n=2) <= (5, 5): # hg<=5.5
......@@ -518,6 +524,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def RawBlame(self, request: RawBlameRequest,
context) -> RawBlameResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
filepath = request.path
if not filepath:
......@@ -535,15 +542,16 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def LastCommitForPath(self,
request: LastCommitForPathRequest,
context) -> LastCommitForPathResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
revision, path = request.revision, request.path
logger.debug("LastCommitForPath revision=%r, path=%r", revision, path)
ctx = gitlab_revision_changeset(repo, revision)
changeset = latest_changeset_for_path(path, ctx)
return LastCommitForPathResponse(commit=message.commit(changeset))
def ListLastCommitsForTree(self, request: ListLastCommitsForTreeRequest,
context) -> ListLastCommitsForTreeResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
revision = pycompat.sysbytes(request.revision)
from_ctx = gitlab_revision_changeset(repo, revision)
......@@ -594,6 +602,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def CommitsByMessage(self, request: CommitsByMessageRequest,
context) -> CommitsByMessageResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
query = request.query
if not query:
......@@ -635,6 +644,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def ListCommitsByOid(self, request: ListCommitsByOidRequest,
context) -> ListCommitsByOidResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
lookup_error_classes = (error.LookupError, error.RepoLookupError)
for chunk in chunked(pycompat.sysbytes(oid) for oid in request.oid):
......@@ -663,6 +673,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def ListCommitsByRefName(self, request: ListCommitsByRefNameRequest,
context) -> ListCommitsByRefNameResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
ref_names = request.ref_names
......@@ -698,6 +709,7 @@ class CommitServicer(CommitServiceServicer, HGitalyServicer):
def GetCommitMessages(self, request: GetCommitMessagesRequest,
context) -> GetCommitMessagesResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
results = {}
for commit_id in request.commit_ids:
......
......@@ -10,6 +10,7 @@ from io import BytesIO
import attr
from ..errors import (
not_found,
unknown_error,
unavailable_error,
)
......@@ -21,6 +22,7 @@ from ..git import (
EMPTY_TREE_OID,
)
from mercurial import (
copies,
node,
patch as patchmod,
pycompat,
......@@ -29,12 +31,15 @@ from mercurial import (
scmutil,
)
from ..stub.diff_pb2 import (
ChangedPaths,
CommitDiffRequest,
CommitDiffResponse,
CommitDelta as MsgCommitDelta,
CommitDeltaRequest,
CommitDeltaResponse,
DiffStats as MsgDiffStats,
FindChangedPathsRequest,
FindChangedPathsResponse,
RawDiffRequest,
RawDiffResponse,
RawPatchRequest,
......@@ -53,6 +58,7 @@ from ..stream import (
split_batches,
WRITE_BUFFER_SIZE,
)
from ..util import chunked
logger = logging.getLogger(__name__)
......@@ -92,6 +98,15 @@ MAX_PATCH_BYTES_UPPERBOUND = 512000 # 500KB
"""MAX_PATCH_BYTES_UPPERBOUND controls how much max_patch_bytes limit can reach
"""
Status_Type_Map = dict(
added=ChangedPaths.Status.ADDED,
modified=ChangedPaths.Status.MODIFIED,
removed=ChangedPaths.Status.DELETED,
copied=ChangedPaths.Status.COPIED,
# Note: Mercurial includes TYPE_CHANGE
# (symlink, regular file, submodule...etc) in MODIFIED status
)
@attr.s
class Limits:
......@@ -457,6 +472,45 @@ class DiffServicer(DiffServiceServicer, HGitalyServicer):
MAX_NUM_STAT_BATCH_SIZE):
yield DiffStatsResponse(stats=stats)
def FindChangedPaths(self, request: FindChangedPathsRequest,
context) -> FindChangedPathsResponse:
repo = self.load_repo(request.repository, context)
csets = []
for cid in request.commits:
ctx = gitlab_revision_changeset(repo, parse_diff_request_cid(cid))
if ctx is None:
yield not_found(
context, FindChangedPathsResponse,
'FindChangedPaths: commit: %s can not be '
'found' % pycompat.sysstr(cid)
)
return
csets.append(ctx)
def in_changed_paths():
for ctx in csets:
p1 = ctx.p1()
status = p1.status(ctx)
for path in _status_paths(status, Status_Type_Map):
yield path
copy = copies.pathcopies(p1, ctx)
for path in copy:
yield ChangedPaths(
path=path,
status=Status_Type_Map['copied']
)
for paths in chunked(in_changed_paths()):
yield FindChangedPathsResponse(paths=paths)
def _status_paths(status, type_map):
for stype in ['added', 'modified', 'removed']:
for path in status.__getattribute__(stype):
yield ChangedPaths(
path=path,
status=type_map[stype]
)
def parse_diff_request_cid(cid):
"""Perform the conversions from a request commit_id to a usable revision.
......
# coding: utf-8
# Copyright 2021 Georges Racinet <georges.racinet@octobus.net>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import logging
from mercurial import (
pycompat,
)
from ..errors import (
not_implemented,
)
from ..stub.mercurial_repository_pb2 import (
ConfigItemType,
GetConfigItemRequest,
GetConfigItemResponse,
)
from ..stub.mercurial_repository_pb2_grpc import (
MercurialRepositoryServiceServicer,
)
from ..servicer import HGitalyServicer
logger = logging.getLogger(__name__)
class MercurialRepositoryServicer(MercurialRepositoryServiceServicer,
HGitalyServicer):
"""MercurialRepositoryService implementation.
The ordering of methods in this source file is the same as in the proto
file.
"""
def GetConfigItem(self,
request: GetConfigItemRequest,
context) -> GetConfigItemResponse:
repo = self.load_repo(request.repository, context)
section = pycompat.sysbytes(request.section)
name = pycompat.sysbytes(request.name)
if request.as_type == ConfigItemType.BOOL:
# TODO error treatment if value is not boolean
return GetConfigItemResponse(
as_bool=repo.ui.configbool(section, name))
return not_implemented( # pragma no cover
context, GetConfigItemResponse,
issue=60)
......@@ -107,6 +107,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
self,
request: FindDefaultBranchNameRequest,
context) -> FindDefaultBranchNameResponse:
logger.debug("Processing %r", message.Logging(request))
try:
repo = self.load_repo(request.repository, context)
except KeyError as exc:
......@@ -130,6 +131,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
def FindAllBranchNames(self,
request: FindAllBranchNamesRequest,
context) -> FindAllBranchNamesResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
for chunk in chunked(iter_gitlab_branches_as_refs(repo)):
yield FindAllBranchNamesResponse(names=(br[0] for br in chunk))
......@@ -137,6 +139,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
def FindAllTagNames(self,
request: FindAllTagNamesRequest,
context) -> FindAllTagNamesResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
# TODO SPEC it's not clear whether GitLab actually expect the
# tags to be in any ordering. At a quick glance it doesn't, but
......@@ -185,6 +188,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
As of GitLab 12.10, there is no caller for this Repository method
in the whole Rails application.
"""
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
ensure_gitlab_branches_state_file(repo)
......@@ -226,6 +230,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
def FindLocalBranches(self,
request: FindLocalBranchesRequest,
context) -> FindLocalBranchesResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
for chunk in chunked(iter_gitlab_branches(repo)):
yield FindLocalBranchesResponse(
......@@ -236,6 +241,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
def FindAllBranches(self,
request: FindAllBranchesRequest,
context) -> FindAllBranchesResponse:
logger.debug("Processing %r", message.Logging(request))
Branch = FindAllBranchesResponse.Branch
repo = self.load_repo(request.repository, context)
for chunk in chunked(iter_gitlab_branches(repo)):
......@@ -246,6 +252,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
def FindAllTags(self,
request: FindAllTagsRequest,
context) -> FindAllTagsResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
for chunk in chunked(
item for item in pycompat.iteritems(repo.tags())
......@@ -256,6 +263,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
def FindTag(self,
request: FindTagRequest,
context) -> FindTagResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
name = request.tag_name
if repo.tagtype(name) in EXCLUDED_TAG_TYPES:
......@@ -270,11 +278,13 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
request: FindAllRemoteBranchesRequest,
context) -> FindAllRemoteBranchesResponse:
"""There is no concept of "remote branch" in Mercurial."""
logger.debug("Processing %r", message.Logging(request))
return iter(())
def RefExists(self,
request: RefExistsRequest,
context) -> RefExistsResponse:
logger.debug("Processing %r", message.Logging(request))
ref = request.ref
if not ref.startswith(b'refs/'):
# TODO SPEC commment in ref.proto says `ref` must start with
......@@ -311,6 +321,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
def FindBranch(self,
request: FindBranchRequest,
context) -> FindBranchResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
name = request.name
if name.startswith(b'refs/'):
......@@ -330,6 +341,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
def DeleteRefs(self,
request: DeleteRefsRequest,
context) -> DeleteRefsResponse:
logger.debug("Processing %r", message.Logging(request))
except_prefix = request.except_with_prefix
refs = request.refs
if refs and except_prefix:
......@@ -361,6 +373,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
self,
request: ListBranchNamesContainingCommitRequest,
context) -> ListBranchNamesContainingCommitResponse:
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
gl_branches_by_heads = {}
......@@ -390,6 +403,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
self,
request: ListTagNamesContainingCommitRequest,
context) -> ListTagNamesContainingCommitResponse:
logger.debug("Processing %r", message.Logging(request))
# TODO support ordering, see similar method for branches
repo = self.load_repo(request.repository, context)
revs = repo.revs("%s:: and tag()", request.commit_id)
......@@ -409,6 +423,7 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
For now, we'll consider that the id of a tag is the nod id of the
changeset that gives it its current value.
"""
logger.debug("Processing %r", message.Logging(request))
repo = self.load_repo(request.repository, context)
# TODO check that the given id is indeed for a tag, i.e. a
# changeset that affects .hgtags?
......@@ -471,10 +486,8 @@ class RefServicer(RefServiceServicer, HGitalyServicer):
"""Not relevant for Mercurial, does nothing.
"""
# repr(Repository) contains newlines
logger.warning("Ignored irrelevant call to PackRefs method "
"for Mercurial repository %r on storage %r",
request.repository.relative_path,
request.repository.storage_name)
logger.warning("Ignored irrelevant PackRefs request %r",
message.Logging(request))
return PackRefsResponse()
......
......@@ -11,6 +11,8 @@ import tempfile
from mercurial import (
archival,
scmutil,
hg,
vfs as vfsmod,
)
from heptapod.gitlab.branch import gitlab_branch_from_ref
......@@ -22,6 +24,7 @@ from hgext3rd.heptapod.keep_around import (
)
from ..errors import (
internal_error,
invalid_argument,
not_implemented,
)
......@@ -37,6 +40,8 @@ from ..revision import (
gitlab_revision_changeset,