jj-stack 0.1.0__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.
- jj_stack/__init__.py +11 -0
- jj_stack/__main__.py +8 -0
- jj_stack/bootstrap.py +213 -0
- jj_stack/cli.py +1297 -0
- jj_stack/cli_help.py +680 -0
- jj_stack/commands/__init__.py +1 -0
- jj_stack/commands/_cleanup_actions.py +389 -0
- jj_stack/commands/_json_status.py +84 -0
- jj_stack/commands/checkout.py +668 -0
- jj_stack/commands/cleanup/command.py +702 -0
- jj_stack/commands/cleanup/shared.py +62 -0
- jj_stack/commands/cleanup/stale.py +79 -0
- jj_stack/commands/doctor.py +358 -0
- jj_stack/commands/in_use.py +31 -0
- jj_stack/commands/list_.py +667 -0
- jj_stack/commands/merge/__init__.py +1 -0
- jj_stack/commands/merge/command.py +385 -0
- jj_stack/commands/merge/github_stack.py +333 -0
- jj_stack/commands/merge/models.py +97 -0
- jj_stack/commands/merge/plan.py +109 -0
- jj_stack/commands/merge/preconditions.py +162 -0
- jj_stack/commands/merge/render.py +52 -0
- jj_stack/commands/relink.py +210 -0
- jj_stack/commands/submit/__init__.py +1 -0
- jj_stack/commands/submit/auto_close.py +108 -0
- jj_stack/commands/submit/changes.py +90 -0
- jj_stack/commands/submit/command.py +780 -0
- jj_stack/commands/submit/descriptions.py +570 -0
- jj_stack/commands/submit/github_stack.py +139 -0
- jj_stack/commands/submit/inputs.py +168 -0
- jj_stack/commands/submit/models.py +198 -0
- jj_stack/commands/submit/overview_comments.py +178 -0
- jj_stack/commands/submit/prs.py +461 -0
- jj_stack/commands/submit/render.py +129 -0
- jj_stack/commands/sync.py +510 -0
- jj_stack/commands/sync_apply.py +474 -0
- jj_stack/commands/unstack.py +321 -0
- jj_stack/commands/view.py +1151 -0
- jj_stack/completion.py +484 -0
- jj_stack/concurrency.py +93 -0
- jj_stack/config.py +170 -0
- jj_stack/console.py +708 -0
- jj_stack/errors.py +152 -0
- jj_stack/formatting.py +95 -0
- jj_stack/github/__init__.py +1 -0
- jj_stack/github/auth.py +34 -0
- jj_stack/github/client.py +1098 -0
- jj_stack/github/error_messages.py +89 -0
- jj_stack/github/overview_comments.py +35 -0
- jj_stack/github/pr_refs.py +70 -0
- jj_stack/github/resolution.py +208 -0
- jj_stack/github/stack_availability.py +30 -0
- jj_stack/identifiers.py +7 -0
- jj_stack/jj/__init__.py +1 -0
- jj_stack/jj/cli_args.py +23 -0
- jj_stack/jj/client.py +1459 -0
- jj_stack/jj/colors.py +199 -0
- jj_stack/models/__init__.py +1 -0
- jj_stack/models/git.py +15 -0
- jj_stack/models/github.py +243 -0
- jj_stack/models/stack.py +70 -0
- jj_stack/models/tracking.py +115 -0
- jj_stack/pr_branch_namespace.py +78 -0
- jj_stack/stack/__init__.py +1 -0
- jj_stack/stack/change_status.py +216 -0
- jj_stack/stack/convergence.py +501 -0
- jj_stack/stack/convergence_models.py +69 -0
- jj_stack/stack/convergence_observation.py +162 -0
- jj_stack/stack/github_stack_safety.py +132 -0
- jj_stack/stack/global_convergence.py +270 -0
- jj_stack/stack/path.py +235 -0
- jj_stack/stack/pr_branches.py +126 -0
- jj_stack/stack/pr_facts.py +165 -0
- jj_stack/stack/repo.py +95 -0
- jj_stack/stack/selected.py +358 -0
- jj_stack/stack/selection.py +124 -0
- jj_stack/stack/status.py +711 -0
- jj_stack/stack/trunk_evidence.py +174 -0
- jj_stack/state/__init__.py +1 -0
- jj_stack/state/operation_lock.py +220 -0
- jj_stack/state/store.py +260 -0
- jj_stack/ui.py +230 -0
- jj_stack-0.1.0.dist-info/METADATA +170 -0
- jj_stack-0.1.0.dist-info/RECORD +88 -0
- jj_stack-0.1.0.dist-info/WHEEL +4 -0
- jj_stack-0.1.0.dist-info/entry_points.txt +2 -0
- jj_stack-0.1.0.dist-info/licenses/LICENSE +201 -0
- jj_stack-0.1.0.dist-info/licenses/NOTICE +2 -0
jj_stack/cli_help.py
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"""Rendering for `jj-stack`'s semantic, Rich-styled `--help` output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import textwrap
|
|
7
|
+
from argparse import SUPPRESS, ArgumentParser, _SubParsersAction
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from html import escape
|
|
11
|
+
from string.templatelib import Template
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import jj_stack.console as console
|
|
15
|
+
import jj_stack.ui as ui
|
|
16
|
+
|
|
17
|
+
_TOP_LEVEL_HIDDEN_OPTION_STRINGS = frozenset(
|
|
18
|
+
{"--repository", "--config", "--config-file", "--debug", "--time-output"}
|
|
19
|
+
)
|
|
20
|
+
_COMMON_OPTION_STRINGS = frozenset(
|
|
21
|
+
{
|
|
22
|
+
"-h",
|
|
23
|
+
"--help",
|
|
24
|
+
"--repository",
|
|
25
|
+
"--config",
|
|
26
|
+
"--config-file",
|
|
27
|
+
"--debug",
|
|
28
|
+
"--color",
|
|
29
|
+
"--time-output",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
_INLINE_OPTION_PATTERN = re.compile(r"(?<![\w-])(--?[A-Za-z][\w-]*(?:=[^\s]+)?)(?![\w-])")
|
|
33
|
+
_INLINE_METAVAR_PATTERN = re.compile(r"<[^>\s]+>")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class HelpCommand:
|
|
38
|
+
"""One command entry in the grouped top-level help."""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
summary: str
|
|
42
|
+
hidden: bool = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class HelpSection:
|
|
47
|
+
"""A titled block of detailed guidance appended to one command's help."""
|
|
48
|
+
|
|
49
|
+
title: str
|
|
50
|
+
body: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normalized_help_text(content: ui.Message | str) -> str:
|
|
54
|
+
return textwrap.dedent(ui.plain_text(content)).strip()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_ACTION_HELP_RENDERABLE_ATTRIBUTE = "_jj_stack_help_renderable"
|
|
58
|
+
_HELP_SECTIONS_ATTRIBUTE = "_jj_stack_help_sections"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def add_help_argument(
|
|
62
|
+
parser: Any,
|
|
63
|
+
*name_or_flags: str,
|
|
64
|
+
help: ui.Message | str,
|
|
65
|
+
**kwargs: Any,
|
|
66
|
+
) -> Any:
|
|
67
|
+
"""Add an argument whose help text keeps its semantic styling when rendered."""
|
|
68
|
+
|
|
69
|
+
action = parser.add_argument(*name_or_flags, **kwargs)
|
|
70
|
+
action.help = normalized_help_text(help)
|
|
71
|
+
if not isinstance(help, str):
|
|
72
|
+
setattr(action, _ACTION_HELP_RENDERABLE_ATTRIBUTE, help)
|
|
73
|
+
return action
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def add_help_section(
|
|
77
|
+
parser: ArgumentParser,
|
|
78
|
+
*,
|
|
79
|
+
title: str,
|
|
80
|
+
body: ui.Message | str,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Append a detailed prose section to a command's help."""
|
|
83
|
+
|
|
84
|
+
sections = (*_help_sections(parser), HelpSection(title, normalized_help_text(body)))
|
|
85
|
+
setattr(parser, _HELP_SECTIONS_ATTRIBUTE, sections)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def emit_top_level_help(
|
|
89
|
+
parser: ArgumentParser,
|
|
90
|
+
*,
|
|
91
|
+
groups: Sequence[tuple[str, tuple[HelpCommand, ...]]],
|
|
92
|
+
aliases: Mapping[str, tuple[str, ...]],
|
|
93
|
+
include_hidden: bool,
|
|
94
|
+
) -> None:
|
|
95
|
+
console.output(
|
|
96
|
+
ui.prefixed_line(
|
|
97
|
+
_help_heading("Usage: "),
|
|
98
|
+
_top_level_usage_message(include_hidden=include_hidden),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if parser.description:
|
|
103
|
+
console.output()
|
|
104
|
+
_emit_help_paragraphs(parser.description)
|
|
105
|
+
|
|
106
|
+
for title, entries in groups:
|
|
107
|
+
visible_entries = [entry for entry in entries if include_hidden or not entry.hidden]
|
|
108
|
+
if not visible_entries:
|
|
109
|
+
continue
|
|
110
|
+
console.output()
|
|
111
|
+
_emit_help_table_section(
|
|
112
|
+
title,
|
|
113
|
+
tuple(
|
|
114
|
+
(
|
|
115
|
+
ui.cmd(_command_label(entry, aliases, include_aliases=include_hidden)),
|
|
116
|
+
normalized_help_text(entry.summary),
|
|
117
|
+
)
|
|
118
|
+
for entry in visible_entries
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if not include_hidden:
|
|
123
|
+
console.output()
|
|
124
|
+
console.output(
|
|
125
|
+
t"Run {ui.cmd('jj-stack help --all')} to show advanced commands and options."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
option_actions = _top_level_option_actions(parser, include_hidden=include_hidden)
|
|
129
|
+
option_rows = _action_rows(option_actions)
|
|
130
|
+
if option_rows is not None:
|
|
131
|
+
console.output()
|
|
132
|
+
_emit_help_table_section("Options", option_rows)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def emit_command_help(parser: ArgumentParser) -> None:
|
|
136
|
+
console.output(
|
|
137
|
+
ui.prefixed_line(
|
|
138
|
+
_help_heading("Usage: "),
|
|
139
|
+
_command_usage_message(parser),
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if parser.description:
|
|
144
|
+
console.output()
|
|
145
|
+
_emit_help_paragraphs(parser.description)
|
|
146
|
+
|
|
147
|
+
positional_rows = _action_rows(parser._positionals._group_actions)
|
|
148
|
+
if positional_rows is not None:
|
|
149
|
+
console.output()
|
|
150
|
+
_emit_help_table_section(
|
|
151
|
+
parser._positionals.title or "Positional Arguments",
|
|
152
|
+
positional_rows,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
option_actions = [
|
|
156
|
+
action for action in parser._optionals._group_actions if action.help is not SUPPRESS
|
|
157
|
+
]
|
|
158
|
+
command_option_rows = _action_rows(
|
|
159
|
+
[action for action in option_actions if not _is_common_option_action(action)]
|
|
160
|
+
)
|
|
161
|
+
global_option_rows = _action_rows(
|
|
162
|
+
[action for action in option_actions if _is_common_option_action(action)]
|
|
163
|
+
)
|
|
164
|
+
if command_option_rows is not None:
|
|
165
|
+
console.output()
|
|
166
|
+
title = "Command Options" if global_option_rows is not None else "Options"
|
|
167
|
+
_emit_help_table_section(title, command_option_rows)
|
|
168
|
+
if global_option_rows is not None:
|
|
169
|
+
console.output()
|
|
170
|
+
_emit_help_table_section("Global Options", global_option_rows)
|
|
171
|
+
for section in _help_sections(parser):
|
|
172
|
+
console.output()
|
|
173
|
+
console.output(_help_heading(f"{section.title}:"))
|
|
174
|
+
_emit_help_paragraphs(section.body)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def render_all_in_one_markdown(
|
|
178
|
+
parser: ArgumentParser,
|
|
179
|
+
*,
|
|
180
|
+
groups: Sequence[tuple[str, tuple[HelpCommand, ...]]],
|
|
181
|
+
aliases: Mapping[str, tuple[str, ...]],
|
|
182
|
+
) -> str:
|
|
183
|
+
"""Render every command's semantic help as one Markdown and HTML fragment."""
|
|
184
|
+
|
|
185
|
+
lines = ["<!-- Generated by jj-stack; do not edit. -->", ""]
|
|
186
|
+
_append_markdown_overview(
|
|
187
|
+
lines,
|
|
188
|
+
parser,
|
|
189
|
+
groups=groups,
|
|
190
|
+
aliases=aliases,
|
|
191
|
+
include_hidden=True,
|
|
192
|
+
)
|
|
193
|
+
lines.extend(("", "## Commands"))
|
|
194
|
+
for title, entries in groups:
|
|
195
|
+
command_parsers = _command_parsers(parser, entries, include_hidden=True)
|
|
196
|
+
if command_parsers:
|
|
197
|
+
lines.extend(("", f"### {title}"))
|
|
198
|
+
for entry, child_parser in command_parsers:
|
|
199
|
+
_append_markdown_command(
|
|
200
|
+
lines,
|
|
201
|
+
child_parser,
|
|
202
|
+
name=entry.name,
|
|
203
|
+
aliases=aliases.get(entry.name, ()),
|
|
204
|
+
)
|
|
205
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _append_markdown_overview(
|
|
209
|
+
lines: list[str],
|
|
210
|
+
parser: ArgumentParser,
|
|
211
|
+
*,
|
|
212
|
+
groups: Sequence[tuple[str, tuple[HelpCommand, ...]]],
|
|
213
|
+
aliases: Mapping[str, tuple[str, ...]],
|
|
214
|
+
include_hidden: bool,
|
|
215
|
+
) -> None:
|
|
216
|
+
lines.extend(
|
|
217
|
+
(
|
|
218
|
+
"## Usage",
|
|
219
|
+
"",
|
|
220
|
+
_usage_html(parser, _top_level_usage_message(include_hidden=include_hidden)),
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
if parser.description:
|
|
224
|
+
lines.extend(("", _description_html(parser.description)))
|
|
225
|
+
|
|
226
|
+
for title, entries in groups:
|
|
227
|
+
visible = tuple(entry for entry in entries if include_hidden or not entry.hidden)
|
|
228
|
+
if visible:
|
|
229
|
+
lines.extend(("", f"### {title}", "", _command_list_html(visible, aliases)))
|
|
230
|
+
|
|
231
|
+
options = _top_level_option_actions(parser, include_hidden=include_hidden)
|
|
232
|
+
if options:
|
|
233
|
+
lines.extend(("", "## Global options", "", _action_list_html(options)))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _append_markdown_command(
|
|
237
|
+
lines: list[str],
|
|
238
|
+
parser: ArgumentParser,
|
|
239
|
+
*,
|
|
240
|
+
name: str,
|
|
241
|
+
aliases: Sequence[str],
|
|
242
|
+
) -> None:
|
|
243
|
+
lines.extend(("", f"#### {name}"))
|
|
244
|
+
if aliases:
|
|
245
|
+
alias_html = ", ".join(
|
|
246
|
+
f'<code class="cli-alias">{escape(alias)}</code>' for alias in aliases
|
|
247
|
+
)
|
|
248
|
+
lines.extend(("", f'<p class="cli-aliases"><strong>Aliases:</strong> {alias_html}</p>'))
|
|
249
|
+
lines.extend(("", _usage_html(parser, _markdown_command_usage_message(parser))))
|
|
250
|
+
if parser.description:
|
|
251
|
+
description = parser.description
|
|
252
|
+
if aliases:
|
|
253
|
+
spelled = ", ".join(f"jj-stack {alias}" for alias in aliases)
|
|
254
|
+
description = description.removesuffix(f"\n\nAlso spelled {spelled}.")
|
|
255
|
+
lines.extend(("", _description_html(description)))
|
|
256
|
+
|
|
257
|
+
positionals = _visible_actions(parser._positionals._group_actions)
|
|
258
|
+
if positionals:
|
|
259
|
+
lines.extend(("", "##### Positional arguments", "", _action_list_html(positionals)))
|
|
260
|
+
options = tuple(
|
|
261
|
+
action
|
|
262
|
+
for action in _visible_actions(parser._optionals._group_actions)
|
|
263
|
+
if not _is_common_option_action(action)
|
|
264
|
+
)
|
|
265
|
+
if options:
|
|
266
|
+
lines.extend(("", "##### Options", "", _action_list_html(options)))
|
|
267
|
+
for section in _help_sections(parser):
|
|
268
|
+
lines.extend(("", f"##### {section.title}", "", _description_html(section.body)))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _help_sections(parser: ArgumentParser) -> tuple[HelpSection, ...]:
|
|
272
|
+
return getattr(parser, _HELP_SECTIONS_ATTRIBUTE, ())
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _command_parsers(
|
|
276
|
+
parser: ArgumentParser,
|
|
277
|
+
entries: Sequence[HelpCommand],
|
|
278
|
+
*,
|
|
279
|
+
include_hidden: bool,
|
|
280
|
+
) -> tuple[tuple[HelpCommand, ArgumentParser], ...]:
|
|
281
|
+
subparsers = next(
|
|
282
|
+
action for action in parser._actions if isinstance(action, _SubParsersAction)
|
|
283
|
+
)
|
|
284
|
+
return tuple(
|
|
285
|
+
(entry, subparsers.choices[entry.name])
|
|
286
|
+
for entry in entries
|
|
287
|
+
if include_hidden or not entry.hidden
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _command_list_html(
|
|
292
|
+
entries: Sequence[HelpCommand],
|
|
293
|
+
aliases: Mapping[str, tuple[str, ...]],
|
|
294
|
+
) -> str:
|
|
295
|
+
rows = []
|
|
296
|
+
for entry in entries:
|
|
297
|
+
label = f'<code class="cli-command">{escape(entry.name)}</code>'
|
|
298
|
+
entry_aliases = aliases.get(entry.name, ())
|
|
299
|
+
alias_note = ""
|
|
300
|
+
if entry_aliases:
|
|
301
|
+
alias_labels = ", ".join(
|
|
302
|
+
f'<code class="cli-alias">{escape(alias)}</code>' for alias in entry_aliases
|
|
303
|
+
)
|
|
304
|
+
alias_note = (
|
|
305
|
+
f'<span class="cli-aliases"><strong>Aliases:</strong> {alias_labels}</span>'
|
|
306
|
+
)
|
|
307
|
+
rows.append(
|
|
308
|
+
f'<div class="cli-reference-row"><dt>{label}</dt>'
|
|
309
|
+
f"<dd>{_inline_help_html(normalized_help_text(entry.summary))}{alias_note}</dd></div>"
|
|
310
|
+
)
|
|
311
|
+
return '<dl class="cli-reference-list">\n' + "\n".join(rows) + "\n</dl>"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _action_list_html(actions: Sequence[Any]) -> str:
|
|
315
|
+
rows = [
|
|
316
|
+
f'<div class="cli-reference-row"><dt>{_action_label_html(action)}</dt>'
|
|
317
|
+
f"<dd>{_action_help_html(action)}</dd></div>"
|
|
318
|
+
for action in actions
|
|
319
|
+
]
|
|
320
|
+
return '<dl class="cli-reference-list">\n' + "\n".join(rows) + "\n</dl>"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _action_label_html(action: Any) -> str:
|
|
324
|
+
if not action.option_strings:
|
|
325
|
+
label = escape(str(action.metavar or action.dest))
|
|
326
|
+
return f'<code class="cli-argument"><var class="cli-metavar">{label}</var></code>'
|
|
327
|
+
options = ", ".join(
|
|
328
|
+
f'<span class="cli-option">{escape(option)}</span>' for option in action.option_strings
|
|
329
|
+
)
|
|
330
|
+
if action.nargs == 0:
|
|
331
|
+
return f'<code class="cli-argument">{options}</code>'
|
|
332
|
+
metavar = escape(str(action.metavar or action.dest.upper()))
|
|
333
|
+
return f'<code class="cli-argument">{options} <var class="cli-metavar">{metavar}</var></code>'
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _action_help_html(action: Any) -> str:
|
|
337
|
+
content = getattr(action, _ACTION_HELP_RENDERABLE_ATTRIBUTE, action.help or "")
|
|
338
|
+
return _message_html(content)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _description_html(text: str) -> str:
|
|
342
|
+
blocks: list[str] = []
|
|
343
|
+
bullets: list[str] = []
|
|
344
|
+
for paragraph in _help_paragraphs(text):
|
|
345
|
+
if paragraph.startswith("- "):
|
|
346
|
+
bullets.append(f"<li>{_inline_help_html(paragraph[2:])}</li>")
|
|
347
|
+
continue
|
|
348
|
+
if bullets:
|
|
349
|
+
blocks.append("<ul>\n" + "\n".join(bullets) + "\n</ul>")
|
|
350
|
+
bullets = []
|
|
351
|
+
blocks.append(f"<p>{_inline_help_html(paragraph)}</p>")
|
|
352
|
+
if bullets:
|
|
353
|
+
blocks.append("<ul>\n" + "\n".join(bullets) + "\n</ul>")
|
|
354
|
+
return "\n".join(blocks)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _message_html(content: ui.Message) -> str:
|
|
358
|
+
if isinstance(content, tuple):
|
|
359
|
+
return "".join(_message_html(part) for part in content)
|
|
360
|
+
if isinstance(content, Template):
|
|
361
|
+
return "".join(
|
|
362
|
+
_inline_help_html(part)
|
|
363
|
+
if isinstance(part, str)
|
|
364
|
+
else _message_html(ui.resolve_interpolation(part))
|
|
365
|
+
for part in content
|
|
366
|
+
)
|
|
367
|
+
if isinstance(content, ui.SemanticText):
|
|
368
|
+
class_name = "cli-inline"
|
|
369
|
+
if "revset" in content.labels or "metavar" in content.labels:
|
|
370
|
+
class_name = "cli-metavar"
|
|
371
|
+
elif "option" in content.labels:
|
|
372
|
+
return _inline_option_html(content.text)
|
|
373
|
+
elif "local_bookmarks" in content.labels or "remote_bookmarks" in content.labels:
|
|
374
|
+
class_name = "cli-bookmark"
|
|
375
|
+
elif "command" in content.labels and (
|
|
376
|
+
_INLINE_OPTION_PATTERN.search(content.text)
|
|
377
|
+
or _INLINE_METAVAR_PATTERN.search(content.text)
|
|
378
|
+
):
|
|
379
|
+
return _inline_command_html(content.text)
|
|
380
|
+
return f'<code class="{class_name}">{escape(content.text)}</code>'
|
|
381
|
+
return _inline_help_html(str(content))
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _inline_option_html(text: str) -> str:
|
|
385
|
+
match = re.fullmatch(
|
|
386
|
+
r"(?P<option>--?[A-Za-z][\w-]*)(?:(?P<separator>=|\s+)(?P<value>.+))?",
|
|
387
|
+
text,
|
|
388
|
+
)
|
|
389
|
+
if match is None or match.group("value") is None:
|
|
390
|
+
return f'<code class="cli-inline cli-option">{escape(text)}</code>'
|
|
391
|
+
return (
|
|
392
|
+
'<code class="cli-inline cli-option-expression">'
|
|
393
|
+
f'<span class="cli-option">{escape(match.group("option"))}</span>'
|
|
394
|
+
f"{escape(match.group('separator'))}"
|
|
395
|
+
f'<span class="cli-metavar">{escape(match.group("value"))}</span>'
|
|
396
|
+
"</code>"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _inline_command_html(text: str) -> str:
|
|
401
|
+
parts: list[str] = []
|
|
402
|
+
last_index = 0
|
|
403
|
+
tokens: list[tuple[int, int, str]] = []
|
|
404
|
+
for match in _INLINE_OPTION_PATTERN.finditer(text):
|
|
405
|
+
option = match.group(1)
|
|
406
|
+
if "=" in option:
|
|
407
|
+
option_name, value = option.split("=", 1)
|
|
408
|
+
value_start = match.start(1) + len(option_name) + 1
|
|
409
|
+
tokens.append((match.start(1), value_start - 1, "cli-option"))
|
|
410
|
+
tokens.append((value_start, match.end(1), "cli-metavar"))
|
|
411
|
+
continue
|
|
412
|
+
tokens.append((match.start(1), match.end(1), "cli-option"))
|
|
413
|
+
following = re.match(r"\s+(?P<value>(?!-)\S+)", text[match.end(1) :])
|
|
414
|
+
if following is not None and not _INLINE_METAVAR_PATTERN.fullmatch(
|
|
415
|
+
following.group("value")
|
|
416
|
+
):
|
|
417
|
+
value_start = match.end(1) + following.start("value")
|
|
418
|
+
value_end = match.end(1) + following.end("value")
|
|
419
|
+
tokens.append((value_start, value_end, "cli-metavar"))
|
|
420
|
+
tokens.extend(
|
|
421
|
+
(match.start(), match.end(), "cli-metavar")
|
|
422
|
+
for match in _INLINE_METAVAR_PATTERN.finditer(text)
|
|
423
|
+
)
|
|
424
|
+
for start, end, class_name in sorted(tokens):
|
|
425
|
+
if start < last_index:
|
|
426
|
+
continue
|
|
427
|
+
parts.append(escape(text[last_index:start]))
|
|
428
|
+
parts.append(f'<span class="{class_name}">{escape(text[start:end])}</span>')
|
|
429
|
+
last_index = end
|
|
430
|
+
parts.append(escape(text[last_index:]))
|
|
431
|
+
return '<code class="cli-inline cli-command-snippet">' + "".join(parts) + "</code>"
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _inline_help_html(text: str) -> str:
|
|
435
|
+
parts: list[str] = []
|
|
436
|
+
last_index = 0
|
|
437
|
+
for match in re.finditer(r"`([^`]+)`", text):
|
|
438
|
+
parts.append(escape(text[last_index : match.start()]))
|
|
439
|
+
parts.append(_message_html(_help_inline_code(match.group(1))))
|
|
440
|
+
last_index = match.end()
|
|
441
|
+
parts.append(escape(text[last_index:]))
|
|
442
|
+
return "".join(parts)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _usage_html(parser: ArgumentParser, usage: ui.Message | str) -> str:
|
|
446
|
+
text = ui.plain_text(usage) if not isinstance(usage, str) else usage
|
|
447
|
+
token_classes: dict[str, str] = {parser.prog: "cli-command"}
|
|
448
|
+
for action in parser._actions:
|
|
449
|
+
token_classes.update((option, "cli-option") for option in action.option_strings)
|
|
450
|
+
if action.nargs != 0:
|
|
451
|
+
if action.choices is not None and action.metavar is None:
|
|
452
|
+
metavar = "{" + ",".join(str(choice) for choice in action.choices) + "}"
|
|
453
|
+
else:
|
|
454
|
+
metavar = action.metavar or (
|
|
455
|
+
action.dest.upper() if action.option_strings else action.dest
|
|
456
|
+
)
|
|
457
|
+
token_classes[str(metavar)] = "cli-metavar"
|
|
458
|
+
token_classes["<command>"] = "cli-metavar"
|
|
459
|
+
alternatives = "|".join(
|
|
460
|
+
re.escape(token) for token in sorted(token_classes, key=len, reverse=True)
|
|
461
|
+
)
|
|
462
|
+
pattern = re.compile(rf"(?<![\w-])({alternatives})(?![\w-])")
|
|
463
|
+
matches = tuple(pattern.finditer(text))
|
|
464
|
+
parts: list[str] = []
|
|
465
|
+
last_index = 0
|
|
466
|
+
match_index = 0
|
|
467
|
+
while match_index < len(matches):
|
|
468
|
+
match = matches[match_index]
|
|
469
|
+
parts.append(escape(text[last_index : match.start()]))
|
|
470
|
+
token = match.group(1)
|
|
471
|
+
next_match = matches[match_index + 1] if match_index + 1 < len(matches) else None
|
|
472
|
+
if (
|
|
473
|
+
token_classes[token] == "cli-option"
|
|
474
|
+
and next_match is not None
|
|
475
|
+
and token_classes[next_match.group(1)] == "cli-metavar"
|
|
476
|
+
and text[match.end() : next_match.start()] == " "
|
|
477
|
+
):
|
|
478
|
+
next_token = next_match.group(1)
|
|
479
|
+
parts.append(
|
|
480
|
+
'<span class="cli-argument">'
|
|
481
|
+
f'<span class="cli-option">{escape(token)}</span> '
|
|
482
|
+
f'<span class="cli-metavar">{escape(next_token)}</span>'
|
|
483
|
+
"</span>"
|
|
484
|
+
)
|
|
485
|
+
last_index = next_match.end()
|
|
486
|
+
match_index += 2
|
|
487
|
+
continue
|
|
488
|
+
parts.append(f'<span class="{token_classes[token]}">{escape(token)}</span>')
|
|
489
|
+
last_index = match.end()
|
|
490
|
+
match_index += 1
|
|
491
|
+
parts.append(escape(text[last_index:]))
|
|
492
|
+
return '<pre class="cli-synopsis"><code>' + "".join(parts) + "</code></pre>"
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _command_label(
|
|
496
|
+
entry: HelpCommand,
|
|
497
|
+
aliases: Mapping[str, tuple[str, ...]],
|
|
498
|
+
*,
|
|
499
|
+
include_aliases: bool,
|
|
500
|
+
) -> str:
|
|
501
|
+
entry_aliases = aliases.get(entry.name, ())
|
|
502
|
+
if not include_aliases or not entry_aliases:
|
|
503
|
+
return entry.name
|
|
504
|
+
return ", ".join((entry.name, *entry_aliases))
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _top_level_usage_message(*, include_hidden: bool) -> ui.Message:
|
|
508
|
+
if include_hidden:
|
|
509
|
+
return (
|
|
510
|
+
t"{ui.cmd('jj-stack')} [{ui.cmd('--help')}] "
|
|
511
|
+
t"[{ui.cmd('--repository REPO')}] "
|
|
512
|
+
t"[{ui.cmd('--config NAME=VALUE')}] [{ui.cmd('--config-file PATH')}] "
|
|
513
|
+
t"[{ui.cmd('--debug')}] [{ui.cmd('--color WHEN')}] "
|
|
514
|
+
t"[{ui.cmd('--time-output')}] [{ui.cmd('--version')}] "
|
|
515
|
+
t"[{ui.cmd('<command>')} ...]"
|
|
516
|
+
)
|
|
517
|
+
return (
|
|
518
|
+
t"{ui.cmd('jj-stack')} [{ui.cmd('--help')}] [{ui.cmd('--color WHEN')}] "
|
|
519
|
+
t"[{ui.cmd('--version')}] [{ui.cmd('<command>')} ...]"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _command_usage_message(parser: ArgumentParser) -> ui.Message | str:
|
|
524
|
+
body = " ".join(parser.format_usage().split())
|
|
525
|
+
body = re.sub(r"^(?:[Uu]sage:\s*)+", "", body)
|
|
526
|
+
body = re.sub(r"\[-h\]", "[--help]", body)
|
|
527
|
+
if body.startswith(parser.prog):
|
|
528
|
+
return (ui.cmd(parser.prog), body.removeprefix(parser.prog))
|
|
529
|
+
return body
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _markdown_command_usage_message(parser: ArgumentParser) -> ui.Message | str:
|
|
533
|
+
actions = tuple(action for action in parser._actions if not _is_common_option_action(action))
|
|
534
|
+
groups = tuple(
|
|
535
|
+
group
|
|
536
|
+
for group in parser._mutually_exclusive_groups
|
|
537
|
+
if any(action in actions for action in group._group_actions)
|
|
538
|
+
)
|
|
539
|
+
formatter: Any = parser._get_formatter()
|
|
540
|
+
body = " ".join(formatter._format_usage(None, actions, groups, "").split())
|
|
541
|
+
for action in actions:
|
|
542
|
+
if not action.option_strings or action.option_strings[0].startswith("--"):
|
|
543
|
+
continue
|
|
544
|
+
long_option = next(
|
|
545
|
+
(option for option in action.option_strings if option.startswith("--")), None
|
|
546
|
+
)
|
|
547
|
+
if long_option is not None:
|
|
548
|
+
body = re.sub(
|
|
549
|
+
rf"(?<![\w-]){re.escape(action.option_strings[0])}(?![\w-])",
|
|
550
|
+
long_option,
|
|
551
|
+
body,
|
|
552
|
+
)
|
|
553
|
+
if body.startswith(parser.prog):
|
|
554
|
+
return (ui.cmd(parser.prog), body.removeprefix(parser.prog))
|
|
555
|
+
return body
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _help_paragraphs(text: str) -> tuple[str, ...]:
|
|
559
|
+
normalized = normalized_help_text(text)
|
|
560
|
+
if not normalized:
|
|
561
|
+
return ()
|
|
562
|
+
return tuple(" ".join(paragraph.split()) for paragraph in re.split(r"\n\s*\n", normalized))
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _help_inline_code(text: str) -> ui.SemanticText:
|
|
566
|
+
if text.startswith("jj-stack/"):
|
|
567
|
+
return ui.bookmark(text)
|
|
568
|
+
if text.startswith("@") or ("(" in text and text.endswith(")")):
|
|
569
|
+
return ui.revset(text)
|
|
570
|
+
if text.startswith("-"):
|
|
571
|
+
return ui.option(text)
|
|
572
|
+
return ui.cmd(text)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _help_rich_text(text: str) -> ui.Message:
|
|
576
|
+
parts: list[ui.Message] = []
|
|
577
|
+
last_index = 0
|
|
578
|
+
for match in re.finditer(r"`([^`]+)`", text):
|
|
579
|
+
start, end = match.span()
|
|
580
|
+
if start > last_index:
|
|
581
|
+
parts.append(text[last_index:start])
|
|
582
|
+
parts.append(_help_inline_code(match.group(1)))
|
|
583
|
+
last_index = end
|
|
584
|
+
if last_index == 0:
|
|
585
|
+
return text
|
|
586
|
+
if last_index < len(text):
|
|
587
|
+
parts.append(text[last_index:])
|
|
588
|
+
return tuple(parts)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _help_heading(text: str) -> ui.SemanticText:
|
|
592
|
+
return ui.semantic_text(text, "hint", "heading")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _action_help_body(action: Any) -> ui.Message | str:
|
|
596
|
+
content = getattr(action, _ACTION_HELP_RENDERABLE_ATTRIBUTE, None)
|
|
597
|
+
if content is not None:
|
|
598
|
+
return content
|
|
599
|
+
return "\n\n".join(_help_paragraphs(action.help or ""))
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _action_label_message(action) -> ui.Message:
|
|
603
|
+
if not action.option_strings:
|
|
604
|
+
return ui.cmd(str(action.metavar or action.dest))
|
|
605
|
+
label = ", ".join(action.option_strings)
|
|
606
|
+
if action.nargs != 0:
|
|
607
|
+
label = f"{label} {action.metavar or action.dest.upper()}"
|
|
608
|
+
return ui.cmd(label)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _help_table(
|
|
612
|
+
rows: Sequence[tuple[ui.Message, ui.TableCell]],
|
|
613
|
+
) -> ui.DataTable:
|
|
614
|
+
label_width = max(len(ui.plain_text(label)) for label, _ in rows) + 2
|
|
615
|
+
return ui.DataTable(
|
|
616
|
+
columns=(
|
|
617
|
+
ui.TableColumn("", no_wrap=True, width=label_width),
|
|
618
|
+
ui.TableColumn(""),
|
|
619
|
+
),
|
|
620
|
+
rows=tuple(rows),
|
|
621
|
+
box="",
|
|
622
|
+
show_header=False,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _emit_help_table_section(title: str, rows: Sequence[tuple[ui.Message, ui.TableCell]]) -> None:
|
|
627
|
+
console.output(_help_heading(f"{title}:"))
|
|
628
|
+
console.output(_help_table(rows))
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _is_common_option_action(action: Any) -> bool:
|
|
632
|
+
return bool(action.option_strings) and all(
|
|
633
|
+
option in _COMMON_OPTION_STRINGS for option in action.option_strings
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _action_rows(actions: Sequence[Any]) -> tuple[tuple[ui.Message, ui.TableCell], ...] | None:
|
|
638
|
+
visible_actions = _visible_actions(actions)
|
|
639
|
+
if not visible_actions:
|
|
640
|
+
return None
|
|
641
|
+
return tuple(
|
|
642
|
+
(
|
|
643
|
+
_action_label_message(action),
|
|
644
|
+
_action_help_body(action),
|
|
645
|
+
)
|
|
646
|
+
for action in visible_actions
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _visible_actions(actions: Sequence[Any]) -> tuple[Any, ...]:
|
|
651
|
+
return tuple(action for action in actions if action.help is not SUPPRESS)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _top_level_option_actions(
|
|
655
|
+
parser: ArgumentParser,
|
|
656
|
+
*,
|
|
657
|
+
include_hidden: bool,
|
|
658
|
+
) -> tuple[Any, ...]:
|
|
659
|
+
return tuple(
|
|
660
|
+
action
|
|
661
|
+
for action in parser._actions
|
|
662
|
+
if action.option_strings
|
|
663
|
+
and action.help is not SUPPRESS
|
|
664
|
+
and (
|
|
665
|
+
include_hidden
|
|
666
|
+
or not any(
|
|
667
|
+
option in _TOP_LEVEL_HIDDEN_OPTION_STRINGS for option in action.option_strings
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _emit_help_paragraphs(text: str) -> None:
|
|
674
|
+
for index, paragraph in enumerate(_help_paragraphs(text)):
|
|
675
|
+
if index:
|
|
676
|
+
console.output()
|
|
677
|
+
if paragraph.startswith("- "):
|
|
678
|
+
console.output(ui.prefixed_line("- ", _help_rich_text(paragraph[2:])))
|
|
679
|
+
else:
|
|
680
|
+
console.output(_help_rich_text(paragraph))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command package for the standalone CLI."""
|