事件复盘:通过 SSH 从 Windows 服务器 clone 本地仓库失败("does not appear to be a git repository")并修复

日期:2026-01-02 — 2026-01-03
环境概述:Windows Server(OpenSSH for Windows),Git for Windows,仓库位于 D:\McServer...\plugins。通过 SSH(非标准端口 8307)从远端 clone 时出现 does not appear to be a git repository。本地可通过 VSCode Remote SSH 在服务器上直接使用该仓库(说明仓库存在且可用),但使用 git clone 失败。


一、问题摘要(Summary)


二、根因分析(Root cause)

  1. Git 客户端在通过 SSH 向服务器请求时,会在远端执行类似:
    git-upload-pack '/D:/path/to/repo'
    
    注意:git 客户端在构造远端命令时为路径加了单引号和在 Windows 驱动器路径前出现了前导斜杠(/D:/...'D:/...'),这在 Linux shell 下是正常的,但在 Windows 默认 shell(cmd.exe)下参数解析与 nix 不同。
  2. 在 Windows 的默认远端 shell(早期行为通常是 cmd)里,单引号不被当作字符串分隔符,且命令参数解析会导致传给 git-upload-pack 的字符串不被识别为有效目录(例如前导斜杠或额外的引号导致 Git 找不到路径)。
  3. 导致结果:git-upload-pack 收到不正确/额外参数,返回 usage 并退出;因此 git clone 失败。
  4. 另外,where git 在非交互 SSH 会话中可能会“卡”或延迟,原因通常是 PATH 中存在不可达或慢响应的条目(网络路径等);但本事件的主因并非 git 二进制不可用(已通过 git --version 验证)。

三、关键诊断步骤(Timeline & Commands)

(这些步骤也是复盘中实际运行过的命令 —— 可直接复用做验证)

  1. 本地用非交互 SSH 验证远端能运行 git:
ssh -p 8307 Administrator@mc.5201314.vin "git --version"
  1. 在服务器仓库目录确认工作树:
cd /d "D:\McServer\...path...\plugins"
dir ".git" /a
git rev-parse --is-inside-work-tree
git rev-parse --show-toplevel
git status
  1. 在本地测试远端引用(最先简单读 refs):
ssh -p 8307 Administrator@mc.5201314.vin 'git ls-remote "D:/McServer/.../plugins"'
  1. 观察 git clone 失败时 ssh 发送的远端命令(启用 SSH 调试):
$env:GIT_SSH_COMMAND = 'ssh -vvv -p 8307'
git clone "ssh://Administrator@mc.5201314.vin/D:/.../plugins" plugins-debug
  1. 为诊断创建了可记录接收参数的 wrapper(写到 C:\Windows\System32\OpenSSH\git-upload-pack.cmd),并触发远端 git-upload-pack 来记录 wrapper 收到的 FULL ARGSARG1 RAW 等信息,从而确认传参格式。

诊断 wrapper(写入服务器)示例(最终用来观察的内容)记录日志到 C:\Temp\git-upload-pack-args.log


四、采取的修复措施(What we changed)

两条思路都做了考虑(根本与临时):

A. 最终短期/快速修复(已部署并验证成功)

修复脚本(在服务器以管理员 PowerShell 写入):

$wrapperPath = 'C:\Windows\System32\OpenSSH\git-upload-pack.cmd'
$script = @'
@echo off
if "%~1"=="" (
  "C:\Program Files\Git\cmd\git.exe" upload-pack
  exit /b %ERRORLEVEL%
)
set FIRST=%~1
if "%FIRST:~0,1%"=="'" set FIRST=%FIRST:~1%
if "%FIRST:~-1%"=="'" set FIRST=%FIRST:~0,-1%
if "%FIRST:~0,1%"=="/" set FIRST=%FIRST:~1%
"C:\Program Files\Git\cmd\git.exe" upload-pack "%FIRST%"
exit /b %ERRORLEVEL%
'@
Set-Content -Path $wrapperPath -Value $script -Encoding ASCII -Force
icacls $wrapperPath /grant 'Administrators:RX'

B. 根本/长期建议(可选)

New-Item -Path 'HKLM:\SOFTWARE\OpenSSH' -Force | Out-Null
Set-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
Restart-Service sshd
mkdir D:\gitrepos
git clone --bare "D:\McServer\...plugins" "D:\gitrepos\plugins.git"
# 本地 clone:
git clone "ssh://Administrator@mc.5201314.vin:8307/D:/gitrepos/plugins.git" plugins-local

C. 权限/路径检查(用于排除权限问题)

icacls "D:\McServer\...plugins"
icacls "C:\Windows\System32\OpenSSH\git-upload-pack.cmd"

五、验证(How we verified)

$env:GIT_SSH_COMMAND = 'ssh -p 8307'
git clone "Administrator@mc.5201314.vin:D:/McServer/.../plugins" plugins-local
Remove-Item Env:GIT_SSH_COMMAND

六、归纳的排查/自助修复流程(可复用步骤)

当再次遇到类似问题,可按下列顺序排查:

  1. 基本确认(服务器端)

    • 在仓库目录执行:
      git rev-parse --is-inside-work-tree
      git status
      dir .git /a
    • 在服务器交互 shell(或 VSCode Remote)能正常运行说明仓库存在。
  2. 验证非交互环境能否运行 git

    • 本地:
      ssh -p <port> user@host "git --version"
      ssh -p <port> user@host 'powershell -NoProfile -Command "(Get-Command git).Source"'
    • git --version 报错或找不到 git:检查系统 PATH(机器层 vs 用户层),并重启 sshd 服务(或把 Git 路径写入机器 PATH 并重启 sshd)。
  3. 测试远端引用(轻量)

    • 本地运行:
      ssh -p <port> user@host 'git ls-remote "D:/path/to/repo"'
    • 若可列出 refs,说明 git-upload-pack 在远端能工作(可以进一步 debug clone 阶段的 quoting/参数问题)。
  4. 打开详细日志进行定位

    • 在本地设置详细 ssh/git trace:
      $env:GIT_SSH_COMMAND='ssh -vvv -p <port>'
      $env:GIT_TRACE=1
      $env:GIT_TRACE_PACKET=1
      git clone "ssh://user@host/D:/path/to/repo" repo-debug
    • 查看 ssh 日志中 “Sending command:” 行,确定远端运行的 git-upload-pack 的参数格式(是否带前导斜杠或单引号)。
  5. 如果参数有问题(如 /D:/... 或带单引号),优先考虑:

    • 临时修复:在服务器上放 git-upload-pack wrapper(如本文所示)以规范化参数;
    • 长期修复:把 DefaultShell 改为 PowerShell(写入注册表并重启 sshd);
    • 或者创建裸仓库,客户端直接 clone 裸仓库。
  6. 权限检查

    • 若出现 Permission denied (publickey) 或 NTFS 访问错误,使用 icacls 检查权限并修复。
  7. 回滚/恢复

    • 在替换 wrapper 前备份原文件(例如 .bak),若新 wrapper 有问题,可恢复备份。

七、最终建议(Best practices)


八、附录:关键命令/脚本(已实际使用)

$wrapperPath = 'C:\Windows\System32\OpenSSH\git-upload-pack.cmd'
$script = @'
@echo off
if "%~1"=="" (
  "C:\Program Files\Git\cmd\git.exe" upload-pack
  exit /b %ERRORLEVEL%
)
set FIRST=%~1
if "%FIRST:~0,1%"=="'" set FIRST=%FIRST:~1%
if "%FIRST:~-1%"=="'" set FIRST=%FIRST:~0,-1%
if "%FIRST:~0,1%"=="/" set FIRST=%FIRST:~1%
"C:\Program Files\Git\cmd\git.exe" upload-pack "%FIRST%"
exit /b %ERRORLEVEL%
'@
Set-Content -Path $wrapperPath -Value $script -Encoding ASCII -Force
icacls $wrapperPath /grant 'Administrators:RX'
New-Item -Path 'HKLM:\SOFTWARE\OpenSSH' -Force | Out-Null
Set-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
Restart-Service sshd
$env:GIT_SSH_COMMAND = 'ssh -p 8307'
git clone "Administrator@mc.5201314.vin:D:/McServer/.../plugins" plugins-local
Remove-Item Env:GIT_SSH_COMMAND
mkdir D:\gitrepos
git clone --bare "D:\McServer\...plugins" "D:\gitrepos\plugins.git"
# 然后本地:
git clone "ssh://Administrator@mc.5201314.vin:8307/D:/gitrepos/plugins.git" plugins-local

九、结论


如果你愿意,我可以:

你希望我接着做哪项(脚本包 / 存档到 repo / 审批清单 / 其他)?