Merge pull request #5526 from nesquena/release/stage-5522
Release — Windows git.exe resolution for version/cache-busting (#5522)
This commit is contained in:
commit
edfccb9cd4
3 changed files with 204 additions and 0 deletions
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- **Windows: after an upgrade, browsers no longer keep serving stale JS/CSS when git isn't on the server's PATH.** When the WebUI server was launched on Windows from an environment without git on `PATH` (a venv, a service wrapper, a non-interactive launcher), version detection silently degraded to `unknown`, which froze the `?v=<stamp>` static-asset cache key — so browsers kept serving cached `ui.js` / `messages.js` / CSS even after the server restarted with fixed code, and the only workaround was a manual hard-refresh on every device. Version detection now resolves `git.exe` from the Git-for-Windows registry key (and common install paths) when it isn't on `PATH`, so the cache stamp updates and clients pick up frontend fixes automatically. Non-Windows behavior is unchanged. Thanks @allenliang2022. (#5522)
|
||||
- **Transparent Stream is smoother during streaming — less row churn and no thinking-block scrollbar flicker.** Building on the identity-preserving live-row reconcile, the renderer now skips redundant work while a response streams: a preserved row's markup isn't rewritten when the incoming HTML is unchanged, a row isn't reinserted when it's already in the correct position, and long thinking blocks no longer flicker an internal scrollbar as their scroll container appears. Purely a reduction of no-op DOM work — what renders is unchanged, and matching rows still preserve their DOM node, copy button, tool-call data, and expanded/collapsed state. Thanks @Stacey2911. (#5456, #5367)
|
||||
- **Models from a user-defined provider in `config.yaml` now route correctly.** If you defined a custom provider under the `providers:` block with an explicit `models:` allowlist, a bare model id from that list wasn't matched by request-time routing — it silently fell back to the default provider and often 404'd. Resolution now scans your `providers:` allowlists (exact match, same as `custom_providers:`), so those models reach the right endpoint. Copilot is deliberately excluded from this scan because `providers.copilot.models` is a per-model settings map (reasoning effort, limits), not a routing allowlist. Thanks @akay64. (#5511)
|
||||
- **The transcript no longer scrolls up when the composer grows.** When you were pinned to the bottom of a conversation and the composer got taller — multi-line typing, `Shift+Enter`, or a multi-line / dictation paste — the transcript viewport shrank by the same amount and left you stranded a row or two above the bottom (it read as the chat "randomly scrolling up" during normal use). The transcript now re-pins to the bottom whenever the composer grows, but only when you were genuinely still pinned there — if you'd scrolled up to read history, nothing yanks you back down. (#5516, #5514, #5515)
|
||||
|
|
|
|||
|
|
@ -214,12 +214,60 @@ def _run_git(args, cwd, timeout=10):
|
|||
return f'git failed to start: {exc}', False
|
||||
|
||||
|
||||
def _windows_git_from_registry():
|
||||
"""Best-effort resolve git.exe from the Git-for-Windows registry key.
|
||||
|
||||
Git for Windows records its install root at
|
||||
``HKLM\\SOFTWARE\\GitForWindows\\InstallPath`` (and the WOW6432Node mirror
|
||||
for a 32-bit install on 64-bit Windows). ``git.exe`` lives under
|
||||
``<InstallPath>\\cmd\\git.exe``. This is the reliable way to find git when
|
||||
it is installed but NOT on the launching process's PATH — e.g. the WebUI
|
||||
server started from a venv python whose environment does not inherit the
|
||||
interactive shell PATH, which otherwise degrades WEBUI_VERSION to
|
||||
``'unknown'`` and freezes the ``?v=`` static-asset cache-busting stamp.
|
||||
"""
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
return None
|
||||
for hive, flag in (
|
||||
(winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_64KEY),
|
||||
(winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_32KEY),
|
||||
(winreg.HKEY_CURRENT_USER, 0),
|
||||
):
|
||||
try:
|
||||
with winreg.OpenKey(
|
||||
hive, r'SOFTWARE\GitForWindows', 0,
|
||||
winreg.KEY_READ | flag,
|
||||
) as key:
|
||||
install_path, _ = winreg.QueryValueEx(key, 'InstallPath')
|
||||
except OSError:
|
||||
continue
|
||||
if not install_path:
|
||||
continue
|
||||
candidate = os.path.join(install_path, 'cmd', 'git.exe')
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_git_executable():
|
||||
git_executable = shutil.which('git')
|
||||
if git_executable:
|
||||
return git_executable
|
||||
if sys.platform == 'darwin' and os.path.exists('/usr/bin/git'):
|
||||
return '/usr/bin/git'
|
||||
if sys.platform == 'win32':
|
||||
from_registry = _windows_git_from_registry()
|
||||
if from_registry:
|
||||
return from_registry
|
||||
for candidate in (
|
||||
os.path.expandvars(r'%ProgramFiles%\Git\cmd\git.exe'),
|
||||
os.path.expandvars(r'%ProgramFiles(x86)%\Git\cmd\git.exe'),
|
||||
os.path.expandvars(r'%LocalAppData%\Programs\Git\cmd\git.exe'),
|
||||
):
|
||||
if candidate and os.path.exists(candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
155
tests/test_issue_windows_git_version_detection.py
Normal file
155
tests/test_issue_windows_git_version_detection.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
"""Windows parallel to issue #5175 (macOS launchd git-on-PATH miss).
|
||||
|
||||
When the source WebUI server is launched from a Python environment whose PATH
|
||||
does not include git (e.g. a venv on Windows, mirroring the macOS launchd case),
|
||||
``shutil.which('git')`` returns None. Before this fix ``_resolve_git_executable``
|
||||
had only a darwin fallback, so on Windows it returned None -> ``git describe``
|
||||
never ran -> ``WEBUI_VERSION`` degraded to ``'unknown'`` -> the ``?v=<stamp>``
|
||||
static-asset cache key froze, so browsers kept serving stale cached JS/CSS even
|
||||
after the server restarted with fixed code.
|
||||
|
||||
The fix adds a Windows fallback that resolves git.exe from the Git-for-Windows
|
||||
registry ``InstallPath`` (and common install dirs) without relying on PATH.
|
||||
|
||||
These tests mock the OS-specific surfaces (winreg, os.path.exists,
|
||||
os.path.expandvars) so they exercise the Windows code path deterministically on
|
||||
ANY host — the CI runners are Linux, where %VAR% expansion and native path
|
||||
separators differ, so nothing here may depend on the running platform's real
|
||||
environment or filesystem.
|
||||
"""
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import api.updates as updates
|
||||
|
||||
# Deterministic install root used by the winreg stub. Kept as a forward-slash
|
||||
# string so the expected git path is identical regardless of the host's
|
||||
# os.path.join separator (CI is Linux; dev is Windows).
|
||||
FAKE_GIT_ROOT = 'C:/Program Files/Git'
|
||||
FAKE_GIT_EXE = FAKE_GIT_ROOT + '/cmd/git.exe'
|
||||
|
||||
|
||||
def _fake_winreg(install_path=FAKE_GIT_ROOT):
|
||||
"""Minimal winreg stub whose OpenKey/QueryValueEx yield a Git InstallPath."""
|
||||
mod = MagicMock()
|
||||
mod.HKEY_LOCAL_MACHINE = 0
|
||||
mod.HKEY_CURRENT_USER = 1
|
||||
mod.KEY_READ = 0x20019
|
||||
mod.KEY_WOW64_64KEY = 0x0100
|
||||
mod.KEY_WOW64_32KEY = 0x0200
|
||||
|
||||
class _Key:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
mod.OpenKey.return_value = _Key()
|
||||
mod.QueryValueEx.return_value = (install_path, 1)
|
||||
return mod
|
||||
|
||||
|
||||
def _patch_join_forward_slash():
|
||||
"""Make os.path.join deterministic (forward slash) regardless of host OS.
|
||||
|
||||
The production code builds candidates with os.path.join; on Linux CI that
|
||||
uses '/', on Windows '\\'. Pin it so expected paths match on both.
|
||||
"""
|
||||
def fake_join(*parts):
|
||||
return '/'.join(p.rstrip('/\\') for p in parts)
|
||||
return patch.object(updates.os.path, 'join', side_effect=fake_join)
|
||||
|
||||
|
||||
def test_resolve_git_executable_uses_registry_on_windows():
|
||||
winreg = _fake_winreg()
|
||||
|
||||
with patch.object(updates.shutil, 'which', return_value=None), \
|
||||
patch.object(sys, 'platform', 'win32'), \
|
||||
patch.dict('sys.modules', {'winreg': winreg}), \
|
||||
_patch_join_forward_slash(), \
|
||||
patch.object(updates.os.path, 'exists', side_effect=lambda p: p == FAKE_GIT_EXE):
|
||||
resolved = updates._resolve_git_executable()
|
||||
|
||||
assert resolved == FAKE_GIT_EXE
|
||||
|
||||
|
||||
def test_resolve_git_executable_probes_known_dirs_when_registry_absent():
|
||||
# Registry has no value -> QueryValueEx raises -> fall through to dir probe.
|
||||
winreg = _fake_winreg()
|
||||
winreg.QueryValueEx.side_effect = OSError('no InstallPath')
|
||||
|
||||
# The code probes os.path.expandvars('%ProgramFiles%\\Git\\cmd\\git.exe');
|
||||
# mock expandvars deterministically so the test does not depend on the
|
||||
# host's %VAR% expansion (Linux CI does not expand %ProgramFiles%).
|
||||
probed = 'C:/Program Files/Git/cmd/git.exe'
|
||||
|
||||
def fake_expandvars(s):
|
||||
if 'ProgramFiles%' in s and '(x86)' not in s:
|
||||
return probed
|
||||
# Other candidates resolve to a sentinel that never "exists".
|
||||
return '/nonexistent/' + s
|
||||
|
||||
with patch.object(updates.shutil, 'which', return_value=None), \
|
||||
patch.object(sys, 'platform', 'win32'), \
|
||||
patch.dict('sys.modules', {'winreg': winreg}), \
|
||||
patch.object(updates.os.path, 'expandvars', side_effect=fake_expandvars), \
|
||||
patch.object(updates.os.path, 'exists', side_effect=lambda p: p == probed):
|
||||
resolved = updates._resolve_git_executable()
|
||||
|
||||
assert resolved == probed
|
||||
|
||||
|
||||
def test_resolve_git_executable_returns_none_on_windows_when_git_truly_absent():
|
||||
winreg = _fake_winreg()
|
||||
winreg.QueryValueEx.side_effect = OSError('no InstallPath')
|
||||
|
||||
with patch.object(updates.shutil, 'which', return_value=None), \
|
||||
patch.object(sys, 'platform', 'win32'), \
|
||||
patch.dict('sys.modules', {'winreg': winreg}), \
|
||||
patch.object(updates.os.path, 'expandvars', side_effect=lambda s: s), \
|
||||
patch.object(updates.os.path, 'exists', return_value=False):
|
||||
resolved = updates._resolve_git_executable()
|
||||
|
||||
assert resolved is None
|
||||
|
||||
|
||||
def test_non_windows_never_touches_registry_fallback():
|
||||
"""Falsifiable guard: the registry path must be Windows-only."""
|
||||
winreg = _fake_winreg()
|
||||
|
||||
with patch.object(updates.shutil, 'which', return_value=None), \
|
||||
patch.object(sys, 'platform', 'linux'), \
|
||||
patch.dict('sys.modules', {'winreg': winreg}), \
|
||||
patch.object(updates.os.path, 'exists', return_value=True):
|
||||
resolved = updates._resolve_git_executable()
|
||||
|
||||
assert resolved is None
|
||||
winreg.OpenKey.assert_not_called()
|
||||
|
||||
|
||||
def test_detect_webui_version_recovers_via_windows_registry_fallback(tmp_path):
|
||||
winreg = _fake_winreg()
|
||||
|
||||
def fake_exists(p):
|
||||
# git.exe resolves; no api/_version.py fallback file present.
|
||||
return p == FAKE_GIT_EXE
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
assert cmd[0] == FAKE_GIT_EXE
|
||||
if cmd[1:] == ['describe', '--tags', '--always']:
|
||||
return MagicMock(returncode=0, stdout='v0.51.999\n', stderr='')
|
||||
if cmd[1:] == ['diff-index', '--quiet', 'HEAD', '--']:
|
||||
return MagicMock(returncode=0, stdout='', stderr='')
|
||||
raise AssertionError(f'unexpected git args: {cmd[1:]!r}')
|
||||
|
||||
with patch.object(updates.shutil, 'which', return_value=None), \
|
||||
patch.object(sys, 'platform', 'win32'), \
|
||||
patch.dict('sys.modules', {'winreg': winreg}), \
|
||||
_patch_join_forward_slash(), \
|
||||
patch.object(updates.os.path, 'exists', side_effect=fake_exists), \
|
||||
patch.object(updates, 'REPO_ROOT', tmp_path), \
|
||||
patch.object(updates.subprocess, 'run', side_effect=fake_run):
|
||||
version = updates._detect_webui_version()
|
||||
|
||||
assert version == 'v0.51.999'
|
||||
Loading…
Reference in a new issue