git4subeditor 0.1.20__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.
- git4subeditor/__init__.py +41 -0
- git4subeditor/__main__.py +6 -0
- git4subeditor/cli.py +683 -0
- git4subeditor/gitformat.py +427 -0
- git4subeditor/l10n.py +107 -0
- git4subeditor/locales/cn.xml +135 -0
- git4subeditor/locales/en.xml +137 -0
- git4subeditor/locales/ru.xml +137 -0
- git4subeditor/repl.py +256 -0
- git4subeditor/subfile.py +189 -0
- git4subeditor/utils.py +122 -0
- git4subeditor/workspace.py +843 -0
- git4subeditor-0.1.20.dist-info/METADATA +227 -0
- git4subeditor-0.1.20.dist-info/RECORD +17 -0
- git4subeditor-0.1.20.dist-info/WHEEL +4 -0
- git4subeditor-0.1.20.dist-info/entry_points.txt +3 -0
- git4subeditor-0.1.20.dist-info/licenses/LICENSE +21 -0
git4subeditor/cli.py
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
"""Command-line interface for git4subeditor."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import partial
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from git4subeditor import __version__
|
|
11
|
+
from git4subeditor.gitformat import export_sub, pack_sub
|
|
12
|
+
from git4subeditor.l10n import (
|
|
13
|
+
DEFAULT_LANGUAGE,
|
|
14
|
+
SUPPORTED_LANGUAGES,
|
|
15
|
+
configured_language,
|
|
16
|
+
resolve_language,
|
|
17
|
+
set_language,
|
|
18
|
+
t,
|
|
19
|
+
)
|
|
20
|
+
from git4subeditor.subfile import SubFileError, unpack_sub
|
|
21
|
+
from git4subeditor.utils import format_message, print_banner
|
|
22
|
+
from git4subeditor.workspace import (
|
|
23
|
+
IGNORE_PATTERNS,
|
|
24
|
+
InitializeError,
|
|
25
|
+
SubmarineEntry,
|
|
26
|
+
_detect_submarines,
|
|
27
|
+
_relative_display,
|
|
28
|
+
initialize_project,
|
|
29
|
+
load_workspace_info,
|
|
30
|
+
print_submarine_paths,
|
|
31
|
+
read_config_root,
|
|
32
|
+
refresh_submarine_hashes,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _ArgumentParser(argparse.ArgumentParser):
|
|
37
|
+
"""Argument parser with localized help headings."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, *args: object, **kwargs: object) -> None:
|
|
40
|
+
kwargs.setdefault("add_help", False)
|
|
41
|
+
super().__init__(*args, **kwargs) # type: ignore
|
|
42
|
+
self.add_argument("-h", "--help", action="help", help=t("arg.help"))
|
|
43
|
+
self._positionals.title = t("help.positionals")
|
|
44
|
+
self._optionals.title = t("help.options")
|
|
45
|
+
|
|
46
|
+
def format_usage(self) -> str:
|
|
47
|
+
"""Return usage text with a localized prefix."""
|
|
48
|
+
return super().format_usage().replace("usage: ", t("help.usage"), 1)
|
|
49
|
+
|
|
50
|
+
def format_help(self) -> str:
|
|
51
|
+
"""Return help text with a localized usage prefix."""
|
|
52
|
+
return super().format_help().replace("usage: ", t("help.usage"), 1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
56
|
+
"""Build the command-line argument parser."""
|
|
57
|
+
parser = _ArgumentParser(
|
|
58
|
+
prog="git4subeditor",
|
|
59
|
+
description=t("app.description"),
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--version",
|
|
63
|
+
action="version",
|
|
64
|
+
version=f"%(prog)s {__version__}",
|
|
65
|
+
help=t("arg.version"),
|
|
66
|
+
)
|
|
67
|
+
subparsers = parser.add_subparsers(
|
|
68
|
+
dest="command",
|
|
69
|
+
required=True,
|
|
70
|
+
metavar=t("metavar.command"),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
init = subparsers.add_parser(
|
|
74
|
+
"init",
|
|
75
|
+
help=t("command.init"),
|
|
76
|
+
)
|
|
77
|
+
init.add_argument(
|
|
78
|
+
"-f",
|
|
79
|
+
"--force",
|
|
80
|
+
action="store_true",
|
|
81
|
+
help=t("arg.force_init"),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
sub = subparsers.add_parser(
|
|
85
|
+
"sub",
|
|
86
|
+
help=t("command.sub"),
|
|
87
|
+
)
|
|
88
|
+
sub.add_argument(
|
|
89
|
+
"--tree",
|
|
90
|
+
action="store_true",
|
|
91
|
+
help=t("arg.tree"),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
unpack = subparsers.add_parser(
|
|
95
|
+
"unpack",
|
|
96
|
+
help=t("command.unpack"),
|
|
97
|
+
)
|
|
98
|
+
_add_inputs(unpack, t("arg.inputs.sub"))
|
|
99
|
+
unpack.add_argument(
|
|
100
|
+
"-o",
|
|
101
|
+
"--output-dir",
|
|
102
|
+
type=Path,
|
|
103
|
+
default=None,
|
|
104
|
+
help=t("arg.output_dir"),
|
|
105
|
+
)
|
|
106
|
+
unpack.add_argument(
|
|
107
|
+
"--encoding",
|
|
108
|
+
default=None,
|
|
109
|
+
help=t("arg.encoding"),
|
|
110
|
+
)
|
|
111
|
+
unpack.add_argument(
|
|
112
|
+
"--stdout",
|
|
113
|
+
action="store_true",
|
|
114
|
+
help=t("arg.stdout"),
|
|
115
|
+
)
|
|
116
|
+
unpack.add_argument(
|
|
117
|
+
"--no-validate",
|
|
118
|
+
action="store_true",
|
|
119
|
+
help=t("arg.no_validate"),
|
|
120
|
+
)
|
|
121
|
+
_add_force(unpack)
|
|
122
|
+
|
|
123
|
+
export = subparsers.add_parser(
|
|
124
|
+
"export",
|
|
125
|
+
help=t("command.export"),
|
|
126
|
+
)
|
|
127
|
+
export.add_argument(
|
|
128
|
+
"inputs",
|
|
129
|
+
nargs="*",
|
|
130
|
+
type=Path,
|
|
131
|
+
help=t("arg.inputs.sub"),
|
|
132
|
+
)
|
|
133
|
+
export.add_argument(
|
|
134
|
+
"-o",
|
|
135
|
+
"--output-dir",
|
|
136
|
+
type=Path,
|
|
137
|
+
default=None,
|
|
138
|
+
help=t("arg.output_dir"),
|
|
139
|
+
)
|
|
140
|
+
export.add_argument(
|
|
141
|
+
"--sort-attributes",
|
|
142
|
+
action="store_true",
|
|
143
|
+
help=t("arg.sort_attributes"),
|
|
144
|
+
)
|
|
145
|
+
_add_force(export)
|
|
146
|
+
|
|
147
|
+
pack = subparsers.add_parser(
|
|
148
|
+
"pack",
|
|
149
|
+
help=t("command.pack"),
|
|
150
|
+
)
|
|
151
|
+
_add_inputs(pack, t("arg.inputs.xml"))
|
|
152
|
+
pack.add_argument(
|
|
153
|
+
"-o",
|
|
154
|
+
"--output",
|
|
155
|
+
type=Path,
|
|
156
|
+
default=None,
|
|
157
|
+
help=t("arg.output"),
|
|
158
|
+
)
|
|
159
|
+
pack.add_argument(
|
|
160
|
+
"--sort-attributes",
|
|
161
|
+
action="store_true",
|
|
162
|
+
help=t("arg.sort_attributes_pack"),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return parser
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _add_inputs(
|
|
169
|
+
parser: argparse.ArgumentParser,
|
|
170
|
+
help_text: str,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Add the positional input list to a subcommand parser."""
|
|
173
|
+
parser.add_argument("inputs", nargs="+", type=Path, help=help_text)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _add_force(parser: argparse.ArgumentParser) -> None:
|
|
177
|
+
"""Add the shared --force option to a subcommand parser."""
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
"-f",
|
|
180
|
+
"--force",
|
|
181
|
+
action="store_true",
|
|
182
|
+
help=t("arg.force"),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _confirm_overwrite(path: Path) -> bool:
|
|
187
|
+
"""Ask whether an existing export target may be overwritten."""
|
|
188
|
+
try:
|
|
189
|
+
answer = input(t("prompt.overwrite", path=path))
|
|
190
|
+
except EOFError:
|
|
191
|
+
return False
|
|
192
|
+
return answer.strip().casefold() == "y"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _confirm_submarines(
|
|
196
|
+
registered: tuple[SubmarineEntry, ...],
|
|
197
|
+
pending: tuple[SubmarineEntry, ...],
|
|
198
|
+
) -> bool:
|
|
199
|
+
"""Show pending submarine registry changes and ask to apply them."""
|
|
200
|
+
if registered:
|
|
201
|
+
print(t("init.registered_header"))
|
|
202
|
+
for entry in registered:
|
|
203
|
+
print(f" {entry.name}: {entry.path}")
|
|
204
|
+
print(t("init.pending_header"))
|
|
205
|
+
for entry in pending:
|
|
206
|
+
print(f" {entry.name}: {entry.path}")
|
|
207
|
+
try:
|
|
208
|
+
answer: str = input(t("prompt.submarines"))
|
|
209
|
+
except EOFError:
|
|
210
|
+
print()
|
|
211
|
+
return False
|
|
212
|
+
return answer.strip().casefold() == "y"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _confirm_submarine_removals(
|
|
216
|
+
removed: tuple[SubmarineEntry, ...],
|
|
217
|
+
) -> bool:
|
|
218
|
+
"""Show registered submarines whose files are gone and ask to remove them."""
|
|
219
|
+
print(t("init.removed_header"))
|
|
220
|
+
for entry in removed:
|
|
221
|
+
print(f" {entry.name}: {entry.path}")
|
|
222
|
+
try:
|
|
223
|
+
answer: str = input(t("prompt.remove_submarines"))
|
|
224
|
+
except EOFError:
|
|
225
|
+
print()
|
|
226
|
+
return False
|
|
227
|
+
return answer.strip().casefold() == "y"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _confirm_path_change(old: str, new: str, key: str) -> bool:
|
|
231
|
+
"""Ask whether a configured path should be updated."""
|
|
232
|
+
try:
|
|
233
|
+
answer: str = input(t(key, old=old or "-", new=new or "-"))
|
|
234
|
+
except EOFError:
|
|
235
|
+
print()
|
|
236
|
+
return False
|
|
237
|
+
return answer.strip().casefold() == "y"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _prompt_language() -> str:
|
|
241
|
+
"""Ask for the project language in Chinese, English, and Russian."""
|
|
242
|
+
print("请选择语言 / Please select a language / Выберите язык:")
|
|
243
|
+
print(" cn - 中文")
|
|
244
|
+
print(" en - English")
|
|
245
|
+
print(" ru - Русский")
|
|
246
|
+
try:
|
|
247
|
+
answer: str = input("选择 / Select / Выберите [cn/en/ru] (default: en): ")
|
|
248
|
+
except EOFError:
|
|
249
|
+
return "en"
|
|
250
|
+
language: str = answer.strip().lower()
|
|
251
|
+
return language if language in SUPPORTED_LANGUAGES else "en"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass(frozen=True, slots=True)
|
|
255
|
+
class _SubmarineEntry:
|
|
256
|
+
"""A submarine entry read from the project configuration.
|
|
257
|
+
|
|
258
|
+
Attributes:
|
|
259
|
+
name: Registered submarine name.
|
|
260
|
+
path: Submarine path relative to the workspace directory.
|
|
261
|
+
preview_path: Resolved preview image path, or None when no preview is
|
|
262
|
+
configured.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
name: str
|
|
266
|
+
path: str
|
|
267
|
+
preview_path: Path | None = None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _load_registry() -> dict[Path, _SubmarineEntry] | None:
|
|
271
|
+
"""Load submarine entries from the current directory project config.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
A mapping from resolved ``.sub`` paths to registry entries, or None
|
|
275
|
+
when ``git4subeditor.xml`` does not exist.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
SubFileError: If the configuration cannot be read or parsed.
|
|
279
|
+
"""
|
|
280
|
+
root: ET.Element | None = read_config_root()
|
|
281
|
+
if root is None:
|
|
282
|
+
return None
|
|
283
|
+
workspace_root: Path = Path.cwd()
|
|
284
|
+
|
|
285
|
+
registry: dict[Path, _SubmarineEntry] = {}
|
|
286
|
+
for entry in root.findall("./submarines/submarine"):
|
|
287
|
+
path_text: str = (entry.get("path") or "").strip()
|
|
288
|
+
if not path_text:
|
|
289
|
+
continue
|
|
290
|
+
sub_path: Path = (workspace_root / Path(path_text)).resolve()
|
|
291
|
+
preview_text: str = (entry.get("preview") or "").strip()
|
|
292
|
+
preview_path: Path | None = (
|
|
293
|
+
(workspace_root / Path(preview_text)).resolve() if preview_text else None
|
|
294
|
+
)
|
|
295
|
+
registry[sub_path] = _SubmarineEntry(
|
|
296
|
+
name=entry.get("name") or sub_path.stem,
|
|
297
|
+
path=path_text,
|
|
298
|
+
preview_path=preview_path,
|
|
299
|
+
)
|
|
300
|
+
return registry
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _require_registered(
|
|
304
|
+
path: Path,
|
|
305
|
+
registry: dict[Path, _SubmarineEntry] | None,
|
|
306
|
+
) -> _SubmarineEntry | None:
|
|
307
|
+
"""Return the registry entry for a submarine or fail when missing.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
path: Input ``.sub`` file or exported ``.xml`` file.
|
|
311
|
+
registry: Registry returned by ``_load_registry``, or None when no
|
|
312
|
+
project configuration exists.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
The matching registry entry, or None when no configuration exists.
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
SubFileError: If a configuration exists but the submarine is not
|
|
319
|
+
registered.
|
|
320
|
+
"""
|
|
321
|
+
if registry is None:
|
|
322
|
+
return None
|
|
323
|
+
target: Path = path.resolve()
|
|
324
|
+
sub_path: Path = (
|
|
325
|
+
target.with_suffix(".sub") if target.suffix.lower() == ".xml" else target
|
|
326
|
+
)
|
|
327
|
+
try:
|
|
328
|
+
return registry[sub_path]
|
|
329
|
+
except KeyError:
|
|
330
|
+
raise SubFileError(t("err.not_registered", path=path)) from None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _registry_preview_override(
|
|
334
|
+
entry: _SubmarineEntry | None,
|
|
335
|
+
) -> bytes | None:
|
|
336
|
+
"""Return the preview image configured for a registered submarine.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
entry: Registered submarine entry, or None when no configuration
|
|
340
|
+
exists.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Preview image bytes, or None when no registered preview should be
|
|
344
|
+
used. A read failure prints a localized warning and also returns None
|
|
345
|
+
so packing falls back to the XML ``preview`` link.
|
|
346
|
+
"""
|
|
347
|
+
if entry is None or entry.preview_path is None:
|
|
348
|
+
return None
|
|
349
|
+
try:
|
|
350
|
+
return entry.preview_path.read_bytes()
|
|
351
|
+
except OSError:
|
|
352
|
+
print(
|
|
353
|
+
format_message(
|
|
354
|
+
t("warning.registry_preview_failed", name=entry.name),
|
|
355
|
+
label="warning",
|
|
356
|
+
),
|
|
357
|
+
file=sys.stderr,
|
|
358
|
+
)
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _run_command(
|
|
363
|
+
args: argparse.Namespace,
|
|
364
|
+
*,
|
|
365
|
+
show_banner: bool = True,
|
|
366
|
+
) -> int:
|
|
367
|
+
"""Execute a parsed command and return its exit code.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
args: Parsed command arguments.
|
|
371
|
+
show_banner: Whether ``init`` should print the startup banner.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Process exit code: 0 on success, 1 if any input failed, 2 on bad usage.
|
|
375
|
+
"""
|
|
376
|
+
if args.command == "sub":
|
|
377
|
+
try:
|
|
378
|
+
info = load_workspace_info()
|
|
379
|
+
except SubFileError as exc:
|
|
380
|
+
print(format_message(str(exc), label="error"), file=sys.stderr)
|
|
381
|
+
return 1
|
|
382
|
+
if info is None:
|
|
383
|
+
print(t("repl.workspace_missing"))
|
|
384
|
+
return 1
|
|
385
|
+
print_submarine_paths(info, tree=args.tree)
|
|
386
|
+
return 0
|
|
387
|
+
|
|
388
|
+
if args.command == "init":
|
|
389
|
+
if show_banner:
|
|
390
|
+
print_banner(__version__)
|
|
391
|
+
selected_language: str | None = None
|
|
392
|
+
if configured_language() is None:
|
|
393
|
+
selected_language = DEFAULT_LANGUAGE if args.force else _prompt_language()
|
|
394
|
+
set_language(selected_language)
|
|
395
|
+
confirm_mod_root = None
|
|
396
|
+
confirm_git_repo = None
|
|
397
|
+
if not args.force:
|
|
398
|
+
confirm_mod_root = partial(
|
|
399
|
+
_confirm_path_change,
|
|
400
|
+
key="prompt.mod_root_changed",
|
|
401
|
+
)
|
|
402
|
+
confirm_git_repo = partial(
|
|
403
|
+
_confirm_path_change,
|
|
404
|
+
key="prompt.git_repo_changed",
|
|
405
|
+
)
|
|
406
|
+
try:
|
|
407
|
+
result = initialize_project(
|
|
408
|
+
language=selected_language,
|
|
409
|
+
confirm_submarines=None if args.force else _confirm_submarines,
|
|
410
|
+
confirm_removals=None if args.force else _confirm_submarine_removals,
|
|
411
|
+
confirm_mod_root=confirm_mod_root,
|
|
412
|
+
confirm_git_repo=confirm_git_repo,
|
|
413
|
+
)
|
|
414
|
+
except InitializeError as exc:
|
|
415
|
+
print(format_message(str(exc), label="error"), file=sys.stderr)
|
|
416
|
+
return 1
|
|
417
|
+
except OSError as exc:
|
|
418
|
+
print(
|
|
419
|
+
format_message(t("err.init", error=exc), label="error"),
|
|
420
|
+
file=sys.stderr,
|
|
421
|
+
)
|
|
422
|
+
return 1
|
|
423
|
+
if selected_language is not None:
|
|
424
|
+
print(format_message(t("init.language_set"), color="green"))
|
|
425
|
+
if not result.current_git_repository:
|
|
426
|
+
if result.git_repository_path:
|
|
427
|
+
print(
|
|
428
|
+
format_message(
|
|
429
|
+
t(
|
|
430
|
+
"warning.current_not_repo",
|
|
431
|
+
path=result.git_repository_path,
|
|
432
|
+
),
|
|
433
|
+
label="warning",
|
|
434
|
+
),
|
|
435
|
+
file=sys.stderr,
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
print(
|
|
439
|
+
format_message(t("warning.no_repo"), label="warning"),
|
|
440
|
+
file=sys.stderr,
|
|
441
|
+
)
|
|
442
|
+
if result.upper_git_repository_path is not None:
|
|
443
|
+
print(
|
|
444
|
+
format_message(
|
|
445
|
+
t(
|
|
446
|
+
"warning.upper_repo",
|
|
447
|
+
path=result.upper_git_repository_path,
|
|
448
|
+
),
|
|
449
|
+
label="warning",
|
|
450
|
+
),
|
|
451
|
+
file=sys.stderr,
|
|
452
|
+
)
|
|
453
|
+
patterns: str = ", ".join(result.added)
|
|
454
|
+
all_patterns: str = ", ".join(IGNORE_PATTERNS)
|
|
455
|
+
if result.gitignore_created:
|
|
456
|
+
print(
|
|
457
|
+
t(
|
|
458
|
+
"init.created_gitignore",
|
|
459
|
+
name=result.gitignore_path.name,
|
|
460
|
+
patterns=patterns,
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
elif result.added:
|
|
464
|
+
print(
|
|
465
|
+
t(
|
|
466
|
+
"init.updated_gitignore",
|
|
467
|
+
name=result.gitignore_path.name,
|
|
468
|
+
patterns=patterns,
|
|
469
|
+
)
|
|
470
|
+
)
|
|
471
|
+
else:
|
|
472
|
+
print(
|
|
473
|
+
t(
|
|
474
|
+
"init.gitignore_ok",
|
|
475
|
+
name=result.gitignore_path.name,
|
|
476
|
+
patterns=all_patterns,
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
if result.config_created:
|
|
480
|
+
print(t("init.config_created", name=result.config_path.name))
|
|
481
|
+
elif result.config_updated:
|
|
482
|
+
print(t("init.config_updated", name=result.config_path.name))
|
|
483
|
+
else:
|
|
484
|
+
print(t("init.config_exists", name=result.config_path.name))
|
|
485
|
+
if result.submarines_updated and result.submarines:
|
|
486
|
+
print(t("init.registered", count=len(result.submarines)))
|
|
487
|
+
elif not result.submarines_updated:
|
|
488
|
+
print(t("init.submarines_skipped"))
|
|
489
|
+
if result.git_repository_path:
|
|
490
|
+
print(t("status.git_repo", path=result.git_repository_path))
|
|
491
|
+
info = load_workspace_info()
|
|
492
|
+
if info is not None:
|
|
493
|
+
print_submarine_paths(info)
|
|
494
|
+
return 0
|
|
495
|
+
|
|
496
|
+
if args.command == "unpack" and args.stdout and len(args.inputs) > 1:
|
|
497
|
+
print(
|
|
498
|
+
format_message(t("err.stdout_single"), label="error"),
|
|
499
|
+
file=sys.stderr,
|
|
500
|
+
)
|
|
501
|
+
return 2
|
|
502
|
+
|
|
503
|
+
if (
|
|
504
|
+
args.command == "pack"
|
|
505
|
+
and args.output is not None
|
|
506
|
+
and len(args.inputs) > 1
|
|
507
|
+
and not args.output.is_dir()
|
|
508
|
+
):
|
|
509
|
+
print(
|
|
510
|
+
format_message(t("err.output_multi"), label="error"),
|
|
511
|
+
file=sys.stderr,
|
|
512
|
+
)
|
|
513
|
+
return 2
|
|
514
|
+
|
|
515
|
+
registry: dict[Path, _SubmarineEntry] | None = None
|
|
516
|
+
if args.command in {"export", "pack"}:
|
|
517
|
+
try:
|
|
518
|
+
registry = _load_registry()
|
|
519
|
+
except SubFileError as exc:
|
|
520
|
+
print(format_message(str(exc), label="error"), file=sys.stderr)
|
|
521
|
+
return 1
|
|
522
|
+
|
|
523
|
+
auto_export: bool = False
|
|
524
|
+
if args.command == "export" and not args.inputs:
|
|
525
|
+
try:
|
|
526
|
+
info = load_workspace_info()
|
|
527
|
+
except SubFileError as exc:
|
|
528
|
+
print(format_message(str(exc), label="error"), file=sys.stderr)
|
|
529
|
+
return 1
|
|
530
|
+
if info is None:
|
|
531
|
+
print(
|
|
532
|
+
format_message(t("repl.workspace_missing"), label="error"),
|
|
533
|
+
file=sys.stderr,
|
|
534
|
+
)
|
|
535
|
+
return 1
|
|
536
|
+
repository_root: Path = (Path.cwd() / (info.git_repo or ".")).resolve()
|
|
537
|
+
modified = _detect_submarines(
|
|
538
|
+
read_config_root(),
|
|
539
|
+
repository_root,
|
|
540
|
+
Path.cwd(),
|
|
541
|
+
)[2]
|
|
542
|
+
registered = {entry.path: entry for entry in info.submarines}
|
|
543
|
+
targets = tuple(
|
|
544
|
+
entry for entry in modified if registered[entry.path].hash is not None
|
|
545
|
+
)
|
|
546
|
+
if not targets:
|
|
547
|
+
print(t("status.no_changed_submarines"))
|
|
548
|
+
return 0
|
|
549
|
+
print(t("status.export_changed_header"))
|
|
550
|
+
for entry in targets:
|
|
551
|
+
print(f" {entry.name}: {entry.path}")
|
|
552
|
+
if not args.force:
|
|
553
|
+
try:
|
|
554
|
+
answer: str = input(t("prompt.export_changed"))
|
|
555
|
+
except EOFError:
|
|
556
|
+
print()
|
|
557
|
+
return 0
|
|
558
|
+
if answer.strip().casefold() != "y":
|
|
559
|
+
print(t("status.export_declined"))
|
|
560
|
+
return 0
|
|
561
|
+
args.inputs = [Path.cwd() / entry.path for entry in targets]
|
|
562
|
+
auto_export = True
|
|
563
|
+
|
|
564
|
+
exported: list[Path] = []
|
|
565
|
+
errors: int = 0
|
|
566
|
+
for src in args.inputs:
|
|
567
|
+
try:
|
|
568
|
+
if args.command == "unpack":
|
|
569
|
+
unpack_sub(
|
|
570
|
+
src,
|
|
571
|
+
args.output_dir,
|
|
572
|
+
encoding=args.encoding,
|
|
573
|
+
force=args.force,
|
|
574
|
+
to_stdout=args.stdout,
|
|
575
|
+
validate=not args.no_validate,
|
|
576
|
+
)
|
|
577
|
+
elif args.command == "export":
|
|
578
|
+
_require_registered(src, registry)
|
|
579
|
+
force: bool = args.force or auto_export
|
|
580
|
+
target_dir: Path = (
|
|
581
|
+
args.output_dir if args.output_dir is not None else src.parent
|
|
582
|
+
)
|
|
583
|
+
xml_path: Path = target_dir / f"{src.stem}.xml"
|
|
584
|
+
if not force and xml_path.exists():
|
|
585
|
+
if not _confirm_overwrite(xml_path):
|
|
586
|
+
print(
|
|
587
|
+
t(
|
|
588
|
+
"status.skipped",
|
|
589
|
+
path=_relative_display(xml_path),
|
|
590
|
+
)
|
|
591
|
+
)
|
|
592
|
+
continue
|
|
593
|
+
force = True
|
|
594
|
+
result = export_sub(
|
|
595
|
+
src,
|
|
596
|
+
args.output_dir,
|
|
597
|
+
force=force,
|
|
598
|
+
sort_attributes=args.sort_attributes,
|
|
599
|
+
)
|
|
600
|
+
exported.append(src)
|
|
601
|
+
if result.preview_path is not None:
|
|
602
|
+
print(
|
|
603
|
+
t(
|
|
604
|
+
"status.export_preview",
|
|
605
|
+
src=_relative_display(src),
|
|
606
|
+
xml=_relative_display(result.xml_path),
|
|
607
|
+
preview=_relative_display(result.preview_path),
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
else:
|
|
611
|
+
print(
|
|
612
|
+
t(
|
|
613
|
+
"status.export_no_preview",
|
|
614
|
+
src=_relative_display(src),
|
|
615
|
+
xml=_relative_display(result.xml_path),
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
else:
|
|
619
|
+
entry: _SubmarineEntry | None = _require_registered(src, registry)
|
|
620
|
+
preview_override: bytes | None = _registry_preview_override(entry)
|
|
621
|
+
dst = pack_sub(
|
|
622
|
+
src,
|
|
623
|
+
args.output,
|
|
624
|
+
preview_override=preview_override,
|
|
625
|
+
sort_attributes=args.sort_attributes,
|
|
626
|
+
)
|
|
627
|
+
print(
|
|
628
|
+
t(
|
|
629
|
+
"status.pack",
|
|
630
|
+
src=_relative_display(src),
|
|
631
|
+
dst=_relative_display(dst),
|
|
632
|
+
)
|
|
633
|
+
)
|
|
634
|
+
except SubFileError as exc:
|
|
635
|
+
print(format_message(str(exc), label="error"), file=sys.stderr)
|
|
636
|
+
errors += 1
|
|
637
|
+
|
|
638
|
+
if exported and not errors and registry is not None:
|
|
639
|
+
try:
|
|
640
|
+
info = load_workspace_info()
|
|
641
|
+
if info is not None:
|
|
642
|
+
repository_root = (Path.cwd() / (info.git_repo or ".")).resolve()
|
|
643
|
+
scanned, _, _, _ = _detect_submarines(
|
|
644
|
+
read_config_root(),
|
|
645
|
+
repository_root,
|
|
646
|
+
Path.cwd(),
|
|
647
|
+
)
|
|
648
|
+
exported_paths: set[Path] = {path.resolve() for path in exported}
|
|
649
|
+
refresh_submarine_hashes(
|
|
650
|
+
tuple(
|
|
651
|
+
entry
|
|
652
|
+
for entry in scanned
|
|
653
|
+
if (Path.cwd() / entry.path).resolve() in exported_paths
|
|
654
|
+
)
|
|
655
|
+
)
|
|
656
|
+
except SubFileError as exc:
|
|
657
|
+
print(format_message(str(exc), label="error"), file=sys.stderr)
|
|
658
|
+
return 1
|
|
659
|
+
|
|
660
|
+
return 1 if errors else 0
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def main(argv: list[str] | None = None) -> int:
|
|
664
|
+
"""Run the command-line interface.
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
argv: Optional argument list for testing. Defaults to sys.argv[1:].
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
Process exit code: 0 on success, 1 if any input failed, 2 on bad usage.
|
|
671
|
+
"""
|
|
672
|
+
set_language(resolve_language())
|
|
673
|
+
arguments: list[str] = sys.argv[1:] if argv is None else argv
|
|
674
|
+
if not arguments:
|
|
675
|
+
from git4subeditor.repl import run
|
|
676
|
+
|
|
677
|
+
return run()
|
|
678
|
+
args: argparse.Namespace = build_parser().parse_args(arguments)
|
|
679
|
+
return _run_command(args)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
if __name__ == "__main__":
|
|
683
|
+
raise SystemExit(main())
|