gitwise-cli 0.24.2__py3-none-any.whl
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.
- gitwise/__init__.py +11 -0
- gitwise/__main__.py +113 -0
- gitwise/_cli_completions.py +88 -0
- gitwise/_cli_dispatch.py +469 -0
- gitwise/_cli_introspection.py +275 -0
- gitwise/_cli_parser.py +345 -0
- gitwise/_cli_setup_agents.py +439 -0
- gitwise/_i18n_data.json +1934 -0
- gitwise/_paths.py +22 -0
- gitwise/_runtime_config.py +246 -0
- gitwise/audit.py +338 -0
- gitwise/branches.py +183 -0
- gitwise/clean.py +197 -0
- gitwise/commit.py +142 -0
- gitwise/conflicts.py +112 -0
- gitwise/context.py +163 -0
- gitwise/design.py +383 -0
- gitwise/diff.py +309 -0
- gitwise/doctor.py +116 -0
- gitwise/git.py +254 -0
- gitwise/health.py +345 -0
- gitwise/i18n.py +99 -0
- gitwise/log.py +329 -0
- gitwise/merge.py +193 -0
- gitwise/optimize.py +212 -0
- gitwise/output.py +652 -0
- gitwise/pick.py +102 -0
- gitwise/pr.py +543 -0
- gitwise/py.typed +0 -0
- gitwise/schema.py +49 -0
- gitwise/setup.py +551 -0
- gitwise/setup_agents/__init__.py +36 -0
- gitwise/setup_agents/adapters/__init__.py +17 -0
- gitwise/setup_agents/adapters/aider.py +5 -0
- gitwise/setup_agents/adapters/base.py +5 -0
- gitwise/setup_agents/adapters/codex.py +5 -0
- gitwise/setup_agents/adapters/continue_adapter.py +5 -0
- gitwise/setup_agents/adapters/cursor.py +5 -0
- gitwise/setup_agents/adapters/opencode.py +5 -0
- gitwise/setup_agents/adapters/pi.py +5 -0
- gitwise/setup_agents/exec.py +449 -0
- gitwise/setup_agents/format.py +164 -0
- gitwise/setup_agents/plan.py +254 -0
- gitwise/setup_agents/plan_gitfiles.py +167 -0
- gitwise/setup_agents/plan_skills.py +256 -0
- gitwise/setup_agents/providers/__init__.py +96 -0
- gitwise/setup_agents/providers/aider.py +11 -0
- gitwise/setup_agents/providers/base.py +79 -0
- gitwise/setup_agents/providers/claude.py +408 -0
- gitwise/setup_agents/providers/codex.py +11 -0
- gitwise/setup_agents/providers/continue_adapter.py +11 -0
- gitwise/setup_agents/providers/cursor.py +11 -0
- gitwise/setup_agents/providers/opencode.py +11 -0
- gitwise/setup_agents/providers/pi.py +11 -0
- gitwise/setup_agents/state.py +141 -0
- gitwise/setup_agents/types.py +48 -0
- gitwise/share/agents/skills/git-audit/SKILL.md +25 -0
- gitwise/share/agents/skills/git-clean/SKILL.md +22 -0
- gitwise/share/agents/skills/git-optimize/SKILL.md +21 -0
- gitwise/share/aider/CONVENTIONS.md.template +8 -0
- gitwise/share/aider/aider.conf.yml.template +4 -0
- gitwise/share/claude/CLAUDE.md.template +9 -0
- gitwise/share/claude/rules/gitwise.md +16 -0
- gitwise/share/claude/settings.json.template +47 -0
- gitwise/share/claude/skills/git-audit/SKILL.md +25 -0
- gitwise/share/claude/skills/git-clean/SKILL.md +22 -0
- gitwise/share/claude/skills/git-optimize/SKILL.md +21 -0
- gitwise/share/codex/agents/gitwise.toml.template +18 -0
- gitwise/share/continue/rules/gitwise.md.template +14 -0
- gitwise/share/cursor/rules/gitwise.mdc.template +16 -0
- gitwise/share/git-config-modern.txt +48 -0
- gitwise/share/hooks/commit-msg +22 -0
- gitwise/share/hooks/pre-commit +19 -0
- gitwise/share/opencode/agents/gitwise.md.template +14 -0
- gitwise/share/pi/skills/gitwise.md.template +14 -0
- gitwise/share/schemas/v1/input/audit.json +40 -0
- gitwise/share/schemas/v1/input/branches.json +51 -0
- gitwise/share/schemas/v1/input/clean.json +52 -0
- gitwise/share/schemas/v1/input/commands.json +36 -0
- gitwise/share/schemas/v1/input/commit.json +63 -0
- gitwise/share/schemas/v1/input/completions.json +51 -0
- gitwise/share/schemas/v1/input/conflicts.json +46 -0
- gitwise/share/schemas/v1/input/context.json +36 -0
- gitwise/share/schemas/v1/input/diff.json +56 -0
- gitwise/share/schemas/v1/input/doctor.json +36 -0
- gitwise/share/schemas/v1/input/health.json +36 -0
- gitwise/share/schemas/v1/input/log.json +71 -0
- gitwise/share/schemas/v1/input/merge.json +63 -0
- gitwise/share/schemas/v1/input/optimize.json +44 -0
- gitwise/share/schemas/v1/input/pick.json +63 -0
- gitwise/share/schemas/v1/input/pr.json +51 -0
- gitwise/share/schemas/v1/input/schema.json +48 -0
- gitwise/share/schemas/v1/input/setup-agents.json +108 -0
- gitwise/share/schemas/v1/input/setup.json +55 -0
- gitwise/share/schemas/v1/input/show.json +46 -0
- gitwise/share/schemas/v1/input/snapshot.json +36 -0
- gitwise/share/schemas/v1/input/stash.json +68 -0
- gitwise/share/schemas/v1/input/status.json +36 -0
- gitwise/share/schemas/v1/input/suggest.json +36 -0
- gitwise/share/schemas/v1/input/summarize.json +44 -0
- gitwise/share/schemas/v1/input/sync.json +55 -0
- gitwise/share/schemas/v1/input/tag.json +73 -0
- gitwise/share/schemas/v1/input/undo.json +60 -0
- gitwise/share/schemas/v1/input/update.json +40 -0
- gitwise/share/schemas/v1/input/worktree.json +50 -0
- gitwise/show.py +118 -0
- gitwise/snapshot.py +110 -0
- gitwise/stash.py +188 -0
- gitwise/status.py +93 -0
- gitwise/suggest.py +148 -0
- gitwise/summarize.py +202 -0
- gitwise/sync.py +257 -0
- gitwise/tag.py +252 -0
- gitwise/undo.py +145 -0
- gitwise/update.py +42 -0
- gitwise/utils/__init__.py +1 -0
- gitwise/utils/git_output.py +51 -0
- gitwise/utils/json_envelope.py +58 -0
- gitwise/utils/parsing.py +34 -0
- gitwise/worktree.py +182 -0
- gitwise_cli-0.24.2.dist-info/METADATA +151 -0
- gitwise_cli-0.24.2.dist-info/RECORD +125 -0
- gitwise_cli-0.24.2.dist-info/WHEEL +4 -0
- gitwise_cli-0.24.2.dist-info/entry_points.txt +2 -0
- gitwise_cli-0.24.2.dist-info/licenses/LICENSE +21 -0
gitwise/output.py
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
"""Output: Rich-powered human-readable + plain JSON. Detects bat/delta."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
from rich.theme import Theme
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
from rich.text import Text
|
|
25
|
+
from rich.theme import Theme
|
|
26
|
+
|
|
27
|
+
_HAS_RICH = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
_HAS_RICH = False
|
|
30
|
+
|
|
31
|
+
from ._runtime_config import get_runtime_config
|
|
32
|
+
from .design import ColorDepth, pad_right, truncate, visible_length
|
|
33
|
+
from .i18n import confirm_responses, t
|
|
34
|
+
|
|
35
|
+
_COLOR_SYSTEM_MAP: dict[ColorDepth, str] = {
|
|
36
|
+
"truecolor": "truecolor",
|
|
37
|
+
"256color": "256",
|
|
38
|
+
"16color": "16",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_LOG_JSON = os.environ.get("GITWISE_LOG_JSON", "").lower() in ("1", "true")
|
|
42
|
+
_JSON_PRETTY = os.environ.get("GITWISE_JSON_PRETTY", "").lower() in ("1", "true")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _structured_log(level: str, msg: str, **kwargs: Any) -> None:
|
|
46
|
+
entry: dict[str, Any] = {
|
|
47
|
+
"ts": time.time(),
|
|
48
|
+
"level": level,
|
|
49
|
+
"msg": msg,
|
|
50
|
+
}
|
|
51
|
+
if kwargs:
|
|
52
|
+
entry.update(kwargs)
|
|
53
|
+
sys.stderr.write(json.dumps(entry, default=str) + "\n")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_json_pretty(pretty: bool) -> None:
|
|
57
|
+
global _JSON_PRETTY
|
|
58
|
+
_JSON_PRETTY = pretty
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _ModuleAttr:
|
|
62
|
+
__slots__ = ("_name",)
|
|
63
|
+
|
|
64
|
+
def __init__(self, name: str) -> None:
|
|
65
|
+
self._name = name
|
|
66
|
+
|
|
67
|
+
def __bool__(self) -> bool:
|
|
68
|
+
return bool(getattr(get_runtime_config(), self._name))
|
|
69
|
+
|
|
70
|
+
def __repr__(self) -> str:
|
|
71
|
+
return repr(bool(self))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
HAS_BAT = _ModuleAttr("has_bat")
|
|
75
|
+
HAS_DELTA = _ModuleAttr("has_delta")
|
|
76
|
+
IS_TTY = _ModuleAttr("is_tty")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _build_rich_theme() -> dict[str, str]:
|
|
80
|
+
tokens = get_runtime_config().theme_tokens
|
|
81
|
+
return {
|
|
82
|
+
"fg": tokens.fg,
|
|
83
|
+
"secondary": tokens.secondary,
|
|
84
|
+
"dim": tokens.dim or tokens.secondary,
|
|
85
|
+
"accent": tokens.accent,
|
|
86
|
+
"success": tokens.success,
|
|
87
|
+
"error": tokens.error,
|
|
88
|
+
"warning": tokens.warning or tokens.accent,
|
|
89
|
+
"brand": tokens.brand or tokens.accent,
|
|
90
|
+
"bold": f"bold {tokens.fg}",
|
|
91
|
+
"header": f"bold {tokens.fg}",
|
|
92
|
+
"rule": tokens.dim or tokens.secondary,
|
|
93
|
+
"bold.accent": f"bold {tokens.accent}",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _color_disabled() -> bool:
|
|
98
|
+
return os.environ.get("NO_COLOR", "") != "" or os.environ.get(
|
|
99
|
+
"GITWISE_NO_COLOR", ""
|
|
100
|
+
).lower() in ("1", "true")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _color_forced() -> bool:
|
|
104
|
+
return (
|
|
105
|
+
os.environ.get("CLICOLOR_FORCE", "").lower()
|
|
106
|
+
in (
|
|
107
|
+
"1",
|
|
108
|
+
"true",
|
|
109
|
+
)
|
|
110
|
+
or os.environ.get("FORCE_COLOR", "") != ""
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _use_rich() -> bool:
|
|
115
|
+
if not _HAS_RICH:
|
|
116
|
+
return False
|
|
117
|
+
if _color_disabled():
|
|
118
|
+
return False
|
|
119
|
+
if _color_forced():
|
|
120
|
+
return True
|
|
121
|
+
return get_runtime_config().is_tty
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
_use_rich_cached: bool | None = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _should_use_rich() -> bool:
|
|
128
|
+
global _use_rich_cached
|
|
129
|
+
if _use_rich_cached is None:
|
|
130
|
+
_use_rich_cached = _use_rich()
|
|
131
|
+
return _use_rich_cached
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _make_console(*, file: Any = sys.stdout, force: bool = False) -> Console:
|
|
135
|
+
cfg = get_runtime_config()
|
|
136
|
+
depth = cfg.color_depth
|
|
137
|
+
console = Console(
|
|
138
|
+
theme=Theme(_build_rich_theme()),
|
|
139
|
+
color_system=_COLOR_SYSTEM_MAP.get(depth, "auto"), # pyright: ignore[reportArgumentType]
|
|
140
|
+
no_color=None,
|
|
141
|
+
force_terminal=force,
|
|
142
|
+
width=cfg.terminal_width,
|
|
143
|
+
legacy_windows=False,
|
|
144
|
+
highlight=False,
|
|
145
|
+
emoji=False,
|
|
146
|
+
markup=False,
|
|
147
|
+
file=file,
|
|
148
|
+
)
|
|
149
|
+
if cfg.debug:
|
|
150
|
+
sys.stderr.write(
|
|
151
|
+
f"[gitwise debug] console: force_terminal={force}, "
|
|
152
|
+
f"color_system={console.color_system}, "
|
|
153
|
+
f"is_terminal={console.is_terminal}, "
|
|
154
|
+
f"no_color={console.no_color}, "
|
|
155
|
+
f"theme={cfg.theme}, depth={depth}, "
|
|
156
|
+
f"is_tty={cfg.is_tty}\n"
|
|
157
|
+
)
|
|
158
|
+
return console
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
_console: Console | None = None
|
|
162
|
+
_stderr_console: Console | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _get_console() -> Console:
|
|
166
|
+
global _console
|
|
167
|
+
if _console is None:
|
|
168
|
+
_console = _make_console(force=True)
|
|
169
|
+
return _console
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _get_stderr_console() -> Console:
|
|
173
|
+
global _stderr_console
|
|
174
|
+
if _stderr_console is None:
|
|
175
|
+
_stderr_console = _make_console(file=sys.stderr, force=True)
|
|
176
|
+
return _stderr_console
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def info(msg: str) -> None:
|
|
180
|
+
if _should_use_rich():
|
|
181
|
+
_get_console().print(Text(msg, style="dim"))
|
|
182
|
+
else:
|
|
183
|
+
print(msg)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def warn(msg: str) -> None:
|
|
187
|
+
if _LOG_JSON:
|
|
188
|
+
_structured_log("warn", msg)
|
|
189
|
+
return
|
|
190
|
+
prefix = t("warning_label")
|
|
191
|
+
if _should_use_rich():
|
|
192
|
+
text = Text()
|
|
193
|
+
text.append(f"{prefix}: ", style="warning")
|
|
194
|
+
text.append(msg)
|
|
195
|
+
_get_stderr_console().print(text)
|
|
196
|
+
else:
|
|
197
|
+
print(f"{prefix}: {msg}", file=sys.stderr)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def error(msg: str, *, hint: str | None = None) -> None:
|
|
201
|
+
if _LOG_JSON:
|
|
202
|
+
if hint:
|
|
203
|
+
_structured_log("error", msg, hint=hint)
|
|
204
|
+
else:
|
|
205
|
+
_structured_log("error", msg)
|
|
206
|
+
return
|
|
207
|
+
prefix = t("error")
|
|
208
|
+
if _should_use_rich():
|
|
209
|
+
text = Text()
|
|
210
|
+
text.append(f"{prefix}: ", style="error")
|
|
211
|
+
text.append(msg)
|
|
212
|
+
_get_stderr_console().print(text)
|
|
213
|
+
if hint:
|
|
214
|
+
hint_text = Text()
|
|
215
|
+
hint_text.append(f"{t('hint_prefix')}: ", style="dim")
|
|
216
|
+
hint_text.append(hint, style="dim")
|
|
217
|
+
_get_stderr_console().print(hint_text)
|
|
218
|
+
else:
|
|
219
|
+
print(f"{prefix}: {msg}", file=sys.stderr)
|
|
220
|
+
if hint:
|
|
221
|
+
print(f"{t('hint_prefix')}: {hint}", file=sys.stderr)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def ok(msg: str) -> None:
|
|
225
|
+
prefix = t("ok_prefix")
|
|
226
|
+
if _should_use_rich():
|
|
227
|
+
text = Text()
|
|
228
|
+
text.append(f"{prefix} ", style="success")
|
|
229
|
+
text.append(msg)
|
|
230
|
+
_get_console().print(text)
|
|
231
|
+
else:
|
|
232
|
+
print(f"{prefix} {msg}")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def debug(msg: str) -> None:
|
|
236
|
+
if not get_runtime_config().debug:
|
|
237
|
+
return
|
|
238
|
+
if _should_use_rich():
|
|
239
|
+
text = Text()
|
|
240
|
+
text.append("[gitwise debug] ", style="dim")
|
|
241
|
+
text.append(msg)
|
|
242
|
+
_get_stderr_console().print(text)
|
|
243
|
+
else:
|
|
244
|
+
print(f"[gitwise debug] {msg}", file=sys.stderr)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def print_json(data: Any) -> None:
|
|
248
|
+
if _JSON_PRETTY:
|
|
249
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
250
|
+
return
|
|
251
|
+
print(json.dumps(data, ensure_ascii=False, separators=(",", ":")))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def print_blank() -> None:
|
|
255
|
+
if _should_use_rich():
|
|
256
|
+
_get_console().print()
|
|
257
|
+
else:
|
|
258
|
+
print()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def confirm(prompt: str) -> bool:
|
|
262
|
+
if not sys.stdin.isatty():
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
if _should_use_rich():
|
|
266
|
+
try:
|
|
267
|
+
import importlib
|
|
268
|
+
|
|
269
|
+
prompt_mod = importlib.import_module("rich.prompt")
|
|
270
|
+
resp = prompt_mod.Prompt.ask(
|
|
271
|
+
prompt,
|
|
272
|
+
default="",
|
|
273
|
+
show_default=False,
|
|
274
|
+
show_choices=False,
|
|
275
|
+
console=_get_console(),
|
|
276
|
+
)
|
|
277
|
+
return resp.strip().lower() in confirm_responses()
|
|
278
|
+
except (EOFError, KeyboardInterrupt):
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
resp = input(prompt).strip().lower()
|
|
283
|
+
except (EOFError, KeyboardInterrupt):
|
|
284
|
+
return False
|
|
285
|
+
return resp in confirm_responses()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@contextmanager
|
|
289
|
+
def status(message: str) -> Iterator[None]:
|
|
290
|
+
if _should_use_rich():
|
|
291
|
+
with _get_console().status(message):
|
|
292
|
+
yield
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
if get_runtime_config().debug:
|
|
296
|
+
print_dim(message)
|
|
297
|
+
yield
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def bat_pipe(text: str, language: str = "plain") -> None:
|
|
301
|
+
if not text:
|
|
302
|
+
return
|
|
303
|
+
if not text.endswith("\n"):
|
|
304
|
+
text += "\n"
|
|
305
|
+
if language is None or language == "plain":
|
|
306
|
+
for line in text.splitlines():
|
|
307
|
+
print(line)
|
|
308
|
+
return
|
|
309
|
+
cfg = get_runtime_config()
|
|
310
|
+
if cfg.has_bat and cfg.is_tty:
|
|
311
|
+
color_flag = "always" if not _color_disabled() else "never"
|
|
312
|
+
cmd = [
|
|
313
|
+
"bat",
|
|
314
|
+
"--style=plain",
|
|
315
|
+
"--pager=never",
|
|
316
|
+
f"--color={color_flag}",
|
|
317
|
+
"--language",
|
|
318
|
+
language,
|
|
319
|
+
]
|
|
320
|
+
try:
|
|
321
|
+
r = subprocess.run(
|
|
322
|
+
cmd, input=text, text=True, check=False, stderr=subprocess.DEVNULL, timeout=120
|
|
323
|
+
)
|
|
324
|
+
if r.returncode == 0:
|
|
325
|
+
return
|
|
326
|
+
except OSError as e:
|
|
327
|
+
if get_runtime_config().debug:
|
|
328
|
+
sys.stderr.write(f"[gitwise debug] bat execution failed: {e}\n")
|
|
329
|
+
print(text, end="")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def print_header(text: str) -> None:
|
|
333
|
+
if _should_use_rich():
|
|
334
|
+
_get_console().print(text, style="bold")
|
|
335
|
+
else:
|
|
336
|
+
print(text)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def print_section(title: str) -> None:
|
|
340
|
+
if _should_use_rich():
|
|
341
|
+
print_blank()
|
|
342
|
+
_get_console().rule(f" {title} ", style="accent")
|
|
343
|
+
else:
|
|
344
|
+
print_blank()
|
|
345
|
+
print(f" {title}")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def print_bullet(text: str, *, icon: str = "•", accent: bool = False, indent: int = 2) -> None:
|
|
349
|
+
prefix = " " * max(0, indent)
|
|
350
|
+
if _should_use_rich():
|
|
351
|
+
row = Text()
|
|
352
|
+
row.append(prefix)
|
|
353
|
+
row.append(icon, style="accent" if accent else "secondary")
|
|
354
|
+
row.append(" ")
|
|
355
|
+
row.append(text)
|
|
356
|
+
_get_console().print(row)
|
|
357
|
+
else:
|
|
358
|
+
print(f"{prefix}{icon} {text}")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _commit_type_style(message: str) -> str:
|
|
362
|
+
msg = message.strip().lower()
|
|
363
|
+
match = re.match(r"^([a-z]+)(\([^)]*\))?(!)?:", msg)
|
|
364
|
+
if not match:
|
|
365
|
+
if msg.startswith("merge "):
|
|
366
|
+
return "dim"
|
|
367
|
+
return "fg"
|
|
368
|
+
commit_type = match.group(1)
|
|
369
|
+
if commit_type == "feat":
|
|
370
|
+
return "success"
|
|
371
|
+
if commit_type == "fix":
|
|
372
|
+
return "warning"
|
|
373
|
+
if commit_type in {"docs", "style", "test", "ci", "build"}:
|
|
374
|
+
return "accent"
|
|
375
|
+
if commit_type in {"refactor", "perf"}:
|
|
376
|
+
return "brand"
|
|
377
|
+
if commit_type in {"revert"}:
|
|
378
|
+
return "error"
|
|
379
|
+
return "fg"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def print_commit_line(line: str, *, indent: int = 2) -> None:
|
|
383
|
+
prefix = " " * max(0, indent)
|
|
384
|
+
parts = line.strip().split(" ", 1)
|
|
385
|
+
if len(parts) != 2:
|
|
386
|
+
print_bullet(line.strip(), icon="-", accent=False, indent=indent)
|
|
387
|
+
return
|
|
388
|
+
short_hash, subject = parts[0], parts[1]
|
|
389
|
+
if not re.fullmatch(r"[0-9a-f]{7,40}", short_hash):
|
|
390
|
+
print_bullet(line.strip(), icon="-", accent=False, indent=indent)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
if _should_use_rich():
|
|
394
|
+
text = Text()
|
|
395
|
+
text.append(prefix)
|
|
396
|
+
text.append(short_hash, style="secondary")
|
|
397
|
+
text.append(" ", style="dim")
|
|
398
|
+
text.append(subject, style=_commit_type_style(subject))
|
|
399
|
+
_get_console().print(text)
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
print(f"{prefix}- {short_hash} {subject}")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _status_style(code: str) -> str:
|
|
406
|
+
normalized = code.strip().upper()
|
|
407
|
+
if normalized in {"??", "A", "M", "R", "C", "T", "U", "D"}:
|
|
408
|
+
if normalized in {"D", "U"}:
|
|
409
|
+
return "error"
|
|
410
|
+
if normalized in {"M", "R", "C", "T"}:
|
|
411
|
+
return "warning"
|
|
412
|
+
return "success"
|
|
413
|
+
return "secondary"
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _path_style_for_status(code: str) -> str:
|
|
417
|
+
normalized = code.strip().upper()
|
|
418
|
+
if normalized in {"??", "A"}:
|
|
419
|
+
return "success"
|
|
420
|
+
if normalized in {"D", "U"}:
|
|
421
|
+
return "error"
|
|
422
|
+
if normalized in {"M", "R", "C", "T"}:
|
|
423
|
+
return "warning"
|
|
424
|
+
return "fg"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def print_file_status(code: str, path: str, *, indent: int = 2) -> None:
|
|
428
|
+
status = code.strip() or "--"
|
|
429
|
+
prefix = " " * max(0, indent)
|
|
430
|
+
if _should_use_rich():
|
|
431
|
+
text = Text()
|
|
432
|
+
text.append(prefix)
|
|
433
|
+
text.append(status.rjust(2), style=_status_style(status))
|
|
434
|
+
text.append(" ", style="dim")
|
|
435
|
+
text.append(path, style=_path_style_for_status(status))
|
|
436
|
+
_get_console().print(text)
|
|
437
|
+
else:
|
|
438
|
+
print(f"{prefix}{status.rjust(2)} {path}")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _append_diffstat_changes(text: Text, changes: str) -> None:
|
|
442
|
+
for char in changes:
|
|
443
|
+
if char == "+":
|
|
444
|
+
text.append(char, style="success")
|
|
445
|
+
elif char == "-":
|
|
446
|
+
text.append(char, style="error")
|
|
447
|
+
else:
|
|
448
|
+
text.append(char, style="secondary")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def print_diffstat(title: str, entries: list[dict[str, str]]) -> None:
|
|
452
|
+
if not entries:
|
|
453
|
+
return
|
|
454
|
+
if _should_use_rich():
|
|
455
|
+
print_header(title)
|
|
456
|
+
width = get_runtime_config().terminal_width
|
|
457
|
+
path_col = max(24, min(width // 2, max(visible_length(e["path"]) for e in entries)))
|
|
458
|
+
for entry in entries:
|
|
459
|
+
path = truncate(entry["path"], path_col)
|
|
460
|
+
padded_path = pad_right(path, path_col)
|
|
461
|
+
status = entry.get("status", "M")
|
|
462
|
+
changes = entry.get("changes", "")
|
|
463
|
+
row = Text()
|
|
464
|
+
row.append(" ")
|
|
465
|
+
row.append(padded_path, style=_path_style_for_status(status))
|
|
466
|
+
row.append(" ", style="dim")
|
|
467
|
+
_append_diffstat_changes(row, changes)
|
|
468
|
+
_get_console().print(row)
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
print(title)
|
|
472
|
+
for entry in entries:
|
|
473
|
+
print(f" {entry['path']} {entry.get('changes', '')}")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def print_summary_box(title: str, lines: list[str]) -> None:
|
|
477
|
+
if not lines:
|
|
478
|
+
return
|
|
479
|
+
if _should_use_rich():
|
|
480
|
+
_get_console().rule(f" {title} ", style="dim")
|
|
481
|
+
for line in lines:
|
|
482
|
+
row = Text()
|
|
483
|
+
row.append(" ", style="dim")
|
|
484
|
+
row.append(line)
|
|
485
|
+
_get_console().print(row)
|
|
486
|
+
return
|
|
487
|
+
print(title)
|
|
488
|
+
for line in lines:
|
|
489
|
+
print(f" {line}")
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def print_bracket(label: str, value: str = "") -> None:
|
|
493
|
+
if _should_use_rich():
|
|
494
|
+
text = Text()
|
|
495
|
+
text.append(" [", style="secondary")
|
|
496
|
+
text.append(label, style="accent")
|
|
497
|
+
text.append("]", style="secondary")
|
|
498
|
+
if value:
|
|
499
|
+
text.append(f" {value}")
|
|
500
|
+
_get_console().print(text)
|
|
501
|
+
else:
|
|
502
|
+
if value:
|
|
503
|
+
print(f" [{label}] {value}")
|
|
504
|
+
else:
|
|
505
|
+
print(f" [{label}]")
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def print_accent(text: str) -> None:
|
|
509
|
+
if _should_use_rich():
|
|
510
|
+
_get_console().print(text, style="accent")
|
|
511
|
+
else:
|
|
512
|
+
print(text)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def print_dim(text: str) -> None:
|
|
516
|
+
if _should_use_rich():
|
|
517
|
+
_get_console().print(text, style="dim")
|
|
518
|
+
else:
|
|
519
|
+
print(text)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def print_success(text: str) -> None:
|
|
523
|
+
if _should_use_rich():
|
|
524
|
+
_get_console().print(text, style="success")
|
|
525
|
+
else:
|
|
526
|
+
print(text)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def print_error_styled(text: str) -> None:
|
|
530
|
+
if _should_use_rich():
|
|
531
|
+
_get_console().print(text, style="error")
|
|
532
|
+
else:
|
|
533
|
+
print(text)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def print_kv(key: str, value: str) -> None:
|
|
537
|
+
width = get_runtime_config().terminal_width
|
|
538
|
+
key_col = min(24, width // 3)
|
|
539
|
+
padded_key = pad_right(f" {key}", key_col)
|
|
540
|
+
if _should_use_rich():
|
|
541
|
+
text = Text()
|
|
542
|
+
text.append(padded_key, style="secondary")
|
|
543
|
+
text.append(f" {value}")
|
|
544
|
+
_get_console().print(text)
|
|
545
|
+
else:
|
|
546
|
+
print(f"{padded_key} {value}")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def print_status_line(icon: str, label: str, status: str, ok_flag: bool = True) -> None:
|
|
550
|
+
if _should_use_rich():
|
|
551
|
+
console = _get_console()
|
|
552
|
+
width = get_runtime_config().terminal_width
|
|
553
|
+
icon_style = "success" if ok_flag else "error"
|
|
554
|
+
text = Text()
|
|
555
|
+
text.append(f" {icon} ", style=icon_style)
|
|
556
|
+
text.append(label, style="secondary")
|
|
557
|
+
used = visible_length(f" {icon} {label}") + visible_length(status) + 3
|
|
558
|
+
dots = max(1, width - used)
|
|
559
|
+
text.append(" " + "." * dots + " ", style="dim")
|
|
560
|
+
text.append(status, style=icon_style)
|
|
561
|
+
console.print(text)
|
|
562
|
+
else:
|
|
563
|
+
width = get_runtime_config().terminal_width
|
|
564
|
+
base = f" {icon} {label}"
|
|
565
|
+
if not status:
|
|
566
|
+
print(base)
|
|
567
|
+
return
|
|
568
|
+
used = visible_length(base) + visible_length(status) + 2
|
|
569
|
+
dots = max(1, width - used)
|
|
570
|
+
print(f"{base} {'·' * dots} {status}")
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def print_table(
|
|
574
|
+
title: str,
|
|
575
|
+
columns: list[tuple[str, str]],
|
|
576
|
+
rows: list[list[str]],
|
|
577
|
+
*,
|
|
578
|
+
column_styles: list[str] | None = None,
|
|
579
|
+
highlight_rows: set[int] | None = None,
|
|
580
|
+
no_wrap_columns: set[int] | None = None,
|
|
581
|
+
min_widths: dict[int, int] | None = None,
|
|
582
|
+
max_widths: dict[int, int] | None = None,
|
|
583
|
+
overflow_columns: dict[int, Literal["fold", "crop", "ellipsis"]] | None = None,
|
|
584
|
+
column_ratios: dict[int, int] | None = None,
|
|
585
|
+
) -> None:
|
|
586
|
+
if not _should_use_rich() or not rows:
|
|
587
|
+
if title:
|
|
588
|
+
print(title)
|
|
589
|
+
if not rows:
|
|
590
|
+
return
|
|
591
|
+
num_cols = len(columns)
|
|
592
|
+
col_widths = [len(c[0]) for c in columns]
|
|
593
|
+
for row in rows:
|
|
594
|
+
for i, cell in enumerate(row):
|
|
595
|
+
if i < num_cols:
|
|
596
|
+
col_widths[i] = max(col_widths[i], visible_length(cell))
|
|
597
|
+
headers = [columns[i][0] for i in range(num_cols)]
|
|
598
|
+
header_row = " ".join(pad_right(headers[i], col_widths[i]) for i in range(num_cols))
|
|
599
|
+
sep_row = " ".join("-" * col_widths[i] for i in range(num_cols))
|
|
600
|
+
print(f" {header_row}")
|
|
601
|
+
print(f" {sep_row}")
|
|
602
|
+
highlights = highlight_rows or set()
|
|
603
|
+
for idx, row in enumerate(rows):
|
|
604
|
+
prefix = " * " if idx in highlights else " "
|
|
605
|
+
padded = " ".join(
|
|
606
|
+
pad_right(row[i], col_widths[i]) if i < num_cols else row[i]
|
|
607
|
+
for i in range(len(row))
|
|
608
|
+
)
|
|
609
|
+
print(f"{prefix}{padded}")
|
|
610
|
+
return
|
|
611
|
+
|
|
612
|
+
console = _get_console()
|
|
613
|
+
table = Table(
|
|
614
|
+
title=title,
|
|
615
|
+
title_style="bold",
|
|
616
|
+
border_style="dim",
|
|
617
|
+
show_header=True,
|
|
618
|
+
header_style="bold.accent",
|
|
619
|
+
pad_edge=False,
|
|
620
|
+
padding=(0, 1),
|
|
621
|
+
expand=True,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
no_wrap = no_wrap_columns or set()
|
|
625
|
+
mins = min_widths or {}
|
|
626
|
+
maxs = max_widths or {}
|
|
627
|
+
overflows = overflow_columns or {}
|
|
628
|
+
ratios = column_ratios or {}
|
|
629
|
+
|
|
630
|
+
for idx, (col_name, _col_key) in enumerate(columns):
|
|
631
|
+
no_wrap_value = (idx in no_wrap) if no_wrap_columns is not None else True
|
|
632
|
+
overflow_value: Literal["fold", "crop", "ellipsis"] = overflows.get(idx, "ellipsis")
|
|
633
|
+
table.add_column(
|
|
634
|
+
col_name,
|
|
635
|
+
no_wrap=no_wrap_value,
|
|
636
|
+
min_width=mins.get(idx),
|
|
637
|
+
max_width=maxs.get(idx),
|
|
638
|
+
overflow=overflow_value,
|
|
639
|
+
ratio=ratios.get(idx),
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
styles = column_styles or []
|
|
643
|
+
highlights = highlight_rows or set()
|
|
644
|
+
|
|
645
|
+
for idx, row_data in enumerate(rows):
|
|
646
|
+
if idx in highlights:
|
|
647
|
+
table.add_row(*row_data, style="accent")
|
|
648
|
+
else:
|
|
649
|
+
row_style = styles[idx % len(styles)] if styles else ""
|
|
650
|
+
table.add_row(*row_data, style=row_style)
|
|
651
|
+
|
|
652
|
+
console.print(table)
|