mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-06-20 23:09:20 +08:00
fix(git_compat): harden pygit2 fallback path; prefer system git when available (#2972)
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
* fix(git_compat): ignore global git config in pygit2 backend Under Desktop 2.0 the launcher sets CM_USE_PYGIT2=1, so the pygit2 backend ran clone_repository/remote.fetch with no credentials callback and honored the user's global git config. An insteadOf rewrite (https->ssh) or credential helper then forced authentication, failing with 'authentication required but no callback set'. Blank the system/global/XDG config search path at import time so libgit2 operations are hermetic, and normalize SSH-form GitHub URLs to anonymous HTTPS on clone and when opening a repo. Amp-Thread-ID: https://ampcode.com/threads/T-019eafa0-16a1-726e-91a4-dac1a40d4481 Co-authored-by: Amp <amp@ampcode.com> * fix(git_compat): preserve corporate http.proxy in pygit2 backend Snapshot http.proxy from the global git config before blanking the config search path, then pass it explicitly (proxy=) to clone_repository and every remote.fetch() in the pygit2 backend, so corporate-lockdown proxy setups keep working after the insteadOf/SSH hardening. Amp-Thread-ID: https://ampcode.com/threads/T-019eafa0-16a1-726e-91a4-dac1a40d4481 Co-authored-by: Amp <amp@ampcode.com> * fix(git_compat): stop rewriting repo remotes on disk under pygit2 backend Removing _normalize_remote_urls(): persistently rewriting a repo's SSH origin to HTTPS mutates on-disk repo state, which is risky if interrupted. The pygit2 backend already neutralizes auth-forcing global config (insteadOf, credential helpers) by blanking libgit2's config search path, so anonymous HTTPS fetch works without touching the stored remote. Manager already prefers the GitPython/system-git backend when a system git is present (which honors the user's full git config including insteadOf https->ssh and proxies), and only uses the bundled pygit2 when system git is absent or CM_USE_PYGIT2=1 is set. Amp-Thread-ID: https://ampcode.com/threads/T-019eafa0-16a1-726e-91a4-dac1a40d4481 Co-authored-by: Amp <amp@ampcode.com> * fix(git_compat): fetch SSH-origin repos via in-memory anonymous HTTPS Consolidate the five pygit2 fetch sites into a single _fetch_remote helper. When a repo's stored origin is SSH-form (git@host:owner/repo), the bundled pygit2 (no SSH transport) would fail with an auth error; fetch through an in-memory anonymous remote over HTTPS instead, leaving .git/config untouched. Also closes a proxy hole where get_remote() exposed remote.fetch without the preserved http.proxy. Validated against both backends (pygit2 1.19.2 + GitPython): all 47 git_compat tests pass; verified create_anonymous fetch updates refs/remotes/origin/* without persisting any remote or rewriting origin. Amp-Thread-ID: https://ampcode.com/threads/T-019eafa0-16a1-726e-91a4-dac1a40d4481 Co-authored-by: Amp <amp@ampcode.com> --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
parent
01799f8cac
commit
622a7077a5
@ -14,11 +14,35 @@ Exports:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
# Snapshot of the user's global `http.proxy` (captured before the config
|
||||||
|
# search path is blanked under the pygit2 backend) so corporate proxy
|
||||||
|
# settings survive and can be passed explicitly to fetch/clone. None means
|
||||||
|
# "no proxy".
|
||||||
|
_HTTP_PROXY = None
|
||||||
|
|
||||||
|
|
||||||
|
def _to_https_url(url):
|
||||||
|
"""Rewrite an SSH-form git URL to its anonymous HTTPS equivalent.
|
||||||
|
|
||||||
|
Handles `git@host:owner/repo(.git)` and `ssh://git@host/owner/repo(.git)`.
|
||||||
|
Returns the URL unchanged if it is not SSH-form, so pygit2 clone/fetch
|
||||||
|
of public repos never requires SSH credentials even when a repo's own
|
||||||
|
config stores an SSH origin.
|
||||||
|
"""
|
||||||
|
if not url:
|
||||||
|
return url
|
||||||
|
m = re.match(r"^(?:ssh://)?git@([^:/]+)[:/](.+)$", url)
|
||||||
|
if m:
|
||||||
|
return "https://%s/%s" % (m.group(1), m.group(2))
|
||||||
|
return url
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Backend selection
|
# Backend selection
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -49,6 +73,41 @@ if USE_PYGIT2:
|
|||||||
# See CVE-2022-24765 for context on this validation.
|
# See CVE-2022-24765 for context on this validation.
|
||||||
_pygit2.option(_pygit2.GIT_OPT_SET_OWNER_VALIDATION, 0)
|
_pygit2.option(_pygit2.GIT_OPT_SET_OWNER_VALIDATION, 0)
|
||||||
|
|
||||||
|
# Snapshot the global http.proxy BEFORE blanking the config search
|
||||||
|
# path, so a corporate proxy survives and can be passed explicitly
|
||||||
|
# to clone/fetch below.
|
||||||
|
try:
|
||||||
|
_global_cfg = _pygit2.Config.get_global_config()
|
||||||
|
try:
|
||||||
|
_HTTP_PROXY = _global_cfg["http.proxy"] or None
|
||||||
|
except (KeyError, Exception):
|
||||||
|
_HTTP_PROXY = None
|
||||||
|
except Exception:
|
||||||
|
_HTTP_PROXY = None
|
||||||
|
|
||||||
|
# Ignore system/global/XDG git config for libgit2 operations. A
|
||||||
|
# user's global config can carry `insteadOf` rewrites (e.g.
|
||||||
|
# https->ssh) or credential helpers that force authentication,
|
||||||
|
# which libgit2 cannot satisfy without a credentials callback
|
||||||
|
# ("authentication required but no callback set"). The bundled
|
||||||
|
# pygit2 has no SSH transport, so an SSH rewrite can never succeed;
|
||||||
|
# blanking the config search path keeps clone/fetch on anonymous
|
||||||
|
# HTTPS.
|
||||||
|
try:
|
||||||
|
from pygit2.enums import ConfigLevel as _ConfigLevel
|
||||||
|
_cfg_levels = [_ConfigLevel.SYSTEM, _ConfigLevel.XDG, _ConfigLevel.GLOBAL]
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
_cfg_levels = [
|
||||||
|
_pygit2.GIT_CONFIG_LEVEL_SYSTEM,
|
||||||
|
_pygit2.GIT_CONFIG_LEVEL_XDG,
|
||||||
|
_pygit2.GIT_CONFIG_LEVEL_GLOBAL,
|
||||||
|
]
|
||||||
|
for _lvl in _cfg_levels:
|
||||||
|
try:
|
||||||
|
_pygit2.settings.search_path[_lvl] = ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if not USE_PYGIT2:
|
if not USE_PYGIT2:
|
||||||
import git as _git
|
import git as _git
|
||||||
|
|
||||||
@ -388,6 +447,28 @@ class _Pygit2Repo(GitRepo):
|
|||||||
self._repo = _pygit2.Repository(git_dir)
|
self._repo = _pygit2.Repository(git_dir)
|
||||||
self._working_dir = repo_path
|
self._working_dir = repo_path
|
||||||
|
|
||||||
|
def _fetch_remote(self, remote, refspecs=None):
|
||||||
|
"""Fetch *remote* over the preserved proxy, transparently rewriting an
|
||||||
|
SSH-form origin to anonymous HTTPS in memory.
|
||||||
|
|
||||||
|
The bundled pygit2 has no SSH transport, so a stored `git@host:...`
|
||||||
|
origin would fail with an auth error. When the URL is SSH-form we fetch
|
||||||
|
through an in-memory anonymous remote over HTTPS, leaving `.git/config`
|
||||||
|
untouched (no on-disk rewrite).
|
||||||
|
"""
|
||||||
|
https_url = _to_https_url(remote.url)
|
||||||
|
if https_url != remote.url:
|
||||||
|
anon = self._repo.remotes.create_anonymous(https_url)
|
||||||
|
anon.fetch(
|
||||||
|
list(refspecs) if refspecs is not None
|
||||||
|
else list(remote.fetch_refspecs),
|
||||||
|
proxy=_HTTP_PROXY,
|
||||||
|
)
|
||||||
|
elif refspecs is not None:
|
||||||
|
remote.fetch(list(refspecs), proxy=_HTTP_PROXY)
|
||||||
|
else:
|
||||||
|
remote.fetch(proxy=_HTTP_PROXY)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def working_dir(self):
|
def working_dir(self):
|
||||||
return self._working_dir
|
return self._working_dir
|
||||||
@ -444,7 +525,7 @@ class _Pygit2Repo(GitRepo):
|
|||||||
remote = self._repo.remotes[name]
|
remote = self._repo.remotes[name]
|
||||||
|
|
||||||
def _pull():
|
def _pull():
|
||||||
remote.fetch()
|
self._fetch_remote(remote)
|
||||||
branch_name = self.active_branch_name
|
branch_name = self.active_branch_name
|
||||||
branch = self._repo.branches.get(branch_name)
|
branch = self._repo.branches.get(branch_name)
|
||||||
if branch and branch.upstream:
|
if branch and branch.upstream:
|
||||||
@ -457,7 +538,7 @@ class _Pygit2Repo(GitRepo):
|
|||||||
branch_ref.set_target(remote_commit.id)
|
branch_ref.set_target(remote_commit.id)
|
||||||
self._repo.head.set_target(remote_commit.id)
|
self._repo.head.set_target(remote_commit.id)
|
||||||
|
|
||||||
return _RemoteProxy(remote.name, remote.url, remote.fetch, _pull)
|
return _RemoteProxy(remote.name, remote.url, lambda: self._fetch_remote(remote), _pull)
|
||||||
|
|
||||||
def has_ref(self, ref_name):
|
def has_ref(self, ref_name):
|
||||||
for prefix in [f'refs/remotes/{ref_name}', f'refs/heads/{ref_name}',
|
for prefix in [f'refs/remotes/{ref_name}', f'refs/heads/{ref_name}',
|
||||||
@ -654,7 +735,7 @@ class _Pygit2Repo(GitRepo):
|
|||||||
raise GitCommandError(f"No upstream for branch '{branch_name}'")
|
raise GitCommandError(f"No upstream for branch '{branch_name}'")
|
||||||
|
|
||||||
remote_name = upstream.remote_name
|
remote_name = upstream.remote_name
|
||||||
self._repo.remotes[remote_name].fetch()
|
self._fetch_remote(self._repo.remotes[remote_name])
|
||||||
|
|
||||||
upstream = self._repo.branches.get(branch_name).upstream
|
upstream = self._repo.branches.get(branch_name).upstream
|
||||||
if upstream is None:
|
if upstream is None:
|
||||||
@ -779,12 +860,12 @@ class _Pygit2Repo(GitRepo):
|
|||||||
|
|
||||||
def fetch_remote_by_index(self, index):
|
def fetch_remote_by_index(self, index):
|
||||||
remotes = list(self._repo.remotes)
|
remotes = list(self._repo.remotes)
|
||||||
remotes[index].fetch()
|
self._fetch_remote(remotes[index])
|
||||||
|
|
||||||
def pull_remote_by_index(self, index):
|
def pull_remote_by_index(self, index):
|
||||||
remotes = list(self._repo.remotes)
|
remotes = list(self._repo.remotes)
|
||||||
remote = remotes[index]
|
remote = remotes[index]
|
||||||
remote.fetch()
|
self._fetch_remote(remote)
|
||||||
# After fetch, try to ff-merge tracking branch
|
# After fetch, try to ff-merge tracking branch
|
||||||
try:
|
try:
|
||||||
branch_name = self.active_branch_name
|
branch_name = self.active_branch_name
|
||||||
@ -824,7 +905,7 @@ def clone_repo(url, dest, progress=None):
|
|||||||
(checkout, clear_cache, close, etc.).
|
(checkout, clear_cache, close, etc.).
|
||||||
"""
|
"""
|
||||||
if USE_PYGIT2:
|
if USE_PYGIT2:
|
||||||
_pygit2.clone_repository(url, dest)
|
_pygit2.clone_repository(_to_https_url(url), dest, proxy=_HTTP_PROXY)
|
||||||
repo = _Pygit2Repo(dest)
|
repo = _Pygit2Repo(dest)
|
||||||
repo.submodule_update()
|
repo.submodule_update()
|
||||||
return repo
|
return repo
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user