fix(update): drop autostash by stash selector

fix(update): drop autostash by stash selector
This commit is contained in:
Teknium
2026-03-14 22:53:50 -07:00
committed by GitHub
2 changed files with 112 additions and 2 deletions

View File

@@ -2016,6 +2016,22 @@ def _stash_local_changes_if_needed(git_cmd: list[str], cwd: Path) -> Optional[st
def _resolve_stash_selector(git_cmd: list[str], cwd: Path, stash_ref: str) -> Optional[str]:
stash_list = subprocess.run(
git_cmd + ["stash", "list", "--format=%gd %H"],
cwd=cwd,
capture_output=True,
text=True,
check=True,
)
for line in stash_list.stdout.splitlines():
selector, _, commit = line.partition(" ")
if commit.strip() == stash_ref:
return selector.strip()
return None
def _restore_stashed_changes(
git_cmd: list[str],
cwd: Path,
@@ -2052,7 +2068,27 @@ def _restore_stashed_changes(
print(f"Resolve manually with: git stash apply {stash_ref}")
sys.exit(1)
subprocess.run(git_cmd + ["stash", "drop", stash_ref], cwd=cwd, check=True)
stash_selector = _resolve_stash_selector(git_cmd, cwd, stash_ref)
if stash_selector is None:
print("⚠ Local changes were restored, but Hermes couldn't find the stash entry to drop.")
print(" The stash was left in place. You can remove it manually after checking the result.")
print(f" Look for commit {stash_ref} in `git stash list --format='%gd %H'` and drop that selector.")
else:
drop = subprocess.run(
git_cmd + ["stash", "drop", stash_selector],
cwd=cwd,
capture_output=True,
text=True,
)
if drop.returncode != 0:
print("⚠ Local changes were restored, but Hermes couldn't drop the saved stash entry.")
if drop.stdout.strip():
print(drop.stdout.strip())
if drop.stderr.strip():
print(drop.stderr.strip())
print(" The stash was left in place. You can remove it manually after checking the result.")
print(f" If needed: git stash drop {stash_selector}")
print("⚠ Local changes were restored on top of the updated codebase.")
print(" Review `git diff` / `git status` if Hermes behaves unexpectedly.")
return True

View File

@@ -46,6 +46,20 @@ def test_stash_local_changes_if_needed_returns_specific_stash_commit(monkeypatch
assert calls[2][0][-3:] == ["rev-parse", "--verify", "refs/stash"]
def test_resolve_stash_selector_returns_matching_entry(monkeypatch, tmp_path):
def fake_run(cmd, **kwargs):
assert cmd == ["git", "stash", "list", "--format=%gd %H"]
return SimpleNamespace(
stdout="stash@{0} def456\nstash@{1} abc123\n",
returncode=0,
)
monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)
assert hermes_main._resolve_stash_selector(["git"], tmp_path, "abc123") == "stash@{1}"
def test_restore_stashed_changes_prompts_before_applying(monkeypatch, tmp_path, capsys):
calls = []
@@ -53,6 +67,8 @@ def test_restore_stashed_changes_prompts_before_applying(monkeypatch, tmp_path,
calls.append((cmd, kwargs))
if cmd[1:3] == ["stash", "apply"]:
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
if cmd[1:3] == ["stash", "list"]:
return SimpleNamespace(stdout="stash@{1} abc123\n", stderr="", returncode=0)
if cmd[1:3] == ["stash", "drop"]:
return SimpleNamespace(stdout="dropped\n", stderr="", returncode=0)
raise AssertionError(f"unexpected command: {cmd}")
@@ -64,7 +80,8 @@ def test_restore_stashed_changes_prompts_before_applying(monkeypatch, tmp_path,
assert restored is True
assert calls[0][0] == ["git", "stash", "apply", "abc123"]
assert calls[1][0] == ["git", "stash", "drop", "abc123"]
assert calls[1][0] == ["git", "stash", "list", "--format=%gd %H"]
assert calls[2][0] == ["git", "stash", "drop", "stash@{1}"]
out = capsys.readouterr().out
assert "Restore local changes now? [Y/n]" in out
assert "restored on top of the updated codebase" in out
@@ -99,6 +116,8 @@ def test_restore_stashed_changes_applies_without_prompt_when_disabled(monkeypatc
calls.append((cmd, kwargs))
if cmd[1:3] == ["stash", "apply"]:
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
if cmd[1:3] == ["stash", "list"]:
return SimpleNamespace(stdout="stash@{0} abc123\n", stderr="", returncode=0)
if cmd[1:3] == ["stash", "drop"]:
return SimpleNamespace(stdout="dropped\n", stderr="", returncode=0)
raise AssertionError(f"unexpected command: {cmd}")
@@ -109,9 +128,64 @@ def test_restore_stashed_changes_applies_without_prompt_when_disabled(monkeypatc
assert restored is True
assert calls[0][0] == ["git", "stash", "apply", "abc123"]
assert calls[1][0] == ["git", "stash", "list", "--format=%gd %H"]
assert calls[2][0] == ["git", "stash", "drop", "stash@{0}"]
assert "Restore local changes now?" not in capsys.readouterr().out
def test_restore_stashed_changes_keeps_going_when_stash_entry_cannot_be_resolved(monkeypatch, tmp_path, capsys):
calls = []
def fake_run(cmd, **kwargs):
calls.append((cmd, kwargs))
if cmd[1:3] == ["stash", "apply"]:
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
if cmd[1:3] == ["stash", "list"]:
return SimpleNamespace(stdout="stash@{0} def456\n", stderr="", returncode=0)
raise AssertionError(f"unexpected command: {cmd}")
monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)
restored = hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=False)
assert restored is True
assert calls == [
(["git", "stash", "apply", "abc123"], {"cwd": tmp_path, "capture_output": True, "text": True}),
(["git", "stash", "list", "--format=%gd %H"], {"cwd": tmp_path, "capture_output": True, "text": True, "check": True}),
]
out = capsys.readouterr().out
assert "couldn't find the stash entry to drop" in out
assert "stash was left in place" in out
assert "Look for commit abc123" in out
def test_restore_stashed_changes_keeps_going_when_drop_fails(monkeypatch, tmp_path, capsys):
calls = []
def fake_run(cmd, **kwargs):
calls.append((cmd, kwargs))
if cmd[1:3] == ["stash", "apply"]:
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
if cmd[1:3] == ["stash", "list"]:
return SimpleNamespace(stdout="stash@{0} abc123\n", stderr="", returncode=0)
if cmd[1:3] == ["stash", "drop"]:
return SimpleNamespace(stdout="", stderr="drop failed\n", returncode=1)
raise AssertionError(f"unexpected command: {cmd}")
monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)
restored = hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=False)
assert restored is True
assert calls[2][0] == ["git", "stash", "drop", "stash@{0}"]
out = capsys.readouterr().out
assert "couldn't drop the saved stash entry" in out
assert "drop failed" in out
assert "git stash drop stash@{0}" in out
def test_restore_stashed_changes_exits_cleanly_when_apply_fails(monkeypatch, tmp_path, capsys):
calls = []