cgcardona / muse public
reset.py python
153 lines 5.0 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """muse reset <commit> — reset the branch pointer to a prior commit.
2
3 Algorithm
4 ---------
5 1. Block if ``.muse/MERGE_STATE.json`` exists (merge in progress).
6 2. Resolve repo root via ``require_repo()``.
7 3. Read current branch from ``.muse/HEAD``.
8 4. Resolve *commit* argument (``HEAD~N``, full/abbreviated SHA) to a
9 ``MuseCliCommit`` row via :func:`~maestro.services.muse_reset.resolve_ref`.
10 5. Apply the chosen mode:
11
12 ``--soft`` — update ``.muse/refs/heads/<branch>`` only. muse-work/
13 files and the object store are untouched. The producer
14 can immediately ``muse commit`` a new snapshot on top of
15 the rewound head.
16
17 ``--mixed`` (default) — same as ``--soft`` in the current Muse model
18 (no explicit staging area). Included for API symmetry
19 and forward-compatibility.
20
21 ``--hard`` — update the branch ref AND overwrite ``muse-work/`` with
22 the file content recorded in the target snapshot. Objects
23 are read from ``.muse/objects/`` (the blob store populated
24 by ``muse commit``). Prompts for confirmation unless
25 ``--yes`` is given, because this operation discards any
26 uncommitted changes in muse-work/.
27
28 HEAD~N
29 ------
30 muse reset HEAD~1 # one parent back
31 muse reset HEAD~3 # three parents back
32 muse reset abc123 # abbreviated SHA
33 muse reset --hard HEAD~2 # two parents back + restore working tree
34
35 Exit codes
36 ----------
37 0 success
38 1 user error (ref not found, merge in progress, no commits, abort)
39 2 not a Muse repo
40 3 internal error (DB inconsistency, missing object blobs)
41 """
42 from __future__ import annotations
43
44 import asyncio
45 import logging
46
47 import typer
48
49 from maestro.muse_cli._repo import require_repo
50 from maestro.muse_cli.db import open_session
51 from maestro.muse_cli.errors import ExitCode
52 from maestro.services.muse_reset import (
53 MissingObjectError,
54 ResetMode,
55 perform_reset,
56 )
57
58 logger = logging.getLogger(__name__)
59
60 app = typer.Typer()
61
62
63 @app.callback(invoke_without_command=True)
64 def reset(
65 ctx: typer.Context,
66 commit: str = typer.Argument(
67 ...,
68 help=(
69 "Target commit reference. Accepts: HEAD, HEAD~N, "
70 "a full 64-char SHA, or any unambiguous SHA prefix."
71 ),
72 ),
73 soft: bool = typer.Option(
74 False,
75 "--soft",
76 help="Move branch pointer only; muse-work/ unchanged.",
77 ),
78 mixed: bool = typer.Option(
79 False,
80 "--mixed",
81 help="Move branch pointer and reset index (default mode).",
82 ),
83 hard: bool = typer.Option(
84 False,
85 "--hard",
86 help="Move branch pointer AND overwrite muse-work/ with target snapshot.",
87 ),
88 yes: bool = typer.Option(
89 False,
90 "--yes",
91 "-y",
92 help="Skip confirmation prompt for --hard reset.",
93 ),
94 ) -> None:
95 """Reset the branch pointer to a prior commit."""
96 # ── Resolve mode (default: mixed) ────────────────────────────────────
97 mode_count = sum([soft, mixed, hard])
98 if mode_count > 1:
99 typer.echo("❌ Specify at most one of --soft, --mixed, or --hard.")
100 raise typer.Exit(code=ExitCode.USER_ERROR)
101
102 if hard:
103 mode = ResetMode.HARD
104 elif soft:
105 mode = ResetMode.SOFT
106 else:
107 mode = ResetMode.MIXED # default
108
109 # ── Hard-mode confirmation ────────────────────────────────────────────
110 if mode is ResetMode.HARD and not yes:
111 typer.echo(
112 "⚠️ muse reset --hard will OVERWRITE muse-work/ with the target snapshot.\n"
113 " All uncommitted changes will be LOST."
114 )
115 confirmed = typer.confirm("Proceed?", default=False)
116 if not confirmed:
117 typer.echo("Reset aborted.")
118 raise typer.Exit(code=ExitCode.SUCCESS)
119
120 root = require_repo()
121
122 async def _run() -> None:
123 async with open_session() as session:
124 result = await perform_reset(
125 root=root,
126 session=session,
127 ref=commit,
128 mode=mode,
129 )
130
131 if mode is ResetMode.HARD:
132 typer.echo(
133 f"✅ HEAD is now at {result.target_commit_id[:8]} "
134 f"({result.files_restored} files restored, "
135 f"{result.files_deleted} files deleted)"
136 )
137 else:
138 typer.echo(
139 f"✅ HEAD is now at {result.target_commit_id[:8]}"
140 )
141
142 try:
143 asyncio.run(_run())
144 except typer.Exit:
145 raise
146 except MissingObjectError as exc:
147 typer.echo(f"❌ {exc}")
148 logger.error("❌ muse reset hard: %s", exc)
149 raise typer.Exit(code=ExitCode.INTERNAL_ERROR)
150 except Exception as exc:
151 typer.echo(f"❌ muse reset failed: {exc}")
152 logger.error("❌ muse reset error: %s", exc, exc_info=True)
153 raise typer.Exit(code=ExitCode.INTERNAL_ERROR)