cgcardona / muse public
rebase.py python
164 lines 4.9 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """muse rebase <upstream> — rebase commits onto a new base.
2
3 Algorithm
4 ---------
5 1. Find the merge-base (LCA) of HEAD and ``<upstream>``.
6 2. Collect commits on the current branch that are not in ``<upstream>``'s
7 history, ordered oldest-first.
8 3. Replay each commit onto the upstream tip as a new commit (new commit_id,
9 same snapshot delta).
10 4. Advance the branch pointer to the final replayed commit.
11
12 ``--continue`` / ``--abort``
13 -----------------------------
14 Mid-rebase state is stored in ``.muse/REBASE_STATE.json``. On conflict:
15 - ``muse rebase --continue``: resume after manually resolving conflicts.
16 - ``muse rebase --abort``: restore the branch pointer to its pre-rebase HEAD.
17
18 ``--interactive`` / ``-i``
19 ---------------------------
20 Opens ``$EDITOR`` with a plan file listing all commits to replay. Each line is::
21
22 pick <short-sha> <message>
23
24 Actions: ``pick`` (keep), ``squash`` (fold into previous), ``drop`` (skip),
25 ``fixup`` (squash, no message), ``reword`` (keep, change message).
26
27 ``--autosquash``
28 ----------------
29 Detects ``fixup! <message>`` commits and automatically moves them immediately
30 after the matching commit in the replay order.
31 """
32 from __future__ import annotations
33
34 import asyncio
35 import logging
36 from typing import Optional
37
38 import typer
39
40 from maestro.muse_cli._repo import require_repo
41 from maestro.muse_cli.db import open_session
42 from maestro.muse_cli.errors import ExitCode
43 from maestro.services.muse_rebase import (
44 _rebase_abort_async,
45 _rebase_async,
46 _rebase_continue_async,
47 )
48
49 logger = logging.getLogger(__name__)
50
51 app = typer.Typer()
52
53
54 @app.callback(invoke_without_command=True)
55 def rebase(
56 ctx: typer.Context,
57 upstream: Optional[str] = typer.Argument(
58 None,
59 help=(
60 "Branch name or commit ID to rebase onto. "
61 "Omit when using --continue or --abort."
62 ),
63 metavar="UPSTREAM",
64 ),
65 interactive: bool = typer.Option(
66 False,
67 "--interactive",
68 "-i",
69 is_flag=True,
70 help=(
71 "Open $EDITOR with a rebase plan before executing. "
72 "Lines: pick/squash/drop <short-sha> <message>."
73 ),
74 ),
75 autosquash: bool = typer.Option(
76 False,
77 "--autosquash",
78 is_flag=True,
79 help=(
80 "Automatically detect 'fixup! <msg>' commits and move them "
81 "immediately after their matching commit."
82 ),
83 ),
84 rebase_merges: bool = typer.Option(
85 False,
86 "--rebase-merges",
87 is_flag=True,
88 help="Preserve merge commits during replay (experimental).",
89 ),
90 cont: bool = typer.Option(
91 False,
92 "--continue",
93 is_flag=True,
94 help="Resume a rebase that was paused due to conflicts.",
95 ),
96 abort: bool = typer.Option(
97 False,
98 "--abort",
99 is_flag=True,
100 help="Abort the in-progress rebase and restore the branch to its original HEAD.",
101 ),
102 ) -> None:
103 """Rebase commits onto a new base, producing a linear history.
104
105 Use ``--continue`` after resolving conflicts to resume the rebase.
106 Use ``--abort`` to cancel and restore the original branch state.
107 """
108 root = require_repo()
109
110 if abort:
111 async def _run_abort() -> None:
112 await _rebase_abort_async(root=root)
113
114 try:
115 asyncio.run(_run_abort())
116 except typer.Exit:
117 raise
118 except Exception as exc:
119 typer.echo(f"❌ muse rebase --abort failed: {exc}")
120 logger.error("❌ muse rebase --abort error: %s", exc, exc_info=True)
121 raise typer.Exit(code=ExitCode.INTERNAL_ERROR)
122 return
123
124 if cont:
125 async def _run_continue() -> None:
126 async with open_session() as session:
127 await _rebase_continue_async(root=root, session=session)
128
129 try:
130 asyncio.run(_run_continue())
131 except typer.Exit:
132 raise
133 except Exception as exc:
134 typer.echo(f"❌ muse rebase --continue failed: {exc}")
135 logger.error("❌ muse rebase --continue error: %s", exc, exc_info=True)
136 raise typer.Exit(code=ExitCode.INTERNAL_ERROR)
137 return
138
139 if not upstream:
140 typer.echo(
141 "❌ UPSTREAM is required (or use --continue / --abort to manage "
142 "an in-progress rebase)."
143 )
144 raise typer.Exit(code=ExitCode.USER_ERROR)
145
146 async def _run() -> None:
147 async with open_session() as session:
148 await _rebase_async(
149 upstream=upstream,
150 root=root,
151 session=session,
152 interactive=interactive,
153 autosquash=autosquash,
154 rebase_merges=rebase_merges,
155 )
156
157 try:
158 asyncio.run(_run())
159 except typer.Exit:
160 raise
161 except Exception as exc:
162 typer.echo(f"❌ muse rebase failed: {exc}")
163 logger.error("❌ muse rebase error: %s", exc, exc_info=True)
164 raise typer.Exit(code=ExitCode.INTERNAL_ERROR)