Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/scmrepo/git/backend/dulwich/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,32 @@ def write(self, msg: Union[str, bytes]) -> int:


def _get_ssh_vendor() -> "SSHVendor":
import sys

from dulwich.client import SubprocessSSHVendor

from .asyncssh_vendor import AsyncSSHVendor
from .asyncssh_vendor import AsyncSSHVendor, get_unsupported_opts

ssh_command = os.environ.get("GIT_SSH_COMMAND", os.environ.get("GIT_SSH"))
if ssh_command:
logger.debug("dulwich: Using environment GIT_SSH_COMMAND '%s'", ssh_command)
return SubprocessSSHVendor()

if sys.platform == "win32" and os.environ.get("MSYSTEM"):
# see https://github.com/iterative/dvc/issues/7702
logger.debug(
"dulwich: native win32 Python inside MSYS2/git-bash, using MSYS2 OpenSSH"
)
return SubprocessSSHVendor()

default_config = os.path.expanduser(os.path.join("~", ".ssh", "config"))
unsupported = list(get_unsupported_opts([default_config]))
if unsupported:
logger.debug(
"dulwich: unsupported SSH config option(s) '%s', using system OpenSSH",
", ".join(unsupported),
)
return SubprocessSSHVendor()
return AsyncSSHVendor()


Expand Down
61 changes: 60 additions & 1 deletion src/scmrepo/git/backend/dulwich/asyncssh_vendor.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
"""asyncssh SSH vendor for Dulwich."""
import asyncio
from typing import TYPE_CHECKING, Callable, Coroutine, List, Optional
from typing import (
TYPE_CHECKING,
Callable,
Coroutine,
Iterator,
List,
Optional,
Sequence,
)

from dulwich.client import SSHVendor

from scmrepo.asyn import BaseAsyncObject, sync_wrapper
from scmrepo.exceptions import AuthError

if TYPE_CHECKING:
from pathlib import Path

from asyncssh.config import ConfigPaths, FilePath
from asyncssh.connection import SSHClientConnection
from asyncssh.process import SSHClientProcess
from asyncssh.stream import SSHReader
Expand Down Expand Up @@ -172,3 +183,51 @@ async def _run_command(
return AsyncSSHWrapper(conn, proc)

run_command = sync_wrapper(_run_command)


# class ValidatedSSHClientConfig(SSHClientConfig):
# pass


def get_unsupported_opts(config_paths: "ConfigPaths") -> Iterator[str]:
from pathlib import Path, PurePath

if config_paths:
if isinstance(config_paths, (str, PurePath)):
paths: Sequence["FilePath"] = [config_paths]
else:
paths = config_paths

for path in paths:
try:
yield from _parse_unsupported(Path(path))
except FileNotFoundError:
continue


def _parse_unsupported(path: "Path") -> Iterator[str]:
import locale
import shlex

from asyncssh.config import SSHClientConfig

handlers = SSHClientConfig._handlers # pylint: disable=protected-access
with open(path, encoding=locale.getpreferredencoding()) as fobj:
for line in fobj:
line = line.strip()
if not line or line[0] == "#":
continue

try:
args = shlex.split(line)
except ValueError:
continue

option = args.pop(0)
if option.endswith("="):
option = option[:-1]
elif "=" in option:
option, _ = option.split("=", 1)
loption = option.lower()
if loption not in handlers:
yield loption
41 changes: 41 additions & 0 deletions tests/test_dulwich.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import socket
import threading
from io import StringIO
Expand Down Expand Up @@ -157,3 +158,43 @@ def test_dulwich_github_compat(mocker: MockerFixture, algorithm: bytes):
strings = iter((b"ssh-rsa", key_data))
packet.get_string = lambda: next(strings)
_process_public_key_ok_gh(auth, None, None, packet)


@pytest.mark.skipif(os.name != "nt", reason="Windows only")
def test_git_bash_ssh_vendor(mocker):
from dulwich.client import SubprocessSSHVendor

from scmrepo.git.backend.dulwich import _get_ssh_vendor

mocker.patch.dict(os.environ, {"MSYSTEM": "MINGW64"})
assert isinstance(_get_ssh_vendor(), SubprocessSSHVendor)

del os.environ["MSYSTEM"]
assert isinstance(_get_ssh_vendor(), AsyncSSHVendor)


def test_unsupported_config_ssh_vendor():
from dulwich.client import SubprocessSSHVendor

from scmrepo.git.backend.dulwich import _get_ssh_vendor

config = os.path.expanduser(os.path.join("~", ".ssh", "config"))
os.makedirs(os.path.dirname(config), exist_ok=True)

with open(config, "wb") as fobj:
fobj.write(
b"""
Host *
IdentityFile ~/.ssh/id_rsa
"""
)
assert isinstance(_get_ssh_vendor(), AsyncSSHVendor)

with open(config, "wb") as fobj:
fobj.write(
b"""
Host *
UseKeychain yes
"""
)
assert isinstance(_get_ssh_vendor(), SubprocessSSHVendor)