scmrepo 3.3.10__tar.gz → 3.3.12__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.
Potentially problematic release.
This version of scmrepo might be problematic. Click here for more details.
- {scmrepo-3.3.10 → scmrepo-3.3.12}/.github/workflows/tests.yaml +2 -2
- {scmrepo-3.3.10 → scmrepo-3.3.12}/.pre-commit-config.yaml +2 -2
- {scmrepo-3.3.10/src/scmrepo.egg-info → scmrepo-3.3.12}/PKG-INFO +6 -5
- {scmrepo-3.3.10 → scmrepo-3.3.12}/pyproject.toml +4 -3
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/asyn.py +1 -3
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/__init__.py +2 -1
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/dulwich/__init__.py +4 -4
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/dulwich/asyncssh_vendor.py +23 -67
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/dulwich/client.py +10 -3
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/pygit2/__init__.py +3 -1
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/pygit2/callbacks.py +2 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/pygit2/filter.py +1 -1
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/credentials.py +4 -4
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/smudge.py +1 -1
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/objects.py +1 -1
- {scmrepo-3.3.10 → scmrepo-3.3.12/src/scmrepo.egg-info}/PKG-INFO +6 -5
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo.egg-info/requires.txt +3 -3
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_dulwich.py +39 -26
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_git.py +1 -1
- {scmrepo-3.3.10 → scmrepo-3.3.12}/.coveragerc +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/.cruft.json +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/.gitattributes +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/.github/dependabot.yml +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/.github/workflows/release.yaml +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/.github/workflows/update-template.yaml +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/.gitignore +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/CODE_OF_CONDUCT.rst +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/CONTRIBUTING.rst +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/LICENSE +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/README.rst +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/noxfile.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/setup.cfg +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/__init__.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/base.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/exceptions.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/fs.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/__init__.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/base.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/gitpython.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/config.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/__init__.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/client.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/exceptions.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/fetch.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/object.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/pointer.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/progress.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/storage.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/stash.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/noscm.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/progress.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/py.typed +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/urls.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/utils.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo.egg-info/SOURCES.txt +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo.egg-info/dependency_links.txt +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo.egg-info/top_level.txt +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/__init__.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/conftest.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/docker-compose.yml +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/git-init/git.sh +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_credentials.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_fs.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_lfs.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_noscm.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_pygit2.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_scmrepo.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_stash.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_urls.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/user.key +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/user.key.pub +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/vendor/__init__.py +0 -0
- {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/vendor/test_paramiko_vendor.py +0 -0
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
strategy:
|
|
21
21
|
fail-fast: false
|
|
22
22
|
matrix:
|
|
23
|
-
os: [ubuntu-
|
|
23
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
24
24
|
pyv: ['3.9', '3.10', '3.11', '3.12', '3.13']
|
|
25
25
|
|
|
26
26
|
steps:
|
|
@@ -47,7 +47,7 @@ jobs:
|
|
|
47
47
|
run: nox -s tests-${{ matrix.pyv }} -- --slow --cov-report=xml
|
|
48
48
|
|
|
49
49
|
- name: Upload coverage report
|
|
50
|
-
uses: codecov/codecov-action@v5
|
|
50
|
+
uses: codecov/codecov-action@v5
|
|
51
51
|
|
|
52
52
|
- name: Build package
|
|
53
53
|
run: nox -s build
|
|
@@ -20,13 +20,13 @@ repos:
|
|
|
20
20
|
- id: sort-simple-yaml
|
|
21
21
|
- id: trailing-whitespace
|
|
22
22
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
23
|
-
rev: 'v0.
|
|
23
|
+
rev: 'v0.12.3'
|
|
24
24
|
hooks:
|
|
25
25
|
- id: ruff
|
|
26
26
|
args: [--fix, --exit-non-zero-on-fix]
|
|
27
27
|
- id: ruff-format
|
|
28
28
|
- repo: https://github.com/codespell-project/codespell
|
|
29
|
-
rev: v2.4.
|
|
29
|
+
rev: v2.4.1
|
|
30
30
|
hooks:
|
|
31
31
|
- id: codespell
|
|
32
32
|
additional_dependencies: ["tomli"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: scmrepo
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.12
|
|
4
4
|
Summary: scmrepo
|
|
5
5
|
Author-email: Iterative <support@dvc.org>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -17,7 +17,7 @@ Requires-Python: >=3.9
|
|
|
17
17
|
Description-Content-Type: text/x-rst
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: gitpython>3
|
|
20
|
-
Requires-Dist: dulwich>=0.
|
|
20
|
+
Requires-Dist: dulwich>=0.23.1
|
|
21
21
|
Requires-Dist: pygit2>=1.14.0
|
|
22
22
|
Requires-Dist: pygtrie>=2.3.2
|
|
23
23
|
Requires-Dist: fsspec[tqdm]>=2024.2.0
|
|
@@ -30,7 +30,7 @@ Provides-Extra: tests
|
|
|
30
30
|
Requires-Dist: aioresponses<0.8,>=0.7; extra == "tests"
|
|
31
31
|
Requires-Dist: paramiko<4,>=3.4.0; extra == "tests"
|
|
32
32
|
Requires-Dist: pytest<9,>=7; extra == "tests"
|
|
33
|
-
Requires-Dist: pytest-asyncio<
|
|
33
|
+
Requires-Dist: pytest-asyncio<2,>=0.23.2; extra == "tests"
|
|
34
34
|
Requires-Dist: pytest-cov>=4.1.0; extra == "tests"
|
|
35
35
|
Requires-Dist: pytest-docker<4,>=1; extra == "tests"
|
|
36
36
|
Requires-Dist: pytest-mock; extra == "tests"
|
|
@@ -38,12 +38,13 @@ Requires-Dist: pytest-sugar; extra == "tests"
|
|
|
38
38
|
Requires-Dist: pytest-test-utils<0.2,>=0.1.0; extra == "tests"
|
|
39
39
|
Requires-Dist: proxy.py; extra == "tests"
|
|
40
40
|
Provides-Extra: dev
|
|
41
|
-
Requires-Dist: mypy==1.
|
|
41
|
+
Requires-Dist: mypy==1.17.0; extra == "dev"
|
|
42
42
|
Requires-Dist: scmrepo[tests]; extra == "dev"
|
|
43
43
|
Requires-Dist: types-certifi; extra == "dev"
|
|
44
44
|
Requires-Dist: types-mock; extra == "dev"
|
|
45
45
|
Requires-Dist: types-paramiko; extra == "dev"
|
|
46
46
|
Requires-Dist: types-tqdm; extra == "dev"
|
|
47
|
+
Dynamic: license-file
|
|
47
48
|
|
|
48
49
|
scmrepo
|
|
49
50
|
=======
|
|
@@ -23,7 +23,7 @@ requires-python = ">=3.9"
|
|
|
23
23
|
dynamic = ["version"]
|
|
24
24
|
dependencies = [
|
|
25
25
|
"gitpython>3",
|
|
26
|
-
"dulwich>=0.
|
|
26
|
+
"dulwich>=0.23.1",
|
|
27
27
|
"pygit2>=1.14.0",
|
|
28
28
|
"pygtrie>=2.3.2",
|
|
29
29
|
"fsspec[tqdm]>=2024.2.0",
|
|
@@ -43,7 +43,7 @@ tests = [
|
|
|
43
43
|
"aioresponses>=0.7,<0.8",
|
|
44
44
|
"paramiko>=3.4.0,<4",
|
|
45
45
|
"pytest>=7,<9",
|
|
46
|
-
"pytest-asyncio>=0.23.2,<
|
|
46
|
+
"pytest-asyncio>=0.23.2,<2",
|
|
47
47
|
"pytest-cov>=4.1.0",
|
|
48
48
|
"pytest-docker>=1,<4",
|
|
49
49
|
"pytest-mock",
|
|
@@ -52,7 +52,7 @@ tests = [
|
|
|
52
52
|
"proxy.py",
|
|
53
53
|
]
|
|
54
54
|
dev = [
|
|
55
|
-
"mypy==1.
|
|
55
|
+
"mypy==1.17.0",
|
|
56
56
|
"scmrepo[tests]",
|
|
57
57
|
"types-certifi",
|
|
58
58
|
"types-mock",
|
|
@@ -147,6 +147,7 @@ ignore = [
|
|
|
147
147
|
"RET503", # implicit-return
|
|
148
148
|
"SIM117", # multiple-with-statements
|
|
149
149
|
"N818", # error-suffix-on-exception-name
|
|
150
|
+
"PLC0415", # import-outside-top-level
|
|
150
151
|
]
|
|
151
152
|
select = [
|
|
152
153
|
"A", # flake8-buitlins
|
|
@@ -6,7 +6,6 @@ import threading
|
|
|
6
6
|
from typing import Any, Optional
|
|
7
7
|
|
|
8
8
|
from fsspec.asyn import ( # noqa: F401, pylint:disable=unused-import
|
|
9
|
-
_selector_policy,
|
|
10
9
|
sync,
|
|
11
10
|
sync_wrapper,
|
|
12
11
|
)
|
|
@@ -23,8 +22,7 @@ def get_loop() -> asyncio.AbstractEventLoop:
|
|
|
23
22
|
if default_loop[0] is None:
|
|
24
23
|
with lock:
|
|
25
24
|
if default_loop[0] is None:
|
|
26
|
-
|
|
27
|
-
default_loop[0] = asyncio.new_event_loop()
|
|
25
|
+
default_loop[0] = asyncio.new_event_loop()
|
|
28
26
|
loop = default_loop[0]
|
|
29
27
|
th = threading.Thread(
|
|
30
28
|
target=loop.run_forever, # type: ignore[attr-defined]
|
|
@@ -10,6 +10,7 @@ from contextlib import contextmanager
|
|
|
10
10
|
from functools import partialmethod
|
|
11
11
|
from typing import (
|
|
12
12
|
TYPE_CHECKING,
|
|
13
|
+
Any,
|
|
13
14
|
Callable,
|
|
14
15
|
ClassVar,
|
|
15
16
|
Optional,
|
|
@@ -295,7 +296,7 @@ class Git(Base):
|
|
|
295
296
|
# See:
|
|
296
297
|
# https://github.com/iterative/dvc/issues/5641
|
|
297
298
|
# https://github.com/iterative/dvc/issues/7458
|
|
298
|
-
def _backend_func(self, name, *args, **kwargs):
|
|
299
|
+
def _backend_func(self, name, *args, **kwargs) -> Any:
|
|
299
300
|
backends: Iterable[str] = kwargs.pop("backends", self.backends)
|
|
300
301
|
for key in backends:
|
|
301
302
|
if self._last_backend is not None and key != self._last_backend:
|
|
@@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
class DulwichObject(GitObject):
|
|
42
|
-
def __init__(self, repo, name, mode, sha):
|
|
42
|
+
def __init__(self, repo, name, mode, sha) -> None:
|
|
43
43
|
self.repo = repo
|
|
44
44
|
self._name = name
|
|
45
45
|
self._mode = mode
|
|
@@ -136,7 +136,7 @@ def _get_ssh_vendor() -> "SSHVendor":
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
class DulwichConfig(Config):
|
|
139
|
-
def __init__(self, config: Union["ConfigFile", "StackedConfig"]):
|
|
139
|
+
def __init__(self, config: Union["ConfigFile", "StackedConfig"]) -> None:
|
|
140
140
|
self._config = config
|
|
141
141
|
|
|
142
142
|
@property
|
|
@@ -183,7 +183,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
183
183
|
|
|
184
184
|
def __init__( # pylint:disable=W0231
|
|
185
185
|
self, root_dir=os.curdir, search_parent_directories=True
|
|
186
|
-
):
|
|
186
|
+
) -> None:
|
|
187
187
|
from dulwich.errors import NotGitRepository
|
|
188
188
|
from dulwich.repo import Repo
|
|
189
189
|
|
|
@@ -990,7 +990,7 @@ def ls_remote(url: str) -> dict[str, str]:
|
|
|
990
990
|
from dulwich.client import HTTPUnauthorized
|
|
991
991
|
|
|
992
992
|
try:
|
|
993
|
-
refs = porcelain.ls_remote(url)
|
|
993
|
+
refs = porcelain.ls_remote(url).refs
|
|
994
994
|
return {os.fsdecode(ref): sha.decode("ascii") for ref, sha in refs.items()}
|
|
995
995
|
except HTTPUnauthorized as exc:
|
|
996
996
|
raise AuthError(url) from exc
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import os
|
|
5
|
-
from collections.abc import Coroutine, Iterator
|
|
5
|
+
from collections.abc import Coroutine, Iterator
|
|
6
6
|
from typing import (
|
|
7
7
|
TYPE_CHECKING,
|
|
8
8
|
Any,
|
|
@@ -18,6 +18,7 @@ from scmrepo.asyn import BaseAsyncObject, sync_wrapper
|
|
|
18
18
|
from scmrepo.exceptions import AuthError
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Sequence
|
|
21
22
|
from pathlib import Path
|
|
22
23
|
|
|
23
24
|
from asyncssh.auth import KbdIntPrompts, KbdIntResponse
|
|
@@ -40,6 +41,12 @@ async def _read_all(read: Callable[[int], Coroutine], n: Optional[int] = None) -
|
|
|
40
41
|
return b"".join(result)
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
async def _getpass(*args, **kwargs) -> str:
|
|
45
|
+
from getpass import getpass
|
|
46
|
+
|
|
47
|
+
return await asyncio.to_thread(getpass, *args, **kwargs)
|
|
48
|
+
|
|
49
|
+
|
|
43
50
|
class _StderrWrapper:
|
|
44
51
|
def __init__(self, stderr: "SSHReader", loop: asyncio.AbstractEventLoop) -> None:
|
|
45
52
|
self.stderr = stderr
|
|
@@ -64,7 +71,9 @@ class _StderrWrapper:
|
|
|
64
71
|
|
|
65
72
|
|
|
66
73
|
class AsyncSSHWrapper(BaseAsyncObject):
|
|
67
|
-
def __init__(
|
|
74
|
+
def __init__(
|
|
75
|
+
self, conn: "SSHClientConnection", proc: "SSHClientProcess", **kwargs
|
|
76
|
+
) -> None:
|
|
68
77
|
super().__init__(**kwargs)
|
|
69
78
|
self.conn: SSHClientConnection = conn
|
|
70
79
|
self.proc: SSHClientProcess = proc
|
|
@@ -97,51 +106,12 @@ class AsyncSSHWrapper(BaseAsyncObject):
|
|
|
97
106
|
close = sync_wrapper(_close)
|
|
98
107
|
|
|
99
108
|
|
|
100
|
-
# NOTE: Github's SSH server does not strictly comply with the SSH protocol.
|
|
101
|
-
# When validating a public key using the rsa-sha2-256 or rsa-sha2-512
|
|
102
|
-
# signature algorithms, RFC4252 + RFC8332 state that the server should respond
|
|
103
|
-
# with the same algorithm in SSH_MSG_USERAUTH_PK_OK. Github's server always
|
|
104
|
-
# returns "ssh-rsa" rather than the correct sha2 algorithm name (likely for
|
|
105
|
-
# backwards compatibility with old SSH client reasons). This behavior causes
|
|
106
|
-
# asyncssh to fail with a key-mismatch error (since asyncssh expects the server
|
|
107
|
-
# to behave properly).
|
|
108
|
-
#
|
|
109
|
-
# See also:
|
|
110
|
-
# https://www.ietf.org/rfc/rfc4252.txt
|
|
111
|
-
# https://www.ietf.org/rfc/rfc8332.txt
|
|
112
|
-
def _process_public_key_ok_gh(self, _pkttype, _pktid, packet):
|
|
113
|
-
from asyncssh.misc import ProtocolError
|
|
114
|
-
|
|
115
|
-
algorithm = packet.get_string()
|
|
116
|
-
key_data = packet.get_string()
|
|
117
|
-
packet.check_end()
|
|
118
|
-
|
|
119
|
-
# pylint: disable=protected-access
|
|
120
|
-
if (
|
|
121
|
-
(
|
|
122
|
-
algorithm == b"ssh-rsa"
|
|
123
|
-
and self._keypair.algorithm
|
|
124
|
-
not in (
|
|
125
|
-
b"ssh-rsa",
|
|
126
|
-
b"rsa-sha2-256",
|
|
127
|
-
b"rsa-sha2-512",
|
|
128
|
-
)
|
|
129
|
-
)
|
|
130
|
-
or (algorithm not in (b"ssh-rsa", self._keypair.algorithm))
|
|
131
|
-
or key_data != self._keypair.public_data
|
|
132
|
-
):
|
|
133
|
-
raise ProtocolError("Key mismatch")
|
|
134
|
-
|
|
135
|
-
self.create_task(self._send_signed_request())
|
|
136
|
-
return True
|
|
137
|
-
|
|
138
|
-
|
|
139
109
|
class InteractiveSSHClient(SSHClient):
|
|
140
110
|
_conn: Optional["SSHClientConnection"] = None
|
|
141
111
|
_keys_to_try: Optional[list["FilePath"]] = None
|
|
142
112
|
_passphrases: dict[str, str]
|
|
143
113
|
|
|
144
|
-
def __init__(self, *args, **kwargs):
|
|
114
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
145
115
|
super(*args, **kwargs)
|
|
146
116
|
self._passphrases: dict[str, str] = {}
|
|
147
117
|
|
|
@@ -171,7 +141,7 @@ class InteractiveSSHClient(SSHClient):
|
|
|
171
141
|
self._keys_to_try = []
|
|
172
142
|
options = self._conn._options # pylint: disable=protected-access
|
|
173
143
|
config = options.config
|
|
174
|
-
client_keys = cast(Sequence[
|
|
144
|
+
client_keys = cast("Sequence[FilePath]", config.get("IdentityFile", ()))
|
|
175
145
|
if not client_keys:
|
|
176
146
|
client_keys = [
|
|
177
147
|
os.path.expanduser(os.path.join("~", ".ssh", path))
|
|
@@ -202,8 +172,6 @@ class InteractiveSSHClient(SSHClient):
|
|
|
202
172
|
return None
|
|
203
173
|
|
|
204
174
|
async def _read_private_key_interactive(self, path: "FilePath") -> "SSHKey":
|
|
205
|
-
from getpass import getpass
|
|
206
|
-
|
|
207
175
|
from asyncssh.public_key import (
|
|
208
176
|
KeyEncryptionError,
|
|
209
177
|
KeyImportError,
|
|
@@ -215,11 +183,8 @@ class InteractiveSSHClient(SSHClient):
|
|
|
215
183
|
if passphrase:
|
|
216
184
|
return read_private_key(path, passphrase=passphrase)
|
|
217
185
|
|
|
218
|
-
loop = asyncio.get_running_loop()
|
|
219
186
|
for _ in range(3):
|
|
220
|
-
passphrase = await
|
|
221
|
-
None, getpass, f"Enter passphrase for key '{path}': "
|
|
222
|
-
)
|
|
187
|
+
passphrase = await _getpass(f"Enter passphrase for key {path!r}: ")
|
|
223
188
|
if passphrase:
|
|
224
189
|
try:
|
|
225
190
|
key = read_private_key(path, passphrase=passphrase)
|
|
@@ -239,23 +204,20 @@ class InteractiveSSHClient(SSHClient):
|
|
|
239
204
|
lang: str,
|
|
240
205
|
prompts: "KbdIntPrompts",
|
|
241
206
|
) -> Optional["KbdIntResponse"]:
|
|
242
|
-
from getpass import getpass
|
|
243
|
-
|
|
244
207
|
if os.environ.get("GIT_TERMINAL_PROMPT") == "0":
|
|
245
208
|
return None
|
|
246
209
|
|
|
247
|
-
def _getpass(prompt: str) -> str:
|
|
248
|
-
return getpass(prompt=prompt).rstrip()
|
|
249
|
-
|
|
250
210
|
if instructions:
|
|
251
211
|
pass
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
|
|
212
|
+
|
|
213
|
+
response: list[str] = []
|
|
214
|
+
for prompt, _echo in prompts:
|
|
215
|
+
p = await _getpass(f"({name}) {prompt}" if name else prompt)
|
|
216
|
+
response.append(p.rstrip())
|
|
217
|
+
return response
|
|
218
|
+
|
|
219
|
+
async def password_auth_requested(self) -> str:
|
|
220
|
+
return await _getpass()
|
|
259
221
|
|
|
260
222
|
|
|
261
223
|
class AsyncSSHVendor(BaseAsyncObject, SSHVendor):
|
|
@@ -286,12 +248,6 @@ class AsyncSSHVendor(BaseAsyncObject, SSHVendor):
|
|
|
286
248
|
key_filename: Optional path to private keyfile
|
|
287
249
|
"""
|
|
288
250
|
import asyncssh
|
|
289
|
-
from asyncssh.auth import MSG_USERAUTH_PK_OK, _ClientPublicKeyAuth
|
|
290
|
-
|
|
291
|
-
# pylint: disable=protected-access
|
|
292
|
-
_ClientPublicKeyAuth._packet_handlers[MSG_USERAUTH_PK_OK] = (
|
|
293
|
-
_process_public_key_ok_gh
|
|
294
|
-
)
|
|
295
251
|
|
|
296
252
|
try:
|
|
297
253
|
conn = await asyncssh.connect(
|
|
@@ -14,7 +14,7 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
|
|
|
14
14
|
password=None,
|
|
15
15
|
config=None,
|
|
16
16
|
**kwargs,
|
|
17
|
-
):
|
|
17
|
+
) -> None:
|
|
18
18
|
super().__init__(
|
|
19
19
|
base_url=base_url,
|
|
20
20
|
username=username,
|
|
@@ -29,6 +29,7 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
|
|
|
29
29
|
url: str,
|
|
30
30
|
headers: Optional[dict[str, str]] = None,
|
|
31
31
|
data: Optional[Union[bytes, Iterator[bytes]]] = None,
|
|
32
|
+
raise_for_status: bool = True,
|
|
32
33
|
):
|
|
33
34
|
cached_chunks: list[bytes] = []
|
|
34
35
|
|
|
@@ -48,7 +49,10 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
|
|
|
48
49
|
|
|
49
50
|
try:
|
|
50
51
|
result = super()._http_request(
|
|
51
|
-
url,
|
|
52
|
+
url,
|
|
53
|
+
headers=headers,
|
|
54
|
+
data=None if data is None else _cached_data(),
|
|
55
|
+
raise_for_status=raise_for_status,
|
|
52
56
|
)
|
|
53
57
|
except HTTPUnauthorized:
|
|
54
58
|
auth_header = self._get_auth()
|
|
@@ -59,7 +63,10 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
|
|
|
59
63
|
else:
|
|
60
64
|
headers = auth_header
|
|
61
65
|
result = super()._http_request(
|
|
62
|
-
url,
|
|
66
|
+
url,
|
|
67
|
+
headers=headers,
|
|
68
|
+
data=None if data is None else _cached_data(),
|
|
69
|
+
raise_for_status=raise_for_status,
|
|
63
70
|
)
|
|
64
71
|
if self._store_credentials is not None:
|
|
65
72
|
self._store_credentials.approve()
|
|
@@ -289,7 +289,9 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
289
289
|
bare = True
|
|
290
290
|
try:
|
|
291
291
|
with RemoteCallbacks(progress=progress) as cb:
|
|
292
|
-
repo = clone_repository(
|
|
292
|
+
repo = clone_repository(
|
|
293
|
+
url, os.fspath(to_path), callbacks=cb, bare=bare
|
|
294
|
+
)
|
|
293
295
|
if mirror:
|
|
294
296
|
cls._set_mirror(repo, progress=progress)
|
|
295
297
|
except GitError as exc:
|
|
@@ -66,6 +66,8 @@ class RemoteCallbacks(_RemoteCallbacks, AbstractContextManager):
|
|
|
66
66
|
else:
|
|
67
67
|
creds = Credential(username=username_from_url, url=url).fill()
|
|
68
68
|
self._store_credentials = creds
|
|
69
|
+
assert creds.username is not None
|
|
70
|
+
assert creds.password is not None
|
|
69
71
|
return UserPass(creds.username, creds.password)
|
|
70
72
|
except CredentialNotFoundError:
|
|
71
73
|
pass
|
|
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
|
|
|
13
13
|
class LFSFilter(Filter):
|
|
14
14
|
attributes = "filter=*"
|
|
15
15
|
|
|
16
|
-
def __init__(self, *args, **kwargs):
|
|
16
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
17
17
|
self._smudge_buf: Optional[io.BytesIO] = None
|
|
18
18
|
self._smudge_root: Optional[str] = None
|
|
19
19
|
|
|
@@ -105,7 +105,7 @@ class GitCredentialHelper(CredentialHelper):
|
|
|
105
105
|
>>> password = credentials.password
|
|
106
106
|
"""
|
|
107
107
|
|
|
108
|
-
def __init__(self, command: str, use_http_path: bool = False):
|
|
108
|
+
def __init__(self, command: str, use_http_path: bool = False) -> None:
|
|
109
109
|
super().__init__()
|
|
110
110
|
self._command = command
|
|
111
111
|
self._run_kwargs: dict[str, Any] = {}
|
|
@@ -136,7 +136,7 @@ class GitCredentialHelper(CredentialHelper):
|
|
|
136
136
|
if not shutil.which(executable) and shutil.which("git"):
|
|
137
137
|
# If the helper cannot be found in PATH, it might be
|
|
138
138
|
# a C git helper in GIT_EXEC_PATH
|
|
139
|
-
git_exec_path = subprocess.check_output(
|
|
139
|
+
git_exec_path = subprocess.check_output(
|
|
140
140
|
("git", "--exec-path"),
|
|
141
141
|
text=True,
|
|
142
142
|
).strip()
|
|
@@ -353,7 +353,7 @@ def _input_tty(prompt: str = "Username: ") -> str:
|
|
|
353
353
|
class MemoryCredentialHelper(CredentialHelper):
|
|
354
354
|
"""Memory credential helper that supports optional interactive input."""
|
|
355
355
|
|
|
356
|
-
def __init__(self):
|
|
356
|
+
def __init__(self) -> None:
|
|
357
357
|
super().__init__()
|
|
358
358
|
self._credentials: dict[_CredentialKey, Credential] = {}
|
|
359
359
|
|
|
@@ -535,7 +535,7 @@ class Credential(Mapping[str, str]):
|
|
|
535
535
|
path: Optional[str] = None,
|
|
536
536
|
username: Optional[str] = None,
|
|
537
537
|
password: Optional[str] = None,
|
|
538
|
-
password_expiry_utc: Optional[
|
|
538
|
+
password_expiry_utc: Optional[str] = None,
|
|
539
539
|
url: Optional[str] = None,
|
|
540
540
|
):
|
|
541
541
|
self.protocol = protocol
|
|
@@ -17,7 +17,7 @@ def smudge(
|
|
|
17
17
|
batch_size: Optional[int] = None,
|
|
18
18
|
) -> BinaryIO:
|
|
19
19
|
"""Wrap the specified binary IO stream and run LFS smudge if necessary."""
|
|
20
|
-
reader = io.BufferedReader(fobj) # type: ignore[
|
|
20
|
+
reader = io.BufferedReader(fobj) # type: ignore[type-var]
|
|
21
21
|
data = reader.peek(100)
|
|
22
22
|
if any(data.startswith(header) for header in HEADERS):
|
|
23
23
|
# read the pointer data into memory since the raw stream is unseekable
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: scmrepo
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.12
|
|
4
4
|
Summary: scmrepo
|
|
5
5
|
Author-email: Iterative <support@dvc.org>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -17,7 +17,7 @@ Requires-Python: >=3.9
|
|
|
17
17
|
Description-Content-Type: text/x-rst
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: gitpython>3
|
|
20
|
-
Requires-Dist: dulwich>=0.
|
|
20
|
+
Requires-Dist: dulwich>=0.23.1
|
|
21
21
|
Requires-Dist: pygit2>=1.14.0
|
|
22
22
|
Requires-Dist: pygtrie>=2.3.2
|
|
23
23
|
Requires-Dist: fsspec[tqdm]>=2024.2.0
|
|
@@ -30,7 +30,7 @@ Provides-Extra: tests
|
|
|
30
30
|
Requires-Dist: aioresponses<0.8,>=0.7; extra == "tests"
|
|
31
31
|
Requires-Dist: paramiko<4,>=3.4.0; extra == "tests"
|
|
32
32
|
Requires-Dist: pytest<9,>=7; extra == "tests"
|
|
33
|
-
Requires-Dist: pytest-asyncio<
|
|
33
|
+
Requires-Dist: pytest-asyncio<2,>=0.23.2; extra == "tests"
|
|
34
34
|
Requires-Dist: pytest-cov>=4.1.0; extra == "tests"
|
|
35
35
|
Requires-Dist: pytest-docker<4,>=1; extra == "tests"
|
|
36
36
|
Requires-Dist: pytest-mock; extra == "tests"
|
|
@@ -38,12 +38,13 @@ Requires-Dist: pytest-sugar; extra == "tests"
|
|
|
38
38
|
Requires-Dist: pytest-test-utils<0.2,>=0.1.0; extra == "tests"
|
|
39
39
|
Requires-Dist: proxy.py; extra == "tests"
|
|
40
40
|
Provides-Extra: dev
|
|
41
|
-
Requires-Dist: mypy==1.
|
|
41
|
+
Requires-Dist: mypy==1.17.0; extra == "dev"
|
|
42
42
|
Requires-Dist: scmrepo[tests]; extra == "dev"
|
|
43
43
|
Requires-Dist: types-certifi; extra == "dev"
|
|
44
44
|
Requires-Dist: types-mock; extra == "dev"
|
|
45
45
|
Requires-Dist: types-paramiko; extra == "dev"
|
|
46
46
|
Requires-Dist: types-tqdm; extra == "dev"
|
|
47
|
+
Dynamic: license-file
|
|
47
48
|
|
|
48
49
|
scmrepo
|
|
49
50
|
=======
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
gitpython>3
|
|
2
|
-
dulwich>=0.
|
|
2
|
+
dulwich>=0.23.1
|
|
3
3
|
pygit2>=1.14.0
|
|
4
4
|
pygtrie>=2.3.2
|
|
5
5
|
fsspec[tqdm]>=2024.2.0
|
|
@@ -10,7 +10,7 @@ aiohttp-retry>=2.5.0
|
|
|
10
10
|
tqdm
|
|
11
11
|
|
|
12
12
|
[dev]
|
|
13
|
-
mypy==1.
|
|
13
|
+
mypy==1.17.0
|
|
14
14
|
scmrepo[tests]
|
|
15
15
|
types-certifi
|
|
16
16
|
types-mock
|
|
@@ -21,7 +21,7 @@ types-tqdm
|
|
|
21
21
|
aioresponses<0.8,>=0.7
|
|
22
22
|
paramiko<4,>=3.4.0
|
|
23
23
|
pytest<9,>=7
|
|
24
|
-
pytest-asyncio<
|
|
24
|
+
pytest-asyncio<2,>=0.23.2
|
|
25
25
|
pytest-cov>=4.1.0
|
|
26
26
|
pytest-docker<4,>=1
|
|
27
27
|
pytest-mock
|
|
@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock
|
|
|
8
8
|
import asyncssh
|
|
9
9
|
import paramiko
|
|
10
10
|
import pytest
|
|
11
|
+
from paramiko.server import InteractiveQuery
|
|
11
12
|
from pytest_mock import MockerFixture
|
|
12
13
|
from pytest_test_utils.waiters import wait_until
|
|
13
14
|
|
|
@@ -52,13 +53,24 @@ class Server(paramiko.ServerInterface):
|
|
|
52
53
|
"""http://docs.paramiko.org/en/2.4/api/server.html."""
|
|
53
54
|
|
|
54
55
|
def __init__(self, commands, *args, **kwargs) -> None:
|
|
55
|
-
super().__init__(
|
|
56
|
+
super().__init__()
|
|
56
57
|
self.commands = commands
|
|
58
|
+
self.allowed_auths = kwargs.get("allowed_auths", "publickey,password")
|
|
57
59
|
|
|
58
60
|
def check_channel_exec_request(self, channel, command):
|
|
59
61
|
self.commands.append(command)
|
|
60
62
|
return True
|
|
61
63
|
|
|
64
|
+
def check_auth_interactive(self, username: str, submethods: str):
|
|
65
|
+
return InteractiveQuery(
|
|
66
|
+
"Password", "Enter the password", f"Password for user {USER}:"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def check_auth_interactive_response(self, responses):
|
|
70
|
+
if responses[0] == PASSWORD:
|
|
71
|
+
return paramiko.AUTH_SUCCESSFUL
|
|
72
|
+
return paramiko.AUTH_FAILED
|
|
73
|
+
|
|
62
74
|
def check_auth_password(self, username, password):
|
|
63
75
|
if username == USER and password == PASSWORD:
|
|
64
76
|
return paramiko.AUTH_SUCCESSFUL
|
|
@@ -76,12 +88,12 @@ class Server(paramiko.ServerInterface):
|
|
|
76
88
|
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
|
|
77
89
|
|
|
78
90
|
def get_allowed_auths(self, username):
|
|
79
|
-
return
|
|
91
|
+
return self.allowed_auths
|
|
80
92
|
|
|
81
93
|
|
|
82
94
|
@pytest.fixture
|
|
83
95
|
def ssh_conn(request: pytest.FixtureRequest) -> dict[str, Any]:
|
|
84
|
-
server = Server([])
|
|
96
|
+
server = Server([], **getattr(request, "param", {}))
|
|
85
97
|
|
|
86
98
|
socket.setdefaulttimeout(10)
|
|
87
99
|
request.addfinalizer(lambda: socket.setdefaulttimeout(None))
|
|
@@ -133,7 +145,8 @@ def test_run_command_password(server: Server, ssh_port: int):
|
|
|
133
145
|
assert b"test_run_command_password" in server.commands
|
|
134
146
|
|
|
135
147
|
|
|
136
|
-
|
|
148
|
+
@pytest.mark.parametrize("ssh_conn", [{"allowed_auths": "publickey"}], indirect=True)
|
|
149
|
+
def test_run_command_no_password(ssh_port: int):
|
|
137
150
|
vendor = AsyncSSHVendor()
|
|
138
151
|
with pytest.raises(AuthError):
|
|
139
152
|
vendor.run_command(
|
|
@@ -145,6 +158,28 @@ def test_run_command_no_password(server: Server, ssh_port: int):
|
|
|
145
158
|
)
|
|
146
159
|
|
|
147
160
|
|
|
161
|
+
@pytest.mark.parametrize(
|
|
162
|
+
"ssh_conn",
|
|
163
|
+
[{"allowed_auths": "password"}, {"allowed_auths": "keyboard-interactive"}],
|
|
164
|
+
indirect=True,
|
|
165
|
+
ids=["password", "interactive"],
|
|
166
|
+
)
|
|
167
|
+
def test_should_prompt_for_password_when_no_password_passed(
|
|
168
|
+
mocker: MockerFixture, server: Server, ssh_port: int
|
|
169
|
+
):
|
|
170
|
+
mocked_getpass = mocker.patch("getpass.getpass", return_value=PASSWORD)
|
|
171
|
+
vendor = AsyncSSHVendor()
|
|
172
|
+
vendor.run_command(
|
|
173
|
+
"127.0.0.1",
|
|
174
|
+
"test_run_command_password",
|
|
175
|
+
username=USER,
|
|
176
|
+
port=ssh_port,
|
|
177
|
+
password=None,
|
|
178
|
+
)
|
|
179
|
+
assert server.commands == [b"test_run_command_password"]
|
|
180
|
+
mocked_getpass.asssert_called_once()
|
|
181
|
+
|
|
182
|
+
|
|
148
183
|
def test_run_command_with_privkey(server: Server, ssh_port: int):
|
|
149
184
|
key = asyncssh.import_private_key(CLIENT_KEY)
|
|
150
185
|
|
|
@@ -212,28 +247,6 @@ def test_run_command_partial_transfer(ssh_port: int, mocker: MockerFixture):
|
|
|
212
247
|
assert mock_stderr.call_count == 3
|
|
213
248
|
|
|
214
249
|
|
|
215
|
-
@pytest.mark.parametrize("algorithm", [b"ssh-rsa", b"rsa-sha2-256", b"rsa-sha2-512"])
|
|
216
|
-
def test_dulwich_github_compat(mocker: MockerFixture, algorithm: bytes):
|
|
217
|
-
from asyncssh.misc import ProtocolError
|
|
218
|
-
|
|
219
|
-
from scmrepo.git.backend.dulwich.asyncssh_vendor import _process_public_key_ok_gh
|
|
220
|
-
|
|
221
|
-
key_data = b"foo"
|
|
222
|
-
auth = mocker.Mock(
|
|
223
|
-
_keypair=mocker.Mock(algorithm=algorithm, public_data=key_data),
|
|
224
|
-
)
|
|
225
|
-
packet = mocker.Mock()
|
|
226
|
-
|
|
227
|
-
strings = iter((b"ed21556", key_data))
|
|
228
|
-
packet.get_string = lambda: next(strings)
|
|
229
|
-
with pytest.raises(ProtocolError):
|
|
230
|
-
_process_public_key_ok_gh(auth, None, None, packet)
|
|
231
|
-
|
|
232
|
-
strings = iter((b"ssh-rsa", key_data))
|
|
233
|
-
packet.get_string = lambda: next(strings)
|
|
234
|
-
_process_public_key_ok_gh(auth, None, None, packet)
|
|
235
|
-
|
|
236
|
-
|
|
237
250
|
@pytest.mark.skipif(os.name != "nt", reason="Windows only")
|
|
238
251
|
def test_git_bash_ssh_vendor(mocker):
|
|
239
252
|
from dulwich.client import SubprocessSSHVendor
|
|
@@ -771,7 +771,7 @@ def test_ignored(tmp_dir: TmpDir, scm: Git, git: Git, git_backend: str):
|
|
|
771
771
|
assert not git.is_ignored(tmp_dir / "dir1" / "file2.txt")
|
|
772
772
|
|
|
773
773
|
|
|
774
|
-
@pytest.mark.skip_git_backend("pygit2", "gitpython")
|
|
774
|
+
@pytest.mark.skip_git_backend("pygit2", "gitpython", "dulwich")
|
|
775
775
|
def test_ignored_dir_unignored_subdirs(tmp_dir: TmpDir, scm: Git, git: Git):
|
|
776
776
|
tmp_dir.gen({".gitignore": "data/**\n!data/**/\n!data/**/*.csv"})
|
|
777
777
|
scm.add([".gitignore"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|