fix(update): drop autostash by stash selector
This commit is contained in:
parent
4f4e2671ac
commit
47c5c97654
2 changed files with 112 additions and 2 deletions
|
|
@ -1997,6 +1997,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(
|
def _restore_stashed_changes(
|
||||||
git_cmd: list[str],
|
git_cmd: list[str],
|
||||||
cwd: Path,
|
cwd: Path,
|
||||||
|
|
@ -2033,7 +2049,27 @@ def _restore_stashed_changes(
|
||||||
print(f"Resolve manually with: git stash apply {stash_ref}")
|
print(f"Resolve manually with: git stash apply {stash_ref}")
|
||||||
sys.exit(1)
|
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("⚠ Local changes were restored on top of the updated codebase.")
|
||||||
print(" Review `git diff` / `git status` if Hermes behaves unexpectedly.")
|
print(" Review `git diff` / `git status` if Hermes behaves unexpectedly.")
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
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):
|
def test_restore_stashed_changes_prompts_before_applying(monkeypatch, tmp_path, capsys):
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
|
|
@ -53,6 +67,8 @@ def test_restore_stashed_changes_prompts_before_applying(monkeypatch, tmp_path,
|
||||||
calls.append((cmd, kwargs))
|
calls.append((cmd, kwargs))
|
||||||
if cmd[1:3] == ["stash", "apply"]:
|
if cmd[1:3] == ["stash", "apply"]:
|
||||||
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
|
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"]:
|
if cmd[1:3] == ["stash", "drop"]:
|
||||||
return SimpleNamespace(stdout="dropped\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="dropped\n", stderr="", returncode=0)
|
||||||
raise AssertionError(f"unexpected command: {cmd}")
|
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 restored is True
|
||||||
assert calls[0][0] == ["git", "stash", "apply", "abc123"]
|
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
|
out = capsys.readouterr().out
|
||||||
assert "Restore local changes now? [Y/n]" in out
|
assert "Restore local changes now? [Y/n]" in out
|
||||||
assert "restored on top of the updated codebase" 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))
|
calls.append((cmd, kwargs))
|
||||||
if cmd[1:3] == ["stash", "apply"]:
|
if cmd[1:3] == ["stash", "apply"]:
|
||||||
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
|
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"]:
|
if cmd[1:3] == ["stash", "drop"]:
|
||||||
return SimpleNamespace(stdout="dropped\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="dropped\n", stderr="", returncode=0)
|
||||||
raise AssertionError(f"unexpected command: {cmd}")
|
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 restored is True
|
||||||
assert calls[0][0] == ["git", "stash", "apply", "abc123"]
|
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
|
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):
|
def test_restore_stashed_changes_exits_cleanly_when_apply_fails(monkeypatch, tmp_path, capsys):
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue