Skip to content

Commit 62ac00d

Browse files
committed
dulwich: use system ssh when ssh config contains unsupported options
1 parent b12a9f5 commit 62ac00d

File tree

3 files changed

+98
-3
lines changed

3 files changed

+98
-3
lines changed

src/scmrepo/git/backend/dulwich/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def _get_ssh_vendor() -> "SSHVendor":
102102

103103
from dulwich.client import SubprocessSSHVendor
104104

105-
from .asyncssh_vendor import AsyncSSHVendor
105+
from .asyncssh_vendor import AsyncSSHVendor, get_unsupported_opts
106106

107107
ssh_command = os.environ.get("GIT_SSH_COMMAND", os.environ.get("GIT_SSH"))
108108
if ssh_command:
@@ -115,6 +115,15 @@ def _get_ssh_vendor() -> "SSHVendor":
115115
"dulwich: native win32 Python inside MSYS2/git-bash, using MSYS2 OpenSSH"
116116
)
117117
return SubprocessSSHVendor()
118+
119+
default_config = os.path.expanduser(os.path.join("~", ".ssh", "config"))
120+
unsupported = list(get_unsupported_opts([default_config]))
121+
if unsupported:
122+
logger.debug(
123+
"dulwich: unsupported SSH config option(s) '%s', using system OpenSSH",
124+
", ".join(unsupported),
125+
)
126+
return SubprocessSSHVendor()
118127
return AsyncSSHVendor()
119128

120129

src/scmrepo/git/backend/dulwich/asyncssh_vendor.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
"""asyncssh SSH vendor for Dulwich."""
22
import asyncio
3-
from typing import TYPE_CHECKING, Callable, Coroutine, List, Optional
3+
from typing import (
4+
TYPE_CHECKING,
5+
Callable,
6+
Coroutine,
7+
Iterator,
8+
List,
9+
Optional,
10+
Sequence,
11+
)
412

513
from dulwich.client import SSHVendor
614

715
from scmrepo.asyn import BaseAsyncObject, sync_wrapper
816
from scmrepo.exceptions import AuthError
917

1018
if TYPE_CHECKING:
19+
from pathlib import Path
20+
21+
from asyncssh.config import ConfigPaths, FilePath
1122
from asyncssh.connection import SSHClientConnection
1223
from asyncssh.process import SSHClientProcess
1324
from asyncssh.stream import SSHReader
@@ -172,3 +183,51 @@ async def _run_command(
172183
return AsyncSSHWrapper(conn, proc)
173184

174185
run_command = sync_wrapper(_run_command)
186+
187+
188+
# class ValidatedSSHClientConfig(SSHClientConfig):
189+
# pass
190+
191+
192+
def get_unsupported_opts(config_paths: "ConfigPaths") -> Iterator[str]:
193+
from pathlib import Path, PurePath
194+
195+
if config_paths:
196+
if isinstance(config_paths, (str, PurePath)):
197+
paths: Sequence["FilePath"] = [config_paths]
198+
else:
199+
paths = config_paths
200+
201+
for path in paths:
202+
try:
203+
yield from _parse_unsupported(Path(path))
204+
except FileNotFoundError:
205+
continue
206+
207+
208+
def _parse_unsupported(path: "Path") -> Iterator[str]:
209+
import locale
210+
import shlex
211+
212+
from asyncssh.config import SSHClientConfig
213+
214+
handlers = SSHClientConfig._handlers # pylint: disable=protected-access
215+
with open(path, encoding=locale.getpreferredencoding()) as fobj:
216+
for line in fobj:
217+
line = line.strip()
218+
if not line or line[0] == "#":
219+
continue
220+
221+
try:
222+
args = shlex.split(line)
223+
except ValueError:
224+
continue
225+
226+
option = args.pop(0)
227+
if option.endswith("="):
228+
option = option[:-1]
229+
elif "=" in option:
230+
option, _ = option.split("=", 1)
231+
loption = option.lower()
232+
if loption not in handlers:
233+
yield loption

tests/test_dulwich.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,5 +169,32 @@ def test_git_bash_ssh_vendor(mocker):
169169
mocker.patch.dict(os.environ, {"MSYSTEM": "MINGW64"})
170170
assert isinstance(_get_ssh_vendor(), SubprocessSSHVendor)
171171

172-
mocker.patch.dict(os.environ, {"MSYSTEM": None})
172+
del os.environ["MSYSTEM"]
173173
assert isinstance(_get_ssh_vendor(), AsyncSSHVendor)
174+
175+
176+
def test_unsupported_config_ssh_vendor():
177+
from dulwich.client import SubprocessSSHVendor
178+
179+
from scmrepo.git.backend.dulwich import _get_ssh_vendor
180+
181+
config = os.path.expanduser(os.path.join("~", ".ssh", "config"))
182+
os.makedirs(os.path.dirname(config), exist_ok=True)
183+
184+
with open(config, "wb") as fobj:
185+
fobj.write(
186+
b"""
187+
Host *
188+
IdentityFile ~/.ssh/id_rsa
189+
"""
190+
)
191+
assert isinstance(_get_ssh_vendor(), AsyncSSHVendor)
192+
193+
with open(config, "wb") as fobj:
194+
fobj.write(
195+
b"""
196+
Host *
197+
UseKeychain yes
198+
"""
199+
)
200+
assert isinstance(_get_ssh_vendor(), SubprocessSSHVendor)

0 commit comments

Comments
 (0)