fix(cli): fall back to main when current branch has no remote counterpart
`hermes update` crashed with CalledProcessError when run on a local-only
branch (e.g. fix/stoicneko) because `git rev-list HEAD..origin/{branch}`
fails when origin/{branch} doesn't exist. Now verifies the remote branch
exists first and falls back to origin/main.
This commit is contained in:
@@ -2056,7 +2056,15 @@ def cmd_update(args):
|
||||
check=True
|
||||
)
|
||||
branch = result.stdout.strip()
|
||||
|
||||
|
||||
# Fall back to main if the current branch doesn't exist on the remote
|
||||
verify = subprocess.run(
|
||||
git_cmd + ["rev-parse", "--verify", f"origin/{branch}"],
|
||||
cwd=PROJECT_ROOT, capture_output=True, text=True,
|
||||
)
|
||||
if verify.returncode != 0:
|
||||
branch = "main"
|
||||
|
||||
# Check if there are updates
|
||||
result = subprocess.run(
|
||||
git_cmd + ["rev-list", f"HEAD..origin/{branch}", "--count"],
|
||||
|
||||
107
tests/hermes_cli/test_cmd_update.py
Normal file
107
tests/hermes_cli/test_cmd_update.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Tests for cmd_update — branch fallback when remote branch doesn't exist."""
|
||||
|
||||
import subprocess
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.main import cmd_update, PROJECT_ROOT
|
||||
|
||||
|
||||
def _make_run_side_effect(branch="main", verify_ok=True, commit_count="0"):
|
||||
"""Build a side_effect function for subprocess.run that simulates git commands."""
|
||||
|
||||
def side_effect(cmd, **kwargs):
|
||||
joined = " ".join(str(c) for c in cmd)
|
||||
|
||||
# git rev-parse --abbrev-ref HEAD (get current branch)
|
||||
if "rev-parse" in joined and "--abbrev-ref" in joined:
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout=f"{branch}\n", stderr="")
|
||||
|
||||
# git rev-parse --verify origin/{branch} (check remote branch exists)
|
||||
if "rev-parse" in joined and "--verify" in joined:
|
||||
rc = 0 if verify_ok else 128
|
||||
return subprocess.CompletedProcess(cmd, rc, stdout="", stderr="")
|
||||
|
||||
# git rev-list HEAD..origin/{branch} --count
|
||||
if "rev-list" in joined:
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout=f"{commit_count}\n", stderr="")
|
||||
|
||||
# Fallback: return a successful CompletedProcess with empty stdout
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
|
||||
|
||||
return side_effect
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_args():
|
||||
return SimpleNamespace()
|
||||
|
||||
|
||||
class TestCmdUpdateBranchFallback:
|
||||
"""cmd_update falls back to main when current branch has no remote counterpart."""
|
||||
|
||||
@patch("shutil.which", return_value=None)
|
||||
@patch("subprocess.run")
|
||||
def test_update_falls_back_to_main_when_branch_not_on_remote(
|
||||
self, mock_run, _mock_which, mock_args, capsys
|
||||
):
|
||||
mock_run.side_effect = _make_run_side_effect(
|
||||
branch="fix/stoicneko", verify_ok=False, commit_count="3"
|
||||
)
|
||||
|
||||
cmd_update(mock_args)
|
||||
|
||||
commands = [" ".join(str(a) for a in c.args[0]) for c in mock_run.call_args_list]
|
||||
|
||||
# rev-list should use origin/main, not origin/fix/stoicneko
|
||||
rev_list_cmds = [c for c in commands if "rev-list" in c]
|
||||
assert len(rev_list_cmds) == 1
|
||||
assert "origin/main" in rev_list_cmds[0]
|
||||
assert "origin/fix/stoicneko" not in rev_list_cmds[0]
|
||||
|
||||
# pull should use main, not fix/stoicneko
|
||||
pull_cmds = [c for c in commands if "pull" in c]
|
||||
assert len(pull_cmds) == 1
|
||||
assert "main" in pull_cmds[0]
|
||||
|
||||
@patch("shutil.which", return_value=None)
|
||||
@patch("subprocess.run")
|
||||
def test_update_uses_current_branch_when_on_remote(
|
||||
self, mock_run, _mock_which, mock_args, capsys
|
||||
):
|
||||
mock_run.side_effect = _make_run_side_effect(
|
||||
branch="main", verify_ok=True, commit_count="2"
|
||||
)
|
||||
|
||||
cmd_update(mock_args)
|
||||
|
||||
commands = [" ".join(str(a) for a in c.args[0]) for c in mock_run.call_args_list]
|
||||
|
||||
rev_list_cmds = [c for c in commands if "rev-list" in c]
|
||||
assert len(rev_list_cmds) == 1
|
||||
assert "origin/main" in rev_list_cmds[0]
|
||||
|
||||
pull_cmds = [c for c in commands if "pull" in c]
|
||||
assert len(pull_cmds) == 1
|
||||
assert "main" in pull_cmds[0]
|
||||
|
||||
@patch("shutil.which", return_value=None)
|
||||
@patch("subprocess.run")
|
||||
def test_update_already_up_to_date(
|
||||
self, mock_run, _mock_which, mock_args, capsys
|
||||
):
|
||||
mock_run.side_effect = _make_run_side_effect(
|
||||
branch="main", verify_ok=True, commit_count="0"
|
||||
)
|
||||
|
||||
cmd_update(mock_args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Already up to date!" in captured.out
|
||||
|
||||
# Should NOT have called pull
|
||||
commands = [" ".join(str(a) for a in c.args[0]) for c in mock_run.call_args_list]
|
||||
pull_cmds = [c for c in commands if "pull" in c]
|
||||
assert len(pull_cmds) == 0
|
||||
Reference in New Issue
Block a user