hgitaly 18.2.0__tar.gz → 18.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hgitaly-18.2.0/hgitaly.egg-info → hgitaly-18.2.1}/PKG-INFO +1 -1
- hgitaly-18.2.1/hgitaly/VERSION +1 -0
- hgitaly-18.2.1/hgitaly/service/operations.py +653 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/fixture.py +7 -3
- hgitaly-18.2.1/hgitaly/service/tests/test_operations.py +799 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/servicer.py +5 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/workdir.py +3 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1/hgitaly.egg-info}/PKG-INFO +1 -1
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/comparison.py +24 -2
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_operations.py +221 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_repository_service.py +26 -3
- hgitaly-18.2.0/hgitaly/VERSION +0 -1
- hgitaly-18.2.0/hgitaly/service/operations.py +0 -263
- hgitaly-18.2.0/hgitaly/service/tests/test_operations.py +0 -275
- {hgitaly-18.2.0 → hgitaly-18.2.1}/LICENSE +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/MANIFEST.in +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/README.md +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgext3rd/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgext3rd/hgitaly/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgext3rd/hgitaly/revset.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgext3rd/hgitaly/tests/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgext3rd/hgitaly/tests/test_revset.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgext3rd/hgitaly/tests/test_serve.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/branch.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/changelog.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/diff.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/errors.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/feature.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/file_content.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/file_context.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/git.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/gitlab_ref.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/identification.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/logging.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/manifest.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/message.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/oid.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/pagination.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/path.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/peer.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/procutil.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/repository.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/revision.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/revset.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/scripts.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/server/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/server/address.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/server/mono.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/server/prefork.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/server/tests/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/server/tests/test_address.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/server/tests/test_mono.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/server/tests/test_prefork.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/server/tests/test_worker.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/server/worker.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/analysis.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/blob.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/commit.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/diff.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/interceptors.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/mercurial_changeset.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/mercurial_operations.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/mercurial_repository.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/ref.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/repository.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/server.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/test_analysis.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/test_blob.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/test_commit.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/test_default_branch.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/test_diff.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/test_mercurial_changeset.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/test_mercurial_operations.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/test_mercurial_repository.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/test_ref.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/test_repository_service.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/service/tests/test_server.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/ssh.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stream.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/analysis_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/analysis_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/blob_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/blob_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/commit_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/commit_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/diff_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/diff_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/errors_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/errors_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/lint_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/lint_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/mercurial_aux_git_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/mercurial_aux_git_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/mercurial_changeset_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/mercurial_changeset_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/mercurial_operations_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/mercurial_operations_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/mercurial_repository_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/mercurial_repository_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/operations_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/operations_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/ref_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/ref_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/remote_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/remote_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/repository_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/repository_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/server_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/server_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/shared_pb2.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/stub/shared_pb2_grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tag.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/testing/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/testing/bundle.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/testing/context.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/testing/grpc.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/testing/multiprocessing.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/testing/ssh.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/testing/sshd.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/testing/storage.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/testing/tests/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/testing/tests/test_sshd.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/common.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_branch.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_diff.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_errors.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_feature.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_file_context.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_gitlab_ref.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_identification.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_manifest.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_messages.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_oid.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_peer.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_repository.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_revision.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_revset.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_servicer.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_stream.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_tag.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/tests/test_workdir.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly/util.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly.egg-info/SOURCES.txt +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly.egg-info/dependency_links.txt +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly.egg-info/entry_points.txt +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly.egg-info/requires.txt +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/hgitaly.egg-info/top_level.txt +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/install-requirements.txt +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/setup.cfg +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/setup.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/__init__.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/conftest.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/gitaly.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/hgitaly_rhgitaly_comparison.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/rhgitaly.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_blob_tree.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_commit.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_comparison.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_diff.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_gitaly_server.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_mercurial_aux_git.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_mercurial_operations.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_mercurial_repository.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_ref.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_remote.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_rhgitaly_server.py +0 -0
- {hgitaly-18.2.0 → hgitaly-18.2.1}/tests_with_gitaly/test_server.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
18.2.1
|
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
# Copyright 2023 Georges Racinet <georges.racinet@octobus.net>
|
|
2
|
+
#
|
|
3
|
+
# This software may be used and distributed according to the terms of the
|
|
4
|
+
# GNU General Public License version 2 or any later version.
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
7
|
+
from base64 import b64decode
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from grpc import StatusCode
|
|
13
|
+
|
|
14
|
+
from mercurial import (
|
|
15
|
+
cmdutil,
|
|
16
|
+
commands,
|
|
17
|
+
node,
|
|
18
|
+
scmutil,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from heptapod import (
|
|
22
|
+
obsutil,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from heptapod.gitlab.branch import (
|
|
26
|
+
NAMED_BRANCH_PREFIX,
|
|
27
|
+
parse_gitlab_branch,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from ..branch import (
|
|
31
|
+
gitlab_branch_head
|
|
32
|
+
)
|
|
33
|
+
from ..changelog import (
|
|
34
|
+
ancestor,
|
|
35
|
+
merge_content,
|
|
36
|
+
)
|
|
37
|
+
from ..errors import (
|
|
38
|
+
operation_error_treatment,
|
|
39
|
+
structured_abort,
|
|
40
|
+
)
|
|
41
|
+
from ..logging import LoggerAdapter
|
|
42
|
+
from ..revision import (
|
|
43
|
+
changeset_by_commit_id_abort,
|
|
44
|
+
gitlab_revision_changeset,
|
|
45
|
+
validate_oid,
|
|
46
|
+
)
|
|
47
|
+
from ..servicer import HGitalyServicer
|
|
48
|
+
|
|
49
|
+
from ..stub.operations_pb2 import (
|
|
50
|
+
OperationBranchUpdate,
|
|
51
|
+
UserCommitFilesActionHeader,
|
|
52
|
+
UserCommitFilesError,
|
|
53
|
+
UserCommitFilesRequest,
|
|
54
|
+
UserCommitFilesResponse,
|
|
55
|
+
UserFFBranchError,
|
|
56
|
+
UserFFBranchRequest,
|
|
57
|
+
UserFFBranchResponse,
|
|
58
|
+
UserSquashRequest,
|
|
59
|
+
UserSquashResponse,
|
|
60
|
+
UserSquashError,
|
|
61
|
+
)
|
|
62
|
+
from ..stub.errors_pb2 import (
|
|
63
|
+
IndexError,
|
|
64
|
+
ReferenceUpdateError,
|
|
65
|
+
ResolveRevisionError,
|
|
66
|
+
)
|
|
67
|
+
from ..stub.operations_pb2_grpc import OperationServiceServicer
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
base_logger = logging.getLogger(__name__)
|
|
71
|
+
ActionType = UserCommitFilesActionHeader.ActionType
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
DOUBLE_SLASH = b'//'
|
|
75
|
+
DOUBLE_PARSEP = (os.path.sep * 2).encode() # in case it's not double slash
|
|
76
|
+
DIRECTORY_CLIMB_UP = (os.path.pardir + os.path.sep).encode()
|
|
77
|
+
|
|
78
|
+
FORBIDDEN_IN_PATHS = tuple(set((DOUBLE_SLASH,
|
|
79
|
+
DOUBLE_PARSEP,
|
|
80
|
+
DIRECTORY_CLIMB_UP,
|
|
81
|
+
)))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def index_error(context, status_code, error_type, msg, path=b''):
|
|
85
|
+
"""Abort with IndexError structued error.
|
|
86
|
+
|
|
87
|
+
:param str status_code: name of the gRPC status code
|
|
88
|
+
:param str error_type: name of the IndexError type enum
|
|
89
|
+
"""
|
|
90
|
+
structured_abort(
|
|
91
|
+
context,
|
|
92
|
+
getattr(StatusCode, status_code),
|
|
93
|
+
msg,
|
|
94
|
+
UserCommitFilesError(
|
|
95
|
+
index_update=IndexError(
|
|
96
|
+
error_type=getattr(IndexError.ErrorType, error_type),
|
|
97
|
+
path=path
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class OperationServicer(OperationServiceServicer, HGitalyServicer):
|
|
104
|
+
"""OperationsServiceService implementation.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def UserSquash(self,
|
|
108
|
+
request: UserSquashRequest,
|
|
109
|
+
context) -> UserSquashResponse:
|
|
110
|
+
logger = LoggerAdapter(base_logger, context)
|
|
111
|
+
repo = self.load_repo(request.repository, context,
|
|
112
|
+
for_mutation_by=request.user)
|
|
113
|
+
with_hg_git = not repo.ui.configbool(b'heptapod', b'no-git')
|
|
114
|
+
# Gitaly's squash is merge-centric, start_sha is actually the
|
|
115
|
+
# merge target, whereas end_sha is the head of the MR being accepted
|
|
116
|
+
# TODO check that there are no public changesets for a nicer
|
|
117
|
+
# user feedback.
|
|
118
|
+
start_sha, end_sha = request.start_sha, request.end_sha
|
|
119
|
+
if not start_sha:
|
|
120
|
+
context.abort(StatusCode.INVALID_ARGUMENT, "empty StartSha")
|
|
121
|
+
if not end_sha:
|
|
122
|
+
context.abort(StatusCode.INVALID_ARGUMENT, "empty EndSha")
|
|
123
|
+
start_rev, end_rev = start_sha.encode('ascii'), end_sha.encode('ascii')
|
|
124
|
+
start_ctx = gitlab_revision_changeset(repo, start_rev)
|
|
125
|
+
end_ctx = gitlab_revision_changeset(repo, end_rev)
|
|
126
|
+
|
|
127
|
+
for (ctx, rev, error_label) in (
|
|
128
|
+
(start_ctx, start_rev, 'start'), (end_ctx, end_rev, 'end')
|
|
129
|
+
):
|
|
130
|
+
if ctx is None:
|
|
131
|
+
structured_abort(
|
|
132
|
+
context,
|
|
133
|
+
StatusCode.INVALID_ARGUMENT,
|
|
134
|
+
f'resolving {error_label} revision: reference not found',
|
|
135
|
+
UserSquashError(resolve_revision=ResolveRevisionError(
|
|
136
|
+
revision=rev))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
revset = (f"ancestor({start_sha}, {end_sha})::{end_sha}"
|
|
140
|
+
f"- ancestor({start_sha}, {end_sha})").encode('ascii')
|
|
141
|
+
end_ctx = gitlab_revision_changeset(repo, end_sha.encode('ascii'))
|
|
142
|
+
logger.info("Folding revset %s, mirroring to Git=%r",
|
|
143
|
+
revset, with_hg_git)
|
|
144
|
+
# TODO add the hg_git flag or maybe let servicer do it.
|
|
145
|
+
message = request.commit_message
|
|
146
|
+
if not message:
|
|
147
|
+
context.abort(StatusCode.INVALID_ARGUMENT, "empty CommitMessage")
|
|
148
|
+
|
|
149
|
+
# Mercurial does not distinguish between author (actual author of
|
|
150
|
+
# the work) and committer (could be just someone with commit rights
|
|
151
|
+
# relaying the original work). In case an author is provided, it
|
|
152
|
+
# feels right to derive the Mercurial author from it, as preserving
|
|
153
|
+
# the actual work metadata (and copyright) should have priority.
|
|
154
|
+
# This is what `HgGitRepository` used to do on the Rails side.
|
|
155
|
+
author = request.author
|
|
156
|
+
if not author.name:
|
|
157
|
+
context.abort(StatusCode.INVALID_ARGUMENT, "empty Author")
|
|
158
|
+
|
|
159
|
+
# timestamp is supposed to be for the committer, but Mercurial does
|
|
160
|
+
# not have such a distinction, hence it will become the commit date.
|
|
161
|
+
if not request.HasField('timestamp'):
|
|
162
|
+
unix_ts = int(time.time())
|
|
163
|
+
else:
|
|
164
|
+
unix_ts = request.timestamp.seconds
|
|
165
|
+
|
|
166
|
+
opts = {'from': False, # cannot be used as regular kwarg
|
|
167
|
+
'exact': True,
|
|
168
|
+
'rev': [revset],
|
|
169
|
+
'message': message,
|
|
170
|
+
'user': b'%s <%s>' % (author.name, author.email),
|
|
171
|
+
'date': b'%d 0' % unix_ts,
|
|
172
|
+
}
|
|
173
|
+
# Comment taken from `hg_git_repository.rb`:
|
|
174
|
+
# Note that `hg fold --exact` will fail unless the revset is
|
|
175
|
+
# "an unbroken linear chain". That fits the idea of a Merge Request
|
|
176
|
+
# neatly, and should be unsuprising to users: it's natural to expect
|
|
177
|
+
# squashes to stay simple.
|
|
178
|
+
# In particular, if there's a merge of a side topic, it will be
|
|
179
|
+
# unsquashable.
|
|
180
|
+
# Not 100% sure we need a workdir, but I don't see
|
|
181
|
+
# an explicit "inmemory" option as there is for `hg rebase`. What
|
|
182
|
+
# I do see is user (status) messages as in updates, so…
|
|
183
|
+
# If we update the workdir to "end" changeset, then the fold will
|
|
184
|
+
# look like an extra head and be rejected (probably because it is
|
|
185
|
+
# kept active by being the workdir parent).
|
|
186
|
+
# On the other hand, the "start" changeset is by design of the
|
|
187
|
+
# method not to be folded and is probably close enough that we
|
|
188
|
+
# get a reasonable average efficiency.
|
|
189
|
+
with self.working_dir(gl_repo=request.repository,
|
|
190
|
+
repo=repo,
|
|
191
|
+
changeset=start_ctx,
|
|
192
|
+
context=context) as wd:
|
|
193
|
+
# `allowunstable=no` protects us against all instabilities,
|
|
194
|
+
# in particular against orphaning dependent topics.
|
|
195
|
+
# TODO this setting should probably be set in all mutations
|
|
196
|
+
wd.repo.ui.setconfig(b'experimental.evolution', b'allowunstable',
|
|
197
|
+
False)
|
|
198
|
+
with operation_error_treatment(context, UserSquashError,
|
|
199
|
+
logger=logger):
|
|
200
|
+
retcode = self.repo_command(wd.repo, context, 'fold', **opts)
|
|
201
|
+
if retcode == 1:
|
|
202
|
+
revs = wd.repo.revs(revset)
|
|
203
|
+
if len(revs) == 1:
|
|
204
|
+
rev = next(iter(revs))
|
|
205
|
+
self.repo_command(wd.repo, context, 'update', rev)
|
|
206
|
+
self.repo_command(
|
|
207
|
+
wd.repo, context, 'amend',
|
|
208
|
+
message=message,
|
|
209
|
+
note=(b"Description changed for squash request "
|
|
210
|
+
b"for a single changeset"),
|
|
211
|
+
)
|
|
212
|
+
else: # pragma no cover
|
|
213
|
+
# This block is currently unreachable from tests
|
|
214
|
+
# and is here in case of unexpected behaviour change
|
|
215
|
+
# in the `fold` command.
|
|
216
|
+
context.abort(
|
|
217
|
+
StatusCode.INTERNAL,
|
|
218
|
+
"Internal return code 1, but zero or more than "
|
|
219
|
+
"one changeset for revset %r" % revset
|
|
220
|
+
)
|
|
221
|
+
self.repo_command(wd.repo, context, 'update', start_ctx.hex())
|
|
222
|
+
# The workdir repo does not have to be reloaded, whereas the
|
|
223
|
+
# main repo would. We just need to regrab the end changeset
|
|
224
|
+
# (now obsolete)
|
|
225
|
+
end_ctx = wd.repo.unfiltered()[end_ctx.rev()]
|
|
226
|
+
folded = obsutil.latest_unique_successor(end_ctx)
|
|
227
|
+
|
|
228
|
+
return UserSquashResponse(squash_sha=folded.hex().decode('ascii'))
|
|
229
|
+
|
|
230
|
+
def UserFFBranch(self,
|
|
231
|
+
request: UserFFBranchRequest,
|
|
232
|
+
context) -> UserFFBranchResponse:
|
|
233
|
+
logger = LoggerAdapter(base_logger, context)
|
|
234
|
+
repo = self.load_repo(request.repository, context,
|
|
235
|
+
for_mutation_by=request.user)
|
|
236
|
+
with_hg_git = not repo.ui.configbool(b'heptapod', b'no-git')
|
|
237
|
+
|
|
238
|
+
to_publish = changeset_by_commit_id_abort(
|
|
239
|
+
repo, request.commit_id, context)
|
|
240
|
+
if to_publish is None:
|
|
241
|
+
context.abort(
|
|
242
|
+
StatusCode.INTERNAL,
|
|
243
|
+
f'checking for ancestry: invalid commit: "{request.commit_id}"'
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
old_id = request.expected_old_oid
|
|
247
|
+
if old_id and not validate_oid(old_id):
|
|
248
|
+
context.abort(StatusCode.INVALID_ARGUMENT,
|
|
249
|
+
f'cannot parse commit ID: "{old_id}"')
|
|
250
|
+
|
|
251
|
+
if not request.branch:
|
|
252
|
+
context.abort(StatusCode.INVALID_ARGUMENT, "empty branch name")
|
|
253
|
+
|
|
254
|
+
if not request.branch.startswith(NAMED_BRANCH_PREFIX):
|
|
255
|
+
context.abort(StatusCode.FAILED_PRECONDITION,
|
|
256
|
+
"Heptapod fast forwards are currently "
|
|
257
|
+
"for named branches only (no topics nor bookmarks)")
|
|
258
|
+
|
|
259
|
+
current_head = gitlab_branch_head(repo, request.branch)
|
|
260
|
+
if to_publish.branch() != current_head.branch():
|
|
261
|
+
context.abort(StatusCode.FAILED_PRECONDITION,
|
|
262
|
+
"not a fast-forward (Mercurial branch differs)")
|
|
263
|
+
|
|
264
|
+
fail = False
|
|
265
|
+
for cs in merge_content(to_publish, current_head):
|
|
266
|
+
if cs.obsolete():
|
|
267
|
+
fail = True
|
|
268
|
+
fail_msg = "is obsolete"
|
|
269
|
+
if cs.isunstable():
|
|
270
|
+
fail = True
|
|
271
|
+
fail_msg = "is unstable"
|
|
272
|
+
if fail:
|
|
273
|
+
context.abort(StatusCode.FAILED_PRECONDITION,
|
|
274
|
+
f"not a fast-forward (changeset "
|
|
275
|
+
f"{cs.hex().decode('ascii')} {fail_msg})")
|
|
276
|
+
|
|
277
|
+
if old_id and old_id != current_head.hex().decode('ascii'):
|
|
278
|
+
# We did not need to resolve before this, but now we do because
|
|
279
|
+
# Gitaly has a specific error if resolution fails
|
|
280
|
+
if changeset_by_commit_id_abort(repo, old_id, context) is None:
|
|
281
|
+
context.abort(StatusCode.INVALID_ARGUMENT,
|
|
282
|
+
"cannot resolve expected old object ID: "
|
|
283
|
+
"reference not found")
|
|
284
|
+
# no point trying to match the Gitaly error details: we
|
|
285
|
+
# have the much better structured error
|
|
286
|
+
structured_abort(context,
|
|
287
|
+
StatusCode.FAILED_PRECONDITION,
|
|
288
|
+
"expected_old_oid mismatch",
|
|
289
|
+
UserFFBranchError(
|
|
290
|
+
reference_update=ReferenceUpdateError(
|
|
291
|
+
# Gitaly doesn't fill in `reference_name`
|
|
292
|
+
old_oid=old_id,
|
|
293
|
+
new_oid=to_publish.hex().decode('ascii'),
|
|
294
|
+
)))
|
|
295
|
+
|
|
296
|
+
if ancestor(to_publish, current_head) != current_head.rev():
|
|
297
|
+
context.abort(StatusCode.FAILED_PRECONDITION,
|
|
298
|
+
"not fast forward")
|
|
299
|
+
|
|
300
|
+
# TODO use phases.advanceboundary directly? Check cache invalidations
|
|
301
|
+
# carefully. ('phases' command is a pain to call and does lots of
|
|
302
|
+
# unnecessary stuff).
|
|
303
|
+
logger.info("All checks passed, now publishing %r, "
|
|
304
|
+
"mirroring to Git=%r", to_publish, with_hg_git)
|
|
305
|
+
self.publish(to_publish, context)
|
|
306
|
+
return UserFFBranchResponse(branch_update=OperationBranchUpdate(
|
|
307
|
+
commit_id=to_publish.hex().decode('ascii'),
|
|
308
|
+
))
|
|
309
|
+
|
|
310
|
+
def UserCommitFiles(self,
|
|
311
|
+
request: UserCommitFilesRequest,
|
|
312
|
+
context) -> UserCommitFilesResponse:
|
|
313
|
+
logger = LoggerAdapter(base_logger, context)
|
|
314
|
+
first_req = next(request)
|
|
315
|
+
if not first_req.HasField('header'):
|
|
316
|
+
context.abort(
|
|
317
|
+
StatusCode.INVALID_ARGUMENT,
|
|
318
|
+
"empty UserCommitFilesRequestHeader"
|
|
319
|
+
)
|
|
320
|
+
commit_header = first_req.header
|
|
321
|
+
grpc_repo = commit_header.repository
|
|
322
|
+
repo = self.load_repo(grpc_repo, context,
|
|
323
|
+
for_mutation_by=commit_header.user)
|
|
324
|
+
repo_created = branch_created = False
|
|
325
|
+
|
|
326
|
+
gl_branch = commit_header.branch_name
|
|
327
|
+
if not gl_branch:
|
|
328
|
+
context.abort(StatusCode.INVALID_ARGUMENT,
|
|
329
|
+
"empty BranchName")
|
|
330
|
+
|
|
331
|
+
start_sha = commit_header.start_sha
|
|
332
|
+
if start_sha:
|
|
333
|
+
start_rev = start_sha.encode('ascii')
|
|
334
|
+
elif commit_header.start_branch_name:
|
|
335
|
+
start_rev = commit_header.start_branch_name
|
|
336
|
+
else:
|
|
337
|
+
start_rev = gl_branch
|
|
338
|
+
|
|
339
|
+
parsed = parse_gitlab_branch(gl_branch)
|
|
340
|
+
if parsed is None:
|
|
341
|
+
context.abort(StatusCode.INVALID_ARGUMENT,
|
|
342
|
+
"Only named branches and topics are supported "
|
|
343
|
+
"(not bookmarks in particular)")
|
|
344
|
+
hg_branch, topic = parsed
|
|
345
|
+
to_publish = (topic is None
|
|
346
|
+
and repo.ui.configbool(b'experimental',
|
|
347
|
+
b'topic.publish-bare-branch'))
|
|
348
|
+
|
|
349
|
+
start_ctx = gitlab_revision_changeset(repo, start_rev)
|
|
350
|
+
if start_ctx is None:
|
|
351
|
+
if len(repo) > 0:
|
|
352
|
+
context.abort(StatusCode.INTERNAL,
|
|
353
|
+
"Unresolvable start ref or commit")
|
|
354
|
+
else:
|
|
355
|
+
repo_created = branch_created = True
|
|
356
|
+
if commit_header.branch_name:
|
|
357
|
+
old_oid = commit_header.expected_old_oid.encode('ascii')
|
|
358
|
+
if old_oid and start_ctx.hex() != old_oid:
|
|
359
|
+
context.abort(StatusCode.INVALID_ARGUMENT,
|
|
360
|
+
"wrong old oid")
|
|
361
|
+
|
|
362
|
+
if to_publish and self.heptapod_permission != 'publish':
|
|
363
|
+
context.abort(
|
|
364
|
+
StatusCode.PERMISSION_DENIED,
|
|
365
|
+
"insufficient permissions for publication",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
topic_cmd = cmdutil.findcmd(b'topics', commands.table)[1][0]
|
|
369
|
+
with self.working_dir(gl_repo=grpc_repo,
|
|
370
|
+
repo=repo,
|
|
371
|
+
changeset=start_ctx,
|
|
372
|
+
context=context) as wd:
|
|
373
|
+
ui = wd.repo.ui
|
|
374
|
+
# TODO should be set for all mutations
|
|
375
|
+
ui.setconfig(b'experimental.evolution', b'allowunstable', False)
|
|
376
|
+
|
|
377
|
+
logger.info("Preparing commit in working directory %s", wd.path)
|
|
378
|
+
if start_ctx is None or start_ctx.branch() != hg_branch:
|
|
379
|
+
logger.debug("Setting Mercurial branch to %r", hg_branch)
|
|
380
|
+
wd.repo.dirstate.setbranch(hg_branch, None)
|
|
381
|
+
branch_created = True
|
|
382
|
+
if topic is not None and (start_ctx is None
|
|
383
|
+
or start_ctx.topic() != topic):
|
|
384
|
+
logger.debug("Setting topic to %r", topic)
|
|
385
|
+
topic_cmd(ui, wd.repo, topic)
|
|
386
|
+
branch_created = True
|
|
387
|
+
|
|
388
|
+
content_handler = None
|
|
389
|
+
changed_files = set()
|
|
390
|
+
for req in request:
|
|
391
|
+
if not req.HasField('action'):
|
|
392
|
+
context.abort(
|
|
393
|
+
StatusCode.INTERNAL, # See Comparison Test
|
|
394
|
+
"unhandled action payload type: <nil>"
|
|
395
|
+
)
|
|
396
|
+
action = req.action
|
|
397
|
+
if action.HasField('content'):
|
|
398
|
+
if content_handler is None:
|
|
399
|
+
context.abort(
|
|
400
|
+
StatusCode.INTERNAL, # See Comparison Test
|
|
401
|
+
"content sent before action"
|
|
402
|
+
)
|
|
403
|
+
else:
|
|
404
|
+
logger.debug("UserCommitFiles content=%r",
|
|
405
|
+
action.content)
|
|
406
|
+
content_handler.write(action.content)
|
|
407
|
+
elif action.HasField('header'):
|
|
408
|
+
logger.info("UserCommitFiles action header: %r",
|
|
409
|
+
action.header)
|
|
410
|
+
if content_handler is not None:
|
|
411
|
+
content_handler.close()
|
|
412
|
+
content_handler = None
|
|
413
|
+
header = action.header
|
|
414
|
+
UserCommitFilesAction(
|
|
415
|
+
context=context,
|
|
416
|
+
header=header,
|
|
417
|
+
working_dir=wd,
|
|
418
|
+
changed_files=changed_files,
|
|
419
|
+
)()
|
|
420
|
+
|
|
421
|
+
if header.action in (ActionType.CREATE,
|
|
422
|
+
ActionType.UPDATE):
|
|
423
|
+
content_handler = UserCommitFilesContent(wd, header)
|
|
424
|
+
else:
|
|
425
|
+
content_handler = None
|
|
426
|
+
|
|
427
|
+
if content_handler is not None:
|
|
428
|
+
content_handler.close()
|
|
429
|
+
|
|
430
|
+
def do_commit(ui, repo, message, match, opts):
|
|
431
|
+
commit_user = b'%s <%s>' % (
|
|
432
|
+
commit_header.commit_author_name,
|
|
433
|
+
commit_header.commit_author_email
|
|
434
|
+
)
|
|
435
|
+
return repo.commit(message,
|
|
436
|
+
commit_user,
|
|
437
|
+
(time.time(), 0),
|
|
438
|
+
match=match,
|
|
439
|
+
editor=False,
|
|
440
|
+
extra=None,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
logger.info("Performing commit")
|
|
444
|
+
with wd.repo.wlock(), wd.repo.lock():
|
|
445
|
+
try:
|
|
446
|
+
new_node = cmdutil.commit(
|
|
447
|
+
ui, wd.repo, do_commit,
|
|
448
|
+
changed_files,
|
|
449
|
+
{b'addremove': True,
|
|
450
|
+
b'message': commit_header.commit_message}
|
|
451
|
+
)
|
|
452
|
+
if to_publish and new_node is not None:
|
|
453
|
+
logger.info("Commit done, now publishing (no topic)")
|
|
454
|
+
self.publish(wd.repo[new_node], context)
|
|
455
|
+
except Exception as exc:
|
|
456
|
+
msg = str(exc)
|
|
457
|
+
code = StatusCode.INTERNAL
|
|
458
|
+
if 'multiple heads' in msg: # shame but not much better
|
|
459
|
+
code = StatusCode.INVALID_ARGUMENT
|
|
460
|
+
context.abort(code, msg)
|
|
461
|
+
|
|
462
|
+
if new_node is None:
|
|
463
|
+
return UserCommitFilesResponse()
|
|
464
|
+
|
|
465
|
+
return UserCommitFilesResponse(
|
|
466
|
+
branch_update=OperationBranchUpdate(
|
|
467
|
+
commit_id=node.hex(new_node).decode('ascii'),
|
|
468
|
+
repo_created=repo_created,
|
|
469
|
+
branch_created=branch_created,
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class UserCommitFilesAction:
|
|
475
|
+
|
|
476
|
+
def __init__(self, context, header, working_dir, changed_files):
|
|
477
|
+
self.context = context
|
|
478
|
+
self.header = header
|
|
479
|
+
self.changed_files = changed_files
|
|
480
|
+
|
|
481
|
+
wd = self.working_dir = working_dir
|
|
482
|
+
relpath = self.relpath = header.file_path
|
|
483
|
+
self.validate(relpath)
|
|
484
|
+
self.abspath = wd.file_path(relpath)
|
|
485
|
+
|
|
486
|
+
def __call__(self):
|
|
487
|
+
action = self.header.action
|
|
488
|
+
if action == ActionType.CREATE:
|
|
489
|
+
return self.create()
|
|
490
|
+
elif action == ActionType.CREATE_DIR:
|
|
491
|
+
return self.create_dir()
|
|
492
|
+
elif action == ActionType.UPDATE:
|
|
493
|
+
return self.update()
|
|
494
|
+
elif action == ActionType.MOVE:
|
|
495
|
+
return self.move()
|
|
496
|
+
elif action == ActionType.DELETE:
|
|
497
|
+
return self.delete()
|
|
498
|
+
elif action == ActionType.CHMOD:
|
|
499
|
+
return self.chmod()
|
|
500
|
+
|
|
501
|
+
def validate(self, relpath):
|
|
502
|
+
if os.path.isabs(relpath):
|
|
503
|
+
index_error(
|
|
504
|
+
self.context,
|
|
505
|
+
status_code='INVALID_ARGUMENT',
|
|
506
|
+
error_type='ERROR_TYPE_INVALID_PATH',
|
|
507
|
+
msg='absolute',
|
|
508
|
+
path=relpath,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
for p in FORBIDDEN_IN_PATHS:
|
|
512
|
+
if p in relpath:
|
|
513
|
+
relpath_str = relpath.decode('utf-8', 'surrogateescape')
|
|
514
|
+
|
|
515
|
+
if p == DIRECTORY_CLIMB_UP:
|
|
516
|
+
# ill-named by upstream
|
|
517
|
+
error_type = 'ERROR_TYPE_DIRECTORY_TRAVERSAL'
|
|
518
|
+
msg = 'Path cannot include directory traversal'
|
|
519
|
+
else:
|
|
520
|
+
error_type = 'ERROR_TYPE_INVALID_PATH'
|
|
521
|
+
msg = f'invalid path: "{relpath_str}"'
|
|
522
|
+
|
|
523
|
+
index_error(
|
|
524
|
+
self.context,
|
|
525
|
+
status_code='INVALID_ARGUMENT',
|
|
526
|
+
error_type=error_type,
|
|
527
|
+
msg=msg,
|
|
528
|
+
path=relpath,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
def create(self):
|
|
532
|
+
if os.path.exists(self.abspath):
|
|
533
|
+
index_error(
|
|
534
|
+
self.context,
|
|
535
|
+
status_code='ALREADY_EXISTS',
|
|
536
|
+
error_type='ERROR_TYPE_FILE_EXISTS',
|
|
537
|
+
msg="A file with this name already exists",
|
|
538
|
+
path=self.relpath,
|
|
539
|
+
)
|
|
540
|
+
# Gitaly does this as well
|
|
541
|
+
os.makedirs(os.path.dirname(self.abspath), exist_ok=True)
|
|
542
|
+
self.changed_files.add(self.abspath)
|
|
543
|
+
# actual creation to be done by content handler opening the file
|
|
544
|
+
|
|
545
|
+
def require_file_existence(self, abspath=None, relpath=None):
|
|
546
|
+
if abspath is None:
|
|
547
|
+
abspath = self.abspath
|
|
548
|
+
relpath = self.relpath
|
|
549
|
+
|
|
550
|
+
if not os.path.exists(abspath):
|
|
551
|
+
index_error(
|
|
552
|
+
self.context,
|
|
553
|
+
status_code='NOT_FOUND',
|
|
554
|
+
error_type='ERROR_TYPE_FILE_NOT_FOUND',
|
|
555
|
+
msg="A file with this name doesn't exist",
|
|
556
|
+
path=relpath
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def require_file_absence(self, abspath=None, relpath=None):
|
|
560
|
+
if abspath is None:
|
|
561
|
+
abspath = self.abspath
|
|
562
|
+
relpath = self.relpath
|
|
563
|
+
|
|
564
|
+
if os.path.exists(abspath):
|
|
565
|
+
if os.path.isdir(abspath):
|
|
566
|
+
error_type = 'ERROR_TYPE_DIRECTORY_EXISTS'
|
|
567
|
+
kind = "directory"
|
|
568
|
+
else:
|
|
569
|
+
error_type = 'ERROR_TYPE_FILE_EXISTS'
|
|
570
|
+
kind = "file"
|
|
571
|
+
index_error(
|
|
572
|
+
self.context,
|
|
573
|
+
status_code='ALREADY_EXISTS',
|
|
574
|
+
error_type=error_type,
|
|
575
|
+
msg=f"A {kind} with this name already exists",
|
|
576
|
+
path=relpath,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
def update(self):
|
|
580
|
+
self.require_file_existence()
|
|
581
|
+
self.changed_files.add(self.abspath)
|
|
582
|
+
# writing to be done by content handler
|
|
583
|
+
|
|
584
|
+
def create_dir(self):
|
|
585
|
+
"""Create a directory by putting an empty file in it."""
|
|
586
|
+
self.require_file_absence()
|
|
587
|
+
abspath = self.abspath
|
|
588
|
+
|
|
589
|
+
os.makedirs(abspath, exist_ok=False)
|
|
590
|
+
keep = os.path.join(abspath, b'.hgkeep')
|
|
591
|
+
open(keep, 'ab').close()
|
|
592
|
+
self.changed_files.add(keep)
|
|
593
|
+
|
|
594
|
+
def move(self):
|
|
595
|
+
self.require_file_absence()
|
|
596
|
+
abspath = self.abspath
|
|
597
|
+
prev_relpath = self.header.previous_path
|
|
598
|
+
self.validate(prev_relpath)
|
|
599
|
+
prev_abspath = self.working_dir.file_path(prev_relpath)
|
|
600
|
+
self.require_file_existence(abspath=prev_abspath,
|
|
601
|
+
relpath=prev_relpath)
|
|
602
|
+
|
|
603
|
+
repo = self.working_dir.repo
|
|
604
|
+
with repo.wlock(), repo.dirstate.changing_files(repo):
|
|
605
|
+
cmdutil.copy(repo.ui, repo, [prev_abspath, abspath],
|
|
606
|
+
opts={}, rename=True)
|
|
607
|
+
self.changed_files.add(prev_abspath)
|
|
608
|
+
self.changed_files.add(abspath)
|
|
609
|
+
|
|
610
|
+
def delete(self):
|
|
611
|
+
self.require_file_existence()
|
|
612
|
+
|
|
613
|
+
repo = self.working_dir.repo
|
|
614
|
+
with repo.wlock(), repo.dirstate.changing_files(repo):
|
|
615
|
+
m = scmutil.match(repo[None], [self.abspath], {})
|
|
616
|
+
uipathfn = scmutil.getuipathfn(repo,
|
|
617
|
+
legacyrelativevalue=True)
|
|
618
|
+
cmdutil.remove(repo.ui, repo, m,
|
|
619
|
+
prefix=b"", uipathfn=uipathfn,
|
|
620
|
+
after=False, force=False,
|
|
621
|
+
subrepos=None, dryrun=False,
|
|
622
|
+
)
|
|
623
|
+
self.changed_files.add(self.abspath)
|
|
624
|
+
|
|
625
|
+
def chmod(self):
|
|
626
|
+
self.require_file_existence()
|
|
627
|
+
set_hg_executable(self.abspath, self.header.execute_filemode)
|
|
628
|
+
self.changed_files.add(self.abspath)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class UserCommitFilesContent:
|
|
632
|
+
|
|
633
|
+
def __init__(self, workdir, header):
|
|
634
|
+
self.file_path = workdir.file_path(header.file_path)
|
|
635
|
+
self.fobj = open(self.file_path, 'wb')
|
|
636
|
+
self.make_hg_executable = (header.action == ActionType.CREATE
|
|
637
|
+
and header.execute_filemode)
|
|
638
|
+
self.base64 = header.base64_content
|
|
639
|
+
|
|
640
|
+
def close(self):
|
|
641
|
+
self.fobj.close()
|
|
642
|
+
if self.make_hg_executable:
|
|
643
|
+
set_hg_executable(self.file_path, True)
|
|
644
|
+
|
|
645
|
+
def write(self, content):
|
|
646
|
+
if self.base64:
|
|
647
|
+
content = b64decode(content)
|
|
648
|
+
self.fobj.write(content)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def set_hg_executable(path, executable):
|
|
652
|
+
# only the executable bit is actually tracked by Mercurial
|
|
653
|
+
os.chmod(path, 0o700 if executable else 0o600)
|
|
@@ -11,6 +11,7 @@ import shutil
|
|
|
11
11
|
from hgitaly import feature
|
|
12
12
|
from hgitaly.logging import CORRELATION_ID_MD_KEY
|
|
13
13
|
from hgitaly.servicer import (
|
|
14
|
+
HEPTAPOD_PERMISSION_KEY,
|
|
14
15
|
NATIVE_PROJECT_MD_KEY,
|
|
15
16
|
PY_HEPTAPOD_SKIP_HOOKS,
|
|
16
17
|
SKIP_HOOKS_MD_KEY,
|
|
@@ -180,9 +181,6 @@ class ServiceFixture:
|
|
|
180
181
|
|
|
181
182
|
def grpc_metadata(self):
|
|
182
183
|
"""Bake method call metadata.
|
|
183
|
-
|
|
184
|
-
Takes care of feature flags only as of this writing, but could also
|
|
185
|
-
be useful with other metadata (correlation id etc.)
|
|
186
184
|
"""
|
|
187
185
|
metadata = feature.as_grpc_metadata(self.feature_flags)
|
|
188
186
|
corr_id = self.correlation_id
|
|
@@ -200,6 +198,9 @@ class MutationServiceFixture(ServiceFixture):
|
|
|
200
198
|
It has a default :attr:`user`, generates the appropriate gRPC metadata,
|
|
201
199
|
etc.
|
|
202
200
|
"""
|
|
201
|
+
|
|
202
|
+
heptapod_permission = None
|
|
203
|
+
|
|
203
204
|
def __enter__(self):
|
|
204
205
|
super(MutationServiceFixture, self).__enter__()
|
|
205
206
|
self.ref_stub = RefServiceStub(self.grpc_channel)
|
|
@@ -215,6 +216,9 @@ class MutationServiceFixture(ServiceFixture):
|
|
|
215
216
|
native = getattr(self, 'hg_native', True)
|
|
216
217
|
mds.append((NATIVE_PROJECT_MD_KEY, str(native)))
|
|
217
218
|
mds.append((SKIP_HOOKS_MD_KEY, 'true'))
|
|
219
|
+
perm = self.heptapod_permission
|
|
220
|
+
if perm is not None:
|
|
221
|
+
mds.append((HEPTAPOD_PERMISSION_KEY, perm))
|
|
218
222
|
return mds
|
|
219
223
|
|
|
220
224
|
def list_refs(self, repo=None):
|