fix(updates): resolve git under macOS launchd (#5175)
This commit is contained in:
parent
c35835d8ba
commit
a052f390f8
2 changed files with 148 additions and 1 deletions
|
|
@ -15,6 +15,7 @@ import os
|
|||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
|
|
@ -187,9 +188,12 @@ def _run_git(args, cwd, timeout=10):
|
|||
On failure, returns stderr (or stdout as fallback) so callers can
|
||||
surface actionable git error messages instead of empty strings.
|
||||
"""
|
||||
git_executable = _resolve_git_executable()
|
||||
if not git_executable:
|
||||
return 'git executable not found', False
|
||||
try:
|
||||
r = subprocess.run(
|
||||
['git'] + args, cwd=str(cwd), capture_output=True,
|
||||
[git_executable] + args, cwd=str(cwd), capture_output=True,
|
||||
text=True, timeout=timeout,
|
||||
encoding='utf-8', errors='replace',
|
||||
)
|
||||
|
|
@ -210,6 +214,15 @@ def _run_git(args, cwd, timeout=10):
|
|||
return f'git failed to start: {exc}', False
|
||||
|
||||
|
||||
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'
|
||||
return None
|
||||
|
||||
|
||||
def _dirty_suffix(path: Path, timeout=1) -> str:
|
||||
"""Return a best-effort ``-dirty`` suffix without blocking version display."""
|
||||
out, ok = _run_git(['diff-index', '--quiet', 'HEAD', '--'], path, timeout=timeout)
|
||||
|
|
|
|||
134
tests/test_issue5175_macos_launchd_git.py
Normal file
134
tests/test_issue5175_macos_launchd_git.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import api.updates as updates
|
||||
|
||||
|
||||
def test_run_git_uses_which_result_when_available(tmp_path):
|
||||
with patch.object(updates.shutil, 'which', return_value='C:/Tools/git.exe'), \
|
||||
patch.object(updates.subprocess, 'run') as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout='v0.51.999\n', stderr='')
|
||||
|
||||
out, ok = updates._run_git(['describe', '--tags'], tmp_path)
|
||||
|
||||
assert ok is True
|
||||
assert out == 'v0.51.999'
|
||||
assert mock_run.call_args.args[0][0] == 'C:/Tools/git.exe'
|
||||
|
||||
|
||||
def test_run_git_falls_back_to_usr_bin_git_on_darwin(tmp_path):
|
||||
def fake_run(cmd, **kwargs):
|
||||
if cmd[0] == 'git':
|
||||
raise FileNotFoundError
|
||||
if cmd[0] != '/usr/bin/git':
|
||||
raise AssertionError(f'unexpected executable: {cmd[0]!r}')
|
||||
return MagicMock(returncode=0, stdout='v0.51.999\n', stderr='')
|
||||
|
||||
with patch.object(updates.shutil, 'which', return_value=None), \
|
||||
patch.object(sys, 'platform', 'darwin'), \
|
||||
patch.object(updates.os.path, 'exists', return_value=True), \
|
||||
patch.object(updates.subprocess, 'run', side_effect=fake_run):
|
||||
out, ok = updates._run_git(['describe', '--tags'], tmp_path)
|
||||
|
||||
assert ok is True
|
||||
assert out == 'v0.51.999'
|
||||
|
||||
|
||||
def test_run_git_returns_not_found_when_no_executable(tmp_path):
|
||||
calls = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
calls.append(cmd[0])
|
||||
if cmd[0] == '/usr/bin/git':
|
||||
raise AssertionError('non-macOS path miss must not use /usr/bin/git')
|
||||
raise FileNotFoundError
|
||||
|
||||
with patch.object(updates.shutil, 'which', return_value=None), \
|
||||
patch.object(sys, 'platform', 'linux'), \
|
||||
patch.object(updates.os.path, 'exists', return_value=True), \
|
||||
patch.object(updates.subprocess, 'run', side_effect=fake_run):
|
||||
out, ok = updates._run_git(['status'], tmp_path)
|
||||
|
||||
assert ok is False
|
||||
assert out == 'git executable not found'
|
||||
assert '/usr/bin/git' not in calls
|
||||
|
||||
|
||||
def test_run_git_returns_not_found_when_usr_bin_git_absent_on_darwin(tmp_path):
|
||||
calls = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
calls.append(cmd[0])
|
||||
raise FileNotFoundError
|
||||
|
||||
with patch.object(updates.shutil, 'which', return_value=None), \
|
||||
patch.object(sys, 'platform', 'darwin'), \
|
||||
patch.object(updates.os.path, 'exists', return_value=False), \
|
||||
patch.object(updates.subprocess, 'run', side_effect=fake_run):
|
||||
out, ok = updates._run_git(['status'], tmp_path)
|
||||
|
||||
assert ok is False
|
||||
assert out == 'git executable not found'
|
||||
assert '/usr/bin/git' not in calls
|
||||
|
||||
|
||||
def test_detect_webui_version_recovers_via_launchd_fallback(tmp_path):
|
||||
def fake_run(cmd, **kwargs):
|
||||
if cmd[0] == 'git':
|
||||
raise FileNotFoundError
|
||||
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', 'darwin'), \
|
||||
patch.object(updates.os.path, 'exists', return_value=True), \
|
||||
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'
|
||||
|
||||
|
||||
def test_check_repo_does_not_report_git_not_found_via_launchd_fallback(tmp_path):
|
||||
(tmp_path / '.git').mkdir()
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
if cmd[0] == 'git':
|
||||
raise FileNotFoundError
|
||||
git_args = cmd[1:]
|
||||
if git_args == ['fetch', 'origin', '--tags', '--force']:
|
||||
return MagicMock(returncode=0, stdout='', stderr='')
|
||||
if git_args == ['tag', '--list', 'v*', '--sort=-v:refname']:
|
||||
return MagicMock(returncode=0, stdout='', stderr='')
|
||||
if git_args == ['rev-parse', '--abbrev-ref', '@{upstream}']:
|
||||
return MagicMock(returncode=0, stdout='origin/master\n', stderr='')
|
||||
if git_args == ['rev-list', '--count', 'HEAD..origin/master']:
|
||||
return MagicMock(returncode=0, stdout='0\n', stderr='')
|
||||
if git_args == ['merge-base', 'HEAD', 'origin/master']:
|
||||
return MagicMock(returncode=0, stdout='abcdef1234567890\n', stderr='')
|
||||
if git_args == ['rev-parse', '--short', 'abcdef1234567890']:
|
||||
return MagicMock(returncode=0, stdout='abcdef1\n', stderr='')
|
||||
if git_args == ['rev-parse', '--short', 'origin/master']:
|
||||
return MagicMock(returncode=0, stdout='fedcba9\n', stderr='')
|
||||
if git_args == ['remote', 'get-url', 'origin']:
|
||||
return MagicMock(
|
||||
returncode=0,
|
||||
stdout='https://github.com/nesquena/hermes-webui.git\n',
|
||||
stderr='',
|
||||
)
|
||||
if git_args == ['diff-index', '--quiet', 'HEAD', '--']:
|
||||
return MagicMock(returncode=0, stdout='', stderr='')
|
||||
raise AssertionError(f'unexpected git args: {git_args!r}')
|
||||
|
||||
with patch.object(updates.shutil, 'which', return_value=None), \
|
||||
patch.object(sys, 'platform', 'darwin'), \
|
||||
patch.object(updates.os.path, 'exists', return_value=True), \
|
||||
patch.object(updates.subprocess, 'run', side_effect=fake_run):
|
||||
info = updates._check_repo(tmp_path, 'webui')
|
||||
|
||||
assert info['behind'] == 0
|
||||
assert info['dirty'] is False
|
||||
assert 'error' not in info
|
||||
Loading…
Reference in a new issue