scmrepo 3.3.9__tar.gz → 3.3.11__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.9 → scmrepo-3.3.11}/.github/workflows/tests.yaml +2 -2
- {scmrepo-3.3.9 → scmrepo-3.3.11}/.pre-commit-config.yaml +2 -2
- {scmrepo-3.3.9/src/scmrepo.egg-info → scmrepo-3.3.11}/PKG-INFO +4 -3
- {scmrepo-3.3.9 → scmrepo-3.3.11}/pyproject.toml +1 -1
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/fs.py +0 -31
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/__init__.py +2 -2
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/dulwich/asyncssh_vendor.py +19 -65
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/gitpython.py +1 -1
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/pygit2/__init__.py +4 -2
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/pygit2/callbacks.py +2 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/credentials.py +1 -1
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/fetch.py +1 -1
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/objects.py +1 -1
- {scmrepo-3.3.9 → scmrepo-3.3.11/src/scmrepo.egg-info}/PKG-INFO +4 -3
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo.egg-info/requires.txt +1 -1
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_dulwich.py +39 -26
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_lfs.py +1 -1
- {scmrepo-3.3.9 → scmrepo-3.3.11}/.coveragerc +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/.cruft.json +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/.gitattributes +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/.github/dependabot.yml +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/.github/workflows/release.yaml +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/.github/workflows/update-template.yaml +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/.gitignore +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/CODE_OF_CONDUCT.rst +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/CONTRIBUTING.rst +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/LICENSE +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/README.rst +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/noxfile.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/setup.cfg +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/__init__.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/asyn.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/base.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/exceptions.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/__init__.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/base.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/dulwich/__init__.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/dulwich/client.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/pygit2/filter.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/config.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/__init__.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/client.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/exceptions.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/object.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/pointer.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/progress.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/smudge.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/storage.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/stash.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/noscm.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/progress.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/py.typed +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/urls.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/utils.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo.egg-info/SOURCES.txt +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo.egg-info/dependency_links.txt +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo.egg-info/top_level.txt +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/__init__.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/conftest.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/docker-compose.yml +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/git-init/git.sh +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_credentials.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_fs.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_git.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_noscm.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_pygit2.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_scmrepo.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_stash.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_urls.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/user.key +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/user.key.pub +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/vendor/__init__.py +0 -0
- {scmrepo-3.3.9 → scmrepo-3.3.11}/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@
|
|
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.7
|
|
23
|
+
rev: 'v0.11.7'
|
|
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.
|
|
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.11
|
|
4
4
|
Summary: scmrepo
|
|
5
5
|
Author-email: Iterative <support@dvc.org>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -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.15.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
|
=======
|
|
@@ -3,9 +3,7 @@ import os
|
|
|
3
3
|
import posixpath
|
|
4
4
|
from typing import TYPE_CHECKING, Any, BinaryIO, Callable, Optional
|
|
5
5
|
|
|
6
|
-
from fsspec.callbacks import _DEFAULT_CALLBACK
|
|
7
6
|
from fsspec.spec import AbstractFileSystem
|
|
8
|
-
from fsspec.utils import isfilelike
|
|
9
7
|
|
|
10
8
|
if TYPE_CHECKING:
|
|
11
9
|
from io import BytesIO
|
|
@@ -242,32 +240,3 @@ class GitFileSystem(AbstractFileSystem):
|
|
|
242
240
|
return paths
|
|
243
241
|
|
|
244
242
|
return [self.info(_path) for _path in paths]
|
|
245
|
-
|
|
246
|
-
def get_file(
|
|
247
|
-
self, rpath, lpath, callback=_DEFAULT_CALLBACK, outfile=None, **kwargs
|
|
248
|
-
):
|
|
249
|
-
# NOTE: temporary workaround while waiting for
|
|
250
|
-
# https://github.com/fsspec/filesystem_spec/pull/1191
|
|
251
|
-
|
|
252
|
-
if isfilelike(lpath):
|
|
253
|
-
outfile = lpath
|
|
254
|
-
elif self.isdir(rpath):
|
|
255
|
-
os.makedirs(lpath, exist_ok=True)
|
|
256
|
-
return None
|
|
257
|
-
|
|
258
|
-
with self.open(rpath, "rb", **kwargs) as f1:
|
|
259
|
-
if outfile is None:
|
|
260
|
-
outfile = open(lpath, "wb") # noqa: SIM115
|
|
261
|
-
|
|
262
|
-
try:
|
|
263
|
-
callback.set_size(getattr(f1, "size", None))
|
|
264
|
-
data = True
|
|
265
|
-
while data:
|
|
266
|
-
data = f1.read(self.blocksize)
|
|
267
|
-
segment_len = outfile.write(data)
|
|
268
|
-
if segment_len is None:
|
|
269
|
-
segment_len = len(data)
|
|
270
|
-
callback.relative_update(segment_len)
|
|
271
|
-
finally:
|
|
272
|
-
if not isfilelike(lpath):
|
|
273
|
-
outfile.close()
|
|
@@ -258,7 +258,7 @@ class Git(Base):
|
|
|
258
258
|
self.hooks_dir.mkdir(exist_ok=True)
|
|
259
259
|
hook = self.hooks_dir / name
|
|
260
260
|
|
|
261
|
-
directive = f"#!{shutil.which(interpreter) or '/bin/sh'
|
|
261
|
+
directive = f"#!{shutil.which(interpreter) or '/bin/sh'}"
|
|
262
262
|
hook.write_text(f"{directive}\n{script}\n", encoding="utf-8")
|
|
263
263
|
hook.chmod(0o777)
|
|
264
264
|
|
|
@@ -287,7 +287,7 @@ class Git(Base):
|
|
|
287
287
|
def no_commits(self):
|
|
288
288
|
return not bool(self.get_ref("HEAD"))
|
|
289
289
|
|
|
290
|
-
# Prefer
|
|
290
|
+
# Prefer reusing the most recently used backend when possible. When
|
|
291
291
|
# changing backends (due to unimplemented calls), we close the previous
|
|
292
292
|
# backend to release any open git files/contexts that may cause conflicts
|
|
293
293
|
# with the new backend.
|
|
@@ -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
|
|
@@ -97,45 +104,6 @@ class AsyncSSHWrapper(BaseAsyncObject):
|
|
|
97
104
|
close = sync_wrapper(_close)
|
|
98
105
|
|
|
99
106
|
|
|
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
107
|
class InteractiveSSHClient(SSHClient):
|
|
140
108
|
_conn: Optional["SSHClientConnection"] = None
|
|
141
109
|
_keys_to_try: Optional[list["FilePath"]] = None
|
|
@@ -171,7 +139,7 @@ class InteractiveSSHClient(SSHClient):
|
|
|
171
139
|
self._keys_to_try = []
|
|
172
140
|
options = self._conn._options # pylint: disable=protected-access
|
|
173
141
|
config = options.config
|
|
174
|
-
client_keys = cast(Sequence[
|
|
142
|
+
client_keys = cast("Sequence[FilePath]", config.get("IdentityFile", ()))
|
|
175
143
|
if not client_keys:
|
|
176
144
|
client_keys = [
|
|
177
145
|
os.path.expanduser(os.path.join("~", ".ssh", path))
|
|
@@ -202,8 +170,6 @@ class InteractiveSSHClient(SSHClient):
|
|
|
202
170
|
return None
|
|
203
171
|
|
|
204
172
|
async def _read_private_key_interactive(self, path: "FilePath") -> "SSHKey":
|
|
205
|
-
from getpass import getpass
|
|
206
|
-
|
|
207
173
|
from asyncssh.public_key import (
|
|
208
174
|
KeyEncryptionError,
|
|
209
175
|
KeyImportError,
|
|
@@ -215,11 +181,8 @@ class InteractiveSSHClient(SSHClient):
|
|
|
215
181
|
if passphrase:
|
|
216
182
|
return read_private_key(path, passphrase=passphrase)
|
|
217
183
|
|
|
218
|
-
loop = asyncio.get_running_loop()
|
|
219
184
|
for _ in range(3):
|
|
220
|
-
passphrase = await
|
|
221
|
-
None, getpass, f"Enter passphrase for key '{path}': "
|
|
222
|
-
)
|
|
185
|
+
passphrase = await _getpass(f"Enter passphrase for key {path!r}: ")
|
|
223
186
|
if passphrase:
|
|
224
187
|
try:
|
|
225
188
|
key = read_private_key(path, passphrase=passphrase)
|
|
@@ -239,23 +202,20 @@ class InteractiveSSHClient(SSHClient):
|
|
|
239
202
|
lang: str,
|
|
240
203
|
prompts: "KbdIntPrompts",
|
|
241
204
|
) -> Optional["KbdIntResponse"]:
|
|
242
|
-
from getpass import getpass
|
|
243
|
-
|
|
244
205
|
if os.environ.get("GIT_TERMINAL_PROMPT") == "0":
|
|
245
206
|
return None
|
|
246
207
|
|
|
247
|
-
def _getpass(prompt: str) -> str:
|
|
248
|
-
return getpass(prompt=prompt).rstrip()
|
|
249
|
-
|
|
250
208
|
if instructions:
|
|
251
209
|
pass
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
|
|
210
|
+
|
|
211
|
+
response: list[str] = []
|
|
212
|
+
for prompt, _echo in prompts:
|
|
213
|
+
p = await _getpass(f"({name}) {prompt}" if name else prompt)
|
|
214
|
+
response.append(p.rstrip())
|
|
215
|
+
return response
|
|
216
|
+
|
|
217
|
+
async def password_auth_requested(self) -> str:
|
|
218
|
+
return await _getpass()
|
|
259
219
|
|
|
260
220
|
|
|
261
221
|
class AsyncSSHVendor(BaseAsyncObject, SSHVendor):
|
|
@@ -286,12 +246,6 @@ class AsyncSSHVendor(BaseAsyncObject, SSHVendor):
|
|
|
286
246
|
key_filename: Optional path to private keyfile
|
|
287
247
|
"""
|
|
288
248
|
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
249
|
|
|
296
250
|
try:
|
|
297
251
|
conn = await asyncssh.connect(
|
|
@@ -184,7 +184,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
184
184
|
# In fix_env, we delete LD_LIBRARY_PATH key if it was empty before
|
|
185
185
|
# PyInstaller modified it. GitPython, in git.Repo.clone_from, uses
|
|
186
186
|
# env to update its own internal state. When there is no key in
|
|
187
|
-
# env, this value is not updated and GitPython
|
|
187
|
+
# env, this value is not updated and GitPython reuses
|
|
188
188
|
# LD_LIBRARY_PATH that has been set by PyInstaller.
|
|
189
189
|
# See [1] for more info.
|
|
190
190
|
# [1] https://github.com/gitpython-developers/GitPython/issues/924
|
|
@@ -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:
|
|
@@ -720,7 +722,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
720
722
|
for refname in remote_refs:
|
|
721
723
|
if fnmatch.fnmatch(refname, lh):
|
|
722
724
|
src = refname
|
|
723
|
-
dst = f"{rh_prefix}{refname[len(lh_prefix):]}"
|
|
725
|
+
dst = f"{rh_prefix}{refname[len(lh_prefix) :]}"
|
|
724
726
|
result[dst] = cb.result.get(
|
|
725
727
|
src, _default_status(src, dst, remote_refs)
|
|
726
728
|
)
|
|
@@ -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
|
|
@@ -324,7 +324,7 @@ def _input_tty(prompt: str = "Username: ") -> str:
|
|
|
324
324
|
try:
|
|
325
325
|
fd = os.open(
|
|
326
326
|
"/dev/tty",
|
|
327
|
-
os.O_RDWR | os.O_NOCTTY, #
|
|
327
|
+
os.O_RDWR | os.O_NOCTTY, # type: ignore[attr-defined]
|
|
328
328
|
)
|
|
329
329
|
tty = io.FileIO(fd, "w+")
|
|
330
330
|
stack.enter_context(tty)
|
|
@@ -122,7 +122,7 @@ def _collect_objects(
|
|
|
122
122
|
and (result := _ROOT_PATH_PREFIX_REGEX.match(path := include[0]))
|
|
123
123
|
):
|
|
124
124
|
root = result.group("prefix")
|
|
125
|
-
if path in {root, f
|
|
125
|
+
if path in {root, f"{root.rstrip('/')}/**"}:
|
|
126
126
|
include = []
|
|
127
127
|
else:
|
|
128
128
|
root = "/"
|
|
@@ -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.11
|
|
4
4
|
Summary: scmrepo
|
|
5
5
|
Author-email: Iterative <support@dvc.org>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -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.15.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
|
=======
|
|
@@ -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
|
|
@@ -32,7 +32,7 @@ def storage(tmp_dir_factory: TempDirFactory) -> LFSStorage:
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
@pytest.fixture
|
|
35
|
-
def lfs(tmp_dir: TmpDir, scm: Git) -> None:
|
|
35
|
+
def lfs(tmp_dir: TmpDir, scm: Git) -> None:
|
|
36
36
|
tmp_dir.gen(".gitattributes", "*.lfs filter=lfs diff=lfs merge=lfs -text")
|
|
37
37
|
scm.add([".gitattributes"])
|
|
38
38
|
scm.commit("init lfs attributes")
|
|
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
|
|
File without changes
|
|
File without changes
|