diff --git a/hermes_cli/main.py b/hermes_cli/main.py index b997d91cf..4d49d81d7 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -2570,20 +2570,44 @@ def _restore_stashed_changes( has_conflicts = bool(unmerged.stdout.strip()) if restore.returncode != 0 or has_conflicts: - # Reset the working tree so Hermes is runnable with the updated code - subprocess.run( - git_cmd + ["reset", "--hard", "HEAD"], - cwd=cwd, - capture_output=True, - ) - print("✗ Update pulled new code, but restoring local changes failed.") + print("✗ Update pulled new code, but restoring local changes hit conflicts.") if restore.stdout.strip(): print(restore.stdout.strip()) if restore.stderr.strip(): print(restore.stderr.strip()) - print("The working tree has been reset to a clean state.") - print("Your changes are still preserved in git stash.") - print(f"Resolve manually with: git stash apply {stash_ref}") + + # Show which files conflicted + conflicted_files = unmerged.stdout.strip() + if conflicted_files: + print("\nConflicted files:") + for f in conflicted_files.splitlines(): + print(f" • {f}") + + print("\nYour stashed changes are preserved — nothing is lost.") + print(f" Stash ref: {stash_ref}") + + # Ask before resetting (if interactive) + do_reset = True + if prompt_user: + print("\nReset working tree to clean state so Hermes can run?") + print(" (You can re-apply your changes later with: git stash apply)") + print("[Y/n] ", end="", flush=True) + response = input().strip().lower() + if response not in ("", "y", "yes"): + do_reset = False + + if do_reset: + subprocess.run( + git_cmd + ["reset", "--hard", "HEAD"], + cwd=cwd, + capture_output=True, + ) + print("Working tree reset to clean state.") + else: + print("Working tree left as-is (may have conflict markers).") + print("Resolve conflicts manually, then run: git stash drop") + + print(f"Restore your changes with: git stash apply {stash_ref}") sys.exit(1) stash_selector = _resolve_stash_selector(git_cmd, cwd, stash_ref) diff --git a/tests/hermes_cli/test_update_autostash.py b/tests/hermes_cli/test_update_autostash.py index 7c90f0d2b..9b8b6d79a 100644 --- a/tests/hermes_cli/test_update_autostash.py +++ b/tests/hermes_cli/test_update_autostash.py @@ -210,7 +210,8 @@ def test_restore_stashed_changes_keeps_going_when_drop_fails(monkeypatch, tmp_pa 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_prompts_before_reset_on_conflict(monkeypatch, tmp_path, capsys): + """When conflicts occur interactively, user is prompted before reset.""" calls = [] def fake_run(cmd, **kwargs): @@ -230,16 +231,43 @@ def test_restore_stashed_changes_exits_cleanly_when_apply_fails(monkeypatch, tmp hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=True) out = capsys.readouterr().out - assert "Your changes are still preserved in git stash." in out - assert "git stash apply abc123" in out - assert "working tree has been reset to a clean state" in out - # Verify reset --hard was called to clean up conflict markers + assert "Conflicted files:" in out + assert "hermes_cli/main.py" in out + assert "stashed changes are preserved" in out + assert "Reset working tree to clean state" in out + assert "Working tree reset to clean state" in out reset_calls = [c for c, _ in calls if c[1:3] == ["reset", "--hard"]] assert len(reset_calls) == 1 -def test_restore_stashed_changes_resets_when_unmerged_files_detected(monkeypatch, tmp_path, capsys): - """Even if stash apply returns 0, conflict markers must be cleaned up.""" +def test_restore_stashed_changes_user_declines_reset(monkeypatch, tmp_path, capsys): + """When user declines reset, working tree is left as-is.""" + calls = [] + + def fake_run(cmd, **kwargs): + calls.append((cmd, kwargs)) + if cmd[1:3] == ["stash", "apply"]: + return SimpleNamespace(stdout="", stderr="conflict\n", returncode=1) + if cmd[1:3] == ["diff", "--name-only"]: + return SimpleNamespace(stdout="cli.py\n", stderr="", returncode=0) + raise AssertionError(f"unexpected command: {cmd}") + + monkeypatch.setattr(hermes_main.subprocess, "run", fake_run) + # First input: "y" to restore, second input: "n" to decline reset + inputs = iter(["y", "n"]) + monkeypatch.setattr("builtins.input", lambda: next(inputs)) + + with pytest.raises(SystemExit, match="1"): + hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=True) + + out = capsys.readouterr().out + assert "left as-is" in out + reset_calls = [c for c, _ in calls if c[1:3] == ["reset", "--hard"]] + assert len(reset_calls) == 0 + + +def test_restore_stashed_changes_auto_resets_non_interactive(monkeypatch, tmp_path, capsys): + """Non-interactive mode auto-resets without prompting.""" calls = [] def fake_run(cmd, **kwargs): @@ -258,8 +286,9 @@ def test_restore_stashed_changes_resets_when_unmerged_files_detected(monkeypatch hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=False) out = capsys.readouterr().out - assert "working tree has been reset to a clean state" in out - assert "git stash apply abc123" in out + assert "Working tree reset to clean state" in out + reset_calls = [c for c, _ in calls if c[1:3] == ["reset", "--hard"]] + assert len(reset_calls) == 1 def test_stash_local_changes_if_needed_raises_when_stash_ref_missing(monkeypatch, tmp_path):