refactorai-cli 0.4.4__tar.gz → 0.4.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/PKG-INFO +2 -2
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/pyproject.toml +2 -2
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/cloud_rr.py +14 -2
- refactorai_cli-0.4.8/refactorai_cli/commands/branch_cmds.py +340 -0
- refactorai_cli-0.4.8/refactorai_cli/commands/request_cmds.py +129 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/run_cmds.py +711 -11
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/watch_cmds.py +83 -13
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/git_scope.py +213 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/main.py +6 -0
- refactorai_cli-0.4.8/refactorai_cli/refactor_branch_store.py +116 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/review_runner.py +20 -3
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/watch_state.py +34 -14
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli.egg-info/PKG-INFO +2 -2
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli.egg-info/SOURCES.txt +3 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli.egg-info/requires.txt +1 -1
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/README.md +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/client.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/hook_cmds.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commands/toolchains_cmds.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/commit_queue.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/dotenv_loader.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.4.4 → refactorai_cli-0.4.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
4
4
|
Summary: Local-first CLI for the refactor platform
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -8,7 +8,7 @@ Requires-Dist: typer>=0.12.0
|
|
|
8
8
|
Requires-Dist: httpx>=0.27.0
|
|
9
9
|
Requires-Dist: rich>=13.7.0
|
|
10
10
|
Requires-Dist: PyYAML>=6.0.1
|
|
11
|
-
Requires-Dist: refactorai-core>=3.2.
|
|
11
|
+
Requires-Dist: refactorai-core>=3.2.5
|
|
12
12
|
|
|
13
13
|
# refactorai-cli
|
|
14
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "refactorai-cli"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.8"
|
|
4
4
|
description = "Local-first CLI for the refactor platform"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -12,7 +12,7 @@ dependencies = [
|
|
|
12
12
|
"httpx>=0.27.0",
|
|
13
13
|
"rich>=13.7.0",
|
|
14
14
|
"PyYAML>=6.0.1",
|
|
15
|
-
"refactorai-core>=3.2.
|
|
15
|
+
"refactorai-core>=3.2.5",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.scripts]
|
|
@@ -650,6 +650,7 @@ def run_cloud_refactor_requests(
|
|
|
650
650
|
approval_cb: Callable[[dict], bool] | None = None,
|
|
651
651
|
apply_byok_payload: Callable[[dict, object], None] | None = None,
|
|
652
652
|
toolchain_consent_cb: Callable[[dict], bool] | None = None,
|
|
653
|
+
commit_cb: Callable[[object], None] | None = None,
|
|
653
654
|
) -> RunArtifact:
|
|
654
655
|
"""Drive pending RRs through the cloud server-run loop and return a summary.
|
|
655
656
|
|
|
@@ -659,6 +660,11 @@ def run_cloud_refactor_requests(
|
|
|
659
660
|
shared BYOK resolver injected by the caller to avoid a circular import.
|
|
660
661
|
``toolchain_consent_cb`` gates first-run managed-toolchain provisioning; when
|
|
661
662
|
None, provisioning proceeds without prompting (e.g. ``--auto-approve``).
|
|
663
|
+
|
|
664
|
+
``commit_cb`` (R29) mirrors the local executor's post-apply hook: it is fired
|
|
665
|
+
once per RR that reaches ``APPLIED`` so the CLI can record one git commit per
|
|
666
|
+
resolved request on an isolated refactor branch. Best-effort; git failures
|
|
667
|
+
never break the cloud RR loop.
|
|
662
668
|
"""
|
|
663
669
|
project_root = Path(project_root)
|
|
664
670
|
progress_cb = progress_cb or (lambda e: None)
|
|
@@ -867,7 +873,7 @@ def run_cloud_refactor_requests(
|
|
|
867
873
|
_record_outcome(
|
|
868
874
|
project_root, by_id, completed_id, verdict, action.get("message", ""),
|
|
869
875
|
attempts_by_id.get(completed_id, 0 if verdict == "deferred" else 1),
|
|
870
|
-
outcomes, counts, progress_cb,
|
|
876
|
+
outcomes, counts, progress_cb, commit_cb=commit_cb,
|
|
871
877
|
)
|
|
872
878
|
if verdict == "failed":
|
|
873
879
|
stopped = True
|
|
@@ -956,7 +962,7 @@ def _apply_and_verify(
|
|
|
956
962
|
|
|
957
963
|
|
|
958
964
|
def _record_outcome(project_root, by_id, rr_id, verdict, message, attempts,
|
|
959
|
-
outcomes: list, counts: dict, progress_cb) -> None:
|
|
965
|
+
outcomes: list, counts: dict, progress_cb, *, commit_cb=None) -> None:
|
|
960
966
|
status_map = {
|
|
961
967
|
"applied": APPLIED,
|
|
962
968
|
"denied": DENIED,
|
|
@@ -968,6 +974,12 @@ def _record_outcome(project_root, by_id, rr_id, verdict, message, attempts,
|
|
|
968
974
|
rr.status = status_map.get(verdict, rr.status)
|
|
969
975
|
rr.attempt_count = max(int(getattr(rr, "attempt_count", 0) or 0), int(attempts or 0))
|
|
970
976
|
save_request(project_root, rr)
|
|
977
|
+
# R29: one git commit per applied RR (client-side git policy), best-effort.
|
|
978
|
+
if commit_cb is not None and rr.status == APPLIED:
|
|
979
|
+
try:
|
|
980
|
+
commit_cb(rr)
|
|
981
|
+
except Exception:
|
|
982
|
+
pass
|
|
971
983
|
outcomes.append({
|
|
972
984
|
"location": getattr(rr, "location", rr_id) if rr is not None else rr_id,
|
|
973
985
|
"operation_type": getattr(rr, "operation_type", "content_patch") if rr is not None else "content_patch",
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""`refactor branch` commands: manage the local refactor branch (docs/42 `R29`).
|
|
2
|
+
|
|
3
|
+
Phase 3 of the local refactor-branch flow. After ``refactor code --commit``
|
|
4
|
+
records one commit per resolved request on an isolated ``refactor/<branch>/<sha>``
|
|
5
|
+
branch, these commands let the developer inspect it (``status``), merge it into
|
|
6
|
+
their working branch **locally** (``merge`` — never pushes), or discard it
|
|
7
|
+
(``drop``).
|
|
8
|
+
|
|
9
|
+
Everything here is local git + the central RR/branch stores; there is no runtime
|
|
10
|
+
or network dependency. ``refactor_core`` imports are lazy so a stale installed
|
|
11
|
+
core can never break loading the CLI.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
|
|
23
|
+
from refactorai_cli import git_scope, refactor_branch_store
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
app = typer.Typer(
|
|
27
|
+
name="branch",
|
|
28
|
+
help="Manage the local refactor branch and its per-request commits (docs/42).",
|
|
29
|
+
no_args_is_help=True,
|
|
30
|
+
add_completion=False,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _project_repo() -> Path:
|
|
35
|
+
"""Resolve the project root and require a git repo (local-only commands)."""
|
|
36
|
+
from refactorai_cli.local_constitution import find_config
|
|
37
|
+
|
|
38
|
+
cfg = find_config(Path.cwd())
|
|
39
|
+
if cfg is None:
|
|
40
|
+
console.print("[yellow]branch:[/yellow] missing refactor.config. Run `refactor init` first.")
|
|
41
|
+
raise typer.Exit(code=1)
|
|
42
|
+
root = cfg.parent
|
|
43
|
+
if not git_scope.is_git_repo(root):
|
|
44
|
+
console.print("[red]branch:[/red] not inside a git repository.")
|
|
45
|
+
raise typer.Exit(code=1)
|
|
46
|
+
return root
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _committed_by_branch(project_root: Path) -> dict[str, list]:
|
|
50
|
+
"""Map refactor branch name -> RRs that have a per-request commit on it."""
|
|
51
|
+
from refactor_core.refactor_requests import list_requests
|
|
52
|
+
|
|
53
|
+
out: dict[str, list] = {}
|
|
54
|
+
for rr in list_requests(project_root):
|
|
55
|
+
branch = str(getattr(rr, "refactor_branch", "") or "")
|
|
56
|
+
if branch and getattr(rr, "commit_sha", ""):
|
|
57
|
+
out.setdefault(branch, []).append(rr)
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _squash_message(project_root: Path, target: str, source: str) -> str:
|
|
62
|
+
rr_count = len(_committed_by_branch(project_root).get(target, []))
|
|
63
|
+
return (
|
|
64
|
+
f"refactor: squash {target} ({rr_count} request(s)) [skip-refactor]\n\n"
|
|
65
|
+
f"Squashed refactor commits from {target} into {source}.\n"
|
|
66
|
+
"Refactor-Trailer: tool=refactorai; kind=squash-merge"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def source_moved_sha(project_root: Path, target: str, source: str) -> str | None:
|
|
71
|
+
"""Return the source tip sha if it advanced past the branch's recorded base.
|
|
72
|
+
|
|
73
|
+
``None`` when the source is unchanged (or unknown). Used to warn before a
|
|
74
|
+
merge that git will bridge a gap the refactor branch was not built against.
|
|
75
|
+
"""
|
|
76
|
+
meta = refactor_branch_store.load_branch(project_root, target)
|
|
77
|
+
recorded = meta.source_sha if meta else ""
|
|
78
|
+
if not recorded:
|
|
79
|
+
return None
|
|
80
|
+
tip = git_scope.resolve_sha(project_root, source)
|
|
81
|
+
return tip if (tip and tip != recorded) else None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def rebase_refactor_branch(project_root: Path, target: str, source: str) -> tuple[bool, str]:
|
|
85
|
+
"""Rebase ``target`` onto ``source`` so it sits atop the current source tip.
|
|
86
|
+
|
|
87
|
+
Conflict-safe (auto-aborts). Updates the recorded base sha on success so a
|
|
88
|
+
subsequent ``source_moved_sha`` check is clean.
|
|
89
|
+
"""
|
|
90
|
+
ok, output = git_scope.rebase(project_root, source, target)
|
|
91
|
+
if not ok:
|
|
92
|
+
return False, output
|
|
93
|
+
tip = git_scope.resolve_sha(project_root, source)
|
|
94
|
+
if tip:
|
|
95
|
+
meta = refactor_branch_store.load_branch(project_root, target)
|
|
96
|
+
refactor_branch_store.record_branch(
|
|
97
|
+
project_root,
|
|
98
|
+
target,
|
|
99
|
+
source_branch=source,
|
|
100
|
+
source_sha=tip,
|
|
101
|
+
)
|
|
102
|
+
return True, f"rebased {target} onto {source}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def merge_branch(
|
|
106
|
+
project_root: Path,
|
|
107
|
+
target: str,
|
|
108
|
+
*,
|
|
109
|
+
source: str,
|
|
110
|
+
squash: bool = False,
|
|
111
|
+
) -> tuple[bool, str]:
|
|
112
|
+
"""Merge ``target`` into ``source`` locally (never pushes). Returns (ok, msg).
|
|
113
|
+
|
|
114
|
+
Pure of prompting/printing so it is reused by both the ``merge`` command and
|
|
115
|
+
the ``code`` finalize step. Ensures we are on ``source`` first; a dirty tree
|
|
116
|
+
or a conflict returns ``(False, msg)`` and leaves both branches intact.
|
|
117
|
+
"""
|
|
118
|
+
if not git_scope.branch_exists(project_root, target):
|
|
119
|
+
return False, f"refactor branch '{target}' does not exist"
|
|
120
|
+
if not source or not git_scope.branch_exists(project_root, source):
|
|
121
|
+
return False, f"source branch '{source}' does not exist"
|
|
122
|
+
current = git_scope.current_branch(project_root)
|
|
123
|
+
if current != source:
|
|
124
|
+
if git_scope.is_worktree_dirty(project_root):
|
|
125
|
+
return False, "working tree is dirty; commit or stash before merging"
|
|
126
|
+
if not git_scope.checkout(project_root, source):
|
|
127
|
+
return False, f"could not check out source branch '{source}'"
|
|
128
|
+
ok, output = git_scope.merge(project_root, target, squash=squash)
|
|
129
|
+
if not ok:
|
|
130
|
+
return False, f"merge failed / conflicted: {output}"
|
|
131
|
+
if squash:
|
|
132
|
+
sha = git_scope.commit(project_root, _squash_message(project_root, target, source))
|
|
133
|
+
if not sha:
|
|
134
|
+
return True, f"nothing to squash-merge from {target}"
|
|
135
|
+
return True, f"squash-merged {target} into {source} ({sha[:9]})"
|
|
136
|
+
return True, f"merged {target} into {source}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def drop_branch(
|
|
140
|
+
project_root: Path,
|
|
141
|
+
target: str,
|
|
142
|
+
*,
|
|
143
|
+
source: str | None = None,
|
|
144
|
+
) -> tuple[bool, str]:
|
|
145
|
+
"""Delete ``target`` and clear its metadata (discards its commits if unmerged).
|
|
146
|
+
|
|
147
|
+
Switches off the branch first (to ``source``) when it is checked out. Pure of
|
|
148
|
+
prompting/printing so both ``drop`` and finalize can reuse it.
|
|
149
|
+
"""
|
|
150
|
+
if not git_scope.branch_exists(project_root, target):
|
|
151
|
+
refactor_branch_store.remove_branch(project_root, target)
|
|
152
|
+
return True, f"'{target}' did not exist; cleared metadata"
|
|
153
|
+
current = git_scope.current_branch(project_root)
|
|
154
|
+
if current == target:
|
|
155
|
+
dest = source
|
|
156
|
+
if not dest:
|
|
157
|
+
meta = refactor_branch_store.load_branch(project_root, target)
|
|
158
|
+
dest = meta.source_branch if meta else ""
|
|
159
|
+
if not dest or not git_scope.branch_exists(project_root, dest):
|
|
160
|
+
return False, "on the refactor branch; check out another branch first"
|
|
161
|
+
if git_scope.is_worktree_dirty(project_root):
|
|
162
|
+
return False, "working tree is dirty; commit or stash before dropping"
|
|
163
|
+
if not git_scope.checkout(project_root, dest):
|
|
164
|
+
return False, f"could not switch off '{target}'"
|
|
165
|
+
if git_scope.delete_branch(project_root, target, force=True):
|
|
166
|
+
refactor_branch_store.remove_branch(project_root, target)
|
|
167
|
+
return True, f"dropped {target}"
|
|
168
|
+
return False, f"could not delete '{target}'"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _resolve_target_branch(project_root: Path, branch: str | None) -> str:
|
|
172
|
+
"""Pick the refactor branch to act on (explicit, else the single candidate)."""
|
|
173
|
+
if branch:
|
|
174
|
+
return branch
|
|
175
|
+
metas = refactor_branch_store.list_branches(project_root)
|
|
176
|
+
existing = [m.branch for m in metas if git_scope.branch_exists(project_root, m.branch)]
|
|
177
|
+
if len(existing) == 1:
|
|
178
|
+
return existing[0]
|
|
179
|
+
if len(existing) > 1:
|
|
180
|
+
console.print("[yellow]branch:[/yellow] multiple refactor branches exist; pass --branch:")
|
|
181
|
+
for name in existing:
|
|
182
|
+
console.print(f" - {name}")
|
|
183
|
+
raise typer.Exit(code=2)
|
|
184
|
+
# Fallbacks: a recorded (but now deleted) branch, RR records, or being on one.
|
|
185
|
+
committed = _committed_by_branch(project_root)
|
|
186
|
+
if len(committed) == 1:
|
|
187
|
+
return next(iter(committed))
|
|
188
|
+
current = git_scope.current_branch(project_root)
|
|
189
|
+
if current.startswith("refactor/"):
|
|
190
|
+
return current
|
|
191
|
+
console.print("[red]branch:[/red] no refactor branch found. Run `refactor code --commit <sha>` first.")
|
|
192
|
+
raise typer.Exit(code=1)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@app.command("status")
|
|
196
|
+
def status() -> None:
|
|
197
|
+
"""Show refactor branches, their source, and per-request commits."""
|
|
198
|
+
root = _project_repo()
|
|
199
|
+
current = git_scope.current_branch(root)
|
|
200
|
+
console.print(f"Repository: [bold]{root}[/bold]")
|
|
201
|
+
console.print(f"Current branch: [cyan]{current or '(unknown)'}[/cyan]")
|
|
202
|
+
|
|
203
|
+
committed = _committed_by_branch(root)
|
|
204
|
+
metas = {m.branch: m for m in refactor_branch_store.list_branches(root)}
|
|
205
|
+
names: list[str] = []
|
|
206
|
+
for name in list(metas.keys()) + list(committed.keys()):
|
|
207
|
+
if name not in names:
|
|
208
|
+
names.append(name)
|
|
209
|
+
|
|
210
|
+
if not names:
|
|
211
|
+
console.print("[dim]No refactor branches yet. Use `refactor code --commit <sha>`.[/dim]")
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
for name in names:
|
|
215
|
+
exists = git_scope.branch_exists(root, name)
|
|
216
|
+
meta = metas.get(name)
|
|
217
|
+
source = meta.source_branch if meta else ""
|
|
218
|
+
rrs = committed.get(name, [])
|
|
219
|
+
applied = [rr for rr in rrs if not getattr(rr, "reverted", False)]
|
|
220
|
+
reverted = [rr for rr in rrs if getattr(rr, "reverted", False)]
|
|
221
|
+
header = f"\n[cyan]{name}[/cyan]"
|
|
222
|
+
if not exists:
|
|
223
|
+
header += " [dim](deleted)[/dim]"
|
|
224
|
+
console.print(
|
|
225
|
+
f"{header} [dim]source={source or '?'} "
|
|
226
|
+
f"commits={len(applied)} reverted={len(reverted)}[/dim]"
|
|
227
|
+
)
|
|
228
|
+
if not rrs:
|
|
229
|
+
continue
|
|
230
|
+
table = Table(show_lines=False)
|
|
231
|
+
table.add_column("rr_id", no_wrap=True)
|
|
232
|
+
table.add_column("status", no_wrap=True)
|
|
233
|
+
table.add_column("commit", no_wrap=True)
|
|
234
|
+
table.add_column("location")
|
|
235
|
+
for rr in rrs:
|
|
236
|
+
table.add_row(
|
|
237
|
+
rr.rr_id,
|
|
238
|
+
str(getattr(rr, "status", "")),
|
|
239
|
+
str(getattr(rr, "commit_sha", "") or "")[:9],
|
|
240
|
+
getattr(rr, "location", ""),
|
|
241
|
+
)
|
|
242
|
+
console.print(table)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@app.command("merge")
|
|
246
|
+
def merge(
|
|
247
|
+
branch: str | None = typer.Option(None, "--branch", help="Refactor branch to merge (default: the single existing one)."),
|
|
248
|
+
squash: bool = typer.Option(False, "--squash", help="Squash the refactor commits into one on the source branch."),
|
|
249
|
+
rebase: bool = typer.Option(False, "--rebase", help="If the source branch advanced, rebase the refactor branch onto it before merging."),
|
|
250
|
+
yes: bool = typer.Option(False, "--yes", help="Skip the confirmation prompt."),
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Merge the refactor branch into its source branch locally (never pushes)."""
|
|
253
|
+
root = _project_repo()
|
|
254
|
+
target = _resolve_target_branch(root, branch)
|
|
255
|
+
if not git_scope.branch_exists(root, target):
|
|
256
|
+
console.print(f"[red]branch:[/red] refactor branch '{target}' does not exist.")
|
|
257
|
+
raise typer.Exit(code=1)
|
|
258
|
+
|
|
259
|
+
meta = refactor_branch_store.load_branch(root, target)
|
|
260
|
+
source = meta.source_branch if meta else ""
|
|
261
|
+
current = git_scope.current_branch(root)
|
|
262
|
+
if not source or not git_scope.branch_exists(root, source):
|
|
263
|
+
# Fall back to the current branch when it is not the refactor branch.
|
|
264
|
+
if current and current != target:
|
|
265
|
+
source = current
|
|
266
|
+
else:
|
|
267
|
+
console.print(
|
|
268
|
+
"[red]branch:[/red] could not determine the source branch. "
|
|
269
|
+
"Check out your working branch, then re-run `refactor branch merge`."
|
|
270
|
+
)
|
|
271
|
+
raise typer.Exit(code=2)
|
|
272
|
+
|
|
273
|
+
# docs/42 Part O: warn when the source branch advanced past the pinned base.
|
|
274
|
+
moved = source_moved_sha(root, target, source)
|
|
275
|
+
if moved:
|
|
276
|
+
recorded = meta.source_sha if meta else ""
|
|
277
|
+
console.print(
|
|
278
|
+
f"[yellow]branch:[/yellow] source '{source}' advanced since {target} was "
|
|
279
|
+
f"created ([dim]{(recorded or '?')[:9]} -> {moved[:9]}[/dim])."
|
|
280
|
+
)
|
|
281
|
+
if rebase:
|
|
282
|
+
console.print(f"[dim]branch: rebasing {target} onto {source}...[/dim]")
|
|
283
|
+
ok, msg = rebase_refactor_branch(root, target, source)
|
|
284
|
+
if not ok:
|
|
285
|
+
console.print(
|
|
286
|
+
f"[red]branch: rebase failed / conflicted.[/red]\n[dim]{msg}[/dim]\n"
|
|
287
|
+
"[dim]Resolve the refactor branch manually, then re-run merge.[/dim]"
|
|
288
|
+
)
|
|
289
|
+
raise typer.Exit(code=1)
|
|
290
|
+
console.print(f"[green]{msg}.[/green]")
|
|
291
|
+
else:
|
|
292
|
+
console.print(
|
|
293
|
+
"[dim]Pass --rebase to replay the refactor commits on the new tip "
|
|
294
|
+
"first, or continue to let git bridge the gap.[/dim]"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if not yes and sys.stdin.isatty():
|
|
298
|
+
how = "squash-merge" if squash else "merge"
|
|
299
|
+
if not typer.confirm(f"{how} {target} into {source} (local, no push)?", default=True):
|
|
300
|
+
console.print("[dim]branch: merge aborted.[/dim]")
|
|
301
|
+
raise typer.Exit(code=1)
|
|
302
|
+
|
|
303
|
+
ok, message = merge_branch(root, target, source=source, squash=squash)
|
|
304
|
+
if not ok:
|
|
305
|
+
console.print(f"[red]branch: {message}[/red]")
|
|
306
|
+
console.print(
|
|
307
|
+
"[dim]Resolve conflicts and commit, or run `git merge --abort` to back out. "
|
|
308
|
+
"Both branches are intact.[/dim]"
|
|
309
|
+
)
|
|
310
|
+
raise typer.Exit(code=1)
|
|
311
|
+
|
|
312
|
+
console.print(f"[green]{message}[/green] [dim](local only)[/dim]")
|
|
313
|
+
console.print(
|
|
314
|
+
f"[dim]The refactor commits are now on {source}. Push when you're ready; "
|
|
315
|
+
f"drop the branch with `refactor branch drop --branch {target}`.[/dim]"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@app.command("drop")
|
|
320
|
+
def drop(
|
|
321
|
+
branch: str | None = typer.Option(None, "--branch", help="Refactor branch to drop (default: the single existing one)."),
|
|
322
|
+
yes: bool = typer.Option(False, "--yes", help="Skip the confirmation prompt."),
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Delete the refactor branch and forget its metadata (discards its commits)."""
|
|
325
|
+
root = _project_repo()
|
|
326
|
+
target = _resolve_target_branch(root, branch)
|
|
327
|
+
|
|
328
|
+
if not yes and sys.stdin.isatty() and git_scope.branch_exists(root, target):
|
|
329
|
+
if not typer.confirm(
|
|
330
|
+
f"drop {target}? This discards its refactor commits (unless already merged).",
|
|
331
|
+
default=False,
|
|
332
|
+
):
|
|
333
|
+
console.print("[dim]branch: drop aborted.[/dim]")
|
|
334
|
+
raise typer.Exit(code=1)
|
|
335
|
+
|
|
336
|
+
ok, message = drop_branch(root, target)
|
|
337
|
+
if not ok:
|
|
338
|
+
console.print(f"[red]branch: {message}.[/red]")
|
|
339
|
+
raise typer.Exit(code=2 if "check out" in message or "dirty" in message else 1)
|
|
340
|
+
console.print(f"[green]{message}.[/green]")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""`refactor request` commands: per-request listing + revert (docs/42 `R29`).
|
|
2
|
+
|
|
3
|
+
Phase 3 of the local refactor-branch flow. ``list`` surfaces each Refactor
|
|
4
|
+
Request with its ``rr_id`` and per-request commit; ``revert --id`` reverses a
|
|
5
|
+
single applied request via ``git revert`` (history-preserving) and marks it
|
|
6
|
+
``reverted``.
|
|
7
|
+
|
|
8
|
+
Local git + the central RR store only; ``refactor_core`` imports are lazy so a
|
|
9
|
+
stale installed core can never break loading the CLI.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
from refactorai_cli import git_scope
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
app = typer.Typer(
|
|
25
|
+
name="request",
|
|
26
|
+
help="Inspect and revert individual Refactor Requests (docs/42).",
|
|
27
|
+
no_args_is_help=True,
|
|
28
|
+
add_completion=False,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _project_repo(*, require_git: bool) -> Path:
|
|
33
|
+
from refactorai_cli.local_constitution import find_config
|
|
34
|
+
|
|
35
|
+
cfg = find_config(Path.cwd())
|
|
36
|
+
if cfg is None:
|
|
37
|
+
console.print("[yellow]request:[/yellow] missing refactor.config. Run `refactor init` first.")
|
|
38
|
+
raise typer.Exit(code=1)
|
|
39
|
+
root = cfg.parent
|
|
40
|
+
if require_git and not git_scope.is_git_repo(root):
|
|
41
|
+
console.print("[red]request:[/red] not inside a git repository.")
|
|
42
|
+
raise typer.Exit(code=1)
|
|
43
|
+
return root
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command("list")
|
|
47
|
+
def list_requests_cmd() -> None:
|
|
48
|
+
"""List Refactor Requests with rr_id, status, and per-request commit."""
|
|
49
|
+
root = _project_repo(require_git=False)
|
|
50
|
+
from refactor_core.refactor_requests import list_requests
|
|
51
|
+
|
|
52
|
+
rrs = list_requests(root)
|
|
53
|
+
if not rrs:
|
|
54
|
+
console.print("[dim]No refactor requests. Run `refactor review` to create them.[/dim]")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
table = Table(title="Refactor requests", show_lines=False)
|
|
58
|
+
table.add_column("rr_id", no_wrap=True)
|
|
59
|
+
table.add_column("status", no_wrap=True)
|
|
60
|
+
table.add_column("commit", no_wrap=True)
|
|
61
|
+
table.add_column("branch", no_wrap=True)
|
|
62
|
+
table.add_column("location")
|
|
63
|
+
color = {
|
|
64
|
+
"pending": "yellow",
|
|
65
|
+
"in_progress": "yellow",
|
|
66
|
+
"applied": "green",
|
|
67
|
+
"reverted": "magenta",
|
|
68
|
+
"denied": "red",
|
|
69
|
+
"failed": "red",
|
|
70
|
+
"deferred": "cyan",
|
|
71
|
+
}
|
|
72
|
+
for rr in rrs:
|
|
73
|
+
status = str(getattr(rr, "status", ""))
|
|
74
|
+
style = color.get(status, "white")
|
|
75
|
+
table.add_row(
|
|
76
|
+
rr.rr_id,
|
|
77
|
+
f"[{style}]{status}[/{style}]",
|
|
78
|
+
str(getattr(rr, "commit_sha", "") or "")[:9] or "[dim]-[/dim]",
|
|
79
|
+
str(getattr(rr, "refactor_branch", "") or "") or "[dim]-[/dim]",
|
|
80
|
+
getattr(rr, "location", ""),
|
|
81
|
+
)
|
|
82
|
+
console.print(table)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command("revert")
|
|
86
|
+
def revert(
|
|
87
|
+
id: str = typer.Option(..., "--id", help="rr_id of the request to revert."),
|
|
88
|
+
yes: bool = typer.Option(False, "--yes", help="Skip the confirmation prompt."),
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Revert a single request's commit via `git revert` and mark it reverted."""
|
|
91
|
+
root = _project_repo(require_git=True)
|
|
92
|
+
from refactor_core.refactor_requests import REVERTED, load_request, save_request
|
|
93
|
+
|
|
94
|
+
rr = load_request(root, id)
|
|
95
|
+
if rr is None:
|
|
96
|
+
console.print(f"[red]request:[/red] no request with rr_id '{id}'.")
|
|
97
|
+
raise typer.Exit(code=1)
|
|
98
|
+
|
|
99
|
+
sha = str(getattr(rr, "commit_sha", "") or "") or (git_scope.commit_sha_for_rr(root, id) or "")
|
|
100
|
+
if not sha:
|
|
101
|
+
console.print(
|
|
102
|
+
f"[red]request:[/red] no per-request commit recorded for '{id}'. "
|
|
103
|
+
"Only requests resolved on a refactor branch can be reverted this way."
|
|
104
|
+
)
|
|
105
|
+
raise typer.Exit(code=1)
|
|
106
|
+
|
|
107
|
+
if getattr(rr, "reverted", False):
|
|
108
|
+
console.print(f"[yellow]request:[/yellow] '{id}' is already reverted.")
|
|
109
|
+
raise typer.Exit(code=0)
|
|
110
|
+
|
|
111
|
+
if not yes and sys.stdin.isatty():
|
|
112
|
+
if not typer.confirm(
|
|
113
|
+
f"revert request {id} (commit {sha[:9]}) via git revert?", default=False
|
|
114
|
+
):
|
|
115
|
+
console.print("[dim]request: revert aborted.[/dim]")
|
|
116
|
+
raise typer.Exit(code=1)
|
|
117
|
+
|
|
118
|
+
ok, output = git_scope.revert_commit(root, sha)
|
|
119
|
+
if not ok:
|
|
120
|
+
console.print(f"[red]request: git revert failed / conflicted.[/red]\n[dim]{output}[/dim]")
|
|
121
|
+
console.print(
|
|
122
|
+
"[dim]Resolve conflicts and commit, or run `git revert --abort` to back out.[/dim]"
|
|
123
|
+
)
|
|
124
|
+
raise typer.Exit(code=1)
|
|
125
|
+
|
|
126
|
+
rr.reverted = True
|
|
127
|
+
rr.status = REVERTED
|
|
128
|
+
save_request(root, rr)
|
|
129
|
+
console.print(f"[green]Reverted[/green] request {id} (revert of {sha[:9]}).")
|